uhci_pci_detach() swallows device_delete_children() failure: unconditional IRQ/IO/DMA teardown and kfree'd softc under a still-attached child, with detach reporting success
Summary
The return of device_delete_children is discarded: when a child vetoes detach the function still stops/resets the controller, tears down the interrupt, releases IRQ/IO, frees all USB DMA, and returns 0 - newbus then kfrees/NULLs the softc while the vetoing child stays attached to a driverless parent, and kldunload/hot-unplug is told it succeeded. Reproduced live on a real PIIX3 UHCI (veto child -> device_detach returns 0 with orphan attached=1 -> next touch Fatal trap 12 on NULL softc+0x5458, guest dead in DDB). No in-tree u4b child vetoes today (usb_detach returns 0) and DS_BUSY propagates first - but any future vetoing child becomes a guaranteed UAF with a lying success return; same class as DF-5528/DF-4558, inherited from FreeBSD. check+propagate fix validated (EBUSY 16, resources intact, clean retry after cooperation).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5544 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.3 KB | β raw | |
| VERDICT.md | β | 6.5 KB | β raw | |
| veto.c | β | 8.2 KB | view raw | |
| Makefile | β | 98 B | β download | |
| build.sh | β | 127 B | view raw | |
| run.sh | β | 149 B | view raw | |
| build.log | β | 982 B | view raw | |
| run.log | β | 2.8 KB | view raw | |
| run.fix.log | β | 1.9 KB | view raw | |
| panic.txt | β | 1.4 KB | view raw | |
| fix_serial_console_full.log | β | 14.7 KB | view raw | |
| env.txt | β | 819 B | view raw | |
| fix.diff | β | 524 B | view raw | |
| manifest.json | β | 1.6 KB | view raw | |
| verdict.json | β | 5.1 KB | view raw |
DF-5544 β uhci_pci_detach() swallows device_delete_children() failure and tears down under a live child
What
sys/bus/u4b/controller/uhci_pci.c:374 discards the return value of
device_delete_children(self) and unconditionally proceeds to stop the
controller, tear down the interrupt, release IRQ + I/O, free all USB DMA
memory (usb_bus_mem_free_all, :409) and report success (:411).
If any child device vetoes detach (device_detach(child) != 0), the
parent still completes: newbus finishes the "successful" detach, unbinds
the driver and β via device_set_driver(dev, NULL) in
sys/kern/subr_bus.c β kfrees the softc. The vetoing child remains
attached to a driverless parent whose IRQ/IO/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.
Reproduce (baseline, unpatched kernel)
Guest needs a real UHCI controller:
QEMU_EXTRA="-device piix3-usb-uhci" dfbsd-qemu/vm.sh up
./build.sh # builds veto.ko in the guest (KLD harness) ./run.sh # kldload veto.ko; sysctl hw.df5544=1; sysctl hw.df5544=2
Expected output (see run.log / panic.txt):
1. veto0: DETACH VETO: returning EBUSY β the child refuses
2. uhci0: detached + device_detach(uhci0) returned 0 β swallowed
3. uhci0 STILL has 1 child device(s): veto0(attached=1) β orphan
4. Fatal trap 12: page fault ... df5544_thread β kernel panic
Fix validation
fix.diff (checked + propagated return). Apply in guest /usr/src,
make quickkernel && make installkernel, reboot, rerun the same
trigger: returned 16 -- honest failure, controller/DMA intact, no
panic; retry after the child cooperates detaches cleanly (run.fix.log).
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(uhci0) 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) triggers full teardown under a
live child. Same class as DF-5528 (fwohci_pci.c) and DF-4558
(atapi-cam.c), inherited verbatim from FreeBSD.
DF-5544 β VERDICT
Status: reproduced (panic, deterministic under the harness trigger) β fix validated (fixed). Confidence: certain.
The finding
uhci_pci_detach() (sys/bus/u4b/controller/uhci_pci.c:369-412) calls
device_delete_children(self) at :374 and discards the return value.
When a child device vetoes detach β device_delete_children β
device_delete_child β device_detach(child) returning EBUSY or a
driver error (sys/kern/subr_bus.c:885-890, 2128-2145) β the function
nevertheless:
- stops and resets the controller under the bus lock (:380-387),
- tears down the interrupt (:390-399),
- releases IRQ (:400-403) and I/O (:404-408),
- frees all USB DMA memory and destroys the bus DMA tags
(
usb_bus_mem_free_all, :409 β usb_controller.c:1011-1021), - returns 0 (:411).
device_detach() in sys/kern/subr_bus.c then completes the "successful"
detach: prints uhci0: detached, sets DS_NOTPRESENT, and
device_set_driver(dev, NULL) kfrees dev->softc and NULLs it.
The vetoing child is left attached to a driverless parent with freed
IRQ/IO/DMA and freed softc β every subsequent interaction is a NULL-deref
or use-after-free, and the caller (devclass_delete_driver, kldunload,
PCI hot-unplug) is told the unload succeeded.
Verification (guest, real QEMU UHCI)
-device piix3-usb-uhci β uhci0: <Intel 82371SB (PIIX3) USB
controller> port 0xc080-0xc09f irq 10 at device 3.0 on pci0, healthy
usbus0/uhub0/ugen0.1 at boot.
Harness veto.ko (this directory): attaches a child device veto0 to
uhci0 whose detach method returns EBUSY while the uhci driver is
bound β a stand-in for the newbus detach contract β plus a worker
thread that calls device_detach(uhci0) (exactly what
devclass_delete_driver() at subr_bus.c:897 does) on
sysctl hw.df5544=1. v1 of the harness triggered from the sysctl
handler itself and self-deadlocked on the per-CPU gd_sysctllock
(handlers run under it; the usb teardown calls device_sysctl_fini)
β a harness artifact, documented, not the finding; v2/v3 use the
worker thread.
Observed (run.log, panic.txt β raw serial in panic.txt):
usbus0: detached <- child 1 deletes cleanly veto0: DETACH VETO: returning EBUSY <- child 2 vetoes uhci0: detached <- swallowed; full teardown DF5544: device_detach(uhci0) returned 0 <- the lie DF5544: post: uhci0 driver=(none) state=0 DF5544: uhci0 STILL has 1 child device(s): veto0(attached=1) [sysctl hw.df5544=2 -> touch parent ctx via orphan] Fatal trap 12: page fault while in kernel mode fault virtual address = 0x5458 <- NULL softc + field offset Stopped at df5544_thread+0x2a1: movq 0x5458(%rax),%rcx db> <- guest dead
The panic address 0x5458 is device_get_softc(uhci0) (NULL, kfree'd by
device_set_driver) + the softc field offset β the orphan's only path
to its parent context is freed memory.
Fix validation (fix.diff)
One-line semantic fix: check and propagate. Applied in guest /usr/src,
make quickkernel + make installkernel, reboot (kernel #1: Wed Oct 7
09:02:16 UTC 2026), same harness, same trigger:
- R1 (veto active):
uhci0: child device failed to detach (16); refusing controller teardown;device_detach returned 16; post-statedriver=uhci state=3;pframes_pc.buffer=0xfffff8008df7b000(unchanged β DMA intact); guest alive after the exact touch that panicked the baseline. - R2 (retry after child cooperates): clean complete detach,
uhci0 STILL has 0 child device(s), return 0 β honest success, guest alive (run.fix.log).
fix_status: fixed (baseline reproduced / patched behavior correct).
Honest scope / severity calibration
The veto child is synthetic: no in-tree u4b child detach returns
nonzero today (usb_detach returns 0 unconditionally,
usb_controller.c:242), and device_busy() propagates DS_BUSY up the
tree (subr_bus.c:1846-1857) so a busy child makes device_detach(uhci0)
fail before the glue runs. The defect is the violated newbus detach
contract plus its proven consequence (deterministic teardown under a
live child β freed softc/IRQ/IO/DMA with reported success). Trigger
requires the privileged detach paths (kldunload / PCI hot-unplug /
rescan) plus a vetoing child β hence Low severity, matching the
same-class precedents DF-5528 (fwohci_pci.c, this pass) and DF-4558
(atapi-cam.c). Code inherited verbatim from FreeBSD (uhci_pci_detach
also ignores the return there).
Negative results recorded during this pass-2 (not reported)
- DF-5394 attach-race family: the attach-error unwind entry
(:357-359) is dead code on this driver β
uhci_initreturns 0 unconditionally (uhci.c:653) andusb_attachreturns 0 unconditionally (usb_controller.c:182), sodevice_probe_and_attachcannot fail here; the remaining DF-5394 windows (kldunload/hot-unplug of a healthy uhci) belong to the usb_controller.c finding. - DF-5509 interrupt-arming class: no UHCI analog β UHCI_INTR=0 is
written at :281 before the handler is armed at :331, the handler
reads the correct register through tag/handle latched at :276-277 and
acknowledges everything (uhci.c:1438-1484), never touches
sc_bus.bdev(uhci.c:1420-1422 comment), and iterates anintr_qthatusb_bus_mem_alloc_allTAILQ_INIT'd (usb_controller.c:971). - LEGSUP/PIRQ/SMI handoff (:351, :84-89): single atomic config write clearing SMI enables + setting USBPIRQDEN; no park loop, no window.
- Suspend/resume (:419-420): properly wired through
uhci_set_hw_power_sleepβuhci_suspend/uhci_resume(uhci.c:3216-3226, 657-697). usb_bus_mem_alloc_allfailure path (:261-264): self-cleans includinglockuninit(usb_controller.c:989-991);device_detach'sDS_ATTACHEDguard (subr_bus.c:2135-2136) prevents any lateruhci_pci_detachon the unattached device β no double free.ksprintf(sc->sc_vendorβ¦)(:304-314): max 8 bytes intochar sc_vendor[16](uhci.h:241) β safe.- DF-1105 (bdev-NULL crash in
uhci_reset'sdevice_printf(sc->sc_bus.bdev, β¦)at uhci.c:349/366) and DF-5296 (unvalidated BAR size) β known, not re-reported.
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, quickkernel + installkernel + reboot; identical trigger returns 16 with 'refusing controller teardown', driver bound, state DS_ATTACHED, DMA buffers unchanged, no panic on the touch that killed the baseline; retry after the child cooperates completes a clean 0-children detach. Bad behavior gone.
run.fix.log; fix_serial_console_full.log; guest kernel #1 Wed Oct 7 09:02:16 UTC 2026
Confirmed kernel references
Detail
Exploit chain
privileged: kldunload uhci / PCI hot-unplug -> devclass_delete_driver -> device_detach(uhci0) -> uhci_pci_detach -> device_delete_children fails (child veto) -> return value discarded -> IRQ/IO released, usb_bus_mem_free_all frees frame list + QH/TD pools, returns 0 -> device_set_driver(NULL) kfrees softc -> still-attached child operates on freed/NULL parent state -> supervisor page fault (observed) / UAF on kfree'd M_BUS softc and freed DMA pages (e.g. cached pointers). Not reachable by an unprivileged user through this file.
Evidence (decisive lines)
panic.txt (raw serial: veto -> 'uhci0: detached' -> returned 0 -> veto0(attached=1) -> Fatal trap 12, Stopped at df5544_thread+0x2a1 movq 0x5458(%rax)); run.log (baseline transcript + root-cause chain); run.fix.log + fix_serial_console_full.log (patched: 'child device failed to detach (16); refusing controller teardown', returned 16, driver=uhci state=3, pframes buffer unchanged, guest alive; retry clean with 0 children); veto.c/Makefile/build.sh/run.sh (harness); env.txt; fix.diff
PoC changes
v1 triggered device_detach from the sysctl handler and self-deadlocked on the per-CPU gd_sysctllock (usb teardown calls device_sysctl_fini); v2/v3 moved the trigger into a module-owned kernel thread and made the veto child cooperate on demand (hw.df5544=5) so the fix's retry path could be demonstrated. Also: guest booted with QEMU_EXTRA='-device piix3-usb-uhci' (vm.sh default has no USB), uhci is compiled into X86_64_GENERIC so the real kldunload path was driven via device_detach() from the harness, identical to devclass_delete_driver().
Verified recommended fix
Check device_delete_children(self) at uhci_pci.c:374 and return the error before touching controller resources, so a child veto leaves IRQ/IO/DMA/softc fully intact and the caller retries later.
Verdict
uhci_pci_detach() discards device_delete_children()'s return at sys/bus/u4b/controller/uhci_pci.c:374 and unconditionally stops the controller, tears down the interrupt, releases IRQ+I/O, frees all USB DMA memory and returns 0 (:411). Demonstrated live on the QEMU guest with a real PIIX3 UHCI: with one child vetoing detach, device_detach(uhci0) reported complete success ('uhci0: detached'), left the vetoing child ATTACHED to the now-driverless uhci0 whose softc had been kfree'd/NULLed by device_set_driver(dev,NULL) β the next touch of the parent context through the orphan was a Fatal trap 12 page fault (NULL softc + 0x5458 field offset), 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) β same latent-API-hazard posture as DF-5528 (fwohci_pci.c) and DF-4558 (atapi-cam.c), but here proven end-to-end including the panic consequence. Root-context trigger (kldunload/PCI hot-unplug) => Low severity; mechanics certain. fix.diff (check + propagate) validated on a rebuilt guest kernel: identical trigger now returns EBUSY with resources fully intact and no panic; a later retry after the child cooperates completes a clean 0-children detach.
No comments yet.