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)
PoC verification
Evidence pack
findings/poc/DF-3218 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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)
- Guest:
mkdir /dirfs-host(a real directory). - vkernel boots to
/bin/sh(via /sbin/vkinit wrapper) on a fifo console. - 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_rootstill NULL (no VFS_ROOT yet). - Guest:
mv /dirfs-host /dirfs-host.real && ln -s /nonexistent-xyz /dirfs-hostβ the mount's host path is now a dangling symlink. - 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
fixedRebuilt 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']
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
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.
No comments yet.