Zero-level power state: OOB read performance_levels[-1] and integer underflow memset overflow in SMC state upload
Summary
si_parse_power_table can produce power state with performance_level_count=0 (BIOS state with ucNumDPMLevels=0 or all clockInfoIndex out of range). si_convert_power_state_to_smc only guards count>MAX not count==0, reads performance_levels[count-1]=levels[-1] OOB. si_upload_sw_state computes state_size=(0-1)*sizeof(level) underflows to ~SIZE_MAX, memset(smc_state,0,~SIZE_MAX) overflows heap. Also si_dpm_get_sclk/mclk read levels[count-1] from sysfs paths. Attacker: crafted VBIOS via VFIO romfile. Fix: reject count==0 in parser and consumers.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1128 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | userspace logic harness: OOB read of performance_levels[-1] when count==0 | 5.0 KB | view raw |
| fix.diff | suggested-fix | reject count==0 in si_convert_power_state_to_smc + sclk/mclk getters | 1.7 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o harness harness.c | 240 B | view raw |
| run.sh | run-script | ./harness | 91 B | view raw |
| run.log | run-log | decisive harness run, full output | 1.2 KB | view raw |
| fix_build.log | build-log | amdgpu.ko rebuilt cleanly with fix applied (3,741,128 bytes) | 23.8 KB | view raw |
| env.txt | environment | uname, cc version, kldstat | 278 B | view raw |
| VERDICT.md | verdict | full narrative: OOB read confirmed, state_size underflow claim refuted | 6.0 KB | β raw |
| README.md | readme | human-facing summary | 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-1128 β amdgpu/si_dpm.c zero performance_level_count OOB read
TL;DR
- Status: REPRODUCED (source + harness). Primitive is an OOB read of
state->performance_levels[count-1]whenperformance_level_count == 0, confirmed by source trace + a userspace math harness that mirrors the kernel code. - Impact: leak / panic (read primitive; not write-capable). The same
pattern is reachable from sysfs via
si_dpm_get_sclk/si_dpm_get_mclkwhich can leak the OOB-read value to userspace. - Caveat on the finding summary: the
state_sizeunderflow half of the summary is incorrect β with the real struct layout it evaluates to4, not~SIZE_MAX. See VERDICT.md Β§"Finding-summary accuracy note".
Why no live trigger on this guest
The bug is in amdgpu.ko (AMD Southern Islands DPM). The QEMU audit guest
has no AMD GPU (pciconf -l shows no ATI/AMD display device), so
amdgpu.ko is never kldloaded at boot. The trigger requires SI-class
hardware plus a crafted VBIOS whose ATOM_PPLIB power state has
ucNumDPMLevels==0 (or all clockInfoIndex values out of range).
Files
harness.cβ userspace logic harness mirroringsi_convert_power_state_to_smcand the sclk/mclk getters. Demonstrates the OOB read primitive.fix.diffβ git-apply-able unified diff againstsys/dev/drm/amd/amdgpu/si_dpm.c. Rejectscount==0insi_convert_power_state_to_smc,si_upload_sw_state, and the sclk/mclk getters.run.logβ full output of the harness run.env.txtβ guest environment.
Reproduce
./build.sh && ./run.sh
Expected: harness prints the OOB read of performance_levels[-1] (returns
0xcccccccc from the simulated pre-object backing buffer), confirms the
finding's state_size underflow claim is wrong (real value is 4), and
demonstrates that the same count-1 pattern is used by the sysfs-readable
si_dpm_get_sclk/si_dpm_get_mclk.
Fix validation
The fix.diff was applied to the in-guest /usr/src tree and amdgpu.ko
was rebuilt with make from /usr/src/sys/dev/drm/amd. The full module
linked successfully (3.74 MB amdgpu.ko). See VERDICT.md for the build log
excerpt.
DF-1128 β Verdict
Verdict: REPRODUCED (source-level + harness) β leak / panic class, no escalation chain (read-only primitive)
Bug confirmation
si_parse_power_table (amdgpu/si_dpm.c:7210-7300) iterates over
power_state->v2.ucNumDPMLevels (a BIOS-controlled u8) and calls
si_parse_pplib_clock_info for each in-range clockInfoIndex. The only
place that sets ps->performance_level_count is line 7141 inside
si_parse_pplib_clock_info. So if the BIOS state has ucNumDPMLevels==0
OR all clockInfoIndex values are >= ucNumEntries, the inner loop body
never runs and performance_level_count stays at its kzalloc-initialized
0.
That count then reaches three consumers with no count==0 guard:
-
si_convert_power_state_to_smcat amdgpu/si_dpm.c:5662-5665:c if (state->performance_level_count > SISLANDS_MAX_HARDWARE_POWERLEVELS) return -EINVAL; threshold = state->performance_levels[state->performance_level_count-1].sclk * 100 / 100;The guard only rejectscount > MAX. Withcount == 0, the array index(u16)0 - 1is computed inintas-1, so the access isperformance_levels[-1]β a 4-byte OOB read of thesclkfield before the start of theperformance_levelsarray (i.e. into the preceding slab object / slab metadata). -
si_dpm_get_sclkat amdgpu/si_dpm.c:7876 (sysfs path):c return requested_state->performance_levels[requested_state->performance_level_count - 1].sclk;Same OOB read; the result is returned to userspace as the requested sclk viaradeon_pm_info-style debugfs/sysfs. β info leak of adjacent slab data. -
si_dpm_get_mclkat amdgpu/si_dpm.c:7888: identical OOB pattern for mclk.
A sibling guard at line 2405 (si_populate_power_containment_values)
already does the right thing:
if (state->performance_level_count == 0)
return -EINVAL;
So the fix is well-precedented inside the same file; the three sites above simply missed it.
Harness confirmation
harness.c mirrors the kernel arithmetic. With
performance_level_count == 0 and a backing buffer initialised to
0xcc..., the harness reads threshold = 0xcccccccc from
performance_levels[-1] β exactly the OOB read the source trace predicts.
Output captured in run.log.
Finding-summary accuracy note
The finding summary also claims:
si_upload_sw_state computes state_size=(0-1)*sizeof(level) underflows to ~SIZE_MAX, memset(smc_state,0,~SIZE_MAX) overflows heap.
This is incorrect for the actual struct layout. With the real
definitions in sislands_smc.h:
- sizeof(struct SISLANDS_SMC_SWSTATE) == 4 (header) + sizeof(LEVEL)
- sizeof(struct SISLANDS_SMC_HW_PERFORMANCE_LEVEL) == sizeof(LEVEL)
So:
state_size = sizeof(SWSTATE) + ((count - 1) * sizeof(LEVEL))
= (4 + sizeof(LEVEL)) + (-1 * sizeof(LEVEL))
= 4 (after unsigned wrap mod 2^64 then truncation to u32)
The harness confirms this: state_size = 0x00000004 (4 bytes). The
memset(smc_state, 0, 4) therefore only writes 4 bytes β not a heap
overflow. The realistic primitive is the OOB read at line 5665 and the
matching sysfs-readable OOB reads at 7876/7888.
The OOB read is the actionable bug. This finding is therefore downgraded
internally from "integer underflow memset heap overflow" to "missing zero
count guard β OOB read / info leak". The fix.diff still closes all three
sites.
Exploit chain
This is a read-only primitive (4-byte OOB read before the slab object
backing struct si_ps, plus sysfs exfiltration via si_dpm_get_sclk /
si_dpm_get_mclk). No write capability β no privilege-escalation chain
exists for this bug on its own. The realistic impact ceiling is:
- Info leak of 4 bytes of adjacent kernel heap (slab metadata or
neighbouring object) readable via the
si_dpm_get_sclk(!low)sysfs path. Repeated calls with different kzalloc bucket grooming could harvest slab layout / pointer bits β useful as a KASLR-defeat helper on systems where KASLR is enabled (DragonFly disables it by default, so this is mostly informational on default installs). - Panic if the read crosses into an unmapped page (rare; depends on slab layout).
Trigger conditions (not met on this guest)
- AMD Southern Islands GPU present (PCI vendor
1002, SI family). amdgpu.koloaded (default on systems with the hardware).- Crafted VBIOS whose
ATOM_PPLIB_POWERPLAYTABLEproduces a state withperformance_level_count == 0. Reachable via VFIO GPU passthrough with a reflashed ROM, or an emulated KVM GPU with a custom ROM.
The QEMU audit guest has no AMD GPU; amdgpu.ko is present in
/boot/kernel/ but never loaded. The bug is therefore confirmed at the
source + harness level, not via a live runtime trigger. This matches the
"latent bug, harness-confirmed primitive" outcome documented for similar
driver-only findings.
Fix
fix.diff rejects count == 0 in:
- si_convert_power_state_to_smc (line 5662 β guards the line 5665 OOB read)
- si_upload_sw_state (line 5738 β defense-in-depth, since the line 5665
guard now fires earlier; still worth keeping for robustness)
- si_dpm_get_sclk / si_dpm_get_mclk (lines 7876, 7888 β guards the
sysfs-readable OOB read)
The fix is minimal and mirrors the existing guard at line 2405.
Fix validation
git apply --check(well,patch -p1 --checksince /usr/src isn't a git repo on the guest) β clean apply, all 4 hunks.cd /usr/src/sys/dev/drm/amd && makewith the diff applied βamdgpu.kobuilt cleanly (rc=0, 3,741,128 bytes).- Reverted with
patch -R -p1.
Since the bug cannot be triggered live on the guest (no AMD GPU), the
"behaviour comparison" half of fix-validation is done at the harness level:
the harness includes both the buggy and the fixed code paths side-by-side;
the buggy path performs the OOB read, the fixed path returns the
-EINVAL equivalent. See run.log for the side-by-side output.
fix_status: fixed (compiles cleanly, harness confirms the patched
code path rejects the bad input).
Fix verification
fixedvalidated
radeon.ko build rc=0 + harness before/after
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source+harness. si_convert_power_state_to_smc count=0 -> performance_levels[-1] OOB read. No AMD GPU.
No comments yet.