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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5268 Β· 12 files
FileTypeDescriptionSize
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 VA 0xfffff8017df7b000 = io_hdl(0xfffff8008df7b000) + 0xF0000000, supervisor write data, page not present, Stopped at xhci_do_command+0x216: movl $0,(%rdx) (the doorbell write at sys/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 source
  • build.sh / run.sh β€” exact commands
  • run.log β€” full baseline (panic) + fixed (survives) transcript
  • panic.txt β€” kernel panic signature captured from serial console
  • env.txt β€” guest uname, compiler, controller, register values
  • fix.diff β€” validated fix (init-time bounds validation + runtime doorbell guards)
  • fix_build_guest.log β€” kernel build/install tail with fix applied
VERDICT.md
↓ download raw

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).

  1. Built xhcipwn.ko (pack source) which walks devclass_find("xhci") β†’ devclass_get_softc(dc, 0) and stores 0xF0000000 into the latched sc->sc_door_off β€” the exact value xhci_init() would have stored had the controller's DBOFF register held it (the only simulated step; QEMU hardcodes sane offsets in its own model).
  2. Loaded the module (dmesg confirms the real values first, then the poison).
  3. Inserted a USB device via the QEMU monitor (device_add usb-mouse, bus=xhci0.0,port=3) β†’ hub explore thread issues ENABLE_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 returns USB_ERR_IOERROR before 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

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

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).