Lost-wakeup window in fifo_open() FIFO rendezvous (uninterlocked check-then-sleep, no re-check loop)
Summary
fifo_open() checks the rendezvous condition (fi_writers==0 for readers, fi_readers==0 for writers) under fifo_lock, then drops fifo_lock, drops the vnode lock, and only then tsleep()s on &fi_readers/&fi_writers with no interlock; a peer completing its 0->1 counter transition in that gap sends wakeup() to nobody and the opener sleeps until the NEXT 0->1 transition (i.e. for as long as the peer holds its fd open). No post-wakeup re-check loop either (comment asserts wakeup implies a peer; FreeBSD uses while()+msleep). Unpriv local user; blocked open() hangs for the peer's fd lifetime - single-thread local DoS at worst; no memory-safety impact. Hammered ~7.7M opens in 3 configurations on 6-vCPU INVARIANTS: NOT reproduced (max latency 202.8ms under load - scheduler noise, never near the hold time); the window is structural but empirically the wakeup-sending path always outlasts the sleeper's path into the sleep queue. Fix: loop on the condition + tsleep_interlock/PINTERLOCKED pattern.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3160 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fifo_open_race.c | β | 2.6 KB | view raw | |
| run.log | β | 1.2 KB | view raw | |
| env.txt | β | 286 B | view raw | |
| VERDICT.md | β | 3.2 KB | β raw | |
| README.md | β | 2.2 KB | β raw |
DF-3160 β lost-wakeup window in fifo_open() reader/writer rendezvous
What
fifo_open() (sys/vfs/fifofs/fifo_vnops.c:295-335) implements the POSIX
FIFO open rendezvous (a blocking reader waits for a writer, a blocking
writer waits for a reader) with a check-then-sleep protocol that is not
interlocked: the condition (fip->fi_writers == 0 / fip->fi_readers == 0)
is tested under fifo_lock, then the thread drops fifo_lock, drops the
vnode lock, and only then calls tsleep(). A peer arriving in the gap
between the unlock and the sleep-queue insertion sends wakeup() to nobody
β a classic lost wakeup. There is also no re-check loop after wakeup
("We must have got woken up because we had a writer" β
fifo_vnops.c:305-310), unlike FreeBSD which restructured this to
while (...) { msleep(chan, vp->v_vnlock, ...) } with the vnode lock as
sleep interlock.
Consequence if hit
The blocked open() stays asleep until the next 0->1 transition of the
opposite counter, i.e. as long as the peer keeps its descriptor open. The
sleep is PCATCH so the thread remains killable; impact ceiling is a
per-thread hang (local DoS).
PoC
fifo_open_race.c β a reader process hammers
open(O_RDONLY|O_NONBLOCK)/close with periodic long holds, a writer process
measures open(O_WRONLY) latency. A genuine lost wakeup shows up as an
open() stall matching the reader's hold time.
Build / run
cc -O2 -Wall -o race fifo_open_race.c rm -f /tmp/df3160.f; mkfifo /tmp/df3160.f (./race r /tmp/df3160.f 2000 50 &) ./race w /tmp/df3160.f 170
Expected output if the bug manifests
LOST-WAKEUP-SUSPECT: open took <~hold-time> ms (iter N)
Actual result (see VERDICT.md)
NOT reproduced: ~7.7M writer opens across three hammer configurations (unloaded single pair; 4 loaded pairs with 1s holds; 4 loaded pairs with 2s holds) never produced a stall matching the hold time. Max observed latency was 202.8 ms under 8-process load (scheduling noise), vs 0.2 ms unloaded. The defect is structural (code-reading proof) but empirically the wakeup-sending path (peer's full open under fifo_lock including soisreconnected) is consistently slower than the sleeper's path into the sleep queue, so the window appears unhittable on this 6-vCPU guest.
DF-3160 VERDICT β not_reproduced (structural defect, empirically not hittable here)
Reproduced?
No. Three hammer runs on the audit QEMU guest (DragonFly 6.5-DEVELOPMENT #0, 6 vCPU, INVARIANTS stock kernel):
- Unloaded, 1 reader + 1 writer, 60 s:
writer done: 1455169 opens, 0 slow (>50ms), max 0.2 ms - 4 fifos, 4 readers (1 s hold every 8th cycle) + 4 writers, 170 s:
one writer:
693731 opens, 114 slow (>50ms), max 202.8 ms - 4 fifos, 4 readers (2 s hold every 50th cycle) + 4 writers, 170 s:
all four writers:
0 slow (>50ms), max 21.8 ms (4.8M opens total).
Why the run-2 stalls are NOT the lost wakeup
A genuine lost wakeup pins the writer until the reader's next 0->1
transition; with 1 s holds the stall distribution must reach ~1000 ms.
Every one of the 114 stalls was 68-203 ms β never near the hold time β
and run 3 (identical duty cycle, longer holds, more opens) produced zero.
They are fip-teardown/recreate (fifo_fip_create: two socreate +
unp_connect2 on every readers->0->1 cycle) plus scheduler noise under
8-process load.
Structural analysis (the defect is real in the code)
- Writer path:
fifo_vnops.c:319-325β checkfi_readers == 0under fifo_lock;fifo_unlock(vp); vn_unlock(vp); tsleep(&fip->fi_writers, PCATCH, ...). - The two lock releases and the
tsleepare not atomic w.r.t. the reader'swakeup(&fip->fi_writers)(fifo_vnops.c:276), and after the wakeup the condition is never re-checked (comment at :328-333 asserts the wakeup implies a reader). - FreeBSD's equivalent loops (
while (fip->fi_writers == 0)) and sleeps with the vnode lock as interlock (msleep(&fip->fi_readers, vp->v_vnlock, ...)), which closes the window. - DFly
tsleephas no built-in condition interlock (classic sleep-queue wakeup; no pending-bit).
Impact if it were hit
One blocked open() stalls until a future peer arrives; PCATCH makes it
killable. Ceiling: local DoS of one thread. Not memory corruption.
Recommended fix
Re-check the condition in a loop and interlock the sleep with a lock the waking side holds across its counter transition (vp->v_token is already taken by the io paths; the wakeup side would take it around the 0->1 transition + wakeup), e.g.:
while (... && fip->fi_writers == 0) {
tsleep_interlock(&fip->fi_readers, PCATCH);
fifo_unlock(vp);
vn_unlock(vp);
error = tsleep(&fip->fi_readers,
PCATCH | PINTERLOCKED, "fifoor", 0);
...
}
with the FREAD/FWRITE arrival paths in fifo_open sending their wakeup() while holding vp->v_token. (Sketch β a DFfly committer should pick the exact token; the essential part is interlock + loop.)
verdict.json summary
status=not_reproduced, reproduced=0, impact=none (no manifestation), confidence=speculative (code defect certain, exploitability not demonstrated), attempts=3, ~7.7M open() iterations.
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_testableConfirmed kernel references
Detail
Evidence (decisive lines)
['run.log: three hammer runs with per-run latency summaries', 'VERDICT.md: why the 68-203ms stalls in run 2 are noise, not the race', 'fifo_open_race.c: reader close/reopen + hold vs writer open() latency']
PoC changes
Authored from scratch: latency-probing writer (blocking open) vs reader cycling open/close with periodic holds; hold length is the discriminator (a genuine lost wakeup stalls for the remaining hold time).
Verified recommended fix
Loop on the rendezvous condition and interlock the tsleep (tsleep_interlock + PINTERLOCKED) with the waking side (fifo_open's 0->1 transitions) holding the interlock across its wakeup().
Verdict
Structural lost-wakeup defect in fifo_open()'s rendezvous (condition checked under fifo_lock, then two lock releases, then uninterrlocked tsleep; no re-check loop - fifo_vnops.c:295-335), but NOT reproduced: ~7.7M writer opens across three hammer configurations on the 6-vCPU INVARIANTS guest never produced a stall matching the reader's hold time (max 202.8 ms under 8-process load = scheduler/fip-recreate noise; unloaded max 0.2 ms). The wakeup-sending path (peer open under fifo_lock incl. soisreconnected) empirically always outlasts the sleeper's path into the sleep queue. Ceiling if hit: killable single-thread hang (PCATCH).
No comments yet.