# DF-1556 — VERDICT

**Status:** REPRODUCED (primitive confirmed via source trace + userspace harness)
**Impact:** heap-overflow (OOB write), bounded only by attacker-controlled firmware `PageLength`
**Confidence:** certain
**Class:** CWE-787 Out-of-bounds Write — missing `MIN()` length guard on `bcopy`

## Verdict (one line)

The bug is real and confirmed at `sys/dev/raid/mps/mps_config.c:1117` — the
final `bcopy(page, config_page, cm->cm_length)` is the **only** config-page
getter in this file that omits the `MIN(cm->cm_length, sizeof(...))` guard
every sibling uses; a malicious/compromised LSI WarpDrive HBA returning
`PageLength >= 22` overflows the caller's 84-byte buffer.

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

The guest has **no LSI SAS HBA** (`pciconf -l` shows only virtio devices), 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.
Per the procedure, the primitive is instead proved at the **object/harness
level** (the DF-0594/0616/0281 latent-bug pattern), with the live trigger
conditions documented below.

## Mechanism (trigger → primitive → effect)

1. **Trigger.** Any local context that causes `mps(4)` to enumerate RAID
   config pages on a WarpDrive-class HBA sets `sc->mps_flags &
   MPS_FLAGS_WD_AVAILABLE` (`mpsvar.h:292`) and reaches
   `mps_wd_config_pages()` (`mps_sas.c:3320`).  This happens during normal
   `mps_attach_sas()` and on every SAS topology-change / IR-config-change
   event.  An attacker who supplies a crafted reply (a malicious PCIe card, a
   compromised HBA firmware, or a passed-through SAS controller in a VM)
   controls the page contents.
2. **Caller allocation** at `mps_config.c:378-380`:
   ```c
   raid_vol_pg0 = kmalloc(sizeof(Mpi2RaidVolPage0_t) +
       (sizeof(Mpi2RaidVol0PhysDisk_t) * MPS_MAX_DISKS_IN_VOL),
       M_MPT2, M_ZERO | M_INTWAIT);
   ```
   = `44 + 4*10 = 84` bytes (`MPS_MAX_DISKS_IN_VOL=10`, `mpsvar.h:106`;
   `Mpi2RaidVolPage0_t`=44 per `mpi2_cnfg.h:1362-1383`).
3. **Length computed from firmware** at `mps_config.c:1084`:
   ```c
   cm->cm_length = le16toh(mpi_reply->Header.PageLength) * 4;
   ```
   — taken straight from the firmware reply, **no clamp against the
   destination type**.  `PageLength` is a `U16` (range 0..65535), so
   `cm_length` can be up to 262140.
4. **Sink — the unbounded bcopy** at `mps_config.c:1117`:
   ```c
   bcopy(page, config_page, cm->cm_length);
   ```
   Every sibling getter bounds this copy: `MIN(cm->cm_length,
   sizeof(Mpi2IOCPage8_t))` (line 163), `MIN(cm->cm_length, sz)` (634),
   `MIN(cm->cm_length, sizeof(Mpi2SasDevicePage0_t))` (880),
   `MIN(cm->cm_length, sizeof(Mpi2BiosPage3_t))` (998),
   `MIN(cm->cm_length, sizeof(Mpi2RaidVolPage1_t))` (1237),
   `MIN(cm->cm_length, sizeof(Mpi2RaidPhysDiskPage0_t))` (1382).  Only
   line 1117 omits it.
5. **Primitive.** A page claiming `PageLength=22` (`cm_length=88`) already
   overflows by 4 bytes; `PageLength=255` overflows by 936 bytes; the
   theoretical max (`PageLength=65535`) overflows by ~256 KB.  The overflow
   contents are attacker-controlled (the firmware-supplied page bytes) and
   land in kernel heap adjacent to the 84-byte `raid_vol_pg0` allocation.
6. **The `NumPhysDisks > 8` sanity check at line 400 happens AFTER the bcopy
   at line 1117 has already run**, so it does not protect the copy.  This is
   the key fact that makes the bug exploitable on the very first call.

## Harness proof (`harness.c`)

The harness reproduces the exact kernel C logic of the buggy bcopy against an
84-byte buffer (sized identically to the caller's allocation at
`mps_config.c:378-380`) with a `PROT_NONE` guard page placed immediately
after, so any overflow faults with `SIGSEGV`.  Compiled and run as the
unprivileged `maxx` user:

```
$ ./run.sh 22            # minimal overflow
overflow by 4 bytes -> write will land in guard page (PROT_NONE) -> SIGSEGV
>>> SIGSEGV/11 caught: OOB write past 84-byte caller buffer confirmed.
>>> PRIMITIVE CONFIRMED: a malicious HBA returning PageLength=22 (cm_length=88)
    overflows the mps_wd_config_pages RAID-volume buffer by 4 bytes.

$ ./run.sh 255           # max overflow with U8 PageLength
overflow by 936 bytes -> ... -> SIGSEGV
>>> PRIMITIVE CONFIRMED: ... overflows ... by 936 bytes.

$ ./run.sh 21            # control: exact-fit, no overflow
cm_length=84 <= buf=84: no overflow this run.
```

## 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.  Documenting the **realistic ceiling**: on a host where a malicious or
passed-through LSI WarpDrive HBA is present, the primitive is a
fully-attacker-controlled heap overflow of up to ~64 KB past an 84-byte
`kmalloc` in `M_MPT2`, sufficient for kernel-heap grooming (corrupt an
adjacent victim object — function-pointer ops vector, `ucred *`, refcount —
then convert to code-exec / privilege escalation).  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 kernel bcopy
  logic with guard-page fault detection.  All struct layouts are reproduced
  verbatim from the kernel headers (with citations in comments); the
  `CALLER_BUF_SZ` is hard-coded to the kernel's actual value `84` with a
  `_Static_assert` so the overflow math is unambiguous.
- `build.sh`, `run.sh` — exact reproducible commands.
- `fix.diff` — the verified fix (see below).

## Recommended fix

Bound the final `bcopy` by the caller's buffer capacity, **not** by
`sizeof(Mpi2RaidVolPage0_t)` (the finding's original proposal).  The naive
fix would truncate the legitimate variable-length PhysDisk tail (the loop at
`mps_config.c:461` reads up to 8 PhysDisk entries past the 44-byte header).
The correct capacity is exactly what the sole in-tree caller allocates:

```c
bcopy(page, config_page,
    MIN(cm->cm_length, sizeof(Mpi2RaidVolPage0_t) +
    sizeof(Mpi2RaidVol0PhysDisk_t) * MPS_MAX_DISKS_IN_VOL));
```

This **supersedes** the finding markdown's `## Recommended fix` (which would
drop the PhysDisk tail).  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 1114); (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 — `MIN()` against the caller's actual 84-byte capacity
preserves the legitimate PhysDisk tail while clamping overflow.  See
`fix_build.log` and `fix_run.log`.
