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

Stack OOB write in pp_dpm_get_pp_num_states when hwmgr->num_ps > 16

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.

  1. Hardware supply-chain / malicious peripheral: an attacker attaches an AMD GPU whose VBIOS PowerPlayInfo state-array header advertises ucNumEntries > 16 (a single malformed byte in the option ROM). On host boot, the driver parses it, sets num_ps to the claimed value, and any subsequent read of /sys/class/drm/cardN/device/pp_num_states (mode 0444, world-readable β€” amdgpu_pm.c:1002) or pp_cur_state (also S_IRUGO) by an unprivileged user corrupts the kernel stack and typically panics the box.

  2. 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 crafted ATOM_PPLIB table whose ucNumEntries is > 16; amd_powerplay_reset() re-parses it into hwmgr->num_ps; any subsequent unprivileged read of pp_num_states triggers 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].

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.

  • DF-1560 (sibling): pp_dpm_set_pp_table heap overflow β€” can be used to plant the malformed table that triggers this finding.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1561 Β· 8 files
FileTypeDescriptionSize
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
README.md readme human-readable summary
↓ download 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.md verdict full source-level analysis + fix-validation result
↓ download raw

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 in fix_apply.log).
  • Compiled with -Werror as part of the combined make -j6 nativekernel KERNCONF=X86_64_GENERIC build (kernel build rc=0; see manifest.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

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.