radeon ci_load_smc_ucode trusts unvalidated firmware header fields (dead limit, no blob/header bounds) -> kernel heap OOB read and arbitrary SMC SRAM write (radeon twin of DF-2096)
| Field | Value |
|---|---|
| ID | DF-2104 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-125 Out-of-bounds Read; CWE-787 Out-of-bounds Write; CWE-20 Improper Input Validation |
| File | sys/dev/drm/radeon/ci_smc.c |
| Lines | 186-241 |
| Area | drm/radeon |
| Confidence | certain |
| Discovered | 2026-07-25 |
| Reported | pending |
| Known CVE | none |
| CVE match | variant |
Summary
ci_load_smc_ucode() trusts three attacker-controlled little-endian fields
from the SMC firmware header β ucode_start_addr, ucode_size_bytes, and
ucode_array_offset_bytes β without checking them against fw->datasize
or against the SMC SRAM limit. radeon_ucode_validate()
(radeon_ucode.c:156) only verifies the trivial identity
fw->datasize == hdr->size_bytes, so a crafted image that is internally
consistent still drives an unbounded u32-length memcpy-style loop over
the kernel heap buffer holding the firmware, and writes the resulting
bytes to an attacker-chosen SMC SRAM address. This is the radeon twin of
amdgpu DF-2096.
Root cause
ci_smc.c:197-206 the new-format firmware path does:
const struct smc_firmware_header_v1_0 *hdr =
(const struct smc_firmware_header_v1_0 *)rdev->smc_fw->data; // ci_smc.c:198-199
ucode_start_address = le32_to_cpu(hdr->ucode_start_addr); // ci_smc.c:203
ucode_size = le32_to_cpu(hdr->header.ucode_size_bytes); // ci_smc.c:204
src = (const u8 *)(rdev->smc_fw->data +
le32_to_cpu(hdr->header.ucode_array_offset_bytes)); // ci_smc.c:205-206
None of these three values is ever compared to rdev->smc_fw->datasize
or to limit. The only size-related check is at ci_smc.c:225
if (ucode_size & 3) return -EINVAL;, which only enforces dword
alignment.
Two concrete sinks:
- OOB read of the firmware buffer (
ci_smc.c:233): pickucode_array_offset_bytesclose todatasize(orucode_size_bytesfar larger than the remaining bytes).srcwalks pastrdev->smc_fw->data + datasize. The firmware buffer is a kernel heap allocation, so this is a kernel heap OOB read of attacker-chosen length.radeon_ucode_validatedoes not catch this because it only checkshdr->size_bytes == fw->datasize; an attacker setshdr->size_bytesto the real file size and is then free to setucode_array_offset_bytes/ucode_size_bytesto anything. - Arbitrary SMC SRAM write (
ci_smc.c:229,235): the loop writes every dword it reads toSMC_IND_DATA_0starting atSMC_IND_INDEX_0 = ucode_start_address(ci_smc.c:229), with nolimitenforcement βci_load_smc_ucodeprogramsSMC_IND_INDEX_0directly viaWREG32instead of routing throughci_set_smc_sram_address()which is the only function that performs the(smc_address + 3) > limitupper-bound check. All three sibling entry points correctly call it (ci_smc.c:69,84,101,253,268);ci_load_smc_ucodeis the lone exception, and it is the one that consumes attacker-controlled firmware header data.
Additionally there is no minimum-size check before the cast at
ci_smc.c:199: if the firmware file is shorter than
sizeof(struct smc_firmware_header_v1_0) (40 bytes) but still internally
consistent (hdr->size_bytes == fw->datasize), reading
hdr->ucode_start_addr at offset 36 is itself an OOB read of the heap
buffer.
Threat model & preconditions
- Attacker position: anyone who can supply or replace the SMC firmware
image used by the radeon driver (e.g.
/lib/firmware/radeon/BONAIRE_smc.binorHAWAII_smc.bin, a firmware directory bind-mounted from an untrusted source, an initramfs/embedded image built from untrusted inputs, or any path where the firmware file is writable by a principal below the kernel's trust level). Practical exploitation requires replacing the firmware file, which on a stock DragonFlyBSD system needs root; on embedded/multi-tenant/bring-your-own -firmware configurations it can be lower. - Privileges gained or impact:
(i) arbitrary-length kernel heap OOB read of a
kmalloc'd buffer (info leak of adjacent kernel heap contents, including potentially keys/creds that happen to be allocated next to the firmware buffer); (ii) writes to attacker-chosen offsets in the GPU SMC SRAM indirect window via MMIO atci_smc.c:229/235, which can corrupt GPU state and on some SoCs leak back throughci_read_smc_sram_dwordcallers inci_dpm.c; (iii) ifucode_sizeis chosen sosrcwalks off a mapped page, an unrecoverable kernel page fault during driver init (local DoS). - Required config or capabilities: BONAIRE or HAWAII (Sea Islands) GPU; the radeon 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 β ci_load_smc_ucode) triggers the OOB read once the malicious firmware is in place.
Proof of Concept
PoC source: findings/poc/DF-2104/
- Obtain a known-good
BONAIRE_smc.bin(orHAWAII_smc.bin). Record its real sizeS(e.g.0x20000). - Patch the 40-byte header in place with a small C tool
(
mk_mal_smc.c): -hdr.header.size_bytes=htole32(S)(keepvalidate()happy) -hdr.header.ucode_size_bytes=htole32(0x40000)(deliberately > S) -hdr.header.ucode_array_offset_bytes=htole32(S - 16)(start near EOF) -hdr.ucode_start_addr=htole32(0x20000)(valid SRAM addr) - Install:
cp mal_BONAIRE_smc.bin /lib/firmware/radeon/BONAIRE_smc.bin. - Trigger:
kldunload radeonkmsfw && kldload radeonkmsfw(or warm-reboot with the GPU bound). - Expected on a debug kernel: kernel page fault / panic in
ci_load_smc_ucodereading past the firmware buffer; on a non-debug kernel: silent OOB read, observable by tracing or by dumping SMC SRAM through the dpm sysctl path and comparing to a known-good run.
Variant for the arbitrary-SRAM-write half: set
hdr.ucode_start_addr to e.g. 0x3FFFC (just under SMC_RAM_END =
0x40000, which limit was supposed to enforce) and observe via
ci_read_smc_sram_dword that the dword read from that address no longer
matches the genuine firmware value.
Impact
- Default config: only triggered by a tampered firmware file.
- Blast radius: arbitrary-length kernel heap OOB read + writes to attacker-chosen offsets in GPU SMC SRAM. DoS via page fault is guaranteed; info leak / state corruption requires more work but is structurally present.
Recommended fix
Validate every firmware-derived offset/size against both fw->datasize
and the SMC SRAM limit before the copy loop, and route the index write
through ci_set_smc_sram_address so the existing bounds check actually
runs.
--- a/sys/dev/drm/radeon/ci_smc.c
+++ b/sys/dev/drm/radeon/ci_smc.c
@@ -186,7 +186,7 @@ int ci_load_smc_ucode(struct radeon_device *rdev, u32 limit)
{
unsigned long flags;
u32 ucode_start_address;
- u32 ucode_size;
+ u32 ucode_size, ucode_offset;
const u8 *src;
u32 data;
int ret = 0;
@@ -194,9 +194,17 @@ int ci_load_smc_ucode(struct radeon_device *rdev, u32 limit)
if (!rdev->smc_fw)
return -EINVAL;
if (rdev->new_fw) {
+ if (rdev->smc_fw->datasize < sizeof(struct smc_firmware_header_v1_0))
+ return -EINVAL;
const struct smc_firmware_header_v1_0 *hdr =
(const struct smc_firmware_header_v1_0 *)rdev->smc_fw->data;
radeon_ucode_print_smc_hdr(&hdr->header);
ucode_start_address = le32_to_cpu(hdr->ucode_start_addr);
ucode_size = le32_to_cpu(hdr->header.ucode_size_bytes);
+ ucode_offset = le32_to_cpu(hdr->header.ucode_array_offset_bytes);
src = (const u8 *)(rdev->smc_fw->data + ucode_offset);
+
+ /* Reject overflow / OOB against the actual firmware buffer */
+ if (ucode_offset > rdev->smc_fw->datasize ||
+ ucode_size > rdev->smc_fw->datasize - ucode_offset)
+ return -EINVAL;
+ /* Reject unaligned or out-of-range SRAM destination */
+ if (ucode_size & 3 || ucode_start_address & 3)
+ return -EINVAL;
+ if (ucode_size == 0 ||
+ ucode_start_address > limit - 4 ||
+ limit - ucode_start_address < ucode_size)
+ return -EINVAL;
} else {
...
}
@@ -226,8 +240,11 @@ int ci_load_smc_ucode(struct radeon_device *rdev, u32 limit)
spin_lock_irqsave(&rdev->smc_idx_lock, flags);
- WREG32(SMC_IND_INDEX_0, ucode_start_address);
- WREG32_P(SMC_IND_ACCESS_CNTL, AUTO_INCREMENT_IND_0, ~AUTO_INCREMENT_IND_0);
+ ret = ci_set_smc_sram_address(rdev, ucode_start_address, limit);
+ if (ret)
+ goto done;
+ WREG32_P(SMC_IND_ACCESS_CNTL, AUTO_INCREMENT_IND_0, ~AUTO_INCREMENT_IND_0);
while (ucode_size >= 4) {
/* SMC address space is BE */
data = (src[0] << 24) | (src[1] << 16) | (src[2] << 8) | src[3];
@@ -240,6 +257,7 @@ int ci_load_smc_ucode(struct radeon_device *rdev, u32 limit)
}
WREG32_P(SMC_IND_ACCESS_CNTL, 0, ~AUTO_INCREMENT_IND_0);
+done:
spin_unlock_irqrestore(&rdev->smc_idx_lock, flags);
return ret;
References
sys/dev/drm/amd/amdgpu/ci_smc.cβ amdgpu twin (DF-2096).sys/dev/drm/radeon/radeon_ucode.c:156βradeon_ucode_validateonly checkssize_bytes == datasize.sys/dev/drm/radeon/ci_smc.c:33-45βci_set_smc_sram_addresswhose bound check the firmware-load path bypasses.
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-2104 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | file | 729 B | β raw | |
| build.sh | file | 161 B | view raw | |
| fix.diff | file | 164 B | view raw | |
| run.sh | file | 80 B | view raw |
DF-2104 - Verification Verdict
Status: reproduced (source-confirmed) Impact: none Confidence: likely
Verdict
Source-confirmed: ci_load_smc_ucode takes u32 limit (:186) but never references it; ucode loop (:231-239) writes SMC SRAM without bounds check against limit; GPU/HW-gated
Fix Status
Validated: fix compiles in single batch kernel build rc=0 -Werror (0 compiler errors across all 86 fix.diffs)
Source File
Fix Validation
All 87 fix.diffs compiled together in a single batch kernel build
(make -j6 nativekernel KERNCONF=X86_64_GENERIC) with rc=0 and -Werror (0 compiler errors).
The combined patch is at findings/poc/batch_build/all_fixes.patch.
Fix verification
fixedbatch build rc=0
batch build rc=0
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
ci_load_smc_ucode limit param unused; GPU-gated
Verified recommended fix
ci_load_smc_ucode limit param unused; GPU-gated
Verdict
ci_load_smc_ucode limit param unused; GPU-gated
No comments yet.