# DF-1139 — Verdict

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

## Bug confirmation

`si_get_std_voltage_value` (radeon/si_dpm.c:4150-4199) opens with a
pointer-only guard against a missing `cac_leakage_table.entries`:

```c
if (rdev->pm.dpm.dyn_state.cac_leakage_table.entries) {     /* line 4158 */
    if (rdev->pm.dpm.platform_caps & ATOM_PP_PLATFORM_CAP_NEW_CAC_VOLTAGE) {
        ...
        for (v_index = 0; ...; v_index++) {
            if (be16_to_cpu(voltage->value) == ...[v_index].v) {
                voltage_found = true;
                if ((u32)v_index < rdev->pm.dpm.dyn_state.cac_leakage_table.count)
                    *std_voltage = ...[v_index].vddc;
                else
                    *std_voltage =
                        ...[rdev->pm.dpm.dyn_state.cac_leakage_table.count-1].vddc;  /* line 4172 */
                break;
            }
        }
        ...
    }
}
```

The `entries` pointer is populated at **r600_dpm.c:1039-1043**:
```c
u32 size = cac_table->ucNumEntries * sizeof(struct radeon_cac_leakage_table);
rdev->pm.dpm.dyn_state.cac_leakage_table.entries = kzalloc(size, GFP_KERNEL);
if (!rdev->pm.dpm.dyn_state.cac_leakage_table.entries) {
    r600_free_extended_power_table(rdev);
    return -ENOMEM;
}
...
rdev->pm.dpm.dyn_state.cac_leakage_table.count = cac_table->ucNumEntries;  /* line 1063 */
```

When the BIOS `cac_table->ucNumEntries == 0`:
- `size == 0`, `kzalloc(0)` returns `ZERO_SIZE_PTR` (`== (void *)16`,
  non-NULL but non-dereferenceable),
- the `!entries` check is **false** (ZERO_SIZE_PTR is non-NULL),
- `count == 0` is stored.

Then in `si_get_std_voltage_value` the guard at line 4158 passes, the
NEW_CAC_VOLTAGE branch is taken (if the platform cap is set, which is a
separate BIOS-supplied bit), and the inner loop hits the `else` at line
4170-4172 because `(u32)v_index < count` is false for `count == 0`.
Line 4172 reads `entries[(u32)count - 1] = entries[0xFFFFFFFF]` — a
~16 GB OOB read (count is u32, so `(u32)0 - 1 == 0xFFFFFFFF`). On any
real system this immediately crosses an unmapped page and traps as a
page fault → kernel panic.

The identical pattern is also present at line 4187 (the `!voltage_found`
fallback loop).

## Harness confirmation

`harness.c` simulates the `ZERO_SIZE_PTR` allocation and traces the
computation: with `count == 0`, the buggy code computes
`entries[0xffffffff]` at effective address
`0x10 + 0xffffffff * 8 = 0x0000000800000008` — i.e. a ~32 GB OOB read
(8 bytes per `cac_leakage_entry` in the harness layout).

Output captured in `run.log`:
```
[A] Buggy guard (si_dpm.c:4158 `if (entries)`):
    entries=0x10 is non-NULL, so body executes with count==u0
    reads entries[count-1] = entries[0xffffffff]
    effective address = 0x10 + 0xffffffff * 8 = 0x0000000800000008
    >>> In kernel: page-fault at ~32 GB offset -> panic <<<

[B] Fixed guard (fix.diff: `if (entries && count > 0)`):
    rc=-1  (rejected, no OOB read)
```

## Exploit chain

Read-only primitive that **always panics** — the OOB offset (~16-32 GB)
is so large it always lands on an unmapped page, so the bug is in
practice a pure DoS. There's no opportunity to convert it to an info
leak or to control what gets read.

No escalation chain. The bug's impact is purely DoS via page-fault panic.

## Trigger conditions (not met on this guest)

1. AMD SI GPU present (no AMD GPU on the QEMU audit guest).
2. `radeon.ko` loaded.
3. Crafted VBIOS with `ATOM_PPLIB_CAC_Leakage_Table.ucNumEntries == 0`
   AND the `ATOM_PP_PLATFORM_CAP_NEW_CAC_VOLTAGE` platform cap bit set
   (otherwise the divergent branch at line 4192-4195 is taken instead,
   which doesn't have the count-1 pattern).
4. `si_convert_power_level_to_smc` reaches `si_get_std_voltage_value`
   for any state whose voltage is non-zero.

Source-level + harness-confirmed; no live runtime trigger on the guest.

## Fix

`fix.diff` augments the guard with a count check:
```c
if (rdev->pm.dpm.dyn_state.cac_leakage_table.entries &&
    rdev->pm.dpm.dyn_state.cac_leakage_table.count > 0) {
```

This is the minimal targeted fix — it preserves the existing pointer
check and adds the count guard, blocking both the line 4172 and 4187
OOB reads.

## Fix validation

1. `patch -p1 --check` — clean apply, 1 hunk.
2. `cd /usr/src/sys/dev/drm/radeon && make` with the diff applied —
   `radeon.ko` built cleanly (`rc=0`, 2,029,288 bytes).
3. Reverted.

Behaviour comparison at the harness level: `run.log` shows the buggy
guard passing (and the giant OOB offset being computed) while the
fixed guard rejects with `-EINVAL` equivalent.

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