# DF-3800 — Stale IOC-facts target-array reallocation → heap OOB write after Diag Reset

- File: `sys/dev/raid/mpr/mpr_sas.c`
- Class: kernel heap OOB write (firmware-gated)
- Severity: Medium / Confidence: likely
- Status on guest: **not_reproduced — no SAS3 HBA in the QEMU guest** (driver module present, no PCI device, SIM never attaches)

## What breaks (source chain)

1. At attach, `mprsas_attach_sas()` sizes the target array from the *then-current*
   IOC facts and caches the count:
   - `sys/dev/raid/mpr/mpr_sas.c:760` `sassc->maxtargets = sc->facts->MaxTargets + sc->facts->MaxVolumes;`
   - `sys/dev/raid/mpr/mpr_sas.c:761-762` `kmalloc(sizeof(struct mprsas_target) * sassc->maxtargets)`

2. After any Diag Reset (`mpr_reinit` → `mpr_iocfacts_allocate(sc, attaching=0)`),
   new IOC facts are read into `sc->facts`; if any size-determining field changed,
   `reallocating = TRUE` (`sys/dev/raid/mpr/mpr.c:551-576`).

3. The target array is then **re-allocated to the OLD (pre-reset) size**:
   - `sys/dev/raid/mpr/mpr.c:630-634` calls
     `mprsas_realloc_targets(sc, saved_facts.MaxTargets + saved_facts.MaxVolumes)`
     where `saved_facts` is the pre-reset copy (`sys/dev/raid/mpr/mpr.c:473-475`).
   - `sys/dev/raid/mpr/mpr_sas.c:3911-3937` (`mprsas_realloc_targets`) frees the old
     array and kmallocs the new one with the passed-in count, and **never updates
     `sassc->maxtargets`** (nor takes the current facts).

4. Meanwhile the mapping layer is rebuilt from the **NEW** facts:
   - `sys/dev/raid/mpr/mpr_mapping.c:2564-2565`
     `sc->max_volumes = sc->facts->MaxVolumes; sc->max_devices = sc->facts->MaxTargets + sc->max_volumes;`
   - `sys/dev/raid/mpr/mpr_mapping.c:2141-2146` mapping_table allocated with `max_devices` entries.

### Direction A: NEW facts > OLD facts → OOB write past `sassc->targets[]`

Discovery re-runs after the reset (`mprsas_startup` → port enable). For each device,
`mprsas_add_device()` computes the target id from the (new-sized) mapping table:

- `sys/dev/raid/mpr/mpr_mapping.c:2662-2674` `mpr_mapping_get_tid()` returns
  `mt_entry->id` for any entry in `[0, sc->max_devices)` — **no clamp to
  `sassc->maxtargets`**.
- `sys/dev/raid/mpr/mpr_sas_lsi.c:908` `targ = &sassc->targets[id];`
- `sys/dev/raid/mpr/mpr_sas_lsi.c:925-965` writes ~20 fields
  (`devinfo, devname(2×u32), encl_handle, encl_slot, encl_level, connector_name[4],
  handle, parent_handle, sasaddr, parent_sasaddr, parent_devinfo, tid, linkrate,
  flags, scsi_req_desc_type, encl_level_valid, TAILQ_INIT×2, SLIST_INIT …)`.

With `id ≥ sassc->maxtargets` (old size) this is a heap OOB write of
`id * sizeof(struct mprsas_target)`-offset, firmware-influenced content,
magnitude up to `(MaxTargets_new − MaxTargets_old) * sizeof(struct mprsas_target)`.
Same for RAID volumes: `sys/dev/raid/mpr/mpr_sas_lsi.c:1424/1433`
(`mpr_mapping_get_raid_tid` → `&sassc->targets[id]`).

### Direction B: NEW facts < OLD facts → OOB write past `mapping_table[]`

CAM targets enumerated before the reset (ids `< maxtargets_old`, they survive
`AC_BUS_RESET` announced by `mprsas_handle_reinit`) keep issuing I/O:

- `sys/dev/raid/mpr/mpr_sas.c:2706-2707`
  `sc->mapping_table[target_id].TLR_bits = (u8)MPI2_SCSIIO_CONTROL_NO_TLR;` —
  1-byte OOB **write** past the shrunk mapping_table.
- OOB **reads** at `sys/dev/raid/mpr/mpr_sas.c:2092, 2764-2768, 2789`
  (`sc->mapping_table[target_id].TLR_bits`, `.device_info`).

The `KASSERT(target_id < sassc->maxtargets)` guards in this file
(`mpr_sas.c:1072, 1929, 3348, 3495, 3616, 3748`) are INVARIANTS-only
(`sys/sys/systm.h:94-118`) — compiled out in production kernels — and in
Direction B they would pass anyway (`target_id < maxtargets_old`).

## Threat model

The size delta is firmware-reported. A malicious or field-updated IOC image that
changes `MaxTargets`/`MaxVolumes` across a Diag Reset (diag reset itself is
reachable through ordinary error recovery: `mprsas_tm_timeout` → `mpr_reinit`,
`sys/dev/raid/mpr/mpr_sas.c:1255-1270`) triggers the mismatch. On-line firmware
upgrade is a supported operation for these HBAs, making even benign deltas
plausible; a hostile IOC trivially controls both sides.

## Guest verification attempt (why not_reproduced)

- Guest kernel: DragonFly 6.5-DEVELOPMENT (#0 Thu Jul 2 06:02:54 UTC 2026) X86_64_GENERIC.
- `pciconf -l`: no LSI/Avago/Broadcom SAS3 device — QEMU i440fx + PIIX ATA + virtio only.
- `kldload mpr` → "module pci/mpr already exists" (driver compiled into the kernel),
  but with no PCI probe match `mpr_attach`/`mpr_attach_sas` never run, no SIM is
  registered, `sc->sassc == NULL` forever.
- Therefore no runtime path into `mprsas_realloc_targets`, `mprsas_add_device`, or
  the completion handlers exists on this guest. See `env.txt`.

Honest classification: **not_reproduced** (feature absent), impact `none` on this
guest; the defect itself is source-provable with certainty.

## Fix

`mprsas_realloc_targets` must (a) be given the *current* facts size, (b) update
`sassc->maxtargets`, and (c) the caller must pass current facts. See `fix.diff`
(also adds the missing `sassc->maxtargets` update and clamps mapping lookups to
`sassc->maxtargets` as defense-in-depth).

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