# DF-3954 — radeon kv_dpm `num_levels==0` → `levels[0xFFFFFFFF]` wild read (~48 GiB)

**File:** `sys/dev/drm/radeon/kv_dpm.c` (pass 2, GLM 5.3)
**Severity:** Medium · **CWE:** CWE-191/CWE-125 (integer underflow → OOB read)
**Confidence:** certain (source) · **Status:** inconclusive on guest (no AMD GPU);
arithmetic-simulation harness verified.

## What

`kv_parse_power_table()` (kv_dpm.c:2681-2694) advances `k` only for clock
entries whose `clockInfoIndex < clock_info_array->ucNumEntries` and only while
`k < SUMO_MAX_HARDWARE_POWERLEVELS`. A VBIOS power state with
`ucNumDPMLevels == 0` — or with **every** clockInfoIndex out of range (`continue`)
— never calls `kv_parse_pplib_clock_info()`, so the kzalloc'd `struct kv_ps`
keeps `num_levels == 0` (`u32`, kv_dpm.h:82; `levels[]` is the FIRST member).

Consumers index `levels[num_levels - 1]` with u32 arithmetic → `0 - 1 =
0xFFFFFFFF` → `levels + 0xFFFFFFFF × sizeof(struct kv_pl)=12` = **+0xBFFFFFFF4
(~48 GiB)** past the heap object:

- `kv_set_valid_clock_range()` — kv_dpm.c:1721 (`if-path`) and :1745-1747
  (else-path) — reached from `kv_dpm_set_power_state()` (:1866/:1895) whenever
  such a state is picked as requested
- `kv_dpm_get_sclk()` — kv_dpm.c:2886 — reached from `dce8_bandwidth_update`
  (cik.c:9287/9327) after that state became requested

A zero-level state is pickable: `radeon_dpm_pick_power_state()`
(radeon_pm.c:950-1051) matches on `ps->class` only, never `num_levels`.

## Reproduce

```
cd findings/poc/DF-3954
./build.sh          # cc -O2 -Wall -o trigger_df3954 trigger_df3954.c
./run.sh
```

Expected (crash mode): `num_levels(u32) = 0 => index = 0xffffffff =>
displacement = 0xbfffffff4 (48.00 GiB)`, child SIGSEGV sig=11,
`RESULT: WILD READ FAULTS AS PREDICTED`. Kernel equivalent: wild kernel read
~48 GiB past `kv_ps` → page fault → panic.

## Threat model

Crafted VBIOS on a KV/BT/ML APU (physical/root/supply-chain). Boot- or
mode-set-time panic (DoS). No unprivileged-local vector in radeon (unlike the
amdgpu twin DF-3888, which had an info-ioctl caller). The wild read is a
*load* only — the value feeds clock comparisons, no leak channel.

## Fix

Reject zero-level states at parse time and guard the consumers (fix.diff).
