hpt_timeout (takes driver_lock) vs callout_stop under driver_lock: DragonFly's synchronous callout_stop creates an AB-BA sleep deadlock - permanent storage wedge
Summary
Each hptmv I/O arms a 20s callout whose handler first takes driver_lock; command completion calls the synchronous callout_stop on that callout while holding driver_lock. If the timeout fires concurrently with a completion, CPU0 sleeps forever in 'costp' holding driver_lock while CPU1 sleeps forever acquiring it - the controller, its DPC worker and (because the SIM shares &sim_mplock) potentially all legacy-locked CAM I/O wedge permanently. Rare per-command race, deterministic-once-hit. LK_NOWAIT+requeue fix (the DF-3715 family's lockmgr variant).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3922 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.2 KB | β raw | |
| VERDICT.md | β | 2.1 KB | β raw | |
| fix.diff | β | 569 B | view raw | |
| env.txt | β | 1.2 KB | view raw | |
| manifest.json | β | 934 B | view raw | |
| verdict.json | β | 2.1 KB | view raw |
DF-3922 β hptmv 20 s command timeout vs completion: AB-BA wedge (callout_stop sleeping while holding driver_lock while hpt_timeout waits for that lock)
What
Every hptmv I/O arms a 20 s callout on the CCB's own callout handle:
- sys/dev/raid/hptmv/entry.c:2669 (hpt_io_dmamap_callback) and
entry.c:2865-2866 (OsSendCommand, CAM_SCATTER_VALID path):
callout_reset(ccb->ccb_h.timeout_ch, 20*hz, hpt_timeout, ccb);
- hpt_timeout (entry.c:2625-2632) first takes driver_lock:
lock_driver(); fResetVBus(_VBUS_P0); unlock_driver();
- completion runs under driver_lock (all CheckPendingCall sites:
hpt_intr:2123-2132, hpt_action:2273-2286, worker thread 2408-2428) and
calls callout_stop(ccb->ccb_h.timeout_ch) from fOsCommandDone
(entry.c:2910).
On DragonFly, callout_stop() is the synchronous variant:
sys/kern/kern_timeout.c:1091-1094 β _callout_cancel_or_stop(cc, CALLOUT_STOP, 1),
which at kern_timeout.c:910-921 sleeps in ssleep(c, &c->spin, 0, "costp", 0)
with no timeout until the in-progress callback returns.
Deadlock
If the 20 s callout begins executing (callback INPROG, not yet through
lock_driver()) at the instant a completion path holding driver_lock
reaches callout_stop():
- CPU0: holds driver_lock, sleeps forever in costp waiting for the
callout to finish;
- CPU1: hpt_timeout blocks forever in lockmgr(&driver_lock, LK_EXCLUSIVE);
- driver_lock is never released β every later hptmv entry (ioctl, CAM I/O,
DPC worker, ISR via ithread) blocks on it β controller and its CAM devq
wedge permanently. hpt_action is entered under sim_mplock
(entry.c:2046-2047 passes &sim_mplock to cam_sim_alloc), so the giant
CAM MP-lock is also held by any thread blocked entering this SIM - a
system-wide storage wedge, not just one card.
Classic timeout-handler/lock-order inversion (CWE-667); the standard fix in SCSI LLDDs is to not call the synchronous stop under the lock, or to make the timeout handler take the lock via trylock/requeue.
Reproduce (needs RR1820 hardware + long-lived IO + timing luck)
Sustained I/O with one command completing within the microseconds around its 20-second timeout. Race window is per-command tiny; over months of uptime on a busy controller it is a plausible wedge. Cannot be triggered on the audit guest (no hardware).
Fix
--- a/sys/dev/raid/hptmv/entry.c
+++ b/sys/dev/raid/hptmv/entry.c
@@ -2626,7 +2626,15 @@ hpt_timeout(void *arg)
static void
hpt_timeout(void *arg)
{
- _VBUS_INST(&((PBUS_DMAMAP)((union ccb *)arg)->ccb_adapter)->pAdapter->VBus)
- lock_driver();
- fResetVBus(_VBUS_P0);
- unlock_driver();
+ /* must not sleep waiting for driver_lock while a completion path may
+ * be sleeping in callout_stop() holding it (AB-BA wedge). Requeue
+ * until the lock is free; fOsCommandDone's callout_stop cancels us. */
+ for (;;) {
+ _VBUS_INST(&((PBUS_DMAMAP)((union ccb *)arg)->ccb_adapter)->pAdapter->VBus)
+ if (lockmgr(&driver_lock, LK_EXCLUSIVE | LK_NOWAIT) == 0)
+ break;
+ callout_reset(((union ccb *)arg)->ccb_h.timeout_ch, hz, hpt_timeout, arg);
+ return;
+ }
+ fResetVBus(_VBUS_P0);
+ unlock_driver();
}
(see fix.diff for the exact tree-accurate hunk)
DF-3922 VERDICT
status: not_reproduced (hardware-gated race) | impact observed: none (potential: permanent kernel/storage wedge) | source certainty: likely
Mechanics verified at source level
callout_stop()on DragonFly is synchronous: kern_timeout.c:1091-1094 passes sync=1; _callout_cancel_or_stop (kern_timeout.c:859-930) at :910-921 sleeps inssleep(..., "costp", 0)β no timeout β while the callback is CALLOUT_INPROG.hpt_timeout(entry.c:2625-2632) executeslock_driver()as its first action and only then resets the bus.fOsCommandDone(entry.c:2910) callscallout_stop(ccb->ccb_h.timeout_ch)while running underdriver_lock(every CheckPendingCall/DPC execution context in this driver holds it: hpt_intr 2123-2132, hpt_action 2273-2286, worker 2408-2428).- => if the callout fires concurrently with a completion holding the lock, CPU0 sleeps in "costp" holding driver_lock, CPU1 sleeps in lockmgr waiting for driver_lock. Neither has a timeout. controller + DPC worker + all CAM entries wedge; because the SIM uses &sim_mplock (entry.c:2047), the giant CAM MP lock serializes other legacy SIMs against blocked entrants.
Why "likely" and not "certain"
The window requires the 20 s callout to begin executing in the exact microseconds a completion under driver_lock reaches callout_stop. The mechanism is certain (verified code), the manifestation is a rare race β hence confidence "likely". Hardware-gated: cannot be raced on the audit guest (no RR1820).
Note
hptmv also reuses the CCB's CAM-owned callout (ccb_h.timeout_ch) for its private timeout β CAM allocates/frees this callout (cam_xpt.c:4934-4945); the stop/start dance here is the only synchronization, which is what makes the synchronous-stop semantics load-bearing for correctness.
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 converts hpt_timeout to LK_NOWAIT+requeue; cannot validate without hardware
fix.diff in this pack; never applied to sys/
Confirmed kernel references
Detail
Exploit chain
sustained IO on RR1820 -> command completion racing its 20 s timeout -> AB-BA sleep deadlock -> unkillable storage wedge (DoS)
Evidence (decisive lines)
VERDICT.md: lock/callout order trace with kern_timeout.c:920 ssleep and entry.c:2629/2910
PoC changes
n/a (fresh pack)
Verified recommended fix
hpt_timeout must not sleep on driver_lock (LK_NOWAIT + requeue), removing the AB-BA against callout_stop under the lock
Verdict
Mechanics certain, manifestation racy. DragonFly callout_stop() sleeps without timeout waiting for an in-progress callback (kern_timeout.c:910-921). hpt_timeout's first action is lock_driver() (entry.c:2629); fOsCommandDone calls callout_stop on the same callout while holding driver_lock (entry.c:2910, all completion contexts hold it). Concurrent fire => CPU0 sleeps in 'costp' holding driver_lock, CPU1 sleeps acquiring it: permanent wedge of the controller, its DPC worker and - because the SIM shares &sim_mplock (entry.c:2047) - potentially all legacy-locked CAM I/O. Not reproducible on the audit guest (no RR1820 hardware).
No comments yet.