β¬’ 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 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
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 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
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)