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

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:220 casts adev->pm.fw->data to smc_firmware_header_v1_0 * and ci_smc.c:224 reads hdr->ucode_start_addr (at struct offset 0x20) with no check that fw->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-227 compute src = fw->data + ucode_array_offset_bytes and ci_smc.c:236 loops while (ucode_size >= 4) reading src[0..3] each pass, with no check that ucode_array_offset_bytes + ucode_size <= fw->datasize.
  • ci_smc.c:233 writes ucode_start_address straight into mmSMC_IND_INDEX_0 with no check against limit.

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() at ci_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_size up to 0xFFFFFFFC). 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): set size_bytes = total_file_size (== datasize, so validate() returns 0), header_size_bytes = 0x24, header_version_major/minor = 1/0, ip_version_major/minor non-zero, ucode_version = anything, ucode_size_bytes = 0x1000 (4-byte aligned, passes the & 3 check), ucode_array_offset_bytes = 0xFFFFF000 (far beyond the file), crc32 = anything.
  • smc_firmware_header_v1_0 extension (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.

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

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-2096 Β· 2 files
FileTypeDescriptionSize
fix.diff suggested-fix git-apply-able fix 581 B view raw
VERDICT.md verdict source-trace confirmation 680 B ↓ raw
VERDICT.md verdict source-trace confirmation
↓ download 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Added bounds checks; batch build rc=0.

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