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

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 the psp_compare_sram_data macro. The only in-tree caller is amdgpu_psp_check_fw_loading_status at 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 amdgpu on VEGA10/VEGA12; ability to load a ucode blob with ucode_size_bytes not a multiple of 4 (e.g. 5).
  • Reachability: currently blocked by #if 0 at the call site. Becomes exploitable the moment anyone re-enables that block or wires psp_check_fw_loading_status into 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.

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_size source: amdgpu_ucode.c:347.
  • #if 0 gate 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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1839 Β· 4 files
FileTypeDescriptionSize
fix.diff suggested-fix git-apply-able fix for the cited bug 431 B view raw
VERDICT.md verdict source-confirmation analysis 713 B ↓ raw
build.sh build-script N/A (source-only) 61 B view raw
run.sh run-script N/A (source-only) 87 B view raw
VERDICT.md verdict source-confirmation analysis
↓ download 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 verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff compiled in batch kernel build rc=0 -Werror

fix.diff compiled in batch kernel build rc=0 -Werror
↓ fix.diffcombined build rc=0

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)