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

ehci_pci_attach arms ehci_interrupt before controller takeover/init: the pre-init handler is mute (reads USBSTS from the HCSPARAMS capability register, sc_eintrs==0 forces exit WITHOUT acknowledging) β€” a level-INTx IRQ storm runs for the entire window, including the up-to-5-second BIOS-handoff park, with no code touching USBINTR/USBSTS until ehci_init's hcreset

Summary

bus_setup_intr runs BEFORE take_controller (whose LEGSUP park sleeps up to 5s) and before ehci_init latches sc_offs/sc_eintrs - through the whole window the M_ZERO'd softc makes the armed handler read 'USBSTS' at the HCSPARAMS register and goto done WITHOUT acknowledging: any BIOS-residue or malicious (USBINTR & USBSTS)!=0 state re-fires level INTx until hcreset. Guest-proven on stock code: 311,162 interrupts through one parked attach vs 55 control (5657x), and a PERMANENT variant (~33K intr/s sustained, no self-heal) when the storm starves the attaching thread. Malicious EHCI silicon/emulator or BIOS handoff residue; boot-time needs no privilege. Quiesce-USBINTR/USBSTS-before-setup_intr fix validated (park storm -> delta 0, full enumeration). Distinct root cause from DF-1092/2335 (arming order vs walk bounds).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5509 Β· 18 files
FileTypeDescriptionSize
mutearm.c β€” 8.1 KB view raw
Makefile β€” 54 B ↓ download
build.sh β€” 204 B view raw
run.sh β€” 198 B view raw
trigger_endtoend.sh β€” 1.5 KB view raw
trigger_mute.sh β€” 675 B view raw
mon.py β€” 537 B view raw
build.log β€” 1015 B view raw
run.log β€” 3.1 KB view raw
serial_console_full.log β€” 20.8 KB view raw
env.txt β€” 928 B view raw
fix.diff β€” 1.4 KB view raw
fix_build_guest.log β€” 1.4 KB view raw
fix_validation_3pt.log β€” 245 B view raw
VERDICT.md β€” 7.5 KB ↓ raw
README.md β€” 3.3 KB ↓ raw
manifest.json β€” 2.0 KB view raw
verdict.json β€” 5.3 KB view raw

DF-5509 β€” ehci_pci.c arms ehci_interrupt before controller init: pre-init interrupts are mute (no acknowledge) β†’ level-INTx IRQ storm

What (root cause, one paragraph)

ehci_pci_attach() registers the interrupt handler at sys/bus/u4b/controller/ehci_pci.c:390-391 β€” before ehci_pci_take_controller() (:398, whose BIOS-handoff wait can park the attach for up to 5 s at :532-545) and before ehci_init() (:451) runs. In that whole window the fresh M_ZERO softc still holds the pre-init values sc_offs == 0 and sc_eintrs == 0 (set only at ehci.c:308/353; softc zeroing confirmed at sys/kern/subr_bus.c:1953-1954). The armed handler ehci_interrupt() then:

  • reads "USBSTS" at io_hdl + sc_offs(0) + 0x04 β€” the HCSPARAMS capability register, never the real status register (measured: reads 6),
  • and because status & sc->sc_eintrs == 0, unconditionally exits via goto done at ehci.c:1509-1511 without writing USBSTS to acknowledge.

If the controller arrives with (USBINTR & USBSTS) != 0 β€” classic BIOS-ownership-handoff residue, or a malicious/compromised controller β€” the level-triggered INTx re-fires for every instant of the window. Nothing clears USBINTR/USBSTS until ehci_hcreset() at ehci.c:215 (first reached via ehci.c:335, after the 5 s handoff park). On this 6-vCPU guest the storm is bounded (~5 s, 311,162 interrupts measured in the window vs 55 in the control); on fewer CPUs β€” or whenever the storming IRQ thread starves the attaching thread β€” ehci_hcreset is never reached and the storm is permanent (demonstrated: ~33K interrupts/s sustained indefinitely until the softc was un-muted).

Guest environment (env.txt)

DragonFly 6.5-DEVELOPMENT x86_64, QEMU 7.2.22 i440fx, -device usb-ehci,id=ehci0 cold-boot β†’ ehci0@pci0:0:3:0 (8086:24cd), BAR 0xfebd1000/0x1000, IRQ 10, CAPLENGTH 0x20, 6 ports, HCCPARAMS=0x6880 (EECP=0x68 β†’ QEMU-implemented writable LEGSUP semaphores at config 0x68). ehci as /boot/kernel/ehci.ko.

Build

# in guest (needs /usr/src + /usr/obj/usr/src/sys/X86_64_GENERIC):
mkdir -p /root/ehciarm && cp mutearm.c Makefile /root/ehciarm/
cd /root/ehciarm && make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC

Run

See run.sh (exact sequence) and VERDICT.md (measured results).

Expected decisive numbers (unpatched kernel):

  • Phase 2 (park + one real port event, stock driver): ehci0 interrupts 393,106 β†’ 704,268 = 311,162 in the attach window.
  • Phase 3 control (park, no pending event): 55.
  • Phase 4 (pre-init sc_eintrs=0 on live driver + one port event): sustained ~33K/s, load 3.6 and climbing, never recovers until the softc is restored (then stops dead: 1,603,736 β†’ 1,605,667 β‰ˆ tail only).

Fix (fix.diff, validated)

Quiesce the controller's interrupt state before bus_setup_intr(): read CAPLENGTH (absolute, valid pre-init), write USBINTR=0 and acknowledge-all (USBSTS=0xFFFFFFFF). Patched kernel: same Phase-2 trigger produces a control-sized delta and USB still enumerates normally.

Threat model

Malicious/compromised EHCI silicon or emulated controller, or BIOS/SMM residue at ownership handoff. Not reachable by an unprivileged local user through this file (boot/kldload-time state; /dev/usb nodes are operator/root). Impact class matches the file's known findings DF-1092 / DF-2335 (Low, hardware-preconditioned kernel DoS).

VERDICT.md
↓ download raw

DF-5509 β€” VERDICT

Status: reproduced (impact: dos). Confidence: certain. Fix authored after verification and validated on the guest (same trigger: unpatched 311,162 interrupts through the armed window; patched: 0 during the window, USB fully functional).

Root cause

ehci_pci_attach() arms the interrupt handler at sys/bus/u4b/controller/ehci_pci.c:390-391 β€” before ehci_pci_take_controller() (:398, whose LEGSUP BIOS-handoff wait :517-546 parks the attach for up to 5 s) and before ehci_init() (:451) latches the geometry. newbus allocates the softc with M_INTWAIT | M_ZERO (sys/kern/subr_bus.c:1953-1954), so through the whole window sc_offs == 0 (set only at ehci.c:308) and sc_eintrs == 0 (set only at ehci.c:353). The armed ehci_interrupt() (ehci.c:1490) then:

  1. reads "USBSTS" at io_hdl + sc_offs(0) + EHCI_USBSTS(0x04) β€” i.e. the HCSPARAMS capability register (measured: 6, N_PORTS β€” never the real status register);
  2. computes status & sc->sc_eintrs == 0 and exits via goto done (ehci.c:1509-1511) without writing USBSTS to acknowledge.

With (USBINTR & USBSTS) != 0 at handler-arm time β€” exactly what a BIOS mid-handoff (or a malicious controller) leaves behind β€” the level-triggered INTx re-fires for every instant of the window. The first code that touches USBINTR/USBSTS after arming is ehci_hcreset() (ehci.c:215, EOWRITE4 USBCMD=0 + HCRESET) reached only via ehci_init() at ehci.c:335 β€” after the up-to-5 s handoff park. This ordering defect is distinct from the file's known findings (DF-1092/DF-2335: the walk itself can be unbounded) and from DF-5282 (ehci.c latching unvalidated geometry): the handler is mute here regardless of geometry correctness, because it is armed before the softc fields it depends on exist.

Verification methodology

QEMU 7.2.22 i440fx, DragonFly 6.5-DEVELOPMENT, -device usb-ehci,id=ehci0 cold-boot (ehci0@pci0:0:3:0, 8086:24cd, BAR 0xfebd1000/0x1000, IRQ 10, 6 ports). QEMU's usb-ehci implements the LEGSUP extended capability at EECP=0x68 with writable BIOS_SEM/OS_SEM, so the entire BIOS-handoff dance is exercisable from the guest (root pciconf -w). The device-side preconditions (USBINTR=0x3F enabled, USBCMD.RS=1, BIOS_SEM=1 β€” the "BIOS residue") were set by a root KLD (ehciarm.ko, DF-5282's poison methodology) and a QEMU-monitor device_add usb-kbd fired a real port-change event at a chosen instant. 100% stock driver code in every measured run; softc re-zeroing on each kldload verified from subr_bus.c:1943-1954.

Observed results

  1. Dry run β€” BIOS_SEM=1, clean controller state: serial console shows usbus0: waiting for BIOS to give up control β†’ timed out waiting for BIOS (~5 s park inside ehci_pci_take_controller), attach then completes normally. The park is deterministic and opens exactly between bus_setup_intr (:391) and ehci_init (:451).
  2. End-to-end bounded storm (stock driver) β€” armed residue + park + one real port event mid-park: ehci0 interrupt counter 393,106 β†’ 704,268 = 311,162 interrupts through a single attach; attach nonetheless completes (ehci_hcreset clears the state at ehci.c:335) and the keyboard enumerates. On this 6-vCPU guest the storm is bounded; the attaching CPU is saturated for the whole park.
  3. Control β€” identical park, no pending event: Ξ” = 55 interrupts. A single port event in the armed pre-init window is a 5,657Γ— interrupt amplification on stock driver code.
  4. Permanent variant β€” pre-init handler state injected on the live, otherwise-healthy driver (sc_eintrs = 0 β€” the exact M_ZERO value; sc_offs left CORRECT at 0x20): one real port event β†’ sustained ~33,000 interrupts/s, ~900 K interrupts over 14 s, load average 3.6 and climbing, no self-heal (nothing will ever acknowledge; no hcreset is coming). Restoring sc_eintrs=0x37 stops the storm dead within one delivery (1,603,736 β†’ 1,605,667 β‰ˆ tail only, load 0.00). This is the boot-window outcome on machines where the storming IRQ thread starves the attaching thread (e.g. 1-vCPU): ehci_hcreset is never reached β†’ permanent boot-time machine live-lock.

Why this is the driver's defect (attribution)

A controller that can assert INTx forever can DoS the machine regardless of any driver β€” that generic case is out of scope (same attribution DF-5282 made). The driver-controllable defect here is the ordering: the handler is registered while it provably cannot acknowledge anything (sc_offs=0 β†’ reads the wrong register; sc_eintrs=0 β†’ masks every bit), and the window deliberately includes a 5-second sleep. A 2-instruction quiesce before bus_setup_intr (the validated fix) removes the entire window.

QEMU device-model notes (repro fidelity)

  • QEMU's ehci_opreg_write for USBINTR does not re-evaluate the INTx line, but ehci_port_attach raises PCD and re-evaluates immediately (hw/usb/hcd-ehci.c v7.2: ehci_raise_irq/ehci_update_irq) β€” so arming before the port event reproduces the handoff-residue semantics exactly.
  • Real EHCI silicon latches USBSTS.PCD on connect changes regardless of run state; QEMU only does so via paths that need the frame timer, hence the arm also sets USBCMD.RS (the state a BIOS actively servicing ports is in at handoff).
  • Amusing side-observation: after the runs, QEMU's LEGSUP dword had NEXT=0x10; the walk then reads the BAR0 dword (0xfebd1000 β†’ NEXT=0x10) at config 0x10 forever β€” an accidental re-confirmation of the KNOWN DF-1092/DF-2335 unbounded-chain class (not re-reported).

Fix (fix.diff β€” validated)

In ehci_pci_attach(), immediately after the MMIO resource is mapped and before bus_setup_intr(): read CAPLENGTH (low byte of capability register 0 β€” an absolute offset, valid pre-init), then write USBINTR = 0 and USBSTS = 0xFFFFFFFF (acknowledge-all). A pending interrupt can then never be level-asserted at handler-arm time; interrupts are re-enabled later by ehci_init_sub() (ehci.c:264) once sc_offs and sc_eintrs are valid.

Validation (see fix_build_guest.log, fix_validation_3pt.log):

  • Patch applied to guest /usr/src, make quickkernel (ehci_pci.c recompiled, log:5530) + make installkernel, rebooted.
  • Pass-branch: real QEMU EHCI boots and enumerates normally (roothub + 6 keyboards; 72 boot interrupts β€” normal).
  • Same trigger as the baseline storm: T1 park-start 322,795 β†’ T2 mid-park 322,795 (Ξ”=0) β†’ T3 post-attach 322,900 (Ξ”=105 total) with the port event fired inside the park β€” vs 311,162 unpatched. Storm eliminated; USB functional.

Threat model / severity

Malicious or compromised EHCI silicon/emulator, or BIOS/SMM residue at ownership handoff; kldload-time re-attach is root-gated, boot-time attach needs no privilege at all beyond the device's own state. Not reachable by an unprivileged local user through this file. Consequence: bounded ~5 s full-machine IRQ storm (measured 311 K interrupts) or permanent boot live-lock when the attaching CPU is starved (demonstrated variant). Severity Low β€” calibrated with the file's known findings DF-1092 / DF-2335 (same hardware-preconditioned DoS class, same file, Low each).

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 applied to guest /usr/src, quickkernel+installkernel, rebooted. Pass-branch: real QEMU EHCI attaches and enumerates normally (72 boot interrupts). Same trigger that produced 311,162 interrupts unpatched now yields delta=0 during the BIOS-handoff park (3-point sampling: 322795 -> 322795 -> 322900) with the port event fired inside the park; USB fully functional afterwards.

fix_validation_3pt.log; fix_build_guest.log
↓ fix.diffper-fix-DF-5509

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (all counter measurements: 393106->704268=311162 storm run; 55 control; 1131077->1231156->1603736 permanent variant stopping dead at 1605667 after sc_eintrs restore); serial_console_full.log (ehciarm probe/arm lines, 'usbus0: waiting for BIOS to give up control' / 'timed out waiting for BIOS' per run, ukbd enumeration after each storm); fix_validation_3pt.log (patched: T1 322795, T2 mid-park 322795 delta=0, T3 322900 delta=105 with the port event fired inside the park); fix_build_guest.log (ehci_pci.c recompiled, ehci.ko installed 16:08, pass-branch 72 boot interrupts); VERDICT.md (full narrative incl. QEMU hcd-ehci.c semantics that make the repro faithful)

PoC changes

Wrote the trigger from scratch (no seed): ehciarm.ko implements 3 modes (probe / mute sc_eintrs=0 / arm USBINTR=0x3F+RS on the driver-less controller) via kenv; the 5s BIOS-handoff park is forced with root pciconf writes to QEMU's writable LEGSUP BIOS_SEM (config 0x6a); the port event comes from the QEMU monitor (device_add usb-kbd). First end-to-end attempt did not storm: QEMU's ehci only latches port-change status via paths that need USBCMD.RS, so the arm was corrected to leave the controller RUNNING (the state a BIOS actively servicing ports is in). Learned from QEMU v7.2 hcd-ehci.c that USBINTR writes never re-evaluate the INTx line while ehci_port_attach does - so arming must precede the port event, exactly the handoff-residue order.

Verified recommended fix

In ehci_pci_attach(), immediately after mapping the MMIO BAR and BEFORE bus_setup_intr(): read CAPLENGTH (absolute, low byte of cap reg 0) and write USBINTR=0 plus USBSTS=0xFFFFFFFF (ack-all) so no interrupt can be pending/level-asserted while the pre-init mute handler is armed (fix.diff, validated).

Verdict

ehci_pci_attach() registers ehci_interrupt() at ehci_pci.c:390-391 BEFORE ehci_pci_take_controller() (:398, up-to-5s LEGSUP BIOS-handoff park at :517-546) and BEFORE ehci_init() (:451) sets sc_offs/sc_eintrs (ehci.c:308/353). In that window the M_ZERO softc (subr_bus.c:1953) holds sc_offs=0 and sc_eintrs=0, so the armed handler reads 'USBSTS' from MMIO+0x04 = the HCSPARAMS capability register (measured: 6) and unconditionally exits via goto done WITHOUT acknowledging (ehci.c:1504-1513). Any (USBINTR & USBSTS)!=0 state at handler-arm time - BIOS handoff residue or malicious controller - makes the level INTx re-fire until ehci_hcreset (ehci.c:215/335), the first code after the window. Reproduced end-to-end on stock driver code: one real port event (QEMU monitor device_add) during the BIOS-handoff park of a kldload attach produced 311,162 ehci0 interrupts vs 55 in the no-event control (5,657x); the permanent variant (pre-init sc_eintrs=0 injected on the live driver, sc_offs left correct) sustains ~33,000 intr/s indefinitely with no self-heal until the softc is restored. Bounded ~5s full-machine IRQ storm at boot/attach; permanent boot live-lock when the attaching CPU is starved. Hardware/BIOS-preconditioned, root-gated to re-trigger in a running system: Low, same class as DF-1092/DF-2335 but a distinct root cause (handler-arming order, not the walk bounds).