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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3573 Β· 12 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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.ko and the radeonkmsfw_BONAIRE_* modules exist in /boot/kernel but nothing attaches, so cik_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)

  1. 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) set err and continue.
  2. 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 sets err only when 1 <= new_fw < num_fw. With all-legacy firmware (new_fw == 0) the final err is simply whatever the last successful assignment left: 0. β†’ function returns 0 with the wrong-size fw installed, new_fw=false.
  3. The consumers are constant-bounded. With new_fw == false the loaders loop over CIK_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) and CIK_SDMA_UCODE_SIZE (1050 dw, cik_sdma.c:501/508) β€” reading from rdev->X_fw->data regardless of the actual allocation. The size gate in (1) was the ONLY thing guaranteeing datasize >= CONSTANT*4.
  4. Net effect: heap OOB read of (CONSTANT*4 - datasize) bytes per short legacy fw, streamed into CP_*_UCODE_DATA / RLC_GPM_UCODE_DATA / SDMA*_UCODE_DATA register 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.
  5. Upstream parity verified: downloaded Linux v4.7 drivers/gpu/drm/radeon/cik.c and confirmed the identical asymmetric goto 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 at out: (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_testable
baseline no→ patch + rebuild →patched clean

fix.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).
↓ fix.diffper-fix-DF-3573

Confirmed kernel references

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.