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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3852 Β· 11 files
FileTypeDescriptionSize
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
README.md readme build/run/expected + hardware reproduction notes
↓ download 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-364 RMW + r100.c:4132-4138 slow-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().

VERDICT.md verdict full narrative with path:line trace
↓ download raw

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, via voltage_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_offset itself. 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

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

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 for DragonFly cc 8.3 default C standard (first build failed, see notes)

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.