Legacy firmware size-check bypass (err clobbered by later request_firmware success) β heap OOB reads in constant-bounded legacy ucode loaders
Summary
cik_init_microcode() only guards the pfp legacy-size failure with goto out. The me/ce/mec/rlc/sdma/mc/smc 'Bogus length' blocks set err=-EINVAL and fall through, and every subsequent 'err = request_firmware(...)' assigns 0 on success. With an all-legacy firmware set the final mixing check re-sets err only when mixing new/old, so the function returns 0 with a wrong-size legacy firmware installed. The legacy loaders then bound their read loops by compile-time constants (CIK_ME_UCODE_SIZE, CIK_MEC_UCODE_SIZE, BONAIRE/KV/KB/ML_RLC_UCODE_SIZE, CIK_SDMA_UCODE_SIZE) reading CONSTANT*4 bytes from rdev->X_fw->data regardless of the allocation: heap OOB read of up to 15744B (MEC), streamed into CP/RLC/SDMA ucode FIFOs; page-crossing reads can panic. Verified byte-identical in Linux v4.7 - live upstream bug (cve_match=equivalent). Distinct from DF-1119 (that covers unchecked new-format header fields; this is the legacy size gate itself being bypassable). Root-gated (fw module install); mis-provisioned-system panic/corruption ceiling. Logic-level A/B reproduced 3/3 on the guest with fix.diff flipping accept->reject and controls unchanged; kernel-runtime honestly not_testable (virtio-only guest, no CIK device). Also killed: IH ring-walk OOB, DCE8 div0, workqueue-vs-unload, tiling/golden tables, IRQ handler indices.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3573 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fwsize_bypass.c | β | 11.1 KB | view raw | |
| build.sh | β | 161 B | view raw | |
| run.sh | β | 158 B | view raw | |
| build.log | β | 239 B | view raw | |
| run.log | β | 3.5 KB | view raw | |
| run.2.log | β | 2.1 KB | view raw | |
| env.txt | β | 530 B | view raw | |
| fix.diff | β | 1.6 KB | view raw | |
| VERDICT.md | β | 5.0 KB | β raw | |
| README.md | β | 3.1 KB | β raw | |
| manifest.json | β | 1.5 KB | view raw | |
| verdict.json | β | 5.0 KB | view raw |
DF-3573 β legacy-firmware size-check bypass in cik_init_microcode() β heap OOB reads in legacy ucode loaders
File: sys/dev/drm/radeon/cik.c (pass 2, GLM 5.3)
What
cik_init_microcode() (cik.c:1961-2299) validates legacy (old-format) firmware
sizes, but only the pfp block follows a "Bogus length" with goto out
(cik.c:2060). The me / ce / mec / rlc / sdma / mc / smc blocks set
err = -EINVAL and fall through (cik.c:2080-2084, 2103-2107, 2126-2130,
2164-2168, 2187-2191, 2216-2221, 2248-2252). Every subsequent
err = request_firmware(...) assignment clobbers the error on success, so
when any later firmware in the sequence loads cleanly the function returns 0
with the wrong-size firmware still installed and rdev->new_fw = false.
The legacy loaders then bound their read loops by compile-time constants,
not by fw->datasize:
| loader | loop bound | site |
|---|---|---|
cik_cp_gfx_load_microcode PFP/CE/ME |
CIK_{PFP,CE,ME}_UCODE_SIZE (2144 dw) |
cik.c:3966, 3973, 3980 |
cik_cp_compute_load_microcode MEC1/2 |
CIK_MEC_UCODE_SIZE (4192 dw) |
cik.c:4318, 4326 |
cik_rlc_resume RLC |
BONAIRE/KV/KB/ML_RLC_UCODE_SIZE |
cik.c:6001-6002 |
cik_sdma_load_microcode SDMA0/1 |
CIK_SDMA_UCODE_SIZE (1050 dw) |
cik_sdma.c:501, 508 |
A legacy firmware file shorter than CONSTANT*4 therefore produces a kernel
heap OOB read of up to CONSTANT*4 - datasize bytes (e.g. 7552 bytes for
a 1 KiB ME file), streamed into CP/RLC register FIFOs. (The MC legacy loader
is self-bounded by datasize/4 at cik.c:1880 β harmless.)
Not a DFly port bug: verified byte-identical to Linux v4.7
drivers/gpu/drm/radeon/cik.c (upstream carries the same asymmetric goto
out). Distinct from DF-1119 (new-format header fields unchecked): this is the
legacy path and the size gate itself being bypassable.
Build / Run (guest)
scp fwsize_bypass.c dfbsd-maxx:/tmp/ cc -O -Wall -o /tmp/fwsize_bypass /tmp/fwsize_bypass.c cc -O -Wall -DFIXED -o /tmp/fwsize_bypass_fixed /tmp/fwsize_bypass.c /tmp/fwsize_bypass # baseline acceptance state machine /tmp/fwsize_bypass_fixed # with fix.diff applied
The harness is a statement-faithful transcription of cik.c:2049-2297 (BONAIRE, all-legacy) plus a simulation of the constant-bounded legacy loops. It runs on any Unix β no radeon hardware needed β because the kernel path itself requires a CIK GPU to attach, which this (virtio-only) guest does not have.
Expected output (baseline)
== S1: truncated legacy ME firmware ==
cik_init_microcode -> err=0 (ACCEPTED), installed fw: ..., me_fw(1024B), ...
OOB READ: me_fw holds 1024 bytes, cik.c:3980 loop reads 8576 -> 7552 bytes past allocation
and, under -DFIXED (fix.diff):
== S1: truncated legacy ME firmware == cik_init_microcode -> err=-22 (rejected), installed fw: (none)
Controls: stock sizes accepted in both modes; a truncated pfp is rejected
in both modes (pfp's block already has goto out).
Fix
fix.diff β add goto out; after every "Bogus length" err = -EINVAL
(7 sites), matching the pfp precedent. Applies with patch -p0 from the
tree root; verified against a scratch copy.
DF-3573 VERDICT β legacy-firmware size-check bypass β heap OOB reads in legacy loaders
Status: not_testable (kernel path requires CIK radeon hardware; guest is virtio-only) Β· logic-level reproduction: 3/3 runs, deterministic Β· confidence: certain (source-traced + upstream-diffed)
Reproduced?
At the logic level, yes β deterministically, on the guest itself (compiled with the guest's cc 8.3). At the kernel level, no β and honestly so:
- The trigger path is
vgapci attach (CIK device) β radeon_driver_load β cik_init β cik_init_microcode β (later) cik_cp_resume β cik_cp_gfx_load_microcode legacy loop. - The audit guest's only display device is
vgapci0@pci0:0:2.0: chip=0x11111234(QEMU virtual VGA). QEMU has no radeon CIK emulation.radeon.koand theradeonkmsfw_BONAIRE_*modules exist in /boot/kernel but nothing attaches, socik_init_microcode()can never run. - Per the honest-status table this is "guest reachable but the target device
is absent" β
not_testable,reproduced=0,impact=none(on this guest).
Why the finding is nonetheless certain (the full source chain)
- The gate is asymmetric. Only the pfp block does
err = -EINVAL; goto out;(cik.c:2056-2061). The me/ce/mec/rlc/sdma blocks (cik.c:2080-2084, 2103-2107, 2126-2130, 2164-2168, 2187-2191), the mc block (2216-2221) and the smc block (2248-2252) seterrand continue. - err is a scratch variable. Each next stage begins
err = request_firmware(&rdev->X_fw, ...)which assigns 0 on success (e.g. cik.c:2096-2098). After the last stage, the only remaining guard is the mixing check (cik.c:2265-2272), which setserronly when1 <= new_fw < num_fw. With all-legacy firmware (new_fw == 0) the finalerris simply whatever the last successful assignment left: 0. β function returns 0 with the wrong-size fw installed,new_fw=false. - The consumers are constant-bounded. With
new_fw == falsethe loaders loop overCIK_ME_UCODE_SIZE(2144 dwords, cik.c:3980),CIK_MEC_UCODE_SIZE(4192 dw, cik.c:4318/4326),BONAIRE|KV|KB|ML_RLC_UCODE_SIZE(cik.c:6001-6002) andCIK_SDMA_UCODE_SIZE(1050 dw, cik_sdma.c:501/508) β reading fromrdev->X_fw->dataregardless of the actual allocation. The size gate in (1) was the ONLY thing guaranteeingdatasize >= CONSTANT*4. - Net effect: heap OOB read of
(CONSTANT*4 - datasize)bytes per short legacy fw, streamed intoCP_*_UCODE_DATA/RLC_GPM_UCODE_DATA/SDMA*_UCODE_DATAregister FIFOs. Worst single span: MEC 4192*4=16768 B minus file size. Can also fault (panic) if the read walks past the last mapped page of the firmware allocation. - Upstream parity verified: downloaded Linux v4.7
drivers/gpu/drm/radeon/cik.cand confirmed the identical asymmetricgoto out(only pfp) β this is an upstream bug faithfully ported, not a DFly transcription error.cve_match=equivalent.
Harness result (run.log, run.2.log, 3/3 stable)
- S1 (truncated legacy ME, 1024 B vs 8576 B): baseline
err=0 (ACCEPTED),me_fw(1024B)installed β simulated cik.c:3980 loop reads 8576 B β 7552 bytes past allocation. With-DFIXED(fix.diff):err=-22, all firmware released atout:(models cik.c:2275-2297). - S2 control (truncated pfp): rejected in BOTH modes β proving the asymmetry is the bug, not the harness.
- S3 control (all stock): accepted in both modes β fix has no false positives on a healthy system.
Exploit chain β why this stops at defense-in-depth
Precondition: root must place a wrong-size legacy firmware module
(radeonkmsfw_BONAIRE_me.ko &c.) in /boot/kernel β the DFly firmware
loader only reads root-owned modules. That makes the primitive root-gated
(no unprivβroot path), consistent with DF-1119's rating of the sibling
new-format issue. The OOB data goes to GPU ucode registers, not directly to
userspace, so even the leak ceiling is indirect. Residual risk: kernel panic
(page-crossing OOB) and GPU-state corruption on systems with mis-installed
firmware β a robustness hole more than an attacker primitive, hence Low.
Fix validation
fix.diff (7 Γ goto out, pfp-parity) applies patch -p0 clean to a
scratch copy of sys/dev/drm/radeon/cik.c and compiles conceptually identical
logic in the harness's FIXED mode (reject + release-all). A patched-kernel
boot test is impossible on this guest (no CIK device), so
fix_status=not_testable; the logic-level A/B (baseline accepts / fixed
rejects / controls unchanged) is recorded in run.log.
Recommendation
Carry the fix.diff upstream as well β the asymmetric goto out is live in
Linux's radeon cik.c today.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testablefix.diff (7x goto out, patch -p0 clean against a scratch copy) flips the harness from ACCEPTED+7552B-OOB to rejected-with-release-all while leaving both control scenarios unchanged (run.log FIXED section). A patched-kernel boot test is impossible on this guest: no CIK radeon device exists, so cik_init_microcode cannot run. fix validated at logic level only.
run.log '=== FIXED (with fix.diff: goto out after every bogus length) ===' -> 'S1 ... err=-22 (rejected), installed fw: (none)'; S3 control unchanged (err=0, all fw ok); fix.diff dry-run/apply log in the analysis transcript (patch -p0: 7 hunks applied).
Confirmed kernel references
- sys/dev/drm/radeon/cik.c:2056
- sys/dev/drm/radeon/cik.c:2080
- sys/dev/drm/radeon/cik.c:2103
- sys/dev/drm/radeon/cik.c:2126
- sys/dev/drm/radeon/cik.c:2164
- sys/dev/drm/radeon/cik.c:2187
- sys/dev/drm/radeon/cik.c:2216
- sys/dev/drm/radeon/cik.c:2248
- sys/dev/drm/radeon/cik.c:2265
- sys/dev/drm/radeon/cik.c:3966
- sys/dev/drm/radeon/cik.c:3973
- sys/dev/drm/radeon/cik.c:3980
- sys/dev/drm/radeon/cik.c:4318
- sys/dev/drm/radeon/cik.c:4326
- sys/dev/drm/radeon/cik.c:6001
- sys/dev/drm/radeon/cik_sdma.c:501
- sys/dev/drm/radeon/radeon_ucode.h:38
Detail
Evidence (decisive lines)
run.log + run.2.log: 'S1: truncated legacy ME firmware -> err=0 (ACCEPTED) ... me_fw(1024B)' and 'OOB READ: me_fw holds 1024 bytes, cik.c:3980 loop reads 8576 -> 7552 bytes past allocation'; FIXED section of run.log: 'err=-22 (rejected), installed fw: (none)'; S2 control (truncated pfp) rejected in BOTH modes proving the asymmetry; VERDICT.md has the full 5-step source chain and the upstream diff note; env.txt records the guest has no radeon hardware (vgapci0 chip=0x11111234).
PoC changes
No seeded PoC existed (pass-2 new finding). Harness transcribed statement-for-statement from cik.c:2049-2297 with the exact constants; initial version under-modeled 'err = request_firmware()' assigning 0 on success (the very clobber under test) and was corrected; 'out:' release-all on error modeled per cik.c:2275-2297.
Verified recommended fix
Add 'goto out;' after every Bogus-length err=-EINVAL in cik_init_microcode (7 sites, pfp-parity) -- see fix.diff; optionally also clamp legacy loader loops to min(CONSTANT, fw->datasize/4).
Verdict
Legacy-firmware size-check bypass in cik_init_microcode(): only the pfp block does 'goto out' after a Bogus-length error (cik.c:2060); me/ce/mec/rlc/sdma/mc/smc set err=-EINVAL and continue (cik.c:2080-2252), and every later successful request_firmware() zeroes err, so an all-legacy system accepts a wrong-size firmware whenever any subsequent fw loads cleanly (final guard at cik.c:2265 only fires when mixing new/old). The legacy loaders then read CIK__UCODE_SIZE4 bytes from the fw buffer regardless of its size (cik.c:3966/3973/3980, 4318/4326, 6001-6002; cik_sdma.c:501/508) -> heap OOB read of up to CONSTANT*4-datasize bytes (7552 B for a 1 KiB ME file; 15.7 KiB worst case for MEC). Kernel-level verification impossible on this guest: virtio VGA only (0x1234:0x1111), no CIK device, radeon.ko never attaches. Logic-level reproduction on the guest is deterministic (3/3): baseline accepts the truncated ME fw and prints the 7552-byte OOB span; the FIXED build (fix.diff: 7x 'goto out', pfp parity) rejects with err=-EINVAL and releases everything at out:. Precondition (root-installed wrong-size fw module) gates this to defense-in-depth, consistent with DF-1119's rating of the sibling new-format issue. Upstream Linux v4.7 verified byte-identical (asymmetric goto out) -- live upstream bug, cve_match=equivalent.
No comments yet.