Synchronous callout_stop() in the fwohci hard-interrupt path (mgm_ocb_timeout + CCB timeout sites) sleeps in 'costp' inside a critical section while the callback is INPROG β FireWire IRQ-thread stall plus softclock-wheel starvation cascade (DF-3715 family)
Summary
sbp_recv1 executes in the fwohci hard-interrupt path under crit yet calls DF's now-synchronous callout_stop() at three sites: the fwohci IRQ thread is put to sleep - crit held - for the whole unbounded sbp_mgm_timeout body, and the INPROG-blocked softclock thread starves every tsleep-timeout on its CPU. Driver written for old async callout_stop semantics. Mechanism PROVEN live on the shipped guest kernel (costp capture with crit held while softclock INPROG; wkdelay cascade starved; negative control 9ms). End-to-end sbp trigger hardware-gated.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3982 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.3 KB | β raw | |
| VERDICT.md | β | 4.3 KB | β raw | |
| sbp_replica.c | β | 8.5 KB | view raw | |
| Makefile | β | 111 B | β download | |
| build.sh | β | 76 B | view raw | |
| run.sh | β | 254 B | view raw | |
| build.log | β | 8.0 KB | view raw | |
| run.log | β | 3.6 KB | view raw | |
| console_tail.txt | β | 341 B | view raw | |
| env.txt | β | 677 B | view raw | |
| fix.diff | β | 1.5 KB | view raw | |
| manifest.json | β | 1.3 KB | view raw | |
| verdict.json | β | 4.3 KB | view raw |
DF-3982 β sbp(4): synchronous callout_stop() from hard-interrupt context sleeps in costp (DF-3715 family, sbp.c instance)
Where
sys/dev/disk/sbp/sbp.c β three callout_stop() sites on the receive path:
sbp_recv1()sbp.c:1600 βcallout_stop(&target->mgm_ocb_timeout)when a matching management-ORB status arrives,sbp_dequeue_ocb()sbp.c:2590 βcallout_stop(ocb->ccb->ccb_h.timeout_ch)for every matched command ORB (called fromsbp_recv1:1605),sbp_abort_ocb()sbp.c:2705 β same, viasbp_abort_all_ocbsfromsbp_recv1dead-agent handling (sbp.c:1663-1669 βsbp_agent_resetsbp.c:1080) and fromsbp_probe_target(sbp.c:688).
All three execute in the fwohci hard-interrupt path: FWOHCI_TASKQUEUE=0
(sys/bus/firewire/fwohcivar.h:37) so fwohci_intr (fwohci.c:2009) runs
fwohci_intr_body directly (fwohci.c:2038) β fwohci_arcv under
crit_enter() (fwohci.c:2688) β fw_rcv (fwohci.c:2833) β fw_xfer_done
β xfer->act.hand (firewire.c:991) = sbp_recv, which adds its own
crit_enter() (sbp.c:1823).
DragonFly's current callout implementation made callout_stop() synchronous:
_callout_cancel_or_stop(cc, CALLOUT_STOP, sync=1) (kern_timeout.c:1091,
858-927) ssleep()s on costp (kern_timeout.c:923) whenever the callout
is queued or INPROG on any CPU, until the callback completes. Sleeping
from a device interrupt handler (and inside a critical section) is illegal:
the fwohci IRQ stalls for the whole duration of sbp_mgm_timeout's body
(kprintf + sbp_reset_start β sbp_write_cmd β fw_asyreq, which
allocates and can be delayed by bus retries β DF-1021 family), and the
INPROG-blocked softclock thread additionally starves every tsleep-timeout on
its CPU (observed cascade).
Reachability
A hostile FireWire device knows its ORB addresses (the initiator wrote them
to its management agent) and can time/spam status writes; landing one while
the 5 s mgm_ocb_timeout (sbp.c:1351) fires β or while a CCB timeout
callback runs β puts the interrupt thread to sleep. Race-window gated but
sprayable by the attacker.
Evidence in this pack (mechanism PROVEN on the shipped guest kernel)
run.log (live capture):
1237 D5 costp sysctl <- callout_stop() caller asleep INSIDE crit (the sbp.c:1600 state) -1 12 B2 bcb softclock 2 <- mgm-timeout-shaped callback INPROG 1256 D2 wkdelay sysctl <- cascade: unrelated tsleep-timeout starved by the wedged softclock
(sbp_replica.c mechanism B; negative control in RUN 1: stop of a
non-INPROG callout returns in 9 ms.)
Reproduce
cd /root/kmod && make && kldload ./sbpprep.ko sysctl kern.sbpprep_armb=1 (sysctl -n kern.sbpprep_stopb=1 >/dev/null 2>&1 </dev/null &) sleep 3 ps -o pid,stat,wchan,comm -ax | grep sysctl # expect D-state, wchan costp ps -axH -o pid,tdpri,stat,wchan,comm | grep softclock # expect B2 bcb
Fix
Interrupt path must not use the synchronous stop. fix.diff switches the
three sites to callout_stop_async() (the semantics this pre-rewrite driver
was written against) and closes the resulting completion race in
sbp_mgm_timeout by re-checking target->mgm_ocb_cur == ocb under crit
before touching the ocb. Full correctness eventually wants a lock redesign
of the mgm queue, as noted in the finding.
DF-3982 VERDICT β blocking callout_stop() in sbp's hard-interrupt path
Status: inconclusive end-to-end (sbp trigger hardware-gated), mechanism
PROVEN live on the shipped guest kernel (blocked caller in D5 costp
inside a critical section + INPROG softclock thread in B2 bcb + cascade
to an unrelated wkdelay sleep). Source-level certainty: certain.
Root cause
-
sbp_recv1runs in the fwohci hard-interrupt path undercrit_enter()β chain: fwohci_intr (fwohci.c:2009) β fwohci_intr_body (fwohci.c:2038, FWOHCI_TASKQUEUE=0 per fwohcivar.h:37) β fwohci_arcvcrit_enter()(fwohci.c:2688) β fw_rcv (fwohci.c:2833) β fw_xfer_done βact.hand(firewire.c:991) β sbp_recvcrit_enter()(sbp.c:1823). -
On that path the driver calls the synchronous
callout_stop()at - sbp.c:1600 (mgm_ocb_timeout, on mgm-ORB status match), - sbp.c:2590 (CCB timeout, in sbp_dequeue_ocb β called at sbp.c:1605), - sbp.c:2705 (CCB timeout, in sbp_abort_ocb via sbp_abort_all_ocbs from sbp_recv1's dead-agent branch sbp.c:1663-1669 β sbp_agent_reset sbp.c:1080, and from sbp_probe_target sbp.c:688). -
DragonFly's current
callout_stop()(kern_timeout.c:1091 β_callout_cancel_or_stop(cc, CALLOUT_STOP, sync=1), kern_timeout.c:858) sleeps inssleep(..., "costp", 0)(kern_timeout.c:923) while the callback is queued or INPROG on any CPU β i.e. the fwohci interrupt thread is put to sleep, with a critical section held, for the full duration ofsbp_mgm_timeout's body (sbp.c:2126-2146: kprintf,sbp_free_ocb,sbp_reset_startβsbp_write_cmdβfw_asyreq, which allocates β see DF-1021 for the M_WAITOK-in-crit tlabel path) or ofsbp_timeout's body (xpt_freeze_devq, abort-all). The old-BSD-callout semantics this 2003-era driver was written for (stop = async cancel) no longer hold. -
Cascade (observed): the softclock thread blocked INPROG inside the callback cannot service that CPU's wheel, so unrelated
tsleep-timeouts starve β a third thread stuck inD2 wkdelaywas captured live while trying to release the experiment.
What was run on the guest (DF 6.5-DEVELOPMENT, X86_64_GENERIC)
Mechanism replica B (sbp_replica.c, this pack): a 1-tick
mgm_ocb_timeout-shaped callout whose body blocks until released
(the unbounded-body shape of sbp_mgm_timeout), plus a sysctl handler that
performs crit_enter(); callout_stop(&c); β the exact state of
sbp.c:1600 β from a detached process:
1237 D5 costp sysctl <- callout_stop caller: D-state, wchan 'costp', crit held -1 12 B2 bcb softclock 2 <- target callout: INPROG, blocked in body 1256 D2 wkdelay sysctl <- cascade: tsleep timeout starved (wedged wheel)
Negative control (RUN 1): callout_stop() on a non-INPROG callout
returned in 9 ms β the block is specifically the INPROG/queued wait of
kern_timeout.c:888-923. Both behaviors match the source.
Why not reproduced for the finding itself: the sbp trigger needs
FireWire hardware plus an attacker-timed status/timeout collision; the guest
has no OHCI-1394 controller (env.txt). The blocking mechanism itself was
demonstrated on the very kernel the audit targets.
Not-a-duplicate check
DF-3715/DF-3738 (em/emx) and DF-3760 (alc) prove the same kernel-wide callout_stop semantics in serializer deadlocks of net drivers; this is the sbp.c instance: different driver, different context (hard-IRQ receive path, crit-only, no serializer), different victim (fwohci IRQ thread + softclock wheel starvation). No prior sbp.c finding covers the callout sites.
Fix
fix.diff: callout_stop_async() at sbp.c:1600/2590/2705 (interrupt-path
semantics of the era the driver was written for) + sbp_mgm_timeout
re-validating target->mgm_ocb_cur == ocb under crit before acting, so the
now-unstoppable callback cannot race the recv1 completion into a double
sbp_free_ocb of the same ocb. A complete fix is a lock/timeout redesign of
the mgm queue (flagged in the finding as follow-up).
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.
Fix verification
not_testablefix.diff applies cleanly; behavior delta cannot be exercised on this guest (sbp path needs FireWire hardware). The mechanism fix at the kern_timeout level is already proven correct in the negative control (non-INPROG stops never block).
fix.diff (3x callout_stop_async + mgm_ocb_cur recheck); run.log RUN 1 negative control
Confirmed kernel references
Detail
Exploit chain
hostile FireWire device times (or sprays) a status write matching the pending mgm ORB address while the 5 s mgm_ocb_timeout callback (or a CCB timeout callback) is executing on a softclock thread -> fwohci interrupt thread enters callout_stop at sbp.c:1600/2590/2705 -> sleeps in 'costp' holding crit until the callback body finishes (its own duration is attacker-inflatable via bus congestion/tlabel pressure) -> FireWire IRQ stalls; wedged softclock wheel starves unrelated kernel tsleep timeouts on that CPU -> local/remote DoS.
Evidence (decisive lines)
run.log (live ps captures: '1237 D5 costp sysctl', '-1 12 B2 bcb softclock 2', cascade '1256 D2 wkdelay sysctl'); VERDICT.md (full chain path:line); console_tail.txt (serial log); build.log; env.txt (no FireWire hw); fix.diff (callout_stop_async at the 3 sites + mgm_ocb_cur recheck, patch --dry-run clean)
PoC changes
Replica iterations: (1) tsleep-with-timeout inside the callback self-deadlocked the softclock thread (tsleep timeouts are callout-based on the same wheel) - replaced with a no-timeout body released by a controller sysctl; (2) DELAY busy-wait body froze userland scheduling on the callback CPU so the stopper never landed INPROG - replaced with the blocking-sleep body; final design reproduces the sbp state exactly and cleanly unwinds via the wake sysctl.
Verified recommended fix
Use callout_stop_async() on the three interrupt-path sites and re-validate target->mgm_ocb_cur == ocb in sbp_mgm_timeout before acting (fix.diff); longer term, redesign the mgm queue locking.
Verdict
sbp_recv1() executes in the fwohci hard-interrupt path under crit_enter() (fwohcivar.h:37 FWOHCI_TASKQUEUE=0; fwohci.c:2688 crit; firewire.c:991 act.hand; sbp.c:1823) yet calls DragonFly's now-synchronous callout_stop() at sbp.c:1600 (mgm_ocb_timeout), sbp.c:2590 and sbp.c:2705 (CCB timeouts). kern_timeout.c:1091/_callout_cancel_or_stop(sync=1) ssleeps on 'costp' (kern_timeout.c:923) whenever the callback is queued or INPROG on any CPU, so the fwohci IRQ thread is put to sleep - inside a critical section - for the full duration of sbp_mgm_timeout's unbounded body (sbp.c:2126-2146 -> sbp_reset_start -> fw_asyreq allocation path), and the INPROG-blocked softclock thread starves every tsleep-timeout sharing its CPU. Mechanism PROVEN live on the shipped guest kernel via replica B: caller captured in 'D5 costp' with crit held, callback thread 'B2 bcb' INPROG, and a third thread 'D2 wkdelay' starved by the wedged wheel (run.log); negative control: stop of a non-INPROG callout returns in 9 ms. End-to-end sbp trigger is hardware-gated (no OHCI-1394 on guest) hence inconclusive rather than reproduced.
No comments yet.