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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3160 Β· 5 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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

  1. Unloaded, 1 reader + 1 writer, 60 s: writer done: 1455169 opens, 0 slow (>50ms), max 0.2 ms
  2. 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
  3. 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 β€” check fi_readers == 0 under fifo_lock; fifo_unlock(vp); vn_unlock(vp); tsleep(&fip->fi_writers, PCATCH, ...).
  • The two lock releases and the tsleep are not atomic w.r.t. the reader's wakeup(&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 tsleep has 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.

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_testable
per-fix-DF-3160

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