xhci_init latches unmasked device-controlled register offsets (CAPLENGTH/RTSOFF/DBOFF): every later XREAD/XWRITE is an unbounded OOB MMIO access past the mapped BAR
Summary
sc_oper_off/sc_runt_off/sc_door_off copied verbatim from controller MMIO (DBOFF up to 0xFFFFFFFC) with no validation against sc_io_size - the XREAD/XWRITE macros add them raw to the bus handle, so a malicious/firmware-compromised xHCI controller (Thunderbolt/USB4/ExpressCard hotplug, VM emulated) gets fully device-chosen OOB MMIO reads+writes: doorbell writes on every command/transfer/resume, IMAN/ERDP/USBSTS on every interrupt. REPRODUCED on QEMU/KVM guest with real qemu-xhci + poison KLD storing 0xF0000000 into sc_door_off (bit-identical to a hostile DBOFF): ordinary usb-mouse device_add -> Fatal trap 12 supervisor write at io_hdl+0xF0000000, 'Stopped at xhci_do_command+0x216', deterministic 1 attempt; fix.diff (init-time ENXIO validation + runtime doorbell guards) rebuilt nativekernel -> identical trigger no panic, graceful enumeration failure, 4 further plug/unplug cycles clean. Local unpriv NOT in scope (ugen 0600). Distinct from DF-1106/DF-2336 (XECP walk).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5268 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poison.c | β | 2.9 KB | view raw | |
| Makefile | β | 97 B | β download | |
| build.sh | β | 128 B | view raw | |
| run.sh | β | 161 B | view raw | |
| run.log | β | 2.0 KB | view raw | |
| panic.txt | β | 671 B | view raw | |
| env.txt | β | 481 B | view raw | |
| fix.diff | β | 2.2 KB | view raw | |
| fix_build_guest.log | β | 579 B | view raw | |
| README.md | β | 3.3 KB | β raw | |
| VERDICT.md | β | 5.4 KB | β raw | |
| verdict.json | β | 4.0 KB | view raw |
DF-5268 β xHCI driver latches unmasked device-controlled register offsets (CAPLENGTH/RTSOFF/DBOFF) β unbounded OOB MMIO read/write
Build
See build.sh β builds xhcipwn.ko in the guest (DragonFly 6.5-DEVELOPMENT,
cc 8.3). Needs /usr/src + kernel obj tree (vm.sh reset with-src, then
make nativekernel KERNCONF=X86_64_GENERIC once for headers).
Run
See run.sh. Boot the guest with QEMU_EXTRA="-device qemu-xhci,id=xhci0,p2=8,p3=8",
kldload ./xhcipwn.ko, then insert any USB device via the QEMU monitor
(device_add usb-mouse,id=m1,bus=xhci0.0,port=3).
Expected
- Vulnerable kernel (unmodified snapshot): instant kernel panic β
Fatal trap 12: page fault while in kernel mode, fault VA0xfffff8017df7b000=io_hdl(0xfffff8008df7b000) + 0xF0000000,supervisor write data, page not present,Stopped at xhci_do_command+0x216: movl $0,(%rdx)(the doorbell write atsys/bus/u4b/controller/xhci.c:1235). - Fixed kernel (+fix.diff): no panic, no fault; device enumeration fails
gracefully (
ugen0.2 <Unknown> (disconnected)), guest stays up across repeated plug/unplug cycles.
Why this is the real thing (not a synthetic bug)
xhci_init() (sys/bus/u4b/controller/xhci.c:626-629) copies three register
base offsets verbatim from device MMIO into the softc:
sc->sc_oper_off = XREAD1(sc, capa, XHCI_CAPLENGTH); /* 8 bits */
sc->sc_runt_off = XREAD4(sc, capa, XHCI_RTSOFF) & ~0x1F; /* 32 bits */
sc->sc_door_off = XREAD4(sc, capa, XHCI_DBOFF) & ~0x3; /* 32 bits */
sc->sc_io_size β the mapped BAR size, recorded at
sys/bus/u4b/controller/xhci_pci.c:206 β is never consulted anywhere in
xhci.c. Every XREAD*/XWRITE* macro (xhcireg.h:205-222) is a raw
bus_space_* on sc_io_hdl + offset (a direct *(volatile uint32_t *)
dereference on x86). With DBOFF up to 0xFFFFFFFC, RTSOFF up to
0xFFFFFFE0, the driver will read/write gigabytes past the 16 KB mapping.
The PoC module performs exactly one thing: it stores into the latched
sc->sc_door_off the value 0xF0000000 β bit-for-bit what xhci_init()
would itself have stored had the controller returned that DBOFF (the only
step simulated, because QEMU's own xhci hardcodes sane offsets). Everything
afterwards β the doorbell write, the page fault β is the driver's unmodified
code on the real code path reached from ordinary USB device insertion
(ENABLE_SLOT command β xhci_do_command() β XWRITE4(sc, door,
XHCI_DOORBELL(0), 0) at xhci.c:1235).
Threat model: malicious/compromised xHCI controller (Thunderbolt/USB4/PCIe
hotplug, emulated device in VM, firmware-implanted controller). Sibling of
DF-1106 / DF-2336 (the extended-capability list walk in xhci_pci.c);
this finding is the base-offset variant in xhci.c β every register
access in the driver, not just the attach-time XECP walk.
Artifacts
poison.c,Makefileβ PoC kernel module sourcebuild.sh/run.shβ exact commandsrun.logβ full baseline (panic) + fixed (survives) transcriptpanic.txtβ kernel panic signature captured from serial consoleenv.txtβ guest uname, compiler, controller, register valuesfix.diffβ validated fix (init-time bounds validation + runtime doorbell guards)fix_build_guest.logβ kernel build/install tail with fix applied
DF-5268 VERDICT
status: reproduced (impact: panic β write-capable OOB MMIO primitive demonstrated; escalation bounded by requiring malicious hardware / PCIe device access)
What was claimed
sys/bus/u4b/controller/xhci.c:626-629 (xhci_init()) latches three
register base offsets (CAPLENGTH, RTSOFF, DBOFF) straight from
controller MMIO. The macros at xhcireg.h:205-222 add these offsets to the
bus-space handle for every subsequent register access with no bounds
check against sc->sc_io_size (set once at xhci_pci.c:206, never read by
this file). A controller that returns oversized DBOFF/RTSOFF (both 32-bit
fields) turns each doorbell write into an OOB MMIO write up to 4 GB past
the mapped BAR, and each interrupter-register access into an OOB MMIO read.
Write-capable OOB sites reachable on the code path proven here:
- xhci.c:1235 β XWRITE4(sc, door, XHCI_DOORBELL(0), 0) (every xHCI command)
- xhci.c:2869 β XWRITE4(sc, door, XHCI_DOORBELL(index), ...) (every transfer start,
xhci_endpoint_doorbell)
- xhci.c:4313 β doorbell ring loop in xhci_device_resume
Read sites: xhci.c:1653,1661 (USBSTS/IMAN on every interrupt), ERDP/ERSTSZ
writes (xhci.c:1148-1149, 477, 513-521, 526-528), PORTSC reads/writes in
xhci_roothub_exec (all oper/runt accesses).
How it was verified (baseline)
Guest: DragonFly 6.5-DEVELOPMENT x86_64 (#0 snapshot), QEMU/KVM with
-device qemu-xhci,id=xhci0,p2=8,p3=8 (1b36:000d, 16 KB BAR at
0xfebd0000, real offsets oper=0x40/runt=0x1000/door=0x2000).
- Built
xhcipwn.ko(pack source) which walksdevclass_find("xhci")βdevclass_get_softc(dc, 0)and stores0xF0000000into the latchedsc->sc_door_offβ the exact valuexhci_init()would have stored had the controller's DBOFF register held it (the only simulated step; QEMU hardcodes sane offsets in its own model). - Loaded the module (dmesg confirms the real values first, then the poison).
- Inserted a USB device via the QEMU monitor (
device_add usb-mouse, bus=xhci0.0,port=3) β hub explore thread issuesENABLE_SLOTβxhci_do_command().
Result: instant kernel panic
Fatal trap 12: page fault while in kernel mode fault virtual address = 0xfffff8017df7b000 [io_hdl + 0xF0000000 exactly] fault code = supervisor write data, page not present Stopped at xhci_do_command+0x216: movl $0,(%rdx)
The fault VA equals sc_io_hdl + poisoned_door_off + XHCI_DOORBELL(0) β the
driver's own write instruction on its own code path; a write fault at a
fully device-chosen address (subject to & ~0x3 alignment). One run, one
panic; deterministic.
Fix validation
Authored fix.diff (in-pack):
1. xhci_init(): validate oper/runt/door offsets against sc_io_size
(ENXIO on violation β malicious controller never attaches), and
2. runtime guards before the two hottest doorbell writes
(xhci_do_command at :1253+, xhci_endpoint_doorbell at :2892+).
Cycle: vm.sh reset with-src β applied fix.diff in guest /usr/src β
make nativekernel KERNCONF=X86_64_GENERIC + make installkernel β
rebooted (kernel #1 Thu Oct 1 21:45:59 UTC 2026) β re-ran the identical
PoC (same module, same poison, same monitor trigger):
- No trap.
dmesg | grep -c "Fatal trap"β 0. - Enumeration degrades gracefully (
ugen0.2 <Unknown> (disconnected)); the guarded command path returnsUSB_ERR_IOERRORbefore touching MMIO. - Guest survived 4 further device_add/device_del cycles with the poison still loaded. Baseline vs patched behavior differs exactly as claimed.
Escalation ceiling (honest bound)
The primitive is a device-directed OOB MMIO write (and read) at a controller-chosen 32-bit offset past the BAR mapping. Where the address lands unmapped β panic (proven here). Where it lands on adjacent mapped MMIO (other devices' registers, APIC space, firmware flash mappings) β cross-device register corruption β potentially persistent platform compromise (e.g., rewriting other devices' BARs/DMA engines). Local unprivileged users are not in the threat model (ugen nodes are 0600 root:operator); the attacker is malicious hardware (Thunderbolt/USB4/ ExpressCard hotplug, compromised controller firmware, or a VM's emulated controller), matching AV:P. uid=0-in-guest is not reachable from this primitive alone on this platform without additional PCIe-sited gadgets.
Kin findings (same class, different code): DF-1106 / DF-2336 cover the XECP
list walk in xhci_pci.c:398-413; this finding covers the base register
offsets in xhci.c:626-629 used by every runtime access β separate defect,
separate fix site.
Kernel references
sys/bus/u4b/controller/xhci.c:626-629(unmasked offset latch)sys/bus/u4b/controller/xhcireg.h:205-222(unchecked bus_space accessors)sys/bus/u4b/controller/xhci_pci.c:204-213(io_tag/hdl/size setup; size unused downstream)sys/bus/u4b/controller/xhci.c:1235(command doorbell write β panic site)sys/bus/u4b/controller/xhci.c:1653,1661(per-interrupt IMAN read)sys/bus/u4b/controller/xhci.c:2855-2872(endpoint doorbell write)
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 kernel: deterministic panic (Fatal trap 12, supervisor write) on trigger. Kernel+fix.diff (nativekernel build #1): identical trigger produces no panic, no fault; enumeration fails gracefully; guest survived 4 additional plug/unplug cycles with the poisoned offset still installed. Bad behavior eliminated.
run.log (both phases); fix_build_guest.log; panic.txt vs post-fix dmesg grep -c 'Fatal trap' = 0
Confirmed kernel references
Detail
Exploit chain
malicious xHCI controller (Thunderbolt/PCIe hotplug or emulated) returns DBOFF=0xF0000000 in capability registers -> xhci_init latches it unchecked -> any USB device insertion triggers ENABLE_SLOT -> xhci_do_command XWRITE4(sc,door,XHCI_DOORBELL(0),0) writes to io_hdl+0xF0000000 -> kernel page fault panic (proven); with an offset that lands on adjacent mapped MMIO -> cross-device register corruption instead (bounded by hardware topology)
Evidence (decisive lines)
panic.txt (fault VA 0xfffff8017df7b000 = io_hdl 0xfffff8008df7b000 + 0xF0000000, supervisor WRITE); run.log (baseline panic + fixed-kernel survival); poison.c (latch simulation module); fix.diff + fix_build_guest.log (validated fix)
PoC changes
Authored the PoC from scratch (no seed). KLD module walks devclass 'xhci' -> softc and writes the malicious DBOFF value into the already-latched sc_door_off, which is bit-identical to what xhci_init() would have stored from that register; trigger is a genuine USB device insertion via QEMU monitor.
Verified recommended fix
Validate CAPLENGTH/RTSOFF/DBOFF against sc_io_size in xhci_init() (ENXIO on violation) and bounds-check doorbell writes at use (fix.diff)
Verdict
xhci_init() latches CAPLENGTH/RTSOFF/DBOFF register offsets verbatim from controller MMIO (xhci.c:626-629) with no bounds check against the mapped BAR size; all XREAD/XWRITE macros (xhcireg.h:205-222) then use them raw. A malicious controller returning DBOFF=0xF0000000 makes the very next doorbell write an OOB MMIO write ~3.75GB past the 16KB BAR. Verified on QEMU/KVM DragonFly 6.5-DEVELOPMENT: latching simulation of the malicious register value + ordinary USB device insertion (ENABLE_SLOT command) yields 'Fatal trap 12: supervisor write data, page not present' at xhci_do_command+0x216 (movl $0,(%rdx)) with fault VA exactly io_hdl+0xF0000000. fix.diff (init-time validation + runtime doorbell guards) rebuilt and re-tested: identical trigger produces no panic, graceful enumeration failure, guest survives 4 plug/unplug cycles with the poison still loaded. Impact ceiling: device-directed OOB MMIO read/write (panic proven; adjacent-register corruption possible), attacker class is malicious/compromised PCIe-attached xHCI hardware (AV:P).
No comments yet.