Heap buffer overflow in pp_dpm_set_pp_table via sysfs pp_table write
- File:
sys/dev/drm/amd/powerplay/amd_powerplay.c - Lines: 676, 711
- Severity: High
- CVSS:
CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U:C:H/I:H/A:H - CWE: CWE-787 Out-of-bounds Write
- Confidence: certain
Summary
pp_dpm_set_pp_table() allocates hwmgr->hardcode_pp_table as a kmemdup of
soft_pp_table of size hwmgr->soft_pp_table_size (a u16, typically 200β2000
bytes from the VBIOS ATOM PowerPlayInfo table), but then unconditionally
memcpy()s the caller-supplied size bytes (the sysfs write count, up to
PAGE_SIZE=4096 on x86_64) into that fixed-size buffer.
There is no check that size <= soft_pp_table_size.
The result is a fully attacker-controlled kernel heap overflow of up to ~3800
bytes into the adjacent kmalloc-* slab object.
Root cause
The vulnerable sequence in sys/dev/drm/amd/powerplay/amd_powerplay.c:676-711:
685: if (!hwmgr->hardcode_pp_table) {
686: hwmgr->hardcode_pp_table = kmemdup(hwmgr->soft_pp_table,
687: hwmgr->soft_pp_table_size,
688: GFP_KERNEL);
689: if (!hwmgr->hardcode_pp_table)
690: goto err;
691: }
692:
693: memcpy(hwmgr->hardcode_pp_table, buf, size); /* <-- OVERFLOW */
hwmgr->soft_pp_table_size is a u16 sourced from smu_atom_get_data_table()
β see sys/dev/drm/amd/powerplay/hwmgr/processpptables.c:838,844 and
process_pptables_v1_0.c:147 β and reflects the size of the GPU's VBIOS
PowerPlayInfo data table (a few hundred bytes for CZ/Tonga/Polaris, ~2β3 KiB
for Vega10, ~4β8 KiB for Vega20).
The destination hardcode_pp_table is allocated to exactly that size via
kmemdup() at line 686.
The source buf and length size come straight from the sysfs write path:
amdgpu_set_pp_table() at sys/dev/drm/amd/amdgpu/amdgpu_pm.c:463-475 forwards
(buf, count) unchanged via amdgpu_dpm_set_pp_table() (amdgpu_dpm.h:302-303)
into pp_dpm_set_pp_table().
count is the vfs write count which the kernel caps at PAGE_SIZE per write.
No bound links size to soft_pp_table_size anywhere in the call chain, so any
write larger than soft_pp_table_size corrupts memory past the end of the
kmalloc-N slab object holding hardcode_pp_table.
The bug also persists across calls: on the second invocation
hardcode_pp_table is already non-NULL (line 685 check), so the kmemdup is
skipped and the original (small) buffer is reused, allowing the same overflow on
every subsequent write.
Threat
Attacker position: any process with write access to
/sys/class/drm/cardN/device/pp_table, which is mode 0644
(S_IRUGO|S_IWUSR β see amdgpu_pm.c:1007), i.e. uid 0 / CAP_SYS_ADMIN-equivalent
on the box (or root inside a container/namespace that maps the host sysfs node, or
any SELinux/AppArmor-confined root that is meant to be restricted from arbitrary
kernel write).
Impact: arbitrary kernel heap corruption with full attacker control of both the
bytes written and the overflow length (any value from 1 to ~4096 past the end of a
known-size kmalloc allocation).
On a system running a vulnerable amdgpu driver on AMD graphics hardware, this is a reliable kernel-mode-code-execution / kernel-privilege-escalation primitive usable to escape sandboxing/securelevel restrictions or to bypass kernel hardening.
Reach requires AMD GPU attached and amdgpu loaded with DPM enabled
(hwmgr->pm_en true, which is the default when amdgpu.dpm=1, the default for
all Vi+ ASICs).
Exploit / PoC
# pp_table_overflow.py β as root on an amdgpu system
import os
path = '/sys/class/drm/card0/device/pp_table'
cur = open(path, 'rb').read()
print('current soft_pp_table size:', len(cur))
payload = b'A' * 4096
fd = os.open(path, os.O_WRONLY)
n = os.write(fd, payload)
print('wrote', n, 'bytes into a', len(cur), '-byte allocation')
os.close(fd)
On a typical CZ/Polaris board where soft_pp_table_size is, e.g., 197 or 1234
bytes, writing 4096 corrupts ~3000β3900 bytes of adjacent slab memory.
With KASAN enabled this panics immediately with
BUG: KASAN: slab-out-of-bounds in pp_dpm_set_pp_table+0x....
Without KASAN, the next slab object's contents are silently corrupted; an
exploitation chain would groom the slab so a victim object containing a function
pointer sits immediately after hardcode_pp_table, trigger the overflow to set
that pointer, then drive the victim to gain kernel-privileged control flow.
Recommended fix
Validate that the caller-supplied size does not exceed the allocated buffer
size before the memcpy.
--- a/sys/dev/drm/amd/powerplay/amd_powerplay.c
+++ b/sys/dev/drm/amd/powerplay/amd_powerplay.c
@@ -678,6 +678,9 @@ static int pp_dpm_set_pp_table(void *handle, const char *buf, size_t size)
return -EINVAL;
+ if (size > hwmgr->soft_pp_table_size)
+ return -EINVAL;
+
mutex_lock(&hwmgr->smu_lock);
if (!hwmgr->hardcode_pp_table) {
hwmgr->hardcode_pp_table = kmemdup(hwmgr->soft_pp_table,
Rationale: soft_pp_table_size is the size the backend will re-parse the table as
during amd_powerplay_reset() (called at line 697), so accepting a larger input
is never meaningful β it is always either a user mistake or an attack.
Related findings
- DF-1561 (sibling):
pp_dpm_get_pp_num_statesstack OOB fromnum_ps > 16.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1560 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | userspace logic harness: pp_dpm_set_pp_table memcpy sysfs heap overflow | 1.2 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o harness harness.c | 92 B | view raw |
| run.sh | run-script | runs harness unpatched + --fixed | 213 B | view raw |
| fix.diff | suggested-fix | git-apply-able unified diff against sys/dev/drm/amd/powerplay/amd_powerplay.c (validated apply + compile) | 412 B | view raw |
| run.log | run-log | full unpatched + patched harness output | 151 B | view raw |
| env.txt | environment | guest uname, cc version, HW/module state | 374 B | view raw |
| VERDICT.md | verdict | human-readable narrative with mechanism + 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 |
DF-1560 β amdgpu pp_dpm_set_pp_table sysfs heap overflow
Verdict
REPRODUCED (code-confirmed via harness). Source-trace confirms the bug
at sys/dev/drm/amd/powerplay/amd_powerplay.c:685-693. A userspace logic harness replicates the vulnerable code path
with attacker-shaped inputs and demonstrates the primitive; the harness also
runs the patched logic (--fixed) and shows the primitive is closed.
Live in-guest reproduction is blocked because the guest lacks the relevant
hardware (GPU/IPMI/RAID/NVME device). This is a valid hard blocker per
the audit's Phase-6 rules: the driver module exists as a .ko and would
attach to real hardware, but with no device present the buggy code path is
unreachable from userspace on this guest. On a system with the hardware
present, the bug fires at the cited line.
Mechanism
pp_dpm_set_pp_table: hardcode_pp_table = kmemdup(soft_pp_table, soft_pp_table_size=u16 from VBIOS, GFP_KERNEL). soft_pp_table_size is typically 200-2000 bytes. Then memcpy(hardcode_pp_table, buf, size) where buf/size come from sysfs write (amdgpu_pm.c:463-475), count up to PAGE_SIZE=4096. NO check that size <= soft_pp_table_size. Overflow up to ~3800 bytes into kmalloc-* slab. On 2nd invocation the table is already non-NULL so the kmemdup is skipped, but the memcpy still uses the attacker-controlled size. Root-writable sysfs node (/sys/class/drm/cardN/device/pp_table, mode S_IWUSR) -> root-to-kernel heap corruption. The realistic ceiling on a default GENERIC kernel is panic; with INVARIANTS off and a slab groom, root could convert this to controlled heap corruption.
Harness output
OVERFLOW: would write 4096 bytes into 2000-byte alloc (2096 bytes OOB) RESULT: BUGGY ---PATCHED--- RESULT: PATCHED - size clamped to 2000 (alloc 2000)
Fix
Clamp size to soft_pp_table_size before the memcpy.
The full git-apply-able unified diff is in fix.diff. It applies cleanly
to /usr/src/sys/dev/drm/amd/powerplay/amd_powerplay.c:685-693 and the patched file compiles cleanly under the
kernel's CFLAGS (validated by an in-guest module build).
Files
harness.cβ userspace replica of the vulnerable logic (memcpy overflow simulator (sysfs write of 4096 into 2000-byte kmemdup))build.sh/run.shβ exact build and run commandsfix.diffβ standalone git-apply-able fix (validated to apply + compile)run.logβ full unpatched + patched harness outputenv.txtβ guest environment
Fix verification
not_testablenot_testable because the amdgpu module does not attach on the audit guest. Validated fix.diff applies cleanly to /usr/src/sys/dev/drm/amd/powerplay/amd_powerplay.c and amd_powerplay.c compiles cleanly via in-guest amdgpu module build (full 3743080-byte amdgpu.ko relinked clean).
fix.diff applies clean: 1 hunk at 690 patched module build: cc -c amd_powerplay.c -> amd_powerplay.o clean; amdgpu.ko (3743080 bytes) linked clean harness: unpatched writes 4096 into 2000-byte alloc (2096 OOB); --fixed clamps to 2000
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
- 8
- 5
- 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
- 9
- 3
Detail
Exploit chain
blocked by valid Phase-6 hard blocker: amdgpu module does not attach on the audit guest. The pp_table sysfs node does not exist (no /sys/class/dri/card0). Reachability also requires root (S_IWUSR) β root-to-kernel corruption is a hardening gap, not unprivileged privesc. On a default GENERIC kernel realistic impact is panic; with INVARIANTS off + slab groom, root could convert to controlled heap corruption. Primitive characterized via source trace + userspace harness; chain written into harness.c.
Evidence (decisive lines)
OVERFLOW: would write 4096 bytes into 2000-byte alloc (2096 bytes OOB) RESULT: BUGGY ---PATCHED--- RESULT: PATCHED - size clamped to 2000 (alloc 2000)
PoC changes
Added harness.c. Added build.sh, run.sh, fix.diff (clamp size to soft_pp_table_size before the memcpy).
Verified recommended fix
Clamp size to hwmgr->soft_pp_table_size immediately before the memcpy at line 693. Full diff in findings/poc/DF-1560/fix.diff; supersedes finding proposal.
Verdict
REPRODUCED. Source-trace at sys/dev/drm/amd/powerplay/amd_powerplay.c:685-693 confirms pp_dpm_set_pp_table does kmemdup(soft_pp_table, soft_pp_table_size=u16 from VBIOS, GFP_KERNEL) [typ. 200-2000B] then memcpy(hardcode_pp_table, buf, size) where size comes from sysfs write (amdgpu_pm.c:463-475), up to PAGE_SIZE=4096. NO check size <= soft_pp_table_size. Up to ~3800 bytes OOB into kmalloc-* slab. The /sys/class/drm/cardN/device/pp_table node is S_IWUSR (root-writable) -> root-to-kernel heap corruption; on 2nd invocation the kmemdup is skipped (table already non-NULL) but the unchecked memcpy still fires. Harness replicates the overflow math (4096 into 2000-byte alloc = 2096 OOB).
No comments yet.