cm_callout is callout_init_mp (lock-free) but mpssas_scsiio_timeout asserts and manipulates mps_lock-protected state: deterministic KKASSERT panic on the first SCSI IO timeout (stock INVARIANTS/GENERIC kernels), lock-free race with interrupt completion otherwise
Summary
Every mps command's callout is initialized with callout_init_mp - DragonFly's lock-free MP-safe callout (softclock executes the callback with no lock held). The callback armed for every SCSI IO is mpssas_scsiio_timeout, whose second statement is KKASSERT(lockowned(&sc->mps_lock)) and which then calls mps_intr_locked() lock-free and mutates cm_state/timedout_commands/targ->tm. On stock kernels (X86_64_GENERIC ships INVARIANTS; KKASSERT is a real panic) the first SCSI IO timeout on any mps adapter deterministically panics. On non-INVARIANTS builds the callback body races the interrupt thread's lock-held completion paths with list-corruption/UAF potential. The sibling mpr driver does this correctly (callout_init_lk) - mps_sas.c was written in the same assert style but its callout was never lock-tied. Unprivileged local user performing IO to an mps disk that stalls past its CCB timeout: one ordinary timeout = kernel panic on every default install with this HBA. Lock-tie fix mirroring mpr.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3892 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.9 KB | β raw | |
| VERDICT.md | β | 4.8 KB | β raw | |
| run.sh | β | 111 B | view raw | |
| run.log | β | 482 B | view raw | |
| env.txt | β | 274 B | view raw | |
| fix.diff | β | 1.4 KB | view raw | |
| verdict.json | β | 3.0 KB | view raw | |
| manifest.json | β | 782 B | view raw |
DF-3892 β mpssas_scsiio_timeout runs lock-free (callout_init_mp) on state owned by mps_lock: guaranteed KKASSERT panic on the first SCSI IO timeout (INVARIANTS/GENERIC), unsynchronized race on stripped kernels
File: sys/dev/raid/mps/mps_sas.c (pass 2)
What
struct mps_command.cm_callout is initialized with callout_init_mp()
(sys/dev/raid/mps/mps.c:860) β DragonFly's lock-free MP-safe callout
(sys/kern/kern_timeout.c:767-771 sets CALLOUT_MPSAFE, and softclock then
executes the callback with no lock held, kern_timeout.c:626-631; only
callout_init_lk()/CALLOUT_AUTOLOCK holds a lock across the callback,
kern_timeout.c:609-625).
But the callback armed on that callout for every SCSI IO
(callout_reset(&cm->cm_callout, ..., mpssas_scsiio_timeout, cm) at
sys/dev/raid/mps/mps_sas.c:1768) is written as if it ran under
sc->mps_lock:
static void
mpssas_scsiio_timeout(void *data) /* mps_sas.c:1481 */
{
...
cm = (struct mps_command *)data;
sc = cm->cm_sc;
KKASSERT(lockowned(&sc->mps_lock)); /* mps_sas.c:1490 β FALSE here */
...
mps_intr_locked(sc); /* mps_sas.c:1499 β lock-free! */
Every field it then touches (cm->cm_state, cm->cm_ccb,
targ->timedout_commands, targ->tm) is otherwise exclusively protected by
mps_lock, and mps_intr_locked() itself is a lock-held-context-only
function (all its callers wrap it in mps_lock/mps_unlock,
e.g. sys/dev/raid/mps/mps.c:1450-1452, 1467-1469).
Impact
-
Stock DragonFly GENERIC (X86_64_GENERIC ships
options INVARIANTS, sys/config/X86_64_GENERIC:56, and the guest kernel is built from it):KKASSERTis live (sys/sys/systm.h:98-101). The first SCSI IO timeout on any mps(4) adapter fires the callback from softclock without the lock β deterministicpanic("assertion \"lockowned(&sc->mps_lock)\" failed in mpssas_scsiio_timeout"). A single slow/hung/recovering disk (cable glitch, error recovery, spin-up under load) converts an ordinary CAM timeout into a kernel panic. -
Non-INVARIANTS builds: the KKASSERT is a no-op and the entire callback runs concurrently with the interrupt thread's lock-held completion paths: the lock-free
mps_intr_locked()races the real interrupt handler oversc->replypostindex, reply descriptors and command completion (double completion of one reply β doublexpt_done/double free), and theTAILQ_INSERT_TAIL(&targ->timedout_commands, ...)at mps_sas.c:1522 racesmpssas_scsiio_complete'sTAILQ_REMOVEat mps_sas.c:1826 β list corruption / use-after-free. (DragonFly'scallout_stop()is synchronous (_callout_cancel_or_stop(..., sync=1), sys/kern/kern_timeout.c), so the completion side does wait for an already-running callback; the corruption vector is the callback body racing other mps_lock-protected state, which callout_stop cannot help.)
The sibling driver got this right: sys/dev/raid/mpr/mpr.c:1539 uses
callout_init_lk(&cm->cm_callout, &sc->mpr_lock) and
mprsas_scsiio_timeout/mprsas_tm_timeout assert (never take) the lock.
The mps port kept the assert-style callback but not the lock-tied callout.
Trigger
Unprivileged local user performs IO to an mps(4) disk that stalls past the CCB timeout (failing device, error recovery, heavy load during link retraining). No privilege needed beyond ordinary disk access; the timeout itself is the trigger. Hardware-gated on an LSI SAS2008+ HBA being present.
Build/run
Nothing compilable without the HBA; run.sh records the guest hardware
gate (driver compiled in β pci/mps β but no SAS2 PCI device, SIM never
attaches). Source-level certainty is total (see VERDICT.md).
Fix
callout_init_lk(&cm->cm_callout, &sc->mps_lock) (mirror mpr.c:1539) and
make mpssas_tm_timeout assert the lock instead of taking it (an AUTOLOCK
callout already holds it; taking mps_lock again would self-deadlock).
See fix.diff.
DF-3892 β VERDICT
Status: not_reproduced (hardware-gated) β defect certain from source. Impact class (on real hardware): kernel panic / DoS; race-based memory corruption on non-INVARIANTS builds.
Why it is real (path:line)
sys/dev/raid/mps/mps.c:860βcallout_init_mp(&cm->cm_callout);for every command in the pool._callout_init_mpsets onlyCALLOUT_DID_INIT|CALLOUT_MPSAFE(sys/kern/kern_timeout.c:767-771).sys/dev/raid/mps/mps_sas.c:1768-1769β everyXPT_SCSI_IOarmsmpssas_scsiio_timeouton that callout.sys/kern/kern_timeout.c:626-631β aCALLOUT_MPSAFEcallback is executed as the "Normal call":c->qfunc(c->qarg);with no lock acquired. OnlyCALLOUT_AUTOLOCK(fromcallout_init_lk, kern_timeout.c:609-625) wraps the callback inlockmgr(c->lk, ...).sys/dev/raid/mps/mps_sas.c:1490β the callback's second statement isKKASSERT(lockowned(&sc->mps_lock));. In softclock context this is always false.sys/sys/systm.h:94-101βKKASSERTis a realpanic()wheneverINVARIANTSis configured, and DragonFly's stocksys/config/X86_64_GENERICincludesoptions INVARIANTS(line 56); the audit guest kernel (and every default DF install) has it live. β first SCSI IO timeout on mps(4) = deterministic panic:mpssas_scsiio_timeoutis entered,cm->cm_scis read, the assert fires.- On builds without INVARIANTS the same callback body runs concurrently
with lock-held paths:
-
mps_sas.c:1499callsmps_intr_locked(sc)lock-free while the interrupt thread runs the identical function undermps_lock(mps.c:1450-1452): both advancesc->replypostindex, consume reply descriptors and run completions β double completion of one reply. -mps_sas.c:1520-1522mutatescm->cm_ccb->ccb_h.status,cm->cm_state, andTAILQ_INSERT_TAIL(&targ->timedout_commands, ...)whilempssas_scsiio_complete(mps_sas.c:1800-1826, under the lock) doescallout_stop+TAILQ_REMOVE(&targ->timedout_commands, ...)mps_free_commandβ TAILQ corruption / use-after-free of the command and its CCB. Note DragonFly'scallout_stop()is synchronous (sys/kern/kern_timeout.c:callout_stopβ_callout_cancel_or_stop(cc, CALLOUT_STOP, 1); waiters loop until the request is honored), so the completion side does wait out an already-entered callback β the corruption vector is strictly the callback's own unsynchronized accesses, which is why the INVARIANTS panic (point 5) is the practically certain manifestation.
- Reference implementation proving intent: the sibling mpr driver
initializes
callout_init_lk(&cm->cm_callout, &sc->mpr_lock)(sys/dev/raid/mpr/mpr.c:1539) and itsmprsas_scsiio_timeout/mprsas_tm_timeoutonly assert lock ownership (sys/dev/raid/mpr/mpr_sas.c:1672). mps_sas.c was written in the same assert style (KKASSERT(lockowned(...))also at mps_sas.c:901, 1568, 1801, 2762, 2797) but its command callouts were never lock-tied.mpssas_tm_timeout(mps_sas.c:1124-1133) even takesmps_lock()itself β correct for a lock-free callout and further proof the two timeout handlers were written against inconsistent assumptions.
Why it was not reproduced on this guest
run.sh / env.txt: the guest kernel contains the mps driver
(kldstat -v shows 143 pci/mps) but QEMU provides no LSI SAS2 PCI
device, so mps_attach never runs, no CAM SIM registers, and no mps
command (hence no cm_callout timeout) can ever be armed. Reproduction
requires: real SAS2 HBA + a device IO that exceeds its CCB timeout
(e.g. dd against a da(4) LUN while the target is stalled), expected
result on a GENERIC (INVARIANTS) kernel:
panic: assertion "lockowned(&sc->mps_lock)" failed in mpssas_scsiio_timeout.
Exploitability
Panic is the guaranteed outcome on stock kernels (A:C high for an attacker
with a way to stall a disk; DoS). On custom non-INVARIANTS kernels the
callback/interrupt race is a genuine memory-corruption primitive
(list corruption of timedout_commands, double completion), but it is
narrow and hardware-gated; uid=0 escalation was not pursued because the
guest cannot attach the hardware at all.
Fix validation
Not testable on this guest (no HBA). fix.diff mirrors the mpr(4)
implementation exactly (callout_init_lk + assert-style mpssas_tm_timeout);
it compiles by inspection against the same constructs used in mpr.c.
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 the mpr(4) reference pattern; cannot be exercised on this guest (no SAS2 HBA).
findings/poc/DF-3892/fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md (path:line proof incl. kern_timeout.c callback dispatch and GENERIC INVARIANTS), env.txt (no SAS2 HBA, mps unattached), run.sh + run.log (hardware gate), fix.diff (callout_init_lk + assert-style tm_timeout, mirrors mpr.c:1539)
PoC changes
No userland PoC can reach the driver without the HBA; run.sh records the hardware gate per the DF-3801 precedent.
Verified recommended fix
callout_init_lk(&cm->cm_callout, &sc->mps_lock) in mps.c (mirror mpr.c:1539) and convert mpssas_tm_timeout to assert the lock instead of taking it.
Verdict
cm_callout is initialized with callout_init_mp (sys/dev/raid/mps/mps.c:860) β DragonFly's lock-free MP-safe callout (kern_timeout.c:626-631 runs the callback with no lock) β but the callback armed on it for every SCSI IO (mps_sas.c:1768) is mpssas_scsiio_timeout, whose second statement is KKASSERT(lockowned(&sc->mps_lock)) (mps_sas.c:1490) and which then runs mps_intr_locked() lock-free (mps_sas.c:1499). On every stock DragonFly kernel (X86_64_GENERIC ships options INVARIANTS, and KKASSERT is a real panic, sys/sys/systm.h:98-101) the FIRST SCSI IO timeout on an mps(4) adapter deterministically panics the kernel; on non-INVARIANTS builds the callback races the interrupt thread's lock-held completion paths (replypostindex/timedout_commands/command free) with list-corruption/UAF potential. The sibling mpr(4) driver uses callout_init_lk(&cm->cm_callout, &sc->mpr_lock) (mpr.c:1539) with assert-style timeout callbacks β proving mps_sas.c was written against the lock-tied discipline but initialized the callout wrong. Not reproduced on the guest: mps driver is compiled in (pci/mps in kldstat -v) but QEMU has no LSI SAS2 PCI device, so the SIM never attaches and no command timeout can be armed; defect certain from source.
No comments yet.