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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3193 Β· 10 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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):

  1. [888] daemon: WITHHOLDING reply for FUSE_STATFS unique=3 β€” worker (pid 890) now sleeps in fuse_ipc_tx() on its fip.
  2. [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).
  3. [888] daemon: FUSE_DESTROY -> reply, exit, close(fd) β€” fd close β†’ fuse_cdevpriv_dtor β†’ fuse_mount_free drops the final ref β†’ fmp kfree'd (~1.5 s into the cycle).
  4. ~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 end fuse_ipc_remove() executed mtx_lock(&fmp->ipc_lock) and walked fmp->request_head/reply_head on 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): timeout executing 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

inconclusive
baseline reproduced→ patch + rebuild →patched clean

Fix 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
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (patched fuse.ko, module rebuild)

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