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

gmc_v7_0_mc_load_microcode trusts MC firmware header offset/size fields without bounds-checking against fw->datasize (DF-1457 v7 twin)

  • File: sys/dev/drm/amd/amdgpu/gmc_v7_0.c
  • Lines: 189, 190, 192, 193, 194, 195, 196, 197, 198, 208, 209, 210, 211, 213, 214
  • 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

gmc_v7_0_mc_load_microcode() parses the loaded MC firmware blob's mc_firmware_header_v1_0 fields (io_debug_array_offset_bytes / io_debug_size_bytes / ucode_array_offset_bytes / ucode_size_bytes) and uses them directly as the base pointer and loop count for two read loops that feed 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.

A crafted amdgpufw_{bonaire,hawaii,topaz}_mc firmware whose size_bytes field is self-consistent with the file length but whose inner offset/size fields are inflated causes the loops to read past the firmware's kmalloc backing, reading adjacent kernel heap and writing those bytes into mmMC_SEQ_IO_DEBUG_DATA / mmMC_SEQ_SUP_PGM.

This is the gmc_v7_0 twin of DF-1457 (gmc_v8_0) in the recurring amdgpu_ucode_validate family (DF-1130/1164/1256/1316/1451/1457).

Root cause

At gmc_v7_0.c:189 the firmware data pointer is cast to const struct mc_firmware_header_v1_0 * with no check that fw->datasize >= sizeof(*hdr) (36 bytes).

At gmc_v7_0.c:193: regs_size = le32_to_cpu(hdr->io_debug_size_bytes) / (4 * 2) β€” u32/unsigned-int arithmetic, yields up to 0x1FFFFFFF iterations.

At gmc_v7_0.c:194-195: io_mc_regs = fw->data + le32_to_cpu(hdr->io_debug_array_offset_bytes) β€” u8* + u32, no clamp.

At gmc_v7_0.c:196: ucode_size = le32_to_cpu(hdr->header.ucode_size_bytes) / 4 β€” yields up to ~0x3FFFFFFF iterations.

At gmc_v7_0.c:197-198: fw_data = fw->data + le32_to_cpu(hdr->header.ucode_array_offset_bytes) β€” same, no clamp.

Then at gmc_v7_0.c:208-211 the io-reg loop calls le32_to_cpup(io_mc_regs++) = le32toh(*io_mc_regs) (defined sys/dev/drm/include/linux/kernel.h:146) β€” a direct 4-byte pointer dereference with no bounds guard.

At gmc_v7_0.c:213-214 the ucode loop does the same with fw_data++.

amdgpu_ucode_validate() at amdgpu_ucode.c:251-260 only checks fw->datasize == le32_to_cpu(hdr->size_bytes), so a firmware whose outer size_bytes matches the file length but whose inner ucode_array_offset_bytes/ucode_size_bytes (or io_debug_*) point past the buffer passes validation and proceeds to read out of bounds.

Concretely: io_debug_array_offset_bytes = datasize, io_debug_size_bytes = 16 β†’ regs_size = 2 β†’ 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.

Threat

Attacker position: any subject that can place or replace the firmware blob loaded under the name amdgpufw_bonaire_mc / amdgpufw_hawaii_mc / amdgpufw_topaz_mc (declared via MODULE_FIRMWARE at gmc_v7_0.c:54-56) 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 discrete CIK GPU (CHIP_BONAIRE / CHIP_HAWAII / CHIP_TOPAZ, i.e. !(adev->flags & AMD_IS_APU)) at driver attach via gmc_v7_0_hw_init (gmc_v7_0.c:1117-1123) β†’ gmc_v7_0_mc_load_microcode.

Impact:

  • (a) reliable kernel panic / DoS by setting ucode_size_bytes to a value large enough that the read loop walks into an unmapped page;
  • (b) small controlled OOB read of adjacent kernel heap whose contents are then written to GPU MMIO registers (mmMC_SEQ_IO_DEBUG_DATA / mmMC_SEQ_SUP_PGM) β€” not a direct kernel-to-userspace leak, hence C:N, but corrupts MC state.

Match to DF-1457: identical code shape, identical amdgpu_ucode_validate gap, identical threat model.

Exploit / PoC

Craft a malicious firmware file amdgpufw_bonaire_mc (a normal Bonaire MC firmware whose header size_bytes matches the file length, so amdgpu_ucode_validate passes, but whose common_firmware_header.ucode_array_offset_bytes and mc_firmware_header.io_debug_array_offset_bytes fields are set to point past the file end).

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

Trigger driver re-attach on a Bonaire-class GPU (kldload amdgpu / boot / re-bind the PCI device).

At gmc_v7_0.c:1118 gmc_v7_0_hw_init calls gmc_v7_0_mc_load_microcode, which at gmc_v7_0.c:213-214 enters the WREG32 loop reading via le32_to_cpup(fw_data++) from fw->data + inflated_offset; the dereference reads kernel heap past the allocation.

A PoC that demonstrates the DoS path is the simpler variant: set ucode_size_bytes = 0x40000000 (β†’ ucode_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-1538/):

  1. mkmaliciousfw.c β€” read a stock amdgpufw_bonaire_mc, patch the four header fields at their known offsets (common_firmware_header.ucode_array_offset_bytes at offset 0x14, mc_firmware_header.io_debug_size_bytes at offset 0x24, .io_debug_array_offset_bytes at 0x28, .ucode_size_bytes at 0x10 of the common header) so size_bytes still equals the file length but the inner offsets point past EOF, 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 gmc_v7_0_mc_load_microcode.

Reproducible on any DragonFlyBSD system with a CIK discrete GPU (Bonaire/Hawaii/Topaz) 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 each (offset, size) pair stays within fw->datasize before entering the read loops.

@@ -178,16 +178,32 @@
 static int gmc_v7_0_mc_load_microcode(struct amdgpu_device *adev)
 {
    const struct mc_firmware_header_v1_0 *hdr;
    const __le32 *fw_data = NULL;
    const __le32 *io_mc_regs = NULL;
+   u32 ucode_offset, ucode_size_bytes, io_offset, io_size_bytes;
    u32 running;
    int i, ucode_size, regs_size;

    if (!adev->gmc.fw)
        return -EINVAL;
+   if (adev->gmc.fw->datasize < sizeof(*hdr))
+       return -EINVAL;

    hdr = (const struct mc_firmware_header_v1_0 *)adev->gmc.fw->data;
    amdgpu_ucode_print_mc_hdr(&hdr->header);

    adev->gmc.fw_version = le32_to_cpu(hdr->header.ucode_version);
-   regs_size = le32_to_cpu(hdr->io_debug_size_bytes) / (4 * 2);
-   io_mc_regs = (const __le32 *)
-       (adev->gmc.fw->data + le32_to_cpu(hdr->io_debug_array_offset_bytes));
-   ucode_size = le32_to_cpu(hdr->header.ucode_size_bytes) / 4;
-   fw_data = (const __le32 *)
-       (adev->gmc.fw->data + le32_to_cpu(hdr->header.ucode_array_offset_bytes));
+   io_size_bytes = le32_to_cpu(hdr->io_debug_size_bytes);
+   io_offset    = le32_to_cpu(hdr->io_debug_array_offset_bytes);
+   ucode_size_bytes = le32_to_cpu(hdr->header.ucode_size_bytes);
+   ucode_offset     = le32_to_cpu(hdr->header.ucode_array_offset_bytes);
+
+   if (io_offset > adev->gmc.fw->datasize ||
+       io_size_bytes > adev->gmc.fw->datasize - io_offset)
+       return -EINVAL;
+   if (ucode_offset > adev->gmc.fw->datasize ||
+       ucode_size_bytes > adev->gmc.fw->datasize - ucode_offset)
+       return -EINVAL;
+
+   regs_size   = io_size_bytes    / (4 * 2);
+   ucode_size  = ucode_size_bytes / 4;
+   io_mc_regs  = (const __le32 *)(adev->gmc.fw->data + io_offset);
+   fw_data     = (const __le32 *)(adev->gmc.fw->data + ucode_offset);

    running = REG_GET_FIELD(RREG32(mmMC_SEQ_SUP_CNTL), MC_SEQ_SUP_CNTL, RUN);

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 OOB read and the under-sized-header read of hdr->size_bytes that amdgpu_ucode_validate itself performs (amdgpu_ucode.c:256).

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

  • 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.
  • DF-1130, DF-1164 (siblings): cik/gmc related firmware-loading OOB family.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1538 Β· 4 files
FileTypeDescriptionSize
fix.diff suggested-fix Fix for gmc_v7 MC firmware header OOB 308 B view raw
VERDICT.md verdict Source-only verification verdict 804 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-1538: gmc_v7 MC firmware header OOB

Verdict

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

Mechanism

Firmware header cast without datasize>=sizeof(*hdr) check -> OOB read of header fields.

Source reference: sys/dev/drm/amd/amdgpu/gmc_v7_0.c:189-195.

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/gmc_v7_0.c:189. Combined 41-fix kernel build rc=0 -Werror clean.

PoC changes

fix.diff authored; validated by combined kernel build.

Verified recommended fix

Add datasize>=sizeof(*hdr). Matches finding.

Verdict

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