mpt_timeout() uses the CCB after xpt_done() hands it back β callout_stop() does not interlock against the in-progress timeout callback (freed/recycled CCB -> wild deref or wrong-request abort)
Summary
mpt_req_untimeout is plain callout_stop() on a callout_init_mp() callout, which does not block / does not interlock against an in-progress callback. mpt_timeout reads ccb->ccb_h.ccb_mpt_ptr before the lock and ccb->ccb_h.ccb_req_ptr after acquiring it; if the callback already fired and is blocked on the mpt lock while the completion path recycles (pooled XPT_SCSI_IO CCBs) or kfree's (zone CTIO CCBs) the CCB, the callback resumes reading freed memory: garbage req pointer -> req->serno wild read -> panic; stale pointer -> TAILQ move of an innocent live request to the timeout list -> spurious abort of an unrelated command. Window user-amplifiable via pass-supplied ccb_h.timeout=1ms. Operator-group local user with a pass node, under I/O load with short timeouts: race-window kernel panic (DoS); list-corruption double-remove panics. Fix: arm the callout with the pool-resident request_t instead of the CCB + state re-validation under lock.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3673 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 958 B | β raw | |
| VERDICT.md | β | 5.0 KB | β raw | |
| mpt_timeout_race.c | β | 3.2 KB | view raw | |
| build.sh | β | 99 B | view raw | |
| run.sh | β | 150 B | view raw | |
| env.txt | β | 281 B | view raw | |
| run.log | β | 281 B | view raw | |
| fix.diff | β | 3.7 KB | view raw | |
| verdict.json | β | 3.6 KB | view raw | |
| manifest.json | β | 854 B | view raw |
DF-3673 β mpt_timeout() uses the CCB after xpt_done() hands it back
Severity: Medium (memcorrupt) β source-provable race; runtime-gated on MPT hardware.
Build / Run (host with an mpt-attached LUN)
cc -O -o mpt_timeout_race mpt_timeout_race.c ./mpt_timeout_race /dev/passN
Hammer the device with 1 ms-timeout READ(6)s; every completion that lands in
the callout-callback window makes mpt_timeout() re-enter with a freed /
recycled CCB (mpt_cam.c:1240-1252 vs completion at :2662/:2710).
Expected on vulnerable kernel under load: kernel panic (wild req pointer
read via req->serno), or spurious abort of an unrelated command with
pending/timeout-list confusion.
Why this guest cannot run it
Same as DF-3672: mpt is compiled in (X86_64_GENERIC:93) but the QEMU guest has no LSI PCI device β no mpt bus, no I/O, no callouts armed by this driver.
See VERDICT.md for the full interleaving proof and env.txt for the probe.
DF-3673 β VERDICT
status: inconclusive (execution-gated: no MPT hardware on the audit guest; source-level certainty: likely β race is provable, timing-dependent)
Root cause
mpt_req_untimeout() is callout_stop() (sys/dev/disk/mpt/mpt.h:769-772)
and mpt callouts are initialized callout_init_mp() (mpt.h:774-775,
called from mpt.c:2703). For non-lock-aided callouts, DragonFly's
callout_stop() "does not block" and "does not interlock against a callout
that is in-progress" (sys/kern/kern_timeout.c:1073-1096).
The timeout callback:
/* sys/dev/disk/mpt/mpt_cam.c:1234-1254 */
static void
mpt_timeout(void *arg)
{
union ccb *ccb = (union ccb *)arg;
struct mpt_softc *mpt;
mpt = ccb->ccb_h.ccb_mpt_ptr; /* read BEFORE the lock */
MPT_LOCK(mpt); /* blocks if intr holds lock */
req = ccb->ccb_h.ccb_req_ptr; /* read AFTER ccb may be freed */
mpt_prt(mpt, "request %p:%u timed out ...", req, req->serno, ...);
if ((req->state & REQ_STATE_QUEUED) == REQ_STATE_QUEUED) {
TAILQ_REMOVE(&mpt->request_pending_list, req, links);
TAILQ_INSERT_TAIL(&mpt->request_timeout_list, req, links);
req->state |= REQ_STATE_TIMEDOUT;
mpt_wakeup_recovery_thread(mpt);
}
}
The completion path (interrupt thread, mpt lock held):
/* sys/dev/disk/mpt/mpt_cam.c:2662, 2710 */
mpt_req_untimeout(req, mpt_timeout, ccb); /* callout_stop β no
* interlock if running */
...
xpt_done(ccb); /* CCB handed back to CAM */
Interleaving (two CPUs):
- callout deadline expires; softclock enters
mpt_timeout, readsccb->ccb_h.ccb_mpt_ptr, blocks onMPT_LOCK. - interrupt completes the command:
callout_stop(no effect β callback already running), completes and recycles the CCB viaxpt_done(): - XPT_SCSI_IO CCBS come from the periph pool (cam_periph_getccb) and are immediately reusable by the next command; - XPT_CONT_TARGET_IO CCBS (armed with a 60 s timeout atmpt_cam.c:4849) are zone-allocated by pass(4) (scsi_pass.c:495-504, USER_CCB βxpt_alloc_ccb) and kfree'd (xpt_free_ccb,sys/bus/cam/cam_xpt.c:3927-3934), so their memory is reusable by any zone allocation. mpt_timeoutacquires the lock, readsccb_req_ptrfrom freed/reused memory: - garbage pointer βreq->sernoderef β wild kernel read β panic; - stale-but-valid pointer (pooled case) βTAILQ_REMOVE+TAILQ_INSERT_TAILmove an unrelated, live request to the timeout list and mark itREQ_STATE_TIMEDOUTβ recovery thread aborts an innocent command; its later completion removes it from the timeout list (mpt_cam.c:2711-2717) β meanwhile the aborted-command bookkeeping (mpt_recover_commands,mpt_cam.c:4035-4116) operates on a ccb pointer (req->ccb) that belongs to the new owner β confusion.
The same untimeout-then-done pattern exists at mpt_cam.c:5252 and
mpt_cam.c:5315 (turbo target replies).
User amplification
ccb->ccb_h.timeout is a user-set field (ms) copied by xpt_merge_ccb();
mpt_execute_req{,_a64} arm the callout with it (mpt_cam.c:1638-1641,
2025-2028). A pass(4) caller issuing XPT_SCSI_IO with timeout = 1 on a
busy device maximizes expiry-vs-completion collisions. (Upstream FreeBSD's
mpt_timeout uses MPT_LOCK_ASSERT instead of taking the lock; DFly's
self-locking variant additionally reads ccb_mpt_ptr before the lock, but
the core race β non-interlocking untimeout + CCB handed back before the
blocked callback resumes β is shared.)
Impact
Kernel use of a freed/recycled CAM CCB β wild-pointer read (panic / local
DoS from an operator-group user on hardware) or wrong-request abort with
pending/timeout list confusion (double-remove panics). A write primitive via
TAILQ_REMOVE(&mpt->request_pending_list, req, links) would require the
OOB-fetched req to be attacker-shaped; not demonstrated here.
Why inconclusive on this guest
Same as DF-3672: mpt compiled into X86_64_GENERIC but zero LSI PCI devices β no mpt bus, no pass node on one, no completions, no callouts armed by this driver. The race is source-provable (see above) but its reproduction needs MPT hardware under I/O load.
Fix
Stop touching the CCB after the completion path may have handed it back:
arm the callout with the pool-resident request_t * (never freed until
detach), re-read req->ccb only after acquiring the lock, and bail unless
the request is still queued. See fix.diff.
Evidence
mpt_timeout_race.cβ the load pattern that would trigger it.env.txtβ guest hardware absence proof.
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 authored against read-only sys/ (request_t gains to_arg + sc back-pointer; callout arg becomes the request; under-lock re-validation). Build-and-run validation impossible: bug not triggerable on the baseline guest (no MPT hardware).
fix.diff; env.txt documents hardware absence
Confirmed kernel references
Detail
Exploit chain
operator-group user -> pass CAMIOCOMMAND XPT_SCSI_IO with ccb_h.timeout=1ms in a tight loop -> callout fires on CPU A, blocks on mpt lock -> completion on CPU B: callout_stop (no interlock), xpt_done recycles/frees CCB -> CPU A resumes, reads ccb_req_ptr from reused memory -> wild request_t* deref (panic) or wrong-request TAILQ_MOVE + REQ_STATE_TIMEDOUT -> recovery aborts unrelated command
Evidence (decisive lines)
VERDICT.md (full interleaving with path:line); mpt_timeout_race.c (load harness); env.txt/run.log (no LSI hardware on guest); fix.diff (pool-resident callout arg + under-lock re-validation)
PoC changes
n/a (authored from scratch)
Verified recommended fix
Arm the callout with the pool-resident request_t instead of the CCB; take the mpt lock via a request back-pointer, re-validate REQ_STATE_QUEUED under the lock before touching the CCB
Verdict
Source-provable CCB lifetime race, timing-dependent, not executable on this guest (no MPT hardware). mpt_req_untimeout() is plain callout_stop() (mpt.h:769-772) on a callout_init_mp() callout, which per kern_timeout.c:1073-1096 does not interlock against an in-progress callback. mpt_timeout() (mpt_cam.c:1234-1255) reads ccb->ccb_h.ccb_mpt_ptr before the lock and ccb->ccb_h.ccb_req_ptr after acquiring it; if the timeout callback is already executing and blocked on the mpt lock while the completion path (mpt_scsi_reply_handler:2662-2723, or the turbo target paths 5252/5315) runs callout_stop + xpt_done, the callback resumes with a CCB that CAM has recycled (pooled XPT_SCSI_IO CCBS) or kfree'd (zone CTIO CCBS, armed 60s at mpt_cam.c:4849): garbage req pointer -> req->serno wild read -> panic, or stale req -> TAILQ move of an innocent request to the timeout list -> spurious abort / list confusion. Window is user-amplifiable: pass(4) callers set ccb_h.timeout per command (1ms). Expected impact on hardware: local DoS/panic from an operator-group user; write-primitive via TAILQ_REMOVE not demonstrated.
No comments yet.