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

Forced-polling mode leaks the pci_alloc_1intr MSI allocation on every xhci attach/detach cycle (release gated on a never-created IRQ resource)

Summary

pci_alloc_1intr allocates classic-MSI vectors unconditionally, but in forced-polling mode (hw.usb.xhci.use_polling=1) the IRQ resource is never created and pci_release_msi is only called inside if(sc_irq_res): every polling-mode attach/detach cycle leaks MSI messages and rid entries from the finite vector pool until later device bring-up silently degrades to INTx. Reproduced on nec-usb-xhci,msi=on,msix=off + tunable: clean detach, then the harness pci_release_msi probe returned 0 (vectors still allocated); unconditional-release fix validated (probe -> 19/ENODEV). Privileged boot-tunable context, slow DoS only.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5546 Β· 14 files
FileTypeDescriptionSize
README.md β€” 2.8 KB ↓ raw
VERDICT.md β€” 3.9 KB ↓ raw
msileak.c β€” 5.8 KB view raw
Makefile β€” 105 B ↓ download
build.sh β€” 243 B view raw
run.sh β€” 139 B view raw
build.log β€” 1002 B view raw
run.log β€” 860 B view raw
run.fix.log β€” 505 B view raw
run.hw-precondition.log β€” 1.2 KB view raw
fix_build.log β€” 1.4 KB view raw
fix.diff β€” 735 B view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 5.5 KB view raw

DF-5546 β€” xhci_pci polling mode leaks the MSI allocation on every detach

What

xhci_pci_attach() calls pci_alloc_1intr(self, xhci_use_msi, &rid, &irq_flags) unconditionally (sys/bus/u4b/controller/xhci_pci.c:234) — when the controller exposes classic MSI this allocates MSI vectors, sets rid=1 and adds the rid→irq resource-list entries. In forced polling mode (hw.usb.xhci.use_polling=1 boot tunable) the IRQ resource is then never created (:253-268 skips bus_alloc_resource_any), and xhci_pci_detach() calls pci_release_msi() only under if (sc->sc_irq_res) (:366-371). Result: on every polling-mode attach/detach cycle (kldunload/kldload, hot-(un)plug) the MSI message allocation, its resource-list entries and sc_irq_rid state leak — there is no IRQ resource to gate the release on. Repeated cycles drain the finite MSI vector pool, degrading future device bring-up.

Also note the dead comment at :266 β€” the "FALLTHROUGH - use polling" after IRQ-allocation failure actually goto errors at :326 unless the polling tunable was set; only the tunable path reaches the leak.

Reproduce (baseline, unpatched kernel)

Guest needs a classic-MSI xHCI and the tunable:

/boot/loader.conf.local: hw.usb.xhci.use_polling=1
QEMU_EXTRA="-device nec-usb-xhci,msi=on,msix=off" dfbsd-qemu/vm.sh up
# boot log shows: xhci0: Interrupt polling at 100Hz
./build.sh
./run.sh     # kldload msileak.ko; sysctl hw.df5546=1

Expected decisive output (run.log):

  1. xhci0 pre-detach: sc_irq_rid=1 sc_irq_res=0 sc_intr_hdl=0 β€” MSI allocated by the glue, NO IRQ resource (polling mode)
  2. device_detach(xhci0) returned 0 β€” clean detach, children fine
  3. pci_release_msi(xhci0) probe returned 0 (LEAK: driver left MSI vectors allocated) β€” the harness had to release what the driver leaked (keeps the guest usable)

(-device qemu-xhci does NOT work for this: that emulated device is MSI-X only and DF's pci_alloc_msi is classic-MSI only, so nothing is allocated β€” see run.hw-precondition.log.)

Fix validation

fix.diff (unconditional pci_release_msi(self) after the IRQ block in detach β€” a no-op returning ENODEV when nothing is allocated). Applied in guest /usr/src together with the DF-5545 fix in one rebuild; rebooted with the same tunable + NEC controller; identical run (run.fix.log):

pci_release_msi(xhci0) probe returned 19 (clean: nothing left to release) β€” the driver itself released the vectors during detach.

Honest scope

Requires the root-set boot tunable (polling mode is a diagnostic fallback) and MSI-capable hardware; the trigger context (module unload / hot-unplug) is privileged. Impact is resource exhaustion / device-init degradation, not memory disclosure. Low severity, mechanics certain and reproduced end-to-end.

VERDICT.md
↓ download raw

DF-5546 VERDICT β€” reproduced (resource leak), fix validated

Bottom line

In forced-polling mode (hw.usb.xhci.use_polling=1), the xHCI PCI glue allocates classic-MSI vectors for the controller (pci_alloc_1intr, xhci_pci.c:234, unconditional) but never creates the IRQ resource (:253-268), and its detach releases MSI only when an IRQ resource exists (:366-371). Proven live on the guest with a classic-MSI NEC uPD720200 (-device nec-usb-xhci,msi=on,msix=off): polling-mode attach leaves sc_irq_rid=1, sc_irq_res=NULL, sc_intr_hdl=NULL; after a fully clean device_detach(xhci0) (usbus detaches fine β€” no veto involved), pci_release_msi(xhci0) still had work to do β€” it returned 0 and released the vectors the driver leaked. On the fixed kernel (fix.diff applied in the guest, same rebuild as DF-5545) the identical run prints probe returned 19 (clean: nothing left to release).

Root-cause chain (path:line)

  1. xhci_pci.c:232-235 β€” pci_alloc_1intr(self, xhci_use_msi /*=1*/, &rid, &irq_flags): on MSI-capable hardware this runs pci_alloc_msi(dev, &rid, 1, cpu) (sys/bus/pci/pci.c:4365) β†’ rid=1, msi_alloc=1, vector + resource-list entries created.
  2. xhci_pci.c:253 β€” polling tunable set β†’ the whole IRQ-resource block is skipped; attach proceeds with the callout poller (:320-324).
  3. xhci_pci.c:362-371 β€” detach: sc->sc_irq_res == NULL β†’ the block containing pci_release_msi(self) is skipped entirely.
  4. Nothing else in the driver or the PCI layer releases driver-allocated MSI on driver detach β†’ leaked msi_alloc message + rid entries per attach/detach cycle; the pool is finite (MSI vector space shared by all devices; exhaustion makes later pci_alloc_msi fail β€” devices silently fall back to INTx or fail to arm interrupts).

Hardware precondition (documented hunt)

First attempt used -device qemu-xhci (1b36:000d): that device implements MSI-X only (pciconf: cap 11[90] = MSI-X, no cap 05) while DF's pci_alloc_1intr/pci_alloc_msi path allocates classic MSI only β€” nothing was allocated, probe correctly returned ENODEV (run.hw-precondition.log). Switching to -device nec-usb-xhci,msi=on,msix=off (classic MSI present: cap 05[70] = MSI supports 16 messages, 64 bit; chip 0x01941033 is in this driver's own probe/quirk tables at xhci_pci.c:94 and :210) satisfied the precondition and the leak reproduced immediately. Real xHCI silicon commonly exposes classic MSI (the NEC part in the driver's own table does), so the precondition is ordinary hardware + the diagnostic tunable.

Runs

  • Baseline (pristine kernel, polling boot, NEC controller): sc_irq_rid=1 sc_irq_res=0 sc_intr_hdl=0 β†’ detach returns 0 β†’ pci_release_msi(xhci0) probe returned 0 (LEAK: driver left MSI vectors allocated) (run.log). Harness releases the leaked vectors itself so the guest stays usable.
  • Fixed kernel (combined DF-5545+DF-5546 rebuild, same controller + tunable, fresh boot): identical pre-state sc_irq_rid=1 sc_irq_res=0, detach returns 0, probe returned 19 (clean: nothing left to release) (run.fix.log).

Exploitability

Privileged-only: boot tunable (root, loader.conf) + module-unload / hot-unplug context. Impact = kernel resource leak (vector pool + PCI resource-list entries), a slow local DoS on device bring-up β€” not an info leak, not memory corruption. Low severity, mechanics certain.

DF-5464 (icu MSI/MSI-X count math, different subsystem/class) was checked against this file: the DF branch here uses pci_alloc_1intr with a fixed count of 1 β€” no count arithmetic, no KASSERT-only validation on this path. Not a recurrence.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to guest /usr/src together with the DF-5545 fix in one quickkernel+installkernel rebuild; identical trigger on the same emulated hardware and tunable now yields pci_release_msi probe ENODEV(19) 'clean: nothing left to release' -- the driver releases the vectors itself during detach. Leak gone; no regression to the normal IRQ-mode detach path (release is idempotent).

run.fix.log; fix_build.log (xhci_pci.c recompile + installkernel completed Wed Oct  7 09:43:48 UTC 2026)
↓ fix.diffper-fix-DF-5546

Confirmed kernel references

Detail

Exploit chain

privileged: loader.conf hw.usb.xhci.use_polling=1 (root) -> xhci_pci_attach on MSI-capable controller: pci_alloc_1intr allocates MSI (rid=1), polling mode skips bus_alloc_resource_any -> later kldunload/PCI hot-unplug -> xhci_pci_detach: sc_irq_res==NULL so the pci_release_msi block is skipped -> leaked msi_alloc vectors + rid entries accumulate per cycle -> MSI pool exhaustion degrades future pci_alloc_msi (devices fall back to INTx or lose interrupts). No unprivileged route.

Evidence (decisive lines)

run.log (baseline: pre-detach sc_irq_rid=1 sc_irq_res=0 sc_intr_hdl=0 -> detach returned 0 -> 'pci_release_msi(xhci0) probe returned 0 (LEAK: driver left MSI vectors allocated)'); run.fix.log (fixed kernel, same pre-state -> 'probe returned 19 (clean: nothing left to release)'); run.hw-precondition.log (qemu-xhci MSI-X-only null result that led to the NEC classic-MSI controller); msileak.c/Makefile/build.sh/run.sh (harness); build.log; fix_build.log (shared rebuild with DF-5545, xhci_pci.c recompiled, installkernel completed); fix.diff

PoC changes

Seed concept only; harness written fresh (msileak.c KLD): reads the polling-mode attach state from the live softc, walks the PCI capability list to locate the MSI cap, performs device_detach(xhci0) from a module kernel thread (gd_sysctllock constraint, same as DF-5544/5545 rigs), then uses pci_release_msi()'s return value as the decisive leak oracle (0 == vectors were still allocated = leak; ENODEV == clean). Two hardware attempts were needed: -device qemu-xhci exposes MSI-X only so DF's classic-MSI alloc fails silently (rid stayed 0, probe ENODEV -- saved as run.hw-precondition.log); -device nec-usb-xhci,msi=on,msix=off provides the classic MSI cap and the leak reproduced.

Verified recommended fix

Call pci_release_msi(self) unconditionally in xhci_pci_detach() after the IRQ-resource block (it is a no-op returning ENODEV when nothing is allocated), so polling-mode attaches cannot leak the MSI allocation.

Verdict

xhci_pci_attach() allocates classic-MSI vectors unconditionally via pci_alloc_1intr() (xhci_pci.c:234, pci.c:4365) but in forced-polling mode (hw.usb.xhci.use_polling=1) never creates the IRQ resource (:253-268), and xhci_pci_detach() releases MSI only under 'if (sc->sc_irq_res)' (:366-371) -- with no IRQ resource the release is skipped and the MSI message allocation, its rid resource-list entries and sc_irq_rid state leak on every polling-mode attach/detach cycle. Proven live: guest booted with -device nec-usb-xhci,msi=on,msix=off (classic-MSI xHCI; qemu-xhci is MSI-X-only and does not exercise the path -- documented in run.hw-precondition.log) plus the boot tunable; polling attach state sc_irq_rid=1/sc_irq_res=0; after a fully clean device_detach(xhci0) (usbus detached normally, no veto involved) the harness's pci_release_msi(xhci0) probe returned 0 -- i.e. the driver left vectors allocated (the harness then released them to keep the guest usable). On the kernel rebuilt with fix.diff (unconditional pci_release_msi(self) in detach, a no-op when nothing is allocated) the identical run returns ENODEV(19) 'clean: nothing left to release'. Impact ceiling: privileged-context kernel resource leak (finite MSI vector pool + per-device resource-list entries) degrading future device interrupt setup across kldload/kldunload or hot-plug cycles -- a slow local DoS, not an info leak or memory corruption. Root boot tunable + privileged unload required => Low severity; mechanics certain and reproduced end-to-end.