amdgpu_ci_load_smc_ucode trusts unvalidated firmware header fields (dead limit, no blob/header bounds) -> OOB read of firmware buffer and unbounded SMC SRAM write
| Field | Value |
|---|---|
| ID | DF-2096 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:L/I:L/A:H |
| CWE | CWE-125 Out-of-bounds Read; CWE-20 Improper Input Validation |
| File | sys/dev/drm/amd/amdgpu/ci_smc.c |
| Lines | 208-244 |
| Area | drm/amdgpu |
| Confidence | likely |
| Discovered | 2026-07-25 |
| Reported | pending |
| Known CVE | none |
| CVE match | variant |
Summary
amdgpu_ci_load_smc_ucode() accepts a u32 limit parameter (the SMC SRAM
size, SMC_RAM_END = 0x40000) but never references it in the function
body. It dereferences fields of a firmware-supplied
smc_firmware_header_v1_0 without checking that the blob is large enough
to contain the header, and uses firmware-controlled
ucode_array_offset_bytes / ucode_size to compute a source pointer and
loop count without verifying they lie inside fw->datasize, nor that
ucode_start_addr + ucode_size lies inside limit. A crafted or corrupted
SMC firmware blob (which only has to pass amdgpu_ucode_validate, i.e.
size_bytes == datasize) causes an out-of-bounds read of the kernel
firmware buffer whose bytes are then written to arbitrary SMC SRAM
addresses, typically resulting in a kernel page fault / panic at GPU init.
Root cause
ci_smc.c:208 declares int amdgpu_ci_load_smc_ucode(struct amdgpu_device *adev, u32 limit)
but limit is dead β grep confirms the only occurrences of limit in
this function are the signature. Every sibling function
(copy_bytes_to_smc:62/72, read/write_smc_sram_dword:258/273) routes
through ci_set_smc_sram_address() which enforces (addr+3) > limit, but
load_smc_ucode does NOT. Specifically:
ci_smc.c:220castsadev->pm.fw->datatosmc_firmware_header_v1_0 *andci_smc.c:224readshdr->ucode_start_addr(at struct offset0x20) with no check thatfw->datasize >= sizeof(*hdr)(36 bytes); a self-consistent but undersized blob (datasize == size_bytes < 36) makes the header read itself OOB.ci_smc.c:225-227computesrc = fw->data + ucode_array_offset_bytesandci_smc.c:236loopswhile (ucode_size >= 4)readingsrc[0..3]each pass, with no check thatucode_array_offset_bytes + ucode_size <= fw->datasize.ci_smc.c:233writesucode_start_addressstraight intommSMC_IND_INDEX_0with no check againstlimit.
The only validation performed is ucode_size & 3 at ci_smc.c:229.
amdgpu_ucode_validate() (amdgpu_ucode.c:251-259) only verifies
fw->datasize == hdr->size_bytes, so it does not cover any of these.
Threat model & preconditions
- Attacker position: privileged β must be able to supply or corrupt the
SMC firmware file loaded by
request_firmware()atci_dpm.c:5819(path/lib/firmware/amdgpufw_<bonaire|hawaii>_smc). This normally requires root, or control of the medium backing that path (mounted attacker FS, corrupted flash, MITM on a firmware-fetch usermode helper, or a maliciously shipped driver bundle). - Privileges gained or impact: primary β local kernel DoS (page fault
on unmapped kernel address, or unbounded ~4 GB heap read overrun driven
by
ucode_sizeup to0xFFFFFFFC). Secondary β bytes read from neighbouring kernel heap memory are written into SMC SRAM; no path in this file returns those bytes to userspace, so confidentiality impact is limited to cross-object kernel heap exposure observable only via GPU behaviour, not a direct leak. No privilege escalation demonstrated. - Required config or capabilities: CIK (Bonaire/Hawaii) GPU; the amdgpu driver auto-loads on matching PCI hardware.
- Reachability: simply probing/initialising the CIK GPU (module load,
boot, or hotplug re-probe via
ci_dpm_init_microcode β ci_dpm_hw_init β amdgpu_ci_load_smc_ucode) triggers the OOB read.
This is a firmware-trust-boundary bug; root or firmware-path control is the precondition, hence Low severity.
Proof of concept
Build a minimal malicious SMC firmware blob that is self-consistent (so
amdgpu_ucode_validate passes) but points its payload past its own end:
common_firmware_header(32 bytes): setsize_bytes = total_file_size(==datasize, sovalidate()returns 0),header_size_bytes = 0x24,header_version_major/minor = 1/0,ip_version_major/minornon-zero,ucode_version = anything,ucode_size_bytes = 0x1000(4-byte aligned, passes the& 3check),ucode_array_offset_bytes = 0xFFFFF000(far beyond the file),crc32 = anything.smc_firmware_header_v1_0extension (4 bytes):ucode_start_addr = 0x0.- Append a few bytes of padding so total size > 36 and
== size_bytes.
Drop the result as /lib/firmware/amdgpufw_bonaire_smc (root), then
kldload amdgpu (or trigger device probe).
amdgpu_ci_load_smc_ucode computes src = fw->data + 0xFFFFF000 (a wild
kernel pointer), enters the copy loop, and the first src[0..3]
dereference faults: expect a kernel panic / page-fault in ci_smc.c:238.
Variants: set ucode_array_offset_bytes just past the real payload
(datasize) so the dereference stays mapped and silently reads adjacent
slab objects into SMC SRAM (heap exposure, no panic).
Impact
- Default config: only triggered by a tampered firmware file.
- Blast radius: local kernel DoS via panic at GPU init; narrow cross-object heap exposure if the OOB read stays mapped.
Recommended fix
Validate the firmware blob before trusting any header field: require the
header to fit, require the payload window to lie inside fw->datasize,
and finally honour the existing limit parameter for the SMC SRAM
destination.
--- a/sys/dev/drm/amd/amdgpu/ci_smc.c
+++ b/sys/dev/drm/amd/amdgpu/ci_smc.c
@@ -211,6 +211,7 @@ int amdgpu_ci_load_smc_ucode(struct amdgpu_device *adev, u32 limit)
u32 ucode_start_address;
u32 ucode_size;
const u8 *src;
+ u32 array_offset, fw_size;
u32 data;
if (!adev->pm.fw)
return -EINVAL;
+ fw_size = adev->pm.fw->datasize;
+ if (fw_size < sizeof(struct smc_firmware_header_v1_0))
+ return -EINVAL;
hdr = (const struct smc_firmware_header_v1_0 *)adev->pm.fw->data;
amdgpu_ucode_print_smc_hdr(&hdr->header);
@@ -224,11 +229,23 @@ int amdgpu_ci_load_smc_ucode(struct amdgpu_device *adev, u32 limit)
adev->pm.fw_version = le32_to_cpu(hdr->header.ucode_version);
ucode_start_address = le32_to_cpu(hdr->ucode_start_addr);
ucode_size = le32_to_cpu(hdr->header.ucode_size_bytes);
- src = (const u8 *)
- (adev->pm.fw->data + le32_to_cpu(hdr->header.ucode_array_offset_bytes));
+ array_offset = le32_to_cpu(hdr->header.ucode_array_offset_bytes);
+ src = (const u8 *)(adev->pm.fw->data + array_offset);
if (ucode_size & 3)
return -EINVAL;
+ /* payload window must lie entirely inside the firmware blob */
+ if (array_offset > fw_size || ucode_size > fw_size - array_offset)
+ return -EINVAL;
+ /* SMC SRAM destination must be aligned and inside the advertised limit */
+ if (ucode_start_address & 3)
+ return -EINVAL;
+ if ((u64)ucode_start_address + ucode_size > (u64)limit)
+ return -EINVAL;
spin_lock_irqsave(&adev->smc_idx_lock, flags);
The (u64) casts make the end-bound comparison overflow-proof (the same
class of u32+u32 wrap that is otherwise guarded only by
ci_set_smc_sram_address in the other helpers). This makes the
previously-dead limit parameter actually enforce the SMC SRAM bound it
was always meant to.
References
sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:251-259βamdgpu_ucode_validateonly checkssize_bytes == datasize.- DF-2015 β same class of "trusts unvalidated firmware blob fields" in
vce_v1_0_load_fw. sys/dev/drm/amd/amdgpu/ci_dpm.c:5819βrequest_firmwarecaller.
Timeline
- 2026-07-25 Discovered during automated audit.
- 2026-07-25 Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2096 Β· 2 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | git-apply-able fix | 581 B | view raw |
| VERDICT.md | verdict | source-trace confirmation | 680 B | β raw |
DF-2096 β ci_load_smc_ucode trusts unvalidated firmware header fields
Verdict
REPRODUCED (source-only confirmation). Bug confirmed by source tracing.
Mechanism
amdgpu_ci_load_smc_ucode (ci_smc.c:208): limit parameter never used. Casts fw->data to smc_firmware_header_v1_0* and trusts ucode_array_offset_bytes and ucode_size_bytes without validating against fw->datasize. Malicious firmware blob -> OOB read.
Fix
Add bounds checks: ucode_array_offset+ucode_size > fw->datasize -> return -EINVAL; ucode_start_address+ucode_size > limit -> return -EINVAL.
Batch-build status
Applied with all 24 other fixes; kernel + modules compiled rc=0, 0 errors, -Werror.
Fix verification
fixedAdded bounds checks; batch build rc=0.
Added bounds checks; batch build rc=0.
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
ci_load_smc_ucode limit unused; firmware fields not validated -> OOB read.
Verified recommended fix
ci_load_smc_ucode limit unused; firmware fields not validated -> OOB read.
Verdict
ci_load_smc_ucode limit unused; firmware fields not validated -> OOB read.
No comments yet.