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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4054 Β· 10 files
FileTypeDescriptionSize
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).

VERDICT.md
↓ download raw

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

fixed
baseline no→ patch + rebuild →patched clean

Fix 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)
↓ fix.diffper-fix-DF-4054

Confirmed kernel references

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.