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/modulesfirmware, or any pathfirmware_get()can resolve on DragonFlyBSD βrequest_firmwareatsys/dev/drm/include/linux/firmware.h:39callsfirmware_get(name)). This normally requires root (kldload-equivalent privilege). - Privileges gained or impact: kernel heap corruption β
memcpywrites past the 1 MiBfw_pri_bufGTT 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 amdgpuon VEGA10/VEGA12; ability to write the firmware image (root) or trigger a GPU reset that recovers throughpsp_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.
Recommended fix
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_validateamdgpu_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)
PoC verification
Evidence pack
findings/poc/DF-1838 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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_testablenot_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.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- p
- s
- p
- _
- v
- 3
- _
- 1
- .
- c
- :
- 1
- 3
- 3
- -
- 1
- 3
- 9
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.
No comments yet.