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

pcie_capability_read_dword leaves *dst uninitialized on register-not-implemented path diverging from LinuxKPI contract

Summary

pcie_capability_read_dword() returns -EINVAL without writing *dst when pcie_capability_reg_implemented() is false (linux_pci.c:35-36). Upstream Linux instead does *val=0 return 0. LinuxKPI consumers ported from upstream follow upstream convention ignore return value then read *dst unconditionally consuming uninitialized kernel-stack memory. Two live amdgpu call sites affected: pcie_get_width_cap() (pci.h:612) feeding amdgpu_device.c:3550 sets adev->pm.pcie_mlw_mask from uninitialized-derived link_width and amdgpu/cik.c:1634 gates clk_req_support on stale stack bit. Trigger is genuine runtime predicate: pcie_capability_reg_implemented returns false for PCI_EXP_LNKCAP whenever pcie_cap_has_lnkctl() false i.e. for PCIe v1 device whose type is not ROOT_PORT/ENDPOINT/LEG_END (switch upstream/downstream ports PCIe bridges). GPU upstream bridge can be such device on PCIe 1.x systems with discrete GPU behind switch. Impact: incorrect driver behavior potential hardware mis-programming possible local DoS from bad register write. No proven path to arbitrary code execution or direct userspace info leak uninitialized word masked to few bits before use. [VERIFIED pass2: reproduced - on every real guest PCI device the not-implemented path returns EINVAL leaving the 0xA5A5A5A5 canary in dst with the pcie_get_width_cap consumer deriving width 0x1a from uninitialized stack; no PCIe device on i440fx so implemented path unexercised honestly noted; zero-on-error fix-validated]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2223 Β· 14 files
FileTypeDescriptionSize
rig_head.c rig-source shared batch rig head 2.0 KB view raw
rig_mid.c rig-source verbatim production includes + fixed prototypes 1.1 KB view raw
rig_tail.c rig-source meter + test sections incl. real-device PCI enumeration with canaried dst 18.1 KB view raw
build.sh build-script provenance chain + fixed-variant extraction + kernel-compile 153 B view raw
run.sh run-script sysctl trigger + dmesg extraction 172 B view raw
fix.diff suggested-fix *dst = 0 on the register-not-implemented path (upstream contract) 325 B view raw
build.log build-log FULL untrimmed nativekernel DFV22X build 5.5 MB ↓ download
run.log run-log FULL decisive rig output 7.8 KB view raw
run_section.log run-log DF-2223 lines only 5.2 KB view raw
env.txt environment guest env + hashes + PCI topology 2.1 KB view raw
VERDICT.md verdict full narrative incl. honest non-PCIe-hardware limitation 4.7 KB ↓ raw
README.md readme build/run/expected 3.8 KB ↓ raw
manifest.json manifest this file 2.4 KB view raw
verdict.json verdict machine verdict for persist_poc.py 3.4 KB view raw
README.md readme build/run/expected
↓ download raw

DF-2223 β€” pcie_capability_read_dword() leaves *dst uninitialized on the not-implemented path

Status: REPRODUCED (verbatim-code, kernel-compiled rig, REAL PCI devices) β€” impact class: uninitialized-stack word consumed by return-value-ignoring consumers β†’ driver misbehavior / possible hw mis-programming (DoS class). Fix: A/B-validated in the same harness kernel (fix.diff, *dst = 0).

Part of the DF-22X batch (DF-2216 / DF-2220 / DF-2223 / DF-2224) verified in ONE harness kernel; this pack documents the DF-2223 section.

What this pack contains

Same layout as the DF-2216 pack; finding-specific: fix.diff (against sys/dev/drm/linux_pci.c), run_section.log (DF-2223 lines).

How to reproduce

Identical to the DF-2216 pack (same batch build + run).

Provenance β€” what is REAL here

  1. guest tree linux_pci.c sha256 314205e0... == audited tree; pcie_capability_read_dword compiled into the kernel verbatim.
  2. The predicate chain is entirely real: pcie_capability_reg_implemented (linux/pci.h:473) β†’ pci_is_pcie(dev->dev.bsddev) (real inline over the real pci_get_pciecap_ptr ivar read) and, on the implemented path, pcie_flags_reg β†’ pci_find_capability β†’ real pci_find_extcap + pci_read_config config-space reads.
  3. The only fabricated object is a zeroed struct pci_dev whose dev.bsddev points at each real guest PCI device_t (all 7 children of pci0: hostb0, isab1, atapci0, none0, vgapci0, virtio_pci0/1).
  4. pcie_capability_read_dword_fixed = same bytes + fix.diff, def renamed (fixed_pci.c sha256 c0de4487...).

Expected output (baseline = unmodified production code)

DF2223 BASELINE hostb0      LNKCAP rc=-22 dst=0xa5a5a5a5 UNTOUCHED-CANARY (dst left uninitialized)
DF2223 BASELINE hostb0      pcie_get_width_cap-style consumer: lnkcap=0xa5a5a5a5 -> width=0x1a (derived from UNINITIALIZED stack)
DF2223 BASELINE hostb0      CONTROL FLAGS_TYPE rc=-22 dst=0xa5a5a5a5 (non-PCIe guest: predicate false for every reg)
DF2223 FIXED    hostb0      LNKCAP rc=-22 dst=0x00000000 WRITTEN-0 (upstream contract)
DF2223 FIXED    hostb0      pcie_get_width_cap-style consumer: lnkcap=0x00000000 -> width=0xff (deterministic UNKNOWN)
DF2223 VERDICT: 7 devices exercised; baseline left *dst untouched (canary intact) on every not-implemented read while FIXED zeroes it (upstream Linux contract: *val = 0)
  • dst is canary-loaded (0xA5A5A5A5) before every call; baseline leaves it bit-identical β†’ the caller (which per pcie_get_width_cap, linux/pci.h:608-617, ignores the return value) consumes the uninitialized stack word (width = 0x1a from the canary pattern β€” deterministic proof of consumption).
  • The FIXED variant writes *dst = 0 on the same path β†’ deterministic PCIE_LNK_WIDTH_UNKNOWN, exactly the upstream Linux contract.
  • Honest limitation, visible in the log: this i440fx guest has no PCIe devices, so pci_is_pcie() is false everywhere and even the PCI_EXP_FLAGS_TYPE control hits the early-out; the implemented path (line 38, real config read) is not exercisable on this hardware. The finding is exclusively about the not-implemented path, which IS exercised.

Attacker input / reachability

The predicate is false for every register on non-PCIe devices, and for LNKCAP/LNKCTL/LNKSTA on PCIe v1 devices whose type is not ROOT_PORT / ENDPOINT / LEG_END (pcie_cap_has_lnkctl, linux/pci.h:445-453) β€” e.g. a discrete GPU behind a v1 switch port. Live consumers per the finding: pcie_get_width_cap() (linux/pci.h:612) feeding amdgpu_device.c:3550 (adev->pm.pcie_mlw_mask) and amdgpu/cik.c:1634 (clk_req_support gating). The uninitialized bits are masked before use and never reach userspace β†’ no direct info leak; ceiling is incorrect driver behavior / wrong register programming (DoS class). No write primitive β†’ no privesc route.

VERDICT.md verdict full narrative incl. honest non-PCIe-hardware limitation
↓ download raw

DF-2223 β€” VERDICT

REPRODUCED β€” verbatim-code, kernel-compiled rig driving the real function over REAL guest PCI devices through the fully real predicate chain; fix A/B-validated in the same kernel. Impact: uninitialized-stack word consumed by return-value-ignoring consumers β†’ driver misbehavior / possible hw mis-programming (DoS class). No direct info leak to userspace, no write primitive, no privesc route (see below).

Rig class and why

Same batch harness kernel as DF-2216 (DFV22X); the production sys/dev/drm/linux_pci.c is #included verbatim (sha-gated 314205e0...), bringing the real linux/pci.h inline predicate chain with it.

Line-accurate confirmation

linux_pci.c:35-36:

    if (!pcie_capability_reg_implemented(dev, pos))
        return -EINVAL;         /* *dst never written */

Upstream Linux does *val = 0; return 0; on this path. Consumers ported from upstream follow the upstream contract: pcie_get_width_cap() (linux/pci.h:608-617) ignores the return and reads lnkcap unconditionally; the amdgpu call sites cited by the finding (amdgpu_device.c:3550, amdgpu/cik.c:1634) inherit that. The predicate is genuinely reachable-false: its first check is pci_is_pcie(dev->dev.bsddev) (linux/pci.h:475, real ivar read β€” false for every classic-PCI device), and for PCIe devices pcie_cap_has_lnkctl() (:445-453) is false for LNKCAP/LNKCTL/LNKSTA whenever the capability version is 1 and the type is not ROOT_PORT/ENDPOINT/LEG_END (v1 switch ports, bridges).

Mechanical reproduction (decisive run, run.log)

The rig wraps each of the 7 real guest PCI device_t's in a zeroed struct pci_dev (dev.bsddev = real device) and calls the real function with a canary-loaded *dst (0xA5A5A5A5):

variant result (all 7 devices)
BASELINE PCI_EXP_LNKCAP rc=-22, dst=0xa5a5a5a5 UNTOUCHED-CANARY β€” bit-identical to pre-call
BASELINE consumer emulation (mirrors pcie_get_width_cap exactly: rc ignored, lnkcap consumed) lnkcap=0xa5a5a5a5 β†’ width=0x1a β€” derived from uninitialized stack
FIXED PCI_EXP_LNKCAP rc=-22, dst=0x00000000 β€” WRITTEN-0 (upstream contract)
FIXED consumer width=0xff (PCIE_LNK_WIDTH_UNKNOWN) β€” deterministic

width=0x1a is exactly (0xA5A5A5A5 & 0x3F0) >> 4 β€” the canary flows through the consumer arithmetic, proving the uninitialized word is genuinely consumed, not merely hypothetical. Baseline vs fixed differ in precisely the filed way: one zeroing store.

Honest limitation (also visible in the log): the PCI_EXP_FLAGS_TYPE control was intended to exercise the implemented path (real config read at line 38), but on this i440fx guest every device is classic PCI, so the predicate's pci_is_pcie first-check is false for every register and the control also returns -EINVAL. The implemented path is not exercisable on this hardware; the finding concerns only the not-implemented path, which is fully exercised.

Fix validation (A/B, same kernel)

fix.diff adds *dst = 0; on the not-implemented branch (keeping the -EINVAL return, the conservative minimal divergence-fix; upstream also returns 0 there β€” noted in the finding). The patched function (pcie_capability_read_dword_fixed, extracted from fix.diff-applied bytes) compiled into the same kernel writes the deterministic zero on every device. fix_status: fixed.

Reachability (who can actually trigger this)

Two realistic flavors: (a) any non-PCIe device passed to the shim (every predicate bit false); (b) the filed amdgpu flavor β€” a PCIe v1 GPU behind a switch/bridge port where pcie_cap_has_lnkctl() is false for LNKCAP. The amdgpu consumers then program adev->pm.pcie_mlw_mask and gate clk_req_support from an uninitialized stack word. Requires AMD GPU hardware (absent on the audit guest); the code-level divergence itself is proven on 7 real devices. The uninitialized bits are masked (& 0x3F0 >> 4 etc.) before use and never cross the user boundary β†’ no userspace info leak; the ceiling is wrong register programming / misbehavior β€” a local DoS / incorrect-function class consistent with the filed Low severity.

Privesc assessment (standing directive)

None. The defect mis-initializes one caller-owned u32; there is no OOB access (the early return skips the config read), no write to kernel memory the caller did not pass, no disclosure of the raw uninitialized word to userspace. No primitive β†’ no escalation chain.

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.

Fix verification

fixed
↓ fix.diffper-fix-DF-2223

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log lines DF2223 BASELINE/FIXED per device (hostb0, isab1, atapci0, none0, vgapci0, virtio_pci0, virtio_pci1): baseline rc=-22 dst=0xa5a5a5a5 UNTOUCHED-CANARY on all 7, consumer width=0x1a from uninit stack; FIXED rc=-22 dst=0x00000000, consumer width=0xff deterministic

PoC changes

Rebuilt as verbatim-code kernel-compiled rig; replaced the PoC sketch idea of a purely fabricated pci_dev with a zeroed struct pci_dev wrapping each real guest device_t so the predicate chain and config reads are entirely real; stack-canary instrumentation around *dst

Verified recommended fix

Zero *dst before returning -EINVAL on the register-not-implemented path, matching the upstream LinuxKPI contract consumers expect (see fix.diff)

Verdict

Reproduced mechanically on 7 REAL guest PCI devices through the fully real code path: the verbatim pcie_capability_read_dword, the real pcie_capability_reg_implemented predicate chain (real pci_is_pcie ivar reads, real config-space access helpers), with only struct pci_dev fabricated as a zeroed wrapper whose dev.bsddev points at the real device_t. On every device the baseline function returns -EINVAL for PCI_EXP_LNKCAP while leaving dst completely untouched (stack canary 0xa5a5a5a5 intact) - the exact contract divergence from upstream Linux (val = 0 before the error return). The consumer emulation mirroring the real pcie_get_width_cap inline (linux/pci.h:608-617, return value ignored) then derives width 0x1a from the uninitialized stack word; the fixed variant writes *dst = 0 giving the deterministic PCIE_LNK_WIDTH_UNKNOWN. No userspace info leak (the garbage bits are masked before use); realistic ceiling is driver misbehavior / wrong register programming on hardware where the predicate is false (non-PCIe devices, or PCIe v1 non-endpoint/root devices per the finding), a local DoS / incorrect-driver-behavior class. No corruption or write primitive, no privesc chain. Fix validated A/B in the same kernel.