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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5394 Β· 14 files
FileTypeDescriptionSize
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:

  1. 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_bus is freed underneath the running explore kthread β†’ use-after-free.
  2. wild-softc (observed, deterministic once the interleaving lands): the racing mutation of the device object leaves garbage in dev->softc (observed 0xfffffffffffffff8, 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 #2
  • boot.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_detach and 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.md
↓ download raw

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 does device_set_softc(dev, bus) β€” the usbus device's softc is NULL for the entire duration; at :794 it arms the 4-second power_wdog callout.
  • usb_detach() (:188-243) treats device_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 (observed 0xfffffffffffffff8), passes the NULL guard and executes usb_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_msignal of 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 :191 read to land inside the async window; a slow console (socket serial) throttles the attacher at its :736 printf 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)

  1. 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.
  2. usb_detach(): move usb_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, clean usbus0: detached each 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

fixed
baseline reproduced→ patch + rebuild →patched clean

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

Confirmed kernel references

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.