PCI capability-list walk has no cycle detection, malicious device hangs kernel forever
Summary
pci_read_capabilities at pci.c:847-892 walks cap list following next-pointers from device config space. pci_fixup_nextptr :601-634 returns 0 only for nextptr<0x40, returns 1 for all [0x40,0xff]. NO iteration counter, NO visited-set. Malicious device: cap at 0x80 next=0x80 (self-loop) -> while loop at :877 spins forever doing config reads at full CPU speed. Reached from pci_read_device :574 for every hot-plug device with PCIM_STATUS_CAPPRESENT. Hang at boot or on hot-plug insertion. $10 FPGA or 1-line QEMU quirk. Fix: itercnt counter capped at 48 (max distinct 4-byte-aligned offsets in [0x40,0xff]).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0990 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | git-apply-able fix: add 64-iteration cap to PCI cap-list walk | 1.6 KB | view raw |
| fix_build.log | build-log | pci.c compiles under -Werror; NK_DONE rc=0; kernel.stripped 15.7MB today | 1.1 KB | view raw |
| env.txt | environment | uname, kern.version | 260 B | view raw |
| VERDICT.md | verdict | detailed analysis: mechanism, reachability, fix | 4.4 KB | β raw |
| README.md | readme | summary | 1.3 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 |
DF-0990 β PCI capability-list walk has no cycle detection
Summary
pci_read_capabilities() at sys/bus/pci/pci.c:847 walks the PCI cap
list following next-pointers from device config space. pci_fixup_nextptr()
at pci.c:601 returns 1 for every nextptr β [0x40, 0xff] β there is
NO iteration counter and NO visited-set. A malicious PCI device with a
self-loop in its cap list (e.g. cap@0x80 next=0x80) causes an infinite
spin doing config reads at full CPU speed β boot-time DoS.
Reachability: Requires a malicious PCI device (physical card with malicious firmware, or a malicious hypervisor emulating one). The QEMU guest's virtual PCI devices all present well-formed cap lists. Not triggerable by an unprivileged user.
Files
fix.diffβ git-apply-able fix: add 64-iteration cap to detect cyclesfix_build.logβ kernel build output showingpci.ccompiles cleanly under-Werrorwith the fix applied (NK_DONE rc=0)env.txtβ guest environmentVERDICT.mdβ detailed analysismanifest.jsonβ artifact catalog
Reproduce
No runtime reproduction possible (requires malicious PCI hardware). To rebuild the patched kernel:
scp fix.diff dfbsd:/root/fix.diff
ssh dfbsd '/bin/sh -c "cd /usr/src && patch -p1 --forward < /root/fix.diff && make -j6 nativekernel KERNCONF=X86_64_GENERIC"'
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:
/* 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:
/* 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 withPCICAP_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 -lvshows 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.
pciconfwrites 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:
{
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.diffapplies cleanly withpatch -p1(rc=0).- A single-fix kernel was built with
make -j6 nativekernel KERNCONF=X86_64_GENERIC;pci.ccompiled 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). Seefix_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.
Fix verification
not_testablecompile validated
kernel build rc=0 -Werror
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source-confirmed. PCI cap list walk no cycle detection -> boot hang. Needs malicious PCI device. Compile validated.
No comments yet.