# DF-3610 — Verdict

## Verdict: REPRODUCED (source-level + harness) — NULL-deref panic, no escalation (no control over the dereferenced pointer)

radeon twin of amdgpu **DF-3586**.

## Bug confirmation

`rdev->pm.dpm.boot_ps` is assigned in exactly one place for SI —
`si_parse_pplib_non_clock_info()`:

```c
si_dpm.c:6726   if (rps->class & ATOM_PPLIB_CLASSIFICATION_BOOT)
si_dpm.c:6727       rdev->pm.dpm.boot_ps = rps;
```

`si_parse_power_table()` (si_dpm.c:6808) never checks that a BOOT state was
found, and returns 0 as long as the array walk succeeds. Three crafted-VBIOS
shapes leave `boot_ps == NULL` while parsing "succeeds":

1. no state carries `ATOM_PPLIB_CLASSIFICATION_BOOT` (0x0008) in
   `usClassification`;
2. `state_array->ucNumEntries == 0` — `kzalloc(sizeof(struct radeon_ps) * 0)`
   returns `ZERO_SIZE_PTR` (non-NULL) at si_dpm.c:6840-6843, the loop is a
   no-op, `num_ps = 0`;
3. every state's clock indexes are `>= clock_info_array->ucNumEntries`
   (si_dpm.c:6866 `continue`) — states parse but the classification loop for
   boot never sets boot_ps (shape 1 again) — note `non_clock_info` IS still
   parsed in that case, so only the BOOT classification matters.

The consumer chain has no guard anywhere:

```c
radeon_pm.c:1448     radeon_dpm_init(rdev);            // return IGNORED
radeon_pm.c:1449     current_ps = requested_ps = boot_ps;   // NULL ok'd
radeon_pm.c:1453     ret = radeon_dpm_enable(rdev);    // -> si_dpm_enable
si_dpm.c:6354        struct radeon_ps *boot_ps = rdev->pm.dpm.boot_ps;
si_dpm.c:6401        ret = si_init_smc_table(rdev);
si_dpm.c:4700          radeon_boot_state = rdev->pm.dpm.boot_ps;
si_dpm.c:4746          si_populate_smc_initial_state(rdev, radeon_boot_state, table);
si_dpm.c:4372            struct ni_ps *initial_state = ni_get_ps(NULL);
ni_dpm.c:733-735         return rps->ps_priv;           // read at NULL+0x28
```

`offsetof(struct radeon_ps, ps_priv) == 0x28` (radeon.h:1342-1356, LP64).
Later derefs of the same pointer: si_dpm.c:6417 (`si_populate_mc_reg_table`),
:6438/:6443 (`si_populate_smc_tdp_limits[_2]`), :6463 (`ni_update_current_ps`),
and `si_dpm_disable`:6508-6509.

## Harness confirmation

`run.log`:

```
[A] ... child died from SIGSEGV (signal 11): NULL+0x28 read
    >>> In kernel: page-fault at 0x28 -> panic during DPM enable <<<
[B] Fixed path ... rc=-EINVAL (DPM enable refused, no dereference)
```

The harness forks and executes the real dereference chain
(`si_dpm_enable -> si_init_smc_table -> si_populate_smc_initial_state ->
ni_get_ps(NULL)->ps_priv`) — the child takes SIGSEGV at address 0x28, exactly
the fault the kernel would take (unrecoverable page fault -> panic).

## Trigger conditions (not met on this guest)

1. AMD SI (Tahiti/Pitcairn/Verde/Oland/Hainan) GPU with radeon DPM — the
   audit QEMU guest is virtio-only (no 0x1002 PCI device; verified in env.txt).
2. `radeon.ko` loaded and DPM enabled (default for SI when the PowerPlayInfo
   table parses).
3. Crafted VBIOS as described above (reflash / KVM passthrough / emulated).

## Exploit chain

None — NULL is fully deterministic, no attacker-controlled offset. Pure
boot-time DoS / code-execution-prevention (panic) with a malicious VBIOS.

## Fix

`fix.diff` — refuse to enable DPM without a boot state:

```c
if (boot_ps == NULL) {
        DRM_ERROR("no boot power state in VBIOS power table\n");
        return -EINVAL;
}
```

Validation: `git apply --check` OK; `make si_dpm.o` (KERNBUILDDIR=
/usr/obj/usr/src/sys/X86_64_GENERIC) compiles **-Werror clean** with all five
pass-2 fixes stacked; harness `[B]` shows enable refused with no fault.

`fix_status: fixed` (compile + harness-level); live HW boot test impossible on
this guest.

## 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.
