β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5798

unmount_allproc_cb() mutates p->p_textnch without p->p_token, racing exec's drop/copy β€” namecache reference underflow to UAF potential

Summary

During MNT_FORCE unmount dounmount's retry loop invokes unmount_allproc_cb on every iteration via allproc_scan holding only the allproc list token β€” the callback does an unsynchronized compare-then-drop of p_textnch (vfs_syscalls.c:777-778) while every other p_textnch mutation site (kern_exec.c:549-552, kern_exit.c:498, kern_shutdown.c:599, kern_fork.c:541) is serialized under p_token. Interleave: callback samples mount==mp on the OLD handle -> exec drops old and installs NEW -> callback's cache_drop releases the NEW handle's refs -> premature free -> UAF on next exec/exit/resolve. Guest stress (6 rounds x 8x400 self-reexecing binaries on tmpfs + respawning busy-holder + 10x umount -f per round; >100k execs, 60 force-unmounts, 243 race-avoided warnings proving heavy racing) ran clean across ~4.5 min β€” the tens-of-ns window was not hit; reported speculative with the source-level asymmetry unambiguous. Root by default (umount -f); unpriv if vfs.usermount=1. p_token serialization fix git-apply-clean.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5798 Β· 12 files
FileTypeDescriptionSize
README.md β€” 2.0 KB ↓ raw
VERDICT.md β€” 2.8 KB ↓ raw
reexec.c β€” 539 B view raw
stress.sh β€” 969 B view raw
build.sh β€” 89 B view raw
run.sh β€” 213 B view raw
build.log β€” 57 B view raw
run.log β€” 557 B view raw
dmesg.txt β€” 226 B view raw
fix.diff β€” 416 B view raw
manifest.json β€” 1.0 KB view raw
verdict.json β€” 3.3 KB view raw

DF-5798 β€” unmount_allproc_cb manipulates p->p_textnch without p_token (exec race β†’ refcount underflow/UAF)

What

unmount_allproc_cb() (sys/kern/vfs_syscalls.c:768-787) reads and drops p->p_textnch while holding only the allproc list token (via allproc_scan, kern_proc.c:1401) β€” not p->p_token. Every other p_textnch mutation site serializes under p_token:

  • kern_exec.c:549-552: if (p->p_textnch.ncp) cache_drop(&p->p_textnch); if (nch->mount) cache_copy(nch, &p->p_textnch);
  • kern_exit.c:498, kern_shutdown.c:599, kern_fork.c:541.

The unsynchronized pair (compare-then-drop in the callback vs drop-then-copy in exec) can interleave: the callback samples p->p_textnch.mount == mp on the OLD handle, exec then swaps in the NEW handle, and the callback's cache_drop() releases the new handle's references and zeroes it. The freshly-installed ncp/mount refs are then one too low β†’ premature free of a live namecache entry / mount refcount skew β†’ use-after-free. The callback runs on every retry-loop iteration of dounmount() (vfs_syscalls.c:931-947) for MNT_FORCE unmounts, so each umount -f attempt re-rolls the window up to 50 times.

Reachability: root by default (umount -f); unprivileged if vfs.usermount=1 (mount owner may force-umount their own mount).

Reproduce attempt (guest)

sh stress.sh   # 6 rounds: tmpfs on /mnt/df5798, 8x400 self-reexecing
               # binaries placed ON the mount, respawning cd-holder to
               # keep the retry loop iterating, 10x umount -f per round

~4.5 minutes, >100k execs, 60 umount -f attempts, 243 "nlookup: warning umount race avoided" messages (the namecache layer's own umount-race defusing for nlookup paths) — no panic, no crash. The p_textnch interleave window (compare→drop gap ~tens of ns must contain exec's entire drop+copy swap) was not hit.

Expected if the race is won

Refcount underflow on the new textnch ncp β†’ delayed panic/UAF on later use (exec/exit/vp resolution). Not observed.

VERDICT.md
↓ download raw

DF-5798 VERDICT β€” NOT REPRODUCED (speculative race, code-level case documented)

Bottom line

The race was exercised hard on the guest (6 rounds x (8x400 self-reexecing processes on the mount + respawning busy-holder + 10x umount -f), >100k execs, 60 force-unmount attempts, ~4.5 min) and did not manifest: no panic, no refcount divergence observed, guest stayed up (dmesg shows only the namecache layer's own "nlookup: warning umount race avoided" defusing, 243x — machinery that protects nlookup paths but NOT p_textnch). The code-level race remains: the unsynchronized compare-then-drop in unmount_allproc_cb() (vfs_syscalls.c:777-778) against exec's drop-then-copy (kern_exec.c:549-552) has an interleave that steals a reference from the newly installed handle. The window (callback's compare→drop gap must contain exec's full swap) is on the order of tens of nanoseconds against millisecond process lifetimes — likely why it did not trigger.

Why it is still reported (speculative)

  • unmount_allproc_cb mutates p->p_textnch with no p_token; every other mutation site in the kernel (kern_exec.c:549, kern_exit.c:498, kern_shutdown.c:599, kern_fork.c:541) is serialized by p_token. The asymmetry is unambiguous in the source.
  • The bad interleave is concrete: T1 reads p->p_textnch.mount == mp (old handle) β†’ T2 (exec) cache_drop (zeroes) + cache_copy (installs new handle, takes refs) β†’ T1 cache_drop(&p->p_textnch) drops the NEW handle's refs and zeroes it β†’ the new ncp/mount refs are one too low β†’ premature free of a namecache entry still reachable from the topology/vp β†’ UAF on next touch (exec/exit/resolve).
  • dounmount()'s retry loop (vfs_syscalls.c:888-963) re-runs the callback every iteration (up to 50 per attempt) whenever the mount stays busy, multiplying exposure per umount -f.
  • Trigger privilege: root by default; unprivileged if vfs.usermount=1 (mount owner can force-umount their own mount β€” sys_unmount vfs_syscalls.c:648-665).

Evidence

  • run.log β€” stress trace: STRESS-DONE, mount cleaned up, no panic.
  • dmesg.txt β€” 243x "nlookup: warning umount race avoided" (proves heavy umount racing occurred; that warning path is nlookup-specific).
  • reexec.c, stress.sh β€” the rig.

Fix validation

Not performed (nothing reproduced to validate against). fix.diff is the one-line-token fix: take p_token around the p_textnch drop and re-check the condition under the token.

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

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored and git-apply-check clean; validation not applicable since the race did not reproduce in the baseline run.

findings/poc/DF-5798/fix.diff
↓ fix.diffper-fix-DF-5798

Confirmed kernel references

Detail

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 (decisive lines)

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

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).

Verified 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)

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 callback's cache_drop executes, the drop releases the NEW handle's 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 exec's 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.