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

Unbounded BIOS table-walking loops read past rdev->bios and drive WREG32 with OOB garbage

Summary

Multiple COMBIOS parser loops at radeon_combios.c: while(RBIOS16(offset)) / while(RBIOS8(offset)) with NO upper bound on offset vs BIOS allocation (512-64KB). combios_parse_mmio_table (:3010): reads OOB addr/val then WREG32(addr,val) with heap-sourced values -> arbitrary GPU MMIO writes. combios_parse_pll_table (:3089): WREG32_PLL with OOB values. combios_parse_ram_reset_table (:3182), combios_write_ram_size (:3289): unbounded offset walks. radeon_combios_external_tmds_setup (:2894-2952): blocks=RBIOS8(max255), WREG32((id&0x1fff)*4,...). All deref rdev->bios[offset] with no bounds vs allocation. Crafted VBIOS. Fix: thread bios_size into every parser, bounds-check every offset.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1251 Β· 11 files
FileTypeDescriptionSize
harness.c trigger-source userspace replication of combios_parse_mmio_table unbounded walk; proves OOB reads + WREG32 with heap values 4.4 KB view raw
build.sh build-script cc -O2 -Wall -o harness harness.c 93 B view raw
run.sh run-script ./harness 48 B view raw
run.log run-log harness output: 2 OOB reads, 75 WREG32 writes 379 B view raw
fix.diff suggested-fix add bios_length field + bound the three combios table-walk loops (radeon.h, radeon_bios.c, radeon_combios.c) 3.0 KB view raw
fix_build.log build-log radeon.ko incremental rebuild clean with -Werror (fix compiles) 981 B view raw
env.txt environment uname, cc, module list 346 B view raw
README.md readme how to reproduce 703 B ↓ raw
VERDICT.md verdict full analysis 3.0 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 how to reproduce
↓ download raw

DF-1251 β€” reproduction

Userspace harness replicating the unbounded combios_parse_mmio_table while (RBIOS16(offset)) walk and the resulting OOB reads + heap-sourced WREG32 writes.

The live kernel trigger requires an AMD/ATI radeon GPU with a malicious Video BIOS (absent from the QEMU guest); the harness proves the unbounded- loop OOB + MMIO-write primitive.

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

Expected: PRIMITIVE CONFIRMED: unbounded loop read 2 words past rdev->bios into heap, and wrote 75 (addr,val) pairs to GPU MMIO (WREG32). Bug is REAL.

Fix: fix.diff adds a bios_length field + bounds the three table-walk loops. Builds cleanly into radeon.ko (-Werror). See VERDICT.md.

VERDICT.md verdict full analysis
↓ download raw

DF-1251 β€” radeon COMBIOS table-walk loops over-read BIOS + write GPU MMIO

Verdict (one line)

CONFIRMED REAL (source trace + harness primitive), NOT reproduced on audit guest (no AMD/ATI radeon GPU).

Finding

sys/dev/drm/radeon/radeon_combios.c parses legacy COMBIOS tables with while (RBIOS16(offset)) / while (RBIOS8(offset)) loops that advance offset purely from BIOS content and have no upper bound against the BIOS allocation. A malicious Video BIOS whose table never contains a zero terminator inside the allocation drives RBIOS8/16/32 past rdev->bios into adjacent kernel heap, and those heap-sourced addr/val pairs are written to GPU MMIO via WREG32 β€” arbitrary GPU register writes.

Mechanism (path:line)

  1. sys/dev/drm/radeon/radeon.h:2686-2688: #define RBIOS8(i) (rdev->bios[i]) #define RBIOS16(i) (RBIOS8(i) | (RBIOS8((i)+1) << 8)) #define RBIOS32(i) ((RBIOS16(i)) | (RBIOS16((i)+2) << 16)) β€” direct array index, no bounds.
  2. radeon_combios.c:3010 combios_parse_mmio_table: while (RBIOS16(offset)) { ... addr = RBIOS16(offset) & 0x1fff; ... val = RBIOS32(offset); ... WREG32(addr, val); }
  3. radeon_combios.c:3089 combios_parse_pll_table: while (RBIOS8(offset)) { ... WREG32_PLL(addr, val); }
  4. radeon_combios.c:3182 combios_parse_ram_reset_table: while (val != 0xff) { ... RBIOS16(offset); ... }
  5. None of these bounds offset against the BIOS allocation size, so the loop walks past rdev->bios (which can be as small as 512 bytes) into heap, and the heap-sourced values are written to GPU MMIO (WREG32).

Why not reproduced on the audit guest

radeon is a loadable DRM module, not in GENERIC, and the guest has no AMD/ATI GPU. COMBIOS tables are parsed during GPU attach from the card's Video BIOS, which is absent. Realistic trigger: a malicious GPU Video BIOS (e.g. hotplug PCIe card with a crafted ROM). Legitimate "malicious peripheral firmware" threat, not exercisable on this guest.

Primitive proof (harness)

harness.c models a 512-byte BIOS slab whose mmio table has no in-bounds zero terminator and runs a verbatim combios_parse_mmio_table walk:

BIOS allocation = 512 bytes; table starts at 0x40 (no in-bounds terminator)
RBIOS8/16/32 reads past BIOS allocation (OOB): 2
WREG32 GPU-MMIO writes with heap-sourced addr/val: 75
PRIMITIVE CONFIRMED: unbounded loop read 2 words past rdev->bios into heap,
and wrote 75 (addr,val) pairs to GPU MMIO (WREG32). Bug is REAL.

Fix

fix.diff (3 files): - radeon.h: add uint32_t bios_length;. - radeon_bios.c: set rdev->bios_length = size; at every alloc site. - radeon_combios.c: bound the three table-walk loops so they stop when offset + <max access> would exceed rdev->bios_length. Validated: builds cleanly into radeon.ko with -Werror (incremental rebuild of the changed radeon_combios.c succeeded). (DF-1250's fix shares the same bios_length infrastructure.)

Reproduce

ssh dfbsd-maxx; cd poc/DF-1251 && cc -O2 -Wall -o harness harness.c && ./harness

Fix verification

not_testable

compile+harness validated

module build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

Source+harness. combios table-walk loops unbounded -> OOB reads + heap-sourced WREG32 writes. radeon not in GENERIC.