Unsigned underflow in psp_v3_1_compare_sram_data ucode_size loop: OOB read and MMIO storm (latent - caller inside #if 0)
| Field | Value |
|---|---|
| ID | DF-1839 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:L/I:N/A:H |
| CWE | CWE-191 Integer Underflow; CWE-125 Out-of-bounds Read |
| File | sys/dev/drm/amd/amdgpu/psp_v3_1.c |
| Lines | 553-563 |
| Area | dev/drm/amd (PSP SRAM compare) |
| Confidence | certain |
| Discovered | 2026-07-20 |
| Reported | pending |
| Known CVE | none |
| CVE match | dfly_specific |
Summary
psp_v3_1_compare_sram_data copies ucode->ucode_size into a local
unsigned int ucode_size, then loops while (ucode_size) { ... ucode_size -= 4; }.
When ucode_size is not a multiple of 4 (i.e. 1, 2 or 3), the subtraction
underflows to ~0xFFFFFFFD and the loop continues for ~1 billion iterations,
reading OOB past the ucode->kaddr buffer and hammering
RREG32(fw_sram_data_reg_offset) on every iteration. ucode->ucode_size is
sourced verbatim from le32_to_cpu(header->ucode_size_bytes) in
amdgpu_ucode_init_single_fw (amdgpu_ucode.c:347) with no alignment check.
Latent: the only in-tree caller (amdgpu_psp_check_fw_loading_status at
amdgpu_psp.c:330) is inside #if 0 ... #endif, so the bug is currently
unreachable. It becomes exploitable the moment anyone re-enables that block or
wires psp_check_fw_loading_status into a new caller (a documented TODO in the
driver).
Root cause
psp_v3_1.c:553-564:
unsigned int ucode_size;
uint32_t *ucode_mem = NULL;
...
ucode_size = ucode->ucode_size;
ucode_mem = (uint32_t *)ucode->kaddr;
while (ucode_size) {
fw_sram_reg_val = RREG32(fw_sram_data_reg_offset);
if (*ucode_mem != fw_sram_reg_val)
return false;
ucode_mem++;
/* 4 bytes */
ucode_size -= 4;
}
ucode_size is unsigned int (uint32_t). ucode->ucode_size is set in
amdgpu_ucode_init_single_fw (amdgpu_ucode.c:347) directly from the firmware
common header ucode_size_bytes with no 4-byte alignment check; the firmware
kaddr buffer is sized to ucode_size. If ucode_size is 1/2/3 bytes (or any
value where ucode_size % 4 != 0 and the comparison never short-circuits), the
post-decrement wraps to 0xFFFFFFFD/0xFFFFFFFC/0xFFFFFFFF, the loop
continues, and ucode_mem is advanced past the end of ucode->kaddr β a kernel
OOB read β while simultaneously issuing billions of MMIO reads, hanging the
system. The loop's only early-exit is the SRAM-comparison branch, which is
data-dependent and cannot be relied on to terminate the loop.
Threat model & preconditions
- Attacker position: the function is registered as
psp_v3_1_funcs.compare_sram_data(psp_v3_1.c:624) and is invoked via thepsp_compare_sram_datamacro. The only in-tree caller isamdgpu_psp_check_fw_loading_statusat amdgpu_psp.c:547, which itself is only invoked from amdgpu_psp.c:330 β and that call site is inside#if 0 ... #endif. So the bug is currently latent and unreachable from in-tree code paths. - Privileges gained or impact: when invoked with a non-dword-aligned ucode:
OOB read on a kernel heap object (
ucode->kaddr) plus a sustained MMIO-storm DoS. - Required config or capabilities:
device amdgpuon VEGA10/VEGA12; ability to load a ucode blob withucode_size_bytesnot a multiple of 4 (e.g. 5). - Reachability: currently blocked by
#if 0at the call site. Becomes exploitable the moment anyone re-enables that block or wirespsp_check_fw_loading_statusinto a new caller.
Proof of concept
Cannot be reproduced against the current in-tree code because
psp_v3_1_compare_sram_data has no live caller (the only call site,
amdgpu_psp.c:330, is compiled out under #if 0).
To turn this into a working PoC once the function is re-wired (e.g. by toggling
the #if 0 block to #if 1), supply a ucode blob whose
common_firmware_header.ucode_size_bytes is set to a value that is not a
multiple of 4 (e.g. 5) β that single corrupt byte in any GFX/SDMA/RLC firmware
header triggers the underflow on the first iteration where the SRAM comparison
happens to match. Result: kernel OOB read of ucode->kaddr followed by a
minutes-long MMIO read storm against fw_sram_data_reg_offset, hanging the box.
Given the gating, the PoC is best delivered as a static analysis reproduction
plus a one-line patch to amdgpu_psp.c enabling the call, followed by loading a
ucode with ucode_size_bytes=5. Marked Low because no live path triggers it
today.
Impact
Low-severity latent bug. No live path reaches it; if the #if 0 block at
amdgpu_psp.c:330 is ever enabled (which is a documented TODO), a single
mis-sized byte in a firmware header turns into a kernel OOB read and a system
hang. Defense-in-depth: the loop is fragile by construction and should be
hardened regardless.
Recommended fix
Use a count that cannot underflow, and require dword alignment.
--- a/sys/dev/drm/amd/amdgpu/psp_v3_1.c
+++ b/sys/dev/drm/amd/amdgpu/psp_v3_1.c
@@ -550,6 +550,9 @@ static bool psp_v3_1_compare_sram_data(struct psp_context *psp,
err = psp_v3_1_sram_map(adev, &fw_sram_reg_val, &fw_sram_addr_reg_offset,
&fw_sram_data_reg_offset, ucode_type);
if (err)
return false;
+ /* ucode must be whole dwords, else the byte-by-dword loop underflows */
+ if (ucode->ucode_size == 0 || (ucode->ucode_size % 4) != 0)
+ return false;
WREG32(fw_sram_addr_reg_offset, fw_sram_reg_val);
@@ -565,7 +568,7 @@ static bool psp_v3_1_compare_sram_data(struct psp_context *psp,
if (*ucode_mem != fw_sram_reg_val)
return false;
ucode_mem++;
- /* 4 bytes */
- ucode_size -= 4;
+ ucode_size -= sizeof(uint32_t);
}
return true;
Equivalently, change the loop control to iterate
for (unsigned int i = 0; i < ucode->ucode_size / sizeof(uint32_t); i++) so the
index can never wrap. The same dword-alignment guarantee should also be enforced
at amdgpu_ucode_init_single_fw (amdgpu_ucode.c:347) where ucode_size is first
taken from the header.
References
ucode->ucode_sizesource: amdgpu_ucode.c:347.#if 0gate on the only in-tree caller: amdgpu_psp.c:330.
Timeline
- 2026-07-20 Discovered during automated audit.
- 2026-07-20 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1839 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | git-apply-able fix for the cited bug | 431 B | view raw |
| VERDICT.md | verdict | source-confirmation analysis | 1.0 KB | β raw |
| build.sh | build-script | N/A (source-only) | 65 B | view raw |
| run.sh | run-script | N/A (source-only) | 89 B | view raw |
DF-1839 VERDICT
Verdict: REPRODUCED (source-confirmed)
Impact: Low (driver-level NULL deref / OOB / leak / DoS β hardware-gated)
Mechanism: psp_v3_1_compare_sram_data L553-564 copies ucode->ucode_size into unsigned int ucode_size then loops while(ucode_size){...ucode_size-=4;}. When ucode_size not multiple of 4 (1/2/3) subtraction underfl
Citation: sys/dev/drm/amd/amdgpu/psp_v3_1.c:553-563
Fix: Applied fix.diff β compiles in batch kernel build (rc=0, -Werror).
Verification method: Source-only line-by-line trace of cited path:line. Low-severity driver bug; PoC trigger requires specific hardware or root context. Confirmed the cited vulnerable pattern exists in source.
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
fixedfix.diff compiled in batch kernel build rc=0 -Werror
fix.diff compiled in batch kernel build rc=0 -Werror
Confirmed kernel references
β
Detail
Exploit chain
none (Low severity)
Evidence (decisive lines)
Source-confirmed: ucode_size-=4 underflows when not multiple of 4 (psp_v3_1.c:553-563)
Verified recommended fix
Source-confirmed: ucode_size-=4 underflows when not multiple of 4 (psp_v3_1.c:553-563)
Verdict
Source-confirmed: ucode_size-=4 underflows when not multiple of 4 (psp_v3_1.c:553-563)
No comments yet.