# DF-1141 — Heap OOB write in `ci_setup_default_dpm_tables` (amdgpu ci_dpm.c)

## Verdict
**REPRODUCED (code-level, latent at runtime).** The bug is a genuine
unbounded-copy-into-fixed-array heap OOB write, confirmed by source trace.
It is **not triggerable at runtime on this guest** (no AMD GPU; amdgpu is not
in GENERIC), so runtime impact is **not_testable** here. The `fix.diff` was
validated to **apply + compile** cleanly under `-Werror`.

## 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/amd/amdgpu/ci_dpm.h:60,65`). `ci_dpm_table` holds five such tables (sclk/mclk/vddc/vddci/mvdd) back-to-back (`ci_dpm.h:68-75`).
- **Attacker-controlled count:** the per-table entry counts come straight from VBIOS PowerPlay `ATOM_PPLIB_Clock_Voltage_Dependency_Table.ucNumEntries` (a `u8`, i.e. up to 255) via `amdgpu_parse_clk_voltage_dep_table` (`sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:282-304`), which stores `atom_table->ucNumEntries` into `amdgpu_table->count` **with no upper bound** (`amdgpu_dpm.c:302`).
- **Unbounded loops into fixed arrays** in `ci_setup_default_dpm_tables` (`sys/dev/drm/amd/amdgpu/ci_dpm.c:3582`):
  - sclk dedup loop `:3621` writes `dpm_levels[count]` with `count` derived from `allowed_sclk_vddc_table->count` (write index `count++` on each distinct entry -> up to `ucNumEntries` writes into `[8]`).
  - mclk dedup loop `:3634` (same shape).
  - vddc loop `:3646` writes `vddc_table.dpm_levels[i]` for `i` in `[0, allowed_sclk_vddc_table->count)` (index = `i`, unbounded), then sets `vddc_table.count = allowed_sclk_vddc_table->count` (`:3653`).
  - vddci loop `:3657` + `.count` `:3662`; mvdd loop `:3667` + `.count` `:3672` (identical, unbounded).
- **Effect:** with a crafted VBIOS (`ucNumEntries > 8`), the writes run off the end of each 8-entry `dpm_levels[]` into the next `ci_single_dpm_table` (overwriting the next table's `count` + `dpm_levels`) and ultimately past `struct ci_dpm_table` into adjacent `ci_power_info` fields (`golden_dpm_table`, `voltage_control`, SMC offsets). The corrupted counts then drive the secondary population loops in `ci_populate_all_graphic/memory_levels` past `GraphicsLevel[8]`/`MemoryLevel[6]`.

## Threat model / reachability
- **Attacker:** malicious/reflashed VBIOS, or a malicious PCIe/Thunderbolt AMD GPU whose VBIOS PowerPlay tables ship `ucNumEntries > 8`. Reached at DPM init (`ci_dpm_enable` -> `ci_setup_default_dpm_tables`).
- **On this guest:** NOT reachable. `pciconf` shows only QEMU std VGA (`0x1234:0x1111`); amdgpu is not in `X86_64_GENERIC`. This is the **valid hard blocker: runtime-unreachable on this guest (no matching hardware)** — a latent bug. The realistic impact ceiling (on physical AMD CIK hardware) is kernel heap corruption controllable via crafted VBIOS.

## Exploit chain
None developed — **valid hard blocker**: the primitive is only reachable on
physical AMD Sea Islands hardware (or VFIO-passthrough of such a GPU) 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; the demonstrated work is the source-level confirmation + compiling
fix. (No `exploit.c` written — not a userspace-reachable primitive.)

## PoC changes
No trigger PoC exists (none was seeded for this latent finding). This folder
adds: `fix.diff` (clamp each loop to `MAX_REGULAR_DPM_NUMBER`), `build.sh`
(apply fix + incremental compile of `ci_dpm.o`), `run.sh` (documents the
runtime-unreachable status), `VERDICT.md`, `manifest.json`, `env.txt`,
`build.log`.

## Recommended fix
Clamp every loop in `ci_setup_default_dpm_tables` to `MAX_REGULAR_DPM_NUMBER`
and clamp the trailing `.count` assignments. Implemented in `fix.diff`:
- `:3621`/`:3634`/`:3646`/`:3657`/`:3667` loop conditions gain `&& i < MAX_REGULAR_DPM_NUMBER`;
- `:3653`/`:3662`/`:3672` `.count` set to `min((u32)<src>->count, (u32)MAX_REGULAR_DPM_NUMBER)`.

This **matches the finding proposal** ("clamp each loop to
MAX_REGULAR_DPM_NUMBER"). `min`/`min_t` come from the DRM linux-compat header
`sys/dev/drm/include/linux/kernel.h` already force-included by the module.
