# DF-3754 — Verdict

## Verdict: REPRODUCED (source-level + harness) — NULL-deref panic, no escalation (deterministic NULL, no attacker-controlled offset)

radeon (Northern Islands) twin of si **DF-3610** / ci **DF-3646**.

## Bug confirmation

`rdev->pm.dpm.boot_ps` is assigned in exactly one place on the ni parse
path — `ni_parse_pplib_non_clock_info()`:

```c
ni_dpm.c:3914   if (rps->class & ATOM_PPLIB_CLASSIFICATION_BOOT)
ni_dpm.c:3915       rdev->pm.dpm.boot_ps = rps;
```

That function is only reachable inside the per-state block of
`ni_parse_power_table()` guarded by `if (power_info->pplib.ucStateEntrySize - 1)`
(ni_dpm.c:4018). `ni_parse_power_table()` never verifies that a boot state
was found and returns 0 as long as the header parses. Crafted-VBIOS shapes
that leave `boot_ps == NULL` while parsing "succeeds":

1. `ucNumStates == 0` — the state loop is a no-op, `num_ps = 0`;
2. `ucStateEntrySize == 1` — the whole per-state block is skipped, so
   `rdev->pm.dpm.ps[i].ps_priv` also stays NULL for every state and the
   non-clock info (BOOT classification) is never parsed;
3. `ucNumStates >= 1` but no state carries
   `ATOM_PPLIB_CLASSIFICATION_BOOT` (0x0008) in `usClassification`.

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);
ni_dpm.c:3589        struct radeon_ps *boot_ps = rdev->pm.dpm.boot_ps;
ni_dpm.c:3641        ret = ni_init_smc_table(rdev);
ni_dpm.c:1943          radeon_boot_state = rdev->pm.dpm.boot_ps;   // NULL
ni_dpm.c:1975          ni_populate_smc_initial_state(rdev, NULL, table);
ni_dpm.c:1683            struct ni_ps *initial_state = ni_get_ps(NULL);
ni_dpm.c:733-735         return rps->ps_priv;    // read at NULL+0x28 -> panic
```

`offsetof(struct radeon_ps, ps_priv) == 0x28` (radeon.h:1342-1356, LP64).
Later NULL derefs on the same pointer: ni_dpm.c:3657
(`ni_populate_mc_reg_table(rdev, boot_ps)`), ni_dpm.c:3673
(`ni_populate_smc_tdp_limits`), ni_dpm.c:3698 (`ni_update_current_ps`),
`ni_dpm_disable`:3738. The ps_priv-NULL variant (shape 2) additionally
faults on the first power-state change:
`ni_dpm_pre_set_power_state:3771 -> ni_update_requested_ps:3576/3581`
(`ni_pi->requested_ps = *new_ps` with `new_ps == NULL`, fault at 0x0), and
in `ni_dpm_print_power_state:4282`.

DPM is default-enabled for the NI asics (BARTS/TURKS/CAICOS/CAYMAN/ARUBA,
radeon_pm.c:1546-1572: `radeon_dpm = -1` auto -> `PM_METHOD_DPM` whenever
rlc/smc firmware is present — `radeonkmsfw_*` modules ship in
/boot/kernel).

## Harness confirmation

run.log:

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

The harness builds a synthetic VBIOS PowerPlayInfo table, runs a verbatim
replica of `ni_parse_power_table`'s walk (including the
`ucStateEntrySize - 1` promotion quirk) over it, then executes the real
consumer call graph; the forked children take SIGSEGV at exactly the
predicted fault addresses (0x28 and 0x0) — the in-kernel equivalent is an
unrecoverable page fault / panic during DPM enable.

## Trigger conditions (not met on this guest)

1. AMD NI (Barts/Turks/Caicos/Cayman/Aruba) 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 NI).
3. Crafted VBIOS (reflash / KVM passthrough with custom ROM / supply-chain).

## Exploit chain

None — the faulting pointer is fully deterministic NULL; there is no
attacker-controlled offset or content. Pure boot-time DoS (panic) with a
malicious VBIOS. Same conclusion as the si/ci twins.

## Fix validation

`fix.diff` (guard in `ni_dpm_enable`, mirrors si DF-3610's fix):
`git apply`/`patch -p1` clean against pristine source — alone and stacked
with DF-3755's fix; compiles `-Werror` clean as a kernel object
(`make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC ni_dpm.o`, full
CFLAGS in fix_build.log); harness `[B]` shows enable refused, 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.
