# DF-2336 — Unbounded MMIO in xHCI extended-capability traversal (xhci_pci.c)

## Verdict: NOT REPRODUCED (HW-gated from unprivileged attacker); REAL BUG IN SOURCE (defense-in-depth fix warranted)

## Hardware gate (why the PoC cannot run as an unprivileged user)

The audit QEMU/KVM guest *does* load the xHCI host-controller module
(`kldstat` shows `xhci.ko`) and QEMU exposes an emulated xHCI controller, but
the vulnerable function `xhci_pci_take_controller()` runs **exactly once, during
the controller's attach/probe**, as part of the BIOS-legacy handoff. It is
driven entirely by MMIO reads of the controller's capability registers, never
by a userspace syscall. An unprivileged user:

- cannot re-trigger probe/attach,
- cannot open `/dev/usbctl` for device control (`maxx` is not in the
  `operator` group; only `/dev/usbctl` exists and there are no `/dev/ugen*`
  device nodes — `usbconfig list` returns "No device match"),
- cannot present a *crafted/malicious* xHCI controller to the guest.

```
$ usbconfig list               # No device match or lack of permissions.
$ ls /dev/usb*                 # only /dev/usbctl (operator group; maxx not in operator)
$ id maxx                      # uid=1001(maxx) gid=1001(maxx) groups=1001(maxx)
$ pciconf -l | grep -i xhci    # (controller present but already attached at boot)
```

The bug's threat model is a **malicious PCIe xHCI controller** (compromised
firmware / Thunderbolt / USB4 / ExpressCard / emulated-XHCI VM). QEMU's
emulated xHCI is well-behaved and has already been claimed at boot, so the
vulnerable loop does not re-execute for any action an unprivileged user can
take. This is a hardware-firmware-malice gate, not a userspace-reachable path.

## Source trace — the bug is REAL (sys/bus/u4b/controller/xhci_pci.c)

`xhci_pci_take_controller()` (`xhci_pci.c:384-426`) walks the xHCI
extended-capability linked list:
```c
cparams = XREAD4(sc, capa, XHCI_HCSPARAMS0);          /* device MMIO */
for (eecp = XHCI_HCS0_XECP(cparams) << 2;             /* first offset from MMIO */
     eecp != 0 && XHCI_XECP_NEXT(eec);
     eecp += XHCI_XECP_NEXT(eec) << 2) {              /* next offset from MMIO */
    eec = XREAD4(sc, capa, eecp);                     /* xhci_pci.c:401 */
    if (XHCI_XECP_ID(eec) != XHCI_ID_USB_LEGACY)
        continue;
    bios_sem = XREAD1(sc, capa, eecp + XHCI_XECP_BIOS_SEM);   /* :405 */
    ...
    XWRITE1(sc, capa, eecp + XHCI_XECP_OS_SEM, 1);            /* :410 */
    ...
}
```

The `XREAD4`/`XREAD1`/`XWRITE1` macros (`xhcireg.h:205-222`) are raw
`bus_space_read/write` at offset `eecp + sc->sc_capa_off`:
```c
#define XREAD4(sc, what, a)  bus_space_read_4((sc)->sc_io_tag, (sc)->sc_io_hdl, \
                                              (a) + (sc)->sc_##what##_off)
```

The extended-capability pointer `eecp` is taken entirely from device-controlled
MMIO (`XHCI_HCS0_XECP(cparams)` and `XHCI_XECP_NEXT(eec)`). The 16-bit XECP
field can encode offsets up to `0x3FFFC` (256 KB), far exceeding typical 4-16 KB
xHCI BARs, yet `eecp` is **never bounds-checked against `sc->sc_io_size`** (the
mapped BAR size, set at `xhci_pci.c:206`). There is also **no iteration cap**, so
a malicious controller returning small non-zero NEXT pointers spins millions of
times before `uint32` wraparound — a multi-minute boot/hot-plug hang.

A crafted `HCSPARAMS0`/capability chain from a malicious controller therefore
produces OOB MMIO reads (`:401`, `:405`, `:413`) and a conditional OOB 1-byte
MMIO write (`:410`) past the mapped region, plus a hang. Impact: kernel panic /
DoS, OOB read/write, cross-device register corruption.

## Exploit chain status

Not pursuable — primitive (OOB MMIO R/W + hang) requires a malicious xHCI
controller, which the unprivileged `maxx` user cannot present and which QEMU's
emulated controller is not (valid Phase-6 hard blocker: vulnerable path
reachable only from a malicious PCI device, not from userspace; QEMU's
controller is well-behaved and already attached).

## PoC changes

None. The controller is present but already attached and benign; the bug is not
re-triggerable from userspace. Verified by source trace only.

## Recommended fix

Bounds-check `eecp` against the mapped BAR size and cap the iteration count. See
`fix.diff` (supersedes finding proposal by adding both a bounds check and an
iteration cap).
