devfs_inode_to_vnode() executes vn_lock() on the core worker's NULL result before the caller's NULL check can run β deterministic NULL-deref kernel panic from a crafted-but-root-gated devfs file handle (fhopen/fhstat/fhstatfs), and unauthenticated-remote if a devfs is exported over NFS (VFS_VGET wire path)
Summary
devfs_vfs_fhtovp validates only fid_gen against boottime.tv_sec and passes the caller-controlled fid_ino to devfs_inode_to_vnode(), which sends a sync DEVFS_INODE_TO_VNODE message; the core assigns msg->mdv_ino.vp = devfs_iterate_topology(...), NULL when no node matches. devfs_inode_to_vnode then runs vn_lock(vp, LK_EXCLUSIVE|LK_RETRY) on that NULL at devfs_core.c:971-972 BEFORE returning it, so the callers' vp==NULL checks (:216-217 fhtovp, :251-252 vget) are unreachable on this path. vn_lock(NULL) -> lockmgr_exclusive reads NULL->lk_flags: Fatal trap 12 fault VA 0x0. VERIFIED 1/1 as root: getfh('/dev/null') + overwrite fid_ino=0x41414141 + fhopen(2) -> 'Stopped at lockmgr_exclusive+0x5d', guest down; unpriv control dies EPERM before reaching devfs (SYSCAP_RESTRICTEDROOT gate). devfs_vfs_vget feeds the same helper with wire-supplied dp->d_ino via the kernel NFS server - unauthenticated remote panic if an admin exports a devfs over NFS. Fix validated on rebuilt kernel (NULL guard in devfs_core.c): same PoC returns ENOENT, guest up. Panic-only primitive behind a root gate; no uid0 route.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3236 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.1 KB | β raw | |
| VERDICT.md | β | 4.3 KB | β raw | |
| fhdevfs.c | β | 2.2 KB | view raw | |
| build.sh | β | 107 B | view raw | |
| run.sh | β | 101 B | view raw | |
| panic.txt | β | 2.1 KB | view raw | |
| run.log | β | 1.3 KB | view raw | |
| fix_build.log | β | 5.6 MB | β download | |
| fix_run.log | β | 241 B | view raw | |
| env.txt | β | 249 B | view raw | |
| fix.diff | β | 378 B | view raw | |
| verdict.json | β | 3.7 KB | view raw |
DF-3236 β devfs vn_lock(NULL) via unknown-ino file handle (fhopen)
What
devfs_vfs_fhtovp (sys/vfs/devfs/devfs_vfsops.c:202-221) and
devfs_vfs_vget (:244-256) pass a caller-controlled inode number to
devfs_inode_to_vnode() (sys/vfs/devfs/devfs_core.c:959-976). When the devfs
core worker finds no node with that ino, devfs_iterate_topology() returns
NULL (devfs_core.c:630-649) and the core assigns msg->mdv_ino.vp = NULL
(devfs_core.c:1373-1378). devfs_inode_to_vnode() then executes
vn_lock(vp, LK_EXCLUSIVE | LK_RETRY) on that NULL before returning it
(devfs_core.c:971-972) β the callers' own NULL checks
(devfs_vfsops.c:216-217, 251-252) are unreachable. vn_lock(NULL) β
VOP_LOCK β lockmgr_exclusive dereferences NULL: fatal trap 12.
Reachability / privilege
- Local:
fhopen(2)/fhstat(2)/fhstatfs(2)/fhgetare all gated bySYSCAP_RESTRICTEDROOT(kern/vfs_syscalls.c:4782, 4832, 5013, ...) β root required locally (verified: unprivileged run returns EPERM and never reaches devfs). - Remote: the kernel NFS server calls
VFS_VGET(mp, vp, dp->d_ino, &nvp)with wire-supplied directory inode numbers (nfs_serv.c:3417, 3454) β an exported devfs would turn this into an unauthenticated remote panic, but exporting devfs requires explicit admin action.
Files
fhdevfs.cβ PoC: takes a real devfs file handle viagetfh("/dev/null"), swapsfid_inoto 0x41414141, callsfhopen(2).build.sh/run.shβ exact build/run commands.panic.txtβ serial console capture of the baseline panic.fix.diffβ one-line NULL guard indevfs_inode_to_vnode().VERDICT.md/manifest.json/verdict.jsonβ full verdict + fix validation results.
Reproduce
./build.sh # cc -O -o fhdevfs fhdevfs.c (in-guest) ./run.sh # unprivileged: EPERM (gate works) ; root: kernel panic
Expected baseline (root): Fatal trap 12: page fault while in kernel mode,
fault virtual address = 0x0, Stopped at lockmgr_exclusive+0x5d,
current process = fhdevfs. Guest dies β vm.sh status β down.
Expected after fix: survived: fd=-1 errno=2 (No such file or directory),
guest stays up.
DF-3236 VERDICT β devfs vn_lock(NULL) on unknown-ino file handle
Bottom line
REPRODUCED (baseline) / FIXED (patched kernel). A crafted file handle
whose fid_gen passes devfs's boottime check but whose fid_ino matches no
devfs node drives devfs_inode_to_vnode() into vn_lock(NULL) β
lockmgr_exclusive NULL-deref β fatal trap 12, guest down. One-line guard
fix validated on a rebuilt kernel: same PoC now returns ENOENT, guest stays
up.
Root cause chain (all line-verified)
sys_fhopen(kern/vfs_syscalls.c:4814) β root-gated (SYSCAP_RESTRICTEDROOT, :4832) β copies a 28-bytefhandle_tfrom userland and callsVFS_FHTOVP(:4856).devfs_vfs_fhtovp(sys/vfs/devfs/devfs_vfsops.c:203-221) checks onlyfid_gen != boottime.tv_sec(:211-212) β no fid_len / ino validation β then callsdevfs_inode_to_vnode(mp, fid_ino)(:214).devfs_inode_to_vnode(sys/vfs/devfs/devfs_core.c:959-976) sends a syncDEVFS_INODE_TO_VNODEmessage to the devfs core.- Core handler (devfs_core.c:1373-1378):
msg->mdv_ino.vp = devfs_iterate_topology(root, cb, &ino). On no match the walker returns NULL (devfs_core.c:630-649 βret = callback(node, arg1)with the inode_to_vnode callback returning NULL for non-matching nodes, devfs_core.c:2196-2214). - The bug:
devfs_inode_to_vnodeexecutesvn_lock(vp, LK_EXCLUSIVE | LK_RETRY)on that NULL before returning it (devfs_core.c:971-972). The callers' NULL checks (devfs_vfsops.c:216-217 for fhtovp, :251-252 for vget) are unreachable. vn_lock(NULL)β VOP_LOCK βlockmgr_exclusive+0x5dreadsNULL->lk_flags:Fatal trap 12, fault VA 0x0.
devfs_vfs_vget (devfs_vfsops.c:244-256) feeds the same broken helper with
ino from VFS_VGET; VFS_VGET is only reached from the kernel NFS server's
wire-driven directory walk (nfs_serv.c:3417, 3454) β i.e. an exported devfs
would expose this to unauthenticated remote clients (admin action required
to export, so not counted as a default remote exposure).
Privilege boundary β honestly scoped
- Unprivileged local users CANNOT reach this: every local fh* syscall
(
getfh,fhopen,fhstat,fhstatfs,fhstatvfs) is gated bySYSCAP_RESTRICTEDROOT. Verified live: unprivileged run returns EPERM before touching devfs. Root trigger β Low severity (robustness bug / privileged local DoS, plus latent remote exposure only for an NFS-exported devfs). - No escalation path: the fault is a read of
NULL+constin lockmgr β deterministic panic, nothing controlled.
Verification log
Baseline (stock INVARIANTS kernel #0, Jul 2 2026)
- Unprivileged control:
EPERM(gate works) β never reaches devfs. - Root run: guest died mid-command (ssh hung);
serial console (
panic.txt):Fatal user address access from kernel mode from fhdevfs,Fatal trap 12: page fault while in kernel mode,fault virtual address = 0x0,Stopped at lockmgr_exclusive+0x5d: orl (%r14),%esi,current process = 922(fhdevfs),db>prompt.vm.sh statusβ down. - PoC detail: takes a real devfs fh via
getfh("/dev/null")(byte-exact fsid+gen), swaps onlyfid_inoto 0x41414141. First attempt using sysctl-derivedkern.boottimefailed the gen check (EINVAL) β the sysctl's tv_sec does not byte-match the kernelboottimeglobal; using the real fh fixed it.
Patched kernel (fix.diff: NULL guard before vn_lock, devfs_core.c:972)
make nativekernel+make installkernelin guest (fix_build.log, RC=0).- New kernel
#1: Wed Sep 9 03:33:19 UTC 2026. - Same PoC, root:
survived: fd=-1 errno=2 (No such file or directory)βdevfs_vfs_fhtovp's existingvp == NULL β ENOENTbranch (devfs_vfsops.c:216-217) is now reachable. Guest stayed up (fix_run.log, uptime confirmations).
Impact classification
panic (kernel NULL-deref DoS), reproduced=1, root-gated locally β
severity Low. No memory-corruption primitive (read at fixed NULL offset,
fully deterministic).
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedApplied fix.diff inside guest /usr/src, make nativekernel + installkernel (fix_build.log RC=0), rebooted into kernel #1; exact same PoC under root now returns ENOENT (devfs_vfs_fhtovp:216-217 NULL branch reachable) with the guest staying up - panic gone.
fix_run.log (survived errno=2 + uptime); fix_build.log (kernel build completed 03:36:53, install completed 03:45:37); env.txt (patched uname)
Confirmed kernel references
Detail
Exploit chain
None: deterministic NULL-pointer read at a fixed lockmgr offset; panic-only. Privilege boundary held (unprivileged run = EPERM at the syscall gate).
Evidence (decisive lines)
panic.txt:40-59 (Fatal trap 12, fault VA 0x0, Stopped at lockmgr_exclusive+0x5d, current process = fhdevfs); fix_run.log (patched kernel: survived, errno=2 ENOENT, uptime alive); VERDICT.md (full line-cited chain); fhdevfs.c (getfh-based byte-exact handle, ino swap)
PoC changes
Seed-free (fresh PoC). Two iterations: (1) first version derived fid_gen from sysctl kern.boottime -> failed devfs_vfs_fhtovp's gen check with EINVAL (sysctl tv_sec does not byte-match the kernel boottime global); (2) final version takes a real devfs handle via getfh("/dev/null") and swaps only fid_ino to 0x41414141 - baseline panicked immediately.
Verified recommended fix
Guard vn_lock on the worker result in devfs_inode_to_vnode(): if (vp != NULL) vn_lock(vp, LK_EXCLUSIVE | LK_RETRY); (devfs_core.c:972) - validated on rebuilt kernel.
Verdict
REPRODUCED on stock INVARIANTS kernel: a root user calling fhopen(2) with a devfs file handle whose fid_gen matches boottime.tv_sec but whose fid_ino matches no devfs node drives devfs_vfs_fhtovp (devfs_vfsops.c:214) -> devfs_inode_to_vnode (devfs_core.c:959) into vn_lock(NULL) at devfs_core.c:972 because the core worker's NULL result is locked before the caller's NULL check (devfs_vfsops.c:216) can run -> lockmgr_exclusive+0x5d reads NULL+const -> Fatal trap 12, fault VA 0x0, guest down. Unprivileged users cannot reach it: every local fh* syscall is SYSCAP_RESTRICTEDROOT-gated (verified EPERM live); VFS_VGET exposure requires an NFS-exported devfs (admin action). One-line NULL guard fix validated on a rebuilt guest kernel: same PoC returns ENOENT and the guest stays up. Root-gated deterministic NULL-deref DoS, no corruption/escalation primitive -> Low.
No comments yet.