Lost-wakeup race in ppb_request_bus() can permanently hang a process opening a parallel port device
Summary
ppb_request_bus() checks ppb_owner inside crit section then exits crit section BEFORE calling tsleep(). On SMP another CPU can call ppb_release_bus() (sets ppb_owner=0 calls wakeup(ppb)) in window between crit_exit() and tsleep(). Wakeup delivered to thread not yet slept so lost. Requesting thread calls tsleep with timo=0 (infinite) hangs forever. With PPB_WAIT|PPB_NOINTR uninterruptible permanent hang open() never returns. PPB_WAIT|PPB_NOINTR path not interruptible by signals kill -9 cannot terminate. [VERIFIED pass2: LIVE CAUGHT at trial 16070 - waiter asleep with ppb_owner NULL, wakeup lost in the crit-exit tsleep window, PPB_WAIT unkillable; tsleep_interlock PINTERLOCKED fix-validated 0 lost in 50000 trials]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2329 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| ppbharness3.c | β | 13.1 KB | view raw | |
| Makefile | β | 484 B | β download | |
| run.log | β | 1.6 KB | view raw | |
| fix.diff | β | 900 B | view raw | |
| verdict.json | β | 2.8 KB | view raw | |
| README.md | β | 1.4 KB | β raw |
DF-2329 β lost-wakeup race in ppb_request_bus() permanently hangs a requester
What (sys/bus/ppbus/ppbconf.c:464-497 vs :508-540)
The requester checks ppb_owner inside a crit section, then EXITS the crit section BEFORE tsleep(). ppb_release_bus clears the owner and calls wakeup(ppb) (:538) with no interlock: a release landing between the requester's crit_exit() (:467) and tsleep() (:470/:474) is lost β the requester then sleeps on timo=0 with the PPB_WAIT|PPB_NOINTR variant being NON-INTERRUPTIBLE (no PCATCH: kill -9 cannot terminate).
Rig / Reproduce
Same ppbharness3 rig, test=3: an owner thread acquires (DONTWAIT), releases after a swept DELAY (0..399 us) while a waiter thread pinned to a second CPU (lwkt_migratecpu(1)) executes the real ppb_request_bus(PPB_WAIT|PPB_NOINTR); caught when the waiter stays asleep with ppb_owner == NULL.
Result (run.log)
hw.ppbx3.racetrials: 16071 hw.ppbx3.lostwake: 1 PPBX3: DF-2329 REPRODUCED at trial 16070 (delay 70 us): waiter STILL ASLEEP with PPB owner==NULL -- wakeup was lost; PPB_WAIT|PPB_NOINTR sleep is uninterruptible (kill-proof)
(The rig revived the waiter artificially; production has no reviver.)
Fix
fix.diff interlocks: tsleep_interlock(ppb, PCATCH) INSIDE the crit section + tsleep(... | PINTERLOCKED) after it. Validated on the patched rig (VERDICT.md): zero lost wakeups in 50k trials.
Fix verification
fixedConfirmed kernel references
Detail
Exploit chain
two threads on one ppbus -> owner releases between requester crit_exit (:467) and tsleep (:474) -> wakeup(ppb) delivered before the sleep is armed -> requester sleeps forever on a FREE bus -> PPB_WAIT|PPB_NOINTR: unkillable permanent hang
Evidence (decisive lines)
run.log: hw.ppbx3.lostwake = 1 at trial 16070/16071 with owner==NULL and the REPRODUCED console line; control attach + DF-2327/2328 stock panics on the same rig prove the production code is what executes
PoC changes
Authored the race test in ppbharness3; initial 0/44894 catch rate was a rig flaw (both kthreads on cpu 0: DELAY starved the waiter) -- fixed with lwkt_migratecpu(1) in the waiter; fast-poll drain added so 50k trials run in ~30 s.
Verified recommended fix
tsleep_interlock(ppb, PCATCH) inside the crit section + tsleep(...|PINTERLOCKED) (fix.diff, the sgread pattern).
Verdict
REPRODUCED live: the ppbharness3 race test runs the REAL ppb_request_bus(PPB_WAIT|PPB_NOINTR) on a waiter thread pinned to a second CPU while an owner thread releases inside the crit_exit-to-tsleep window (delay sweep 0..399 us): caught at trial 16070 -- the waiter was STILL ASLEEP with ppb_owner == NULL (the wakeup was lost; timo=0; the NOINTR variant has no PCATCH so the sleep is unkillable). The rig revived the waiter with an artificial wakeup; production has none, so the hang is permanent. The window is exactly the missing-interlock ordering at ppbconf.c:464-497 vs ppb_release_bus :538. Confidence likely rather than certain only because the catch is a scheduling race (1 in ~16 k trials here) while the code-ordering proof is certain.
No comments yet.