β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0773

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0773 Β· 16 files
FileTypeDescriptionSize
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
README.md readme build/run/expected + how it works
↓ download 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

  1. getfh("/dev/null") β†’ valid devfs fhandle (right fh_fsid + fid_gen=boottime.tv_sec).
  2. Mutate fid_ino inside fh.fh_fid to an inode no devfs node has.
  3. fhstat(&fh) β†’ VFS_FHTOVP(devfs) β†’ devfs_vfs_fhtovp β†’ devfs_inode_to_vnode β†’ devfs_iterate_topology returns NULL (no node matches) β†’ msg->mdv_ino.vp = NULL β†’ unconditional vn_lock(NULL) at devfs_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.

VERDICT.md verdict full root-cause + fix-validation narrative
↓ download raw

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:

  1. getfh("/dev/null", &fh) β€” obtains a valid devfs file handle (correct fh_fsid + correct fid_gen == boottime.tv_sec, so it passes the devfs_vfs_fhtovp:211 gen check and reaches line 214).
  2. Mutates ((struct devfs_fid *)&fh.fh_fid)->fid_ino to 0xDEADBEEFCAFEBABE β€” an inode no devfs node has, so devfs_iterate_topology returns NULL.
  3. 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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. 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).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Thu Jul 9 21:10:29 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC (sha256 /boot/kernel/kernel = 86894691e839673ba6a839698e934df74d0bf6f5b69dd81e78304f1e716445d0)

Confirmed kernel references

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.