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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3818 Β· 6 files
FileTypeDescriptionSize
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: KASSERT is a no-op (sys/sys/systm.h:117) β†’ prp_page_info->prp_page reads 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).

VERDICT.md
↓ download raw

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 bumps sc->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_testable
baseline no→ patch + rebuild →patched clean

No NVMe-behind-SAS3 hardware on the guest; the PRP path cannot be driven.

fix.diff; probe.log.
↓ fix.diffper-fix-DF-3818

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