# DF-1227 — VERDICT

**Finding:** `mpr_build_nvme_prp()` heap overflow via an unbounded
user-supplied data length (`sys/dev/raid/mpr/mpr.c` + `mpr_user.c`).
**Status:** NOT TESTABLE on this audit guest. **Confidence (bug is real):** certain.
**Impact ceiling:** kernel heap overflow of attacker-controlled (physical)
addresses from a `MPTIOCTL_PASS_THRU` ioctl — root/operator→kernel corruption
(node is `0640 root:operator`, `mpr_user.c:205`); not reachable by a
non-operator unprivileged user.
**Fix:** authored in `fix.diff`, applied clean, compile-validated into GENERIC.
**Runnable PoC:** `mpr_nvme_prp_overflow.c` (triggers on any host with an mpr
controller + accessible `/dev/mprN`).

## Mechanism (confirmed line-by-line in `sys/`)

1. `mpr_user_pass_thru()` (`mpr_user.c`) services `MPTIOCTL_PASS_THRU`
   (`mpr_ioctl.h:371`, `_IOWR('I', 4, struct mpr_pass_thru)`).
2. `mpr_user.c:800` copyin's the request header; `mpr_user.c:804` caps only
   `RequestSize` to `sc->reqframesz`. **`DataSize`/`DataOutSize` are never
   capped against `sc->maxio`.**
3. `mpr_user.c:904` — `cm->cm_length = MAX(data->DataSize, data->DataOutSize);`
4. `mpr_user.c:908` — `cm->cm_data = kmalloc(cm->cm_length, M_MPRUSER, ...);`
   (an 8 MB user `DataSize` ⇒ 8 MB kernel alloc — itself a DoS vector).
5. `mpr_user.c:945` — `if (function == MPI2_FUNCTION_NVME_ENCAPSULATED)`
   (user-controlled `Function` byte in the request header, `mpi2.h:859`;
   value `0x33`, `mpi2.h:719`):
6. `mpr_user.c:970-971` — `mpr_build_nvme_prp(sc, cm, nvme_encap_request,
   cm->cm_data, data->DataSize, data->DataOutSize);`
7. `mpr.c:2716` — `prp_page_info = mpr_alloc_prp_page(sc);` allocates ONE PRP
   list page (`PAGE_SIZE`, holding `512` 8-byte entries).
8. `mpr.c:2751-2754` — `length = data_in_sz ? data_in_sz : data_out_sz;`
   (the uncapped user value).
9. `mpr.c:2757` `while (length) { ... *prp_entry = htole64(paddr); prp_entry++;
    prp_entry_phys++; ... length -= entry_len; }` — one PRP entry per `PAGE_SIZE`
   of user buffer. With `length = 8 MB` = 2048 pages, the loop runs ~2048 times,
   writing ~1536 entries past the single 512-entry PRP page ⇒ **heap overflow**
   of attacker-chosen physical addresses (`paddr` derived from `cm->cm_data`,
   which the user filled via the WRITE direction or left zeroed).

`sc->maxio` (the controller's true max transfer, computed at `mpr.c:417-428`)
is the natural cap and is already used to size the PRP pool
(`mpr.c:1593-1598`, `prp_buffer_size = PAGE_SIZE * pages_required` where
`pages_required` derives from `maxio/PAGE_SIZE`). So the PRP pool is sized for
`maxio`-sized transfers — but the ioctl never enforces that bound on user
data, defeating the sizing.

## Why it is NOT TESTABLE on this guest

- `pciconf -l` lists no LSI/LSI-Fusion SAS controller; there is no
  `/dev/mpr0` (the cdev is created only in `mpr_attach`, `mpr_user.c:205`).
  Without the node the ioctl entry is unreachable, regardless of privilege.
- Even if a node existed, `make_dev(&mpr_ops, unit, UID_ROOT, GID_OPERATOR,
  0640, ...)` (`mpr_user.c:205`) means only root or the `operator` group can
  open it rw. `maxx` (uid 1001) is not in `operator`, so this is **not** an
  unprivileged-user escalation on a default install — it is a local
  root/operator→kernel hardening gap (an operator-group member, or root, can
  corrupt the kernel).

This is the valid "root/operator-only reachability" hard-blocker case
combined with "no device node at all on this guest". The bug is genuine
(uncapped user data length confirmed above); a runnable PoC
(`mpr_nvme_prp_overflow.c`) is provided for a teammate with the hardware.

## Exploit chain
None developed *on this guest* (no mpr controller ⇒ no `/dev/mprN` ⇒ ioctl
unreachable; and the node is root:operator even if present). The included
`mpr_nvme_prp_overflow.c` drives the actual vulnerable ioctl path and would
overflow the heap on a suitable host; from there the primitive
(slab-adjacent overflow of PAGE_SIZE/8-byte physical-address entries into
`M_MPR`-tagged slab buckets) is a classic heap-grooming escalation, but it
requires hardware this guest does not have. Honest stop: **device/node not
present on this guest + operator-gated**.

## Fix
`fix.diff` adds the missing cap in `mpr_user_pass_thru`, immediately after
the existing `RequestSize` check:
```c
if (data->DataSize > sc->maxio || data->DataOutSize > sc->maxio) {
	mpr_dprint(sc, MPR_FAULT, "%s: data size too large "
	    "(in %d out %d > maxio %u)\n", __func__, data->DataSize,
	    data->DataOutSize, sc->maxio);
	err = EINVAL;
	goto RetFreeUnlocked;
}
```
This is the root-cause fix (the driver already sizes the PRP pool for
`maxio`); it matches how every other code path treats `maxio` as the max
transfer. Supersedes the finding proposal's intent with an exact, line-accurate
diff.

## Build / run on this guest
`./build.sh` compiles the PoC (against `/usr/src/sys` headers). `./run.sh`
runs it; on this guest `open("/dev/mpr0")` fails (no node) and it reports the
reachability verdict. On a host with an mpr controller + operator access, the
same binary triggers the overflow on an unpatched kernel.
