SMU-firmware-fed DPM level count stored unclamped: vega12_setup_single_dpm_table fill loop writes past dpm_levels[16] β firmware-controlled linear heap overflow of the kzalloc'd vega12_hwmgr backend
Summary
vega12_get_number_of_dpm_level takes num_of_levels from smum_get_argument() (raw u32 the SMU firmware placed in the response register) and validates only > 0; the fill loop writes dpm_levels[i] for i < num past dpm_levels[16] (12-byte stores with firmware-chosen 32-bit values). dpm_table is the FIRST member of the kzalloc'd vega12_hwmgr backend, so the walk smashes all ten dpm tables, golden_dpm_table, registry_data... and past the allocation for counts of a few hundred (replica: 100 levels -> 54 bytes past a 636-byte object; uint32 max -> wild writes until page fault). The invariant is already asserted in find_highest_dpm_level but never enforced where the firmware value enters. Reached at every dpm enable on Vega12. vega20_hwmgr.c:466-485 carries the identical missing bound (twin). Same class as the VBIOS-fed smumgr count family but fed from the SMU message layer. Struct-geometry replica run on guest; fix compile-validated in batched build.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4054 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.7 KB | β raw | |
| VERDICT.md | β | 3.8 KB | β raw | |
| trigger.c | β | 3.6 KB | view raw | |
| build.sh | β | 88 B | view raw | |
| run.sh | β | 108 B | view raw | |
| run.log | β | 673 B | view raw | |
| build.log | β | 1.0 KB | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| fix_build.log | β | 1.9 KB | view raw | |
| verdict.json | β | 3.4 KB | view raw |
DF-4054 β vega12_setup_single_dpm_table: SMU-fed DPM level count unclamped
File: sys/dev/drm/amd/powerplay/hwmgr/vega12_hwmgr.c:507-530 (fill loop),
root cause at :481 / :518. Pass 2 (GLM 5.3). Hardware-gated
(requires Vega12 GPU); source-level certainty + host-replica arithmetic
proof + compile-validated fix.
THE BUG
static int vega12_get_number_of_dpm_level(...) /* :468 */
{
ret = smum_send_msg_to_smc_with_parameter(hwmgr,
PPSMC_MSG_GetDpmFreqByIndex, (clk_id << 16 | 0xFF));
...
*num_of_levels = smum_get_argument(hwmgr); /* raw u32 from SMU */
PP_ASSERT_WITH_CODE(*num_of_levels > 0, ...); /* ONLY > 0 checked */
return ret;
}
static int vega12_setup_single_dpm_table(...) /* :507 */
{
...
dpm_table->count = num_of_levels; /* :518 unclamped */
for (i = 0; i < num_of_levels; i++) { /* :520 */
... vega12_get_dpm_frequency_by_index(hwmgr, clk_id, i, &clk);
dpm_table->dpm_levels[i].value = clk; /* :525 OOB >= 16 */
dpm_table->dpm_levels[i].enabled = true; /* :526 OOB >= 16 */
}
}
smum_get_argument() returns a raw uint32 read from the SMU firmware
response register. Only > 0 is enforced; nothing bounds it against
MAX_REGULAR_DPM_NUMBER (16) β the size of dpm_levels[]
(vega12_hwmgr.h:96,107-110). The fill loop then performs a
firmware-controlled linear heap overflow of the kzalloc'd
struct vega12_hwmgr backend (vega12_hwmgr_backend_init :376):
12 bytes per store (enabled, value = SMU-fed 32-bit), marching
through the remaining 9 single_dpm_tables, pcie_table,
golden_dpm_table, registry_data, ... and past the end of the allocation
for large counts (uint32 max -> thousands of pages of sequential wild
writes -> guaranteed fatal fault).
The code knows the invariant β vega12_find_highest_dpm_level (:989)
asserts table->count <= MAX_REGULAR_DPM_NUMBER β but it is never
enforced where the firmware value enters
(vega12_setup_default_dpm_tables :553/566/579/592/605/618/631/644/655/666
call the filler for soc/gfx/mem/eclk/vclk/dclk/dcef/pixel/disp/phy).
Downstream readers then trust count: vega12_find_lowest_dpm_level
(:972 iterates i<count reading dpm_levels[i]),
vega12_apply_clocks_adjust_rules (:1921/1945 dpm_levels[count-1]),
vega12_get_profiling_clk_mask (:1525 count-1 as mask index).
Sibling vega20_hwmgr.c:466-485 (vega20_get_number_of_dpm_level) has
the identical missing bound β same-family twin (separate file).
Same bug class as the VBIOS-fed smumgr counts (DF-3908/3909, DF-4000, DF-4001, DF-4036) but fed from the SMU message layer, not the pptable.
REACHABILITY / THREAT
Requires a Vega12 GPU (Radeon Pro WX 8200 / Vega 12 Polaris-derived
dGPU, or Raven-adjacent ASICs driven by vega12_hwmgr) and SMU firmware
that reports num_of_levels > 16 β i.e. buggy or compromised PSP/SMU
firmware. The SMU is outside the kernel's trust boundary (it is an
independent on-die microcontroller with DMA); the audit files these as
firmware-driven kernel memory corruption. Not attacker-triggerable from
userspace on non-Vega12 hardware; guest has no AMD GPU at all.
REPLICA (host-side arithmetic proof)
trigger.c mirrors the fill loop with the real struct geometry:
./trigger 100 -> bytes written past end of object (guard band): 54
<-- HEAP OVERFLOW and gfx_table.count = 0x00000001 (the enabled=true
store of entry 16 smashing the next table's count field). Run on the
audit guest with cc (no AMD hardware needed β it is a struct-layout
replica, not a driver exercise).
FIX
fix.diff adds the missing upper bound in
vega12_get_number_of_dpm_level:
PP_ASSERT_WITH_CODE(*num_of_levels <= MAX_REGULAR_DPM_NUMBER, ...,
return -EINVAL);
Compile-validated in the batched single-fix kernel build
(fix_build.log, together with DF-4053).
DF-4054 β VERDICT
INCONCLUSIVE at runtime (hardware-gated), source-CERTAIN β the audit guest
has no AMD GPU, so vega12_hwmgr never instantiates. The defect is proven from
the source (line-accurate walk below) plus a struct-geometry replica
(trigger.c, run on the guest with cc β no GPU needed) demonstrating the exact
store arithmetic past dpm_levels[16]. Fix compile-validated in the batched
kernel build (fix_build.log, RC=0, kernel #1).
Root cause (line-accurate)
vega12_get_number_of_dpm_level (vega12_hwmgr.c:468-486):
- :473 sends PPSMC_MSG_GetDpmFreqByIndex with level index 0xFF (level-count query)
- :480 *num_of_levels = smum_get_argument(hwmgr) β a raw uint32 the SMU
firmware placed in the response register; NOTHING the kernel computed
- :481-483 the ONLY validation: > 0
vega12_setup_single_dpm_table (:507-530):
- :518 dpm_table->count = num_of_levels β stored unclamped
- :520-527 for (i = 0; i < num_of_levels; i++) ... dpm_table->dpm_levels[i].value = clk; ... .enabled = true;
dpm_levels is struct vega12_dpm_level[16] (vega12_hwmgr.h:96,107-110;
each level is {bool enabled; uint32 value; uint32 param1} = 12 bytes,
vega12_hwmgr.h:90-92). Any firmware value > 16 writes past the array with
firmware-chosen 32-bit values: the tables are the FIRST member
(dpm_table) of the kzalloc'd struct vega12_hwmgr backend
(:125-135, :307+), so the walk smashes gfx/mem/eclk/... tables' counts and
states, then golden_dpm_table, registry_data, vbios_boot_state,
mclk_latency_table, voltage tables, smc_state_table β and past the
allocation itself for counts of a few hundred (replica: 100 levels -> 54
bytes past a 636-byte object; uint32 firmware max -> sequential wild writes
until the first unmapped page -> fatal fault).
The code already trusts this invariant elsewhere:
- vega12_find_highest_dpm_level :989-991 asserts count <= MAX_REGULAR_DPM_NUMBER and clamps its return β the bound is known, just not enforced where the firmware value enters.
- vega12_find_lowest_dpm_level :972 iterates i < count (OOB read downstream).
- vega12_apply_clocks_adjust_rules :1921/1945 index dpm_levels[count-1].
- vega12_get_profiling_clk_mask :1525-1527 uses count-1 as a mask level.
Reached at every dynamic_state_management_enable (vega12_enable_dpm_tasks
:954 -> vega12_setup_default_dpm_tables :553/566/579/... for all ten clock
tables), i.e. amdgpu driver load / dpm enable on a Vega12 ASIC.
Why it is still a security finding
The SMU/PSP is an independent microcontroller outside the kernel's trust boundary (it owns its own firmware and can DMA); a buggy or compromised firmware returning a large level count converts into a kernel heap overflow with no further preconditions. Same class as the VBIOS-fed count family this audit already filed (DF-3908/3909 tonga/ci smumgr, DF-4000 iceland, DF-4001 fiji, DF-4036 polaris10) β this instance is fed from the SMU message layer. vega20_hwmgr.c:466-485 carries the identical missing bound (twin, separate file, not covered by this finding's fix).
Fix
fix.diff: PP_ASSERT_WITH_CODE(*num_of_levels <= MAX_REGULAR_DPM_NUMBER,
..., return -EINVAL) in vega12_get_number_of_dpm_level β reject impossible
counts at the boundary. Compiled cleanly in the batched build.
Classification
status=inconclusive (no Vega12 hardware on the guest β cannot execute the driver path), reproduced=0, impact=none (runtime), confidence=certain (source-level; struct geometry cross-checked against vega12_hwmgr.h and the replica arithmetic).
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
fixedFix compile-validated only (batched with DF-4053 in the kernel #1 build, RC=0); runtime A/B impossible on this guest β no Vega12 hardware, baseline never reproducible here either.
fix_build.log (nativekernel completed RC=0, installkernel completed, uname #1 running both fixes)
Confirmed kernel references
- sys/dev/drm/amd/powerplay/hwmgr/vega12_hwmgr.c:480
- sys/dev/drm/amd/powerplay/hwmgr/vega12_hwmgr.c:481
- sys/dev/drm/amd/powerplay/hwmgr/vega12_hwmgr.c:518
- sys/dev/drm/amd/powerplay/hwmgr/vega12_hwmgr.c:520
- sys/dev/drm/amd/powerplay/hwmgr/vega12_hwmgr.c:525
- sys/dev/drm/amd/powerplay/hwmgr/vega12_hwmgr.c:989
- sys/dev/drm/amd/powerplay/hwmgr/vega12_hwmgr.h:96
- sys/dev/drm/amd/powerplay/hwmgr/vega12_hwmgr.h:107
- sys/dev/drm/amd/powerplay/hwmgr/vega20_hwmgr.c:479
Detail
Evidence (decisive lines)
run.log (replica output: 'bytes written past end of object (guard band): 54 <-- HEAP OVERFLOW' and 'gfx_table.count = 0x00000001' from entry 16's enabled store), build.log (guest cc 8.3), fix_build.log (batched nativekernel RC=0, kernel #1), VERDICT.md line-accurate walk
PoC changes
No seed existed; trigger.c is a struct-geometry replica of the fill loop using the exact vega12_hwmgr.h types (compiled on the guest β the audit host has no compiler).
Verified recommended fix
Reject impossible SMU level counts at the boundary: PP_ASSERT_WITH_CODE(*num_of_levels <= MAX_REGULAR_DPM_NUMBER, ..., return -EINVAL); in vega12_get_number_of_dpm_level.
Verdict
Hardware-gated (guest has no AMD GPU; vega12_hwmgr never runs), so the driver path is inconclusive at runtime β but the defect is source-certain and line-verified: vega12_get_number_of_dpm_level stores a raw SMU-firmware-fed uint32 (smum_get_argument) with only a >0 check (vega12_hwmgr.c:481) into dpm_table->count (:518), and the fill loop writes dpm_levels[i] for i < num_of_levels (:520-527) past dpm_levels[MAX_REGULAR_DPM_NUMBER=16] (vega12_hwmgr.h:107-110) β a firmware-controlled linear heap overflow of the kzalloc'd struct vega12_hwmgr backend (12-byte stores, firmware-chosen values), marching through all ten dpm tables and past the allocation. A struct-geometry replica (trigger.c, built and run on the guest) shows 100 levels -> 54 bytes past a 636-byte object plus the entry-16 'enabled' store smashing the next table's count. vega12_find_highest_dpm_level:989 already asserts count<=16, proving the missing enforcement. Fix (reject counts > MAX_REGULAR_DPM_NUMBER at :481) compile-validated in the batched kernel build.
No comments yet.