DF-3073 / verdict.json
{ "finding_id": "DF-3073", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "REPRODUCED on the stock INVARIANTS guest: nfs_unmount() ignores vflush() failure under MNT_FORCE and frees the nfsmount (destroying the nfsnode objcache) while a referenced VCHR vnode and its nfsnode are still alive - allocator warning NFS inodes: ... 1 objects were still allocated - after which dounmount() panics unconditionally (unmount: dangling vnode, dounmount+0x87c). The referenced device vnode is supplied by the NFS server (DF-2998 attribute surface) and held open by an unprivileged user via an SCM_RIGHTS-parked descriptor that dounmounts kill-retry loop cannot touch. Deterministic local kernel panic (root pulls the umount -f trigger); the UAF window provably exists but is panic-gated in-syscall, so realistic impact is DoS, not privesc. Fix validated: with nfs_unmount returning the vflush error and clearing NFSMNT_FORCE, the identical PoC yields EBUSY with the guest alive and the mount recoverable.", "exploit_chain": "unprivileged user opens server-injected device node on an NFS mount -> parks fd in unix-socket SCM_RIGHTS receive buffer (no process uses the mount; kill-retry loop finds nobody) -> root runs umount -f -> 50x250ms retry loop exhausts (namecache+mount refs held by the buffered fp) -> vflush(mp,1,FORCECLOSE) returns EBUSY because vflush_scan clears FORCECLOSE for VCHR and counts it busy -> nfs_unmount continues anyway: nfs_free_mount() kfrees nmp and destroys nm_mnode with the dev vnodes nfsnode still allocated (use-after-free condition; later nfs_inactive/nfs_reclaim on that vnode would deref freed nmp) -> dounmount panics unmount: dangling vnode. Ceiling on this path: deterministic panic (the panic fires in the same syscall before the freed state can be acted on by another thread).", "evidence": [ "panic.txt: console capture - vflush: Warning, cannot destroy busy device vnode / NFS inodes: slab ... 1 objects were still allocated / malloc_uninit: 576 bytes of NFS inodes still allocated / panic: unmount: dangling vnode / dounmount+0x87c <- sys_unmount", "run.log: session 3 (decisive) - holder2 DEV_OPEN_OK/FD_PARKED as nobody, umount -f session dies at 23:26:19; sessions 1-2 are controls (clean umount -f without holder; NFS-blocked holder killed by MNTK_UNMOUNTF abort semantics)", "run.fixed.log + fixed.console.txt: patched kernel - same PoC gives UMNTF_RC=1 (EBUSY), guest alive, vfs refused to unmount, error 16, clean unmount after holder release", "fakesrv2.c/holder2.c + build.sh/run.sh: exact reproducible sources and commands" ], "kernel_refs": [ "sys/vfs/nfs/nfs_vfsops.c:1217-1224", "sys/vfs/nfs/nfs_vfsops.c:1232-1243", "sys/vfs/nfs/nfs_vfsops.c:1248-1262", "sys/kern/vfs_mount.c:1145-1146", "sys/kern/vfs_mount.c:1194", "sys/kern/vfs_syscalls.c:839-972", "sys/kern/vfs_syscalls.c:1085-1086", "sys/vfs/nfs/nfs_node.c:383-387", "sys/vfs/nfs/nfs_node.c:439,485", "sys/kern/kern_slaballoc.c:624-631", "sys/vfs/nfs/nfs_socket.c:2059-2066" ], "poc_changes": "Rewrote the DF-3009 seed harness into a functioning NFSv3 server (fakesrv2.c): fixed a byte-swapped SunRPC record mark in send_record() (inherited from the DF-3009 seed - only its literal 0x80000000 mark was correct), added missing nfsstat words in ACCESS/FSSTAT/FSINFO/READDIR/READ replies, added LOOKUP serving a VCHR node with rdev 14:2 and an empty-dir READDIR(+). Replaced the seeds blocked holder idea with holder2.c: a blocked-in-NFS holder cannot work because nfs_sigintr() aborts all RPCs once MNTK_UNMOUNTF is set; the working primitive is an SCM_RIGHTS-parked descriptor that no process owns.", "attempts": 3, "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", "runtime_sec": 47, "guest_dirty": 0, "build_cmd": "sh /root/df3073/build.sh (cc -O2 -o fakesrv2 fakesrv2.c; cc -O2 -o holder2 holder2.c)", "run_cmd": "sh /root/df3073/run.sh (phases A/B controls, phase C: umount -f -> panic)", "code_hash": "fakesrv2.c sha256 47e96588a2bcce4f68f85974f7d5349bc2a43989356e437671319baea88b8b9b; holder2.c sha256 65ba96c47d821198c603f1107c5dea887173724c7a92640900132e75600ac69d", "notes": "Impact honestly bounded at panic: the free-with-live-object is real (allocator-confirmed) but dounmounts unconditional unmount: dangling vnode check fires in the same syscall, so the UAF cannot be acted upon before the machine stops. Trigger requires roots umount -f (the documented remedy for stale NFS mounts); the priming (device node via hostile server + fd parking) is fully unprivileged. NFSMNT_FORCE must be cleared on the new error path or the surviving mount is permanently wedged (nfs_socket.c:200,1288). Guest left clean: vm.sh reset with-src after fix validation.", "recommended_fix": "nfs_unmount(): on vflush() failure return the error unconditionally (also for MNT_FORCE) and clear NFSMNT_FORCE before returning - see fix.diff", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Sat Sep 5 23:42:21 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Patched kernel built in-guest (make nativekernel KERNCONF=X86_64_GENERIC) with fix.diff applied. Exact PoC rerun: umount -f returns EBUSY after the ~14s retry loop, console shows vfs refused to unmount, error 16 with NO objcache-destroy-with-live-objects warning and NO panic; guest stays up; after the holder releases the parked descriptor umount -f succeeds cleanly (RC=0). Baseline panic is gone.", "fix_evidence": [ "run.fixed.log: UMNTF_RC=1 on patched kernel (baseline: panic), mount intact, clean unmount after holder release", "fixed.console.txt: vflush: Warning, cannot destroy busy device vnode followed by unmount: vfs refused to unmount, error 16 - no NFS inodes ... still allocated, no panic", "fix.diff: the one-hunk fix (return error + clear NFSMNT_FORCE)" ] } |