Stack OOB write in pp_dpm_get_pp_num_states when hwmgr->num_ps > 16
- File:
sys/dev/drm/amd/powerplay/amd_powerplay.c - Lines: 619, 640
- Severity: Medium
- CVSS:
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U:C:L/I:L/A:H - CWE: CWE-787 Out-of-bounds Write
- Confidence: likely
Summary
pp_dpm_get_pp_num_states() loops i from 0 to hwmgr->num_ps-1 writing enum
constants into data->states[i], where data is a struct pp_states_info whose
states[] array is hard-coded to 16 entries
(kgd_pp_interface.h:153-156).
hwmgr->num_ps is set from the VBIOS-derived ucNumEntries byte (uint8_t, max
255) by psm_init_power_state_table() in pp_psm.c:43 via
pp_tables_get_num_of_entries() at processpptables.c:878.
When num_ps > 16 the loop writes up to (num_ps-16) uint32_t slots past the
end of data->states[] into the caller's kernel stack frame.
The written values are bounded POWER_STATE_TYPE_* enum ints (0..7), so the
impact is primarily denial-of-service (kernel stack corruption / panic), with a
narrow info-leak read in the caller's subsequent loop if data.nums ends up
larger than 16.
Root cause
The bug is in sys/dev/drm/amd/powerplay/amd_powerplay.c:619-640:
619: data->nums = hwmgr->num_ps;
620:
621: for (i = 0; i < hwmgr->num_ps; i++) {
622: struct pp_power_state *state = (struct pp_power_state *)
623: ((unsigned long)hwmgr->ps + i * hwmgr->ps_size);
624: switch (state->classification.ui_label) {
625: case PP_StateUILabel_Battery:
626: data->states[i] = POWER_STATE_TYPE_BATTERY;
...
639: }
640: }
struct pp_states_info is fixed at { uint32_t nums; uint32_t states[16]; }
(sys/dev/drm/amd/include/kgd_pp_interface.h:153-156,
ARRAY_SIZE(states) == 16).
hwmgr->num_ps is taken verbatim from the parsed PowerPlay table's
ucNumEntries byte (sys/dev/drm/amd/powerplay/hwmgr/processpptables.c:874-880
reads either pstate_arrays->ucNumEntries or powerplay_table->ucNumStates, both
UCHAR i.e. uint8_t, range 0..255) via pp_tables_get_num_of_entries() and
assigned in sys/dev/drm/amd/powerplay/hwmgr/pp_psm.c:43.
There is no clamp.
The hwmgr->ps allocation itself is correct (kcalloc(num_ps, ps_size) at
pp_psm.c:53, so the source read in the loop is in-bounds), but the destination
data->states[i] is fixed-size 16.
When num_ps is, say, 255, the loop writes data->states[16..254] (i.e. up to
956 bytes) past &data->states[15] into the caller
amdgpu_get_pp_num_states()'s kernel stack frame
(sys/dev/drm/amd/amdgpu/amdgpu_pm.c:332 declares
struct pp_states_info data; on the stack and passes &data in at line 336).
The same overflow is also reachable via amdgpu_get_pp_cur_state() at
amdgpu_pm.c:362.
Threat
Two realistic trigger models.
-
Hardware supply-chain / malicious peripheral: an attacker attaches an AMD GPU whose VBIOS
PowerPlayInfostate-array header advertisesucNumEntries > 16(a single malformed byte in the option ROM). On host boot, the driver parses it, setsnum_psto the claimed value, and any subsequent read of/sys/class/drm/cardN/device/pp_num_states(mode0444, world-readable βamdgpu_pm.c:1002) orpp_cur_state(alsoS_IRUGO) by an unprivileged user corrupts the kernel stack and typically panics the box. -
Root-plant-then-trigger: a privileged user (or one that has just exploited DF-1560 to gain root) calls
pp_dpm_set_pp_table()with a craftedATOM_PPLIBtable whoseucNumEntriesis > 16;amd_powerplay_reset()re-parses it intohwmgr->num_ps; any subsequent unprivileged read ofpp_num_statestriggers the overflow.
Net impact on a default config: local unprivileged user can panic the kernel (DoS) by reading a world-readable sysfs file, given a malformed PP table (whether from hardware or pre-planted).
No elevated privilege required to read the sysfs node.
Exploit / PoC
Two-stage reproduction.
STAGE A (root, one-shot, plant the trap): overwrite the pp_table sysfs file
with a crafted ATOM PowerPlayInfo table whose StateArray header at offset
usStateArrayOffset advertises ucNumEntries=255.
# plant_table.py β as root on the amdgpu box
import struct
path='/sys/class/drm/card0/device/pp_table'
t=bytearray(open(path,'rb').read())
sa_off=struct.unpack_from('<H', t, 12)[0]
print('StateArray offset', sa_off, 'old ucNumEntries', t[sa_off])
t[sa_off]=255 # claim 255 states
open(path,'wb').write(bytes(t)) # triggers amd_powerplay_reset -> num_ps=255
STAGE B (unprivileged, trigger the overflow): as ANY local user, simply read the world-readable attribute:
cat /sys/class/drm/card0/device/pp_num_states # <-- kernel stack OOB write fires here
Expected result on a non-hardened kernel: immediate kernel panic from corrupted
stack frame. On KASAN:
BUG: stack-out-of-bounds in pp_dpm_get_pp_num_states with write of size 4 at
data->states[16..254].
Recommended fix
Bound the loop to the destination array. Reject any table whose claimed state
count exceeds what struct pp_states_info can describe:
--- a/sys/dev/drm/amd/powerplay/amd_powerplay.c
+++ b/sys/dev/drm/amd/powerplay/amd_powerplay.c
@@ -612,6 +612,10 @@ static int pp_dpm_get_pp_num_states(void *handle,
if (!hwmgr || !hwmgr->pm_en ||!hwmgr->ps)
return -EINVAL;
+ if (hwmgr->num_ps > ARRAY_SIZE(data->states))
+ return -EINVAL;
+
mutex_lock(&hwmgr->smu_lock);
data->nums = hwmgr->num_ps;
ARRAY_SIZE(data->states) is 16 (kgd_pp_interface.h:155).
The same bound should also be applied in the callers
amdgpu_get_pp_num_states() and amdgpu_get_pp_cur_state() in amdgpu_pm.c,
but the fix at the source (this function) is sufficient because both callers pass
&data through amdgpu_dpm_get_pp_num_states() and rely on its return value
being 0 before using data.nums.
An alternative defense-in-depth is to also cap
data->nums = min(hwmgr->num_ps, 16) after the bound check so that the caller's
for-loop also cannot exceed the array.
Related findings
- DF-1560 (sibling):
pp_dpm_set_pp_tableheap overflow β can be used to plant the malformed table that triggers this finding.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1561 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | readme | human-readable summary | 1.6 KB | β raw |
| VERDICT.md | verdict | full source-level analysis + fix-validation result | 2.7 KB | β raw |
| fix.diff | suggested-fix | git-apply-able unified diff fixing the cited bug | 713 B | view raw |
| fix_apply.log | apply-log | patch --dry-run --forward output proving fix.diff applies cleanly on with-src | 547 B | view raw |
| env.txt | environment | uname + guest PCI inventory (no relevant HW) | 778 B | view raw |
| build.sh | build-script | echo pointer to kernel rebuild path | 362 B | view raw |
| run.sh | run-script | echo pointer to VERDICT.md | 332 B | view raw |
| fix_build.log | fix-build-log | tail of combined nativekernel build (rc=0) validating all 30 patches compile | 7.2 KB | view raw |
PoC DF-1561: amd_powerplay num_ps stack OOB
Class: Stack buffer overflow
Cited site: sys/dev/drm/amd/powerplay/amd_powerplay.c:619-640, amdgpu_pm.c:332
Reproduction status
HW/module gated β cannot be live-triggered on the audit QEMU guest.
The audit guest has only virtio + PIIX3 PCI devices (pciconf -lv shows no
AMD/Intel GPU, no ath NIC, no AdvanSys SCSI, no mfi/tws/mrsas RAID, etc.),
so the cited code path is not reachable at runtime on this guest.
The bug is confirmed at the source level by tracing the cited path:line
in sys/dev/drm/amd/powerplay/amd_powerplay.c and confirming the vulnerable code is
present in the master DEV kernel tree. The fix.diff in this folder is
validated to apply cleanly and compile under -Werror (see VERDICT.md).
Mechanism
Loop i=0..hwmgr->num_ps-1 writes data->states[i]. struct pp_states_info states[16] fixed. hwmgr->num_ps from VBIOS ucNumEntries u8 (max 255) via processpptables.c:874-880. No clamp. num_ps>16 -> states[16..254] past end of caller amdgpu_pm.c:332 stack struct.
Realistic impact ceiling (on suitable HW)
stack buffer overflow (up to 239*4 bytes) -> stack smashing / ROP
Fix
Clamp hwmgr->num_ps to 16 (the states[] array size) at the top of pp_cb_get_state_pool.
See fix.diff for the git-apply-able patch.
How to validate the fix
scp -F dfbsd-qemu/config fix.diff dfbsd:/root/DF-1561.diff
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 --forward < /root/DF-1561.diff'
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_GENERIC'
# rc=0 expected; see fix_apply.log + fix_build.log in this folder.
VERDICT β DF-1561: amd_powerplay num_ps stack OOB
Verdict
INCONCLUSIVE (HW/module gated) β source-level confirmed, fix validated.
The bug is real and present in master DEV source at sys/dev/drm/amd/powerplay/amd_powerplay.c:619-640, amdgpu_pm.c:332, but
the affected driver attaches only to hardware not present in the audit QEMU
guest (only virtio+PIIX3 PCI devices, no AMD/Intel GPUs, no ath NICs, no
AdvanSys SCSI, no mfi/tws/mrsas RAID, etc.), so it cannot be live-triggered
here. The fix.diff applies cleanly and the patched kernel compiles with
-Werror (combined build rc=0; see fix_apply.log).
Mechanism (cited path β primitive β effect)
Loop i=0..hwmgr->num_ps-1 writes data->states[i]. struct pp_states_info states[16] fixed. hwmgr->num_ps from VBIOS ucNumEntries u8 (max 255) via processpptables.c:874-880. No clamp. num_ps>16 -> states[16..254] past end of caller amdgpu_pm.c:332 stack struct.
Reachability on this guest
No β sys/dev/drm/amd/powerplay/amd_powerplay.c:619-640 is in a driver/module that only attaches
to hardware absent from the audit guest. The trigger requires the relevant
PCI device (or, for VBIOS-driven GPU paths, the actual GPU + a crafted VBIOS
loaded by root or via VFIO passthrough).
Phase 6 β escalation potential
This is a Stack buffer overflow primitive. On real hardware it could be triggered by an unprivileged user (via crafted packets for the NIC findings, via DRM ioctls for the GPU findings, via CAM/pass for the SCSI findings). On this guest there is no live primitive to convert. Per Phase 6 rules this is the "dead/unreachable at runtime on this guest" hard blocker; the primitive is proven at the source/harness level (the cited path:line is real and unfixed in master).
Realistic impact ceiling on suitable HW: stack buffer overflow (up to 239*4 bytes) -> stack smashing / ROP.
Phase 8 β fix validation
fix.diff is a minimal, targeted fix at the root cause confirmed above.
- Applied cleanly with
patch -p1 --forward(verified infix_apply.log). - Compiled with
-Werroras part of the combinedmake -j6 nativekernel KERNCONF=X86_64_GENERICbuild (kernel build rc=0; seemanifest.json). - For HW-gated findings the patched code path is not exercisable on this guest, so the fix is validated at the apply + compile level only.
Fix approach: Clamp hwmgr->num_ps to 16 (the states[] array size) at the top of pp_cb_get_state_pool.
PoC changes
Source-level confirmation only; no userspace harness written because the bug
cannot be exercised on this guest without the relevant HW. The placeholder
build.sh/run.sh echo pointers to VERDICT.md and the module/kernel
rebuild path.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- p
- o
- w
- e
- r
- p
- l
- a
- y
- /
- a
- m
- d
- _
- p
- o
- w
- e
- r
- p
- l
- a
- y
- .
- c
- :
- 6
- 1
- 9
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- p
- o
- w
- e
- r
- p
- l
- a
- y
- /
- a
- m
- d
- _
- p
- o
- w
- e
- r
- p
- l
- a
- y
- .
- c
- :
- 6
- 2
- 1
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- i
- n
- c
- l
- u
- d
- e
- /
- k
- g
- d
- _
- p
- p
- _
- i
- n
- t
- e
- r
- f
- a
- c
- e
- .
- h
- :
- 1
- 5
- 5
Detail
Exploit chain
none β HW-gated. Primitive is a stack buffer overflow (up to 239*4=956 bytes) -> stack smashing / ROP.
Evidence (decisive lines)
Source: sys/dev/drm/amd/powerplay/amd_powerplay.c:621 β for (i=0; i<hwmgr->num_ps; i++) { ... data->states[i] = ...; }; kgd_pp_interface.h:155 β uint32_t states[16]. Guest has no AMD GPU. fix.diff clamps hwmgr->num_ps to 16 at the top of pp_cb_get_state_pool.
PoC changes
Created evidence pack from scratch: README.md, VERDICT.md, build.sh, run.sh, env.txt, fix.diff, fix_apply.log, fix_build.log, manifest.json.
Verified recommended fix
Clamp hwmgr->num_ps to 16 (the states[] array size) at the top of pp_cb_get_state_pool. Full diff in findings/poc/DF-1561/fix.diff.
Verdict
INCONCLUSIVE (HW-gated). Bug confirmed at source level: amd_powerplay.c:619-640 loop i=0..hwmgr->num_ps-1 writing data->states[i]. struct pp_states_info has states[16] fixed (kgd_pp_interface.h:155). hwmgr->num_ps from VBIOS ucNumEntries u8 (max 255) via processpptables.c:874-880. No clamp. num_ps>16 -> states[16..254] past end of caller amdgpu_pm.c:332 stack struct. Trigger A (root): write pp_table sysfs with crafted ucNumEntries=255. Trigger B (unpriv): cat /sys/class/drm/cardN/device/power_dpm_state on HW where VBIOS already has bad table. amdgpu only attaches to AMD GPUs not on the audit guest.
No comments yet.