DF-5546 / verdict.json
{ "finding_id": "DF-5546", "status": "reproduced", "reproduced": true, "impact": "dos", "confidence": "certain", "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 harnesss 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.", "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": "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", "kernel_refs": [ "sys/bus/u4b/controller/xhci_pci.c:234", "sys/bus/u4b/controller/xhci_pci.c:253", "sys/bus/u4b/controller/xhci_pci.c:266", "sys/bus/u4b/controller/xhci_pci.c:320", "sys/bus/u4b/controller/xhci_pci.c:366", "sys/bus/pci/pci.c:4348", "sys/bus/pci/pci.c:4365" ], "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 DFs 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.", "attempts": 3, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (baseline; fix validated on rebuilt kernel installed Wed Oct 7 09:43:48 UTC 2026, md5 29a0c51c2e9202a724a7b626b3416d39)", "runtime_sec": 2280, "guest_dirty": 0, "build_cmd": "cd /root/df5546 && make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC", "run_cmd": "kldload /root/df5546/msileak.ko; sysctl hw.df5546=1 (boot: -device nec-usb-xhci,msi=on,msix=off + hw.usb.xhci.use_polling=1)", "code_hash": "c26d0f38302d3fdcc55f418d4e71548e55b670670d8fc4bc5620c628250d3b5c", "notes": "Cross-checked the DF-5464 KASSERT class: this files DF branch has no MSI count math (pci_alloc_1intr fixed count=1) -- not a recurrence. The dead FALLTHROUGH - use polling comment at :266 actually goto-errors at :326 unless the tunable is set; only the tunable path reaches the leak. Guest reset to with-src after the run.", "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.", "fix_status": "fixed", "fix_baseline_reproduced": true, "fix_patched_reproduced": false, "fix_verdict": "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).", "fix_evidence": "run.fix.log; fix_build.log (xhci_pci.c recompile + installkernel completed Wed Oct 7 09:43:48 UTC 2026)" } |