# DF-4009 — mpr NVMe-encapsulated pass-thru: unbounded DataSize drives PRP walk past the arena (OOB write)

## What

`mpr_user_pass_thru()` (sys/dev/raid/mpr/mpr_user.c:740) takes the user's
`DataSize`/`DataOutSize` (raw `uint32_t` from `mpr_pass_thru_t`) and, when the
request's `Function` byte is `MPI2_FUNCTION_NVME_ENCAPSULATED` (0x33 — a byte
the user fully controls in the request header, checked at mpr_user.c:945),
calls:

```c
mpr_build_nvme_prp(sc, cm, nvme_encap_request, cm->cm_data,
    data->DataSize, data->DataOutSize);          /* mpr_user.c:970 */
```

`cm->cm_data` was allocated as `kmalloc(MAX(DataSize, DataOutSize))` —
**with the result never checked** (mpr_user.c:904-914): a size the kernel
cannot satisfy leaves `cm->cm_data == NULL`, and the PRP build still runs.

`mpr_build_nvme_prp()` (sys/dev/raid/mpr/mpr.c:2680-2864) translates the
length into a **linear walk of 8-byte PRP entries through one contiguous
arena** and **never bounds-checks the walk against the arena end** — it relies
entirely on `length` being sane:

```c
while (length) {                                  /* mpr.c:2757 */
        ... *prp_entry = htole64(paddr); prp_entry++; prp_entry_phys++; ...
        if (entry_len > length) length = 0; else length -= entry_len;
}
```

The arena is `PAGE_SIZE * ((maxio/PAGE_SIZE)/511 + 1) * NVME_QDEPTH`
(mpr.c:1593-1598) with `NVME_QDEPTH = MPR_REQ_FRAMES = 2048` (mprvar.h:40,59)
— 8 MiB for any maxio ≤ ~2 MB. The number of entries needed for `length L`
is `ceil(L/4096) + ceil(L/4096)/511` (one extra list pointer per full PRP
page, mpr.c:2765-2783). For `L = 0xFFFFFFFF` that is **1,050,625 entries =
8,405,000 bytes > the 8,388,608-byte arena** — the walk runs **16,416 bytes
past the end even from chunk 0**, and since the walk may begin at an
arbitrary free chunk (`mpr_alloc_prp_page`, mpr.c:2716), starting from a
higher chunk index overruns proportionally more — up to **~8.2 MB of OOB
write** when the first allocated chunk is near the end of the arena (see
`replication_run.log`). The walk happens **before** `mpr_wait_command()`
(mpr_user.c:1050) — no firmware cooperation is needed; the only gate is the
driver being attached (arena allocated at attach).

Written values are semi-controlled: `paddr` starts at
`(bus_addr_t)(uintptr_t)data` (mpr.c:2750) — the kernel virtual address of
`cm->cm_data` (or 0 when the kmalloc failed / was skipped) — and increments
by 4096 per entry. Deterministic, groomable length; 8-byte aligned writes.

## Trigger (privileged surface)

`/dev/mprN` 0640 root:operator (mpr_user.c:205); O_RDONLY open + ioctl
suffices (ioctls not gated on open mode — demonstrated live in the DF-1326
replica run on the same node mode). One
`ioctl(MPTIOCTL_PASS_THRU, {Function=0x33, DataDirection=READ, DataSize=
0xFFFFFFFF, PtrRequest->...})` from an operator-group user. Note
`cm->cm_length = MAX(DataSize, DataOutSize)` with DataSize=0xFFFFFFFF →
`kmalloc` of 4 GB fails fast on this class of machine → `cm->cm_data=NULL`
→ `paddr=0` → the walk still writes 1M+ entries. (A smaller DataSize with a
successful kmalloc writes KVA-derived values instead.)

## Replication (guest, userspace arithmetic-level)

`prp_overwalk.c` replicates the walk/arena arithmetic verbatim
(`replication_run.log` on 6.5-DEVELOPMENT):

```
maxio=128KB -> pages_required=1 arena=8388608 bytes (1048576 entries)
  DataSize=4294967295 start_chunk=0    walk=8405024 in_arena=8388608 OOB=16416   <== OOB WRITE
  DataSize=4294967295 start_chunk=512  ...                                 OOB=2113568 <== OOB WRITE
  DataSize=4294967295 start_chunk=2000 ...                                 OOB=8208416 <== OOB WRITE
  DataSize=1073741824 start_chunk=1536 ...                                 OOB=4104    <== OOB WRITE
```

## Hardware gate & fix

Live trigger requires an mpr-attached controller with the NVMe-encapsulated
path compiled in (any SAS3 with NVMe support; the *build* of the PRP list
needs no FW round-trip). Fix: validate `DataSize`/`DataOutSize` against
`sc->maxio` before building (reject > maxio), check the `kmalloc` result,
and add an arena-end guard in the walk (`prp_entry` vs
`prp_page + pages_available`).
