Racing file creates against cross-parent msdosfs directory renames leaks mount/vnode references: umount(2) hangs forever and the system wedges (unprivileged local DoS)
Summary
Two unprivileged processes racing on any writable msdosfs directory - one performing cross-parent DIRECTORY renames (each leaves the target directory unlocked for the whole doscheckpath() ancestor walk, :822-863, incl. per-hop deget/vput at :857-861), the other performing ordinary create/unlink churn - reproducibly (2/2, once attribution-clean on a fresh reset guest) leave the mount with leaked references (kernel diagnostic: 'unmount(/fatrace): ... 2 mount refs still present'). After all racers exit (verified zero leftover processes), umount hangs forever, and the guest goes fully catatonic: sshd stops accepting connections, serial console frozen, no panic and no DDB - hard system wedge requiring power-off. Control run with identical churn against same-directory FILE renames (no doscheckpath) unmounts instantly and fscks clean, isolating the trigger to the directory-rename path. The kernel's own race detector fired during the churn ('kern_rename: retry due to race', vfs_syscalls.c:4323), proving the interleaving is live. Any local user with write access to a single directory on any mounted msdosfs filesystem (removable FAT media, DOS partition, usermount image) can wedge the entire machine in ~30s using only mkdir/rename/open/unlink. Sequentially every hand-traced path is balanced (incl. deget clearing *depp on error); the leak manifests only under interleaving - exact leaking statement needs a refcount-instrumented kernel build (prime suspects: the vref(tdvp)+doscheckpath-vput pairing under kern_rename EAGAIN retry; relookup-found-tvp combinations; deget error path leaving a hash-resident denode). Availability-only, system-wide, non-recoverable without power cycle.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3117 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | β | 7.4 KB | view raw | |
| build.sh | β | 66 B | view raw | |
| run.sh | β | 130 B | view raw | |
| wedge.sh | β | 1.7 KB | view raw | |
| build.log | β | 304 B | view raw | |
| run.3.log | β | 1.7 KB | view raw | |
| run.5.log | β | 1.8 KB | view raw | |
| run.9.log | β | 1.9 KB | view raw | |
| run.10.wedge.log | β | 850 B | view raw | |
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 4.2 KB | β raw | |
| verdict.json | β | 4.0 KB | view raw | |
| manifest.json | β | 985 B | view raw |
DF-3117 β racing create vs cross-parent msdosfs directory rename wedges the system
What
Two unprivileged processes on any writable msdosfs directory:
- R: cross-parent directory renames (
bouncer: mkdir Din p1, rename into a deep parent p2 β every rename runs doscheckpath()with the target directory unlocked), and - C: a create/stat/unlink churn loop (
creator)
race for 30β60 s. All racer processes exit cleanly, but afterwards:
umountof the filesystem hangs forever (leaked mount/vnode references β the earlier console diagnostic printedunmount(/fatrace): ... 2 mount refs still present), and- the guest then goes catatonic: sshd stops accepting connections
("Connection timed out during banner exchange"), the serial console
freezes, and
vm.sh statusreports down while the QEMU process is still alive. No panic, no DDB β a hard system wedge.
Reproduced twice (run.9 and the attribution-clean run.10), on a fresh
vm.sh reset with-src guest with a fresh image and verified-zero
leftover processes before the umount.
Control: the Window-B configuration (file renames only, no
doscheckpath) unmounts instantly after identical churn (run.3.log /
run.5.log stage [6]) β isolating the trigger to the
directory-rename/doscheckpath path.
Where (kernel_refs)
The leak site is not line-pinned (needs VFS refcount instrumentation); the racing surface spans:
sys/vfs/msdosfs/msdosfs_vnops.c:916-1294β msdosfs_rename state machine (vref/vput/vrele protocol across the doscheckpath + relookup unlock windows,kern_renameEAGAIN retry interplay)sys/vfs/msdosfs/msdosfs_lookup.c:792-872β doscheckpath's deget/vput ancestor walk (brelse/deget error contract verified clean sequentially; suspect under interleaving)sys/kern/vfs_syscalls.c:4316-4332βkern_rename: retry due to raceEAGAIN path (logged live during the race)sys/kern/vfs_default.c:953-1069β vop_compat_nrename
PoC
Same harness as DF-3116 (trigger.c, wedge.sh). The decisive run
is run.10.wedge.log: racers done + "leftover procs: none" + umount
watchdog never completing + ssh channel death.
No fix.diff: a correct fix requires pinpointing the leaking statement (upstream should reproduce under a VFS_REFCOUNT/debug-refcount kernel build). Recommended containment while unfixed: none realistic; severity is availability of the whole system, triggered by any local user with write access to one msdosfs directory.
DF-3117 VERDICT β racing create vs cross-parent dir rename: mount-ref leak β umount hang β system wedge
Status: REPRODUCED (impact=dos). Twice; the decisive run is attribution-clean.
How it reproduces
- Fresh
vm.sh reset with-srcguest (stock INVARIANTS kernel #0, 6 vCPU), fresh 512 MB FAT32 image on /dev/vn0, mounted-u 65534 -g 65534 -m 0777at /fatrace. - p1 = /fatrace/p1 (shallow); p2 = /fatrace/deep/l1/../l100 (a
100-deep chain, so every
doscheckpath()walk is ~100 hops). - Three unprivileged (nobody) racers for 30 s:
- bouncer: 38,661 cross-parent directory renames (mkdir D
in p1, rename into p2) β all succeeded (0 errors), - creator in p2: 58,608 create+verify+rotate cycles β 0 vanished, - creator in p1: 41,422 cycles β 0 vanished. - All racers exit;
psconfirms zero leftover processes ("leftover procs: none"). umount /fatraceunder a 20 s watchdog never completes; the ssh channel then dies ("Connection timed out during banner exchange"), the serial console freezes (no further output, no panic, no DDB), andvm.sh statusreports down while the QEMU process stays alive. Seerun.10.wedge.log(cut off mid-umount by the wedge itself) andrun.9.logfor the first occurrence.
First occurrence (run.9, same race shape, 60 s): identical β umount
stage hung silently, guest later went unresponsive; the console from
that session additionally showed the kernel diagnostic
unmount(/fatrace): ... 2 mount refs still present (from a
subsequent forced umount attempt), i.e. two leaked mount
references, and kern_rename: retry due to race on: "D68634" ->
"D68634" (12,0) (sys/kern/vfs_syscalls.c:4323), proving the rename
race machinery was live during the churn.
Control (isolates the trigger)
The Window-B configuration β identical creator churn against same-dir
file renames (no directory renames, hence no doscheckpath) β
unmounts instantly and fsck's clean (run.3.log / run.5.log stage [6]).
The wedge therefore requires the cross-parent directory rename
path (msdosfs_rename's doscheckpath/relookup unlock windows +
deget/vput ancestor walk), racing ordinary creates.
Root cause status
The leaking statement is not line-pinned. Sequentially, the ref
protocol in msdosfs_rename/doscheckpath/vop_compat_nrename is balanced
on every path I hand-traced (including doscheckpath's brelse/deget
error contract β deget clears *depp on all error returns,
msdosfs_denode.c:291,318,374). The leak therefore manifests only
under a specific interleaving (candidates: the vref(tdvp)+doscheckpath
vput pairing under kern_rename EAGAIN retry, the relookup-found-tvp
combinations at vnops:1056-1102, or deget's error-path denode left
hash-resident at msdosfs_denode.c:363-376). Pinning it requires a
VFS refcount-debug kernel build; recommended first step upstream.
Impact
Any local user with write access to a single directory on any mounted msdosfs filesystem (USB stick, FAT partition, user-mounted image with vfs.usermount=1) can, in ~30 s of unprivileged racing, leak mount references such that the filesystem can never be unmounted and the system subsequently wedges entirely (no panic, no dump β hard catatonia). Availability-only; confidentiality/integrity untouched by this aspect (the integrity consequence of the same racing surface is DF-3116).
Fix
No fix.diff β no line-pinned root cause. Upstream should reproduce
under refcount instrumentation (e.g. DEBUG_VFS_LOCKS / VFS_REFCOUNT
equivalents) on the bouncer+creator workload in wedge.sh; prime
suspects listed above. fix_status=not_testable for the same reason.
Guest state
The wedge dirtied the guest twice; vm.sh reset with-src was run
afterward and the guest is up and clean (source tree verified restored,
no leftover artifacts beyond /tmp copies removed by the reset).
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.
(No fix.diff for this item: documented recommendation only - see Fix above.)
Fix verification
not_testableNo fix authored: the leaking statement is not line-pinned (sequentially-balanced ref protocol on every hand-traced path; leak manifests only under a specific interleaving). Requires refcount-instrumented kernel build upstream.
Confirmed kernel references
Detail
Exploit chain
unprivileged user with write access to one directory on any mounted msdosfs -> run bouncer (cross-parent dir renames into a deep parent) + creator (create churn) for ~30s -> mount/vnode references leak -> umount(2) of the filesystem hangs forever -> system-wide wedge (all userland eventually starves; hard poweroff required).
Evidence (decisive lines)
["findings/poc/DF-3117/run.10.wedge.log β decisive attribution-clean run: racers exit, 'leftover procs: none', umount watchdog never completes, ssh channel dies mid-probe", 'findings/poc/DF-3117/run.9.log β first occurrence: same shape, umount stage hang', 'findings/poc/DF-3117/VERDICT.md β full narrative incl. console diagnostics (unmount ... 2 mount refs still present; kern_rename: retry due to race) and the control run', 'findings/poc/DF-3117/wedge.sh β exact reproducible script']
PoC changes
Same harness as DF-3116 (trigger.c authored from scratch; see DF-3116 poc_changes). wedge.sh added for attribution-clean single-shot reproduction: fresh guest, fresh image, racers verified dead, umount under 20s watchdog.
Verified recommended fix
Reproduce under VFS refcount instrumentation on the wedge.sh workload and fix the leaking statement in the msdosfs rename/doscheckpath/compat chain; until then this is an unprivileged full-system availability bug.
Verdict
Reproduced twice (once attribution-clean on a fresh vm.sh reset with-src guest): 30s of unprivileged racing β 38,661 cross-parent DIRECTORY renames (each running doscheckpath with tdvp unlocked) against ~100k file creates in the target parents, all on a stock FAT32 image β leaves the msdosfs mount with leaked references (kernel diagnostic showed '2 mount refs still present' in the first occurrence). After all racers exit (verified zero leftover processes), umount hangs forever and the guest subsequently goes fully catatonic: sshd stops accepting (banner timeout), serial console freezes, no panic, no DDB; vm.sh reports down while QEMU stays alive. Control run with identical creator churn against same-dir FILE renames (no doscheckpath) unmounts instantly and fscks clean, isolating the trigger to the directory-rename/doscheckpath/deget path. The exact leaking statement is not line-pinned (sequentially-balanced ref protocol on all hand-traced paths, incl. deget's clears-*depp-on-error contract); needs refcount-instrumented kernel build upstream.
No comments yet.