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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4818 Β· 12 files
FileTypeDescriptionSize
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:

  1. NULL irq reaches bus_setup_intr β€” if bus_alloc_resource_any fails for vector k>=1 (kernel memory pressure in rman_reserve_resource, or rman/map desync) while vector 0 succeeds, attach completes with sc->irq[k] == NULL. The admin thread later runs nvme_create_comqueue() which calls bus_setup_intr(sc->dev, sc->irq[ivect], ...) (nvme.c:823) for ivect == qid; on NULL the call chain reaches nexus_setup_intr() which does an unconditional panic("nexus_setup_intr: NULL irq resource!") (sys/platform/pc64/x86_64/nexus.c:504-506).

  2. NULL irq hits the error unwind β€” if instead a LATER vector fails in pci_alloc_msix_vector after an earlier vector's resource alloc returned NULL, the unwind loop (nvme_attach.c:192-199) calls bus_release_resource(dev, SYS_RES_IRQ, rid, NULL) β†’ resource_list_release() β†’ unconditional panic("resource_list_release: resource entry is not busy") (sys/kern/subr_bus.c:2738-2739).

  3. 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) overwrites sc->irq_type, and detach (line 508-517) only tears MSI-X down for irq_type == PCI_INTR_TYPE_MSIX. msix_table_res/msix_pba_res stay set, so a later re-attach of the same device hits KASSERT(... "MSI-X has been setup yet") in pci_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:

  1. ./run.sh baseline β€” boot dfbsd-qemu/vm_nvme.sh (1 nvme controller, msix_qsize=65, -smp 6): expect nvme0: mapped 7 MSIX IRQs (= min(65, ncpus+1)).
  2. ./run.sh emptypool β€” add hw.ioapic.msi_start="192" to /boot/loader.conf and reboot: per-cpu MSI pool empty β†’ every pci_alloc_msix_vector fails at i=0 β†’ unwind no-op β†’ legacy fallback: nvme0: mapped 1 normal-int IRQs, disk still attaches. (run.2.log)
  3. ./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).

VERDICT.md
↓ download raw

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)

Sink proofs

  1. NULL-irq β†’ panic: nvme_create_comqueue (sys/dev/disk/nvme/nvme.c:823) calls bus_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 unconditional panic(), not an assert.
  2. 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.
  3. 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-cpu irq_rman regions (ioapic_abi.c:1200-1245) are derived from the same map, so a vector that ioapic_abi_msix_alloc hands out is by construction resolvable by rman_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, in pci_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/nvme exists, 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.

  1. 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)
  2. Empty pool (hw.ioapic.msi_start=224 β†’ pool [224,192)=βˆ…): first pci_alloc_msix_vector fails 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)
  3. 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..nvme20 mapped 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 to mapped 1 normal-int IRQs and 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.

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

Fix 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)
↓ fix.diffper-fix-DF-4818

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