# DF-1557 — VERDICT

**Status:** REPRODUCED (primitive confirmed via source trace + userspace harness)
**Impact:** controlled single-byte OOB write at controlled offset past `struct mps_softc`
**Confidence:** certain
**Class:** CWE-787 Out-of-bounds Write — unchecked firmware-supplied array index

## Verdict (one line)

The bug is real and confirmed at `sys/dev/raid/mps/mps_config.c:462` — the
WarpDrive PhysDisk mapping loop uses the firmware-supplied `U8 PhysDiskMap`
byte directly as the array index into `sc->DD_column_map[MPS_MAX_DISKS_IN_VOL]`
without any bounds check; any `PhysDiskMap >= 10` writes a controlled byte at
a controlled offset past the end of the `mps_softc` allocation.

## Why this is a valid (latent) primitive, not a false positive

Same reachability caveat as the sibling DF-1556: the guest has **no LSI SAS
HBA**, so the in-kernel `mps(4)` driver — although compiled into
`X86_64_GENERIC` (`sys/config/X86_64_GENERIC:92`) — never attaches and the
live code path cannot be exercised at runtime on this guest.  This is the
documented Phase 6 **valid hard blocker**: the path is dead/unreachable at
runtime on this guest and no in-kernel harness can exercise it without the
missing hardware.  The primitive is proved at the **object/harness level**
(DF-0594/0616/0281 latent-bug pattern).

## Mechanism (trigger → primitive → effect)

1. **Trigger.** Same as DF-1556: `mps_wd_config_pages()` is called from
   `mps_sas.c:3320` during `mps_attach_sas()` and on every SAS
   topology-change / IR-config-change event on any HBA flagged
   `MPS_FLAGS_WD_AVAILABLE`.  An attacker who supplies a crafted RAID Volume
   Page 0 reply controls every byte.
2. **The pre-existing gate at `mps_config.c:399-400`** validates only
   `VolumeType == RAID0` and `NumPhysDisks <= 8`.  It does **not** validate
   `PhysDiskMap`.
3. **The buggy loop at `mps_config.c:460-465`:**
   ```c
   pRVPD = (pMpi2RaidVol0PhysDisk_t)&raid_vol_pg0->PhysDisk;
   for (index = 0; index < raid_vol_pg0->NumPhysDisks; index++) {
       sc->DD_column_map[pRVPD->PhysDiskMap].phys_disk_num =
           pRVPD->PhysDiskNum;
       pRVPD++;
   }
   ```
   The loop *counter* `index` is bounded by `NumPhysDisks` (≤8), but the
   **write index** `pRVPD->PhysDiskMap` is an unchecked `U8` (range 0..255,
   declared at `mpi2_cnfg.h:1317`).
4. **The target array** `sc->DD_column_map[MPS_MAX_DISKS_IN_VOL=10]`
   (`mpsvar.h:442`) is the **final field** of `struct mps_softc` (the struct
   closes at `mpsvar.h:443`).  Each slot is `struct mps_column_map` =
   `{uint16_t dev_handle; uint8_t phys_disk_num;}` = 4 bytes (with 1 byte
   padding, `mpsvar.h:270-273`).
5. **Primitive.** `DD_column_map[10].phys_disk_num` is at byte offset
   `10*4+2 = 42` from `DD_column_map` base — i.e. **2 bytes past the end of
   the softc**.  `DD_column_map[255].phys_disk_num` is at byte offset
   `255*4+2 = 1022` — i.e. **982 bytes past the end of the softc**.  The
   written value (`pRVPD->PhysDiskNum`, also firmware-controlled `U8`) and
   the slot offset are both fully attacker-chosen.

## Harness proof (`harness.c`)

The harness reproduces the exact kernel C logic of the buggy loop against a
buffer laid out like the tail of `struct mps_softc` (`DD_column_map[10]` as
the final field), with a `PROT_NONE` guard page placed immediately after.
Run as the unprivileged `maxx` user:

```
$ ./run.sh 10 65 1        # minimal OOB: PhysDiskMap=10, value=0x41
-> 2 bytes PAST end of DD_column_map
[BUGGY] running loop: sc->DD_column_map[10].phys_disk_num = 0x41 ...
>>> SIGSEGV/11 caught: controlled-byte OOB write past DD_column_map[9] confirmed.
>>> PRIMITIVE CONFIRMED: ... writes byte 0x41 at offset +2 past end of softc.

$ ./run.sh 255 65 1       # max offset: PhysDiskMap=255
-> 982 bytes PAST end of DD_column_map
>>> PRIMITIVE CONFIRMED: ... writes byte 0x41 at offset +982 past end of softc.

$ ./run.sh 9 65 1         # control: PhysDiskMap=9 (last valid slot)
PhysDiskMap=9 < MPS_MAX_DISKS_IN_VOL=10: in-bounds write, no OOB.
wrote in-bounds: DD_column_map[9].phys_disk_num = 0x41
```

## Exploit chain

Not applicable as a default-GENERIC `uid=0` chain — blocked by a **valid hard
blocker**: the vulnerable code path is unreachable at runtime on this guest
(no LSI SAS HBA), so there is no live path from an unprivileged user to the
sink.  **Realistic ceiling** on a host with a malicious/passed-through LSI
WarpDrive HBA: a single-byte, controlled-value, controlled-offset write past
the `mps_softc` object (offset range 0..982 bytes past the allocation).  The
`mps_softc` is a large `kmalloc` object; an offset in the ~0..1024-byte range
is a high-quality heap-corruption primitive suitable for vtable / ops-vector
/ function-pointer overwrite after heap grooming.  No `uid=0` is claimed
here because the live trigger is not exercisable on this guest.

## PoC changes

The PoC directory was empty on arrival.  I authored:
- `harness.c` — self-contained userspace reproduction of the buggy mapping
  loop, with `struct mps_column_map` and `Mpi2RaidVol0PhysDisk_t` reproduced
  verbatim from the kernel headers (with citations in comments).  The
  `DD_column_map` tail is positioned so its last byte touches the guard page
  boundary, making even a 1-byte OOB write fault crisply.
- `build.sh`, `run.sh` — exact reproducible commands.
- `fix.diff` — the verified fix (see below).

## Recommended fix

Add an explicit bounds check on `PhysDiskMap` before indexing
`DD_column_map`, faulting out of WD setup on any out-of-range value (a
conforming WarpDrive config must enumerate each column exactly once).  This
**matches** the finding markdown's `## Recommended fix` proposal (same
logic, same `goto out` idiom used by every other validation failure in this
function).  See `fix.diff`.

## Fix validation (Phase 8)

`fix_status: not_testable` — the live in-kernel before/after test cannot be
run because the guest has no LSI SAS HBA (mps never attaches).  Validated:
(a) `fix.diff` applies cleanly with `patch -p1 --forward` (hunk #1 succeeded
at line 459); (b) the patched `mps_config.c` compiles cleanly into both the
kernel and the `mps` module with `-Werror` and zero warnings; (c) the
single-fix kernel (`#1`, sha256 `7a3b1de7...`) boots and the guest is fully
responsive; (d) the fix logic is correct by inspection — the `>=
MPS_MAX_DISKS_IN_VOL` guard catches every offending value and routes to the
same `goto out` cleanup the rest of the function uses.  See `fix_build.log`
and `fix_run.log`.
