ohci_pci_detach() swallows device_delete_children() failure: unconditional controller reset, 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 disables bus mastering, halts/resets the controller, tears down and releases IRQ+MMIO, frees all HCCA/ED/TD DMA, and returns 0 - newbus then kfrees/NULLs the softc while the vetoing child stays attached to the driverless parent. Live-proven on a real Apple KeyLargo OHCI (-device pci-ohci): veto -> 'ohci0: detached' returned 0 -> orphan veto0(attached=1) -> next touch Fatal trap 12 at fault VA 0x58 (NULL softc + offsetof hcca_pc.buffer), guest dead in DDB; cmd3 additionally reads the stale freed DMA VAs (UAF read of reusable pages). Fourth proven instance of the family (DF-5544 uhci, DF-5545 xhci, DF-5528 fwohci, DF-4558 atapi-cam); no in-tree vetoing child today, privileged-only trigger. check+propagate fix validated on a rebuilt kernel (returns 16, DMA byte-identical, guest alive; clean retry after cooperation). The DF-5546 MSI-analog was ABSENT (plain INTx rid 0).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5562 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.3 KB | β raw | |
| VERDICT.md | β | 8.2 KB | β raw | |
| veto.c | β | 8.9 KB | view raw | |
| Makefile | β | 98 B | β download | |
| build.sh | β | 168 B | view raw | |
| run.sh | β | 156 B | view raw | |
| build.log | β | 982 B | view raw | |
| run.log | β | 3.3 KB | view raw | |
| run.fix.log | β | 2.5 KB | view raw | |
| panic.txt | β | 1.6 KB | view raw | |
| fix_build.log | β | 1.0 KB | view raw | |
| env.txt | β | 700 B | view raw | |
| fix.diff | β | 616 B | view raw | |
| manifest.json | β | 1.5 KB | view raw | |
| verdict.json | β | 7.2 KB | view raw |
DF-5562 β ohci_pci_detach() swallows device_delete_children() failure
OHCI member of the detach-veto family (DF-5544 uhci_pci.c:374, DF-5545 xhci_pci.c:355, DF-5528 fwohci_pci.c, DF-4558 atapi-cam.c β all proven this pass / earlier).
sys/bus/u4b/controller/ohci_pci.c:329 discards the return of
device_delete_children(self); ohci_pci_detach() then unconditionally
resets the OHCI controller (ohci_detach, :337), tears down + releases
the IRQ (:339-351), releases the MMIO BAR (:352-356), frees all USB DMA
(usb_bus_mem_free_all, :357) and returns 0 (:359). A child that
vetoes detach (newbus contract: device_delete_children β
device_delete_child β device_detach(child) == EBUSY,
sys/kern/subr_bus.c:1284-1292) is left attached to a driverless
parent whose softc was kfree'd by device_set_driver(dev, NULL)
(subr_bus.c:2150 β 1943-1945) β while kldunload / PCI hot-unplug
(devclass_delete_driver, subr_bus.c:889-897) is told the unload
succeeded.
Reproduce (guest with a real OHCI controller)
Boot: QEMU_EXTRA="-device pci-ohci" dfbsd-qemu/vm.sh up (ohci is
compiled into X86_64_GENERIC; QEMU's pci-ohci = Apple KeyLargo
0x003f106b, matched at ohci_pci.c:173-174/266-268).
cd /root/df5562 && make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC kldload /root/df5562/veto.ko sysctl hw.df5562=1 # veto0 child attached + device_detach(ohci0) sysctl hw.df5562=2 # touch parent ctx through the orphan -> panic
Expected (buggy kernel): veto0: DETACH VETO: returning EBUSY β
ohci0: detached β device_detach(ohci0) returned 0 β STILL has 1
child device(s): veto0(attached=1) β Fatal trap 12, fault VA 0x58,
Stopped at df5562_thread+0x91: movq 0x58(%rax),%rcx, guest dead in
DDB. (Observed β see panic.txt / run.log.)
Expected (fix.diff kernel): ohci0: child device failed to detach
(16); refusing controller teardown, returned 16, driver=ohci
state=3, HCCA/ED DMA buffers unchanged, guest alive through the
exact touch that killed the baseline; after the child cooperates
(sysctl hw.df5562=5 then =1) a clean 0-children detach returns 0.
(Observed β see run.fix.log.)
Harness: veto.c (DF-5544 v2 pattern β detach driven from a worker
kernel thread because the sysctl path holds gd_sysctllock).
DF-5562 β VERDICT
Status: reproduced (panic, deterministic under the harness trigger) β fix validated (fixed). Confidence: certain.
The finding
ohci_pci_detach() (sys/bus/u4b/controller/ohci_pci.c:323-360) calls
device_delete_children(self) at :329 and discards the return value.
When a child device vetoes detach β device_delete_children β
device_delete_child β device_detach(child) returning EBUSY
(sys/kern/subr_bus.c:1284-1292, 1326-1340) β the function
nevertheless:
- disables PCI bus mastering (:331),
- resets and halts the OHCI controller (
ohci_detach(sc), :337 β ohci.c:430-446:OHCI_INTERRUPT_DISABLE = OHCI_ALL_INTRS,OHCI_CONTROL = OHCI_HCFS_RESET), - tears down the interrupt (:339-345) and releases the IRQ (:348-351),
- releases the MMIO BAR (:352-356),
- frees all USB DMA memory β HCCA, ED trees, TD pools
(
usb_bus_mem_free_all, :357 β usb_controller.c:1011-1021), - returns 0 (:359).
device_detach() in sys/kern/subr_bus.c:2129-2152 then completes the
"successful" detach: prints ohci0: detached, sets DS_NOTPRESENT, and
device_set_driver(dev, NULL) kfrees dev->softc and NULLs it
(subr_bus.c:1935-1945). The vetoing child is left attached to a
driverless parent with freed IRQ/MMIO/DMA and freed softc β every
subsequent interaction is a NULL-deref or use-after-free, and the caller
(devclass_delete_driver, subr_bus.c:889-897 β kldunload, PCI
hot-unplug) is told the unload succeeded.
Verification (guest, real QEMU OHCI)
-device pci-ohci β ohci0: <Apple KeyLargo/Intrepid USB controller>
mem 0xfebd1000-0xfebd10ff irq 11 at device 3.0 on pci0, healthy
usbus0/uhub0/ugen0.1 at boot (ohci compiled into X86_64_GENERIC,
sys/config/X86_64_GENERIC:305).
Harness veto.ko (this directory, DF-5544 v2 pattern): attaches a child
device veto0 to ohci0 whose detach method returns EBUSY while the
ohci driver is bound β a stand-in for the newbus detach contract β plus
a worker thread that calls device_detach(ohci0) (exactly what
devclass_delete_driver() at subr_bus.c:897 does) on
sysctl hw.df5562=1. The detach is issued from a kernel thread, not the
sysctl handler, because the sysctl path holds the per-CPU
gd_sysctllock across handler dispatch while the usb teardown calls
device_sysctl_fini (DF-5544 v1 lesson β harness artifact, not the
finding).
Observed (run.log, panic.txt β raw serial capture):
usbus0: detached <- child 1 deletes cleanly veto0: DETACH VETO: returning EBUSY <- child 2 vetoes ohci0: detached <- swallowed; full teardown DF5562: device_detach(ohci0) returned 0 <- the lie DF5562: post: ohci0 driver=(none) state=0 DF5562: ohci0 STILL has 1 child device(s): veto0(attached=1) [sysctl hw.df5562=2 -> touch parent ctx via orphan] Fatal trap 12: page fault while in kernel mode fault virtual address = 0x58 <- NULL softc + field offset Stopped at df5562_thread+0x91: movq 0x58(%rax),%rcx db> <- guest dead
The panic address 0x58 is device_get_softc(ohci0) (NULL, kfree'd by
device_set_driver) + offsetof(struct ohci_softc,
sc_hw.hcca_pc.buffer) β sc_hw is the first member of ohci_softc
(ohci.h:223-224) β the orphan's only path to its parent context is
freed memory. The frozen ssh session (kernel died mid-command) is
captured in the run transcript.
Fix validation (fix.diff)
One-line semantic fix: check and propagate. Applied in guest /usr/src,
make quickkernel + make installkernel (completed Wed Oct 7 18:32:42
UTC 2026, kernel #1: Wed Oct 7 18:31:30 UTC 2026), reboot with the same
-device pci-ohci, same harness rebuilt against the new kernel, same
trigger:
- R1 (veto active):
ohci0: child device failed to detach (16); refusing controller teardown;device_detach returned 16; post-statedriver=ohci state=3(DS_ATTACHED);hcca_pc.buffer=0xfffff8008df7b000 (was 0xfffff8008df7b000)and ctrl-ED unchanged (DMA intact); the exacthw.df5562=2touch that killed the baseline prints the state safely β guest alive. - R2 (retry after child cooperates):
sysctl hw.df5562=5then=1β clean complete detach,ohci0 STILL has 0 child device(s), return 0 β honest success, guest alive. (During this legitimate teardown the resetting controller logsohci_interrupt: unrecoverable error, controller haltedβ expected reset noise, guest survives.)
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:189-241), and device_busy() propagates DS_BUSY up
the tree so a busy child makes device_detach(ohci0) 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/MMIO/DMA with reported success). Trigger requires the
privileged detach paths (kldunload / PCI hot-unplug / rescan) plus a
vetoing child β hence Low severity, matching the family precedents
DF-5544 (uhci, proven identically this pass), DF-5545 (xhci, proven
identically this pass), DF-5528 (fwohci) and DF-4558 (atapi-cam). Code
inherited from FreeBSD (ohci_pci_detach ignores the return there too).
Pass-2 negatives recorded during this audit (not reported)
- DF-5546 MSI-analog (checked per the family-sweep lead): no analog.
ohci_pci.c never calls
pci_alloc_1intr/pci_alloc_msi; the IRQ is a plain shareable legacy INTx (bus_alloc_resource_any(SYS_RES_IRQ, rid=0, RF_SHAREABLE|RF_ACTIVE), :240-242) released with the matching rid 0 (:348-351). No polling-mode allocation exists to leak. - Attach error unwinding (:318-320): sound.
usb_bus_mem_alloc_allfailure returns ENOMEM before any resource exists (:214-217; the allocator self-cleans). The only reachable unwind entry afterbus_setup_intrisohci_initfailure β and unlikeuhci_init(which returns 0 unconditionally),ohci_initCAN fail (ohci.c:424-425 returns USB_ERR_INVAL whenohci_controller_initfails); the gate at :333 (sc_irq_res && sc_intr_hdl) then reachesohci_detach, which is safe in exactly that state: thesc_tmo_rhsccallout it stops/drains isusb_callout_init_mtx'd at ohci.c:401 BEFOREohci_controller_initat ohci.c:424, and MMIO is still mapped.usb_attachreturns 0 unconditionally (usb_controller.c:163-182) sodevice_probe_and_attach(bdev)cannot fail. No DF-1105-style NULL-bdev window in this glue (no device_printf through bdev on the error path). - Suspend/resume wiring (:367-368):
bus_generic_suspend/resumepass-through only β no PCI-glue state is touched and no memory-safety consequence exists (functional power-management gap at most: this glue never parks the controller in D3; not filed). - Config-space-supplied sizes:
sc_io_size = rman_get_size(...)(:238) is the BAR size, consumed by ohci.c (that file's own row); nothing in ohci_pci.c allocates by a config-supplied size.ksprintf(sc->sc_vendor, ...)(:259-297) writes at most 8+NUL bytes ("CMDTECH"/"(0x%04x)") intochar sc_vendor[16](ohci.h:253) β safe. ohci_pci_take_controllerSMM/BIOS handoff (:92-109): saves BAR+INTLINE, sets D0, restores both β boot-time console takeover path, no attacker-reachable window.- Sun PCIO-2 intpin quirk (:226-227): constant write of 4 when devid matches and intpin==0 β benign.
return (0)also lies whenbus_teardown_intrfails (:339-345, "XXX or should we panic?"): folded into this finding's root cause β the fix returns the child error first; a teardown failure would additionally warrant propagatingerr, covered by the same check-and-propagate discipline.
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 rebuild (completed Wed Oct 7 18:32:42 UTC 2026), reboot with the same -device pci-ohci and the same harness: identical trigger returns 16 with 'child device failed to detach (16); refusing controller teardown', driver still bound (driver=ohci state=3), HCCA/ctrl-ED DMA buffers byte-identical, no panic on the hw.df5562=2 touch that killed the baseline; retry after the child cooperates completes a clean 0-children detach returning 0. Bad behavior gone.
run.fix.log; fix_build.log (quickkernel + installkernel for X86_64_GENERIC completed Wed Oct 7 18:32:42 UTC 2026)
Confirmed kernel references
- sys/bus/u4b/controller/ohci_pci.c:329
- sys/bus/u4b/controller/ohci_pci.c:331
- sys/bus/u4b/controller/ohci_pci.c:337
- sys/bus/u4b/controller/ohci_pci.c:348
- sys/bus/u4b/controller/ohci_pci.c:352
- sys/bus/u4b/controller/ohci_pci.c:357
- sys/bus/u4b/controller/ohci_pci.c:359
- sys/kern/subr_bus.c:1284
- sys/kern/subr_bus.c:1326
- sys/kern/subr_bus.c:2129
- sys/kern/subr_bus.c:2150
- sys/kern/subr_bus.c:1943
- sys/kern/subr_bus.c:889
- sys/bus/u4b/controller/usb_controller.c:189
- sys/bus/u4b/controller/ohci.c:430
Detail
Exploit chain
privileged: kldunload ohci / PCI hot-unplug -> devclass_delete_driver (subr_bus.c:889-897) -> device_detach(ohci0) -> ohci_pci_detach -> device_delete_children fails (child veto at subr_bus.c:1284-1292) -> return value discarded at ohci_pci.c:329 -> ohci_detach() halts/resets the HC, IRQ torn down + released, MMIO BAR released, usb_bus_mem_free_all frees HCCA/ED/TD DMA pages, returns 0 -> device_set_driver(NULL) kfrees softc, device_get_softc(ohci0) becomes NULL -> still-attached child operates on freed/NULL parent state -> supervisor page fault (observed, fault va 0x58) / 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 -> 'ohci0: detached' -> returned 0 -> veto0(attached=1) -> Fatal trap 12 fault va=0x58, Stopped at df5562_thread+0x91 movq 0x58(%rax),%rcx, db>); run.log (baseline transcript + root-cause chain); run.fix.log (patched kernel: R1 'child device failed to detach (16); refusing controller teardown', returned 16, driver=ohci state=3, hcca/ctrl-ED buffers unchanged, guest alive through the killing touch; R2 cooperative retry -> clean 0-children detach); veto.c/Makefile/build.sh/run.sh (harness); build.log; fix_build.log (quickkernel+installkernel completed Wed Oct 7 18:32:42 UTC 2026); env.txt; fix.diff (git-apply --check verified)
PoC changes
Adapted the proven DF-5544 uhci harness to ohci (DRIVER_MODULE(veto, ohci, ...), devclass_find("ohci"), snapshot of sc_hw.hcca_pc / sc_hw.ctrl_start_pc DMA pages instead of the uhci frame list, sysctl hw.df5562). Guest booted with QEMU_EXTRA='-device pci-ohci' (Apple KeyLargo OHCI at pci0:0:3:0); ohci 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.df5562=2 exactly as predicted (fault va 0x58 = NULL softc + offsetof(sc_hw.hcca_pc.buffer)); fix validated on a quickkernel-rebuilt guest with the same rig.
Verified recommended fix
Check device_delete_children(self) at ohci_pci.c:329 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
ohci_pci_detach() discards device_delete_children()'s return at sys/bus/u4b/controller/ohci_pci.c:329 and unconditionally disables bus mastering, resets the controller via ohci_detach() (:337), tears down and releases the IRQ (:339-351), releases the MMIO BAR (:352-356), frees all USB DMA via usb_bus_mem_free_all() (:357) and returns 0 (:359). Demonstrated live on the QEMU guest with a real OHCI controller (-device pci-ohci, Apple KeyLargo 0x003f106b, ohci compiled into X86_64_GENERIC): with one child vetoing detach, device_detach(ohci0) reported complete success ('ohci0: detached', returned 0), left the vetoing child ATTACHED to the now-driverless ohci0 whose softc had been kfree'd/NULLed by device_set_driver(dev,NULL) (subr_bus.c:2150 -> 1943-1945) -- the next touch of the parent context through the orphan was a Fatal trap 12 page fault (NULL softc + 0x58 field offset = offsetof(ohci_softc, sc_hw.hcca_pc.buffer), Stopped at df5562_thread+0x91 movq 0x58(%rax),%rcx), guest dead in DDB with the ssh session frozen mid-command. 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:189-241) -- exact OHCI family recurrence of DF-5544 (uhci, proven identically this pass), DF-5545 (xhci, 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 (quickkernel+installkernel Wed Oct 7 18:32:42 UTC 2026, kernel #1 18:31:30): identical trigger now returns 16 with 'child device failed to detach (16); refusing controller teardown', driver still bound (state=3 DS_ATTACHED), HCCA/ctrl-ED 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 returning 0.
No comments yet.