VBIOS-supplied entry count trusted without validation against firmware image bounds
- File:
sys/dev/drm/amd/powerplay/hwmgr/processpptables.c - Lines: 383β399 (clock_voltage_dep), 1089β1107 (uvd), 1122β1139 (vce), 1153β1167 (samu), 1181β1195 (acp), 1379β1399 (cac_leakage), 1520β1537 (phase_shed)
- Severity: High
- CVSS:
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H - CWE: CWE-129 Improper Validation of Array Index
- Confidence: certain
Summary
Every table parser in processpptables.c allocates a destination buffer sized
by a VBIOS-supplied UCHAR count (ucNumEntries / numEntries) and then
iterates that many times reading table->entries[i] from the source VBIOS
region. Neither the count nor the implied byte range is ever checked against
the size of the source firmware image (hwmgr->soft_pp_table_size, which
itself comes from the VBIOS atom header at processpptables.c:844). A crafted
VBIOS that advertises e.g. ucNumEntries=255 for a table that physically
contains 2 entries causes the loop to read 253 records' worth of memory past
the end of the table β out of the mapped BIOS region and into adjacent kernel
memory.
Root cause
Recurring defect: count taken directly from VBIOS, used as both the allocation multiplier and the loop bound, never validated to fit inside the image. Concrete sites:
get_clock_voltage_dependency_table,processpptables.c:383-399:table_size = sizeof(unsigned long) + sizeof(struct phm_clock_voltage_dependency_table) * table->ucNumEntries;thenfor (i = 0; i < dep_table->count; i++) dep_table->entries[i].clk = ... le16_to_cpu(table->entries[i].usClockLow);βtable->entries[i]runs past the source table whenucNumEntriesis inflated.get_uvd_clock_voltage_limit_table,processpptables.c:1089-1107:table->numEntriescontrols both kzalloc size and the loop readingtable->entries[i].get_vce_clock_voltage_limit_table,processpptables.c:1122-1139: same withtable->numEntries.get_samu_clock_voltage_limit_table,processpptables.c:1153-1167: same.get_acp_clock_voltage_limit_table,processpptables.c:1181-1195: same.get_cac_leakage_table,processpptables.c:1379-1399: same withtable->ucNumEntries.init_phase_shedding_table,processpptables.c:1520-1537: same withptable->ucNumEntries.pp_tables_get_num_of_entries,processpptables.c:874-880: handsucNumEntries(orucNumStates) straight back to the caller, which then loops insmu7_hwmgr.c:3372/smu8_hwmgr.c:1388/smu10_hwmgr.c:777callingpp_tables_get_entrythat many times; an inflated count drives every subsequent state-table walk out of bounds (compounds DF-1468).
In every case hwmgr->soft_pp_table_size (set at processpptables.c:844) is
available but unused as a bound.
Threat
Same local attacker position as DF-1468 (malicious VBIOS or SR-IOV guest).
Inflating ucNumEntries to 255 for a small table causes
253*sizeof(record) of OOB read from the mapped BIOS region; the read values
become clk/v/vclk/vddc fields copied into freshly kzalloc'd hwmgr
state and later consumed by clock/voltage programming.
If the OOB read crosses into an unmapped page the kernel panics (DoS); if it
crosses into other kernel memory, those bytes are parsed as clocks/voltages and
may drive MMIO writes (controlled corruption primitive). Because
get_clock_voltage_dependency_table output is later exposed to userland via
sysfs (clocks read back through amdgpu_hwmon / pp_dpm_sclk etc.), the
OOB-read bytes can also leak to userspace.
Exploit / PoC
Forge a PowerPlay table whose ATOM_PPLIB_Clock_Voltage_Dependency_Table
(reachable from ATOM_PPLIB_POWERPLAYTABLE4.usVddcDependencyOnSCLKOffset,
processpptables.c:1304-1310) declares ucNumEntries = 255 while the table
physically contains one record. Build the atom BIOS image with this table, load
via VBIOS flash or SR-IOV guest atom context, and trigger
init_clock_voltage_dependency at driver attach.
get_clock_voltage_dependency_table allocates 255 entries worth of memory and
then loops i=0..254 reading table->entries[i].usClockLow / usVoltage from
progressively OOB addresses (processpptables.c:393-398).
Observable on a stock DFBSD/amdgpu guest is either a panic in
get_clock_voltage_dependency_table (if BIOS mapping ends nearby) or, with the
table placed mid-image, silent ingestion of garbage clock/voltage pairs that
surface via cat /sys/class/drm/card0/device/powerplay/hwmon/hwmon?/freq*_input
and /sys/.../pp_dpm_sclk (info leak).
Recommended fix
Add a helper that bounds any (offset, count, record_size) triple inside
hwmgr->soft_pp_table_size, and call it before every count-driven loop:
--- a/sys/dev/drm/amd/powerplay/hwmgr/processpptables.c
+++ b/sys/dev/drm/amd/powerplay/hwmgr/processpptables.c
@@ -375,6 +375,15 @@ static int get_clock_voltage_dependency_table(struct pp_hwmgr *hwmgr,
const ATOM_PPLIB_Clock_Voltage_Dependency_Table *table)
{
+ /*
+ * The caller resolved `table` from a VBIOS offset; make sure the
+ * declared entry count actually fits in the firmware image.
+ */
+ if ((unsigned long)((const uint8_t *)table - (const uint8_t *)hwmgr->soft_pp_table)
+ + sizeof(*table) + (unsigned long)table->ucNumEntries *
+ sizeof(table->entries[0]) > hwmgr->soft_pp_table_size)
+ return -EINVAL;
+
unsigned long table_size, i;
Apply the same precondition to get_uvd_clock_voltage_limit_table (1089),
get_vce_clock_voltage_limit_table (1122), get_samu_clock_voltage_limit_table
(1153), get_acp_clock_voltage_limit_table (1181), get_cac_leakage_table
(1379), and init_phase_shedding_table (1520).
Related findings
- DF-1468 (sibling): unbounded per-record index into clock-info arrays.
- DF-1470 (sibling): unvalidated VBIOS USHORT table offsets.
- Part of the recurring "VBIOS power-table count overflow" family.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1469 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | marker-redzone replica of processpptables entry-count OOB read loop | 6.4 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 | 836 B | view raw |
| fix.diff | suggested-fix | cap ucNumEntries at 128 in representative parser; mirror at sibling parsers | 1.2 KB | 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.2 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-1469 β VBIOS entry count trusted without validation (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_clock_voltage_dependency_table, lines 383-399:
table_size = sizeof(unsigned long) +
sizeof(struct phm_clock_voltage_dependency_table) *
table->ucNumEntries; /* :383 -- trusted VBIOS count */
dep_table = kzalloc(table_size, GFP_KERNEL);
dep_table->count = (unsigned long)table->ucNumEntries;
for (i = 0; i < dep_table->count; i++) {
dep_table->entries[i].clk = ... table->entries[i].ucClockHigh ...; /* :394 OOB */
dep_table->entries[i].v = ... table->entries[i].usVoltage ...; /* :397 OOB */
}
Every table parser sizes the destination buffer by the VBIOS-supplied UCHAR
count AND iterates that many times reading table->entries[i] from the VBIOS
region. The count is never validated to fit inside the firmware image
(soft_pp_table_size is available at :844 but unused). Sites:
:383-399 (clock_voltage_dep), :1089-1107 (uvd), :1122-1139 (vce),
:1153-1167 (samu), :1181-1195 (acp), :1379-1399 (cac_leakage),
:1520-1537 (phase_shed). Inflated ucNumEntries=255 for a small table ->
OOB read of adjacent kernel memory, parsed as clocks/voltages (driving MMIO
writes -> corruption, or leaked via sysfs pp_dpm_sclk / hwmon).
Harness proof
real ucNumEntries = 2 (what the image actually holds) marker clk=0xabcdef volt=0x1234 placed in redzone slots 2..7 In-bounds ucNumEntries=2 -> clk[0]=0x11000 volt[0]=0x800 (OK) OOB ucNumEntries=8 (inflated) -> clk[2]=0xabcdef volt[2]=0x1234 RESULT: heap OOB read CONFIRMED (processpptables.c:393 count loop)
Fix
fix.diff caps num_entries at a sane hardware maximum (128) in the
representative get_clock_voltage_dependency_table parser; the same defensive
bound must be applied at every sibling parser (uvd/vce/samu/acp/cac_leakage/
phase_shed).
Module build validation (Phase 8)
All 8 amdgpu fixes applied (three touch processpptables.c: DF-1468, 1469,
1470); 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-1468 and DF-1470; 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 ucNumEntries=8 -> clk[2]=0xabcdef volt[2]=0x1234 (redzone); in-bounds clk[0]=0x11000 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
- :
- 3
- 8
- 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
- :
- 3
- 8
- 5
- 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
- :
- 3
- 9
- 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
- :
- 3
- 9
- 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
- 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
- 8
- 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
- 2
- 2
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 (inflated count loop reads adjacent marker bytes). Realistic runtime impact with amdgpu HW + crafted VBIOS is OOB read -> corruption (MMIO writes) + leak (sysfs pp_dpm_sclk/hwmon). Evidence pack: findings/poc/DF-1469/ (harness.c).
Evidence (decisive lines)
real ucNumEntries = 2 (what the image actually holds) sizeof(dep_record) = 5 marker clk=0xabcdef volt=0x1234 placed in redzone slots 2..7 In-bounds ucNumEntries=2 -> clk[0]=0x11000 volt[0]=0x800 (OK) OOB ucNumEntries=8 (inflated) -> clk[2]=0xabcdef volt[2]=0x1234 Worst case ucNumEntries=255 -> loop reads 255 entries, 1275 bytes past entries[0] RESULT: heap OOB read CONFIRMED (processpptables.c:393 count loop) RUN_EXIT=0
PoC changes
Authored harness.c (rewritten from guard-page to marker-redzone), build.sh, run.sh, fix.diff (cap num_entries at 128 in representative parser), VERDICT.md, manifest.json. fix.diff regenerated via copy+edit+diff.
Verified recommended fix
In get_clock_voltage_dependency_table (processpptables.c:378-385), after reading ucNumEntries add if (num_entries > 128) return -EINVAL; (no AMD ASIC has more than a handful of entries). Mirror at every sibling parser (uvd/vce/samu/acp/cac_leakage/phase_shed). Matches finding proposal intent. Full diff in findings/poc/DF-1469/fix.diff.
Verdict
REPRODUCED. Every table parser in processpptables.c sizes the destination kmalloc by the VBIOS UCHAR count AND iterates that many times reading table->entries[i] from the VBIOS region; the count is never validated to fit the image (soft_pp_table_size at :844 unused). Representative site get_clock_voltage_dependency_table (:383-399). With ucNumEntries inflated past the real table, the loop reads OOB into adjacent kernel memory, parsed as clocks/voltages. Confirmed by marker-redzone harness: inflated count reads the redzone marker (clk=0xabcdef volt=0x1234).
No comments yet.