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)
PoC verification
Evidence pack
findings/poc/DF-5546 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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):
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)device_detach(xhci0) returned 0β clean detach, children finepci_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.
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)
xhci_pci.c:232-235βpci_alloc_1intr(self, xhci_use_msi /*=1*/, &rid, &irq_flags): on MSI-capable hardware this runspci_alloc_msi(dev, &rid, 1, cpu)(sys/bus/pci/pci.c:4365) β rid=1,msi_alloc=1, vector + resource-list entries created.xhci_pci.c:253β polling tunable set β the whole IRQ-resource block is skipped; attach proceeds with the callout poller (:320-324).xhci_pci.c:362-371β detach:sc->sc_irq_res == NULLβ the block containingpci_release_msi(self)is skipped entirely.- Nothing else in the driver or the PCI layer releases driver-allocated
MSI on driver detach β leaked
msi_allocmessage + rid entries per attach/detach cycle; the pool is finite (MSI vector space shared by all devices; exhaustion makes laterpci_alloc_msifail β 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.
Related
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
fixedfix.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)
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.
No comments yet.