nvme(4): unchecked MSI-X vector IRQ resource allocation (and missing pci_teardown_msix on fallback) leads to boot-time kernel panics
Summary
sc->irq[i] = bus_alloc_resource_any is unchecked (only irq[0] validated later): a NULL vector survives attach and panics unconditionally in nexus_setup_intr ('NULL irq resource'); the unwind also passes NULLs to bus_release_resource (resource_list_release panic); and the legacy fallback never calls pci_teardown_msix, so a later re-attach trips the pci_setup_msix KASSERT on INVARIANTS kernels (GENERIC runs INVARIANTS - proven live). LIVE-BRACKETED on guest across 3 boots (baseline 7-vector, empty-pool i=0 clean, mid-loop exhaustion at nvme21 with unwind releasing a live vector) - every kernel-reported failure is handled, only the unchecked NULL panics. Rare environmental pressure at attach; boot DoS only. Check+teardown fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4818 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.0 KB | β raw | |
| VERDICT.md | β | 5.5 KB | β raw | |
| run.sh | β | 108 B | view raw | |
| build.sh | β | 111 B | view raw | |
| run.log | β | 545 B | view raw | |
| run.2.log | β | 1.0 KB | view raw | |
| run.3.log | β | 1.9 KB | view raw | |
| panic.txt | β | 1.8 KB | view raw | |
| env.txt | β | 826 B | view raw | |
| fix.diff | β | 1.2 KB | view raw | |
| verdict.json | β | 4.6 KB | view raw | |
| manifest.json | β | 1.3 KB | view raw |
DF-4818 β nvme(4): unchecked MSI-X vector IRQ resource allocation + missing
MSI-X teardown on fallback (nvme_pci_attach)
What this is
nvme_pci_attach() (sys/dev/disk/nvme/nvme_attach.c:149-205) requests one
SYS_RES_IRQ per MSI-X vector:
160: for (i = 0; error == 0 && i < sc->nirqs; ++i) {
162: error = pci_alloc_msix_vector(dev, i, &sc->rid_irq[i], cpu);
164: if (error)
165: break;
166: sc->irq[i] = bus_alloc_resource_any(dev, SYS_RES_IRQ,
167: &sc->rid_irq[i], RF_ACTIVE);
The return of bus_alloc_resource_any() is never checked, and after the
loop only sc->irq[0] is validated (line 239). Three consequences, all
boot/kldload-time kernel panics:
-
NULL irq reaches bus_setup_intr β if
bus_alloc_resource_anyfails for vector k>=1 (kernel memory pressure inrman_reserve_resource, or rman/map desync) while vector 0 succeeds, attach completes withsc->irq[k] == NULL. The admin thread later runsnvme_create_comqueue()which callsbus_setup_intr(sc->dev, sc->irq[ivect], ...)(nvme.c:823) forivect == qid; on NULL the call chain reachesnexus_setup_intr()which does an unconditionalpanic("nexus_setup_intr: NULL irq resource!")(sys/platform/pc64/x86_64/nexus.c:504-506). -
NULL irq hits the error unwind β if instead a LATER vector fails in
pci_alloc_msix_vectorafter an earlier vector's resource alloc returned NULL, the unwind loop (nvme_attach.c:192-199) callsbus_release_resource(dev, SYS_RES_IRQ, rid, NULL)βresource_list_release()β unconditionalpanic("resource_list_release: resource entry is not busy")(sys/kern/subr_bus.c:2738-2739). -
Missing pci_teardown_msix on the MSI-Xβlegacy fallback β when
pci_setup_msix()succeeded but a vector allocation then fails, the fallback path (line 225) overwritessc->irq_type, and detach (line 508-517) only tears MSI-X down forirq_type == PCI_INTR_TYPE_MSIX.msix_table_res/msix_pba_resstay set, so a later re-attach of the same device hitsKASSERT(... "MSI-X has been setup yet")inpci_setup_msix()(sys/bus/pci/pci.c:1644-1645). INVARIANTS kernels only β which includes the GENERIC config of this audit (sys/config/X86_64_GENERIC:56), as proven live by the unrelated vtpci KASSERT panic captured in run.3.log.
How to build / run
There is no C PoC: the trigger is an environmental failure inside the kernel resource allocator at driver-attach time. The evidence pack instead live-validates every reachable sub-path of the MSI-X error machinery on the QEMU guest, bracketing the defect to exactly the unchecked NULL:
./run.sh baselineβ bootdfbsd-qemu/vm_nvme.sh(1 nvme controller, msix_qsize=65, -smp 6): expectnvme0: mapped 7 MSIX IRQs(= min(65, ncpus+1))../run.sh emptypoolβ addhw.ioapic.msi_start="192"to /boot/loader.conf and reboot: per-cpu MSI pool empty β everypci_alloc_msix_vectorfails at i=0 β unwind no-op β legacy fallback:nvme0: mapped 1 normal-int IRQs, disk still attaches. (run.2.log)./run.sh midloopβdfbsd-qemu/vm_nvme24.sh(24 controllers, -smp 2) +hw.ioapic.msi_start="160"(pool 2x32=64 slots vs 243=72 demanded): nvme21 fails MID-loop, the unwind loop runs with a live allocated vector, releases it, falls back; nvme21..23 attach their disks; no nvme panic*. (run.3.log)
The panic itself (manifestations 1-3) could not be forced on the emulator:
manifestation 1/2 need rman_reserve_resource to fail on a freshly granted
MSI-X irq number (kernel-memory pressure / desync β the per-cpu map and rman
are derived from the same source, sys/platform/pc64/apic/ioapic_abi.c:1200-
1245), and manifestation 3 needs a re-attach cycle of a driver compiled into
the kernel (no sys/modules/nvme exists).
Expected output
See run.log (baseline), run.2.log (empty pool), run.3.log (mid-loop + the incidental vtpci panic β a different driver's bug, see VERDICT.md).
DF-4818 VERDICT β nvme_attach.c MSI-X error path
Status: INCONCLUSIVE (code-certain defect; panic trigger not forceable on the emulator; all reachable sub-paths exercised live and proven safe). Expected impact if triggered: kernel panic (boot / driver-load-time DoS).
Root cause (code-proven)
sys/dev/disk/nvme/nvme_attach.c:166-168βsc->irq[i] = bus_alloc_resource_any(...)return value never checked; error only set bypci_alloc_msix_vector.sys/dev/disk/nvme/nvme_attach.c:239β onlysc->irq[0]is validated.sys/dev/disk/nvme/nvme_attach.c:192-199β unwind passessc->irq[i](possibly NULL) straight intobus_release_resource.sys/dev/disk/nvme/nvme_attach.c:225-238β MSI-Xβlegacy fallback does not callpci_teardown_msix()after a successfulpci_setup_msix();sc->irq_typeis overwritten so detach (lines 508-517) skips teardown.
Sink proofs
- NULL-irq β panic:
nvme_create_comqueue(sys/dev/disk/nvme/nvme.c:823) callsbus_setup_intr(sc->dev, sc->irq[ivect], ...)with the unchecked pointer βpci_setup_intr(sys/bus/pci/pci.c:3285) βbus_generic_setup_intr(sys/kern/subr_bus.c:3019) βnexus_setup_intr(sys/platform/pc64/x86_64/nexus.c:498-506):if (irq == NULL) panic("%s: NULL irq resource!", __func__);β an unconditionalpanic(), not an assert. - NULL-irq in unwind β panic:
bus_release_resource(..., NULL)βbus_generic_rl_release_resourceβresource_list_release(sys/kern/subr_bus.c:2734-2739):if (!rle->res) panic("resource_list_ release: resource entry is not busy")β unconditional. - Re-attach after fallback β KASSERT:
pci_setup_msix(sys/bus/pci/pci.c:1644-1645)KASSERT(cfg->msix.msix_table_res == NULL && ... msix_pba_res == NULL, ("MSI-X has been setup yet")). The audit guest runs INVARIANTS (sys/config/X86_64_GENERIC:56) β proven live: the vtpci KASSERT panic in run.3.log fired on this very kernel.
Trigger analysis (why the panic could not be forced live)
- The per-cpu MSI-X pool (
ioapic_irqmaps[cpu], sys/platform/pc64/apic/ ioapic_abi.c:1248-1310) and the per-cpuirq_rmanregions (ioapic_abi.c:1200-1245) are derived from the same map, so a vector thatioapic_abi_msix_allochands out is by construction resolvable byrman_reserve_resource. Failure of the resource step therefore requires kernel-memory exhaustion inside rman/kmalloc or a genuine desync β neither forceable from QEMU device config or loader tunables. - Pool exhaustion (EMSGSIZE from
ioapic_abi_msix_alloc) fails earlier, inpci_alloc_msix_vectorβ the handled path (live-validated, see below). - Manifestation 3 needs a detach+re-attach of the nvme driver; nvme is
compiled into X86_64_GENERIC and no
sys/modules/nvmeexists, so no kldunload/kldload cycle is possible on this guest.
What WAS verified live (guest, INVARIANTS kernel)
Guest: DragonFly 6.5-DEVELOPMENT v6.5.0.1712.g89e6a-DEVELOPMENT (X86_64_GENERIC), QEMU/KVM.
- Baseline (vm_nvme.sh: 1 controller msix_qsize=65, -smp 6):
nvme0: mapped 7 MSIX IRQsβ nirqs = min(65, ncpus+1) exactly per nvme_attach.c:153-156; nominal queue map; disk attached. (run.log) - Empty pool (
hw.ioapic.msi_start=224β pool [224,192)=β ): firstpci_alloc_msix_vectorfails at i=0, unwind no-op, legacy fallback:nvme0: mapped 1 normal-int IRQs, "no MSI-X support, limit comqs to 4", disk attached β no panic. (run.2.log) - Mid-loop failure (vm_nvme24.sh: 24 controllers, -smp 2,
hw.ioapic.msi_start=160β pool 2Γ32=64 vs 24Γ3=72 demanded): nvme0..nvme20mapped 3 MSIX IRQs(63 slots); nvme21 fails at iβ₯1 β the unwind loop at nvme_attach.c:193-199 executes with a live allocated vector, releases it via bus_release_resource + pci_release_msix_vector, falls back tomapped 1 normal-int IRQsand attaches its disk; nvme22/23 fail at i=0 β fallback. No nvme panic anywhere; enumeration proceeds. (run.3.log)
This brackets the defect precisely: every failure the kernel itself reports
is handled safely; the only unhandled failure is the unchecked NULL from
bus_alloc_resource_any.
Incidental live discovery (NOT this finding, different driver)
During experiment 2/3 the boot panicked after all nvme devices attached:
panic: MSI-X is not setup yet pci_release_msix_vector_method() ... vtpci_intr_release() +0xe1 vtnet_attach() +0x909
i.e. sys/dev/virtual/virtio/virtio_pci.c vtpci_intr_release() calls
pci_release_msix_vector() on an error path where MSI-X was never set up /
already torn down (KASSERT at sys/bus/pci/pci.c:1588-1590). Reproduced on
every boot with an exhausted/empty MSI pool (run.2.log, run.3.log). This is a
real, live-reproduced boot-time panic in virtio_pci.c, outside this batch's
two files β surfaced to the orchestrator for a dedicated audit spawn.
Recommended fix
Check the resource allocation and break with an error; tear MSI-X down when falling back after a successful setup. See fix.diff.
Fix validation
Not performed: the panic itself does not reproduce on the emulator (see trigger analysis), so there is no baseline behavior to diff a patched kernel against. fix_status: not_testable.
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_testableFix validation not possible: the panic precondition (bus_alloc_resource_any returning NULL for a granted MSI-X vector) cannot be induced on the emulator, so no baseline bad behavior exists to diff a patched kernel against. The fix is a straightforward guard + teardown whose correctness follows from the code paths exercised live (unwind and fallback already work correctly for pci_alloc_msix_vector failures).
fix.diff (unified, git-apply-able, authored against the read-only sys/ tree; never applied)
Confirmed kernel references
Detail
Evidence (decisive lines)
README.md and VERDICT.md (full path proof with path:line); run.log (baseline: 'nvme0: mapped 7 MSIX IRQs'); run.2.log (empty pool: 'nvme0: mapped 1 normal-int IRQs', disk attaches); run.3.log (mid-loop: nvme0-20 'mapped 3 MSIX IRQs', nvme21/22/23 fallback after unwind executed with an allocated vector, disks attach); panic.txt (INCIDENTAL: live-reproduced vtpci/vtnet panic 'MSI-X is not setup yet' β different driver, surfaced for separate audit; also proves the guest kernel runs INVARIANTS); fix.diff (checks the alloc, tears MSI-X down on fallback).
PoC changes
No seed PoC existed. The trigger is a kernel-internal resource-allocation failure, so the evidence pack is a three-phase QEMU experiment driver (run.sh + dfbsd-qemu/vm_nvme24.sh) using the hw.ioapic.msi_start loader tunable to pressure the per-cpu MSI-X pool, rather than a C program.
Verified recommended fix
Check bus_alloc_resource_any's return in the MSI-X vector loop and fail the MSI-X attempt on NULL; call pci_teardown_msix() on the MSI-X->legacy fallback when pci_setup_msix had succeeded (fix.diff).
Verdict
Code-certain defect, panic not forceable on the emulator. nvme_pci_attach() never checks the return of bus_alloc_resource_any() for MSI-X vector IRQs (nvme_attach.c:166-168) and validates only sc->irq[0] (:239); a NULL sc->irq[k>=1] propagates to bus_setup_intr in nvme_create_comqueue (nvme.c:823) and panics unconditionally in nexus_setup_intr (nexus.c:505-506), or into bus_release_resource in the attach unwind (nvme_attach.c:193-199) and panics in resource_list_release (subr_bus.c:2738-2739); additionally the MSI-X->legacy fallback never calls pci_teardown_msix after a successful pci_setup_msix, leaving msix_table_res set so a re-attach KASSERT-panics on INVARIANTS kernels (pci.c:1644-1645) β the audit GENERIC runs INVARIANTS. Live on the guest every reachable sub-path was exercised and proven safe: baseline 7-vector MSIX attach; empty-MSI-pool i=0 failure with clean legacy fallback (disk attaches); and a forced mid-loop vector failure (24 controllers, pool 64 vs 72 demanded) where the unwind loop released a live allocated vector, fell back, and the disk attached β no nvme panic. This brackets the defect to exactly the unchecked NULL, whose trigger (rman_reserve_resource failure on a freshly granted MSI-X irq = kernel-memory pressure/desync) cannot be induced from QEMU config or loader tunables. Expected impact if triggered: boot/driver-load-time kernel panic (DoS, no unprivileged trigger path).
No comments yet.