# DF-3619 VERDICT — Heap OOB write in ci_get_svi2_voltage_table

## Bottom line

**Real bug, source-certain; runtime inconclusive on this guest (no AMD GPU).**

The sink is `ci_get_svi2_voltage_table()` at `sys/dev/drm/amd/amdgpu/ci_dpm.c:2264-2268`:

```c
voltage_table->count = voltage_dependency_table->count;      /* u8 from VBIOS, ≤255 */
for (i = 0; i < voltage_table->count; i++) {
        voltage_table->entries[i].value = voltage_dependency_table->entries[i].v;
        voltage_table->entries[i].smio_low = 0;
}
```

`entries[]` is fixed at `MAX_VOLTAGE_ENTRIES == 32`
(`sys/dev/drm/amd/amdgpu/amdgpu_atombios.h:119-133`). The count comes from
`amdgpu_parse_clk_voltage_dep_table()` (`sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:285-302`),
which trusts the u8 `ucNumEntries` from the VBIOS PowerPlay table — up to
**255**, fully attacker-chosen in the crafted-VBIOS model. The GPIO-LUT path
(`amdgpu_atombios_get_voltage_table`, amdgpu_atombios.c:1539-1540) *does*
bound-check `> MAX_VOLTAGE_ENTRIES`; the SVI2 path does not — that asymmetry
is the bug.

## Reachability (traced)

- `ci_dpm_init()` (ci_dpm.c:6017-6041): `voltage_control/vddci_control/mvdd_control`
  become `CISLANDS_VOLTAGE_CONTROL_BY_SVID2` when the VBIOS has SVID2 voltage
  objects — attacker includes them.
- `ci_dpm_hw_init()` → `ci_dpm_enable()` (ci_dpm.c:5300-5307) →
  `ci_construct_voltage_tables()` (ci_dpm.c:2273-2333) → the three
  `ci_get_svi2_voltage_table()` calls (2285/2303/2321) — the copy happens
  *before* the `ci_trim_voltage_table_to_fit_state_table()` calls
  (2292-2294 / 2310-2312 / 2328-2330) that reduce the count to
  SMU7_MAX_LEVELS_{VDDC,VDDCI,MVDD} = 8/4/4. Trims cannot undo the overflow.
- Also re-entered on every resume (`ci_dpm_resume` → `ci_dpm_enable`).

## Overflow geometry (proven by executing the exact struct layouts on the guest)

`struct ci_power_info` tail (ci_dpm.h:230-297): the three 268-byte
`atom_voltage_table`s are followed by `ulv`, pointers, caps, and driver-state
structs (392 more bytes to the end of the object).

| variant | OOB past entries[32] starts at | past END of ci_power_info alloc | max bytes past object (count=255) |
|---|---|---|---|
| vddc  (ci_dpm.c:2285) | count ≥ 33 | count ≥ 149 | 852 |
| vddci (ci_dpm.c:2303) | count ≥ 33 | count ≥ 116 | 1120 |
| mvdd  (ci_dpm.c:2321) | count ≥ 33 | count ≥ 82  | 1388 |

Write pattern per 8-byte slot: u16 attacker value at +0 (VBIOS `usVoltage`),
u32 0 at +4. Clobbers in-object members including the `powertune_defaults`
pointer (forged to `0x0000beef00000000` in the live guard test → wild
pointer deref in `ci_populate_svi_load_line`, ci_dpm.c:445-448), and past
the allocation into adjacent heap for the vddci/mvdd variants.

## Exploit chain (why this matters beyond a panic)

In the crafted-VBIOS threat model the attacker additionally controls heap
grooming timing at driver bind (kzalloc order is deterministic during
amdgpu init). A count in the 82-255 range yields a linear heap overwrite of
up to ~1.4 KB with 2 controlled bytes per 8 — sufficient to corrupt
adjacent allocations' length fields/pointers toward UAF/arb-write, though
the u32-zero-every-8-bytes pattern makes it markedly harder than a dense
overwrite. At minimum: reliable kernel memory corruption / panic at boot.

## Verification narrative

- Guest: `DragonFly dfbsd 6.5-DEVELOPMENT #0 ... x86_64`; virtio-only;
  `vgapci0 chip=0x11111234` (ramfb) — **no AMD ASIC** → ci_dpm unreachable
  at runtime → status **inconclusive** (not false-positive).
- `/boot/kernel/amdgpu.ko` present (124,519,320 bytes) and loads cleanly
  (`[drm] amdgpu kernel modesetting enabled.`) — module-presence check done.
  (Unloading the 124 MB module wedges the DragonFly linker; guest was reset
  with `vm.sh reset with-src` afterward.)
- The logic/geometry PoC (`ci_svi2_overflow.c`) replicates the exact member
  sequence of ci_dpm.h:230-297 + amdgpu_atombios.h structs and the exact
  sink loop, compiled and executed **on the guest** (`build.log`/`run.log`):
  confirms 1784 bytes past `entries[32]`, past-object thresholds 149/116/82,
  and 48/64 guard bytes corrupted behind the allocation in the live run.

## Fix validation

Not possible on this guest (hardware absent): `fix_status: not_testable`.
`fix.diff` adds the same `> MAX_VOLTAGE_ENTRIES → -EINVAL` bound the GPIO
path already has.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
