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)
PoC verification
Evidence pack
findings/poc/DF-3029 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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_testablePatched 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
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.
No comments yet.