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)
PoC verification
Evidence pack
findings/poc/DF-5282 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:
sys/bus/u4b/controller/ehci.c:308βsc->sc_offs = EHCI_CAPLENGTH(...)(8-bit, device MMIO)sys/bus/u4b/controller/ehci.c:327βsc->sc_noport = EHCI_HCS_N_PORTS(...)(4-bit, device MMIO)
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-ehcidevice: 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.
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
- Read path consumes the unvalidated offset β roothub
GET_STATUS(port)(ehci.c:3345) returns real status0x0500(POWER|HIGH_SPEED) with sane geometry; withsc_offs=0xFFthe same request returns0x0400(the POWER bit fromEHCI_PS_PPis gone) becauseEOREAD4now reads io_hdl+0x147/0x15B (unimplemented in-bar). - 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 restoringsc_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. - Interrupt path β hard system live-lock β with
sc_offs=0xFFloaded and the PCD re-enabled byehci_pcd_enable(ehci.c:1429-1441, itself writing USBINTR/USBSTS via the poisoned offset), a single USB device hotplug (QEMU monitordevice_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 cleanwith-srcsnapshots (2Γ unpatched, 1Γ with the fix module loaded). Control run (identical hotplug, no poison):ukbd0attaches 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_testablefix.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
Confirmed kernel references
- sys/bus/u4b/controller/ehci.c:308
- sys/bus/u4b/controller/ehci.c:327
- sys/bus/u4b/controller/ehci.h:351
- sys/bus/u4b/controller/ehci.h:372-383
- sys/bus/u4b/controller/ehci_pci.c:316-324
- sys/bus/u4b/controller/ehci.c:1504-1513
- sys/bus/u4b/controller/ehci.c:1429-1441
- sys/bus/u4b/controller/ehci.c:3345
- sys/bus/u4b/controller/ehci.c:3402
- sys/bus/u4b/controller/ehcireg.h:139
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.
No comments yet.