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)
PoC verification
Evidence pack
findings/poc/DF-1251 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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.
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)
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.radeon_combios.c:3010combios_parse_mmio_table:while (RBIOS16(offset)) { ... addr = RBIOS16(offset) & 0x1fff; ... val = RBIOS32(offset); ... WREG32(addr, val); }radeon_combios.c:3089combios_parse_pll_table:while (RBIOS8(offset)) { ... WREG32_PLL(addr, val); }radeon_combios.c:3182combios_parse_ram_reset_table:while (val != 0xff) { ... RBIOS16(offset); ... }- None of these bounds
offsetagainst the BIOS allocation size, so the loop walks pastrdev->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_testablecompile+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.
No comments yet.