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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3236 Β· 12 files
FileTypeDescriptionSize
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)/fhget are all gated by SYSCAP_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 via getfh("/dev/null"), swaps fid_ino to 0x41414141, calls fhopen(2).
  • build.sh / run.sh β€” exact build/run commands.
  • panic.txt β€” serial console capture of the baseline panic.
  • fix.diff β€” one-line NULL guard in devfs_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.

VERDICT.md
↓ download raw

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)

  1. sys_fhopen (kern/vfs_syscalls.c:4814) β€” root-gated (SYSCAP_RESTRICTEDROOT, :4832) β€” copies a 28-byte fhandle_t from userland and calls VFS_FHTOVP (:4856).
  2. devfs_vfs_fhtovp (sys/vfs/devfs/devfs_vfsops.c:203-221) checks only fid_gen != boottime.tv_sec (:211-212) β€” no fid_len / ino validation β€” then calls devfs_inode_to_vnode(mp, fid_ino) (:214).
  3. devfs_inode_to_vnode (sys/vfs/devfs/devfs_core.c:959-976) sends a sync DEVFS_INODE_TO_VNODE message to the devfs core.
  4. 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).
  5. The bug: devfs_inode_to_vnode executes vn_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.
  6. vn_lock(NULL) β†’ VOP_LOCK β†’ lockmgr_exclusive+0x5d reads NULL->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 by SYSCAP_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+const in 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 only fid_ino to 0x41414141. First attempt using sysctl-derived kern.boottime failed the gen check (EINVAL) β€” the sysctl's tv_sec does not byte-match the kernel boottime global; using the real fh fixed it.

Patched kernel (fix.diff: NULL guard before vn_lock, devfs_core.c:972)

  • make nativekernel + make installkernel in 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 existing vp == NULL β†’ ENOENT branch (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

fixed
baseline reproduced→ patch + rebuild →patched clean

Applied 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)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Wed Sep 9 03:33:19 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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.