NULL deref in devfs_inode_to_vnode β vn_lock called on NULL vp when inode not found
Summary
devfs_core.c:971 vp=msg->mdv_ino.vp. :972 vn_lock(vp,LK_EXCLUSIVE|LK_RETRY) unconditional. When inode not found devfs_iterate_topology returns NULL :1374 msg->mdv_ino.vp=NULL. vn_lock(NULL) calls lockmgr(&vp->v_lock) at vfs_vnops.c:1080 writes address ~0 = panic. Callers devfs_vfs_fhtovp :216-217 devfs_vfs_vget :251-252 check if(vp==NULL) return ENOENT AFTER call but panic occurs inside before return. Trigger: root fhopen/fhstat/fhstatfs with crafted fhandle containing non-existent fid_ino. Or NFS export of devfs + remote non-existent inode handle. Fix: if(vp!=NULL) vn_lock(vp,...).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0773 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | trigger-source | PoC: getfh(/dev/null) + mutate fid_ino + fhstat -> vn_lock(NULL) | 4.6 KB | view raw |
| build.sh | build-script | cc -o trigger trigger.c | 152 B | view raw |
| run.sh | run-script | ./trigger (must be root) | 245 B | view raw |
| fix.diff | suggested-fix | NULL guard before vn_lock at devfs_core.c:972 | 634 B | view raw |
| run.log | run-log | baseline #0 run output + panic signature | 2.1 KB | view raw |
| panic.txt | panic-signature | Fatal trap 12 VA=0x0 lockmgr_exclusive+0x5d from boot.log | 740 B | view raw |
| boot_baseline.txt | boot-log | boot.log before the panic run | 12.9 KB | view raw |
| fix_build.log | build-log | full single-fix kernel nativekernel build (rc=0) | 5.6 MB | β download |
| fix_run.log | run-log | patched #1 kernel run: fhstat returns ENOENT, no panic | 1.3 KB | view raw |
| env.txt | environment | uname, cc, sysctl, devfs mount | 383 B | view raw |
| VERDICT.md | verdict | full root-cause + fix-validation narrative | 6.3 KB | β raw |
| README.md | readme | build/run/expected + how it works | 1.3 KB | β raw |
| manifest.json | manifest | this catalog | 3.3 KB | view raw |
| build.log | build-log | kernel build log excerpt proving -Werror clean compile of patched source | 84 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0773 β NULL deref in devfs_inode_to_vnode (vn_lock on NULL vp)
Medium severity. Local DoS requiring root (SYSCAP_RESTRICTEDROOT).
Build
cc -o trigger trigger.c
Run (must be root)
./trigger
Expected behavior
Vulnerable kernel (6.5-DEVELOPMENT #0):
kernel panic. The process never returns from fhstat; ssh is torn down.
Serial console (dfbsd-qemu/boot.log) shows:
Fatal trap 12: page fault while in kernel mode fault virtual address = 0x0 Stopped at lockmgr_exclusive+0x5d: orl (%r14),%esi db>
Fixed kernel: fhstat returns -1/ENOENT; the binary prints
fhstat returned rc=-1 errno=2 (No such file or directory) and exits 1;
guest stays up.
How it works
getfh("/dev/null")β valid devfs fhandle (rightfh_fsid+fid_gen=boottime.tv_sec).- Mutate
fid_inoinsidefh.fh_fidto an inode no devfs node has. fhstat(&fh)βVFS_FHTOVP(devfs)βdevfs_vfs_fhtovpβdevfs_inode_to_vnodeβdevfs_iterate_topologyreturns NULL (no node matches) βmsg->mdv_ino.vp = NULLβ unconditionalvn_lock(NULL)atdevfs_core.c:972βlockmgr(&vp->v_lock)page-faults on VA 0x0 β panic.
See VERDICT.md for the full root-cause trace and fix.diff for the patch.
DF-0773 β NULL deref in devfs_inode_to_vnode (vn_lock on NULL vp)
Verdict: REPRODUCED β FIX VALIDATED Severity: Medium (CVSS AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H) β local DoS, requires root
The bug
devfs_inode_to_vnode() (sys/vfs/devfs/devfs_core.c:958-976) calls
vn_lock(vp, ...) unconditionally on the vnode pointer it just received
from the devfs core message dispatcher:
958: struct vnode *
959: devfs_inode_to_vnode(struct mount *mp, ino_t target)
960: {
961: struct vnode *vp = NULL;
...
970: devfs_msg_send_sync(DEVFS_INODE_TO_VNODE, msg);
971: vp = msg->mdv_ino.vp;
972: vn_lock(vp, LK_EXCLUSIVE | LK_RETRY); /* <-- no NULL check */
...
975: return vp;
976: }
The dispatcher (devfs_core.c:1373-1377) sets msg->mdv_ino.vp =
devfs_iterate_topology(...). devfs_iterate_topology (devfs_core.c:631-649)
returns NULL when no node matches the target inode, and the per-node callback
devfs_inode_to_vnode_worker_callback (devfs_core.c:2197-2214) returns NULL
for any node whose d_dir.d_ino != target. So whenever target names no
devfs node, vp is NULL and line 972 dereferences it.
vn_lock() β vk_lock(vp,...) β lockmgr(&vp->v_lock,...) with vp==0x0
page-faults at &vp->v_lock β 0 β Fatal trap 12, fault VA=0x0,
lockmgr_exclusive+0x5d: orl (%r14),%esi β DDB, kernel dead.
Why callers don't save it
devfs_vfs_fhtovp (devfs_vfsops.c:202-221) and devfs_vfs_vget
(devfs_vfsops.c:244-256) do check if (vp == NULL) return ENOENT; β
but after the call, at lines 216-217 / 251-252. The panic happens inside
devfs_inode_to_vnode at line 972 before the caller ever sees the return
value, so the NULL check in the callers is dead code for the not-found case.
Reachability / threat model
VFS_FHTOVP is reached through sys_fhopen (syscall 298),
sys_fhstat (478), sys_fhstatfs (297), sys_fhstatvfs (502). All four
are gated by caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT) β i.e. they
require root (sys/kern/vfs_syscalls.c:4832, 5013, 5063). This matches the
CVSS PR:H and the Medium severity: a root user can panic the kernel by
handing any of those syscalls a file handle whose fid_ino names no devfs
node. vfs_vget on devfs is otherwise only driven through NFS export
(setup also requires root), so the realistic ceiling is local rootβkernel
DoS β no unprivileged path was identified, and the primitive is a fixed
NULL deref (no escalation possible).
Reproduction (unpatched #0 kernel)
trigger.c runs as root:
getfh("/dev/null", &fh)β obtains a valid devfs file handle (correctfh_fsid+ correctfid_gen == boottime.tv_sec, so it passes thedevfs_vfs_fhtovp:211gen check and reaches line 214).- Mutates
((struct devfs_fid *)&fh.fh_fid)->fid_inoto0xDEADBEEFCAFEBABEβ an inode no devfs node has, sodevfs_iterate_topologyreturns NULL. - Calls
fhstat(&fh, &sb)βVFS_FHTOVP(devfs)βdevfs_vfs_fhtovpβdevfs_inode_to_vnodeβvn_lock(NULL)β panic.
Observed on 6.5-DEVELOPMENT #0 (2026-07-02 build):
[+] getfh("/dev/null") OK
[+] original devfs_fid: len=16 pad=0 gen=1783630069 ino=175
[+] mutated devfs_fid: len=16 pad=0 gen=1783630069 ino=16045690984503098046
[*] calling fhstat() with bogus devfs fid_ino -> expect panic...
(hang β ssh torn down, run_root times out)
== dfbsd-qemu/boot.log ==
Fatal user address access from kernel mode from trigger at ffffffff80647bad
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x0
instruction pointer = 0x8:0xffffffff80647bad
Stopped at lockmgr_exclusive+0x5d: orl (%r14),%esi
db>
The panic signature is exactly the predicted vn_lock(NULL) β
lockmgr(&vp->v_lock) NULL deref.
Escalation
None. A NULL deref at a fixed kernel offset is a pure DoS β there is no controllable corruption, no primitive to groom. This is a hard blocker for escalation (read: there is nothing to escalate). Realistic impact ceiling: local rootβkernel panic / DoS.
Fix
fix.diff adds the missing NULL guard at the actual fault site
(devfs_core.c:972). Minimal, targeted at the root cause; both VFS callers
already translate vp==NULL into ENOENT:
@@ -969,7 +969,14 @@
msg->mdv_ino.ino = target;
devfs_msg_send_sync(DEVFS_INODE_TO_VNODE, msg);
vp = msg->mdv_ino.vp;
- vn_lock(vp, LK_EXCLUSIVE | LK_RETRY);
+ /*
+ * The dispatcher may set mdv_ino.vp to NULL when no devfs node
+ * matches `target` (devfs_iterate_topology returns NULL).
+ * Guard vn_lock to avoid a NULL-deref panic; callers already
+ * translate vp==NULL into ENOENT.
+ */
+ if (vp != NULL)
+ vn_lock(vp, LK_EXCLUSIVE | LK_RETRY);
devfs_msg_put(msg);
This matches the finding's ## Recommended fix (if(vp!=NULL)
vn_lock(vp,...)) and adds an explanatory comment.
Fix validation (Phase 8)
Built a single-fix kernel from the with-src baseline + this one diff
(make -j6 nativekernel KERNCONF=X86_64_GENERIC, warm obj, ~5 min, rc=0),
installed kernel.stripped β /boot/kernel/kernel + kernel.debug, rebooted
into 6.5-DEVELOPMENT #1 root@dfbsd:/usr/obj/... 2026-07-09 21:10:29,
re-ran the identical PoC:
BEFORE (unpatched #0): panic Fatal trap 12 fault VA=0x0
lockmgr_exclusive+0x5d, guest dead.
AFTER (single-fix #1):
[+] mutated devfs_fid: len=16 pad=0 gen=1783631817 ino=16045690984503098046 [*] calling fhstat() with bogus devfs fid_ino -> expect panic... [-] fhstat returned rc=-1 errno=2 (No such file or directory) β NOT vulnerable? TRIGGER_RC=1
fhstat now returns ENOENT (the post-call NULL check at
devfs_vfs_fhtovp:216-217 finally fires), no panic, guest stays up. Re-ran
twice for determinism β identical. fix_status: fixed.
Files
| file | role |
|---|---|
trigger.c |
minimal PoC: getfh + mutate fid_ino + fhstat |
build.sh / run.sh |
exact build/run commands |
fix.diff |
git-apply-able fix (NULL guard at devfs_core.c:972) |
build.log |
(none separately β compiled in one shot; see fix_build.log) |
run.log |
full baseline reproduction output + panic signature |
panic.txt |
panic excerpt from boot.log |
boot_baseline.txt |
boot.log before the panic run |
fix_build.log |
full single-fix kernel build output (rc=0) |
fix_run.log |
full patched-kernel run output (ENOENT, no panic) |
env.txt |
guest environment |
manifest.json |
machine-readable catalog |
Fix verification
fixedVALIDATED. Applied only this finding's fix.diff to the with-src baseline (patch -p1 hunk #1 succeeded at 969), built a single-fix kernel (make -j6 nativekernel KERNCONF=X86_64_GENERIC, warm obj, rc=0), installed kernel.stripped->/boot/kernel/kernel + kernel.debug, rebooted into 6.5-DEVELOPMENT #1 (build ts 2026-07-09 21:10:29). The IDENTICAL PoC that panicked the unpatched #0 kernel (Fatal trap 12 VA=0x0 lockmgr_exclusive+0x5d, guest dead) now returns fhstat rc=-1 errno=2 (ENOENT) on the patched #1 kernel, the binary exits 1, no panic in boot.log, guest stays up. Re-ran twice for determinism -- identical ENOENT both times. The fix closes the bug: the post-call NULL check in devfs_vfs_fhtovp (devfs_vfsops.c:216-217) finally fires because devfs_inode_to_vnode no longer dereferences NULL before returning it.
baseline #0: '[*] calling fhstat()... (hang)' + boot.log 'Fatal trap 12 page fault VA=0x0 / Stopped at lockmgr_exclusive+0x5d: orl (%r14),%esi / db>' + vm.sh status=down. patched #1: '[*] calling fhstat()... [-] fhstat returned rc=-1 errno=2 (No such file or directory) -- NOT vulnerable? TRIGGER_RC=1' + vm.sh status=up + boot.log clean (login: prompt, no panic).
Confirmed kernel references
- sys/vfs/devfs/devfs_core.c:958
- sys/vfs/devfs/devfs_core.c:971
- sys/vfs/devfs/devfs_core.c:972
- sys/vfs/devfs/devfs_core.c:1373
- sys/vfs/devfs/devfs_core.c:631
- sys/vfs/devfs/devfs_core.c:2197
- sys/vfs/devfs/devfs_vfsops.c:202
- sys/vfs/devfs/devfs_vfsops.c:214
- sys/vfs/devfs/devfs_vfsops.c:216
- sys/vfs/devfs/devfs_vfsops.c:244
- sys/vfs/devfs/devfs_vfsops.c:249
- sys/kern/vfs_syscalls.c:4814
- sys/kern/vfs_syscalls.c:4832
- sys/kern/vfs_syscalls.c:5001
- sys/kern/vfs_syscalls.c:5013
Detail
Exploit chain
none -- this is a fixed-offset NULL deref (vn_lock(NULL) -> lockmgr derefs vp->v_lock at ~VA 0x0), a pure local DoS with no controllable corruption and therefore no escalation chain (valid hard blocker: the primitive is read/deref-only with no write, no groomable object). Realistic impact ceiling: local root->kernel panic. Reachability is root-only: VFS_FHTOVP is reached only via fhopen/fhstat/fhstatfs/fhstatvfs, all gated by caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT) at sys/kern/vfs_syscalls.c:4832/5013/5063, matching the CVSS PR:H and Medium severity. No unprivileged path to devfs_vfs_vget was identified (vfs_vget on synthetic devfs is otherwise only driven through NFS export, which also requires root to set up).
Evidence (decisive lines)
BEFORE (unpatched #0): PoC ran getfh(/dev/null) -> mutated devfs_fid ino=16045690984503098046 -> fhstat -> run never returned, ssh torn down. dfbsd-qemu/boot.log: 'Fatal user address access from kernel mode from trigger at ffffffff80647bad / Fatal trap 12: page fault while in kernel mode / fault virtual address = 0x0 / Stopped at lockmgr_exclusive+0x5d: orl (%r14),%esi / db>'. vm.sh status after run: down. AFTER (single-fix #1 kernel): identical PoC -> '[*] calling fhstat()... [-] fhstat returned rc=-1 errno=2 (No such file or directory) -- NOT vulnerable? TRIGGER_RC=1'; guest stays up; re-ran twice, identical ENOENT each time.
PoC changes
Authored the entire evidence pack from scratch (no markdown/PoC folder existed -- only the DB row). Wrote trigger.c: as root, getfh(/dev/null) for a valid devfs fhandle (correct fh_fsid + fid_gen=boottime.tv_sec to pass the devfs_vfs_fhtovp:211 gen check), then mutate the in-place struct devfs_fid.fid_ino to 0xDEADBEEFCAFEBABE (no node matches) and call fhstat() to drive VFS_FHTOVP->devfs_vfs_fhtovp->devfs_inode_to_vnode->vn_lock(NULL). build.sh/run.sh wrap cc and the root invocation. VERDICT.md, README.md, manifest.json, fix.diff (NULL guard at devfs_core.c:972), and full logs (run.log, panic.txt, fix_build.log, fix_run.log, env.txt, boot_baseline.txt) all written.
Verified recommended fix
At sys/vfs/devfs/devfs_core.c:972 in devfs_inode_to_vnode(), guard the vn_lock with 'if (vp != NULL)' before dereferencing -- when devfs_iterate_topology returns NULL (no node matches target), return NULL and let the existing caller checks (devfs_vfs_fhtovp:216-217, devfs_vfs_vget:251-252) translate that into ENOENT. The full git-apply-able diff is in findings/poc/DF-0773/fix.diff; it matches the finding markdown's '## Recommended fix' proposal (if(vp!=NULL) vn_lock(vp,...)) and adds an explanatory comment.
Verdict
REPRODUCED. devfs_inode_to_vnode() (sys/vfs/devfs/devfs_core.c:972) calls vn_lock(vp, LK_EXCLUSIVE|LK_RETRY) unconditionally on vp, which comes from msg->mdv_ino.vp at line 971. The dispatcher at devfs_core.c:1373-1377 sets that field to devfs_iterate_topology(...), which returns NULL whenever no devfs node matches target (worker callback devfs_inode_to_vnode_worker_callback:2197-2214 returns NULL for any node whose d_dir.d_ino != target). So an attacker-supplied inode that names no devfs node yields vp==NULL and vn_lock(NULL)->lockmgr(&vp->v_lock) page-faults at VA 0x0. Confirmed on the unpatched #0 kernel: the PoC (root: getfh(/dev/null), mutate fid_ino to 0xDEADBEEFCAFEBABE, fhstat) panics with 'Fatal trap 12 fault VA=0x0, Stopped at lockmgr_exclusive+0x5d: orl (%r14),%esi' in dfbsd-qemu/boot.log, guest dies, ssh torn down. The post-call NULL checks in devfs_vfs_fhtovp (devfs_vfsops.c:216-217) and devfs_vfs_vget (:251-252) are dead code for the not-found case because the panic happens inside devfs_inode_to_vnode before the caller can inspect the return.
No comments yet.