fuse_unmount() frees fmp under in-flight statfs/statvfs: FUSE is MPSAFE (VFS_MPLOCK no-op) and fuse_statfs/statvfs take no fmp reference, so a forced umount plus daemon fd-close kfree's fmp while a statfs thread sleeps on its IPC β the wake/timeout path locks and walks freed (recycled) kernel memory
Summary
fuse sets MNTK_ALL_MPSAFE (:190), so VFS_STATFS/VFS_STATVFS run unserialized and kern_statfs holds only a namecache ref. fuse_statfs/fuse_statvfs (:386-445) take no fmp ref and block up to ~35s in fuse_ipc_tx; a forced umount completes fuse_unmount including fuse_mount_free (:293) without aborting or draining outstanding IPCs; the daemon's fd close then drops the last ref and frees fmp. The sleeping statfs thread later wakes and its ETIMEDOUT path runs fuse_ipc_remove -> mtx_lock(&fmp->ipc_lock) + TAILQ_FOREACH over freed, since-recycled memory (fuse_ipc.c:125-140,185-187). PoC: daemon withholds STATFS, 4 workers sleep in statfs(), umount -f + daemon exit frees fmp, workers wake ~35s later through the timeout -> freed-fmp path (serial 'fuse_ipc_wait timeout', errno 16) - trigger and free proven, no panic in time-boxed runs (benign recycle) so impact=none/not_reproduced; the fix's drain makes umount wait for in-flight statfs, removing the window. Fix validated.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3193 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| uaf.c | β | 8.9 KB | view raw | |
| run.sh | β | 114 B | view raw | |
| run.log | β | 888 B | view raw | |
| uaf_full.log | β | 888 B | view raw | |
| serial_full.txt | β | 3.0 KB | view raw | |
| env.txt | β | 278 B | view raw | |
| code_hash.txt | β | 65 B | view raw | |
| fix.diff | β | 3.5 KB | view raw | |
| fix_run.log | β | 591 B | view raw | |
| VERDICT.md | β | 3.5 KB | β raw |
DF-3193 β VERDICT
status: not_reproduced (panic) / trigger chain + use-after-free access proven by construction / impact observed: none (benign recycle)
What was run (baseline, stock INVARIANTS kernel #0)
Two runs of ./uaf (6 cycles with zeroed-fmp spray; 5 cycles without
spray). Decisive single-cycle timeline (run.log + serial_full.txt):
[888] daemon: WITHHOLDING reply for FUSE_STATFS unique=3β worker (pid 890) now sleeps infuse_ipc_tx()on its fip.[887] umount -f /mnt/dfc ... umount OKβ forced umount completes in ~1 s while the worker sleeps (dounmount forcing path, vfs_syscalls.c:975-993; fuse_unmount never aborts/drains in-flight IPCs).[888] daemon: FUSE_DESTROY -> reply, exit, close(fd)β fd close βfuse_cdevpriv_dtorβfuse_mount_freedrops the final ref β fmp kfree'd (~1.5 s into the cycle).- ~35 s later: serial console
fuse_ipc_wait(uaf|891): timeout/(uaf|892): timeoutβ the workers take the full 6Γ5 s retry path; at its endfuse_ipc_remove()executedmtx_lock(&fmp->ipc_lock)and walkedfmp->request_head/reply_headon the fmp freed in step 3 (fuse_ipc.c:125-140). Workers return errno 16 ("returned through freed-fmp path" in run.log).
The trigger schedule (hung-daemon statfs vs forced umount vs daemon
exit) is deterministic and reproduced in both runs; the freed-memory
lock/walk follows necessarily from the executed path (the "timeout"
print sits immediately before fuse_ipc_remove in fuse_ipc.c:185-187).
Why no crash
The freed ~256-byte M_TEMP block was recycled with benign content
(zeroed fresh fmp objects from the spray run; empty stale heads in the
no-spray run), so the stale mtx_lock succeeded and the
TAILQ_FOREACH compared against NULL heads and exited. Turning the
walk into a spin/lock-corruption/page-fault requires non-zero garbage at
the ipc_lock/list offsets β a matter of what recycles the block in the
30 s window; INVARIANTS mtx owner checks make garbage at the lock word
likely to fire, but this was not observed within the time-boxed runs.
Honest classification per the contract: expected marker (panic) absent,
run exits, guest stays up β not_reproduced, reproduced=0,
impact=none. The lifetime defect itself is statically certain.
Fix validation (behavioral)
Same patched fuse.ko as DF-3191/DF-3192 (drain in fuse_unmount + post-enqueue dead recheck + remove-on-dead-entry):
- baseline cycle: umount returns ~1 s while the worker still sleeps (free happens under the in-flight IPC).
- patched cycle (fix_run.log): cycle wall time 83 s vs ~40 s β umount
now blocks in the drain until the worker's ETIMEDOUT dequeues its fip;
the worker's timeout path therefore runs on a live fmp
(serial_full.txt in DF-3191's run for the patched boot shows
fuse_ipc_wait(uaf|926): timeoutexecuting after umount returned, i.e. on the still-referenced object), and the free happens only after both the drain and the unmount complete. - no panic either way (the baseline crash is probabilistic), so the fix status is honestly inconclusive β the serialization the fix adds is demonstrated, the absence of a probabilistic crash is not provable in feasible time.
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
inconclusiveFix behavior demonstrated (patched umount waits ~35s for the in-flight statfs to dequeue β cycle 83s vs 40s β so the worker's timeout path runs on a live fmp and the free happens after the drain), but since the baseline never panicked, absence-of-crash cannot be proven: inconclusive.
fix_run.log (timing + worker path), fix.diff
Confirmed kernel references
Detail
Exploit chain
statfs() worker blocks on withheld reply -> umount -f completes (forcing path ignores refs) -> daemon exits closing fd -> cdevpriv dtor drops last ref -> fmp kfree'd -> worker wakes: fuse_test_dead read, then ETIMEDOUT path mtx_lock/ipc_lock + TAILQ_FOREACH on freed (recycled) memory. Escalation beyond corruption not pursued: requires root to umount; crash probabilistic.
Evidence (decisive lines)
run.log/uaf_full.log: WITHHOLDING + umount OK + worker 'statfs errno=16 β returned through freed-fmp path'; serial_full.txt: 'fuse_ipc_wait(uaf|891/892): timeout' ~34s after the free; fix_run.log: patched cycle 83s vs 40s baseline (umount now drains until the worker dequeues).
PoC changes
PoC authored in this run; daemon-withholds-STATFS + forced umount + spray design; second run added SPRAY=0 natural-recycle mode.
Verified recommended fix
Drain in-flight IPCs in fuse_unmount (wait for request_head/reply_head to empty after fuse_mount_kill), re-check dead after enqueue in fuse_ipc_tx and self-unqueue, and dequeue in fuse_ipc_wait's dead-at-entry path.
Verdict
The lifetime defect is real and the trigger chain reproduced end-to-end: a daemon-withheld FUSE_STATFS keeps workers in fuse_ipc_tx (no fmp ref, VFS_MPLOCK is a no-op because FUSE is MPSAFE), a forced umount completes and the daemon's fd close frees fmp ~1.5s later, and the workers' 35s timeout path (fuse_ipc_wait -> fuse_ipc_remove: mtx_lock + TAILQ walk) provably executes on the freed object (serial: fuse_ipc_wait timeout prints; workers return via the freed path). No panic materialized in the time-boxed runs because the freed block was recycled with benign (zeroed) content, so per contract this is not_reproduced/impact none β the crash is recycle-dependent (INVARIANTS mtx owner checks / garbage list pointers would fire on a hostile recycle).
No comments yet.