# DF-5230 VERDICT — NULL `uvd_fw` deref in `uvd_v4_2_resume()` (kv DPM ungate path)

**Status: reproduced (kernel panic), synthetic-state harness calling the
production function; production trigger chain proven statically link-by-link.
Impact: panic (local DoS on CIK/KV APU hardware with a mixed fw config).
Confidence: certain (the deref itself is mechanically demonstrated twice;
the field trigger's hw/fw precondition is stated precisely below).**

## The defect

`uvd_v4_2_resume()` reads `rdev->uvd_fw->datasize` at
`sys/dev/drm/radeon/uvd_v4_2.c:51` with no `uvd_fw`/`vcpu_bo`/`has_uvd` guard.

Two callers exist. One is guarded; the other is not:

* `cik_uvd_start()` (`cik.c:8167-8175`) returns early when `!rdev->has_uvd`,
  and the `radeon_uvd_resume()` it calls first checks `vcpu_bo == NULL`
  (`radeon_uvd.c:286`). Safe.
* `kv_dpm_powergate_uvd(rdev, false)` — the UVD **un-gate** branch — calls
  `uvd_v4_2_resume(rdev)` directly at `kv_dpm.c:1628` with **no guard at
  all**. (The ungate even calls `uvd_v1_0_start()` right after, `kv_dpm.c:1629`.)

## Production trigger chain (every link verified path:line)

```
system suspend / pm_fini / dpm-disable
  -> cik_suspend()                     cik.c:8545 (radeon_pm_suspend FIRST —
                                        the `if (rdev->has_uvd)` guard at
                                        cik.c:8550 sits BELOW this call)
  -> radeon_pm_suspend()               radeon_pm.c:1269-1271
  -> radeon_pm_suspend_dpm()           radeon_pm.c:1257
  -> radeon_dpm_disable(rdev)          radeon.h:2797 macro
  -> kv_dpm_disable()                  kv_dpm.c:1305,1316 (asic bind
                                        radeon_asic.c:2285)
  -> kv_dpm_powergate_uvd(rdev,false)  kv_dpm.c:1608
       [caps_uvd_pg == true unconditionally, kv_dpm.c:2773;
        pi->uvd_power_gated was flipped to true at boot by
        kv_dpm_late_enable() -> powergate_uvd(true), kv_dpm.c:1300 —
        so the `== gate` early-return at kv_dpm.c:1612 does NOT fire]
  -> uvd_v4_2_resume(rdev)             kv_dpm.c:1628   NO GUARD
  -> rdev->uvd_fw->datasize            uvd_v4_2.c:51   uvd_fw == NULL
```

`radeon_uvd_init()` sets `fw_header_present=false` (`radeon_uvd.c:137`) and
`max_handles=10` (:138) *before* attempting the firmware load, and on CIK the
load of `radeonkmsfw_bonaire_uvd` (+:176 fallback `radeonkmsfw_BONAIRE_uvd`)
fails when the fw kmods are not loaded → `uvd_fw` stays NULL →
`cik.c:8150-8157` clears `has_uvd` → UVD never starts, **but DPM is
independent**: on KAVERI/KABINI/MULLINS (KV family, `kv_dpm_*`) DPM comes up
from the SMC firmware (`kv_smc.c`), `kv_dpm_late_enable()` power-gates UVD at
boot, and the first suspend / pm-fini then un-gates it through the unguarded
path above.

## Preconditions (why Low)

1. KV-family CIK APU (KAVERI/KABINI/MULLINS — radeonkms, DPM capable).
2. DPM enabled ⇒ SMC fw kmod loaded (`radeonkmsfw_*_smc`).
3. UVD fw kmods (`radeonkmsfw_bonaire_uvd` / `radeonkmsfw_BONAIRE_uvd`) NOT
   loaded — `radeon_uvd_init()` failed, `uvd_fw == NULL`.

On DragonFly all radeon firmware ships as separate opt-in kmods
(`sys/dev/drm/radeonfw/*/`), so "SMC loaded, UVD not" is a natural
misconfiguration, not a contrived one. Trigger: any system suspend (lid close
/ S3 — an unprivileged physical action), `kldunload radeonkms`
(`radeon_pm_fini_dpm`, `radeon_pm.c:1694`), or a `power_method` sysfs switch.
Result: deterministic NULL-deref panic. Ceiling = DoS; the fault is a pure
data read (`datasize`, offset 0x10) — no write primitive, no info leak.

## Verification on this guest (no AMD GPU — `env.txt`: `NO_AMD_PCI`)

The full suspend chain needs KV hardware, so the harness (`df5230.c`)
reconstructs the **exact post-failed-init device state** (zeroed
`struct radeon_device` + `max_handles=10`, i.e. `fw_header_present=false`,
`gpu_addr=0`, `uvd_fw=NULL` — the state `kv_dpm.c:1628` would pass) and calls
the **production** `uvd_v4_2_resume()` from the freshly built `radeon.ko`
(the module that ships the function; no code copied or stubbed):

* baseline ×2 (second from a fresh `vm.sh reset` with source reverted):
  `Fatal trap 12 ... fault virtual address = 0x10 ... Stopped at
  uvd_v4_2_resume+0x30: movq 0x10(%rdx),%rdx` — guest down (`panic.txt`,
  `panic.2.txt`).
* patched (fix.diff applied in guest `/usr/src`, radeon.ko rebuilt, identical
  trigger): `DF5230: survived, uvd_v4_2_resume returned -22`, guest up
  (`run.fixed.log`).

The faulting instruction `movq 0x10(%rdx),%rdx` is the `datasize` load at
`uvd_v4_2.c:51` (0x10 = `offsetof(struct firmware, datasize)`,
`sys/sys/firmware.h:51-56`), and it fires **before** the first `WREG32`
(`uvd_v4_2.c:52`), so the synthetic run exercises precisely the audited deref
and nothing else.

## Negative-check list (everything else hunted in uvd_v4_2.c)

* Window-chain overhang past `vcpu_bo` (+512 B with the shipping fw) — real,
  filed separately as **DF-5229** (family of DF-4832/DF-4524/DF-4742).
* `uvd_fw` NULL on the *guarded* caller (`cik.c:8167`): killed — has_uvd
  covers it; `radeon_uvd_resume()` also checks `vcpu_bo` (radeon_uvd.c:286).
* Boot-time gate path (`kv_dpm_late_enable` → `uvd_v1_0_stop`): killed —
  register writes only (uvd_v1_0.c:391-411), no fw deref.
* Integer width: `addr` u64, `WREG32` truncations are by-design (bits 28-31
  at :67, bits 32-39 at :71); `size` u32 values all small constants — killed.
* `max_handles` sourced from fw version (`radeon_uvd.c:165-166`, ≤30) —
  `handles[RADEON_MAX_UVD_HANDLES]` is 30 (radeon.h:1674,1685) — no overflow —
  killed (and DF-5229 shows SESSION*N is bounded by bo sizing anyway).
* This file consumes no user-controlled data at all (registers + fw-derived
  scalars only) — the only user-reachable surface is via CS on real hw, which
  is DF-1655/DF-4500/DF-4501 territory in radeon_uvd.c, not here.

## Recommended fix

`fix.diff` (one-line guard + comment, `git apply --check` clean): return
`-EINVAL` when `rdev->uvd_fw == NULL` in `uvd_v4_2_resume`, mirroring the
`has_uvd`/`vcpu_bo` guards of the other two paths. Validated on-guest above.
(An alternative placement — guard in `kv_dpm_powergate_uvd()` — fixes only
the known caller; the function-local guard also covers future callers.)

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