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

Unbounded VBIOS ucSclkEntryNum overflows fixed 8-entry stack table in atomctrl_get_smc_sclk_range_table

Summary

atomctrl_get_smc_sclk_range_table() loops for (i=0; i<psmu_info->ucSclkEntryNum; i++) writing table->entry[i] where ucSclkEntryNum is a UCHAR taken straight from the VBIOS SMU_Info atom table with no comparison against the fixed array size (MAX_SCLK_RANGE=8).

Both the source (ATOM_SMU_INFO_V2_1.asSclkFcwRangeEntry[8], atombios.h:5640) and destination (pp_atom_ctrl_sclk_range_table.entry[8], ppatomctrl.h:244) are 8 entries; ucSclkEntryNum can be 0..255.

Both callers (polaris10_smumgr.c:803 and vegam_smumgr.c:673) allocate the destination as a 64-byte stack local, so ucSclkEntryNum>8 smashes up to ~2 KB of kernel stack.

psmu_info is also dereferenced without a NULL check.

Root cause

ppatomctrl.c:1368-1371 fetches psmu_info = smu_atom_get_data_table(...) which smu_helper.c:660-672 returns NULL when the SMU_Info data table header fails to parse. ppatomctrl.c:1374 then reads psmu_info->ucSclkEntryNum with no NULL guard (NULL deref β†’ panic, a lesser DoS).

More seriously, the loop bound at :1374 is the raw VBIOS UCHAR ucSclkEntryNum and there is no PP_ASSERT_WITH_CODE((psmu_info->ucSclkEntryNum <= MAX_SCLK_RANGE), ...) anywhere β€” contrast atomctrl_get_voltage_table_v3:546-551 which DOES cap ucGpioEntryNum against PP_ATOMCTRL_MAX_VOLTAGE_ENTRIES.

Each iteration writes 5 fields (ucVco_setting, ucPostdiv, usFcw_pcc, usFcw_trans_upper, usRcw_trans_lower) into table->entry[i]; sizeof(pp_atom_ctrl_sclk_range_table_entry)=8 (ppatomctrl.h:234-240) and entry[] has MAX_SCLK_RANGE=8 slots (ppatomctrl.h:232,244) = 64 bytes total.

The two callers allocate struct pp_atom_ctrl_sclk_range_table range_table_from_vbios = {{{0}}}; on their stack frames (polaris10_smumgr.c:803 inside polaris10_get_sclk_range_table, vegam_smumgr.c:673 inside vegam_get_sclk_range_table) and pass &range_table_from_vbios as table.

With ucSclkEntryNum=255 the loop writes 255*8=2040 bytes starting at table->entry[0], i.e. ~1976 bytes past the end of the 64-byte stack object, overwriting saved frame pointer, return address, and adjacent frames.

This is the same defect class as DF-1468..DF-1472 (VBIOS-supplied count into a fixed kernel array) but uniquely yields a stack write rather than a read.

Threat

Local, kernel-context, reached on the amdgpu driver-load / powerplay-init path for CHIP_POLARIS10/11/12 (polaris10_smumgr) and VEGAM (vegam_smumgr).

Precondition: attacker controls the GPU VBIOS SMU_Info atom table. Two realistic placements matching the AGENT.md FS/VBIOS-image threat model:

  1. Host root or an attacker with physical/PCI-bar access flashes a malicious VBIOS ROM;
  2. SR-IOV / VFIO passthrough where a malicious or compromised host feeds a guest amdgpu driver a forged atom BIOS image β€” smu_atom_get_data_table (smu_helper.c:669) returns bios + data_start directly from the per-device atom_context, so the guest kernel consumes attacker bytes.

Impact: kernel stack buffer overflow during driver init. Without -fstack-protector on this build path it is a return-address overwrite β†’ local kernel code execution; with stack canaries it is a reliable kernel panic (DoS) on every load.

The OOB read side (psmu_info->asSclkFcwRangeEntry[i] for i>=8) also leaks adjacent VBIOS/kernel bytes into the SMC DPM table that is later uploaded to the GPU.

Exploit / PoC

This surface has no syscall entry point; the PoC is a forged VBIOS atom data table, mirroring the DF-1468 family.

  1. Dump a reference SMU_Info (DATA, SMU_Info) atom table from a Polaris10/ VEGAM target via atombios parsing.
  2. Patch the ATOM_SMU_INFO_V2_1.ucSclkEntryNum byte (offset of ucSclkEntryNum within the struct = offsetof after the ATOM_COMMON_TABLE_HEADER) from its valid value (≀8) to 0xFF, leaving the 8 real asSclkFcwRangeEntry entries in place.
  3. Recompute the atom master-data-table size/checksum so amdgpu_atom_parse_data_header accepts the forged image; write the image to the GPU ROM (host) or inject it into the guest atom_context (SR-IOV).
  4. Bind amdgpu on a POLARIS10/POLARIS11/POLARIS12/VEGAM part; the load path polaris10_smumgr.c β†’ polaris10_get_sclk_range_table:807 β†’ atomctrl_get_smc_sclk_range_table:1374 runs unconditionally and writes table->entry[8..254].

Success = immediate kernel panic (stack smashing detected / page fault on stack guard) proving memory corruption; on a non-canary kernel build, replace the overflowed return-address bytes in the forged VBIOS image with a chosen value to redirect polaris10_get_sclk_range_table's return into kernel shellcode.

Bounds-check ucSclkEntryNum against MAX_SCLK_RANGE and NULL-check psmu_info before the loop, matching the existing PP_ASSERT_WITH_CODE style used elsewhere in this file (e.g. :546-551).

--- a/sys/dev/drm/amd/powerplay/hwmgr/ppatomctrl.c
+++ b/sys/dev/drm/amd/powerplay/hwmgr/ppatomctrl.c
@@ -1365,11 +1365,19 @@ int atomctrl_get_smc_sclk_range_table(struct pp_hwmgr *hwmgr, struct pp_atom_ctr

    ATOM_SMU_INFO_V2_1 *psmu_info =
        (ATOM_SMU_INFO_V2_1 *)smu_atom_get_data_table(hwmgr->adev,
            GetIndexIntoMasterTable(DATA, SMU_Info),
            &size, &frev, &crev);

+   PP_ASSERT_WITH_CODE((NULL != psmu_info),
+           "Could not find SMU Info Table in BIOS.", return -EINVAL;);
+
+   PP_ASSERT_WITH_CODE((psmu_info->ucSclkEntryNum <= MAX_SCLK_RANGE),
+           "Too many SCLK range entries in SMU Info table!",
+           return -EINVAL;);
+
    for (i = 0; i < psmu_info->ucSclkEntryNum; i++) {
        table->entry[i].ucVco_setting = psmu_info->asSclkFcwRangeEntry[i].ucVco_setting;
  • DF-1468, DF-1469, DF-1470 (siblings, processpptables.c): same VBIOS-count-OOB class.
  • DF-1497, DF-1498 (siblings): same file's other VBIOS-parser defects.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1496 Β· 12 files
FileTypeDescriptionSize
harness.c trigger-source replica of atomctrl_get_smc_sclk_range_table with ucSclkEntryNum=255 into a stack-local entry[8] + canary 7.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 979 B view raw
fix.diff suggested-fix PP_ASSERT_WITH_CODE NULL check on psmu_info + ucSclkEntryNum<=MAX_SCLK_RANGE bound 974 B view raw
fix_module_proof.txt fix-build-proof ppatomctrl.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.4 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
VERDICT.md verdict full narrative: mechanism, reachability, harness, fix
↓ download raw

DF-1496 β€” Unbounded ucSclkEntryNum overflows fixed 8-entry stack array (ppatomctrl.c)

Verdict: REPRODUCED (source-level + harness) β€” latent amdgpu-powerplay bug, stack smash

The bug

sys/dev/drm/amd/powerplay/hwmgr/ppatomctrl.c, function atomctrl_get_smc_sclk_range_table, lines 1361-1386:

ATOM_SMU_INFO_V2_1 *psmu_info =
    (ATOM_SMU_INFO_V2_1 *)smu_atom_get_data_table(...);   /* may return NULL */
...
for (i = 0; i < psmu_info->ucSclkEntryNum; i++) {          /* :1374 NO bound */
    table->entry[i].ucVco_setting    = psmu_info->asSclkFcwRangeEntry[i]....;  /* :1375 */
    table->entry[i].ucPostdiv        = ...;                                     /* :1376 */
    table->entry[i].usFcw_pcc        = le16_to_cpu(...);                        /* :1378 */
    ...
}

ucSclkEntryNum is a UCHAR (0..255) from the VBIOS SMU_Info table with no comparison vs MAX_SCLK_RANGE (8). The source VBIOS array asSclkFcwRangeEntry[8] (atombios.h:5640) and the dest table->entry[8] (ppatomctrl.h:232,244, 8 entries * 8 bytes = 64 bytes) are both fixed at 8. Callers polaris10_smumgr.c:803 and vegam_smumgr.c:673 allocate a 64-byte stack-local struct pp_atom_ctrl_sclk_range_table. With ucSclkEntryNum = 255 the loop writes 255 entries (2040 bytes) starting at the stack array, smashing the stack frame -> return-address overwrite (code exec) or stack-canary panic. psmu_info is also deref'd without a NULL check. POLARIS10/11/12/VEGAM.

Harness proof

VBIOS psmu_info->ucSclkEntryNum  = 255 (u8, NO check vs MAX_SCLK_RANGE)
MAX_SCLK_RANGE                   = 8 (ppatomctrl.h:232)
dest stack table                 = 64 bytes (entry[8])
loop would write                 = 2040 bytes (255 entries)
OVERSHOOT past entry[8]          = 1976 bytes of stack smash
entry[7]  = {vco=0x17 postdiv=0x27}  (in-bounds, last legal)
bytes past entry[8] = 0x10 0x20 0x00 0x10 ... (was zero)
post_canary = 0x3000200010002010  (expected 0x2222222222222222)
RESULT: stack overflow CONFIRMED at ppatomctrl.c:1375

Fix

fix.diff adds two PP_ASSERT_WITH_CODE guards before the loop: 1. NULL check on psmu_info (smu_atom_get_data_table can return NULL). 2. ucSclkEntryNum <= MAX_SCLK_RANGE bound check.

Module build validation (Phase 8)

All 8 amdgpu fixes applied; amdgpu.ko built under -Werror: ppatomctrl.o (14240 bytes) produced, 0 errors, amdgpu.ko linked.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED via module build: fix.diff applied cleanly; amdgpu.ko built under -Werror with 0 errors; ppatomctrl.o (14240 bytes) produced, amdgpu.ko linked. The PP_ASSERT_WITH_CODE bounds compiles into the module. Runtime before/after not possible (no AMD GPU HW).

baseline (harness): OVERSHOOT=1976 bytes; post_canary=0x3000200010002010 (expected 0x2222222222222222) -- stack smash
patched (module build): OK ppatomctrl.o (14240 bytes); amdgpu.ko = 3741488 bytes; error count: 0; AMDGPU_DONE
↓ fix.diffn/a (module build)

Confirmed kernel references

Detail

Exploit chain

Blocked by dead-code-on-guest hard blocker (valid): amdgpu powerplay not in GENERIC and no AMD GPU HW on the audit guest; kernel path cannot trigger end-to-end. Primitive proven at harness level (stack-local entry[8] smashed by ucSclkEntryNum=255, canary overwritten). Realistic runtime impact with POLARIS10/11/12/VEGAM HW + crafted VBIOS is stack smash -> RCE (return-addr overwrite) or stack-canary panic. Evidence pack: findings/poc/DF-1496/ (harness.c).

Evidence (decisive lines)

VBIOS psmu_info->ucSclkEntryNum  = 255 (u8, NO check vs MAX_SCLK_RANGE)
MAX_SCLK_RANGE                   = 8 (ppatomctrl.h:232)
dest stack table                 = 64 bytes (entry[8])
loop would write                 = 2040 bytes (255 entries)
OVERSHOOT past entry[8]          = 1976 bytes of stack smash
entry[7]  = {vco=0x17 postdiv=0x27}  (in-bounds, last legal)
bytes past entry[8] = 0x10 0x20 0x00 0x10 ... (was zero)
post_canary = 0x3000200010002010  (expected 0x2222222222222222)
RESULT: stack overflow CONFIRMED at ppatomctrl.c:1375
RUN_EXIT=0

PoC changes

Authored harness.c (stack-local entry[8] replica with pre/post canaries), build.sh, run.sh, fix.diff (PP_ASSERT_WITH_CODE NULL check + ucSclkEntryNum<=MAX_SCLK_RANGE bound), VERDICT.md, manifest.json. fix.diff regenerated via copy+edit+diff.

Verified recommended fix

In atomctrl_get_smc_sclk_range_table (ppatomctrl.c:1368-1374), before the loop add PP_ASSERT_WITH_CODE((NULL != psmu_info), ..., return -EINVAL); and PP_ASSERT_WITH_CODE((psmu_info->ucSclkEntryNum <= MAX_SCLK_RANGE), ..., return -EINVAL);. Matches finding proposal. Full diff in findings/poc/DF-1496/fix.diff.

Verdict

REPRODUCED. atomctrl_get_smc_sclk_range_table (ppatomctrl.c:1361-1386) loops for(i=0;i<psmu_info->ucSclkEntryNum;i++) writing table->entry[i] where ucSclkEntryNum is UCHAR (0..255) from VBIOS SMU_Info with NO check vs MAX_SCLK_RANGE=8 (ppatomctrl.h:232). Both source asSclkFcwRangeEntry[8] and dest entry[8] are fixed at 8. Callers polaris10_smumgr.c:803 and vegam_smumgr.c:673 allocate a 64-byte stack-local dest; ucSclkEntryNum=255 writes 2040 bytes -> stack smash (return-addr overwrite / canary panic). psmu_info also deref'd without NULL check. Confirmed by harness: post_canary clobbered (0x3000200010002010 vs expected 0x2222222222222222).