# DF-1574 — Heap OOB write in get_vddc_lookup_table (process_pptables_v1_0.c)

## Verdict

**NOT REPRODUCED ON THIS GUEST** — the bug is real in source (line-by-line
confirmed), but the affected translation unit is part of the `amdgpu` KLD
module which is not in `X86_64_GENERIC` and is not loaded on this QEMU
guest (no AMD GPU hardware present). This is a *latent* finding: a real
OOB write that is unreachable at runtime on this guest and cannot be
exercised by any harness we can build here without either (a) `kldload
amdgpu` (root-only) or (b) AMD GPU hardware with a crafted VBIOS. The
fix is authored, applies cleanly, and compiles cleanly with the kernel's
own `-Werror` CFLAGS.

## The bug (confirmed in source)

`sys/dev/drm/amd/powerplay/hwmgr/process_pptables_v1_0.c`, function
`get_vddc_lookup_table` (lines 153-195):

* Line 168-169 — allocation uses the *caller's* `max_levels`:
  `table_size = sizeof(uint32_t) + sizeof(phm_ppt_v1_voltage_lookup_record) * max_levels;`
* Both callers (`init_dpm_2_parameters`) pass `max_levels=16`
  (lines 287 and 296).
* With `sizeof(uint32_t)=4` and `sizeof(record)=10` (5x uint16:
  us_calculated/us_vdd/us_cac_low/us_cac_mid/us_cac_high — see
  `hwmgr_ppt.h:78-84`), the allocation is **164 bytes** (kmalloc-256).
* Line 176 — `table->count = vddc_lookup_pp_tables->ucNumEntries;`
  (`ucNumEntries` is UCHAR 0..255 from VBIOS — `vega10_pptable.h:237`).
* Line 178 — loop is bounded by `ucNumEntries`, **not** by `max_levels`:
  `for (i = 0; i < vddc_lookup_pp_tables->ucNumEntries; i++)`.
* Each iteration writes 10 bytes at offset `4 + i*10`.
* If `ucNumEntries > 16` the loop runs past the 164-byte allocation.
* Worst case `ucNumEntries=255`: last write at byte offset
  `4 + 254*10 = 2544` → **~2390 bytes of controlled OOB write** into the
  adjacent kmalloc-256 slab bucket.

### Sibling functions are correct (rules out pattern blindness)

* `get_mclk_voltage_dependency_table` (line 367+),
  `get_sclk_voltage_dependency_table_by_bitmap` (line 320+),
  `get_mm_voltage_dependency_table`, `get_pcie_table` — all use
  `clk_dep_table->ucNumEntries` for BOTH the `table_size` allocation
  AND the loop bound (lines 328-336, 531-542, 565-576, ...).
* `get_vddc_lookup_table` is the **only** helper in this file that splits
  allocation-vs-loop bounds — a clear inconsistency.

## Reachability on this guest

* The amdgpu driver is **not** in `X86_64_GENERIC`
  (`grep -iE 'drm|radeon|amdgpu' sys/config/X86_64_GENERIC` → empty).
* It is also not in `LINT64` (`amdgpu` is not listed; only `drm` and
  `radeon` are).
* `amdgpu` is built only as a KLD module from
  `sys/dev/drm/amd/amdgpu/Makefile` (which lists `process_pptables_v1_0.c`).
* At test time `kldstat` shows no drm/amdgpu/radeon modules loaded.
* `pciconf -l` shows only QEMU std VGA (`vgapci0@pci0:0:2:0`,
  `chip=0x11111234`), no AMD GPU.
* Therefore `get_vddc_lookup_table` cannot be reached at runtime on this
  guest. The CAM/scsi layer does not touch it; the only paths into it are
  `pp_tables_v1_0_initialize` (called during amdgpu driver attach, which
  requires real AMD GPU hardware or VFIO PCI passthrough of an AMD GPU).

### Realistic threat model (when reachable)

* An attacker who controls the VBIOS image supplied to the amdgpu driver
  can trigger the OOB.  Two real-world scenarios:
  1. **VFIO PCI passthrough** — a malicious guest OS supplies a crafted
     ROM file (e.g. via `vfio-pci.romfile=`) that the host driver parses
     on attach.
  2. **Malicious FPGA GPU / malicious PCIe device** — presents an AMD
     vendor/device ID and serves a hostile PowerPlay table during probe.
* In both cases the attacker must already have some privilege: root
  inside a VFIO-passthrough guest, or physical PCIe access.  This is a
  **root→kernel** (or "kernel trusts PCI firmware") hardening gap, not
  an unprivileged-user→root escalation.
* On the realistic DragonFly target (default GENERIC, INVARIANTS ON),
  the slab INVARIANTS checks (chunk_mark_allocated/magic) would catch
  the cross-object overwrite and panic the kernel — i.e. the realistic
  impact on GENERIC is **panic/DoS**, not uid0.  Without INVARIANTS the
  primitive is a large attacker-shaped heap write.

## Exploit chain

Not applicable.  The primitive is real (attacker-shaped ~2.4 KB heap
write into kmalloc-256 bucket), but its only trigger paths are
root-controlled (kldload + malicious VBIOS) on the default kernel, and
the guest has no AMD GPU hardware at all.  This is a **valid hard
blocker**: the write is reachable only from an already-root context
(kldload / VFIO romfile control / physical PCIe), and the affected code
path is dead at runtime on this guest.  The bug is documented as a
latent memory-corruption defect with a real primitive characterization
and a verified fix.

## The fix

`fix.diff` — clamp both `table->count` and the loop bound to
`min(ucNumEntries, max_levels)`.  Minimal, targeted, one logical change.

```diff
-table->count = vddc_lookup_pp_tables->ucNumEntries;
-
-for (i = 0; i < vddc_lookup_pp_tables->ucNumEntries; i++) {
+/* ucNumEntries comes from VBIOS (UCHAR, 0..255) but the buffer was
+ * sized for max_levels entries.  Clamp the count and loop bound to
+ * max_levels to prevent an out-of-bounds write past the slab object. */
+if (vddc_lookup_pp_tables->ucNumEntries > max_levels)
+	table->count = max_levels;
+else
+	table->count = vddc_lookup_pp_tables->ucNumEntries;
+
+for (i = 0; i < table->count; i++) {
```

## PoC changes

No PoC source exists in `findings/poc/DF-1574/` other than `fix.diff`
and the build/run logs.  A userspace PoC cannot exercise this path
because the amdgpu driver is not loadable on the guest and the bug
requires a malicious VBIOS at driver attach time.  The trigger would
have to be a crafted VBIOS ROM image supplied via `vfio-pci.romfile=`
or a malicious FPGA GPU — neither of which the QEMU guest can present.

## Reproduce

Not runnable on this guest (no AMD GPU; amdgpu not loadable).

```
# On a host with a real AMD GPU + DragonFly amdgpu driver:
#   1. Craft a VBIOS ROM with a Tonga PowerPlay table whose
#      Voltage_Lookup_Table has ucNumEntries > 16.
#   2. kldload amdgpu  (or boot with the GPU present)
#   3. Observe panic (INVARIANTS ON) or heap corruption (INVARIANTS OFF).
# Apply this finding's fix.diff and the OOB is gone.
```

The standalone translation-unit compile of the patched file
(`process_pptables_v1_0.c`) is captured in `fix_build.log`.
