# DF-2005 — VERDICT

**Status:** INCONCLUSIVE (HW-gated; source-only confirmation)
**Reproduced:** 0 (no trigger possible on this guest)
**Impact:** none (on this guest) — latent OOB-read primitive in HW-gated code
**Confidence:** certain (source-trace); speculative (runtime — never exercised)
**Class:** CWE-125 out-of-bounds read in VBIOS PowerPlay table parser

## Verdict (one line)

The bug is **real at the source level** — `check_powerplay_tables()` performs
no size validation against `sizeof(ATOM_Vega12_POWERPLAYTABLE)` before the
caller reads `sizeof(PPTable_t)` (~470+ bytes) at fixed offset ~244 via
`memcpy` — but the vulnerable code is **HW-gated** (no AMD GPU on the guest,
amdgpu is not in `X86_64_GENERIC`, no AMD GPU device present), so the
primitive cannot be triggered on this guest. Status `inconclusive` per the
HW-gated rule; the `fix.diff` is validated by building the patched
`amdgpu.ko` module with `-Werror` rc=0.

## Why it cannot fire on this guest (HW-gated)

- `sys/config/X86_64_GENERIC` lists `device amd` (AMD 53C974 SCSI) and
  `device amdtemp` (AMD CPU temp sensor). **Neither is `amdgpu`.**
- `kldstat` shows no amdgpu module loaded.
- `pciconf -lv` shows the only VGA device is
  `vgapci0: chip=0x11111234` (QEMU std-VGA, vendor 0x1234 — **not** AMD
  vendor 0x1002). No AMD GPU hardware is present for `amdgpu` to bind.
- `vega12_processpptables.c` is only compiled into `amdgpu.ko`
  (see `sys/dev/drm/amd/amdgpu/Makefile:181`). On real AMD Vega10/Vega12
  hardware, `vega12_hwmgr_init()` registers `vega12_pptable_funcs` and the
  powerplay backend calls `vega12_pp_tables_initialize()`. None of this
  runs without an AMD GPU.

The audit guest has **no** AMD GPU passthrough, so the driver never probes,
and the parsing code path is dead at runtime. This is a **latent bug**
reachable only on real Vega-class AMD hardware (or via malicious hypervisor
supplying a crafted VBIOS option ROM to a guest with AMD GPU passthrough).

## Mechanism (source-trace confirmation)

Vulnerable file: `sys/dev/drm/amd/powerplay/hwmgr/vega12_processpptables.c`

1. **`get_powerplay_table()` (lines 44-62)** fetches the table pointer as
   `bios + data_start`, where `data_start` is a `u16` offset read from the
   VBIOS data-table header (`atom.c:1401` in `amdgpu_atom_parse_data_header`).
   At line 58 the **declared size** is cached:
   `hwmgr->soft_pp_table_size = size;`

2. **`check_powerplay_tables()` (lines 64-75)** gates on only two checks:
   - `powerplay_table->sHeader.format_revision >= 9` (line 68-70)
   - `powerplay_table->sHeader.structuresize > 0` (line 71-72)

   **It never compares `soft_pp_table_size` nor `structuresize` against
   `sizeof(ATOM_Vega12_POWERPLAYTABLE)`.** This is the root-cause omission.

3. **`vega12_pp_tables_initialize()` (lines 267-294)** calls
   `check_powerplay_tables()` at line 280, and on success proceeds to
   `init_powerplay_table_information()` at line 289.

4. **`init_powerplay_table_information()` (lines 191-264)** reads fixed
   struct offsets that may exceed the actual table allocation:
   - lines 209-215: indexed reads of `ODSettingsMax[]`
   - lines 217-224: `phm_copy_overdrive_settings_limits_array` copies the
     full 15-element `ODSettingsMax`/`ODSettingsMin` arrays (60 bytes each)
   - lines 252-253: `phm_copy_clock_limits_array` copies the full 10-element
     `PowerSavingClockMax`/`PowerSavingClockMin` arrays (40 bytes each)
   - **line 259**: `memcpy(pptable_information->smc_pptable,
     &(powerplay_table->smcPPTable), sizeof(PPTable_t));` — the critical
     sink. Reads ~470+ bytes starting at the offset of `smcPPTable`.

### Offset math (packed struct, `#pragma pack(push,1)`, `vega12_pptable.h:26`)

`ATOM_Vega12_POWERPLAYTABLE` layout before `smcPPTable`:
- `sHeader` (atom_common_table_header: u16+u8+u8) = 4
- `ucTableRevision` = 1
- `usTableSize` = 2
- `ulGoldenPPID`, `ulGoldenRevision` = 8
- `usFormatID` = 2
- `ulPlatformCaps` = 4
- `ucThermalControllerType` = 1
- 6 × USHORT (`usSmallPowerLimit1`, …, `usSoftwareShutdownTemp`) = 12
- `PowerSavingClockMax[10]` = 40
- `PowerSavingClockMin[10]` = 40
- `ODSettingsMax[15]` = 60
- `ODSettingsMin[15]` = 60
- `usReserve[5]` = 10

Total **before `smcPPTable`: 244 bytes**. The line-259 `memcpy` then copies
`sizeof(PPTable_t)` (≈470+ bytes, defined at
`sys/dev/drm/amd/powerplay/inc/vega12/smu9_driver_if.h:26-510`), so reads
extend from offset 244 to ≈714+. A truncated VBIOS table that passes
`check_powerplay_tables` (e.g. with `structuresize=1`, `format_revision=9`,
actual buffer < 244 bytes) makes all of these reads OOB.

### BIOS allocation size is attacker-influenced

`amdgpu_read_bios_from_rom` (`amdgpu_bios.c:172`) derives
`len = AMD_VBIOS_LENGTH(header)` from a header field inside the VBIOS image
itself, so an attacker-controlled VBIOS can produce a small allocation with
`data_start` near its end, guaranteeing OOB. (`igp_read_bios_from_vram` uses
a fixed 256 KB buffer — OOB stays in-buffer, still an info leak into
`pptable_information->smc_pptable`, potentially exposed via sysfs
`pp_dpm_sclk` / `pp_od_clk_voltage`.)

## Threat model

Attacker position: **malicious hypervisor** supplying a crafted VBIOS option
ROM to a guest VM with AMD GPU passthrough, or a **physical attacker** with
a malicious PCIe GPU card. No userspace privilege or interaction required
— the parser runs during amdgpu driver init.

Realistic impact ceiling on real hardware: **kernel heap OOB read of up to
~1 KB**, leaking adjacent slab data into `pptable_information->smc_pptable`
(likely exfiltrable via sysfs), or **kernel panic** if the read crosses a
page boundary into unmapped memory.

## Why no live PoC / no escalation chain

There is no corruption primitive to develop (read-only OOB), and the path is
not reachable on this guest (no AMD GPU). Per the Phase 6 valid-hard-blocker
list: *the vulnerable code path is dead/unreachable at runtime on this guest
AND no harness can exercise it* — a userspace harness cannot drive VBIOS
parsing without an AMD GPU device for amdgpu to bind. This is a valid hard
blocker; the primitive is documented at the source level instead.

## Fix

`fix.diff` adds two `PP_ASSERT_WITH_CODE` guards in
`check_powerplay_tables()`, matching the surrounding macro style:

1. `hwmgr->soft_pp_table_size >= sizeof(ATOM_Vega12_POWERPLAYTABLE)` — the
   cached VBIOS-declared data-table size must accommodate the full struct.
2. `powerplay_table->sHeader.structuresize >= sizeof(ATOM_Vega12_POWERPLAYTABLE)` —
   defense in depth on the in-table declared size.

Both checks use the existing `PP_ASSERT_WITH_CODE` macro (defined in
`sys/dev/drm/amd/powerplay/inc/pp_debug.h:37-43`), so they emit a `pr_warn`
and `return -1` on failure, short-circuiting `vega12_pp_tables_initialize`
before any field is read.

This **matches the finding proposal** in
`findings/DF-2005-…md:## Recommended fix` with one stylistic improvement:
the in-table `sHeader.structuresize` is read as host-endian (matching the
existing line 71-72 check and the un-annotated `uint16_t structuresize`
field in `atom_common_table_header`, `atomfirmware.h:224-229`), avoiding a
spurious `le16_to_cpu` that would imply an `__le16` annotation the struct
does not have.

## Phase 8 — fix validation (amdgpu.ko module build)

The bug is HW-gated, so the patched kernel cannot be runtime-tested on this
guest. Phase 8 is therefore the **module-build** form of validation:
apply `fix.diff` to in-guest `/usr/src`, build `amdgpu.ko` with `-Werror`,
and confirm `rc=0`.

- **Before (unpatched):** `sys/dev/drm/amd/amdgpu/Makefile` builds
  `vega12_processpptables.c` (line 181) — file compiles, the missing-check
  bug is silently present in the shipped `.ko`.
- **After (patched):** apply `fix.diff`, rebuild `amdgpu.ko`; the fix adds
  only standard `PP_ASSERT_WITH_CODE(...)` calls with no new headers,
  types, or symbols — `make -Werror` passes `rc=0`. See `fix_build.log`.

Since the fix is provably a pure addition of two macro invocations against
an existing public struct type, and the module links cleanly, this
constitutes the strongest validation possible on a guest without the
required hardware.

## Reproduce

On a system **with a Vega-class AMD GPU** and DragonFlyBSD amdgpu:

1. Build `amdgpu.ko` against `sys/` with this `fix.diff` applied.
2. Craft a truncated ATOM VBIOS image (per `README.md`) — `format_revision=9`,
   `structuresize=1`, `data_start` near end of a small buffer.
3. Boot with the malicious VBIOS via `-device vfio-pci,romfile=...` or as
   a PCIe option ROM.
4. **Without fix:** kernel panic in `init_powerplay_table_information` (or
   corrupted sysfs reads via the VRAM path).
5. **With fix:** `dmesg` shows
   `amdgpu: [powerplay] PowerPlay Table size smaller than ATOM_Vega12_POWERPLAYTABLE!`
   and amdgpu init gracefully fails — no OOB read.

## Kernel references (verified in source)

- `sys/dev/drm/amd/powerplay/hwmgr/vega12_processpptables.c:64-75` — `check_powerplay_tables` weak validation
- `sys/dev/drm/amd/powerplay/hwmgr/vega12_processpptables.c:58` — `soft_pp_table_size` cached but never consulted
- `sys/dev/drm/amd/powerplay/hwmgr/vega12_processpptables.c:259` — `memcpy(sizeof(PPTable_t))` from truncated table
- `sys/dev/drm/amd/powerplay/hwmgr/vega12_processpptables.c:217-224,252-253` — fixed-array copies
- `sys/dev/drm/amd/powerplay/hwmgr/vega12_processpptables.c:280-289` — `check_powerplay_tables` gates `init_powerplay_table_information`
- `sys/dev/drm/amd/powerplay/hwmgr/vega12_pptable.h:75-105` — packed `ATOM_Vega12_POWERPLAYTABLE` layout (244 B preamble + `smcPPTable`)
- `sys/dev/drm/amd/include/atomfirmware.h:224-229` — `atom_common_table_header` (4 B)
- `sys/dev/drm/amd/powerplay/inc/pp_debug.h:37-43` — `PP_ASSERT_WITH_CODE` macro semantics
- `sys/dev/drm/amd/amdgpu/Makefile:181` — file compiled into `amdgpu.ko`
- `sys/config/X86_64_GENERIC:88,186` — only `device amd` (SCSI) and `device amdtemp`, no amdgpu
