DF-5798 / verdict.json
{ "finding_id": "DF-5798", "status": "not_reproduced", "verdict": "Code-level race verified by source analysis but not manifested on the guest. unmount_allproc_cb() (vfs_syscalls.c:777-778) reads and cache_drop()s p->p_textnch holding only the allproc list token (allproc_scan, kern_proc.c:1401-1432), not p->p_token - the lock every other p_textnch mutation site holds (kern_exec.c:549-552, kern_exit.c:498, kern_shutdown.c:599). If the callback samples p->p_textnch.mount==mp on the old handle and exec swaps in a new handle (drop+cache_copy) before the callbacks cache_drop executes, the drop releases the NEW handles ncp/mount refs and zeroes it - a one-ref underflow on a live namecache entry (premature free -> UAF). dounmount()s MNT_FORCE retry loop re-runs the callback every iteration (up to 50 per umount -f). Stress: 6 rounds, 8x400 self-reexecing binaries on a tmpfs mount, respawning busy-holder, 10x umount -f per round (~4.5 min, >100k execs, 60 force-unmounts, 243 nlookup umount-race warnings proving heavy racing) - no panic, no divergence. The compare-to-drop gap (~tens of ns) must contain execs entire swap; window not hit in ~3000 callback scans x 10 procs. Trigger requires root (umount -f) by default; unprivileged only if vfs.usermount=1.", "reproduced": false, "impact": "none", "confidence": "speculative", "exploit_chain": "root (or usermount owner) umount -f || race exec drop/copy interleave in unmount_allproc_cb -> p_textnch new-handle ref stolen -> ncp premature free -> UAF on next exec/exit/resolve (panic to potential exploitation); chain NOT demonstrated", "evidence": "run.log: STRESS-DONE, mount cleaned up, no panic; dmesg.txt: 243x nlookup: warning umount race avoided (racing occurred; that defusing is nlookup-specific and does not cover p_textnch); fix.diff applies cleanly", "kernel_refs": [ "sys/kern/vfs_syscalls.c:777", "sys/kern/vfs_syscalls.c:768", "sys/kern/vfs_syscalls.c:946", "sys/kern/kern_exec.c:549", "sys/kern/kern_exit.c:498", "sys/kern/kern_proc.c:1401" ], "poc_changes": "Wrote from scratch: reexec.c (self-reexecing binary placed on the victim mount so every exec installs p_textnch there) and stress.sh (busy-holder forcing the dounmount retry loop to iterate so the callback re-runs per iteration).", "attempts": 1, "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": 285, "guest_dirty": 1, "build_cmd": "cc -O -o /tmp/reexec /tmp/reexec.c", "run_cmd": "sh /tmp/stress.sh", "code_hash": "f8b3938b4b18e57f6516d2f3ae23fb883adc16177f57f9625618bbbf7ba9bbc4", "notes": "One-line fix (p_token around the drop, re-check under token) included; safe against deadlock since no other token is held across the callback at that point. Even unfixed, practical exploitability is low given the nanosecond interleave window.", "recommended_fix": "Take p->p_token around the p_textnch read+drop in unmount_allproc_cb() and re-check the condition under the token (see fix.diff)", "fix_status": "not_tested", "fix_baseline_reproduced": false, "fix_patched_reproduced": false, "fix_verdict": "fix.diff authored and git-apply-check clean; validation not applicable since the race did not reproduce in the baseline run.", "fix_evidence": "findings/poc/DF-5798/fix.diff" } |