# DF-2324 — capability-list traversal has no cycle detection — VERDICT

**status: reproduced / impact: panic (the mechanical capture of a
kernel-hang / unbounded-enumeration-loop denial of service; the
watchdog panic is the proof instrument, the observed behavior is that
the walker never returns).**

## Line-accurate analysis (verified mechanically)

- `pci_read_capabilities` (pci.c:877-892) loops
  `while (pci_fixup_nextptr(&nextptr))` where `nextptr` is read from
  DEVICE config space (`REG(ptr + PCICAP_NEXTPTR, 1)` at pci.c:882).
  `pci_fixup_nextptr` (pci.c:601-634) only masks the low 2 bits and
  rejects values < 0x40 — it does NOT detect re-visits. A device that
  points 0x40 -> 0x60 -> 0x40 makes this loop spin forever.
- `pci_find_extcap_method` (pci.c:1407-1414) loops
  `while (ptr != 0)` with `ptr = pci_read_config(child,
  ptr + PCICAP_NEXTPTR, 1)` — NO masking (unlike pci_fixup_nextptr),
  NO < 0x40 rejection, NO iteration bound. A self-loop or 2-cycle
  spins forever; unaligned pointers are followed verbatim.
- Both walkers run during device enumeration (boot for every PCI
  device; hotplug/rescan later) and in driver probe/attach paths —
  a malicious device hangs the kernel in a tight loop with the bus
  lock context of the enumeration path (boot hang / livelocked CPU).

## What was run

Fabricated config space driven through the VERBATIM walkers
(sha256-gated extracts; see DF-2323 pack rig, shared).

| run | kernel | result |
|-----|--------|--------|
| `cap linear` (PMG->MSI->MSIX->EXPRESS->0) | DFPCI (stock, INVARIANTS) | enumerated fully; cfg digest logged (rig/linear_baseline.log): pp_cap=0xf001 msi_loc=0x50 msix_loc=0x60 msix_ctrl=0x3 msix_msgnum=4 msix_tbar=0x14 pba 0x14/0x10 expr_ptr=0x70 expr_cap=0x41 pcie=1 |
| `extcap linear` | DFPCI | find MSI rc=0 capreg=0x50; find HT rc=2 (ENOENT) |
| `extcap odd` (next=0x45, 0x4A) | DFPCI | walker followed UNALIGNED pointers 0x40->0x45->0x4A->0 — no masking (the pci_fixup_nextptr contrast the finding asks about) |
| `cap cycle` (0x40<->0x60) | DFPCI | sysctl write NEVER returned; after ~12 s the hard-IRQ systimer watchdog fired: `panic: DFPCI_WATCHDOG_HANG: 'cap cycle' stuck in cap-list loop (cycle, no iteration bound)` — panic_cap_cycle.txt (systimer_intr <- pcpu_timer_process_oncpu hard-IRQ frames) |
| `extcap cycle` (0x40->0x60->0x40) | DFPCI | same hang, same watchdog capture — panic_extcap_cycle.txt |
| `cap cycle` | DFPCIPF (production + fix.diff) | **RETURNED** — `=== cap cycle RETURNED (loop terminated!)` after the (0x100-0x40)/4 = 48-iteration bound |
| `extcap cycle` | DFPCIPF | **RETURNED rc=6 (ENXIO)** via the 64-iteration bound |
| `cap linear` | DFPCIPF | CAPDIGEST byte-IDENTICAL to the unfixed baseline (diff clean) — legit enumeration unchanged |
| `extcap linear` / `extcap odd` | DFPCIPF | find MSI rc=0 capreg=0x50, HT ENOENT unchanged; odd chain now masks (0x45->0x44) and terminates safely (rc=ENOENT) |

The watchdog (rig/dfpci_post.c dfh_watchdog, 2 Hz systimer, fires from
hard-interrupt context at 12 s) is the decisive-hang instrument: a
48-entry max walk returns in microseconds; a non-returning walker
after 12 s of spinning is a hang by construction, and the panic
backtrace shows the systimer interrupt frames.

## Impact

Malicious PCI device (AV:P / crafted passthrough) hangs the kernel in
an unbounded loop at enumeration time: boot never completes (every
boot), hotplug rescan wedges, driver probe paths livelock. No memory
corruption; availability only. If triggered post-boot the looping
thread spins with preemption points absent (tight C loop) — on this
UP-ish guest the console went unresponsive immediately.

## Fix (validated A/B)

`rig/fix.diff` (combined diff, DF-2324 hunks):
- `pci_read_capabilities`: `int ncap` guard, `if (++ncap >
  (0x100 - 0x40) / 4) break;` — config space has at most 48 aligned
  capability slots above 0x40.
- `pci_find_extcap_method`: sanitize `ptr &= ~0x3` (same masking as
  pci_fixup_nextptr, closing the unaligned-pointer follow) and
  `if (++ncap > 0x100 / 4) return (ENXIO);`.

A/B: cycles now terminate (RETURNED), the linear-chain digest is
byte-identical pre/post fix, extcap find results unchanged.

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

(No fix.diff for this item: documented recommendation only - see Fix above.)
