# DF-3813 — mpr_wait_command()/mpr_request_polled() miss MPR_FLAGS_REALLOCATED on the concurrent-reinit wakeup/poll path → use-after-free on `struct mpr_command`

## What

`mpr_wait_command()` (sys/dev/raid/mpr/mpr.c:3557-3621) sleeps on the caller's
command:

```c
error = lksleep(cm, &sc->mpr_lock, 0, "mprwait", timeout*hz);   /* mpr.c:3578 */
```

A diag-reset reinit from ANY other thread (CAM I/O timeout via mprsas TM →
`mpr_reinit()`, the periodic FAULT watchdog at mpr.c:2041-2048, a TM timeout
callout at mpr_sas.c:1263, another ioctl user) does:

1. `mprsas_handle_reinit()` → `mprsas_complete_all_commands()` — for
   sleeper-owned commands (cm_complete==NULL, MPR_CM_FLAGS_WAKEUP set) it only
   calls `wakeup(cm)`; it does NOT free them (mpr_sas.c:1178-1199).
2. `mpr_iocfacts_allocate(sc, attaching=FALSE)` — if the IOC Facts changed
   across the reset (`reallocating`, mpr.c:551-576) → `mpr_iocfacts_free()`
   (mpr.c:631) **kfrees the entire `sc->commands[]` array** (mpr.c:791-797)
   while the sleeper is still blocked reacquiring `mpr_lock`.

The sleeper then reacquires the lock, `lksleep()` returns **0** and
`mpr_wait_command()` returns success with `*cmp` pointing into freed memory.
Only the function's *own-timeout* (EWOULDBLOCK) path checks
`MPR_FLAGS_REALLOCATED` (mpr.c:3611-3617) — the woken-by-concurrent-reinit
path does not, and `mpr_request_polled()`'s polling loop (mpr.c:3641,
`while ((cm->cm_flags & MPR_CM_FLAGS_COMPLETE) == 0)`) keeps dereferencing
`cm` after such a free as well (its REALLOCATED check at mpr.c:3668 only runs
after its own 60 s timeout).

Callers then use the dangling `cm`:
- `mpr_user.c:701/1050` — `rpl = cm->cm_reply` (UAF read), `mpr_free_command()`
  (UAF write: re-inserts a pointer into the middle of the freed-and-reused
  slab onto `sc->req_list` → the same `mpr_command` gets handed to two owners
  → cross-command kernel memory corruption)
- `mpr_sas_lsi.c:1173` — `callout_stop(&cm->cm_callout)` (UAF write)
- `mpr_user.c:846` — `mprsas_free_tm(sc, cm)` (UAF write)

This is the mpr.c-side sibling of DF-3800 (filed on mpr_sas.c for the stale
`sassc->maxtargets`/`mprsas_realloc_targets` aspect of the same realloc pass).

## Trigger (needs hardware)

1. SAS3 controller present; firmware whose IOC Facts differ across a diag
   reset (mid FW-upgrade, or a fault that comes back with different
   RequestCredit/MaxTargets/IOCCapabilities) so the reinit reallocates.
2. Thread A: any synchronous command (`MPRIO_MPR_COMMAND` ioctl, config-page
   read, SATA ID, task management) → asleep in `mpr_wait_command()`.
3. Thread B: force the reinit — e.g. unprivileged raw I/O against a wedged
   target until CAM timeout → `mprsas_tm_timeout`/`mpr_wait_command` timeout
   → `mpr_reinit()`.

## Build / run

```
cc -O -Wall -o poc poc_concurrent_reinit.c -lpthread
./poc            # opens /dev/mpr0, floods IOC Facts sync requests
```

Success criterion (on equipped hardware): kernel panic or corrupted command
state (double-owned `mpr_command`) shortly after forcing the reinit; console
shows "Reinitializing controller" + realloc path (MPR_FLAGS_REALLOCATED) with
a concurrent `mprwait` sleeper.

## Honest negative on this guest

The audit guest has no SAS3 HBA. `kldstat -v | grep -w mpr` shows `142 pci/mpr`
(driver compiled into X86_64_GENERIC), but `/dev/mpr0` does not exist, so the
race cannot be exercised at runtime. See `guest_run.log`, `VERDICT.md`.

## Env

- DragonFly dfbsd 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026
  (X86_64_GENERIC), x86_64
- `pci/mpr` module linked into the kernel; no LSI/AVAGO PCI device; no
  `/dev/mpr*`; no `hw.mpr` sysctl tree
