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)
PoC verification
Evidence pack
findings/poc/DF-2223 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
- guest tree
linux_pci.csha256314205e0...== audited tree;pcie_capability_read_dwordcompiled into the kernel verbatim. - The predicate chain is entirely real:
pcie_capability_reg_implemented(linux/pci.h:473) βpci_is_pcie(dev->dev.bsddev)(real inline over the realpci_get_pciecap_ptrivar read) and, on the implemented path,pcie_flags_regβpci_find_capabilityβ realpci_find_extcap+pci_read_configconfig-space reads. - The only fabricated object is a zeroed
struct pci_devwhosedev.bsddevpoints at each real guest PCI device_t (all 7 children of pci0: hostb0, isab1, atapci0, none0, vgapci0, virtio_pci0/1). pcie_capability_read_dword_fixed= same bytes +fix.diff, def renamed (fixed_pci.csha256c0de4487...).
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)
dstis canary-loaded (0xA5A5A5A5) before every call; baseline leaves it bit-identical β the caller (which perpcie_get_width_cap,linux/pci.h:608-617, ignores the return value) consumes the uninitialized stack word (width = 0x1afrom the canary pattern β deterministic proof of consumption).- The FIXED variant writes
*dst = 0on the same path β deterministicPCIE_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 thePCI_EXP_FLAGS_TYPEcontrol 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.
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
fixedConfirmed 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.
No comments yet.