# 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.
