Voltage-GPIO MMIO register index taken raw from VBIOS and executed by r100_pm_misc() β arbitrary in-BAR register RMW and out-of-BAR MM_INDEX writes (up to 0x3FFFC)
Summary
radeon_combios_get_power_modes() builds the VOLTAGE_GPIO record with gpio.reg = RBIOS16(...)*4 and gpio.mask = (1<<tmp) straight from the powerplay table with no range check. r100_pm_misc() later executes RREG32/WREG32 at that offset on every PM clock transition of pre-AVIVO ASICs. Offsets >= rmmio_size (64KB BAR) are re-routed into RADEON_MM_INDEX with the raw 18-bit attacker offset - an out-of-BAR indexed device access; in-range values give an arbitrary in-BAR register RMW with a BIOS-chosen mask bit. HARNESS proves the primitive deterministically (BIOS word 0xffff -> MM_INDEX 0x3fffc). Distinct from DF-1251 (OOB-read garbage at init; this is in-bounds data executed at PM time as a register INDEX) and from DF-1252. Upstream parity verified (Linux v5.10). Crafted VBIOS on a pre-AVIVO mobility board; no privilege needed after the image is in the machine. GPU-register corruption incl. clock/aperture/window registers via the indexed path.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3852 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | userspace replica of the combios powerplay voltage-GPIO parse + r100_pm_misc sink; proves BIOS word 0xffff drives MM_INDEX 0x3fffc (out of the 64KB BAR) | 5.1 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o harness harness.c | 65 B | view raw |
| run.sh | run-script | ./harness (crash-tolerant wrapper for the SIGFPE/SIGSEGV harnesses) | 89 B | view raw |
| build.log | build-log | full compiler output (guest) | 65 B | view raw |
| run.log | run-log | full runtime output (guest) - decisive | 632 B | view raw |
| fix.diff | suggested-fix | validated combios voltage-GPIO register index -> out-of-BAR indexed MMIO RMW (r100_pm_misc sink) - git/patch-applyable vs sys/ | 1.7 KB | view raw |
| fix_apply.log | apply-log | patch --dry-run -p1 -d /usr/src on the guest: clean apply | 314 B | view raw |
| VERDICT.md | verdict | full narrative with path:line trace | 4.7 KB | β raw |
| README.md | readme | build/run/expected + hardware reproduction notes | 1.9 KB | β raw |
| env.txt | environment | guest uname + toolchain + 'dmesg radeon lines: 0' | 210 B | view raw |
| verdict.json | verdict-json | machine verdict (persist_poc.py input) | 3.8 KB | view raw |
DF-3852 β COMBIOS voltage-GPIO register index injected into MMIO without range validation
Verdict (one line)
CONFIRMED REAL by source trace + harness primitive; kernel-side execution NOT
observable on the audit guest (virtio-only, no AMD/ATI GPU) β inconclusive.
Build / run
./build.sh # cc -O2 -Wall -o harness harness.c ./run.sh # prints the parsed gpio.reg and the resulting MMIO routing
Expected output
The crafted COMBIOS powerplay record (misc bit 0x4 = ATOM_PM_MISCINFO_VOLTAGE_DROP_SUPPORT)
yields gpio.reg = 0xffff * 4 = 0x3FFFC. The r100_pm_misc() RMW then targets
offset 0x3FFFC β 3x past the 64 KB radeon MMIO BAR β which r100_mm_wreg_slow()
routes into RADEON_MM_INDEX as a raw indexed access. Harness prints:
[combios] VOLTAGE_GPIO parsed: gpio.reg=0x0003fffc ... [slow] WREG32: MM_INDEX <- 0x0003fffc (>= rmmio_size 0x10000) -> indexed write OUTSIDE the MMIO BAR PRIMITIVE PROVEN: BIOS word 0xffff -> MMIO offset 0x3fffc >= rmmio_size 0x10000 ...
What the harness is / is not
- IS: a 1:1 replication of the kernel parsing math
(
radeon_combios.c:2748-2763) and the sink semantics (r100.c:352-364RMW +r100.c:4132-4138slow-path routing). - IS NOT: a kernel run. The audit guest has no radeon device, so the
in-kernel MMIO write cannot be observed there. On real pre-AVIVO mobility
hardware this executes at every PM clock transition
(
radeon_pm.c:219/241 -> r100_pm_misc).
Reproduce on target hardware
Crafted COMBIOS image (see harness.c bytes): powerplay table, rev 5,
misc = 0x0004, gpio register word 0xFFFF, mask bit 9. Flash/replace the
VBIOS on an RS100βRS480/R100βR420-class mobility board (or supply via
PCI ROM BAR on a hotplug card), load radeonkms, trigger a PM transition
(suspend/resume or profile change). The GPU-side indexed access at 0x3FFFC
fires from r100_pm_misc().
DF-3852 β VERDICT
Status: inconclusive (primitive proven in harness; kernel path hardware-gated, no radeon GPU on audit guest)
Full narrative
Where the bug is
radeon_combios_get_power_modes() parses the legacy COMBIOS POWERPLAY table
only on RADEON_IS_MOBILITY boards (sys/dev/drm/radeon/radeon_combios.c:2720-2721).
When the BIOS word at offset+0x5 has bit 0x4 set, the parser builds a
VOLTAGE_GPIO record whose MMIO register index is taken raw from the BIOS
and multiplied by 4:
radeon_combios.c:2748-2752(rev < 6):gpio.reg = RBIOS16(offset + 0x5 + 0xb) * 4; gpio.mask = (1 << tmp);radeon_combios.c:2754-2760(rev >= 6, viavoltage_table_offset):gpio.reg = RBIOS16(voltage_table_offset) * 4; gpio.mask = (1 << tmp);
No range check. A 16-bit BIOS word gives reg anywhere in 0x0..0x3FFFC.
The sink
r100_pm_misc() (sys/dev/drm/radeon/r100.c:352-364) executes on every PM
clock transition for pre-AVIVO ASICs (.misc = &r100_pm_misc, radeon_asic.c:247+),
reached from radeon_pm_set_clocks() (radeon_pm.c:219/241):
if ((voltage->type == VOLTAGE_GPIO) && (voltage->gpio.valid)) {
tmp = RREG32(voltage->gpio.reg); /* attacker-chosen offset */
tmp |= voltage->gpio.mask; /* or &= ~mask */
WREG32(voltage->gpio.reg, tmp);
The enabling bit is the same bit the combios parser tested:
ATOM_PM_MISCINFO_VOLTAGE_DROP_SUPPORT == 0x00000004 (atombios.h:7620) ==
the parser's misc & 0x4 β one crafted word turns the whole path on.
Why this is out-of-BAR / out-of-range
RREG32/WREG32 are r100_mm_rreg/r100_mm_wreg (radeon.h:2536-2539). For
reg < rdev->rmmio_size (the BAR, 64 KB on pre-AVIVO radeon) the access is a
direct readl/writel inside the mapping β the BIOS already picks any
register there for a chosen-bit RMW. For reg >= rmmio_size the access falls
to r100_mm_rreg_slow/r100_mm_wreg_slow (r100.c:4125-4138), which load the
raw offset into RADEON_MM_INDEX and hit RADEON_MM_DATA β an indexed
device access outside the mapped window with the attacker's full 18-bit
offset. Either way the VBIOS controls the target register of a kernel-issued
read-modify-write; combined with the UB mask (1 << tmp, tmp = BIOS byte,
garbage/any-bit for tmp >= 32 β see DF-3856) the write content is
attacker-chosen as well.
Not host-RAM corruption by itself: the primitive is device-space register corruption (arbitrary GPU register RMW, incl. clock/aperture/window registers via the indexed path), i.e. GPU hang / hardware misprogramming / potential second-stage GART abuse by an unprivileged DRM client on the same node.
Relationship to the known findings
- DF-1251 covers the init-table loops whose OOB-read garbage flows into WREG32 at driver init. This finding is different: in-bounds BIOS data (no over-read), parsed at init, executed later at PM time, whose value (a register index) is injected into RREG32/WREG32 with no validation.
- DF-1252 covers the OOB read of
voltage_table_offsetitself. Here the read is assumed in-bounds; the defect is the unvalidated register index and mask derived from it.
Why inconclusive and not reproduced
The audit guest is virtio-only (env.txt): there is no radeon device, no
rdev->rmmio, no VBIOS source. The kernel path cannot be exercised. The
harness (harness.c) replicates the parser and the sink 1:1 and proves the
primitive deterministically (BIOS word 0xFFFF -> MMIO offset 0x3FFFC ->
out-of-BAR indexed write). On real hardware the same code path is
unconditional once the crafted table is present; nothing in the path depends
on anything but the BIOS bytes and the (chip-fixed) mobility flag.
Fix
fix.diff: validate gpio_reg < 0x10000 and mask bit < 32 in both rev
paths; mark the record invalid otherwise. Apply-verified against the pristine
tree (patch --dry-run clean, see fix_apply.log). Runtime fix validation
not possible on this guest (no hardware): fix_status = not_testable.
kernel_refs
- sys/dev/drm/radeon/radeon_combios.c:2739-2763 (parse)
- sys/dev/drm/radeon/radeon_combios.c:2748-2752, 2754-2760 (reg/mask assignment)
- sys/dev/drm/radeon/r100.c:345-364 (r100_pm_misc sink)
- sys/dev/drm/radeon/r100.c:4125-4138 (MM_INDEX slow path)
- sys/dev/drm/radeon/radeon_pm.c:219-241 (trigger)
- sys/dev/drm/radeon/atombios.h:7620 (0x4 = VOLTAGE_DROP_SUPPORT)
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 authored against pristine sys/ and apply-verified on the guest's /usr/src copy (fix_apply.log: hunk #1 succeeded at 2746); runtime baseline/patched comparison impossible on this guest (no radeon hardware), so fix validation is apply+review only
fix.diff; fix_apply.log
Confirmed kernel references
Detail
Exploit chain
crafted VBIOS on pre-AVIVO mobility board -> combios powerplay parse at load (misc bit 0x4 = ATOM_PM_MISCINFO_VOLTAGE_DROP_SUPPORT enables both parse and sink) -> first PM clock transition executes r100_pm_misc() RREG32/WREG32 RMW at BIOS-chosen offset (up to 0x3FFFC via MM_INDEX) with BIOS-chosen mask -> device register corruption (further GART/aperture abuse would chain via unprivileged DRM command submission)
Evidence (decisive lines)
run.log: '[slow] WREG32: MM_INDEX <- 0x0003fffc (>= rmmio_size 0x10000) -> indexed write OUTSIDE the MMIO BAR' + 'PRIMITIVE PROVEN' line; VERDICT.md path:line trace; fix_apply.log proves fix.diff applies to pristine /usr/src
PoC changes
harness replicates kernel parse + sink 1:1 (no seed existed); fixed a missing
Verified recommended fix
Validate gpio.reg < 0x10000 and mask bit < 32 in both rev paths of the combios powerplay voltage-GPIO parse; mark the record invalid otherwise (fix.diff)
Verdict
Source-certain: radeon_combios_get_power_modes() takes the voltage-GPIO MMIO register index raw from the VBIOS (RBIOS16(...)*4, no range check, radeon_combios.c:2748-2760) and r100_pm_misc() (r100.c:352-364) later executes RREG32/WREG32 at that offset on every PM clock transition; offsets >= rmmio_size (64KB BAR) are re-routed by r100_mm_wreg_slow() into RADEON_MM_INDEX with the raw attacker offset (0x3FFFC max) -> out-of-BAR indexed device register RMW; in-range values give an arbitrary in-BAR register RMW with a BIOS-chosen mask bit. Harness (kernel logic replicated 1:1) proves the primitive deterministically: BIOS word 0xffff -> MM_INDEX 0x3fffc. Kernel-side execution not observable: audit guest is virtio-only, 0 radeon devices (env.txt), so runtime reproduction is impossible on this host. Expected impact on target hw: arbitrary GPU-register corruption incl. clock/aperture/window registers (GPU hang/brick; potential second-stage GART abuse by an unprivileged DRM client). Not host kernel memory corruption by itself.
No comments yet.