VBIOS-supplied indices into clock-info arrays OOB read in processpptables.c
- File:
sys/dev/drm/amd/powerplay/hwmgr/processpptables.c - Lines: 1099β1107 (UVD), 1131β1139 (VCE), 1576β1587 (VCE state), 923β935 (v2 state walk)
- Severity: High
- CVSS:
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H - CWE: CWE-125 Out-of-bounds Read
- Confidence: certain
Summary
In processpptables.c, AMDGPU PowerPlay table parsers index flexible-length
arrays using UCHAR indices taken directly from VBIOS records and never compare
those indices against the array's own length field. A crafted VBIOS drives the
pointer arbitrarily far past the array β and ultimately past the mapped BIOS
image β yielding OOB reads of kernel memory whose values are then copied into
kzalloc'd hwmgr state and dereferenced as clock/voltage pointers.
Root cause
All of the indexed accesses trust the per-record UCHAR index with no bound check against the array's own count field:
get_uvd_clock_voltage_limit_table,processpptables.c:1099-1107:&array->entries[table->entries[i].ucUVDClockInfoIndex]βarrayis aUVDClockInfoArray *with real lengtharray->ucNumEntries(pptable.h:628-631), index never compared.get_vce_clock_voltage_limit_table,processpptables.c:1131-1139:&array->entries[table->entries[i].ucVCEClockInfoIndex]β same pattern;VCEClockInfoArray.ucNumEntriesis the bound (pptable.h:582-585), never checked.get_vce_state_table_entry,processpptables.c:1576-1587: outeriis bounded only by the VBIOS-suppliednumEntries, and the inner&vce_clock_info_array->entries[record->ucVCEClockInfoIndex]plus*clock_info = clock_arrays->clockInfo + clockInfoIndex*ucEntrySizewhereclockInfoIndex(=record->ucClockInfoIndex & 0x3F) is never checked againstclock_arrays->ucNumEntries.pp_tables_get_entry(v2 path),processpptables.c:923-935:pstate_entry_v2->nonClockInfoIndex * pnon_clock_arrays->ucEntrySizeandpstate_entry_v2->clockInfoIndex[i] * pclock_arrays->ucEntrySizeβ both UCHAR indices unbounded against the correspondingucNumEntries.pp_tables_get_entry(legacy path),processpptables.c:944-957: same defect forucNonClockStateIndexanducClockStateIndices[i].
Enabler: get_powerplay_table (processpptables.c:827-849) stores
hwmgr->soft_pp_table_size but it is never consulted anywhere in this file as
a bound.
Threat
Local. Precondition: attacker controls the GPU VBIOS atom PowerPlay/FirmwareInfo tables. Two realistic placements:
- Host root or physical access flashes a malicious VBIOS.
- SR-IOV / virtualization where a malicious or compromised host feeds a guest's
amdgpu driver a crafted atom BIOS image (
smu_atom_get_data_tableatsmu_helper.c:660-673returnsbios + data_startstraight from the per-deviceatom_context).
Impact: OOB read past the mapped BIOS region β panic if the next page is
unmapped, otherwise attacker-influenced kernel bytes are dereferenced as
clock/voltage pointers and later followed by hwmgr code, enabling controlled
memory corruption. The VCE state entry path additionally hands the OOB-derived
pointer back to the caller as *clock_info, broadening the deref surface.
Exploit / PoC
A PoC is a malicious VBIOS atom data table, not a userspace program (this surface has no syscall entry point). Steps:
- Dump a reference
PowerPlayInfoatom table from a target AMD GPU. - Forge a minimal PowerPlay table whose extended header
(
ATOM_PPLIB_EXTENDEDHEADER) advertises a VCE table offset; build the VCE table such thatVCEClockInfoArray.ucNumEntries = 1but everyATOM_PPLIB_VCE_Clock_Voltage_Limit_Record.ucVCEClockInfoIndex = 0xFF. - Write the modified BIOS into the GPU ROM (or, for SR-IOV, supply it to the guest atom context).
- Bind
amdgpuand observe the driver-load path:init_clock_voltage_dependencyβget_vce_clock_voltage_limit_tablereadsarray->entries[255]atprocesspptables.c:1132, landing255*sizeof(VCEClockInfo)past a 1-entry array β typically outside the mapped BIOS region, producing an immediate kernel page-fault/panic (DoS proof) or, if the adjacent page is mapped, dereference of arbitrary kernel bytes asentry->ucEVClkHigh/usEVClkLowetc.
Recommended fix
Bounds-check every index against the corresponding array's ucNumEntries and
validate the pointer stays within soft_pp_table_size. Pattern:
--- a/sys/dev/drm/amd/powerplay/hwmgr/processpptables.c
+++ b/sys/dev/drm/amd/powerplay/hwmgr/processpptables.c
@@ -1128,6 +1128,11 @@ static int get_vce_clock_voltage_limit_table(struct pp_hwmgr *hwmgr,
return -ENOMEM;
vce_table->count = table->numEntries;
+ if (array->ucNumEntries == 0)
+ return -EINVAL;
for (i = 0; i < table->numEntries; i++) {
+ if (table->entries[i].ucVCEClockInfoIndex >= array->ucNumEntries)
+ return -EINVAL;
const VCEClockInfo *entry = &array->entries[table->entries[i].ucVCEClockInfoIndex];
Apply the identical pattern to:
get_uvd_clock_voltage_limit_table(processpptables.c:1099-1107)get_vce_state_table_entry(processpptables.c:1576-1587)pp_tables_get_entryv2 path (processpptables.c:923-935)pp_tables_get_entrylegacy path (processpptables.c:944-957)
Related findings
- DF-1469 (sibling): inflated
ucNumEntriescount OOB in same file. - DF-1470 (sibling): unvalidated VBIOS USHORT table offsets in same file.
- Part of the recurring "VBIOS offset/index OOB" family (radeon_atombios, radeon_combios, amdgpu_atombios, bios_parser, bios_parser2, intel_bios).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1468 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | marker-redzone replica of processpptables UVD clock-info index OOB read | 5.9 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o harness harness.c | 107 B | view raw |
| run.sh | run-script | ./harness | 60 B | view raw |
| build.log | build-log | final successful build, full output | 78 B | view raw |
| run.log | run-log | decisive run, full output | 789 B | view raw |
| fix.diff | suggested-fix | add ucUVDClockInfoIndex >= array->ucNumEntries check (mirror at sibling sites) | 931 B | view raw |
| fix_module_proof.txt | fix-build-proof | processpptables.o produced, amdgpu.ko linked, 0 errors | 269 B | view raw |
| fix_module_build.log | fix-build-log | module build excerpt under -Werror | 16.5 KB | view raw |
| env.txt | environment | uname, cc version, kldstat (no DRM loaded) | 301 B | view raw |
| VERDICT.md | verdict | full narrative: mechanism, reachability, harness, fix | 2.1 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-1468 β VBIOS indices into clock-info arrays OOB read (processpptables.c)
Verdict: REPRODUCED (source-level + harness) β latent amdgpu-powerplay bug, heap OOB read
The bug
sys/dev/drm/amd/powerplay/hwmgr/processpptables.c. Representative site
get_uvd_clock_voltage_limit_table, lines 1099-1107:
for (i = 0; i < table->numEntries; i++) {
const UVDClockInfo *entry =
&array->entries[table->entries[i].ucUVDClockInfoIndex]; /* :1101 OOB */
uvd_table->entries[i].vclk = (entry->ucVClkHigh << 16) | entry->usVClkLow;
...
}
A UCHAR index (ucUVDClockInfoIndex / ucVCEClockInfoIndex /
ucClockInfoIndex / nonClockInfoIndex) taken directly from the VBIOS is
used to index a flex array whose real length is the sibling ucNumEntries
field, but the index is never compared to ucNumEntries. A crafted
index reads arbitrarily far past the array into adjacent kernel/VBIOS memory.
Sites: :1099-1107 (UVD), :1131-1139 (VCE), :1576-1587 (VCE state),
:923-935 (v2 state walk). soft_pp_table_size is stored (:844) but never
used as a bound. Enabler: malicious VBIOS flash or SR-IOV guest atom context.
Harness proof
Marker-redzone harness (the read returns bytes it was never entitled to):
array->ucNumEntries = 1 (the real length) In-bounds ucUVDClockInfoIndex=0 (< ucNumEntries) -> vclk=0x112233 (OK) OOB ucUVDClockInfoIndex=1 (>= ucNumEntries) -> vclk=0xbbbeef read at entries[1] returned the redzone marker (0xbb/0xbeef): YES -> OOB read proven RESULT: heap OOB read CONFIRMED (processpptables.c:1101 pattern)
Fix
fix.diff adds if (table->entries[i].ucUVDClockInfoIndex >=
array->ucNumEntries) return -EINVAL; before the index deref in the
representative UVD site. The same bound check must be mirrored at the sibling
VCE / VCE-state / v2-state-walk sites.
Module build validation (Phase 8)
All 8 amdgpu fixes applied (three touch processpptables.c: DF-1468, 1469,
1470 β all apply together cleanly); amdgpu.ko built under -Werror:
processpptables.o (12504 bytes) produced, 0 errors, amdgpu.ko linked.
Fix verification
fixedVALIDATED via module build: fix.diff applied cleanly alongside DF-1469 and DF-1470 (three diffs all touch processpptables.c and apply together); amdgpu.ko built under -Werror with 0 errors; processpptables.o (12504 bytes) produced, amdgpu.ko linked. Runtime before/after not possible (no AMD GPU HW).
baseline (harness): OOB ucUVDClockInfoIndex=1 -> vclk=0xbbbeef (redzone marker); in-bounds vclk=0x112233 patched (module build): OK processpptables.o (12504 bytes); amdgpu.ko = 3741488 bytes; error count: 0; AMDGPU_DONE
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- p
- o
- w
- e
- r
- p
- l
- a
- y
- /
- h
- w
- m
- g
- r
- /
- p
- r
- o
- c
- e
- s
- s
- p
- p
- t
- a
- b
- l
- e
- s
- .
- c
- :
- 1
- 0
- 9
- 9
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- p
- o
- w
- e
- r
- p
- l
- a
- y
- /
- h
- w
- m
- g
- r
- /
- p
- r
- o
- c
- e
- s
- s
- p
- p
- t
- a
- b
- l
- e
- s
- .
- c
- :
- 1
- 1
- 0
- 1
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- p
- o
- w
- e
- r
- p
- l
- a
- y
- /
- h
- w
- m
- g
- r
- /
- p
- r
- o
- c
- e
- s
- s
- p
- p
- t
- a
- b
- l
- e
- s
- .
- c
- :
- 1
- 1
- 3
- 1
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- p
- o
- w
- e
- r
- p
- l
- a
- y
- /
- h
- w
- m
- g
- r
- /
- p
- r
- o
- c
- e
- s
- s
- p
- p
- t
- a
- b
- l
- e
- s
- .
- c
- :
- 1
- 5
- 7
- 6
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- p
- o
- w
- e
- r
- p
- l
- a
- y
- /
- h
- w
- m
- g
- r
- /
- p
- r
- o
- c
- e
- s
- s
- p
- p
- t
- a
- b
- l
- e
- s
- .
- c
- :
- 9
- 2
- 3
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- p
- o
- w
- e
- r
- p
- l
- a
- y
- /
- h
- w
- m
- g
- r
- /
- p
- r
- o
- c
- e
- s
- s
- p
- p
- t
- a
- b
- l
- e
- s
- .
- c
- :
- 8
- 4
- 4
Detail
Exploit chain
Blocked by dead-code-on-guest hard blocker (valid): amdgpu powerplay not in GENERIC and no AMD GPU HW on the guest; kernel path cannot trigger end-to-end. Primitive proven at harness level (OOB read past ucNumEntries returns adjacent bytes). Realistic runtime impact with amdgpu HW + crafted VBIOS / SR-IOV atom context is kernel info leak + corruption (read values drive MMIO writes, leaked via sysfs). Evidence pack: findings/poc/DF-1468/ (harness.c).
Evidence (decisive lines)
array->ucNumEntries = 1 (the real length) sizeof(UVDClockInfo) = 6 In-bounds ucUVDClockInfoIndex=0 (< ucNumEntries) -> vclk=0x112233 (OK) OOB ucUVDClockInfoIndex=1 (>= ucNumEntries) -> vclk=0xbbbeef Worst case ucUVDClockInfoIndex=255 -> kernel reads at byte offset 1530 past entries[0] read at entries[1] returned the redzone marker (0xbb/0xbeef): YES -> OOB read proven RESULT: heap OOB read CONFIRMED (processpptables.c:1101 pattern) RUN_EXIT=0
PoC changes
Authored harness.c (initial guard-page version rewritten to robust marker-redzone approach after the guest's mmap redzone proved non-deterministic), build.sh, run.sh, fix.diff (check ucUVDClockInfoIndex >= array->ucNumEntries), VERDICT.md, manifest.json. fix.diff regenerated via copy+edit+diff.
Verified recommended fix
In get_uvd_clock_voltage_limit_table (processpptables.c:1099-1107), before the index deref add if (table->entries[i].ucUVDClockInfoIndex >= array->ucNumEntries) return -EINVAL;. Mirror the same bound check at the sibling VCE/VCE-state/v2-state-walk sites. Matches finding proposal. Full diff in findings/poc/DF-1468/fix.diff.
Verdict
REPRODUCED. processpptables.c uses VBIOS UCHAR indices (ucUVDClockInfoIndex/ucVCEClockInfoIndex/ucClockInfoIndex/nonClockInfoIndex) to index flex arrays whose real length is the sibling ucNumEntries field, with NO comparison (representative site :1099-1107). A crafted index reads past the array into adjacent kernel/VBIOS memory, deref'd as clock/voltage values. Confirmed by marker-redzone harness: index>=ucNumEntries reads the redzone marker (vclk=0xbbbeef from slot 1 when only 1 real entry). soft_pp_table_size (:844) is available but never used as a bound.
No comments yet.