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)
PoC verification
Evidence pack
findings/poc/DF-5798 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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_cbmutatesp->p_textnchwith nop_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) β T1cache_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 perumount -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_testablefix.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
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.
No comments yet.