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

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)

No comments yet.

PoC verification

Evidence pack

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

VERDICT.md
↓ download raw

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-state driver=ohci state=3 (DS_ATTACHED); hcca_pc.buffer=0xfffff8008df7b000 (was 0xfffff8008df7b000) and ctrl-ED unchanged (DMA intact); the exact hw.df5562=2 touch that killed the baseline prints the state safely β€” guest alive.
  • R2 (retry after child cooperates): sysctl hw.df5562=5 then =1 β†’ clean complete detach, ohci0 STILL has 0 child device(s), return 0 β€” honest success, guest alive. (During this legitimate teardown the resetting controller logs ohci_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_all failure returns ENOMEM before any resource exists (:214-217; the allocator self-cleans). The only reachable unwind entry after bus_setup_intr is ohci_init failure β€” and unlike uhci_init (which returns 0 unconditionally), ohci_init CAN fail (ohci.c:424-425 returns USB_ERR_INVAL when ohci_controller_init fails); the gate at :333 (sc_irq_res && sc_intr_hdl) then reaches ohci_detach, which is safe in exactly that state: the sc_tmo_rhsc callout it stops/drains is usb_callout_init_mtx'd at ohci.c:401 BEFORE ohci_controller_init at ohci.c:424, and MMIO is still mapped. usb_attach returns 0 unconditionally (usb_controller.c:163-182) so device_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/resume pass-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)") into char sc_vendor[16] (ohci.h:253) β€” safe.
  • ohci_pci_take_controller SMM/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 when bus_teardown_intr fails (: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 propagating err, 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

fixed
baseline reproduced→ patch + rebuild →patched clean

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

Confirmed kernel references

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.