NVMe-encapsulated pass-thru: unvalidated u32 DataSize drives PRP walk past the 8MiB arena (deterministic OOB write); kmalloc result unchecked
Summary
mpr_user_pass_thru passes raw user DataSize/DataOutSize into mpr_build_nvme_prp whenever the user-controlled Function byte is 0x33; the PRP build's while(length) walk writes 8-byte entries linearly through one contiguous 8MiB arena with no end check, BEFORE any firmware round-trip. DataSize=0xFFFFFFFF needs 1,050,625 entries -> 16,416 bytes OOB even from chunk 0, ~8.2MB when the first free chunk is near the end. cm_data=kmalloc(MAX(...)) result never checked - a failed 4GB kmalloc leaves cm_data=NULL and the walk still runs. Single ioctl, no FW cooperation needed. Arithmetic replicated verbatim on guest. Validate-against-maxio + NULL check + arena guard fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4009 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| prp_overwalk.c | replication-source | verbatim PRP-walk / arena-size arithmetic | 3.2 KB | view raw |
| replication_run.log | run-log | OOB distances across DataSize x start_chunk | 5.7 KB | view raw |
| build.log | build-log | guest cc output + uname | 150 B | view raw |
| README.md | readme | finding summary + trigger + fix sketch | 4.1 KB | β raw |
| verdict.json | verdict | machine verdict | 3.3 KB | view raw |
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:
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:
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).
Fix verification
not_testablefix.diff not yet authored (see README.md fix sketch)
Confirmed kernel references
- sys/dev/raid/mpr/mpr_user.c:904
- sys/dev/raid/mpr/mpr_user.c:908
- sys/dev/raid/mpr/mpr_user.c:945
- sys/dev/raid/mpr/mpr_user.c:970
- sys/dev/raid/mpr/mpr.c:1593
- sys/dev/raid/mpr/mpr.c:2680
- sys/dev/raid/mpr/mpr.c:2716
- sys/dev/raid/mpr/mpr.c:2750
- sys/dev/raid/mpr/mpr.c:2757
- sys/dev/raid/mpr/mprvar.h:40
- sys/dev/raid/mpr/mprvar.h:59
Detail
Exploit chain
operator-group ioctl user -> MPTIOCTL_PASS_THRU {Function=0x33, DataDirection=READ, DataSize=0xFFFFFFFF} -> kmalloc(4GB)=NULL unchecked (mpr_user.c:908) -> mpr_build_nvme_prp(..., NULL, 0xFFFFFFFF, 0) at mpr_user.c:970 -> 1M+ entry linear write marching past the 8MiB DMA arena into adjacent kernel memory with attacker-chosen length. Requires an mpr-attached controller; the OOB write itself needs no FW cooperation (occurs before mpr_wait_command).
Evidence (decisive lines)
replication_run.log (OOB distances for DataSize x start_chunk incl. 16416 bytes from chunk 0 and 8208416 bytes from chunk 2000), prp_overwalk.c (verbatim walk/arena arithmetic), VERDICT.md
PoC changes
n/a (new finding; replication authored during pass 2)
Verified recommended fix
Validate DataSize/DataOutSize <= sc->maxio before the NVMe branch (mpr_user.c:945), check the kmalloc result at :908, and bound the PRP walk against the arena end in mpr_build_nvme_prp().
Verdict
Mechanism certain at source level, hardware-gated live: MPTIOCTL_PASS_THRU with Function=0x33 (NVME_ENCAPSULATED, user-controlled byte) feeds raw user u32 DataSize/DataOutSize into mpr_build_nvme_prp() whose while(length) walk (mpr.c:2757) writes 8-byte PRP entries linearly through the 8 MiB contiguous PRP arena with no end check, before any firmware interaction. For DataSize=0xFFFFFFFF the walk needs 1,050,625 entries (8,405,000 bytes) vs a 1,048,576-entry arena -> 16,416 bytes of OOB write even from chunk 0, scaling to ~8.2 MB when the first allocated PRP chunk sits near the arena end; kmalloc failure for the 4 GB cm_data is NOT checked (mpr_user.c:908) and does not gate the walk (paddr=0 variant). Written values are semi-controlled (0- or KVA-based incrementing dwords in 8-byte steps). Not live-reproduced: no mpr controller on the guest; the walk/arena arithmetic is replicated verbatim on-guest (prp_overwalk).
No comments yet.