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

ehci_init latches unvalidated device-controlled register geometry (CAPLENGTH/N_PORTS): every EOREAD4/EOWRITE4 lands at attacker-chosen in-BAR offsets, blind/mute interrupt handling live-locks the system on first port event, and OOB MMIO results on undersized-BAR hardware

Summary

CAPLENGTH->sc_offs (8-bit) and HCS_N_PORTS->sc_noport (4-bit) stored with no check against sc_io_size; every EOREAD4/EOWRITE4 computes io_hdl+sc_offs+reg with furthest reach PORTSC(noport)=0x44+4*noport (window 0x17F). Malicious controller (AV:P): CAPLENGTH=0xFF shifts all accesses - blind/mute interrupt (can't see/ack USBSTS.PCD once offset -> level-triggered INTx re-fires forever = hard system live-lock from one device plug, 3/3 on guest), bounded OOB MMIO write (15 device-influenced dword slots) on undersized BAR. Guest-verified read misdirection (PP bit lost), write misdirection (SET_PORT_SUSPEND OK but PORTSC never saw it), live-lock 3/3 + clean control; EHCI member of the xHCI register-geometry family (DF-5268/DF-2336/DF-1106). Validate-geometry-in-init fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5282 Β· 15 files
FileTypeDescriptionSize
README.md β€” 2.9 KB ↓ raw
VERDICT.md β€” 7.1 KB ↓ raw
poison.c β€” 3.6 KB view raw
Makefile β€” 53 B ↓ download
baseline.sh β€” 935 B view raw
poisoned.sh β€” 1.0 KB view raw
build.sh β€” 174 B view raw
run.sh β€” 173 B view raw
build.log β€” 1.4 KB view raw
run.log β€” 5.3 KB view raw
fix_build_guest.log β€” 1.1 KB view raw
env.txt β€” 337 B view raw
fix.diff β€” 1.3 KB view raw
manifest.json β€” 1.4 KB view raw
verdict.json β€” 6.0 KB view raw

DF-5282 β€” EHCI unvalidated register geometry (sc_offs / sc_noport)

What

ehci_init() latches the device-reported register geometry with no validation:

sc->sc_io_size (BAR size, ehci_pci.c:324) is never consulted. Every subsequent EOREAD4/EOWRITE4 (ehci.h:372-383) adds sc_offs to the register offset raw. Furthest reach: 0xFF + EHCI_PORTSC(15) = 0x17F bytes past the BAR base. With an adversarially undersized BAR (< 0x180 β€” also device-controlled) every port-register access is out-of-bounds MMIO read/write (DF-5268 class, bounded variant). Same defect family as DF-5268/DF-2336/DF-1106 (xHCI).

Guest environment

  • DragonFly 6.5-DEVELOPMENT x86_64 (QEMU/KVM, QEMU 7.2), ICH4-style usb-ehci device: BAR 0x1000 @ 0xfebd1000, CAPLENGTH=0x20, 6 ports.
  • ehci is a KLD module (/boot/kernel/ehci.ko).

Build

# in guest (needs /usr/src + /usr/obj kernel build dir):
mkdir -p /root/ehcipwn && cp poison.c Makefile /root/ehcipwn/
cd /root/ehcipwn && make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC

Run (A/B demonstration β€” read/write path geometry confusion)

./baseline.sh          # roothub GET_STATUS / SET+CLEAR SUSPEND roundtrip
kldload /root/ehcipwn/ehcipwn.ko     # latches sc_offs=0xFF (== malicious CAPLENGTH)
./poisoned.sh          # same requests now hit io_hdl+0x147/0x15B
kldunload ehcipwn

Expected: baseline port status 0x0504 (POWER|HIGH|SUSPEND) / 0x0500; poisoned GET_STATUS returns 0x0400 (POWER bit lost β€” wrong register read); poisoned SET_SUSPEND "succeeds" but the real PORTSC is untouched (0x0500 after restore).

Run (hard live-lock β€” interrupt path)

kldload /root/ehcipwn/ehcipwn.ko
# QEMU monitor: device_add usb-kbd,id=k1,bus=ehci0.0,port=1

Expected (unpatched kernel): the first port-change interrupt is read via the poisoned offset (returns 0), the handler exits without acking the real USBSTS.PCD, the level-triggered INTx re-fires forever -> the guest live-locks: ssh dies within ~20 s, serial console silent, qemu spins at 40-85% CPU, vm.sh status => down. Reproduced 3/3 from clean snapshots (2 baseline + 1 patched-module kernel). Control run (identical hotplug, no poison): ukbd0 attaches, guest healthy.

Fix

fix.diff β€” init-time geometry validation in ehci_init() before any sc_offs-relative access. Rejects hostile CAPLENGTH/N_PORTS combinations against sc_io_size (closes the from-boot malicious-geometry latching and the undersized-BAR OOB). No-regression verified on the patched module (real EHCI passes, full USB function incl. hotplug). The reject branch cannot be exercised on QEMU (its EHCI model hardcodes CAPLENGTH=0x20 / BAR=0x1000); the post-init corruption vector is equivalent to a device storming its own IRQ line β€” see VERDICT.md.

VERDICT.md
↓ download raw

DF-5282 β€” VERDICT

Status: reproduced (impact: dos β€” system-wide hard live-lock); memory-safety facet (OOB MMIO) is code-certain but bounded and could not be manifested on this guest (hard blocker: QEMU's EHCI model hardcodes a 4 KB BAR). Confidence: certain.

Root cause

ehci_init() (sys/bus/u4b/controller/ehci.c:308,327) latches two device-controlled MMIO values with no validation against the mapped resource size:

sc->sc_offs   = EHCI_CAPLENGTH(EREAD4(sc, EHCI_CAPLEN_HCIVERSION)); /* 8 bits */
sc->sc_noport = EHCI_HCS_N_PORTS(sparams);                          /* 4 bits */

Every later register access is bus_space_{read,write}_4(io_hdl, sc->sc_offs + reg) (ehci.h:372-383). sc->sc_io_size (stored by the glue at ehci_pci.c:324) is never consulted anywhere in the driver. The furthest register touched is EHCI_PORTSC(sc_noport) = 0x44 + 4*noport (ehci.c:2068 root-intr scan; ehci.c:3251,3345,3395 roothub requests; ehcireg.h:139), so the driver's whole access window is [sc_offs, sc_offs + 0x44 + 4*sc_noport] βŠ† [0, 0x17F]. A malicious or broken controller that reports CAPLENGTH=0xFF (and, for the OOB to materialize, a BAR smaller than 0x180 β€” both fully device-controlled) turns every port-register access into an out-of-bounds MMIO read/write of up to (0x17F βˆ’ bar_size) bytes past the mapping β€” the EHCI member of the xHCI register-geometry family (DF-5268 CAPLENGTH/DBOFF/RTSOFF, DF-2336/DF-1106 XECP walk). Because CAPLENGTH is 8 bits here, the EHCI variant's reach is bounded to 383 bytes, unlike xHCI's unbounded 32-bit offsets.

Verification methodology (DF-5268 pattern)

QEMU's EHCI hardcodes sane register values, so the malicious latched value was injected via a KLD (poison.c) that walks devclass "ehci" β†’ softc and stores 0xFF into the already-latched sc->sc_offs β€” bit identical to what ehci_init() would have stored from a CAPLENGTH register reading 0xFF. Everything downstream is the driver's unmodified code. Guest: DragonFly 6.5-DEVELOPMENT x86_64, QEMU 7.2 ICH4-style usb-ehci (8086:24cd), BAR 0xfebd1000–0xfebd1fff (0x1000), CAPLENGTH 0x20, 6 ports, ehci as /boot/kernel/ehci.ko.

Observed results

  1. Read path consumes the unvalidated offset β€” roothub GET_STATUS(port) (ehci.c:3345) returns real status 0x0500 (POWER|HIGH_SPEED) with sane geometry; with sc_offs=0xFF the same request returns 0x0400 (the POWER bit from EHCI_PS_PP is gone) because EOREAD4 now reads io_hdl+0x147/0x15B (unimplemented in-bar).
  2. Write path consumes the unvalidated offset β€” baseline SET_FEATURE(UHF_PORT_SUSPEND) (ehci.c:3402, EOWRITE4) makes the SUSPEND bit visible (0x0504); the identical request with the poison loaded returns OK but, after restoring sc_offs, the real PORTSC shows 0x0500 β€” no SUSPEND β€” proving the kernel MMIO write landed at the wrong offset (io_hdl+0x147) and never reached the port register.
  3. Interrupt path β†’ hard system live-lock β€” with sc_offs=0xFF loaded and the PCD re-enabled by ehci_pcd_enable (ehci.c:1429-1441, itself writing USBINTR/USBSTS via the poisoned offset), a single USB device hotplug (QEMU monitor device_add usb-kbd) wedges the whole machine: ehci_interrupt() (ehci.c:1504-1513) reads USBSTS via the poisoned offset β†’ 0 β†’ exits without acking the real USBSTS.PCD β†’ the level-triggered INTx re-fires forever. ssh dies within ~20 s, the serial console prints nothing further (no panic β€” pure live-lock), the qemu process spins at 40–85% CPU, vm.sh status β‡’ down. Reproduced 3/3 from clean with-src snapshots (2Γ— unpatched, 1Γ— with the fix module loaded). Control run (identical hotplug, no poison): ukbd0 attaches and the guest stays healthy.

The OOB facet β€” why it did not manifest here (hard blocker)

Max poisoned offset = 0xFF + 0x44 + 415 = 0x17F (383). QEMU's EHCI MMIO region is 0x1000 bytes (device model constant; verified via the monitor BAR listing and sc_io_size=0x1000 printed by the KLD), so every poisoned access stays inside the mapping β€” no page fault, no adjacent MMIO corruption is possible* on this guest. Manifesting the OOB requires an adversarially undersized BAR (< 0x180), which QEMU's usb-ehci device model cannot be configured to expose. On real malicious hardware the same driver code performs the accesses past the mapping (bounded 0x17F window): driver-controlled dword writes with device-influenced values at 15 selectable 4-byte slots (EHCI_PORTSC(1..15)), plus reads. Adjacent PCI device registers mapped within 383 bytes after such a BAR are corruptible.

Attribution of the live-lock

The wedge requires the geometry to become adversarial after the controller was initialized and real interrupts enabled (the KLD simulates mid-life corruption / re-negotiation, or a from-boot malicious device would have real USBINTR never written). A device that can flip its CAPLENGTH mid-life could also simply assert INTx forever β€” an IRQ storm no driver fix can prevent. The driver-controllable defect is the unvalidated latch itself plus the resulting blind/mute interrupt handling and geometry confusion (items 1–3 above); the fix below closes the latch-time vector.

Fix (fix.diff, authored post-verification; never applied to sys/)

Init-time validation in ehci_init() immediately after both values are latched and before the first sc_offs-relative access (ehci_hcreset at ehci.c:335): reject sc_offs < 0x08 or sc_offs + 0x44 + 4*sc_noport > sc_io_size with USB_ERR_IOERROR.

Validation performed: - Patch applied cleanly to the guest's /usr/src; quickkernel + installkernel rebuilt ehci.ko containing the guard (string verified in the installed module; auto-loaded at boot). - No regression (pass-branch): patched module boots the real QEMU EHCI (offs=0x20, noport=6, io_size=0x1000 β†’ passes), full USB function including device enumeration. - Reject-branch: not exercisable on this guest (QEMU EHCI hardcodes CAPLENGTH=0x20 / BAR=0x1000) β€” fix_status not_testable for that branch. - Wedge re-run on the patched kernel: the post-init corruption trigger still wedges the guest (expected β€” init-time validation cannot cover an offset that flips after init; documented above). This vector is attributed to generic malicious-device IRQ storming.

Threat model

Malicious / compromised EHCI silicon or emulated controller (AV:P), or a config-space/caps-corrupted device. Not reachable by an unprivileged local user through this file (no ioctls here; /dev/usb nodes are operator/root). Consequences: (a) deterministic USB-subsystem misbehavior/hard machine live-lock from a single device plug, (b) bounded OOB MMIO read/write on adversarially-undersized-BAR hardware.

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

not_testable
baseline reproduced→ patch + rebuild →patched reproduced

fix.diff (init-time geometry guard) applied to the guest's /usr/src copy, quickkernel+installkernel rebuilt, patched ehci.ko verified loaded (guard string present; kldstat). No-regression validated: real EHCI passes the guard, full USB function incl. device hotplug. The guard's reject branch cannot be exercised on this guest because QEMU's EHCI device model hardcodes CAPLENGTH=0x20 and BAR=0x1000 (no property exposes either), hence not_testable for the from-boot malicious-geometry vector the fix closes. The reproduced wedge (post-init offset corruption + hotplug) still occurs on the patched kernel by construction β€” init-time validation cannot cover an offset that changes after init; that vector is equivalent to a malicious device hard-storming its own INTx line and requires platform-level IRQ mitigation, not a driver fix. fix_patched_reproduced=true records exactly that observation (same trigger re-wedged the patched guest), not a failure of the guard against the vector it targets.

fix_build_guest.log (clean hunk apply at line 327, kernel+module rebuild/install, guard string in installed ehci.ko, kldstat shows module loaded, no-regression enumeration); run.log PHASE 5 (patched-kernel trigger re-run: down at t=18s, documented as post-init vector); final vm.sh reset with-src
↓ fix.diffper-fix-DF-5282

Confirmed kernel references

Detail

Exploit chain

malicious/compromised EHCI controller reports CAPLENGTH=0xFF (and an undersized BAR for the corruption facet) -> ehci_init latches it unchecked (ehci.c:308) -> all EOREAD4/EOWRITE4 land at io_hdl+0xFF+reg -> roothub/interrupt paths read and write wrong (or out-of-mapping) registers -> deterministic USB geometry confusion; on the first port-change event the unackable PCD interrupt live-locks the entire system (proven on guest); on undersized-BAR hardware the same writes are bounded OOB MMIO corruption of adjacent PCI MMIO space (not demonstrable under QEMU's fixed 4KB bar)

Evidence (decisive lines)

run.log (A/B port-status deltas 0x0500/0x0504 vs 0x0400; poisoned SET_SUSPEND missing the real register; Run A wedge x2 + Run B control); VERDICT.md (full mechanism + OOB blocker); build.log / fix_build_guest.log; fix.diff

PoC changes

Authored from scratch (no seed). KLD walks devclass 'ehci' -> softc, prints geometry, latches sc_offs=0xFF (uint8_t, bit-identical to init latching CAPLENGTH=0xFF); triggers are genuine roothub control requests via usbconfig do_request and a real QEMU device hotplug. Initial builds needed ehci.c's full include set for ehci_softc_t.

Verified recommended fix

Validate sc_offs and sc_noport against sc_io_size in ehci_init() before any operational-register access (fix.diff); reject hostile geometry with USB_ERR_IOERROR

Verdict

ehci_init() latches the device-reported register geometry with no validation: sc_offs = CAPLENGTH (ehci.c:308, 8-bit device MMIO) and sc_noport = HCS_N_PORTS (ehci.c:327, 4-bit); sc_io_size (stored by the glue at ehci_pci.c:324) is never consulted, and every EOREAD4/EOWRITE4 (ehci.h:372-383) adds sc_offs raw. DF-5268 class, bounded variant: the driver's furthest access is sc_offs + EHCI_PORTSC(noport) <= 0x17F, so with an adversarially undersized BAR (< 0x180, fully device-controlled) every port-register access is OOB MMIO read/write of up to (0x17F - bar_size) bytes past the mapping (15 selectable dword write slots at PORTSC(1..15)). Verified on QEMU/KVM DragonFly 6.5-DEVELOPMENT (ICH4-style usb-ehci, BAR 0x1000, CAPLENGTH 0x20, 6 ports): latch-simulation KLD storing 0xFF into sc_offs (bit-identical to init latching a malicious CAPLENGTH) + the driver's unmodified roothub requests show (a) read path consumes the poison (GET_STATUS 0x0500 -> 0x0400, PP bit lost), (b) write path consumes the poison (SET_PORT_SUSPEND returns OK but the real PORTSC is provably untouched), (c) interrupt path: first device hotplug after the poison live-locks the whole machine (handler reads USBSTS via the poisoned offset, exits without acking real PCD, level INTx re-fires forever) β€” reproduced 3/3 from clean snapshots with a clean 1/1 control; ssh dead, console silent, no panic text, qemu spinning 40-85% CPU. The OOB itself cannot manifest on this guest (hard blocker: QEMU's EHCI model hardcodes a 4KB BAR > 0x17F max reach) and is documented as conditional on adversarial hardware geometry. Threat actor: malicious/compromised EHCI hardware (AV:P), same class family as DF-5268/DF-2336/DF-1106.