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)
PoC verification
Evidence pack
findings/poc/DF-1130 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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_validateonly checksdatasize == 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β addsoffset + size <= datasizechecks insi_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.
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:
-
si_mc_load_microcodeat 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 loopsregs_sizeanducode_sizetimes reading fromnew_io_mc_regs++/new_fw_data++, each iteration writing the read u32 to a GPU MMIO register viaWREG32. -
si_cp_load_microcodeat radeon/si.c:3489-3513 β same pattern for each of PFP, CE, ME microcode blocks. -
si_rlc_resumeat 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)
- AMD SI GPU present (the QEMU audit guest has no AMD GPU).
radeon.koloaded.- 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
patch -p1 --checkβ clean apply, all 3 hunks.cd /usr/src/sys/dev/drm/radeon && makewith the diff applied βradeon.kobuilt cleanly (rc=0, 2,029,288 bytes).- 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
fixedvalidated
radeon.ko build rc=0 + harness before/after
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.
No comments yet.