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

PSP firmware header fields used without bounds validation: heap overflow and OOB read on corrupt SOS/ASD firmware

Field Value
ID DF-1838
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H
CWE CWE-20 Improper Input Validation; CWE-787 Out-of-bounds Write; CWE-191 Integer Underflow
File sys/dev/drm/amd/amdgpu/psp_v3_1.c
Lines 126-155
Area dev/drm/amd (PSP firmware loader)
Confidence likely
Discovered 2026-07-20
Reported pending
Known CVE none
CVE match dfly_specific

Summary

psp_v3_1_init_microcode reads ucode_array_offset_bytes, ucode_size_bytes, sos_offset_bytes, and sos_size_bytes directly out of the firmware blob and uses them as pointer offsets and memcpy lengths without any bounds check. amdgpu_ucode_validate (amdgpu_ucode.c:251-260) only verifies fw->datasize == hdr->size_bytes, so all internal fields are attacker-controlled. The unsigned subtraction sys_bin_size = ucode_size_bytes - sos_size_bytes underflows to ~4 GiB when sos_size_bytes > ucode_size_bytes; sys_start_addr / sos_start_addr are computed by adding unverified offsets to the blob base. The resulting values are later consumed as memcpy lengths into the fixed 1 MiB fw_pri_buf in psp_v3_1_bootloader_load_sysdrv (line 195) and psp_v3_1_bootloader_load_sos (line 255), producing a kernel heap overflow write and an OOB read on the firmware object.

Root cause

psp_v3_1.c:130-139:

hdr = (const struct psp_firmware_header_v1_0 *)adev->psp.sos_fw->data;
adev->psp.sos_fw_version  = le32_to_cpu(hdr->header.ucode_version);
adev->psp.sos_feature_version = le32_to_cpu(hdr->ucode_feature_version);
adev->psp.sos_bin_size    = le32_to_cpu(hdr->sos_size_bytes);
adev->psp.sys_bin_size    = le32_to_cpu(hdr->header.ucode_size_bytes) -
                            le32_to_cpu(hdr->sos_size_bytes);   /* uint32_t subtraction, no overflow check */
adev->psp.sys_start_addr  = (uint8_t *)hdr +
                            le32_to_cpu(hdr->header.ucode_array_offset_bytes);   /* no check vs fw->datasize */
adev->psp.sos_start_addr  = (uint8_t *)adev->psp.sys_start_addr +
                            le32_to_cpu(hdr->sos_offset_bytes);                  /* no check vs fw->datasize */

hdr->header is the common_firmware_header (amdgpu_ucode.h:26-37); ucode_size_bytes / ucode_array_offset_bytes are 32-bit fields. The only validation done before this code runs is amdgpu_ucode_validate (amdgpu_ucode.c:251), which compares only the outer size_bytes field to fw->datasize and returns 0 β€” it never checks the inner layout fields.

Both sys_bin_size and sos_bin_size are stored into the psp_context as raw uint32_t (amdgpu_psp.h:108-109).

The same defect exists for the ASD blob at psp_v3_1.c:150-155 (asd_ucode_size from hdr->header.ucode_size_bytes; asd_start_addr from ucode_array_offset_bytes), consumed later by amdgpu_psp.c:254-255 memcpy(psp->fw_pri_buf, psp->asd_start_addr, psp->asd_ucode_size).

Later, in psp_v3_1_bootloader_load_sysdrv (psp_v3_1.c:192-195):

memset(psp->fw_pri_buf, 0, PSP_1_MEG);
memcpy(psp->fw_pri_buf, psp->sys_start_addr, psp->sys_bin_size);

fw_pri_buf is allocated as exactly PSP_1_MEG (0x100000) at amdgpu_psp.c:350-354. If sys_bin_size was set via the underflow to, say, 0xFFFFFF00, the memcpy writes ~4 GiB into a 1 MiB buffer and reads past the firmware object. Identical pattern at psp_v3_1.c:252-255 for the SOS binary.

Threat model & preconditions

  • Attacker position: write access to the firmware image used by the amdgpu driver for VEGA10/VEGA12 (e.g. /boot/modules firmware, or any path firmware_get() can resolve on DragonFlyBSD β€” request_firmware at sys/dev/drm/include/linux/firmware.h:39 calls firmware_get(name)). This normally requires root (kldload-equivalent privilege).
  • Privileges gained or impact: kernel heap corruption β€” memcpy writes past the 1 MiB fw_pri_buf GTT buffer, corrupting adjacent slab objects and reading past the firmware object. Floor: deterministic kernel panic on every GPU init / resume / GPU-reset-recovery path (psp_load_fw / psp_hw_start).
  • Required config or capabilities: device amdgpu on VEGA10/VEGA12; ability to write the firmware image (root) or trigger a GPU reset that recovers through psp_load_fw. The corruption is reached on every init.
  • Reachability: any user who can trigger a GPU reset can leverage a pre-planted corrupt firmware to turn a noisy GPU fault into deterministic kernel heap corruption. On systems where firmware is delivered out-of-band (network provisioning, vendor-signed-but-malformed blobs, filesystem corruption), the kernel must not trust inner-header fields even when the outer size matches.

Proof of concept

PoC source: findings/poc/DF-1838/gen_fw.py

Build & run

python3 gen_fw.py
# Install as the SOS firmware used by the driver (path resolved by
# firmware_get("amdgpufw_vega10_sos")). Then reboot with VEGA10/VEGA12
# hardware present, or kldload amdgpu, or trigger a GPU reset recovery.

Expected output

kernel page-fault / panic in memcpy called from psp_v3_1_bootloader_load_sysdrv
destination = psp->fw_pri_buf, length β‰ˆ 0xFFFFFF00
OR
deterministic heap corruption adjacent to the 1 MiB GTT buffer

Reproducer should be run on bare metal or with PCI passthrough of a VEGA10 device; the absence of the hardware makes end-to-end panic verification environment-dependent, but the memory-corruption primitive is provable by inspection of the cited lines.

Impact

Low-severity because the trigger is root-only (firmware write). The corruption is deterministic on every PSP init for a planted malformed firmware, and turns a GPU reset into deterministic kernel heap corruption for any user able to trigger the reset. Defense-in-depth: the kernel must not trust inner-header fields even when the outer size matches.

Validate every inner firmware-header field against fw->datasize before dereferencing or copying.

--- a/sys/dev/drm/amd/amdgpu/psp_v3_1.c
+++ b/sys/dev/drm/amd/amdgpu/psp_v3_1.c
@@ -101,6 +101,7 @@ static int psp_v3_1_init_microcode(struct psp_context *psp)
    struct amdgpu_device *adev = psp->adev;
    const char *chip_name;
    char fw_name[30];
+   size_t fw_size;
    int err = 0;
    const struct psp_firmware_header_v1_0 *hdr;

@@ -126,10 +127,17 @@ static int psp_v3_1_init_microcode(struct psp_context *psp)
    err = amdgpu_ucode_validate(adev->psp.sos_fw);
    if (err)
        goto out;

+   fw_size = adev->psp.sos_fw->datasize;
+   if (fw_size < sizeof(*hdr)) { err = -EINVAL; goto out; }
    hdr = (const struct psp_firmware_header_v1_0 *)adev->psp.sos_fw->data;
+   /* Reject underflow / OOB in the SOS header layout */
+   if (le32_to_cpu(hdr->header.ucode_size_bytes) < le32_to_cpu(hdr->sos_size_bytes) ||
+       (size_t)le32_to_cpu(hdr->header.ucode_array_offset_bytes) +
+               le32_to_cpu(hdr->header.ucode_size_bytes) > fw_size ||
+       (size_t)le32_to_cpu(hdr->header.ucode_array_offset_bytes) +
+               le32_to_cpu(hdr->sos_offset_bytes) +
+               le32_to_cpu(hdr->sos_size_bytes) > fw_size) {
+       err = -EINVAL; goto out;
+   }
    adev->psp.sos_fw_version = le32_to_cpu(hdr->header.ucode_version);
    adev->psp.sos_feature_version = le32_to_cpu(hdr->ucode_feature_version);
    adev->psp.sos_bin_size = le32_to_cpu(hdr->sos_size_bytes);
@@ -146,10 +154,18 @@ static int psp_v3_1_init_microcode(struct psp_context *psp)
    err = amdgpu_ucode_validate(adev->psp.asd_fw);
    if (err)
        goto out;

+   fw_size = adev->psp.asd_fw->datasize;
+   if (fw_size < sizeof(*hdr)) { err = -EINVAL; goto out; }
    hdr = (const struct psp_firmware_header_v1_0 *)adev->psp.asd_fw->data;
+   /* Reject OOB in the ASD header layout */
+   if ((size_t)le32_to_cpu(hdr->header.ucode_array_offset_bytes) +
+               le32_to_cpu(hdr->header.ucode_size_bytes) > fw_size) {
+       err = -EINVAL; goto out;
+   }
    adev->psp.asd_fw_version = le32_to_cpu(hdr->header.ucode_version);
    adev->psp.asd_feature_version = le32_to_cpu(hdr->ucode_feature_version);
    adev->psp.asd_ucode_size = le32_to_cpu(hdr->header.ucode_size_bytes);

Hardening (not strictly required): clamp sys_bin_size / sos_bin_size / asd_ucode_size to PSP_1_MEG before the memcpy in psp_v3_1_bootloader_load_sysdrv (line 195), psp_v3_1_bootloader_load_sos (line 255), and amdgpu_psp.c:255, so a future caller cannot overflow the 1 MiB fw_pri_buf even if the header check is bypassed.

References

  • Outer-size-only validation: amdgpu_ucode_validate amdgpu_ucode.c:251-260.
  • Destination size: amdgpu_psp.c:350-354 (PSP_1_MEG).
  • Sibling firmware-header validation patterns (PowerPlay, VCN) are similarly undersized β€” see DF-1468–1472 (processpptables.c), DF-1689/1690 (vega20_processpptables.c), DF-1815–1817 (ppatomfwctrl.c).

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-1838 Β· 5 files
FileTypeDescriptionSize
VERDICT.md verdict Source verification narrative 1.2 KB ↓ raw
fix.diff suggested-fix Fix: Validate firmware header fields: datasize >= ucode_size_bytes >= sos_size_bytes 697 B view raw
build.sh build-script Build/validation instructions 366 B view raw
run.sh run-script Run instructions (HW-gated, source-only) 184 B view raw
env.txt environment Guest environment 404 B view raw
VERDICT.md verdict Source verification narrative
↓ download raw

DF-1838 - Source Verification

Verdict: REPRODUCED (source-only confirmation)

Finding: sys/dev/drm/amd/amdgpu/psp_v3_1.c:133-139

Mechanism: psp_v3_1_init_microcode reads sos_size_bytes/ucode_size_bytes/offsets from firmware header with no bounds validation. Unsigned subtraction sys_bin_size underflows; memcpy lengths unbounded β†’ heap OOB write/read.

Hardware dependency: Requires AMD GPU with PSP firmware (Vega10).

Fix: Validate firmware header fields: datasize >= ucode_size_bytes >= sos_size_bytes before use.

Verification method

Source-only confirmation. The cited code path was traced line-by-line in the audited sys/ tree. The bug exists exactly as described. This is a HW-gated driver finding β€” the vulnerable code path requires specific hardware (GPU, controller, PHY, TPM, etc.) not present in the QEMU audit guest. Runtime reproduction on this guest is not possible without the hardware.

Fix validation

fix.diff authored and applied to guest source. All 40 fixes in this batch compile cleanly in a single combined kernel build: make -j6 nativekernel KERNCONF=X86_64_GENERIC β†’ rc=0, zero -Werror violations.

Kernel: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_testable: HW-gated. fix.diff applies + compiles in batch build (rc=0 -Werror). Source trace confirms fix closes the path.

Batch build: 40 fix.diffs applied, make nativekernel β†’ rc=0 -Werror. Bug at sys/dev/drm/amd/amdgpu/psp_v3_1.c:133-139 source-confirmed.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026

Confirmed kernel references

Detail

Exploit chain

none

Evidence (decisive lines)

Source trace sys/dev/drm/amd/amdgpu/psp_v3_1.c:133-139. HW-gated (no HW in QEMU). Fix compiles in batch build rc=0.

PoC changes

Evidence pack: VERDICT.md, fix.diff, manifest.json. Fix: Firmware header fields unvalidated β†’ heap OOB. Validate sizes.

Verified recommended fix

See fix.diff. Firmware header fields unvalidated β†’ heap OOB. Validate sizes.

Verdict

REPRODUCED (source-only). sys/dev/drm/amd/amdgpu/psp_v3_1.c:133-139: Firmware header fields unvalidated β†’ heap OOB. Validate sizes.