DF-3218 / verdict.json
{ "finding_id": "DF-3218", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "dirfs_roots 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.", "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 <mountpoint> -> 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": [ "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 \u2014 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 \u2014 vkernel alive" ], "kernel_refs": [ "sys/vfs/dirfs/dirfs_vfsops.c:246", "sys/vfs/dirfs/dirfs_vfsops.c:250", "sys/vfs/dirfs/dirfs_vfsops.c:258", "sys/vfs/dirfs/dirfs_vfsops.c:261", "sys/vfs/dirfs/dirfs_vfsops.c:262", "sys/vfs/dirfs/dirfs_subr.c:300", "sys/vfs/dirfs/dirfs_subr.c:305", "sys/vfs/dirfs/dirfs_subr.c:96" ], "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).", "attempts": 6, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (guest); vkernel: VKDIRFS ident, DragonFly 6.5-DEVELOPMENT", "runtime_sec": 2100, "guest_dirty": 1, "build_cmd": "cd /usr/src && make -j6 nativekernel KERNCONF=VKDIRFS (after include+link fixes; full log build.vkernel-full.log)", "run_cmd": "sh /tmp/vkpoc2.sh (pack run.sh): vkernel -m 512m -r /vkroot.img -e vfs.root.mountfrom=ufs:vkd0s1a -e init_path=/sbin/vkinit; then /bin/vkmount /dirfs-host /hostroot; host: mv+ln dangling symlink; then: ls /hostroot", "code_hash": "548ac03fe363342e1841267fdc2c6ccc90472b640281055afb1e4e717d80462a", "notes": "dirfs is vkernel64-only: kernel here is the vkernel process on the guest; the panic kills the entire vkernel (all its processes), reachable by any vkernel user traversing the mountpoint. Double-free + dangling dm_root are latent behind the deterministic NULL-write; fix.diff removes all three. Guest was left with the scratch build tree; reset with-src afterwards.", "recommended_fix": "In dirfs_roots 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).", "fix_status": "fixed", "fix_kernel_uname": "vkernel VKDIRFS (DragonFly 6.5-DEVELOPMENT, guest scratch tree with fix.diff)", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "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).", "fix_evidence": [ "run.fixed.log", "fix-build.log", "fix.diff" ] } |