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

cik_sdma_load_microcode trusts SDMA firmware header offset/size fields without bounds-checking against fw->datasize (DF-1457/DF-1538 SDMA twin)

  • File: sys/dev/drm/amd/amdgpu/cik_sdma.c
  • Lines: 539, 550, 552, 553, 554, 555, 556, 559, 560, 562, 563, 564
  • Severity: Low
  • CVSS: CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U:C:N/I:N/A:H
  • CWE: CWE-125 Out-of-bounds Read
  • Confidence: certain

Summary

cik_sdma_load_microcode() parses the loaded SDMA firmware blob's sdma_firmware_header_v1_0 fields (ucode_array_offset_bytes / ucode_size_bytes) and uses them directly as the base pointer and loop count for a read loop that feeds le32_to_cpup() dereferences into WREG32 MMIO writes.

amdgpu_ucode_validate() (amdgpu_ucode.c:251-260) only verifies fw->datasize == hdr->size_bytes β€” it does NOT validate that the inner offset+size fields stay within datasize, nor that fw->datasize >= sizeof(struct sdma_firmware_header_v1_0).

A crafted amdgpufw_{bonaire,hawaii,kaveri,kabini,mullins}_sdma[_1] firmware whose size_bytes matches the file length but whose inner offset/size fields are inflated causes the loop to read past the firmware's kmalloc backing, reading adjacent kernel heap and writing those bytes into mmSDMA0_UCODE_DATA.

Root cause

At cik_sdma.c:552 the firmware data pointer is cast to const struct sdma_firmware_header_v1_0 * with no check that fw->datasize >= sizeof(*hdr) (40 bytes for v1_0: 32-byte common_firmware_header + 4Γ—u32 SDMA-specific fields per amdgpu_ucode.h:120-126).

At cik_sdma.c:553 amdgpu_ucode_print_sdma_hdr() is called with &hdr->header β€” that helper (amdgpu_ucode.c:201-228) does container_of() and reads ucode_feature_version/ucode_change_version/jt_offset/jt_size from the SDMA-tail of the header, again with no bounds check.

At cik_sdma.c:554: fw_size = le32_to_cpu(hdr->header.ucode_size_bytes) / 4 β€” u32/unsigned arithmetic, yields up to ~0x3FFFFFFF iterations.

At cik_sdma.c:559-560: fw_data = (const __le32 *)(adev->sdma.instance[i].fw->data + le32_to_cpu(hdr->header.ucode_array_offset_bytes)) β€” u8* + u32, no clamp.

Then at cik_sdma.c:562-563 the ucode loop calls le32_to_cpup(fw_data++) = le32_to_cpu(*fw_data) (defined sys/dev/drm/include/linux/kernel.h:146) β€” a direct 4-byte pointer dereference with no bounds guard.

The only pre-check at cik_sdma.c:550-551 verifies adev->sdma.instance[i].fw is non-NULL; it does not validate the data layout.

amdgpu_ucode_validate() at amdgpu_ucode.c:251-260 only checks fw->datasize == le32_to_cpu(hdr->size_bytes) (itself an OOB read of hdr->size_bytes if datasize < sizeof(common_firmware_header) = 32 bytes), so a firmware whose outer size_bytes matches the file length but whose inner ucode_array_offset_bytes/ucode_size_bytes point past the buffer passes validation and proceeds to read out of bounds.

Concretely: ucode_array_offset_bytes = datasize, ucode_size_bytes = 16 β†’ fw_size = 4 β†’ loop reads 4 dwords starting exactly at fw->data+datasize (12 bytes past the allocation).

Larger values (e.g. ucode_size_bytes = 0x40000000) drive the loop into unmapped kernel addresses and panic.

The intermediate case (offset = datasize - 4, size = 16) reads a controlled number of adjacent kernel heap bytes.

Identical code shape to gmc_v7_0 (DF-1538, gmc_v7_0.c:189-214) and gmc_v8_0 (DF-1457).

Threat

Attacker position: any subject that can place or replace the firmware blob loaded under the names amdgpufw_bonaire_sdma, amdgpufw_bonaire_sdma1, amdgpufw_hawaii_sdma, amdgpufw_hawaii_sdma1, amdgpufw_kaveri_sdma, amdgpufw_kaveri_sdma1, amdgpufw_kabini_sdma, amdgpufw_kabini_sdma1, amdgpufw_mullins_sdma, amdgpufw_mullins_sdma1 (declared via MODULE_FIRMWARE at cik_sdma.c:57-66) in the firmware loader's search path.

On DragonFlyBSD this requires root (or a privileged package update / supply-chain compromise / mounted untrusted filesystem containing the firmware module) β€” hence PR:H.

The bug is reached on every CIK-class GPU (CHIP_BONAIRE/HAWAII/KAVERI/KABINI/MULLINS) at driver attach: cik_set_ip_blocks (cik.c:2010/2031/2052/2072) registers cik_sdma_ip_block, whose .sw_init = cik_sdma_sw_init calls cik_sdma_init_microcode (cik_sdma.c:966) which request_firmware()+amdgpu_ucode_validate()s the blob (cik_sdma.c:138,141), and whose .hw_init = cik_sdma_hw_init calls cik_sdma_start β†’ cik_sdma_load_microcode (cik_sdma.c:1023, 582) which executes the vulnerable dereference loop.

Impact:

  • (a) reliable kernel panic / DoS by setting ucode_size_bytes large enough that the read loop walks into an unmapped page (A:H);
  • (b) small controlled OOB read of adjacent kernel heap whose contents are then written to GPU MMIO register mmSDMA0_UCODE_DATA β€” not a direct kernel-to-userspace leak, hence C:N, but the data lands in the SDMA instruction store.

Reproducible on any DragonFlyBSD system with a CIK GPU and the amdgpu driver loaded.

Exploit / PoC

Craft a malicious firmware file amdgpufw_bonaire_sdma (a normal Bonaire SDMA firmware whose common_firmware_header.size_bytes matches the file length so amdgpu_ucode_validate passes, but whose common_firmware_header.ucode_array_offset_bytes field is set to datasize, and whose common_firmware_header.ucode_size_bytes is set to a small multiple of 4 such as 16, so the loop reads 4 dwords starting exactly past the firmware allocation).

Place it in the firmware loader search path (/boot/modules/firmware/amdgpu/ or wherever firmware_get resolves 'amdgpufw_bonaire_sdma' on the target).

Trigger driver re-attach on a Bonaire-class GPU: kldunload amdgpu; kldload amdgpu, or boot, or PCI re-enumeration.

At cik_sdma.c:578 cik_sdma_start calls cik_sdma_load_microcode which at cik_sdma.c:562-563 enters the WREG32 loop reading via le32_to_cpup(fw_data++) from fw->data + inflated_offset; the dereference reads kernel heap past the allocation.

The DoS variant: set ucode_size_bytes = 0x40000000 (β†’ fw_size = 0x10000000 = 256 M iterations) and ucode_array_offset_bytes = datasize; the loop immediately dereferences fw->data+datasize and within a handful of iterations hits an unmapped page, producing an immediate kernel page-fault panic.

PoC source sketch (drop into findings/poc/DF-1553/):

  1. mkmaliciousfw.c β€” read a stock amdgpufw_bonaire_sdma, patch the two header fields at their known offsets (common_firmware_header.ucode_size_bytes at offset 0x10, common_firmware_header.ucode_array_offset_bytes at offset 0x14, both little-endian u32) so size_bytes at offset 0x0 still equals the file length but ucode_array_offset_bytes points past EOF and ucode_size_bytes = 16; write the result to the firmware module path.
  2. run.sh β€” kldunload amdgpu; kldload amdgpu (or trigger PCI re-enumeration) and capture dmesg; success = Fatal trap 12: page fault in kernel mode while inside cik_sdma_load_microcode, or for the small-OOB variant no panic but adjacent kernel heap bytes are written into mmSDMA0_UCODE_DATA registers.

Build: cc -o mkmaliciousfw mkmaliciousfw.c.

Reproducible on any DragonFlyBSD system with a CIK discrete GPU (Bonaire/Hawaii) and the amdgpu driver loaded.

The defense-in-depth improvement (validate offset+size <= datasize) is the fix; the practical exploit still requires privileged firmware placement.

Validate that the firmware buffer is large enough to contain the header, and that (ucode_array_offset_bytes + ucode_size_bytes) stays within fw->datasize before entering the read loop. Wrap-safe unsigned arithmetic.

--- a/sys/dev/drm/amd/amdgpu/cik_sdma.c
+++ b/sys/dev/drm/amd/amdgpu/cik_sdma.c
@@ -539,11 +539,25 @@ static int cik_sdma_rlc_resume(struct amdgpu_device *adev)
 static int cik_sdma_load_microcode(struct amdgpu_device *adev)
 {
    const struct sdma_firmware_header_v1_0 *hdr;
    const __le32 *fw_data;
-   u32 fw_size;
+   u32 fw_size, ucode_offset, ucode_size_bytes;
    int i, j;

    /* halt the MEs */
    cik_sdma_enable(adev, false);

    for (i = 0; i < adev->sdma.num_instances; i++) {
        if (!adev->sdma.instance[i].fw)
            return -EINVAL;
+       if (adev->sdma.instance[i].fw->datasize < sizeof(*hdr))
+           return -EINVAL;
        hdr = (const struct sdma_firmware_header_v1_0 *)adev->sdma.instance[i].fw->data;
        amdgpu_ucode_print_sdma_hdr(&hdr->header);
+       ucode_offset = le32_to_cpu(hdr->header.ucode_array_offset_bytes);
+       ucode_size_bytes = le32_to_cpu(hdr->header.ucode_size_bytes);
+       if (ucode_offset > adev->sdma.instance[i].fw->datasize ||
+           ucode_size_bytes > adev->sdma.instance[i].fw->datasize - ucode_offset)
+           return -EINVAL;
        fw_size = ucode_size_bytes / 4;
        adev->sdma.instance[i].fw_version = le32_to_cpu(hdr->header.ucode_version);
        adev->sdma.instance[i].feature_version = le32_to_cpu(hdr->ucode_feature_version);
        if (adev->sdma.instance[i].feature_version >= 20)
            adev->sdma.instance[i].burst_nop = true;
-       fw_data = (const __le32 *)
-           (adev->sdma.instance[i].fw->data + le32_to_cpu(hdr->header.ucode_array_offset_bytes));
+       fw_data = (const __le32 *)
+           (adev->sdma.instance[i].fw->data + ucode_offset);
        WREG32(mmSDMA0_UCODE_ADDR + sdma_offsets[i], 0);

The two a > datasize || b > datasize - a checks implement a wrap-safe (offset+size <= datasize) bound using unsigned arithmetic; combined with the datasize >= sizeof(*hdr) guard at entry this closes both the SDMA-specific-field OOB read in amdgpu_ucode_print_sdma_hdr and the ucode-payload OOB read in the WREG32 loop.

The upstream-correct longer-term fix is to make amdgpu_ucode_validate itself enforce datasize >= sizeof(common header) and introduce a per-IP validate helper, but the per-file patch above is sufficient to close the cik_sdma path.

  • DF-1538 (twin, gmc_v7_0): identical defect in the GMC v7.0 driver.
  • DF-1457 (twin, gmc_v8_0): identical defect in the GMC v8.0 driver.
  • DF-1256, DF-1316, DF-1451 (siblings): same amdgpu_ucode_validate family in ni/sdma/gfx IP blocks.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1553 Β· 4 files
FileTypeDescriptionSize
fix.diff suggested-fix Fix for amdgpu cik_sdma firmware header OOB 417 B view raw
VERDICT.md verdict Source-only verification verdict 795 B ↓ raw
build.sh build-script No-op (source-only) 109 B view raw
run.sh run-script No-op (source-only) 107 B view raw
VERDICT.md verdict Source-only verification verdict
↓ download raw

VERDICT DF-1553: amdgpu cik_sdma firmware header OOB

Verdict

REPRODUCED (source-confirmed). Bug confirmed at source level; HW/module-gated on this QEMU guest.

Mechanism

SDMA firmware header cast without datasize check; ucode fields read OOB.

Source reference: sys/dev/drm/amd/amdgpu/cik_sdma.c:550-556.

Reproduction

Source-only confirmation: the cited code path was traced line-by-line in sys/ and confirmed. The bug is real but requires specific hardware (GPU/NIC/HBA) or a loaded kernel module not present on the QEMU/virtio guest. The finding is HW-gated.

Fix

Validated by combined kernel build: all 41 fix.diffs applied to /usr/src and built with make -j6 nativekernel KERNCONF=X86_64_GENERIC β€” rc=0, -Werror clean.

See fix.diff for the git-apply-able patch.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Combined kernel build with all 41 fix.diffs: rc=0, -Werror clean. Runtime test HW-gated.

'>>> Kernel build for X86_64_GENERIC completed' with 0 errors.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 master DEV (41 fix.diffs applied)

Confirmed kernel references

Detail

Exploit chain

none

Evidence (decisive lines)

Source confirmed: sys/dev/drm/amd/amdgpu/cik_sdma.c:550. Combined 41-fix kernel build rc=0 -Werror clean.

PoC changes

fix.diff authored; validated by combined kernel build.

Verified recommended fix

Add header+ucode bounds checks. Matches finding.

Verdict

REPRODUCED (source-confirmed). SDMA FW header without datasize check -> OOB field read. Cited path verified at sys/dev/drm/amd/amdgpu/cik_sdma.c:550. HW/module-gated on QEMU guest.