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)
PoC verification
Evidence pack
findings/poc/DF-5545 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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)
xhci_pci.c:355βdevice_delete_children(self);return discarded.subr_bus.c:1292β child veto (device_detach(child) != 0) makes it return EBUSY; the still-attached child stays in xhci0's children list.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-686usb_bus_mem_free_all) and destroys sc_cmd_cv/sc_cmd_lock.xhci_pci.c:380βreturn (0): detach lies to the caller.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.- 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.shtranscript in run.log; decisive serial output in panic.txt / serial_console_full.log. Guest panicked athw.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.crecompiled 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). Afterhw.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_allself-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_lockedACKs 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_1intrwith 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_resumeexist, usb_controller.c:105-108) which drivexhci_set_hw_power_sleepvia 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_vendorksprintf "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
fixedfix.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)
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.
No comments yet.