# DF-4086 — `_mapping_get_ir_maprange` off-by-one: `end_idx == max_devices` → OOB read + conditional 32-byte OOB write of `mapping_table[]`

**File:** `sys/dev/raid/mps/mps_mapping.c` (range calc `:221-235`; OOB sinks
`:366`, `:409`, `:457/:462`, write `:2222-2232`, secondary `:1647`, `:2126-2129`)
**Class:** CWE-191 integer underflow/off-by-one → CWE-125 read + CWE-787 write
**Severity:** High — pass 2 (GLM 5.3), bucket `memcorrupt`
**Fresh pass-2 finding** (not DF-1374: no DPM DeviceIndex involved; not
DF-1375: no slot arithmetic; the mpr sibling checklist DF-3914..3919 does not
cover it either — verified against the findings DB).

## The bug (source-confirmed)

```c
/* mps_mapping.c:221-235 */
if (volume_mapping_flags == MPI2_IOCPAGE8_IRFLAGS_LOW_VOLUME_MAPPING) {
        *start_idx = 0;
        if (ioc_pg8_flags & MPI2_IOCPAGE8_FLAGS_RESERVED_TARGETID_0)
                *start_idx = 1;
} else
        *start_idx = sc->max_devices - sc->max_volumes;
*end_idx = *start_idx + sc->max_volumes - 1;          /* <-- unclamped */
```

`max_devices = facts->MaxTargets + facts->MaxVolumes` (`:1734`). If the IOC
facts report **`MaxTargets == 0`** while IOC pg8 sets
`RESERVED_TARGETID_0` **and** low volume mapping (all firmware-controlled),
then `start_idx = 1`, `max_devices = max_volumes`, and

```
end_idx = 1 + max_volumes - 1 == max_devices     /* one past last index */
```

Every IR walk in the file uses **inclusive** `<=` against `end_idx`:

| sink | effect at `map_idx == max_devices` |
|---|---|
| `_mapping_get_ir_mt_idx_from_wwid` `:366` | OOB **read** of `physical_id` |
| `_mapping_get_ir_mt_idx_from_handle` `:409` | OOB **read** of `dev_handle` |
| `_mapping_get_free_ir_mt_idx` `:457/:462` | OOB reads; **returns** `max_devices` when the OOB memory lacks `MPS_MAP_IN_USE` |
| `mps_mapping_check_devices` `:1647` | OOB r/w of `init_complete`/`missing_count` (function currently dead — no callers — noted) |
| `_mapping_check_update_ir_mt_idx` `:2126-2129` | OOB read of `device_info` |
| `mps_mapping_ir_config_change_event` `:2222-2232` | **full 32-byte struct OOB WRITE**: `physical_id` = firmware volume WWID, `dev_handle`, `channel=MPS_RAID_CHANNEL`, `id`, `device_info = MPS_DEV_RESERVED|MPS_MAP_IN_USE`, `init_complete=0` |

The write is reached whenever `_mapping_get_free_ir_mt_idx` returns the OOB
index (table entries all in use, adjacent heap garbage without the IN_USE
bit) — then a RAID-volume IR config-change event (firmware-sourced) writes
one `dev_mapping_table` entry immediately past the
`kmalloc(max_devices*32)` allocation. Heap-adjacent corruption with mostly
controlled contents; on the intentionally permissive audit guest this is
classic slab-neighbor overwrite material.

The high-volume-mapping branch (`start = max_devices - max_volumes`,
`end = max_devices - 1`) is always in bounds; the bug needs the low-mapping +
reserved + `MaxTargets == 0` combination, all in one firmware image.

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

No mps HBA in the audit guest (`env.txt`). `harness_df4086.c` replicates the
range function, the `<=` walks and the event write verbatim:

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

Decisive output (`run.log`, stable in `run.2.log`):

```
facts: MaxTargets=0 MaxVolumes=255 -> max_devices=255
IR maprange: start=1 end=255  (end == max_devices => OFF-BY-ONE: `<=` walks hit index 255)
wwid walk  :366 read physical_id at mapping_table[255] -> OOB READ
free-idx walk :457 returned 255 (== max_devices, OUT OF BOUNDS)
event write: mapping_table[255] = {wwid 0x57119418badb00b5, handle 0x0b1d, info 0x30000000}
guard[0].physical_id == fw WWID: 32-byte heap OOB WRITE at index max_devices
RESULT: OFF-BY-ONE OOB READ + CONDITIONAL OOB WRITE PROVEN
```

## Fix

`fix.diff` — clamp `*end_idx` to `max_devices - 1` in
`_mapping_get_ir_maprange()` so all `<=` walks stay in bounds regardless of
firmware facts. Compile-validated with the rest of this batch's mps fixes
(`-Werror`, `mps.ko` linked — `fix_build.log`).
