# DF-0990 — PCI capability-list walk has no cycle detection

## Verdict
**NOT REPRODUCED at runtime on the default guest — bug is real as a code
pattern (boot-time infinite loop / CPU spin), but triggering requires a
malicious PCI device whose config-space cap list contains a cycle.**
Defense-in-depth fix authored + compiled.

## Mechanism
`pci_read_capabilities()` at `sys/bus/pci/pci.c:847` walks the PCI
capability list following next-pointers from device config space:

```c
/* pci.c:872-892 */
nextptr = REG(ptrptr, 1);

while (pci_fixup_nextptr(&nextptr)) {
    const struct pci_read_cap *rc;
    int ptr = nextptr;

    nextptr = REG(ptr + PCICAP_NEXTPTR, 1);
    val     = REG(ptr + PCICAP_ID, 1);
    for (rc = pci_read_caps; rc->read_cap != NULL; ++rc) {
        if (rc->cap == val) {
            rc->read_cap(pcib, ptr, nextptr, cfg);
            break;
        }
    }
}
```

`pci_fixup_nextptr()` at `sys/bus/pci/pci.c:601-634`:

```c
/* pci.c:601-634 */
static int
pci_fixup_nextptr(int *nextptr0)
{
    int nextptr = *nextptr0;
    KASSERT(nextptr <= 0xff, ("Illegal next pointer %d", nextptr));
    if (nextptr & 0x3) {
        ...
        nextptr &= ~0x3;
    }
    *nextptr0 = nextptr;

    if (nextptr < 0x40) {
        ...
        return 0;
    }
    return 1;
}
```

**The function returns 1 for every `nextptr ∈ [0x40, 0xff]** — there is
no iteration counter and no visited-set.** A malicious PCI device whose
config space contains a self-loop (e.g. cap at offset 0x80 with
`PCICAP_NEXTPTR = 0x80`) causes the while loop to spin forever doing
config reads at full CPU speed. The kernel hangs at boot during PCI
enumeration.

This is reached from `pci_read_device()` (`sys/bus/pci/pci.c:574`) for
every PCI device present at boot.

## Reachability analysis (the crucial question)

The PCI config space is presented by the device firmware (or by the
hypervisor for virtual devices). On the audit guest:

- The QEMU/KVM virtual PCI devices (PIIX, virtio-blk, virtio-net, e1000,
  xhci, etc.) all present well-formed cap lists with no cycles. A survey
  with `pciconf -lv` shows the standard set of caps (MSI, PM, PCIe, etc.)
  and no anomalies.
- There is no syscall that lets an unprivileged user mutate the PCI cap
  list of an existing device. `pciconf` writes require root.

For an attacker to trigger this:

- They must present a **malicious physical PCI device** (PCIe card with
  malicious firmware) at boot, OR
- They must be running a **malicious hypervisor / host** that emulates a
  PCI device with a cyclic cap list (the guest is then DoSed at boot), OR
- They must exploit a separate vulnerability that grants them a
  config-space write primitive.

All three are outside the "unprivileged local user on the default guest"
threat model. **Valid hard blocker: the bug is real but cannot be
triggered by an unprivileged syscall on the default kernel + default
devices.**

## Exploit chain
**Not applicable / blocked by valid hard blocker (no malicious PCI
device).** The primitive is a boot-time CPU spin / DoS; there is no
memory corruption and no escalation path. Fixing the parser is the right
action.

## Fix
The fix in `fix.diff` adds an iteration counter to the cap-list walk.
The PCI spec allows at most `(256 - 64) / 4 = 48` distinct cap pointers
(pointers are >= 0x40 and 4-byte aligned), so 64 is a generous upper
bound that catches any cycle while still allowing all legitimate cap
lists:

```c
{
    int cap_iter = 0;
    while (pci_fixup_nextptr(&nextptr)) {
        ...
        if (++cap_iter > 64) {
            kprintf("pci%d:%d:%d:%d: capability list walk "
                "exceeded %d entries (cyclic list?), aborting\n",
                cfg->domain, cfg->bus, cfg->slot, cfg->func,
                cap_iter - 1);
            break;
        }
        ...
    }
}
```

## Fix validation
**Compile-only.** The bug requires a malicious PCI device not present on
the guest, so a runtime behavior-change cannot be demonstrated
(`fix_status: not_testable`). Validated:

- `fix.diff` applies cleanly with `patch -p1` (rc=0).
- A single-fix kernel was built with `make -j6 nativekernel
  KERNCONF=X86_64_GENERIC`; `pci.c` compiled under `-Werror`
  (no warnings, no errors); the kernel linked successfully
  (`/usr/obj/usr/src/sys/X86_64_GENERIC/kernel.stripped`,
  timestamp 14:22 today's build, NK_DONE rc=0). See `fix_build.log`.

## PoC changes
The PoC directory was seeded empty. No runtime trigger is possible on
the default guest. Authored `fix.diff` and this `VERDICT.md`.
