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

```c
/* 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):

```c
/* 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):

1. callout deadline expires; softclock enters `mpt_timeout`, reads
   `ccb->ccb_h.ccb_mpt_ptr`, blocks on `MPT_LOCK`.
2. interrupt completes the command: `callout_stop` (no effect — callback
   already running), completes and recycles the CCB via `xpt_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 at `mpt_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.
3. `mpt_timeout` acquires the lock, reads `ccb_req_ptr` from freed/reused
   memory:
   - garbage pointer → `req->serno` deref → wild kernel read → panic;
   - stale-but-valid pointer (pooled case) → `TAILQ_REMOVE` +
     `TAILQ_INSERT_TAIL` move an *unrelated, live* request to the timeout
     list and mark it `REQ_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.
