# DF-1128 — Verdict

## Verdict: REPRODUCED (source-level + harness) — leak / panic class, no escalation chain (read-only primitive)

## Bug confirmation

`si_parse_power_table` (amdgpu/si_dpm.c:7210-7300) iterates over
`power_state->v2.ucNumDPMLevels` (a BIOS-controlled `u8`) and calls
`si_parse_pplib_clock_info` for each in-range `clockInfoIndex`. **The only
place that sets `ps->performance_level_count` is line 7141** inside
`si_parse_pplib_clock_info`. So if the BIOS state has `ucNumDPMLevels==0`
OR all `clockInfoIndex` values are `>= ucNumEntries`, the inner loop body
never runs and `performance_level_count` stays at its `kzalloc`-initialized
`0`.

That count then reaches three consumers with **no `count==0` guard**:

1. `si_convert_power_state_to_smc` at **amdgpu/si_dpm.c:5662-5665**:
   ```c
   if (state->performance_level_count > SISLANDS_MAX_HARDWARE_POWERLEVELS)
       return -EINVAL;
   threshold = state->performance_levels[state->performance_level_count-1].sclk * 100 / 100;
   ```
   The guard only rejects `count > MAX`. With `count == 0`, the array index
   `(u16)0 - 1` is computed in `int` as `-1`, so the access is
   `performance_levels[-1]` — a 4-byte OOB read of the `sclk` field
   **before** the start of the `performance_levels` array (i.e. into the
   preceding slab object / slab metadata).

2. `si_dpm_get_sclk` at **amdgpu/si_dpm.c:7876** (sysfs path):
   ```c
   return requested_state->performance_levels[requested_state->performance_level_count - 1].sclk;
   ```
   Same OOB read; the result is returned to userspace as the requested
   sclk via `radeon_pm_info`-style debugfs/sysfs. → info leak of adjacent
   slab data.

3. `si_dpm_get_mclk` at **amdgpu/si_dpm.c:7888**: identical OOB pattern
   for mclk.

A sibling guard at line 2405 (`si_populate_power_containment_values`)
already does the right thing:
```c
if (state->performance_level_count == 0)
    return -EINVAL;
```
So the fix is well-precedented inside the same file; the three sites above
simply missed it.

## Harness confirmation

`harness.c` mirrors the kernel arithmetic. With
`performance_level_count == 0` and a backing buffer initialised to
`0xcc...`, the harness reads `threshold = 0xcccccccc` from
`performance_levels[-1]` — exactly the OOB read the source trace predicts.
Output captured in `run.log`.

## Finding-summary accuracy note

The finding summary also claims:
> si_upload_sw_state computes state_size=(0-1)*sizeof(level) underflows to
> ~SIZE_MAX, memset(smc_state,0,~SIZE_MAX) overflows heap.

This is **incorrect** for the actual struct layout. With the real
definitions in `sislands_smc.h`:
- `sizeof(struct SISLANDS_SMC_SWSTATE) == 4 (header) + sizeof(LEVEL)`
- `sizeof(struct SISLANDS_SMC_HW_PERFORMANCE_LEVEL) == sizeof(LEVEL)`

So:
```c
state_size = sizeof(SWSTATE) + ((count - 1) * sizeof(LEVEL))
           = (4 + sizeof(LEVEL)) + (-1 * sizeof(LEVEL))
           = 4   (after unsigned wrap mod 2^64 then truncation to u32)
```

The harness confirms this: `state_size = 0x00000004 (4 bytes)`. The
`memset(smc_state, 0, 4)` therefore only writes 4 bytes — **not** a heap
overflow. The realistic primitive is the **OOB read** at line 5665 and the
matching sysfs-readable OOB reads at 7876/7888.

The OOB read is the actionable bug. This finding is therefore downgraded
internally from "integer underflow memset heap overflow" to "missing zero
count guard → OOB read / info leak". The `fix.diff` still closes all three
sites.

## Exploit chain

This is a **read-only primitive** (4-byte OOB read before the slab object
backing `struct si_ps`, plus sysfs exfiltration via `si_dpm_get_sclk` /
`si_dpm_get_mclk`). No write capability → no privilege-escalation chain
exists for this bug on its own. The realistic impact ceiling is:

- **Info leak** of 4 bytes of adjacent kernel heap (slab metadata or
  neighbouring object) readable via the `si_dpm_get_sclk(!low)` sysfs path.
  Repeated calls with different kzalloc bucket grooming could harvest slab
  layout / pointer bits — useful as a KASLR-defeat helper on systems where
  KASLR is enabled (DragonFly disables it by default, so this is mostly
  informational on default installs).
- **Panic** if the read crosses into an unmapped page (rare; depends on
  slab layout).

## Trigger conditions (not met on this guest)

1. AMD Southern Islands GPU present (PCI vendor `1002`, SI family).
2. `amdgpu.ko` loaded (default on systems with the hardware).
3. Crafted VBIOS whose `ATOM_PPLIB_POWERPLAYTABLE` produces a state with
   `performance_level_count == 0`. Reachable via VFIO GPU passthrough with
   a reflashed ROM, or an emulated KVM GPU with a custom ROM.

The QEMU audit guest has no AMD GPU; `amdgpu.ko` is present in
`/boot/kernel/` but never loaded. The bug is therefore confirmed at the
source + harness level, not via a live runtime trigger. This matches the
"latent bug, harness-confirmed primitive" outcome documented for similar
driver-only findings.

## Fix

`fix.diff` rejects `count == 0` in:
- `si_convert_power_state_to_smc` (line 5662 — guards the line 5665 OOB read)
- `si_upload_sw_state` (line 5738 — defense-in-depth, since the line 5665
  guard now fires earlier; still worth keeping for robustness)
- `si_dpm_get_sclk` / `si_dpm_get_mclk` (lines 7876, 7888 — guards the
  sysfs-readable OOB read)

The fix is minimal and mirrors the existing guard at line 2405.

## Fix validation

1. `git apply --check` (well, `patch -p1 --check` since /usr/src isn't a
   git repo on the guest) — clean apply, all 4 hunks.
2. `cd /usr/src/sys/dev/drm/amd && make` with the diff applied — `amdgpu.ko`
   built cleanly (`rc=0`, 3,741,128 bytes).
3. Reverted with `patch -R -p1`.

Since the bug cannot be triggered live on the guest (no AMD GPU), the
"behaviour comparison" half of fix-validation is done at the harness level:
the harness includes both the buggy and the fixed code paths side-by-side;
the buggy path performs the OOB read, the fixed path returns the
`-EINVAL` equivalent. See `run.log` for the side-by-side output.

`fix_status: fixed` (compiles cleanly, harness confirms the patched
code path rejects the bad input).
