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

fuse_io_thread lost-wakeup window (no tsleep interlock between empty-queue check and sleep) can strand a bio indefinitely

Field Value
ID DF-3029
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:C/C:N/I:N/A:H
CWE CWE-662 / CWE-820
File sys/vfs/fuse/fuse_vnops.c
Lines 2013-2023 vs :1682-1693
Area vfs/fuse
Confidence likely
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket base:vfs
Reported pending
Known CVE none
CVE match novel

Summary

fuse_io_thread decides the bioq is empty, drops helper_spin, then tsleeps with no interlock; a wakeup() from fuse_vop_strategy landing in that window is lost, stranding the queued bio β€” its waiter (user thread in biowait, or pageout via UIO_NOCOPY strategy) blocks forever unless unrelated fuse I/O re-wakes the helper. The correct tsleep_interlock+PINTERLOCKED pattern is already used by fuse_ipc_wait in the same subsystem. 240s/8-thread/585M-op stress could not hit the nanosecond-scale window (honest not_reproduced); fix.diff restructures the loop (interlocked sleep with queue recheck) and passed a no-regression smoke test. Unpriv user hammering concurrent I/O for hours can eventually win the race on the mount's last outstanding I/O β€” probabilistic local DoS.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of fuse_vnops.c (GLM 5.3); interleaving-proven, honestly not reproduced in bounded stress.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3029 Β· 9 files
FileTypeDescriptionSize
lostwakeup.c β€” 3.5 KB view raw
fuse_daemon.c β€” 16.5 KB view raw
build.sh β€” 137 B view raw
run.sh β€” 669 B view raw
run.log β€” 292 B view raw
fix.diff β€” 809 B view raw
fix_run.log β€” 343 B view raw
VERDICT.md β€” 1.6 KB ↓ raw
env.txt β€” 203 B view raw
VERDICT.md
↓ download raw

DF-3029 verdict β€” NOT REPRODUCED in bounded stress (defect stands by analysis)

Root cause (path:line)

sys/vfs/fuse/fuse_vnops.c:2013-2023 (fuse_io_thread) vs sys/vfs/fuse/fuse_vnops.c:1682-1693 (fuse_vop_strategy producer: spin_lock; TAILQ_INSERT; spin_unlock; wakeup).

Classic lost wakeup: the consumer's queue-empty decision is not interlocked with its tsleep. If the producer's wakeup() lands in the instructions between the helper's spin_unlock (after observing an empty queue) and its tsleep, the wakeup is dropped; the queued bio is never processed and its waiter sleeps forever (user read in biowait, or pageout via UIO_NOCOPY strategy). Only a subsequent fuse I/O on the same mount re-wakes the helper; an otherwise-idle mount hangs permanently.

Verification attempt (honest)

8 threads x 240s of randomized 4K preads against a 16MB fuse file (worst-case queue churn, ~2.4M reads/s sustained): 585,939,514 ops, zero stalls. The window is ~tens of nanoseconds against a ~400ns+ syscall loop; a hit is expected only at very low probability per idle-transition (hours-to-days of hammering). Classification: not_reproduced (bounded run), finding confidence likely (the code defect is unambiguous; compare the correct interlocked pattern in fuse_ipc.c:171-175).

Fix

fix.diff restructures the loop to tsleep_interlock + recheck under the spinlock + tsleep(PINTERLOCKED), and also rechecks fmp->dead under the lock for a clean unmount exit. Patched module passes a 120s no-regression smoke (290,965,533 reads, no stall, same throughput). fix_status=not_testable (the race itself cannot be triggered deterministically).

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

Patched module passes a 120s stress smoke test with identical throughput and no stalls; the lost-wakeup race itself cannot be deterministically triggered pre-fix, so behavioral A/B validation is not possible.

fix_run.log, fix.diff
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0 (fuse.ko rebuilt with fix.diff in-guest)

Confirmed kernel references

Detail

Exploit chain

unpriv user hammers concurrent reads on a fuse mount for hours/days; when the race hits on the last outstanding I/O of an otherwise idle mount, a thread blocks uninterruptibly forever (DoS); if the stranded waiter is the pageout daemon the stall can cascade system-wide

Evidence (decisive lines)

['run.log: NO_STALL progress=585939514 in 240s (bounded attempt)', 'fuse_vnops.c:2013-2023 vs 1682-1694 interleaving analysis in VERDICT.md', 'fix_run.log: patched 120s smoke 290965533 reads no regression']

PoC changes

none (original design); lostwakeup.c monitors shared progress counters with a 20s stall detector

Verified recommended fix

Use tsleep_interlock + queue recheck under helper_spin + tsleep(PINTERLOCKED) in fuse_io_thread (mirroring fuse_ipc_wait).

Verdict

fuse_io_thread decides the bioq is empty, drops helper_spin, then tsleeps with no interlock; a wakeup from fuse_vop_strategy in that few-instruction window is lost and strands the bio (its waiter - user biowait or pageout - hangs until unrelated fuse I/O re-wakes the helper). The defect is unambiguous by inspection (the same file uses the correct tsleep_interlock pattern in fuse_ipc_wait); a 240s, 8-thread, 585M-op stress could not hit the nanosecond-scale window, so the PoC verdict is honestly not_reproduced.