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

CP/MEC/RLC firmware loaders OOB read via untrusted ucode_size_bytes

Summary

gfx_v7_0_cp_gfx_load_microcode (:2447-2473 PFP/CE/ME), gfx_v7_0_cp_compute_load_microcode (:2709-2737 MEC1/MEC2), gfx_v7_0_rlc_resume (:3572-3577 RLC): fw_size=ucode_size_bytes/4 from firmware header; loop WREG32(reg, le32_to_cpup(fw_data++)) for i<fw_size without validating ucode_array_offset+ucode_size<=fw->datasize. Crafted firmware with large ucode_size_bytes reads past blob into adjacent kernel heap, writes to GPU microcode RAM. amdgpu_ucode_validate only checks datasize==size_bytes. Same class as DF-1119/1130/1133. Fix: validate in amdgpu_ucode_validate.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1164 Β· 9 files
FileTypeDescriptionSize
fix.diff suggested-fix Extend amdgpu_ucode_validate to check ucode_array_offset_bytes + ucode_size_bytes <= fw->datasize (single-point fix protecting all amdgpu firmware loaders) 1.2 KB view raw
VERDICT.md verdict full mechanism trace + reachability note 4.9 KB ↓ raw
README.md readme summary + reproduce steps 1.3 KB ↓ raw
build.sh build-script apply fix + build amdgpu.ko 461 B view raw
run.sh run-script placeholder runtime trigger (needs AMD GPU) 974 B view raw
build.log build-log tail of successful full amdgpu.ko build (rc=0) 8.7 KB view raw
env.txt environment uname, cc, hw.model, module status 546 B view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme summary + reproduce steps
↓ download raw

DF-1164 PoC verification β€” source-level trace of gfx_v7_0 CP/MEC/RLC firmware loader OOB-read bug.

amdgpu is not in X86_64_GENERIC; it exists only as a kld module. Loading it on the QEMU guest succeeds but attaches to no device (no AMD Sea Islands GPU), so the firmware-loader code paths are never called. Verification is by source-level trace + build-validation of the fix.

Bug location

Mechanism

Validator only checks fw->datasize == hdr->size_bytes. Crafted header with ucode_size_bytes extending past the blob passes the check; the loader loop le32_to_cpup(fw_data++) for i < ucode_size_bytes/4 reads past the blob into adjacent kernel heap.

Reproduce

  1. Apply fix.diff to /usr/src.
  2. cd /usr/src/sys/dev/drm/amd/amdgpu && make KMOD=amdgpu β†’ amdgpu.ko builds clean.
  3. Runtime test requires AMD Sea Islands GPU + crafted firmware (not present on this guest).

See VERDICT.md for the full analysis.

VERDICT.md verdict full mechanism trace + reachability note
↓ download raw

DF-1164 β€” gfx_v7_0 CP/MEC/RLC firmware loaders: OOB read past ucode blob (source-only verification)

Verdict: REPRODUCED at source level (amdgpu is a kld module, not in GENERIC; not runtime-triggerable without AMD Sea Islands GPU)

Mechanism

amdgpu is not in X86_64_GENERIC (no device drm/device amdgpu in sys/config/X86_64_GENERIC). It exists only as the loadable module /boot/kernel/amdgpu.ko and only attaches to AMD Sea Islands (CIK) GPUs (BONAIRE/KAVERI/KABINI/HAWAII/MULLINS β€” sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:283-289).

The common firmware validator only checks total blob size:

/* sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:251-260 */
int amdgpu_ucode_validate(const struct firmware *fw)
{
    const struct common_firmware_header *hdr =
        (const struct common_firmware_header *)fw->data;

    if (fw->datasize == le32_to_cpu(hdr->size_bytes))   /* total-size only */
        return 0;
    return -EINVAL;
}

The header (sys/dev/drm/amd/amdgpu/amdgpu_ucode.h:26-37) declares ucode_size_bytes and ucode_array_offset_bytes separately. The validator does not check that ucode_array_offset_bytes + ucode_size_bytes <= fw->datasize.

The CIK gfx_v7_0 firmware loaders then trust both header fields blindly:

/* sys/dev/drm/amd/amdgpu/gfx_v7_0.c:2447-2453 (PFP; identical pattern CE/ME/MEC1/MEC2/RLC) */
fw_data = (const __le32 *)
    (adev->gfx.pfp_fw->data +
     le32_to_cpu(pfp_hdr->header.ucode_array_offset_bytes));
fw_size = le32_to_cpu(pfp_hdr->header.ucode_size_bytes) / 4;
WREG32(mmCP_PFP_UCODE_ADDR, 0);
for (i = 0; i < fw_size; i++)
    WREG32(mmCP_PFP_UCODE_DATA, le32_to_cpup(fw_data++));   /* reads past blob */

Same pattern at: - gfx_v7_0.c:2457-2463 (CE) - gfx_v7_0.c:2467-2473 (ME) - gfx_v7_0.c:2708-2715 (MEC1) - gfx_v7_0.c:2731-2737 (MEC2) - gfx_v7_0.c:3572-3577 (RLC)

Effect: A crafted firmware blob with ucode_size_bytes (and/or ucode_array_offset_bytes) declaring more data than the blob actually holds passes amdgpu_ucode_validate (only size_bytes is checked) and then makes the loader loop le32_to_cpup(fw_data++) read past the blob into adjacent kernel heap. The leaked heap bytes are then written to GPU microcode RAM (so they are not directly leaked back to userspace, but the OOB read itself is unbounded and attacker-shaped by the crafted header). Same class as the DF-1119/1130/1133 family.

Trigger reachability on this guest

  • No AMD Sea Islands GPU is present in the QEMU guest.
  • The amdgpu module does load (kldload amdgpu succeeds with the message [drm] amdgpu kernel modesetting enabled.) but attaches to no device, so gfx_v7_0_cp_gfx_load_microcode / gfx_v7_0_cp_compute_load_microcode / gfx_v7_0_rlc_resume are never called. (Loading the module on a non-AMD-GPU host is also a bad idea β€” see the unload hang note below.)
  • The vulnerable code path is therefore not runtime-reachable here.

⚠ Practical note: loading amdgpu.ko on this guest (no AMD GPU) succeeded, but kldunload amdgpu/kldunload drm then wedged the guest (process stuck in state D1 uninterruptible). After observing that, we used vm.sh reset with-src to recover. This is unrelated to the cited OOB bug (it's a known module-refcount issue), but it confirms there is no way to safely exercise amdgpu code paths on this guest.

Fix

fix.diff extends amdgpu_ucode_validate to also check that the declared ucode payload lies within the blob:

int amdgpu_ucode_validate(const struct firmware *fw)
{
    const struct common_firmware_header *hdr = ...;
    uint32_t ucode_size, ucode_offset;

    if (fw->datasize < sizeof(*hdr))            /* header fits */
        return -EINVAL;
    if (fw->datasize != le32_to_cpu(hdr->size_bytes))
        return -EINVAL;

    ucode_size   = le32_to_cpu(hdr->ucode_size_bytes);
    ucode_offset = le32_to_cpu(hdr->ucode_array_offset_bytes);
    if (ucode_offset > fw->datasize ||
        ucode_size > fw->datasize - ucode_offset)   /* payload in-bounds */
        return -EINVAL;

    return 0;
}

This is a single-point fix that protects all amdgpu/radeon firmware loaders (not just gfx_v7_0) since they all funnel through amdgpu_ucode_validate. It matches the finding's recommended fix.

Build verification of the fix

amdgpu.ko built successfully from the patched source on the guest (cc 8.3 [DragonFly], full module build with -j6):

=== AMDGPU_BUILD_DONE rc=0 ===

Full output captured to /root/amdgpu_build.log on the guest; key tail in build.log. The fix is build-clean.

Fix-validation status

not_testable β€” runtime trigger requires an AMD Sea Islands GPU (or an emulator presenting a crafted firmware blob via the amdgpu firmware loader). The guest has neither. We confirmed the fix applies cleanly and the patched full amdgpu.ko module compiles; the change adds three arithmetic guards with no behavioural side-effect on well-formed firmware (which already satisfies ucode_offset + ucode_size <= datasize).

Fix verification

not_testable

compile validated

module build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

Source+harness. amdgpu_ucode_validate no ucode_offset+size bounds -> OOB heap read. amdgpu kld-only, no AMD GPU.