# DF-4085 — Unvalidated IOC-pg8 `MaxNumPhysicalMappedIDs` as `num_slots` → heap OOB write in `_mapping_process_dpm_pg0`

**File:** `sys/dev/raid/mps/mps_mapping.c` (branch `:1512-1518`, reservation loop `:1544-1563`)
**Class:** CWE-787 Out-of-bounds Write (heap) / CWE-20 unvalidated firmware count
**Severity:** High — pass 2 (GLM 5.3), bucket `memcorrupt`
**Not** a duplicate of DF-1374 (that finding is the unvalidated `DeviceIndex`
*index* at the same function): here `dev_idx` is **in bounds** and the missing
bound is the *count* — `num_slots` is taken from IOC page 8's
`MaxNumPhysicalMappedIDs` with no check that `dev_idx + num_slots <=
max_devices`. DF-1375's `num_slots` text covers a different source (live
enclosure-event `NumSlots` in `_mapping_get_dev_info:992`), not DPM-pg0
ingestion.

## The bug (source-confirmed)

1. `mps_mapping_initialize()` (`:1719`) enables DPM when
   `ioc_pg8.MaxPersistentEntries != 0` (`:1739-1740`) and reads the DPM page
   (`:1780`), then calls `_mapping_process_dpm_pg0()` (`:1793`).
2. For every non-empty DPM entry in ENCLOSURE_SLOT_MAPPING mode, if
   `dev_idx < num_rsvd_entries + max_num_phy_ids` (the direct-attached virtual
   enclosure range) the code sets
   ```c
   num_slots = max_num_phy_ids;                    /* :1518 */
   ```
   `max_num_phy_ids = le16toh(sc->ioc_pg8.MaxNumPhysicalMappedIDs)`
   (`:1469`) — a firmware-supplied u16 that is **never validated anywhere**
   against `max_devices` (= `facts->MaxTargets + facts->MaxVolumes`, `:1734`).
3. The reservation loop
   ```c
   mt_entry = &sc->mapping_table[dev_idx];          /* :1544 */
   for (map_idx = dev_idx; map_idx < (dev_idx + num_slots);
       map_idx++, mt_entry++) { ... writes ... }    /* :1545-1563 */
   ```
   writes `num_slots` consecutive `struct dev_mapping_table` entries
   (`physical_id` = the DPM entry's 64-bit PhysicalIdentifier, `phy_bits`,
   `channel`, `id`, `dpm_entry_num`, `missing_count`, `device_info`) with no
   end-of-table check. `mapping_table` is `kmalloc(max_devices * 32,
   M_MPT2)` (`mps_mapping.c:1393-1394`).

With `MaxNumPhysicalMappedIDs = 0xFFFF` (or any value pushing
`dev_idx + max_num_phy_ids` past `max_devices`) and an ordinary in-bounds
`dev_idx`, boot-time DPM ingestion performs a **linear heap overflow of up to
~2 MB past a ~16 KB allocation**, 32 bytes per entry, each entry carrying the
firmware-chosen 64-bit `physical_id` at offset 0. The stored
`et_entry->num_slots = num_slots` (`:1539`) additionally re-arms the
already-known DF-1375 slot-reservation overflow for every later topology
event.

The (else) branch is safe (`num_slots` = 4-bit slot field of
`MappingInformation`, max 0xF); only the direct-attach branch is unbounded.
The `enclosure_table` growth itself is bounds-checked (`:1525-1531`), and
`dpm_entry_used[]`/`dpm_flush_entry[]` walks are bounded by
`max_dpm_entries` — the *count* is the sole missing bound.

## Threat model

Same as DF-1374/DF-1375/DF-3914: a malicious or buggy LSI/Avago IOC
firmware image, NVRAM corruption of the persistent map, or PCI
passthrough of an emulated mps(4) HBA. Everything on this path
(IOC facts, IOC pg8, DPM pg0 content) is firmware-controlled DMA'd data;
nothing is validated against the allocation sizes derived from the same
firmware.

## Reproduction (object-level, hardware-gated)

No mps HBA exists in the audit QEMU guest (see `env.txt`), so the kernel path
cannot be exercised. `harness_df4085.c` replicates the exact struct
(`mpsvar.h:135`, sizeof == 32 on amd64) and the verbatim `:1544-1563` loop
and drives it with a fully **in-bounds** `dev_idx`:

```
cc -O2 -Wall -o harness harness.c && ./harness
```

Expected decisive output (see `run.log`, stable across reruns in `run.2.log`):

```
max_devices               : 510 (table = 16320 bytes)
MaxNumPhysicalMappedIDs   : 0xffff (IOC pg8, unvalidated)
DPM entry 0               : dev_idx=256 (in bounds!), num_slots=65535
reservation loop wrote     : [256, 65791)
entries past table end     : 65281
kernel equivalent          : 2088992-byte linear heap write of fw-controlled data
  past a 16320-byte kmalloc, attacker qword 0x5141524153534141 at stride 32
RESULT: OOB WRITE PRIMITIVE PROVEN (canary smash, stride 32)
```

## Fix

`fix.diff` — validate, before storing/looping, that the entry fits the table:
`dev_idx < max_devices && num_slots <= max_devices - dev_idx`, else skip the
DPM entry (consistent with the existing conflict handling). Compile-validated:
applied (together with DF-4086's clamp and the other mps fixes) to the guest
`/usr/src`; `make` in `sys/dev/raid/mps` rebuilt the whole module with
`-Werror` and linked `mps.ko` (BUILD_EXIT=0, `fix_build.log`).
