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)
PoC verification
Evidence pack
findings/poc/DF-1164 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
- Validator (insufficient):
sys/dev/drm/amd/amdgpu/amdgpu_ucode.c:251-260 - Header with separate size/offset fields:
amdgpu_ucode.h:26-37 - Loader loops that trust both fields:
sys/dev/drm/amd/amdgpu/gfx_v7_0.c:2447-2453(PFP)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)
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
- Apply
fix.diffto /usr/src. cd /usr/src/sys/dev/drm/amd/amdgpu && make KMOD=amdgpuβ amdgpu.ko builds clean.- Runtime test requires AMD Sea Islands GPU + crafted firmware (not present on this guest).
See VERDICT.md for the full analysis.
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 amdgpusucceeds with the message[drm] amdgpu kernel modesetting enabled.) but attaches to no device, sogfx_v7_0_cp_gfx_load_microcode/gfx_v7_0_cp_compute_load_microcode/gfx_v7_0_rlc_resumeare 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.koon this guest (no AMD GPU) succeeded, butkldunload amdgpu/kldunload drmthen wedged the guest (process stuck in stateD1uninterruptible). After observing that, we usedvm.sh reset with-srcto 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_testablecompile 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.
No comments yet.