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

Firmware header OOB heap read in si_mc/si_cp/si_rlc microcode loaders via unchecked offset/size fields

Summary

si_mc_load_microcode (si.c:1574-1634), si_cp_load_microcode (si.c:3489-3513), si_rlc_resume (si.c:5876-5888) compute payload pointers and loop counts from firmware header fields (ucode_array_offset_bytes, ucode_size_bytes, io_debug_size_bytes, io_debug_array_offset_bytes) WITHOUT bounds checking against fw->datasize. radeon_ucode_validate only checks datasize==size_bytes. Crafted firmware -> heap OOB read past firmware buffer into adjacent kernel heap -> panic or limited heap data disclosure to GPU MMIO registers. Same bug class as DF-1119 (cik.c). Reached at every boot/resume on SI hardware regardless of accel_working=false gate. Fix: validate all offset/size fields against fw->datasize in radeon_ucode_validate or inline at each loader.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1130 Β· 11 files
FileTypeDescriptionSize
harness.c trigger-source userspace harness: crafted firmware header with OOB ucode_array_offset/size 4.9 KB view raw
fix.diff suggested-fix add offset+size<=datasize checks in si_mc/cp/rlc loaders 4.2 KB view raw
build.sh build-script cc -O2 -Wall -o harness harness.c 240 B view raw
run.sh run-script ./harness 91 B view raw
run.log run-log decisive harness run, full output 678 B view raw
fix_build.log build-log radeon.ko rebuilt cleanly with fix applied (2,029,288 bytes) 23.8 KB view raw
env.txt environment uname, cc version, kldstat 278 B view raw
VERDICT.md verdict full narrative: 3 loaders, validator gap, harness proof 4.5 KB ↓ raw
README.md readme human-facing summary 1.4 KB ↓ 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 human-facing summary
↓ download raw

DF-1130 β€” radeon/si.c firmware loader OOB heap read

TL;DR

  • Status: REPRODUCED (source + harness). Three SI microcode loaders consume firmware header offset/size fields without bounds-checking them against fw->datasize. radeon_ucode_validate only checks datasize == size_bytes, so a crafted firmware with mismatched payload offset/size causes an arbitrary-length OOB heap read.
  • Impact: panic / DoS (read-only primitive; the OOB u32s are written to GPU MMIO registers, not returned to userspace β€” primary realistic outcome is a page-fault panic during boot/resume microcode load).

Why no live trigger on this guest

Bug is in radeon.ko. The QEMU audit guest has no AMD GPU, so radeon.ko is never kldloaded. Trigger requires SI hardware + a crafted firmware blob (or a malicious VBIOS).

Files

  • harness.c β€” userspace harness simulating the crafted firmware and validating both buggy and fixed loader logic.
  • fix.diff β€” adds offset + size <= datasize checks in si_mc_load_microcode, si_cp_load_microcode, si_rlc_resume.
  • run.log, env.txt.

Reproduce

./build.sh && ./run.sh

Expected: harness shows radeon_ucode_validate accepting the crafted firmware, the buggy loader computing a 19,440-byte OOB read, and the fixed loader rejecting with -EINVAL.

Fix validation

fix.diff applied to /usr/src; radeon.ko rebuilt cleanly (2,029,288 bytes). See VERDICT.md.

VERDICT.md verdict full narrative: 3 loaders, validator gap, harness proof
↓ download raw

DF-1130 β€” Verdict

Verdict: REPRODUCED (source-level + harness) β€” OOB heap read / panic class, no escalation chain (read-only primitive)

Bug confirmation

Three Southern Islands microcode loaders in radeon/si.c consume firmware header fields (ucode_array_offset_bytes, ucode_size_bytes, io_debug_array_offset_bytes, io_debug_size_bytes) without checking them against fw->datasize:

  1. si_mc_load_microcode at radeon/si.c:1574-1584: c regs_size = le32_to_cpu(hdr->io_debug_size_bytes) / (4 * 2); new_io_mc_regs = (const __le32 *) (rdev->mc_fw->data + le32_to_cpu(hdr->io_debug_array_offset_bytes)); ucode_size = le32_to_cpu(hdr->header.ucode_size_bytes) / 4; new_fw_data = (const __le32 *) (rdev->mc_fw->data + le32_to_cpu(hdr->header.ucode_array_offset_bytes)); Then at line 1622-1636 the loader loops regs_size and ucode_size times reading from new_io_mc_regs++ / new_fw_data++, each iteration writing the read u32 to a GPU MMIO register via WREG32.

  2. si_cp_load_microcode at radeon/si.c:3489-3513 β€” same pattern for each of PFP, CE, ME microcode blocks.

  3. si_rlc_resume at radeon/si.c:5876-5888 β€” same pattern for the RLC microcode.

The upstream validator radeon_ucode_validate (radeon_ucode.c:156-165) only checks fw->datasize == hdr->size_bytes:

if (fw->datasize == le32_to_cpu(hdr->size_bytes))
    return 0;

This passes for any crafted firmware whose header size_bytes matches the file length, independently of whether the payload offset/size stay inside the file. A crafted firmware can therefore set:

size_bytes                  = file_length      // passes validator
ucode_array_offset_bytes    = file_length - 16 // near end of file
ucode_size_bytes            = 0x10000          // huge

and the loader will read 0x10000 bytes starting near the end of the firmware buffer β€” 0x10000 - 16 bytes of which are OOB heap reads.

Harness confirmation

harness.c constructs exactly that crafted firmware (1024-byte buffer, ucode_array_offset_bytes=0xff0, ucode_size_bytes=0x4000) and shows: - radeon_ucode_validate returns 0 (passes) - The buggy loader path computes an OOB read of 19,440 bytes past the firmware buffer. - The fixed loader path rejects with -EINVAL.

Output captured in run.log.

Exploit chain

This is a read-only primitive β€” the OOB-read u32 values are written to GPU MMIO registers (WREG32), not directly returned to userspace. The realistic impact is therefore:

  • Panic when the OOB read crosses into an unmapped page (the firmware buffer is vmalloc/kmalloc-backed and the read can run for thousands of bytes β€” easily crosses a page boundary on a slab or vmalloc hole).
  • MMIO side-channel of adjacent heap data written into GPU registers. On hardware this could in principle be read back by a GPU command buffer, but that requires a much more elaborate setup.

No write primitive, no escalation chain. The bug's primary impact is DoS via panic during microcode load (which happens at every boot/resume on SI hardware).

Trigger conditions (not met on this guest)

  1. AMD SI GPU present (the QEMU audit guest has no AMD GPU).
  2. radeon.ko loaded.
  3. Crafted SI firmware blob in /boot/modules/radeonkmsfw_*.ko (the kernel links against these firmware modules) β€” requires either a malicious distro or an attacker who can replace the firmware files (typically root-only, but a malicious VBIOS reflashed via VFIO can also surface as crafted power tables; the firmware header itself, however, is supplied by the loaded .ko).

Source-level + harness-confirmed; no live runtime trigger on the guest.

Fix

fix.diff adds explicit offset + size <= datasize checks in all three loaders (si_mc_load_microcode, si_cp_load_microcode, si_rlc_resume). Each check returns -EINVAL if the payload would escape the firmware buffer. The fix is local to each loader (no change to radeon_ucode_validate) β€” minimally invasive.

Fix validation

  1. patch -p1 --check β€” clean apply, all 3 hunks.
  2. cd /usr/src/sys/dev/drm/radeon && make with the diff applied β€” radeon.ko built cleanly (rc=0, 2,029,288 bytes).
  3. Reverted.

Since the bug cannot be triggered live on the guest (no AMD GPU), the behaviour comparison is at the harness level: run.log shows the buggy path computing a 19,440-byte OOB read while the fixed path returns -EINVAL.

fix_status: fixed (compiles cleanly, harness confirms the patched code path rejects the bad input).

Fix verification

fixed

validated

radeon.ko build rc=0 + harness before/after
↓ fix.diffn/a (module-level)

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

Source+harness. si_mc/cp/rlc firmware header offset+size no bounds -> OOB heap read. No AMD GPU.