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

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): pick ucode_array_offset_bytes close to datasize (or ucode_size_bytes far larger than the remaining bytes). src walks past rdev->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_validate does not catch this because it only checks hdr->size_bytes == fw->datasize; an attacker sets hdr->size_bytes to the real file size and is then free to set ucode_array_offset_bytes/ucode_size_bytes to anything.
  • Arbitrary SMC SRAM write (ci_smc.c:229,235): the loop writes every dword it reads to SMC_IND_DATA_0 starting at SMC_IND_INDEX_0 = ucode_start_address (ci_smc.c:229), with no limit enforcement β€” ci_load_smc_ucode programs SMC_IND_INDEX_0 directly via WREG32 instead of routing through ci_set_smc_sram_address() which is the only function that performs the (smc_address + 3) > limit upper-bound check. All three sibling entry points correctly call it (ci_smc.c:69,84,101,253,268); ci_load_smc_ucode is 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.bin or HAWAII_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 at ci_smc.c:229/235, which can corrupt GPU state and on some SoCs leak back through ci_read_smc_sram_dword callers in ci_dpm.c; (iii) if ucode_size is chosen so src walks 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/

  1. Obtain a known-good BONAIRE_smc.bin (or HAWAII_smc.bin). Record its real size S (e.g. 0x20000).
  2. Patch the 40-byte header in place with a small C tool (mk_mal_smc.c): - hdr.header.size_bytes = htole32(S) (keep validate() 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)
  3. Install: cp mal_BONAIRE_smc.bin /lib/firmware/radeon/BONAIRE_smc.bin.
  4. Trigger: kldunload radeonkmsfw && kldload radeonkmsfw (or warm-reboot with the GPU bound).
  5. Expected on a debug kernel: kernel page fault / panic in ci_load_smc_ucode reading 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.

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

Timeline

  • 2026-07-25 Discovered during automated audit.
  • 2026-07-25 Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2104 Β· 4 files
FileTypeDescriptionSize
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
VERDICT.md file
↓ download 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

sys/dev/drm/radeon/ci_smc.c

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

fixed
baseline reproduced→ patch + rebuild →patched clean

batch build rc=0

batch build rc=0
↓ fix.diffcombined 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