rs780_dpm: unchecked VBIOS pplib offsets/indices/strides in rs780_parse_power_table give wild OOB kernel reads past the kmalloc'd BIOS image
Summary
The PowerPlayInfo walk trusts only attacker-supplied fields: state array offset+i*entrySize (~64.8KB reach), nonClockInfoIndex*size (up to 65025B), clockInfoIndex*size (65025B). atom_parse_data_header validates nothing and the BIOS reader allocates exactly the ROM size - far indices page-fault, near indices silently expose adjacent kernel heap via printed power-state fields. Harness-verified with canary allocator: 16B-past redzone poison ingested into rps class/caps, far variant SIGSEGVs in a guard page. RS780 twin of DF-4190/DF-4146. Bounds-check-with-data_size fix (radeon.ko builds -Werror).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4492 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.6 KB | β raw | |
| VERDICT.md | β | 4.4 KB | β raw | |
| harness.c | β | 48.7 KB | view raw | |
| build.sh | β | 65 B | view raw | |
| run.sh | β | 121 B | view raw | |
| build.log | β | 2.0 KB | view raw | |
| run.log | β | 7.0 KB | view raw | |
| env.txt | β | 390 B | view raw | |
| fix.diff | β | 4.5 KB | view raw | |
| radeon_fix_apply.log | β | 435 B | view raw | |
| radeon_module_build.log | β | 139.5 KB | view raw | |
| verdict.json | β | 3.9 KB | view raw |
DF-4492 β rs780_dpm.c: unchecked VBIOS pplib offsets/indices β wild OOB kernel reads
What
rs780_parse_power_table() (sys/dev/drm/radeon/rs780_dpm.c:789-844) walks the
VBIOS PowerPlayInfo table using only attacker-supplied fields β none are
bounds-checked against the table or the BIOS image:
- state array walk
:812-816:bios + data_offset + usStateArrayOffset + i*ucStateEntrySize(u16 offset + up to 254Γ255 stride β up to ~64 KB past the table start) - non-clock info
:817-821:usNonClockInfoArrayOffset + ucNonClockStateIndex*ucNonClockSize(u8 index Γ u8 stride β up to 65025 bytes) - clock info
:822-827:usClockInfoArrayOffset + ucClockStateIndices[0]*ucClockInfoSize(up to 65025 bytes)
atom_parse_data_header() (atom.c:1366) validates nothing beyond a non-zero
master-table pointer, and the radeon BIOS reader (radeon_bios.c:98)
kzallocs exactly the ROM image size β so the reads run past the kmalloc'd
BIOS buffer into adjacent kernel heap. The ingested bytes land in
rps->class/class2/caps/vclk/dclk and ps->sclk_low/sclk_high/min_voltage
and are (a) printed to the console / debugfs via
rs780_dpm_print_power_state() (:944-948) and (b) programmed into PM
registers.
Sumo twin of DF-4190, trinity twin of DF-4146.
Threat model
Crafted/malicious VBIOS image on an RS780/RS880 IGP (flashed ROM, replaced card, or a passed-through GPU in a VM). Far indices fault the kernel (page-fault panic = DoS); near indices silently ingest adjacent kernel-heap bytes into user-visible state (info leak class).
Reproduce (on the DragonFly audit guest, or any cc)
./build.sh # cc -O2 -fno-delete-null-pointer-checks, baseline + -DFIX_ALL ./run.sh # modes 4492 (redzone poison ingestion) and 4492g (guard page)
Expected baseline (see run.log):
- mode 4492: three
*** CANARY ... heap OOB access ***reports andrps[0].class = 0xa5a5 (== canary poison: OOB READ CONFIRMED),caps/vclk = 0xa5a5a5a5, exit=98 - mode 4492g:
*** SIGNAL 11 at address 0x8004c4280 ***β SIGSEGV exactly in the PROT_NONE guard page behind the 16 KB BIOS image replica, exit=99
Expected with harness_fixed: both modes print
DRM_ERROR: ... walks past the table and exit=0.
Kernel-level reproduction is hardware-gated (no RS780-class GPU exists in the QEMU guest); the harness is a verbatim replica of the parse code β see VERDICT.md.
Fix
fix.diff (also in this directory): request the table size from
atom_parse_data_header() and bound every walk by it; reject
ucStateEntrySize < 2. Validated: applies to the guest's pristine
/usr/src, radeon.ko builds under -Werror (radeon_module_build.log,
MAKE_RC=0).
DF-4492 β VERDICT
Finding: rs780_parse_power_table() trusts unvalidated VBIOS pplib offsets/indices/strides β wild OOB kernel reads past the kmalloc'd BIOS image (kernleak class).
Status: REPRODUCED at logic level (verbatim replica), 2 attempts. Kernel-level trigger hardware-gated: the QEMU audit guest has no RS780/RS880-class ATI IGP, so the radeon driver never probes DPM there (family precedent DF-4190/DF-4146/DF-4124 were verified the same way).
How it was reproduced
harness.c carries the VERBATIM code of rs780_parse_power_table()
(rs780_dpm.c:789-844), rs780_parse_pplib_non_clock_info() (:717-745) and
rs780_parse_pplib_clock_info() (:747-787) with only MMIO stubbed; the
kernel heap is emulated by a canary allocator (64-byte 0xA5 redzones both
sides of every allocation). The VBIOS image is a crafted 16 KB buffer
using the packed ATOM structure definitions from pptable.h (offsets
faithful: ucNumStates@+5, entry sizes +6..+8, array offsets +9/+11/+13).
mode 4492 (near OOB β poison ingestion, leak flavor)
Crafted table claims usNonClockInfoArrayOffset = 10336 with
ucNonClockStateIndex = 250, ucNonClockSize = 24 β the 24-byte
_ATOM_PPLIB_NONCLOCK_INFO load lands at bios+16336, 16 bytes past the
16 KB image, inside the rear redzone. Observed (run.log):
*** CANARY: nonClockInfo[250] ... at 0x8004c4050 (len 24) is OUTSIDE every live allocation -> heap OOB access *** *** CANARY: clockInfo[250] ... OUTSIDE every live allocation -> heap OOB access *** (same landing via 250*20) *** CANARY: state[7] of the ucStateEntrySize=254 walk ... OUTSIDE every live allocation *** (16318+7*254, ~1.7 KB past) rs780_parse_power_table() = 0 num_ps=1 rps[0].class = 0xa5a5 (== canary poison: OOB READ CONFIRMED) rps[0].caps = 0xa5a5a5a5 (== canary poison) rps[0].vclk = 0xa5a5a5a5 (== canary poison)
In the kernel the same loads read whatever follows the kzalloc'd BIOS
image in the kernel heap; class/caps/vclk/sclk flow to
rs780_dpm_print_power_state() (:944-948, console/debugfs) β info leak
class; far variants fault (DoS). Note the poisoned class = 0xa5a5 even
carries bit0 (ATOM_PPLIB_CLASSIFICATION_BOOT), showing the OOB data also
selects which state becomes boot_ps.
mode 4492g (far OOB β deterministic fault)
BIOS image mapped as 16 KB writable + PROT_NONE guard page;
ucClockStateIndices[0] = 66, ucClockInfoSize = 255 β load at
bios+17022 = 638 bytes past the image, inside the guard page. Observed:
clockInfo[66] (x255) lands at 0x8004c427e = 638 bytes PAST the image end (inside the guard page) *** SIGNAL 11 at address 0x8004c4280 *** exit=99
Maximum reach with u8 fields: 255Γ255 = 65025 bytes past the table on the index walks, 254Γ255 β 64.8 KB on the state walk, plus the u16 array offsets β comfortably past a 64 KB BIOS allocation.
Why this is real in the kernel
atom_parse_data_header()(sys/dev/drm/radeon/atom.c:1366-1385) returns the raw u16 master-table offset with no bounds validation ofidx/table extent vsctx->bios.radeon_read_bios()(radeon_bios.c:78-110) allocatesrdev->bioswith exactly the ROM size;atom_context->biospoints at it (atom.c:1283).- The rs780 parse (pass-1 sibling audits confirmed the same shape in sumo/trinity/kv/si) reads via those raw pointers with no table-extent math anywhere.
Fix validation
fix.diff requests the table size (&data_size) from
atom_parse_data_header() and bounds every walk by it via
rs780_pplib_walk_ok() (rejects with consistent cleanup). Applied to the
guest's pristine /usr/src copy:
radeon_fix_apply.log: all 7 hunks applied (Plan A).radeon_module_build.log:radeon.kolinks under -Werror, MAKE_RC=0.harness_fixedmodes 4492/4492g:DRM_ERROR: ... walks past the table, exit=0, no canary violations.
Guest source reverted (md5 verified) and guest reset (vm.sh reset
with-src) after the run.
Attempts
- First run: crafted landings fell inside the 16 KB image (intra-image overread only) β re-crafted offsets/strides to land in the redzone / guard page.
- Decisive run captured in run.log.
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
fixedharness_fixed 4492/4492g reject the crafted indices (DRM_ERROR, -EINVAL, exit 0); combined fix builds radeon.ko under -Werror (MAKE_RC=0, radeon_module_build.log)
run.log fixed sections 'DRM_ERROR: nonClockInfoIndex 250 walks past the table' / 'clockInfoIndex 66 walks past the table', exit=0; radeon_fix_apply.log hunks 1-7; radeon_module_build.log MAKE_RC=0
Confirmed kernel references
Detail
Exploit chain
crafted VBIOS PowerPlayInfo (flashed ROM / replaced card / passed-through GPU) -> OOB read past BIOS allocation -> adjacent kernel-heap bytes ingested into rps->class/caps/vclk/sclk -> printed via rs780_dpm_print_power_state to console/debugfs (info leak); far indices -> kernel page fault (DoS); leaked BOOT-bit in class additionally steers boot_ps selection
Evidence (decisive lines)
run.log '===== mode 4492 =====': three CANARY heap-OOB reports + 'rps[0].class = 0xa5a5 (== canary poison: OOB READ CONFIRMED)', exit=98; '===== mode 4492g =====': 'SIGNAL 11 at address 0x8004c4280' inside the guard page, exit=99; fixed sections print 'DRM_ERROR: ... walks past the table', exit=0; radeon_module_build.log MAKE_RC=0 under -Werror
PoC changes
wrote harness from scratch (no seed): crafted bios image + verbatim parse replica; -fno-delete-null-pointer-checks used so NULL/wild derefs trap like the kernel instead of being optimized away
Verified recommended fix
request the table size from atom_parse_data_header and bounds-check every pplib walk (state array extent, nonClockInfoIndexucNonClockSize+entry, clockInfoIndexucClockInfoSize+entry) with consistent cleanup, as in fix.diff
Verdict
Reproduced at logic level with the verbatim replica (harness modes 4492/4492g): rs780_parse_power_table walks the VBIOS PowerPlayInfo using only attacker-supplied usStateArrayOffset+iucStateEntrySize (:812-816), ucNonClockStateIndexucNonClockSize (:817-821) and ucClockStateIndices[0]*ucClockInfoSize (:822-827) with no bounds checks anywhere (atom_parse_data_header does none); nonClockInfo[250] with a crafted u16 array offset lands 16 bytes past the 16 KB BIOS replica into the canary redzone and its poison is ingested verbatim into rps->class/caps/vclk (0xa5a5/0xa5a5a5a5) which print to console/debugfs via rs780_dpm_print_power_state (:944-948); the far variant (clockInfoIndex 66 x ucClockInfoSize 255) SIGSEGVs exactly in a guard page 638 bytes past the image (kernel: page-fault panic). Max reach ~65 KB past the kmalloc'd BIOS image per index walk, ~130 KB on the state walk. RS780 twin of DF-4190 (sumo) / DF-4146 (trinity). Kernel-level repro hardware-gated (no RS780 IGP in the QEMU guest). Fix proven: harness_fixed rejects the crafted indices; fix.diff builds radeon.ko under -Werror (MAKE_RC=0).
No comments yet.