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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3982 Β· 13 files
FileTypeDescriptionSize
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 from sbp_recv1:1605),
  • sbp_abort_ocb() sbp.c:2705 β€” same, via sbp_abort_all_ocbs from sbp_recv1 dead-agent handling (sbp.c:1663-1669 β†’ sbp_agent_reset sbp.c:1080) and from sbp_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.

VERDICT.md
↓ download raw

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

  1. sbp_recv1 runs in the fwohci hard-interrupt path under crit_enter() β€” chain: fwohci_intr (fwohci.c:2009) β†’ fwohci_intr_body (fwohci.c:2038, FWOHCI_TASKQUEUE=0 per fwohcivar.h:37) β†’ fwohci_arcv crit_enter() (fwohci.c:2688) β†’ fw_rcv (fwohci.c:2833) β†’ fw_xfer_done β†’ act.hand (firewire.c:991) β†’ sbp_recv crit_enter() (sbp.c:1823).

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

  3. DragonFly's current callout_stop() (kern_timeout.c:1091 β†’ _callout_cancel_or_stop(cc, CALLOUT_STOP, sync=1), kern_timeout.c:858) sleeps in ssleep(..., "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 of sbp_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 of sbp_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.

  4. 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 in D2 wkdelay was 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_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-3982

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.