NVMe PRP page pool exhaustion: NULL dereference / KASSERT panic above 2048 concurrent native-SGL NVMe I/Os (unprivileged local DoS)
Summary
The PRP pool is fixed at NVME_QDEPTH = 2048 pages while the command pool reaches 2176. Every NVMe-target I/O needing a native SGL pins one page for the command's lifetime; the 2049th mpr_alloc_prp_page() returns NULL and the dereference is guarded only by KASSERT - panic on INVARIANTS kernels (X86_64_GENERIC ships INVARIANTS), NULL-offset page fault otherwise. Unprivileged raw-device I/O at high queue depth on NVMe-behind-SAS3 hosts (supported configuration) -> kernel panic. Pure DoS, no corruption primitive. Achievability of >=2049 concurrent native-SGL I/Os plausible but unproven; code defect certain.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3818 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 1.8 KB | β raw | |
| probe.log | β | 479 B | view raw | |
| fix.diff | β | 1.3 KB | view raw | |
| manifest.json | β | 1.0 KB | view raw | |
| verdict.json | β | 1.8 KB | view raw |
DF-3818 β NVMe PRP page pool exhaustion: NULL-deref / KASSERT-panic under high queue depth (unprivileged local DoS)
What
Two call sites grab a page from the fixed-size PRP pool and dereference it with only an assertion as protection:
/* mpr_build_nvme_prp() β sys/dev/raid/mpr/mpr.c:2716-2720 (ioctl path) */
prp_page_info = mpr_alloc_prp_page(sc);
KASSERT(prp_page_info != NULL, ("%s: There are no PRP Pages left ..."));
prp_page = (uint64_t *)prp_page_info->prp_page; /* NULL deref */
/* mpr_check_pcie_native_sgl() β mpr.c:2988-2991 (I/O path) */
prp_page_info = mpr_alloc_prp_page(sc);
KASSERT(prp_page_info != NULL, ("%s: There are no PRP Pages left ..."));
curr_buff = (uint32_t *)prp_page_info->prp_page; /* NULL deref */
The pool is fixed at NVME_QDEPTH = MPR_REQ_FRAMES = 2048 pages
(mprvar.h:40,59; allocated in mpr_alloc_nvme_prp_pages(), mpr.c:1593-1631),
while the command pool can hold num_reqs = MIN(max_reqframes,RequestCredit) +
MIN(max_prireqframes,HighPriorityCredit) = up to 2048 + 128 = 2176
concurrent commands (mpr.c:375-381). Every NVMe-target command that needs a
native SGL (mpr_data_cb() β mpr_check_pcie_native_sgl(), mpr.c:3483-3487)
holds one PRP page for the command's lifetime. With β₯ 2048 such commands in
flight, the 2049th mpr_alloc_prp_page() returns NULL and:
- on kernels with INVARIANTS (X86_64_GENERIC ships it, sys/config line 56):
panic("mpr_check_pcie_native_sgl: There are no PRP Pages left ..."); - on kernels without:
KASSERTis a no-op (sys/sys/systm.h:117) βprp_page_info->prp_pagereads at offset-of-field from NULL β kernel page-fault panic.
Either way: kernel panic = local DoS.
Threat
Unprivileged users can open the raw block devices exposed for NVMe drives attached through the SAS3 controller and drive queue depth above 2048 (many threads / large async I/O chains). Requires the (supported but uncommon) NVMe-behind-MPT3 configuration. No memory corruption: pure DoS. Severity Medium per the local-DoS rubric row; confidence speculative on achievability (needs β₯2049 concurrent native-SGL NVMe I/Os), certain on the code defect.
Not runnable here
No SAS3 controller (and hence no NVMe-behind-it) on the audit guest (probe.log).
Fix
fix.diff: mpr_check_pcie_native_sgl() falls back to a normal IEEE SGL
(return 1 β its existing "not built here" contract); mpr_build_nvme_prp()
returns without touching NULL (the FW will reject the malformed request β
degraded but non-fatal), plus a proper fix note (make it int-returning).
DF-3818 β VERDICT
status: inconclusive (runtime) β code defect certain; guest lacks both the SAS3 HBA and any NVMe device behind it.
Trace
- Pool sizing:
mpr_alloc_nvme_prp_pages()mpr.c:1593-1631 allocates exactly NVME_QDEPTH (=2048) PRP pages;mpr_alloc_prp_page()(mprvar.h:545-558) returns NULL when the TAILQ is empty (and only bumpssc->prp_page_alloc_fail). - Consumers: mpr.c:2716 (mpr_build_nvme_prp, NVMe-encapsulation ioctl path) and mpr.c:2988 (mpr_check_pcie_native_sgl, the data path for every NVMe target whose transfer needs a native SGL, entered from mpr_data_cb at mpr.c:3483-3487). Both immediately dereference with only KASSERT guarding.
- Demand ceiling: num_reqs up to 2048+128 (mpr.c:375-381) β each concurrent native-SGL NVMe I/O pins one page for its lifetime β 2049th allocation returns NULL.
- Outcome: INVARIANTS kernels (X86_64_GENERIC has
options INVARIANTS) β explicit panic; non-INVARIANTS β NULL+offset read β page-fault panic (KASSERT no-op per sys/sys/systm.h:117).
Why inconclusive at runtime
Guest: driver compiled in (142 pci/mpr), no controller, no /dev/mpr*, and
no NVMe device behind a SAS3 HBA (probe.log). The I/O path that grabs PRP
pages is unreachable.
Impact ceiling
Unprivileged local kernel panic (DoS) on NVMe-behind-SAS3 hosts at very high queue depth. Not a corruption primitive: the failure is a fixed NULL dereference, not a controlled write.
Fix
fix.diff β graceful fallback instead of NULL deref / assert.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testableNo NVMe-behind-SAS3 hardware on the guest; the PRP path cannot be driven.
fix.diff; probe.log.
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md (pool-vs-demand arithmetic); fix.diff (IEEE-SGL fallback / early return).
PoC changes
No runnable PoC without hardware.
Verified recommended fix
Handle mpr_alloc_prp_page() == NULL: fall back to IEEE SGL in mpr_check_pcie_native_sgl(), fail gracefully in mpr_build_nvme_prp(); long term make pool >= num_reqs.
Verdict
Code defect certain: the PRP pool is fixed at NVME_QDEPTH=2048 pages (mpr.c:1593-1631) while the command pool reaches 2176 (mpr.c:375-381); mpr.c:2988-2991 (I/O path via mpr_data_cb mpr.c:3483-3487) and mpr.c:2716-2720 (ioctl path) dereference mpr_alloc_prp_page() NULL guarded only by KASSERT β panic on INVARIANTS kernels (X86_64_GENERIC ships INVARIANTS) or NULL-offset page fault otherwise. Achievability of >=2049 concurrent native-SGL NVMe I/Os from an unprivileged user is plausible but unproven (needs NVMe-behind-SAS3 hardware). Runtime inconclusive: guest has no such hardware (probe.log).
No comments yet.