# DF-4014 — VERDICT

## Verdict

**Source-certain, HW-gated (not live-reproducible on this guest: no LSI SAS
HBA, /dev/mps\* absent).** `mps_user_pass_thru()` frees its command — and the
user-sized `cm->cm_data` buffer the chip's SGL still points at — the moment
`mps_wait_command()` returns ETIMEDOUT. `mps_wait_command()`
(sys/dev/raid/mps/mps.c:2118-2132) performs **no abort**: it `lksleep()`s on
the command and returns `ETIMEDOUT` while the request is still enqueued to
the hardware with `MPS_CM_FLAGS_WAKEUP` set. The immediate free hands the
command slot back to the allocation free list and kfree()s the DMA target
while the chip retains the SMID and the SGL — a classic free-while-in-flight
(use-after-free) with device-driven writeback into freed memory.

## Bug class

Use-after-free / DMA-after-free triggered by command timeout. The mpr
sibling driver has the *opposite* defect (DF-4010: it **leaks** the buffer
across its reinit path because `mpr_wait_command()` takes `&cm` and the
reinit frees the command itself) — DragonFly's mps kept the old
single-pointer wait API *and* the immediate free, which is the memory-unsafe
direction.

## Source trace (path:line)

1. `sys/dev/raid/mps/mps_user.c:975` — `err = mps_wait_command(sc, cm, 30);`
   under `mps_lock`.
2. `sys/dev/raid/mps/mps.c:2126-2130` — on `lksleep()` timeout the function
   returns `ETIMEDOUT`; nothing dequeues or aborts `cm` (the chip still owns
   the request frame, reply frame and the SGL built over `cm->cm_data`).
3. `sys/dev/raid/mps/mps_user.c:977-982` — `if (err) { ...; mps_unlock(sc);
   goto RetFreeUnlocked; }`.
4. `sys/dev/raid/mps/mps_user.c:1039-1046` — `RetFreeUnlocked:` takes the
   lock, `kfree(cm->cm_data, M_MPSUSER)` and `mps_free_command(sc, cm)`.
5. `sys/dev/raid/mps/mpsvar.h:514-540` — `mps_free_command()` resets the
   command to `MPS_CM_STATE_FREE` and `TAILQ_INSERT_TAIL(&sc->req_list, ...)`;
   the slot is instantly allocatable by any other thread (CAM I/O, another
   ioctl) while the outstanding SMID still refers to it.
6. Late completion: the reply descriptor's SMID resolves back to this `cm`;
   `cm_reply` is written by hardware/DMA and the completion path mutates a
   command that may already be reused (cross-command corruption), and the
   chip's residual DMA lands in the freed `cm->cm_data` chunk.

The task-management branch has the same defect:
`sys/dev/raid/mps/mps_user.c:838-844` (wait, EIO) → `:863 mpssas_free_tm()`
frees the in-flight TM command.

Contrast with the driver's own policy for config pages
(`sys/dev/raid/mps/mps_user.c:240-245`): *"Leave the request. Without
resetting the chip, it's still owned by it and we'll just get into trouble
freeing it now."* — the pass-thru paths violate exactly this.

## Reachability

Any operator-group user (0640 `/dev/mpsN`, ioctls not gated on open mode —
see DF-1359/DF-1326 boundary analysis) can issue `MPTIOCTL_PASS_THRU` with a
request the firmware takes ≥30 s on (or never completes); the timeout then
arms the UAF. Exploitability on real hardware: medium-high (requires an HBA
and control over command latency, but the free-then-DMA window is wide and
repeatable).

## Why not live-reproduced

No LSI Fusion-MPT PCI device exists on the audit QEMU guest; the shipped
driver never attaches (`/dev/mps*` absent), so the wait/timeout path cannot
be entered. A KLD replica of a chip-async timeout was judged out of scope
for fidelity: the essence of the bug is the *device's* late DMA into freed
memory, which a stubbed replica can only assert structurally, not
demonstrate. The source trace above is line-complete.

## Fix

`fix.diff` — abandon the command on wait-timeout (both the TM branch and the
normal branch route to `Ret` without freeing), matching the driver's own
documented abandon policy for timed-out config requests. The abandoned
command is reclaimed on the next diag reset. Note: this trades the UAF for a
bounded leak until a proper `mps_abort_command()` exists; the companion
`mps_user_command()` path (:707-711) already leaks by design (DF-4015).

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