# DF-1307 — Unbounded VCE `clk_idx` heap OOB read in `kv_parse_power_table`

## Verdict: REPRODUCED (source-level + harness) — latent radeon-DRM bug, heap OOB read

The radeon DPM code (`kv_dpm.c`) is part of the `radeon` DRM module, which is
**not in `X86_64_GENERIC`** and **no AMD GPU is present** on the audit guest.
It is a **real latent bug** confirmed by source trace and reproduced at the
access-pattern level with a userspace harness.

## The bug

`sys/dev/drm/radeon/kv_dpm.c`, `kv_parse_power_table`, VCE power-state fill
loop, lines 2705-2707:

```c
for (i = 0; i < RADEON_MAX_VCE_LEVELS; i++) {
    u32 sclk;
    clock_array_index = rdev->pm.dpm.vce_states[i].clk_idx;   /* 6-bit 0..63, VBIOS */
    clock_info = (union pplib_clock_info *)
        &clock_info_array->clockInfo[clock_array_index * clock_info_array->ucEntrySize]; /* NO bounds check */
    ...
}
```

`clk_idx` originates from the VBIOS VCE state record:

```c
/* r600_dpm.c:1127-1128 */
rdev->pm.dpm.vce_states[i].clk_idx = state_entry->ucClockInfoIndex & 0x3f;  /* 6-bit, 0..63 */
```

It is used to **byte-index** `clockInfo[]` (a `UCHAR` flex[1] array,
`pptable.h:446-454`) at offset `clk_idx * ucEntrySize`, with **no bounds check**
against `clock_info_array->ucNumEntries`. With `clk_idx` up to 63 and
`ucEntrySize` up to 255 (UCHAR), the byte offset reaches `63 * 255 = 16065`
past `clockInfo[0]` → **heap OOB read** off the kmalloc'd bios buffer.

**Contrast — the main clock loop at the same function DOES check (line 2683):**
```c
clock_array_index = idx[j];
if (clock_array_index >= clock_info_array->ucNumEntries)   /* <-- bounds check */
    continue;
```
The VCE loop at 2705 was never given the equivalent guard. Sibling of DF-1269
and DF-1306 (the non-clock path, same function, same pattern of missing check).

## Reachability / threat model

`kv_parse_power_table` runs at `radeon` driver attach on Kabini/Kaveri/
Mullins/Trinity (Southern/Sea Islands APUs). Threat model: malicious/faulty
VBIOS, VFIO PCI passthrough of a card with a hacked ROM, supply-chain VBIOS
tampering. Effect: **kernel heap OOB read** (info leak of adjacent slab/heap
contents, up to ~16 KB) and/or DoS (read past mapped bios buffer → panic).
Local, requires attacker control of the VBIOS image — the same trust boundary
the driver already assumes.

## Harness proof

`harness.c` replicates `ClockInfoArray` + the VCE loop's byte-index access,
places `clockInfo[0]` at the tail of a data page with a 5-page `PROT_NONE`
guard region after it (the 16065-byte OOB would skip a single guard page), sets
`clk_idx=63` + `ucEntrySize=255` (the crafted-VBIOS amplification), and shows
the read at byte offset 16065 faults. Decisive run:

```
DF-1307 kv_parse_power_table VCE clk_idx OOB read harness
sizeof(ClockInfoArray) overhead = 3 (clockInfo is UCHAR flex[1])
ClockInfoArray @ 0x8004a0fee (clockInfo[0] @ 0x8004a0ff0), ucNumEntries=2, ucEntrySize=255
VBIOS VCE clk_idx = 63  (NO check vs ucNumEntries=2)
access byte offset = 63 * 255 = 16065  (past the 2 valid entries)
FAULT (signal 11): OOB read at clockInfo[16065] off the bios buffer
  -> in-kernel equivalent: heap OOB read up to 16065 bytes past clockInfo[0]
RESULT: OOB read CONFIRMED at kv_dpm.c:2707 (VCE clk_idx byte-index with NO bounds check vs ucNumEntries)
```

`noinline` + `volatile` inputs prevent gcc `-O2` from constant-folding the
array index and eliding the OOB load as UB.

## Build & run

```
./build.sh   # cc -O2 -Wall -o harness harness.c
./run.sh     # ./harness
```

## Fix

`fix.diff` adds the bounds check mirroring the clock path at line 2683, right
before the unbounded byte-index access, and zeroes the VCE state on the error
path (matching the loop's existing `sclk=0; mclk=0;` epilogue at line 2710-2711):

```c
if (clock_array_index >= clock_info_array->ucNumEntries) {
    rdev->pm.dpm.vce_states[i].sclk = 0;
    rdev->pm.dpm.vce_states[i].mclk = 0;
    continue;
}
```

This **matches the finding proposal** ("check clock_array_index < ucNumEntries").
