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

Unbounded XHCI extended-capability pointer walk allows OOB MMIO read/write and kernel DoS from malicious PCI controller

Summary

xhci_pci_take_controller at xhci_pci.c:398-413 walks XHCI extended-capability list using offsets directly from controller MMIO: initial eecp = XHCI_HCS0_XECP(cparams)<<2 where XHCI_HCS0_XECP is 16-bit field (xhcireg.h:70) so first eecp can be up to 0xFFFF*4=256KB, larger than typical 8-64KB BARs. XHCI_XECP_NEXT is 8-bit. XREAD4/XREAD1/XWRITE1 expand to plain bus_space_read/write with NO bounds check (xhcireg.h:205-222). sc->sc_io_size recorded at xhci_pci.c:206 but NEVER consulted in this file. OOB MMIO reads at :401/:405/:413 and OOB MMIO write at :410 (XWRITE1 OS_SEM). No iteration cap: controller returning NEXT=0xFF from OOB read (0xFFFFFFFF) loops unboundedly with growing eecp until fault. On platforms with precise pmap_mapdev bounds: kernel page fault panic; on platforms returning 0xFFFFFFFF for OOB: infinite loop. Malicious PCI XHCI controller via Thunderbolt/ExpressCard/PCIe hot-plug. Same class as DF-1092 (EHCI). Fix: bound eecp+4<=sc->sc_io_size in loop condition.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1106 Β· 8 files
FileTypeDescriptionSize
df1106.c trigger-source documentation-only source trace stub 1.9 KB view raw
build.sh build-script no compilation needed (latent) 345 B view raw
run.sh run-script no runtime trigger 218 B view raw
fix.diff suggested-fix bound eecp+4 against sc->sc_io_size in the XECP loop 1022 B view raw
VERDICT.md verdict full source-level trace + Phase 6 hard-blocker analysis 3.8 KB ↓ raw
README.md readme claim summary + reproducibility notes 2.8 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme claim summary + reproducibility notes
↓ download raw

DF-1106 β€” Unbounded XHCI extended-capability pointer walk

Claim

xhci_pci_take_controller() at sys/bus/u4b/controller/xhci_pci.c:398-413:

for (eecp = XHCI_HCS0_XECP(cparams) << 2;            /* line 398 */
     eecp != 0 && XHCI_XECP_NEXT(eec);                /* line 399 */
     eecp += XHCI_XECP_NEXT(eec) << 2) {              /* line 400 */
    eec = XREAD4(sc, capa, eecp);                      /* line 401 β€” OOB read */
    if (XHCI_XECP_ID(eec) != XHCI_ID_USB_LEGACY)
        continue;
    bios_sem = XREAD1(sc, capa, eecp + XHCI_XECP_BIOS_SEM);  /* line 405 β€” OOB */
    ...
    XWRITE1(sc, capa, eecp + XHCI_XECP_OS_SEM, 1);            /* line 410 β€” OOB write */
    ...
}
  • XHCI_HCS0_XECP is a 16-bit field (xhcireg.h:70) β†’ initial eecp can be up to 0xFFFF << 2 = 262140 bytes, larger than typical XHCI BARs (8-64 KB).
  • XHCI_XECP_NEXT is an 8-bit field (xhcireg.h:192) β†’ grows eecp by up to 0xFF << 2 = 1020 bytes per iteration, with no termination other than reading a 0 from the controller.
  • XREAD4/XREAD1/XWRITE1 (xhcireg.h:205-222) expand to plain bus_space_read/write_* with no bounds check against sc->sc_io_size (which IS recorded at xhci_pci.c:206 but never consulted in this file).

A malicious PCI XHCI controller (Thunderbolt / ExpressCard / PCIe hot-plug) can: - Supply an eecp pointing past its BAR β†’ on platforms with precise pmap_mapdev bounds this is a kernel page fault panic. - Return NEXT=0xFF from an OOB read (returns 0xFFFFFFFF) β†’ loops unboundedly with growing eecp until fault (DoS). - Make the kernel perform an OOB MMIO write at line 410 (XWRITE1(sc, capa, eecp + XHCI_XECP_OS_SEM, 1)).

Same class as DF-1092 (EHCI).

Reproducibility on this guest

The default audit guest exposes no XHCI controller to the kernel. QEMU is launched with -device virtio-net-pci and no USB controller; the kernel boots without attaching xhci0. pciconf -lv | grep -i xhci is empty and dmesg | grep -iE 'xhci|usb' shows no XHCI attach.

The XHCI driver is built into the GENERIC kernel, but xhci_pci_attach (and thus xhci_pci_take_controller) is never called without an XHCI PCI device. The bug is latent β€” confirmed by source trace, not runtime-triggerable on this guest. QEMU can emulate an XHCI controller (-device qemu-xhci), but adding one would require modifying the vm.sh QEMU command (guest-facing PCI topology change), and the bug is fundamentally a malicious-controller scenario where the controller lies about its capability registers β€” QEMU's emulated XHCI reports honest values, so even with -device qemu-xhci the bounds check would never trigger.

Fix

fix.diff: bound eecp + 4 against sc->sc_io_size in the loop condition so any pointer past the mapped BAR terminates the walk.

VERDICT.md verdict full source-level trace + Phase 6 hard-blocker analysis
↓ download raw

DF-1106 β€” VERDICT

Verdict: NOT REPRODUCED at runtime (latent β€” no XHCI controller on the audit guest, and the trigger is fundamentally a malicious-controller scenario). Bug confirmed by source trace.

Mechanism (source-confirmed)

xhci_pci_take_controller() at sys/bus/u4b/controller/xhci_pci.c:398-413:

for (eecp = XHCI_HCS0_XECP(cparams) << 2;             /* :398 */
     eecp != 0 && XHCI_XECP_NEXT(eec);                 /* :399 */
     eecp += XHCI_XECP_NEXT(eec) << 2) {               /* :400 */
    eec = XREAD4(sc, capa, eecp);                       /* :401 β€” OOB read */
    if (XHCI_XECP_ID(eec) != XHCI_ID_USB_LEGACY)
        continue;
    bios_sem = XREAD1(sc, capa, eecp + XHCI_XECP_BIOS_SEM);  /* :405 β€” OOB */
    if (bios_sem == 0)
        continue;
    ...
    XWRITE1(sc, capa, eecp + XHCI_XECP_OS_SEM, 1);            /* :410 β€” OOB write */
    to = 500;
    while (1) {
        bios_sem = XREAD1(sc, capa, eecp + XHCI_XECP_BIOS_SEM); /* :413 β€” OOB */
        ...
    }
}
  • XHCI_HCS0_XECP is a 16-bit field (sys/bus/u4b/controller/xhcireg.h:70) β†’ initial eecp can be up to 0xFFFF << 2 = 262140, larger than typical XHCI BARs (8-64 KB).
  • XHCI_XECP_NEXT is an 8-bit field (xhcireg.h:192) β†’ grows eecp by up to 0xFF << 2 = 1020 per iteration.
  • XREAD4/XREAD1/XWRITE1 (xhcireg.h:205-222) expand to plain bus_space_read/write_* with no bounds check.
  • sc->sc_io_size IS recorded at xhci_pci.c:206 but is never consulted in this loop.

A malicious XHCI controller can: - Supply an eecp past its BAR β†’ kernel page-fault panic on platforms with precise pmap_mapdev bounds. - Return NEXT=0xFF from an OOB read (i.e. 0xFFFFFFFF) β†’ XHCI_XECP_NEXT(0xFFFFFFFF) = 0xFF (never 0) β†’ loops unboundedly with growing eecp until fault (DoS). - Force the kernel to perform an OOB MMIO write at line 410.

Same class as DF-1092 (EHCI).

Why it can't be triggered on this guest

$ dmesg | grep -iE 'xhci|usb'
(empty)
$ pciconf -lv | grep -iE 'xhci|class: 0x0c03'
(empty)

The QEMU guest has no XHCI controller. Even if one were added via -device qemu-xhci, the QEMU emulated XHCI reports honest extended-capability registers, so eecp would always be a valid in-BAR offset and the OOB walk wouldn't trigger. The bug is fundamentally a malicious-controller scenario (Thunderbolt / ExpressCard / PCIe hot-plug of a device that lies about its capability registers) β€” not reproducible on the audit guest.

Exploit chain

Primitive class: OOB MMIO read/write driven by attacker-controlled PCI capability registers. In principle an OOB MMIO write could be weaponised into code execution if the write targeted sensitive platform MMIO (e.g. SPI flash mapping, MSIX tables). BUT:

  • The primitive requires a malicious PCI device on the bus β€” not reproducible from any userspace action on the audit guest.
  • QEMU's emulated XHCI is well-behaved and won't trigger the bug.

Valid hard blocker ("vulnerable code path is dead code at runtime on this guest AND no harness can exercise it without hardware the guest lacks"). Documented as latent. No uid=0 achievable on this guest.

Fix

fix.diff adds eecp + 4 <= sc->sc_io_size to the loop condition so any pointer past the mapped BAR terminates the walk. Validated as applies + compiles in a clean GENERIC kernel build (rebuilt fresh, no warnings/errors).

Fix validation

not_testable β€” path not runtime-reachable on this guest. Validated fix.diff applies cleanly and compiles with -Werror in a full make nativekernel KERNCONF=X86_64_GENERIC (xhci_pci.o rebuilt fresh, no warnings/errors β€” see ../DF-1096/all_fixes_build.log).

PoC changes

  • Wrote df1106.c (documentation-only stub explaining the source trace).
  • Wrote fix.diff, build.sh, run.sh.

Fix verification

not_testable

compile validated

see evidence pack

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

Source-confirmed. XHCI extended-cap list walk no bounds -> OOB MMIO. No XHCI HW. Compile validated.