mpr_build_nvme_prp heap overflow via unbounded user-supplied data length
Summary
mpr_build_nvme_prp() at mpr.c:2716: allocates 1 PRP page (sc->prp_buffer_size from sc->maxio). Loop at :2757 while(length) driven by caller data_in_sz/data_out_sz with NO bound check. Caller mpr_user_pass_thru (mpr_user.c:904/970-971) passes user DataSize/DataOutSize with NO maxio cap (only RequestSize is capped). DataSize=8MB >> maxio=1MB -> prp_entry writes thousands of 8-byte entries past PRP page into kernel heap. /dev/mprN is 0640 root:operator (operator group). Fix: bound data length against sc->prp_buffer_size/sc->maxio before loop.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1227 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | full path:line trace + reachability + threat model | 5.0 KB | β raw |
| README.md | readme | claim, verdict, runnable-PoC instructions, fix | 2.5 KB | β raw |
| mpr_nvme_prp_overflow.c | trigger-source | runnable ioctl PoC: MPTIOCTL_PASS_THRU with DataSize=8MB, Function=NVME_ENCAPSULATED | 4.3 KB | view raw |
| build.sh | build-script | cc -O -I/usr/src/sys -o mpr_nvme_prp_overflow mpr_nvme_prp_overflow.c | 307 B | view raw |
| run.sh | run-script | runs PoC if /dev/mpr0 exists, else reports unreachable | 655 B | view raw |
| run.log | run-log | PoC build + run output on this guest (open /dev/mpr0 ENOENT) | 729 B | view raw |
| fix.diff | suggested-fix | cap user DataSize/DataOutSize to sc->maxio in mpr_user_pass_thru (root-cause fix) | 985 B | view raw |
| fix_build.log | build-log | GENERIC kernel build with the fix applied (rc=0; mpr_user.o/mpr.ko clean) | 5.6 MB | β download |
| env.txt | environment | guest uname, cc, device topology | 718 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-1227 β mpr_build_nvme_prp heap overflow via unbounded user-supplied data length
Claim
mpr_build_nvme_prp() (sys/dev/raid/mpr/mpr.c:2716) allocates one PRP
list page (mpr_alloc_prp_page β a single PAGE_SIZE buffer holding
PAGE_SIZE/8 = 512 entries). Its while(length) loop (mpr.c:2757) is
driven by data_in_sz/data_out_sz, which the ioctl caller
mpr_user_pass_thru() (sys/dev/raid/mpr/mpr_user.c) copies verbatim from
the user's mpr_pass_thru.DataSize/DataOutSize with no cap against
sc->maxio (only RequestSize is capped, at mpr_user.c:804). A user
DataSize of e.g. 8 MB describes ~2048 pages β the loop writes ~1536 PRP
entries past the end of the single PRP page β kernel heap overflow with
attacker-controlled physical addresses. Reachable on the NVMe-encapsulated
branch (function == MPI2_FUNCTION_NVME_ENCAPSULATED, mpr_user.c:945).
Verdict
NOT TESTABLE on this audit guest (real bug, traced line-by-line; fix
authored and compile-validated into GENERIC). The guest has no LSI SAS
(mpr) controller, so /dev/mpr0 does not exist and the ioctl is
unreachable here. See VERDICT.md. A runnable PoC (mpr_nvme_prp_overflow.c)
is included β it triggers the overflow on any host that (a) has an attached
mpr(4) controller and (b) lets the caller open /dev/mprN rw (root or the
operator group; mpr_user.c:205 creates the node UID_ROOT, GID_OPERATOR,
0640).
Why not reproduced here
pciconf -lshows no LSI/SAS controller and there is no/dev/mpr*; the cdev is created only inmpr_attach(mpr_user.c:205).- Even with the controller, the node is
0640 root:operatorβ a non-operator unprivileged user (e.g.maxx, uid 1001, not in wheel/operator) cannot open it. So the realistic privilege boundary is operator/root β kernel (a local hardening gap), not unpriv β root.
Trigger (on suitable hardware)
cc -O -I/usr/src/sys -o mpr_nvme_prp_overflow mpr_nvme_prp_overflow.c ./mpr_nvme_prp_overflow /dev/mpr0 # as root or operator
Issues MPTIOCTL_PASS_THRU with Function = MPI2_FUNCTION_NVME_ENCAPSULATED
and DataSize = 8 MB. On an unpatched kernel mpr_build_nvme_prp overflows
its single PRP page before the ioctl returns.
Fix
fix.diff caps the user DataSize/DataOutSize to sc->maxio in
mpr_user_pass_thru, right after the existing RequestSize cap
(mpr_user.c:804) β the root-cause fix matching how the rest of the driver
treats maxio as the maximum transfer.
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/)
mpr_user_pass_thru()(mpr_user.c) servicesMPTIOCTL_PASS_THRU(mpr_ioctl.h:371,_IOWR('I', 4, struct mpr_pass_thru)).mpr_user.c:800copyin's the request header;mpr_user.c:804caps onlyRequestSizetosc->reqframesz.DataSize/DataOutSizeare never capped againstsc->maxio.mpr_user.c:904βcm->cm_length = MAX(data->DataSize, data->DataOutSize);mpr_user.c:908βcm->cm_data = kmalloc(cm->cm_length, M_MPRUSER, ...);(an 8 MB userDataSizeβ 8 MB kernel alloc β itself a DoS vector).mpr_user.c:945βif (function == MPI2_FUNCTION_NVME_ENCAPSULATED)(user-controlledFunctionbyte in the request header,mpi2.h:859; value0x33,mpi2.h:719):mpr_user.c:970-971βmpr_build_nvme_prp(sc, cm, nvme_encap_request, cm->cm_data, data->DataSize, data->DataOutSize);mpr.c:2716βprp_page_info = mpr_alloc_prp_page(sc);allocates ONE PRP list page (PAGE_SIZE, holding5128-byte entries).mpr.c:2751-2754βlength = data_in_sz ? data_in_sz : data_out_sz;(the uncapped user value).mpr.c:2757while (length) { ... *prp_entry = htole64(paddr); prp_entry++; prp_entry_phys++; ... length -= entry_len; }β one PRP entry perPAGE_SIZEof user buffer. Withlength = 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 (paddrderived fromcm->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 -llists no LSI/LSI-Fusion SAS controller; there is no/dev/mpr0(the cdev is created only inmpr_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 theoperatorgroup can open it rw.maxx(uid 1001) is not inoperator, 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:
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.
Fix verification
not_testablecompile validated -Werror
module/kernel build rc=0
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source-confirmed. mpr_build_nvme_prp DataSize unbounded vs 1 PRP page -> heap overflow. No mpr controller. Runnable PoC included.
No comments yet.