# DF-1306 — Unbounded `nonClockInfoIndex` 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`, lines 2668-2670:

```c
non_clock_array_index = power_state->v2.nonClockInfoIndex;   /* u8 0..255, VBIOS */
non_clock_info = (struct _ATOM_PPLIB_NONCLOCK_INFO *)
    &non_clock_info_array->nonClockInfo[non_clock_array_index];   /* NO bounds check */
```

`non_clock_array_index` comes straight from the VBIOS (`ATOM_PPLIB_STATE_V2.
nonClockInfoIndex`, a u8) and is used to index `nonClockInfo[]` with **no
bounds check** against `non_clock_info_array->ucNumEntries`.

`nonClockInfo` is a flex[1] array of `ATOM_PPLIB_NONCLOCK_INFO` entries
(`pptable.h:298-309`, 24 bytes packed; `pptable.h:456-464`). With
`nonClockInfoIndex=255` and `ucNumEntries=1`, the access reads at byte offset
`255 * 24 = 6120` past `nonClockInfo[0]` → **heap OOB read** off the kmalloc'd
bios buffer.

**Contrast — the clock path 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 non-clock path (2668-2670) was simply never given the equivalent guard —
this is the regression/inconsistency that the finding calls out. Sibling of
DF-1269 (same pattern in other radeon DPM drivers) and DF-1307 (the VCE loop
two lines down, same function).

## Reachability / threat model

`kv_parse_power_table` runs at `radeon` driver attach on Kabini/Kaveri/
Mullins/Trinity (Southern/Sea Islands APUs), parsing the powerplay tables from
the GPU VBIOS. 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) 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 `NonClockInfoArray` + `ATOM_PPLIB_NONCLOCK_INFO`
verbatim, places `nonClockInfo[0]` at the tail of a page-backed region with the
next page unmapped, sets `nonClockInfoIndex=255` (the crafted-VBIOS value),
and shows the read at `nonClockInfo[255]` faults off the buffer. Decisive run:

```
DF-1306 kv_parse_power_table nonClockInfoIndex OOB read harness
sizeof(ATOM_PPLIB_NONCLOCK_INFO)=28  sizeof(NonClockInfoArray)=32
NonClockInfoArray @ 0x80047cfe0 (nonClockInfo[0] @ 0x80047cfe4), ucNumEntries=1
VBIOS nonClockInfoIndex = 255  (NO check vs ucNumEntries=1)
access byte offset = 255 * 28 = 7140  (past the single valid entry)
FAULT (signal 11): OOB read at nonClockInfo[255] off the bios buffer
  -> in-kernel equivalent: heap OOB read up to 7140 bytes past nonClockInfo[0]
RESULT: OOB read CONFIRMED at kv_dpm.c:2670 (nonClockInfoIndex used with NO bounds check vs ucNumEntries)
```

(28 bytes/entry here is the unpacked size; the kernel ATOM struct is packed to
24, giving the finding's 6120-byte ceiling — same bug either way.)

## 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 index is used, and frees the already-allocated `dpm.ps`
on the error path (matching the existing `kfree(rdev->pm.dpm.ps); return
-ENOMEM;` pattern at line 2675):

```c
if (non_clock_array_index >= non_clock_info_array->ucNumEntries) {
    kfree(rdev->pm.dpm.ps);
    return -EINVAL;
}
```

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