β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-1227

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1227 Β· 11 files
FileTypeDescriptionSize
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
README.md readme claim, verdict, runnable-PoC instructions, fix
↓ download 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 -l shows no LSI/SAS controller and there is no /dev/mpr*; the cdev is created only in mpr_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.

VERDICT.md verdict full path:line trace + reachability + threat model
↓ download raw

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:

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_testable

compile 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.