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

xhci_pci_detach() swallows device_delete_children() failure: unconditional IRQ/MMIO/DMA teardown and kfree'd softc under a still-attached child, with detach reporting success

Summary

device_delete_children's return is discarded: a child veto still halts the controller, releases IRQ+MMIO, frees event/command-ring/DCBAA/scratchpad DMA, and returns 0 - newbus then kfrees/NULLs the softc while the vetoing child stays attached to the orphaned parent. Reproduced live on qemu-xhci: veto -> 'xhci0: detached' returned 0 -> orphan veto0(attached=1) -> Fatal trap 12 (movq 0x20(%rax), NULL softc), guest dead in DDB. Exact recurrence of the DF-5544/DF-5528/DF-4558 family idiom (FreeBSD heritage); no in-tree u4b child vetoes today, DS_BUSY propagates first - proven corruption consequence of a violated API contract on privileged unload/hot-unplug. check+propagate fix validated (EBUSY, DMA intact, clean retry).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5545 Β· 16 files
FileTypeDescriptionSize
README.md β€” 3.2 KB ↓ raw
VERDICT.md β€” 5.4 KB ↓ raw
veto.c β€” 8.9 KB view raw
Makefile β€” 99 B ↓ download
build.sh β€” 209 B view raw
run.sh β€” 156 B view raw
build.log β€” 2.0 KB view raw
run.log β€” 30 B view raw
run.fix.log β€” 84 B view raw
panic.txt β€” 1.5 KB view raw
serial_console_full.log β€” 14.8 KB view raw
fix_build.log β€” 1.4 KB view raw
env.txt β€” 363 B view raw
fix.diff β€” 522 B view raw
manifest.json β€” 1.2 KB view raw
verdict.json β€” 5.8 KB view raw

DF-5545 β€” xhci_pci_detach() swallows device_delete_children() failure and tears down under a live child

What

sys/bus/u4b/controller/xhci_pci.c:355 discards the return value of device_delete_children(self) and unconditionally proceeds to halt the controller, tear down the interrupt, release the IRQ + MMIO BAR, free all USB DMA memory (xhci_uninit β†’ usb_bus_mem_free_all, :378) and report success (:380).

If any child device vetoes detach (device_detach(child) != 0, sys/kern/subr_bus.c:1292), the parent still completes: newbus finishes the "successful" detach, unbinds the driver and β€” via device_set_driver(dev, NULL) in device_detach (sys/kern/subr_bus.c:2156) β€” kfrees the softc. The vetoing child remains attached to a driverless parent whose IRQ/MMIO/DMA are gone and whose softc is freed memory: a lying kldunload/hot-unplug and a guaranteed use-after-free / NULL-deref for whatever the child does next.

Exact family recurrence of DF-5544 (uhci_pci.c:374), DF-5528 (fwohci_pci.c) and DF-4558 (atapi-cam.c), inherited verbatim from FreeBSD.

Reproduce (baseline, unpatched kernel)

Guest needs a real xHCI controller (NEC variant also serves DF-5546): QEMU_EXTRA="-device qemu-xhci" dfbsd-qemu/vm.sh up

./build.sh          # builds veto.ko in the guest (KLD harness)
./run.sh            # kldload veto.ko; sysctl hw.df5545=1; sysctl hw.df5545=2

Expected output (see run.log / panic.txt / serial_console_full.log): 1. veto0: DETACH VETO: returning EBUSY β€” the child refuses 2. xhci0: detached + device_detach(xhci0) returned 0 β€” swallowed 3. xhci0 STILL has 1 child device(s): veto0(attached=1) β€” orphan 4. Fatal trap 12: page fault ... fault virtual address = 0x20 β€” Stopped at df5545_thread+0x91: movq 0x20(%rax),%rdx β€” kernel panic (NULL softc; on other access patterns, UAF read of the freed ring/DCBAA DMA pages via cmd 3)

Fix validation

fix.diff (checked + propagated return). Applied in guest /usr/src together with the DF-5546 fix, make quickkernel && make installkernel, reboot, rerun the identical trigger (run.fix.log): returned 16 β€” honest failure, xhci0: child device failed to detach; refusing controller teardown, driver still bound (state=3), ring/DCBAA buffers untouched, no panic on the same cmd2 that killed the baseline; after the child cooperates (hw.df5545=5) a retry completes a clean 0-children detach. (The literal "SUCCESS ... (the lie)" label in the final retry is a static harness string β€” the STILL has 0 child device(s) line right after it is the actual state.)

Honest scope

The vetoing child is a KLD stand-in for the newbus detach contract β€” no in-tree u4b child detach returns nonzero today (usb_detach returns 0 unconditionally, sys/bus/u4b/controller/usb_controller.c:242), and a DS_BUSY child propagates busy upward so device_detach(xhci0) itself fails first. The hazard is the violated API contract: any current or future child (or a grandchild detach veto resurfacing through device_delete_child's recursive retry, subr_bus.c:1292-1298) triggers full teardown under a live child. Root-context trigger (kldunload / PCI hot-unplug) β‡’ Low severity; mechanics certain and reproduced.

VERDICT.md
↓ download raw

DF-5545 VERDICT β€” reproduced (panic), fix validated

Bottom line

xhci_pci_detach() at sys/bus/u4b/controller/xhci_pci.c:355 ignores the return of device_delete_children(self). Demonstrated live on the QEMU guest with a real xHCI controller (first qemu-xhci, identical mechanics later re-validated on nec-usb-xhci): with one child vetoing detach, device_detach(xhci0) reported complete success (xhci0: detached), left the vetoing child ATTACHED to the now-driverless xhci0 whose softc had been kfree'd/NULLed by device_set_driver(dev, NULL) (subr_bus.c:2156) β€” the next touch of the parent context through the orphan was a Fatal trap 12 page fault (NULL softc + 0x20 field offset, df5545_thread+0x91 movq 0x20(%rax),%rdx), guest dead in DDB.

This is the xHCI instance of the family idiom: DF-5544 (uhci_pci.c:374, proven the same way this pass), DF-5528 (fwohci_pci.c), DF-4558 (atapi-cam.c). FreeBSD heritage: the same swallow exists in FreeBSD's xhci_pci.c.

Root-cause chain (path:line)

  1. xhci_pci.c:355 β€” device_delete_children(self); return discarded.
  2. subr_bus.c:1292 β€” child veto (device_detach(child) != 0) makes it return EBUSY; the still-attached child stays in xhci0's children list.
  3. xhci_pci.c:357-378 β€” teardown proceeds anyway: callout drain, controller halt, busmaster off, intr teardown, IRQ released, MSI released, MMIO BAR released (:372-376), xhci_uninit(sc) (:378) frees all DMA (event/command ring page, DCBAA page, scratchpads; xhci.c:675-686 usb_bus_mem_free_all) and destroys sc_cmd_cv/sc_cmd_lock.
  4. xhci_pci.c:380 β€” return (0): detach lies to the caller.
  5. subr_bus.c:2151-2157 β€” success path: devclass_delete_device, dev->state = DS_NOTPRESENT, device_set_driver(dev, NULL) β‡’ softc kfree'd, device_get_softc(xhci0) β‡’ NULL.
  6. Orphaned attached child + freed softc + unmapped BAR + freed DMA β‡’ NULL-deref (observed) or UAF on freed DMA pages (cmd 3 path).

Runs

  • Baseline (kernel #0 Thu Jul 2 06:02:54 2026, pristine): run.sh transcript in run.log; decisive serial output in panic.txt / serial_console_full.log. Guest panicked at hw.df5545=2; the ssh session hung (DDB), forcing the documented kill.
  • Fixed kernel: both family fixes (DF-5545 + DF-5546) applied to the guest's /usr/src in one rebuild (fix_build.log shows -c .../xhci_pci.c recompiled and "Kernel install ... completed"; /boot/kernel/kernel md5 29a0c51c...). Identical trigger: returned 16 -- honest failure, xhci0: child device failed to detach; refusing controller teardown, driver=xhci state=3 (DS_ATTACHED), ring/DCBAA buffers byte-identical to the pre-detach snapshot, no panic on the exact cmd2 that killed the baseline (run.fix.log). After hw.df5545=5 (child cooperates), retry detaches cleanly: returned 0, STILL has 0 child device(s).

Exploitability

Trigger context is privileged (kldunload / PCI hot-unplug equivalent β€” the harness drives device_detach(xhci0) exactly as devclass_delete_driver does, subr_bus.c:885-897). No unprivileged reach through this file: no in-tree u4b child detach returns nonzero (usb_detach is unconditional-0 at usb_controller.c:242), and a DS_BUSY child propagates EBUSY upward before this code can swallow it. So: latent-API-hazard + proven memory-safety consequence β‡’ Low severity, mechanics certain. No route from unprivileged input was found (checked: ugen open state does not veto newbus detach; the u4b device tree is not newbus-children of usbus).

Pass-2 negatives recorded for this file

  • Attach unwind (xhci_pci.c:220-225) releasing only the MMIO BAR is correct: usb_bus_mem_alloc_all self-cleans on partial failure (usb_controller.c:986-988); no xhci_uninit needed (cv/lock are initialized only after alloc_all succeeds, xhci.c:657-663).
  • No DF-5509 analog: the handler is armed at :279 only AFTER xhci_init set the oper/runt offsets (xhci.c:625-630), and xhci_interrupt_locked ACKs unconditionally (xhci.c:1654-1660: any nonzero USBSTS is written back, IMAN pending cleared) β€” no mute-handler IRQ storm window.
  • MSI count math: DF branch uses pci_alloc_1intr with fixed count=1 (pci.c:4348-4375); no MSI-X count math exists in this file; no DF-5464-style KASSERT-only validation on this path (that class was icu_abi-internal).
  • Suspend/resume delegated to usbus children (usb_suspend/usb_resume exist, usb_controller.c:105-108) which drive xhci_set_hw_power_sleep via bus_methods (xhci.c:4489); glue wiring complete, nothing size/count-bearing.
  • Quirk tables (devidβ†’usedma32 at :209-218, Intel route/imod at :296-308) are compile-time constant switches; sc_vendor ksprintf "0x%04x" into char[16] is bounded (xhci.h).
  • sc_io_size (BAR size, :206) is never used as a bound by xhci (only uhci/ohci use it for barriers) β€” nothing config-space-supplied feeds sizes/counts in this file beyond the already-filed DF-1106/DF-2336 extended-capability walk (not re-reported here).
  • BIOS handoff wait (:411-423) is bounded (500 Γ— 10 ms) and requires a BIOS-owned semaphore; not attacker-reachable.

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 (check + propagate device_delete_children return) applied to guest /usr/src together with the DF-5546 fix in one quickkernel+installkernel rebuild; identical trigger returns 16 with 'refusing controller teardown', driver still bound, state DS_ATTACHED, DMA buffers unchanged, no panic on the cmd2 that killed the baseline; retry after the child cooperates completes a clean 0-children detach. Bad behavior gone.

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

Confirmed kernel references

Detail

Exploit chain

privileged: kldunload xhci / PCI hot-unplug -> devclass_delete_driver (subr_bus.c:897) -> device_detach(xhci0) -> xhci_pci_detach -> device_delete_children fails (child veto at subr_bus.c:1292) -> return value discarded -> IRQ/MMIO released, xhci_uninit frees event+command ring, DCBAA and scratchpad DMA pages, returns 0 -> device_set_driver(NULL) kfrees softc, device_get_softc(xhci0) becomes NULL -> still-attached child operates on freed/NULL parent state -> supervisor page fault (observed) / UAF reads of freed DMA pages (harness cmd3). Not reachable by an unprivileged user through this file.

Evidence (decisive lines)

panic.txt (raw serial: veto -> 'xhci0: detached' -> returned 0 -> veto0(attached=1) -> Fatal trap 12 fault va=0x20, Stopped at df5545_thread+0x91 movq 0x20(%rax),%rdx); serial_console_full.log; run.log (baseline transcript + root-cause chain); run.fix.log (patched: 'child device failed to detach; refusing controller teardown', returned 16, driver=xhci state=3, ring/DCBAA buffers unchanged, guest alive; cooperate retry -> clean 0-children detach); veto.c/Makefile/build.sh/run.sh (harness); build.log; fix_build.log (xhci_pci.c recompiled, installkernel completed, kernel md5 29a0c51c2e9202a724a7b626b3416d39); env.txt; fix.diff

PoC changes

Adapted the proven DF-5544 uhci harness to xhci (DRIVER_MODULE(veto, xhci, ...), snapshot of sc_hw.root_pc/sc_hw.ctx_pc DMA pages instead of uhci frame list, MSI/polling-agnostic). Guest booted with QEMU_EXTRA='-device qemu-xhci' (vm.sh default has no USB); xhci is compiled into X86_64_GENERIC so the kldunload path was driven via device_detach() from a module kernel thread, identical to devclass_delete_driver(). Baseline panic on hw.df5545=2 exactly as predicted; fix run on the combined-fix rebuilt kernel.

Verified recommended fix

Check device_delete_children(self) at xhci_pci.c:355 and return the error before touching controller resources, so a child veto leaves IRQ/MMIO/DMA/softc fully intact and the caller retries later.

Verdict

xhci_pci_detach() discards device_delete_children()'s return at sys/bus/u4b/controller/xhci_pci.c:355 and unconditionally halts the controller, tears down the interrupt, releases IRQ+MMIO, frees all USB DMA via xhci_uninit (:378) and returns 0 (:380). Demonstrated live on the QEMU guest with a real xHCI controller: with one child vetoing detach, device_detach(xhci0) reported complete success ('xhci0: detached'), left the vetoing child ATTACHED to the now-driverless xhci0 whose softc had been kfree'd/NULLed by device_set_driver(dev,NULL) (subr_bus.c:2156) -- the next touch of the parent context through the orphan was a Fatal trap 12 page fault (NULL softc + 0x20 field offset, Stopped at df5545_thread+0x91 movq 0x20(%rax),%rdx), guest dead in DDB. The vetoing child is a KLD stand-in for the newbus detach contract (no in-tree u4b child detach returns nonzero today; usb_detach returns 0 unconditionally at usb_controller.c:242) -- exact family recurrence of DF-5544 (uhci, proven identically this pass), DF-5528 (fwohci) and DF-4558 (atapi-cam). Root-context trigger (kldunload/PCI hot-unplug) => Low severity; mechanics certain. fix.diff (check + propagate return) validated on a rebuilt guest kernel (combined with the DF-5546 fix, one rebuild): identical trigger now returns 16 with 'refusing controller teardown', driver bound, state DS_ATTACHED, ring/DCBAA DMA buffers byte-identical, no panic on the touch that killed the baseline; a later retry after the child cooperates completes a clean 0-children detach.