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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5544 Β· 15 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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-state driver=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_init returns 0 unconditionally (uhci.c:653) and usb_attach returns 0 unconditionally (usb_controller.c:182), so device_probe_and_attach cannot 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 an intr_q that usb_bus_mem_alloc_all TAILQ_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_all failure path (:261-264): self-cleans including lockuninit (usb_controller.c:989-991); device_detach's DS_ATTACHED guard (subr_bus.c:2135-2136) prevents any later uhci_pci_detach on the unattached device β€” no double free.
  • ksprintf(sc->sc_vendor…) (:304-314): max 8 bytes into char sc_vendor[16] (uhci.h:241) β€” safe.
  • DF-1105 (bdev-NULL crash in uhci_reset's device_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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-5544

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.