DF-2668 / verdict.json
{ "finding_id": "DF-2668", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "Reproduced on the stock INVARIANTS guest as root: getfh() on a nullfs-covered path (explicitly supported for NFS exports), then fhstatfs() on that handle panics with 'panic: assertion \"mp->mnt_refs == 0\" failed in mount_drop' and the backtrace mount_drop() <- sys_fhstatfs() <- syscall2(). Root cause: sys_fhstatfs() takes a held mount from vfs_getvfs() (sys/kern/vfs_syscalls.c:5063) but overwrites the pointer with vp->v_mount (:5074) and mount_drop()s that at :5096; nullfs VFS_FHTOVP is a pass-through returning a LOWER-filesystem vnode (sys/vfs/nullfs/null_vfsops.c:389-397), so the vfs_getvfs hold leaks on the nullfs mount while the live lower mount receives an unmatched mount_drop() driving mnt_hold 1->0. On INVARIANTS kernels this trips the KKASSERT (captured); on release kernels the same path kfree()s a live struct mount — a use-after-free of a mount still linked into the mountlist/fsid rb-tree with all its vnodes. sys_fhstatvfs() has the identical defect at :5135/:5149. Root-only trigger, so impact ceiling is privileged-user-triggered kernel corruption, not unprivileged escalation. fix.diff deletes both reassignments; validated on a rebuilt kernel (#1): identical setup returns valid statfs data, no panic, and the nullfs unmounts cleanly afterwards (no leaked hold).", "exploit_chain": "", "evidence": ["run.log (getfh fsid + freeze)", "panic.txt (mount_drop KKASSERT panic with sys_fhstatfs in backtrace)", "fix_validation.log + run.patched.log (patched kernel: fhstatfs ok, clean unmount)", "fix.diff"], "kernel_refs": ["sys/kern/vfs_syscalls.c:5063", "sys/kern/vfs_syscalls.c:5074", "sys/kern/vfs_syscalls.c:5096", "sys/kern/vfs_syscalls.c:5135", "sys/kern/vfs_syscalls.c:5149", "sys/vfs/nullfs/null_vfsops.c:389", "sys/kern/vfs_mount.c:393", "sys/kern/vfs_mount.c:414"], "poc_changes": "PoC written fresh (no seed); raw syscalls getfh(161)/fhstatfs(297) with fhandle_t from sys/mount.h; anchor path passed as argv[1] after the first attempt used a path covered by the nullfs (getfh ENOENT) — fixed by pointing at the nullfs view of /tmp/anchor_file.", "attempts": 2, "guest_uname": "DragonFly dfbsd 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": 35, "guest_dirty": 0, "build_cmd": "cc -O -o /tmp/df2668 /tmp/fhstatfs_hold.c", "run_cmd": "mkdir -p /tmp/nmtest; echo anchor > /tmp/anchor_file; mount -t null /tmp /tmp/nmtest; /tmp/df2668 /tmp/nmtest/anchor_file (as root)", "code_hash": "b3a7c7aa7da4acdd28d56ff7e323251955160098095c059572c06cda21e444ba", "notes": "Trigger requires root (SYSCAP_RESTRICTEDROOT at :5063's siblings :5056/:5118), hence impact=panic not privesc; the release-kernel manifestation is a live-mount UAF. Lower mount in the repro was the tmpfs backing /tmp; root hammer2 would behave identically. Guest reset with-src before and after validation.", "recommended_fix": "Delete the 'mp = vp->v_mount;' reassignments in sys_fhstatfs()/sys_fhstatvfs() so the vfs_getvfs()-held mount is dropped exactly once (see fix.diff).", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #1: Sun Aug 30 10:16:19 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: fhstatfs on the nullfs-covered handle succeeds with valid data, no KKASSERT, and the nullfs mount unmounts cleanly afterwards; mount hold bookkeeping correct.", "fix_evidence": ["fix_validation.log", "run.patched.log", "fix.diff"] } |