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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0990 Β· 7 files
FileTypeDescriptionSize
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
README.md readme summary
↓ download 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 cycles
  • fix_build.log β€” kernel build output showing pci.c compiles cleanly under -Werror with the fix applied (NK_DONE rc=0)
  • env.txt β€” guest environment
  • VERDICT.md β€” detailed analysis
  • manifest.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"'
VERDICT.md verdict detailed analysis: mechanism, reachability, fix
↓ download raw

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 -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:

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

Fix verification

not_testable

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