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

dirfs_root open-failure teardown: dirfs_free_vp NULL-pointer write (vp->v_data at NULL), latent double-free, and never-cleared dangling dmp->dm_root

Summary

On the first VFS_ROOT of a dirfs mount, dirfs_root stores the fresh node in dmp->dm_root (:250) and takes the life-of-mount ref (:246) before open(dmp->dm_path, O_DIRECTORY) (:258). If that host open() fails, the error path calls dirfs_free_vp(dmp, dnp) on a node whose dn_vnode is still NULL - dirfs_free_vp (dirfs_subr.c:298-307) loads vp=dn_vnode==NULL and executes vp->v_data = NULL, a kernel-mode write to 0x128 -> fatal trap. Behind it: dirfs_free_vp->dirfs_node_drop frees the node (ref 1->0) and the immediately following dirfs_node_free(:262) frees it again (double free), and dmp->dm_root keeps pointing at the freed node for the next VFS_ROOT (UAF). PROVEN on guest with a dirfs-enabled vkernel (custom VKDIRFS config; also fixed dirfs bit-rot in the guest scratch tree): swap host dir for dangling symlink, 'ls /hostroot' -> 'Fatal trap 12 ... fault virtual address = 0x128 ... Stopped at dirfs_free_vp+0x3b (movq $0,0x128(%r13))' - every process in the vkernel dies. Precondition met by permission changes, host-dir races, fd exhaustion. Fix validated: dm_root=NULL + single dirfs_node_drop -> 'ls: No such file or directory', vkernel survives. No uid0 route (constant-0 fixed-offset store, confined to vkernel process). NOTE: dirfs does not compile on this tree without include fixes (documented) - vkernel64-only.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3218 Β· 16 files
FileTypeDescriptionSize
run.baseline.log β€” 2.1 KB view raw
run.fixed.log β€” 1.7 KB view raw
run.driver-output.log β€” 1.3 KB view raw
run.driver-output-fixed.log β€” 1.3 KB view raw
run.sh β€” 215 B view raw
vkmount.c β€” 634 B view raw
vkinit.c β€” 249 B view raw
applyfix.sh β€” 1.1 KB view raw
fix.diff β€” 429 B view raw
build.log β€” 10.0 KB view raw
build.vkernel-full.log β€” 295.5 KB ↓ download
fix-build.log β€” 227 B view raw
env.txt β€” 279 B view raw
VERDICT.md β€” 4.2 KB ↓ raw
manifest.json β€” 1.2 KB view raw
verdict.json β€” 4.3 KB view raw
VERDICT.md
↓ download raw

DF-3218 VERDICT

status: reproduced / impact: panic (vkernel-wide DoS) / confidence: certain

Environment

  • QEMU guest: DragonFly 6.5-DEVELOPMENT #0 (native X86_64_GENERIC, 2026-07-02), 6 vCPUs, cc 8.3. dirfs is vkernel64-only, so the PoC runs a vkernel built inside the guest (nativekernel KERNCONF=VKDIRFS, VKERNEL64 + options DIRFS), booted on a 160MB FFS image with a minimal userland, vm.vkernel_enable=1.
  • Build prerequisites (bit-rot fixes, guest tree only β€” the audit repo sys/ is untouched): missing <sys/malloc.h> in dirfs_vnops.c and dirfs_subr.c, missing <sys/uio.h> in dirfs_vnops.c, and __build_id_* undefined at the vkernel link (script-less SYSTEM_LD) β€” without these dirfs does not compile/link at all on the current tree.

Chain (what was run)

  1. Guest: mkdir /dirfs-host (a real directory).
  2. vkernel boots to /bin/sh (via /sbin/vkinit wrapper) on a fifo console.
  3. Inside vkernel (root): /bin/vkmount /dirfs-host /hostroot β†’ mount("dirfs", "/hostroot", 0, "/dirfs-host") β†’ dirfs_mount: copyinstr OK, stat("/dirfs-host") S_ISDIR OK, mount succeeds. dmp->dm_root still NULL (no VFS_ROOT yet).
  4. Guest: mv /dirfs-host /dirfs-host.real && ln -s /nonexistent-xyz /dirfs-host β†’ the mount's host path is now a dangling symlink.
  5. Inside vkernel: ls /hostroot β†’ namei crosses the mountpoint β†’ VFS_ROOT β†’ dirfs_root β†’ dm_root==NULL β†’ lstat("/dirfs-host") SUCCEEDS (the symlink itself; AT_SYMLINK_NOFOLLOW) β†’ dmp->dm_root = dnp (:250), DIRFS_ROOT set β†’ open("/dirfs-host", O_DIRECTORY) FOLLOWS the symlink β†’ ENOENT β†’ fd==-1 β†’ dirfs_free_vp(dmp, dnp) (dirfs_vfsops.c:261) β†’ vp = dnp->dn_vnode == NULL (dirfs_subr.c:300) β†’ vp->v_data = NULL (dirfs_subr.c:305) β†’ kernel-mode write to 0x128.

Observed (baseline, run.baseline.log)

# ls /hostroot
Fatal trap 12: page fault while in kernel mode
fault virtual address      = 0x128
instruction pointer        = 0x2b:0x72080b
current process            = 3 (ls)
Stopped at 0x72080b:       movq $0,0x128(%r13)

Symbolization on the vkernel binary: - nm -n: dirfs_free_vp at 0x7207d0. - objdump -d: 7207fd: movq $0x0,0x48(%rbx) = dnp->dn_vnode = NULL; 72080b: movq $0x0,0x128(%r13) = vp->v_data = NULL (vp==NULL) β€” byte-identical to the faulting instruction; the next instruction is jmpq 7204d0 <dirfs_node_drop> (the path that would drive the double-free if the store did not fault).

0x128 is offsetof(struct vnode, v_data) in this build. The vkernel OS is dead at this point (ddb); every process in the vkernel dies with it.

Fix validation (run.fixed.log)

fix.diff applied to the guest tree, vkernel relinked, identical PoC re-run: - /bin/vkmount /dirfs-host /hostroot β†’ mounted - symlink swap β†’ ls /hostroot β†’ ls: /hostroot: No such file or directory (VFS_ROOT returns the host ENOENT cleanly), echo TRAVERSED runs, vkernel stays alive.

fix_status: fixed (baseline reproduced β†’ patched behavior correct).

Why not uid0

The bug is a NULL-pointer write of the constant 0 at a fixed offset β€” deterministic fatal trap, not a controllable corruption primitive; there is no attacker-influenced data in the store. dirfs runs only inside a vkernel (a userland process on the host), so even a controllable corruption would execute inside the vkernel process, whose "kernel memory" is the process's own memory owned by the same host uid. Realistic ceiling: whole-vkernel panic from a mount-traversal by any vkernel user β€” hence Medium, not higher.

Residual notes

  • The dm_root dangling (defect 3) and the double-free (defect 2) are latent behind the NULL-write in this exact build; the fix removes all three.
  • The dm_root lazy-init race (filed separately as DF-3219) is a different defect on the same lines.

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

Rebuilt the vkernel with fix.diff and re-ran the identical PoC: baseline fatal trap at movq $0,0x128(%r13) is gone; 'ls /hostroot' returns ENOENT cleanly and the vkernel stays alive (run.fixed.log, fix-build.log).

['run.fixed.log', 'fix-build.log', 'fix.diff']
↓ fix.diffvkernel VKDIRFS (DragonFly 6.5-DEVELOPMENT, guest scratch tree with fix.diff)

Confirmed kernel references

Detail

Exploit chain

vkernel-root mounts dirfs from a host dir (mount(2) with data=path); host dir is replaced by a dangling symlink (mv + ln -s); ANY vkernel user runs ls -> VFS_ROOT -> dirfs_root -> lstat(symlink) OK -> dm_root=dnp -> open(O_DIRECTORY) follows symlink -> ENOENT -> dirfs_free_vp -> vp==NULL -> vp->v_data=NULL -> write to 0x128 -> Fatal trap 12, vkernel dead. No uid0: the store is a constant 0 at a fixed offset (no attacker data), and dirfs corruption is confined to the vkernel userland process on the host.

Evidence (decisive lines)

["run.baseline.log: 'Fatal trap 12: page fault while in kernel mode', 'fault virtual address = 0x128', 'current process = 3 (ls)', 'Stopped at 0x72080b: movq $0,0x128(%r13)'", 'VERDICT.md: nm/objdump symbolization β€” 0x72080b is dirfs_free_vp+0x3b, the store vp->v_data=NULL with vp==NULL, immediately before jmpq dirfs_node_drop', "run.fixed.log: after fix.diff, 'ls: /hostroot: No such file or directory' and 'TRAVERSED' echoed β€” vkernel alive"]

PoC changes

dirfs does not compile on the current tree: added sys/malloc.h to dirfs_vnops.c and dirfs_subr.c, sys/uio.h to dirfs_vnops.c, and -Wl,--defsym,__build_id_{start,end}=0 to the vkernel link (guest scratch tree only). PoC itself needed: custom VKDIRFS kernel config (VKERNEL64 + options DIRFS), 160MB FFS root image with minimal userland, vkinit.c console-opening init wrapper (start_init opens no fds), vkmount.c direct mount(2) caller, fifo console driver with a persistent writer (vkernel reads console input from fd 0).

Verified recommended fix

In dirfs_root's open-failure path, clear dmp->dm_root and drop the life-of-mount reference instead of calling dirfs_free_vp/dirfs_node_free on a vnode-less node (see fix.diff).

Verdict

dirfs_root's open()-failure teardown runs dirfs_free_vp on a node with dn_vnode==NULL: vp->v_data=NULL is a kernel-mode write to 0x128 (verified instruction-for-instruction: Stopped at movq $0,0x128(%r13) = dirfs_free_vp+0x3b), followed by a latent double-free (dirfs_node_drop then dirfs_node_free) and a never-cleared dangling dmp->dm_root. Triggered on the guest by an unprivileged-in-vkernel mountpoint traversal after the host path became a dangling symlink; whole-vkernel fatal trap. fix.diff (dm_root=NULL + single dirfs_node_drop) validated: baseline panics, patched returns clean ENOENT and survives.