# DF-1150 — Heap OOB write in `ci_setup_default_dpm_tables` (radeon ci_dpm.c)

## Verdict
**REPRODUCED (code-level / harness), latent at runtime on this guest.** The bug
is a genuine unbounded-copy-into-fixed-array heap OOB write, confirmed by
line-by-line source trace and a userspace harness that reproduces the exact
indexing. It is **not triggerable at runtime on this guest** (radeon is
`optional radeon drm`, NOT in `X86_64_GENERIC`, and the guest has no AMD GPU —
only QEMU std VGA `0x1234:0x1111`). This is the **valid hard blocker:
runtime-unreachable on this guest (no matching hardware)** — a latent
HW-gated write. No `exploit.c` was written: this is not a userspace-reachable
primitive (no unprivileged guest syscall can inject a VBIOS).

## Mechanism (trigger → primitive → effect)
- **Source array:** `struct ci_single_dpm_table { u32 count; struct ci_dpm_level dpm_levels[MAX_REGULAR_DPM_NUMBER]; }` with `MAX_REGULAR_DPM_NUMBER = 8` (`sys/dev/drm/radeon/ci_dpm.h:59,64`). `ci_dpm_table` holds five such tables (sclk/mclk/vddc/vddci/mvdd).
- **Attacker-controlled count:** the per-table entry counts come straight from the VBIOS dependency tables (`allowed_sclk_vddc_table->count`, `allowed_mclk_table->count`) — `u8` up to 255 — stored unclamped.
- **Unbounded loops into fixed arrays** in `ci_setup_default_dpm_tables` (`sys/dev/drm/radeon/ci_dpm.c`):
  - sclk dedup loop `:3514` writes `dpm_levels[count]`, `count++` per distinct entry → up to 255 writes into `[8]`.
  - mclk dedup loop `:3527` (same shape).
  - vddc loop `:3539` writes `vddc_table.dpm_levels[i]` for `i` in `[0, count)` (direct index), **and OOB-reads `std_voltage_table->entries[i]`**; `.count` set to the unbounded count `:3546`.
  - vddci loop `:3550` + `.count` `:3555`; mvdd loop `:3560` + `.count` `:3565` (identical, unbounded).
- **Effect:** with a crafted VBIOS (count>8) the writes run off each 8-entry `dpm_levels[]` into the next `ci_single_dpm_table` and past `struct ci_dpm_table` into adjacent `ci_power_info` heap fields.
- The `ci_reset_single_dpm_table` helper (`:3408`) only zeros `dpm_levels[0..7]`; it does **not** bound the subsequent fill loops.

## Evidence (harness)
`harness.c` (run as unprivileged `maxx`) models all five loops with a crafted
VBIOS count of 255. Result: **1235 writes land at `dpm_levels[8..]`** across the
five loops. With the fix (`&& i < MAX_REGULAR_DPM_NUMBER`), the OOB count drops
to 0. (Full output in `run.log`.)

## Threat model / reachability
- **Attacker:** malicious/reflashed VBIOS, or a malicious PCIe/Thunderbolt AMD Sea Islands GPU whose VBIOS dependency tables ship `count > 8`. Reached at DPM init (`ci_dpm_enable` → `ci_setup_default_dpm_tables`).
- **On this guest:** NOT reachable. radeon is not in GENERIC; no AMD GPU. The realistic impact ceiling (on physical AMD CIK hardware) is kernel heap corruption controllable via crafted VBIOS → kernel panic / DoS; uid0 escalation would require a separate slab-layout-control primitive.

## Exploit chain
None developed — **valid hard blocker**: the primitive is only reachable on
physical AMD Sea Islands hardware (or VFIO passthrough) with a malicious VBIOS,
none of which exist on this QEMU/KVM guest. There is no unprivileged-guest
syscall path to inject a VBIOS. Per the bright-line rule this is a latent
HW-dependent write, not a default-GENERIC unpriv→kernel chain.

## PoC changes
Authored from scratch (the dir was empty). Deliverables: `harness.c`, `fix.diff`
(clamp each loop to `MAX_REGULAR_DPM_NUMBER` + clamp `.count`), `build.sh`,
`run.sh`, `VERDICT.md`, `manifest.json`, `env.txt`, `build.log`, `run.log`.

## Recommended fix
`fix.diff` adds `&& i < MAX_REGULAR_DPM_NUMBER` to each of the five loop
conditions (`:3514/:3527/:3539/:3550/:3560`) and clamps the three trailing
`.count` assignments (`:3546/:3555/:3565`) to at most `MAX_REGULAR_DPM_NUMBER`.
This **matches the finding proposal** ("upper-bound all counts against
MAX_REGULAR_DPM_NUMBER"). Validated: applies + compiles clean (full `radeon.ko`
module build, `ci_dpm.o` linked, `-Werror`) — see `build.log`.

## Fix validation (Phase 8)
- `fix.diff` applies cleanly (`git apply --check` OK; `patch -p1` in-guest OK, 6 clamps).
- Full `radeon.ko` module build (which generates the forwarder dir + compiles ci_dpm.o): `cd /usr/src/sys/dev/drm/radeon && make` → **rc=0**, `radeon.ko` (2029192 B) produced, `ci_dpm.o` compiled & linked, 0 errors under `-Werror`.
- `fix_status: not_testable` for runtime: radeon is not in GENERIC and there is no AMD GPU on the guest, so the live trigger cannot run. The fix is validated at apply + compile level: with the clamps, every loop iterates at most `i < MAX_REGULAR_DPM_NUMBER` and `.count` is capped, so no write reaches `dpm_levels[8]`. On physical CIK hardware the patched module would no longer overflow.

## Kernel references (confirmed during verification)
- `sys/dev/drm/radeon/ci_dpm.h:59` — `#define MAX_REGULAR_DPM_NUMBER 8`
- `sys/dev/drm/radeon/ci_dpm.h:64` — `dpm_levels[MAX_REGULAR_DPM_NUMBER]`
- `sys/dev/drm/radeon/ci_dpm.c:3514` — sclk dedup loop (unbounded)
- `sys/dev/drm/radeon/ci_dpm.c:3527` — mclk dedup loop
- `sys/dev/drm/radeon/ci_dpm.c:3539` — vddc loop (direct index + std_voltage OOB read)
- `sys/dev/drm/radeon/ci_dpm.c:3550` — vddci loop
- `sys/dev/drm/radeon/ci_dpm.c:3560` — mvdd loop
- `sys/dev/drm/radeon/ci_dpm.c:3408` — `ci_reset_single_dpm_table` (no bound on fill loops)
