num_levels==0 count-1 underflow: kv_dpm_get_sclk (:2938) and kv_set_valid_clock_range (:1791/:1817) read levels[0xFFFFFFFF] ~48 GB out (panic; unprivileged ioctl trigger)
Summary
kv_parse_power_table sets ps->num_levels only for valid clock entries; a VBIOS state with ucNumDPMLevels==0 or all-invalid clockInfoIndex[] keeps num_levels==0 - parse-legal, no error. kv_dpm_get_sclk then evaluates levels[num_levels-1] = levels + ~48 GB - wild kernel read -> page fault -> panic. Reachable UNPRIVILEGED once such a state is requested: AMDGPU_INFO ioctl (DRM_AUTH|DRM_RENDER_ALLOW) and every DCE8 modeset. Same underflow in kv_set_valid_clock_range on the first power-state transition. Count-1 family twin of DF-3686/DF-3858. VERIFIED: harness prints levels[4294967295] + 51.5GB offset; crash mode SIGSEGV on the verbatim read.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3888 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| num_levels_underflow.c | β | 3.4 KB | view raw | |
| build.sh | β | 95 B | view raw | |
| run.sh | β | 116 B | view raw | |
| build.log | β | 107 B | view raw | |
| run.log | β | 280 B | view raw | |
| run.2.log | β | 69 B | view raw | |
| env.txt | β | 561 B | view raw | |
| fix.diff | β | 933 B | view raw | |
| VERDICT.md | β | 4.1 KB | β raw | |
| README.md | β | 1.7 KB | β raw | |
| verdict.json | β | 3.0 KB | view raw |
DF-3888 β num_levels == 0 β levels[num_levels - 1] wild read (+~48 GB) in kv_dpm_get_sclk / kv_set_valid_clock_range
File: sys/dev/drm/amd/amdgpu/kv_dpm.c:2938 (get_sclk), :1791/:1817 (set_valid_clock_range)
Class: CWE-191 unsigned count-1 underflow β wild OOB read (count-1 family: DF-3686/DF-3858 twins)
Verdict: inconclusive at runtime on this guest (no AMD GPU); source-certain; harness proves the index/offset and SIGSEGV on the verbatim access.
Build (on the DragonFly guest, as root)
mkdir -p /tmp/DF-3888 && cp num_levels_underflow.c /tmp/DF-3888/h.c cd /tmp/DF-3888 && cc -Wall -O0 -o t h.c
Run
./t # accounting β index and byte offset ./t crash # verbatim read -> SIGSEGV (exit 139)
Expected
- accounting:
num_levels = 0;levels[num_levels - 1] = levels[4294967295]; byte offset4294967295 * 12 = 51539607540(~51.5 GB) pastlevelsβ far outside the 5-entry array (60 bytes) insidekv_power_info. - crash:
Segmentation fault, exit=139 (kernel equivalent: page fault in kernel mode β panic).
Kernel trigger
A parsed power state whose ucNumDPMLevels == 0 (or all clockInfoIndex entries
>= ucNumEntries, continue at kv_dpm.c:2758-2759) keeps ps->num_levels == 0
from kzalloc. Sinks, once such a state becomes the requested state:
kv_dpm_get_sclk(handle, false):2938 β unprivileged via theAMDGPU_INFOioctl (AMDGPU_INFO_DEV_INFO, amdgpu_kms.c:700,DRM_AUTH|DRM_RENDER_ALLOW) and display modeset (dce_v8_0.c:971);kv_set_valid_clock_range:1791 / :1817 (else-branch), on the first power state transition to it (needscheck_state_equalto differ, kv_dpm.c:3261).
DF-3888 β verdict narrative
Finding
kv_parse_power_table only sets ps->num_levels inside
kv_parse_pplib_clock_info (kv_dpm.c:2694: ps->num_levels = index + 1), which is
called only for valid clock entries β the state loop continues on
clock_array_index >= clock_info_array->ucNumEntries (:2758-2759) and breaks at
k >= SUMO_MAX_HARDWARE_POWERLEVELS (:2760). A VBIOS state with
ucNumDPMLevels == 0, or whose clockInfoIndex[] entries are all out of range,
therefore leaves the kzalloc-zeroed struct kv_ps with num_levels == 0 β
parse-legal, no error.
Use sites then compute levels[num_levels - 1] with unsigned num_levels (u32):
kv_dpm_get_sclk(kv_dpm.c:2929-2939):return requested_state->levels[requested_state->num_levels - 1].sclk;(:2938) βlevels[0xFFFFFFFF]βlevels + 51,539,607,540bytes (~48 GB) β wild read.requested_stateiskv_get_ps(&pi->requested_rps); after any power-state selection that lands on a 0-level state,pi->requested_ps.num_levels == 0.kv_set_valid_clock_range(:1772-1832):new_ps->levels[new_ps->num_levels - 1].sclkat :1791 (vddc-table branch) and :1817 (sumo-table branch) β same wild read, plus the comparison chain at :1797-1798/:1822-1826 readstable->entries[highest_valid]with ahighest_validderived from the poisoned loop.
Impact: page fault in kernel mode β panic (the ~48 GB offset is never mapped); not a leak (value unusable). DoS.
Trigger reach: on a machine whose (crafted) VBIOS contains such a state, the
read is reachable unprivileged: the AMDGPU_INFO ioctl
(AMDGPU_INFO_DEV_INFO β amdgpu_dpm_get_sclk(adev, false), amdgpu_kms.c:700,
DRM_IOCTL_DEF_DRV(AMDGPU_INFO, ..., DRM_AUTH|DRM_RENDER_ALLOW) :1207) β i.e. any
user holding the render node; also every DCE8 modeset (dce_v8_0.c:971).
The kv_set_valid_clock_range sink additionally needs the state to survive
kv_check_state_equal (kv_dpm.c:3261: differs from current β first transition).
This is the count-1-underflow family already filed for the powerplay hwmgrs
(DF-3686 smu7, DF-3858 vega20); kv_dpm.c is the legacy-DPM twin, unfixed here.
Root cause (kernel references)
sys/dev/drm/amd/amdgpu/kv_dpm.c:2938βlevels[num_levels - 1]in get_sclksys/dev/drm/amd/amdgpu/kv_dpm.c:1791,:1817β same in set_valid_clock_rangesys/dev/drm/amd/amdgpu/kv_dpm.c:2694+:2756-2769β num_levels only set for valid entriessys/dev/drm/amd/amdgpu/amdgpu_kms.c:700β unprivileged ioctl caller
What was verified, and how
- Source-level certainty. Full-file pass-2 audit; struct geometry
(
struct kv_pl= 12 bytes,levels[5]) from kv_dpm.h; caller chain verified in amdgpu_pm.c (amdgpu_dpm_change_power_state_locked:1895-1945 β pre_set β check_state_equal β set_power_state) and amdgpu_kms.c (ioctl flags :1207). - Harness on the guest (DragonFly 6.5-DEVELOPMENT x86_64, base cc 8.3):
num_levels_underflow.creplicateskv_dpm_get_sclkverbatim with 1:1 layouts. Accounting mode prints index4294967295, offset51539607540bytes (~51.5 GB); crash mode executes the verbatim read against a single-page mapping β SIGSEGV exit=139 (kernel equivalent: page fault in kernel mode β panic). - Hardware absence. Guest is virtio-only; no AMD GPU, amdgpu not loaded. The in-kernel trigger needs a Kaveri/Kabini/Mullins APU with a crafted VBIOS. Recorded inconclusive, impact none on this guest (per pass-2 spawn instructions), source + harness: certain.
Fix validation
fix.diff guards num_levels == 0 (and NULL priv) in kv_dpm_get_sclk and adds an
early return in kv_set_valid_clock_range. Not built on the guest (no AMD hardware)
β fix_status: not_testable.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testablefix.diff authored against the read-only sys/ tree after source+harness verification; not built (no AMD hardware on guest).
findings/poc/DF-3888/fix.diff
Confirmed kernel references
Detail
Exploit chain
crafted-VBIOS state with 0 valid clock levels becomes requested_ps -> unprivileged AMDGPU_INFO ioctl -> amdgpu_dpm_get_sclk(false) -> kv_dpm_get_sclk reads levels[0xFFFFFFFF] (+48 GB) -> kernel page fault -> panic (DoS). Wild-read value is not exfiltrable (offset unmapped).
Evidence (decisive lines)
run.log: 'num_levels = 0', 'levels[4294967295]', 'levels + 51539607540 bytes (~51.5 GB)'; run.2.log: crash mode Segmentation fault exit=139; VERDICT.md; fix.diff
PoC changes
No seed (pass-2 self-authored); single guest build+run cycle succeeded.
Verified recommended fix
Guard num_levels==0 (and NULL ps_priv) in kv_dpm_get_sclk (kv_dpm.c:2935) and early-return in kv_set_valid_clock_range (:1781).
Verdict
Source-certain unsigned count-1 underflow (DF-3686/DF-3858 family, legacy-DPM twin); runtime-inconclusive on this guest (no AMD GPU). kv_parse_power_table only sets ps->num_levels for valid clock entries (kv_dpm.c:2694, loop :2756-2769 with continue at :2758-2759), so a VBIOS state with ucNumDPMLevels==0 or all-invalid clockInfoIndex[] leaves num_levels==0 from kzalloc. kv_dpm_get_sclk (:2938) then reads levels[num_levels-1] = levels[0xFFFFFFFF] = levels + 51,539,607,540 bytes (~48 GB) - a wild kernel read -> page fault -> panic; reachable UNPRIVILEGED on such a machine via the AMDGPU_INFO ioctl AMDGPU_INFO_DEV_INFO (amdgpu_kms.c:700, DRM_AUTH|DRM_RENDER_ALLOW) and every DCE8 modeset (dce_v8_0.c:971). Same underflow at kv_set_valid_clock_range :1791/:1817 on the first transition to such a state. Harness prints index 4294967295 / offset 51539607540 and crash mode SIGSEGVs on the verbatim read (kernel equivalent: panic).
No comments yet.