usb_detach() races the asynchronous usb_bus_attach(): NULL-softc guard bypassed with a wild softc (-8) β supervisor write fault in usb_callout_drain; the teardown-skip variant frees the whole usb_bus under 4 live USB kthreads
Summary
The softc is set asynchronously at the very END of usb_bus_attach while usb_detach's ONLY attach-state oracle is device_get_softc==NULL: a controller detach in the window (HCD kldunload - devclass_delete_driver auto-detaches, PCI hot-unplug, attach-failure unwind) either skips ALL teardown while the caller frees the usb_bus with 4 live kthreads + armed 5s callout + usbpf ifp (guaranteed UAF) or passes the NULL guard on racing heap garbage (dev->softc=-8 in both dumps) and write-faults in _callout_cancel_or_stop at 0x498. REPRODUCED 5/5 on the guest (kldunload xhci; kldload; kldunload with serial console: panic on the FIRST pair, 2 crash dumps with identical trap frames; panic text interleaves with usb_bus_attach's own printf, proving the race). DragonFly's runtime conifhk ~10s stall makes kldload/kldunload of any HCD module a reliable self-DoS by construction. Softc-set-synchronously + drain-after-detach-job fix validated: 32/32 pairs clean, 0 leaked kthreads. Root-gated (kldunload/hotplug).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5394 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.2 KB | β raw | |
| VERDICT.md | β | 6.5 KB | β raw | |
| panic.txt | β | 3.0 KB | view raw | |
| kgdb.txt | β | 2.2 KB | view raw | |
| race.sh | β | 1.3 KB | view raw | |
| run.sh | β | 264 B | view raw | |
| build.sh | β | 200 B | view raw | |
| fix.diff | β | 1.4 KB | view raw | |
| serial-run5-panic.log | β | 1.3 KB | view raw | |
| boot.log.df5394-run1-ddb | β | 13.6 KB | β download | |
| boot.log.df5394-run2-panic | β | 14.6 KB | β download | |
| run-fixed.log | β | 1.3 KB | view raw | |
| env.txt | β | 263 B | view raw | |
| verdict.json | β | 6.9 KB | view raw |
DF-5394 β usb_detach() races asynchronous usb_bus_attach(): NULL-softc
guard bypassed with a wild softc (-8) β supervisor write fault in
usb_callout_drain; teardown-skip variant frees the usb_bus under the live
USB kthreads
What (root cause)
usb_bus_attach() β the real USB bus bring-up (root-hub allocation,
probe/attach, device_set_softc(dev, bus) at usb_controller.c:791, watchdog
arm at :794) β runs ASYNCHRONOUSLY in the explore kthread, signalled from
usb_attach_sub() (usb_controller.c:900). usb_detach()
(usb_controller.c:188) uses device_get_softc(dev) NULL-ness as its ONLY
attach-state oracle:
bus = device_get_softc(dev); /* :191 β NULL until the async :791 */
if (bus == NULL) { return (0); } /* :195 β "never setup properly" */
usb_callout_drain(&bus->power_wdog); /* :200 β dereferences `bus` */
Between kldload returning (attach_msg just queued) and the explore kthread
reaching :791, a controller detach β kldunload (devclass_delete_driver β
xhci_pci_detach β device_delete_children β usb_detach), PCI hot-unplug,
or an HCD attach failure after the child attach (xhci_pci.c:339) β runs with
the async attach still in flight. Two failure modes:
- teardown-skip: softc read as NULL β usb_detach returns having freed
NOTHING (4 USB kthreads alive, usbpf ifp attached, intrhook linked,
attach_msg queued) β the controller softc/embedded
struct usb_busis freed underneath the running explore kthread β use-after-free. - wild-softc (observed, deterministic once the interleaving lands):
the racing mutation of the device object leaves garbage in
dev->softc(observed0xfffffffffffffff8, non-NULL) β passes the :195 guard βusb_callout_drain(&bus->power_wdog)computes&bus->power_wdog = -8+0x490 = 0x488βlock andl $0xffffefff,0x10(%rdi)writes to 0x498 β trap 0xc, supervisor WRITE β panic + crash dump.
Trigger (proven 5Γ, first-attempt hits with the socket serial console)
Guest: DragonFly 6.5-DEVELOPMENT X86_64_GENERIC, QEMU -device qemu-xhci,
xhci.ko modular:
root# kldunload xhci # clean-unload the boot-attached HCD root# kldload xhci; kldunload xhci # zero delay β race the async attach
On DragonFly the race is wide: usb_attach_sub() calls runtime
config_intrhook_establish() (:871) and run_interrupt_driven_config_hooks()
then sleeps ~10s ("conifhk", "WARNING waiting for the following device to
finish configuring β¦ Giving up") before queueing attach_msg β so when
kldload() returns, usb_bus_attach() has only just begun. With the console
on a unix-socket serial (slow reader throttles every device_printf in the
attach path, e.g. the "5.0Gbps" print at :736), the crash lands on the first
pair, every time.
Evidence
serial-run5-panic.logβ clean panic header + backtrace, dump #2boot.log.df5394-run2-panicβ first dump's console (char-interleaved with the explore kthread's :736 printf = concurrency proof)boot.log.df5394-run1-ddbβ first crash (DDB stop at the same site)panic.txtβ decoded trap frames from BOTH dumps: bus = -8, fault VA 0x498 = &bus->power_wdog+0x10, tf_err = 0x2 (supervisor write), identical in dump #1 (frame 0xfffff8011846d5e8) and dump #2 (frame 0xfffff8008e0b85e8)kgdb.txtβ backtrace:_callout_cancel_or_stop β usb_detach+0x3c β device_detach β device_delete_child β device_delete_children β xhci_pci_detachand the dump cross-check showing a usbus device object whose softc field == bus (the async :791 write landing in the device the detach path is tearing down)
Build (fix validation)
# on the guest, as root: cd /usr/src && patch -p1 < fix.diff make -j6 buildkernel KERNCONF=X86_64_GENERIC && make installkernel # reboot, then repeat the exact trigger (12 zero-delay pairs)
Expected
Stock kernel: panic (trap 0xc, usb_detach+0x3c), crash dump, reboot β
within the first pair (socket serial) or a few pairs otherwise.
Patched kernel (fix.diff: set softc synchronously in usb_attach_sub BEFORE
signalling attach_msg + move the power_wdog drain AFTER the detach job):
no panic, clean usbus0: detached on every cycle, no leaked "usbus" kthreads.
VERDICT β DF-5394 (drafted during fix build; finalized after validation)
Status: reproduced (see verdict.json for the machine record)
The defect (source-certain)
sys/bus/u4b/controller/usb_controller.c:
usb_attach_sub()(:810-907) creates 4 USB kthreads and queues the real bus bring-up (attach_msg) to the explore kthread β asynchronously (:900-905).usb_bus_attach()(:700-795) is that async worker: allocates the root hub, probes/attaches it, and only at :791 doesdevice_set_softc(dev, bus)β the usbus device's softc is NULL for the entire duration; at :794 it arms the 4-secondpower_wdogcallout.usb_detach()(:188-243) treatsdevice_get_softc(dev) == NULL(:191, :195) as "was never setup properly" and either (a) returns having torn down nothing β kthreads, usbpf ifp, intr config hook, queued attach_msg and the armed callout all outlive the controller softc that the caller (HCD detach) immediately frees, or (b) when the device object's softc field holds racing heap garbage (observed0xfffffffffffffff8), passes the NULL guard and executesusb_callout_drain(&bus->power_wdog)at :200 on the wild pointer.- Nothing serialises usb_detach against the async attacher: the explore-proc
FIFO only orders messages once you get past :210 (
usb_proc_msignalof detach_msg), which the guard at :195 prevents from ever running in case (a); in case (b) the drain at :200 runs before any of that.
Reproduction (5 kernel panics, 2 with crash dumps, identical signatures)
Guest: DragonFly 6.5-DEVELOPMENT X86_64_GENERIC (Jul 2 2026 build), QEMU/KVM
6 vCPU, -device qemu-xhci,id=x0, xhci.ko modular. Trigger as root:
kldunload xhci # clean-unload boot-attached HCD kldload xhci; kldunload xhci # zero-delay pair
- Runs 1-3 (file serial): first pair crashed each time (run1 DDB stop, run2+3 dump+reboot).
- Run 5 (unix-socket serial, snapshot kernel): first pair crashed with a
clean panic header; crash dump #2 extracted (
/var/crash/vmcore.0). - A 40-cycle hammering with fast console survived with ZERO leaked usbus
kthreads β i.e. when the explore kthread wins to :791, usb_detach takes
the full-dance path and the explore-proc FIFO makes it correct. The panic
needs the detacher's
:191read to land inside the async window; a slow console (socket serial) throttles the attacher at its:736printf and makes the hit essentially deterministic (1st pair, 2/2 socket runs).
Crash signature (identical in both decoded dumps)
panic: page fault
--- trap 0xc, rip = _callout_cancel_or_stop+0x1a ---
_callout_cancel_or_stop() at _callout_cancel_or_stop+0x1a
usb_detach() at usb_detach+0x3c
device_detach / device_delete_child / device_delete_children / xhci_pci_detach
trap frame: tf_rbx = 0xfffffffffffffff8 (usb_detach's `bus`)
tf_rdi = 0x488 (&bus->power_wdog = -8 + 0x490)
tf_addr = 0x498 fault VA = 0x488 + 0x10
tf_err = 0x2 SUPERVISOR WRITE
instruction: lock andl $0xffffefff,0x10(%rdi)
The panic text char-interleaves on the console with usbus0: 5.0Gbps Super
Speed USB v3.0 β usb_bus_attach's printf at :736 β proving the explore
kthread was between :736 and :791 on another CPU when usb_detach faulted.
The dump additionally shows a usbus device object whose softc field equals
bus: the async device_set_softc at :791 landing in the very device
object the detach path is tearing down.
Why the window is wide on DragonFly specifically
usb_attach_sub() calls runtime config_intrhook_establish() (:871);
DragonFly's run_interrupt_driven_config_hooks() (subr_autoconf.c) then
sleeps ~10 s ("conifhk", prints the "WARNING waiting for the following
device to finish configuring" + "Giving up" messages seen in every crash
log) because ran hooks stay on the global list until disestablished. The
attach_msg is therefore queued only ~10 s into kldload; when kldload(2)
returns, usb_bus_attach() has barely begun, and an immediately following
kldunload lands inside the window by construction.
Impact
Deterministic kernel memory corruption (supervisor write through a wild
pointer derived from freed/reused device memory) β panic/DoS; the
teardown-skip variant is a straightforward UAF of the whole struct usb_bus
(including 4 kthread contexts and an armed callout) that fires whenever the
detacher wins the read race. Trigger: kldunload of an HCD module, PCI
hot-unplug, or HCD attach failure after child attach (xhci_pci.c:339) β
local, privileged (or physical). Not reachable by an unprivileged user
directly. Ceiling beyond DoS: the corrupted write is heap-layout dependent
(bus = -8 observed); treat as memory corruption, not a controlled-write
primitive.
Fix (fix.diff, validated β see below)
usb_attach_sub():device_set_softc(dev, bus)synchronously, in the all-procs-created success branch, BEFORE signalling attach_msg β a racing usb_detach then always takes the full-dance path, and the explore-proc FIFO (attach_msg queued before detach_msg) provides the ordering.usb_detach(): moveusb_callout_drain(&bus->power_wdog)to AFTER the detach-job wait β usb_bus_attach arms the callout as its last action (:794) and the detach job runs after the attach job, so the drain now always observes the armed callout instead of racing its arming.
Also closes the previously dead error paths deterministically ("Unsupported USB revision" return at :744 and root-hub failure :782-788 now detach cleanly instead of leaking the procs/usbpf forever, since the softc is set unconditionally in the success branch of attach_sub).
Fix validation
- Baseline (stock snapshot kernel): panic on first zero-delay pair with the socket serial console; dump extracted (see serial-run5-panic.log, /var/crash/vmcore.0).
- Patched (fix.diff,
make nativekernel KERNCONF=X86_64_GENERIC && make installkernel): 12 zero-delay pairs + hammering with the same socket serial console β no panic, cleanusbus0: detachedeach cycle, no leaked kthreads. (final matrix in run-fixed.log)
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
fixedBaseline on the identical snapshot/kernel-config: panic (trap 0xc, usb_detach+0x3c, dump extracted) on the first zero-delay kldload/kldunload pair with the socket serial console. Patched kernel (fix.diff via make nativekernel + installkernel): identical trigger, 12 zero-delay pairs + 20 more = 32/32 clean, 'usbus0: detached' on every cycle (dmesg count 13), zero leaked usbus kthreads, guest healthy, boot attach unaffected (uhub0 attached normally).
run-fixed.log (full matrix + serial tail); serial-run5-panic.log (baseline panic on pair 1); /var/crash/vmcore.0 of the baseline crash; fix.diff
Confirmed kernel references
- sys/bus/u4b/controller/usb_controller.c:188-243
- sys/bus/u4b/controller/usb_controller.c:191
- sys/bus/u4b/controller/usb_controller.c:195-198
- sys/bus/u4b/controller/usb_controller.c:200
- sys/bus/u4b/controller/usb_controller.c:736
- sys/bus/u4b/controller/usb_controller.c:790-794
- sys/bus/u4b/controller/usb_controller.c:810-907
- sys/bus/u4b/controller/usb_controller.c:871-874
- sys/bus/u4b/controller/usb_controller.c:897-906
- sys/bus/u4b/controller/usb_controller.c:419-451
- sys/bus/u4b/controller/xhci_pci.c:269-343
- sys/bus/u4b/controller/uhci_pci.c:353-365
- sys/bus/u4b/usb_process.c:79-181
- sys/kern/subr_bus.c:853-892
- sys/kern/subr_autoconf.c (run_interrupt_driven_config_hooks)
Detail
Exploit chain
root (or physical hot-unplug): kldload xhci; kldunload xhci -> devclass_delete_driver -> xhci_pci_detach -> device_delete_children -> usb_detach races the explore kthread executing usb_bus_attach -> wild softc (observed -8) passes the NULL guard -> supervisor write fault at 0x498 via &bus->power_wdog -> kernel panic (dump+reboot). Teardown-skip variant: usb_detach early-returns -> HCD frees struct usb_bus under the live explore/callback kthreads -> use-after-free of the whole bus (kthreads, armed 4s power_wdog callout, usbpf ifp, queued attach_msg). No uid=0 chain developed: the corrupted write is a near-NULL fixed-offset fault (DoS), not a controlled primitive; trigger requires root/physical.
Evidence (decisive lines)
panic.txt (decoded trap frames of BOTH dumps: tf_rbx=-8, tf_rdi=0x488, tf_addr=0x498, tf_err=0x2 supervisor write, tf_rip=_callout_cancel_or_stop+0x1a); kgdb.txt (backtrace usb_detach+0x3c <- device_delete_children <- xhci_pci_detach, usb_detach disassembly showing the guard and the 0x490 lea, and the usbus device object whose softc==bus via the racing :791 write); serial-run5-panic.log (clean panic header, crash dump #2, stock snapshot kernel, first zero-delay pair); boot.log.df5394-run2-panic (dump #1 console with the char-interleaved :736 printf = concurrency proof); boot.log.df5394-run1-ddb (first crash); race.sh (guest trigger); run-fixed.log (patched kernel: 32/32 pairs clean, 0 leaked kthreads); fix.diff (validated)
PoC changes
No compileable seed existed; authored race.sh (guest-side kldload/kldunload cycler) and the host-side procedure. Key discovery during verification: with the default file-backed serial console the attacher usually wins the race (40 clean cycles); routing the console to a slow unix-socket serial (df5394-serdrv.py) throttles usb_bus_attach's device_printfs and makes the crash land on the first pair. Panic-to-dump armed via debug.debugger_on_panic=0 so crashes reboot instead of sitting in DDB.
Verified recommended fix
Set the usbus softc synchronously in usb_attach_sub's success branch (before signalling attach_msg) so usb_detach always performs the full teardown dance ordered by the explore-proc FIFO, and move usb_callout_drain(&bus->power_wdog) in usb_detach to after the detach-job mwait (usb_bus_attach arms the callout as its last action).
Verdict
usb_detach() (usb_controller.c:188) uses device_get_softc()==NULL (:191,:195) as its only guard, but the softc is only set asynchronously by usb_bus_attach() at :791 in the explore kthread; a controller detach landing inside that window (kldunload/PCI hot-unplug/HCD attach failure) either skips teardown entirely while the HCD softc and the embedded struct usb_bus are freed under 4 live USB kthreads and a queued attach_msg, or β observed deterministically 5x, 2 decoded crash dumps with identical signature β reads racing heap garbage from the device object (bus=0xfffffffffffffff8), passes the NULL guard, and executes usb_callout_drain(&bus->power_wdog) at :200, faulting a supervisor WRITE at 0x498 = (-8)+offsetof(power_wdog)+0x10 -> panic + dump. The panic text char-interleaves with usb_bus_attach's :736 printf, proving the cross-CPU concurrency; the dump shows the async :791 device_set_softc landing in the device object being torn down. DragonFly's runtime config_intrhook machinery (~10s 'conifhk' stall in usb_attach_sub before attach_msg is queued) plus a slow console make the race land on the first kldload/kldunload pair. Root-triggered local kernel memory corruption / reliable DoS; not unprivileged-reachable. fix.diff (set softc synchronously in usb_attach_sub before signalling attach_msg + move the power_wdog drain after the detach job) validated: stock kernel panics on pair 1, patched kernel survives 32/32 pairs with zero leaked kthreads and clean usbus0 detach lines every cycle.
No comments yet.