# DF-4494 — VERDICT

**Finding**: inconsistent error-exit lifetimes in
`rs780_parse_power_table()`/`rs780_dpm_init()`, turned into live
double-free / UAF read+write by `radeon_pm_init_dpm()` discarding the init
return (radeon_pm.c:1448) and by teardown calling `rs780_dpm_fini()`
unconditionally (radeon_pm.c:1705).

**Status: REPRODUCED at logic level (verbatim replica), 2 attempts.**
Kernel-level trigger hardware-gated (no RS780/RS880 IGP in the QEMU
guest); family rows DF-4125/DF-4188/DF-4144/DF-4117/DF-3973 verified the
same way.

## Observed (run.log, canary allocator with 0xB0 free-fill)

### mode 4494 — dangling dpm.ps → double free

`kzalloc(struct igp_ps)` failed at state #1 (allocation #6). Parse kfrees
`dpm.ps` (rs780_dpm.c:830) and returns -ENOMEM; the pointer is **not**
NULLed and `boot_ps` still points at `&ps[0]`:

```
rs780_parse_power_table() = -12 (ENOMEM injected at state 1)
rdev->pm.dpm.ps is NOT (dangling pointer kept)NULL after the failed parse
rdev->pm.dpm.boot_ps = 0x8004d1d00 (points into the freed array)
driver unload -> radeon_pm_fini -> rs780_dpm_fini ...
*** CANARY ALLOCATOR: DOUBLE FREE of 0x8004d1d00 (size 96) DETECTED ***
exit=98
```

(size 96 = `sizeof(struct radeon_ps)` × ucNumStates(2) — the exact
allocation rs780_dpm.c:807-808 made.)

### mode 4494u — UAF read at probe

Same dangling state, then the swallowed-error continue:
`rs780_dpm_enable()` :608 → `rs780_initialize_dpm_power_state()` :78 loads
`ps[0].ps_priv` **from the freed array** (0xB0 free-fill), :82 derefs
0xB0B0B0B0B0B0B0B0 → non-canonical → SIGBUS:

```
*** CANARY: boot_ps (&dpm.ps[0]) ... points into a FREED allocation -> USE AFTER FREE ***
*** SIGNAL 10 at address 0x0: abort ***      exit=99
```

### mode 4494w — UAF write via the self-fini path

Successful parse + missing IntegratedSystemInfo header →
`rs780_dpm_init()` frees everything itself (:933) and returns -EINVAL,
which radeon_pm.c:1448 discards. The first field-write of the continued
probe (`rs780_get_pm_mode_parameters()` :57-58 `pi->crtc_id = 0;
pi->refresh_rate = 60`) lands in the freed `igp_power_info`:

```
*** CANARY: freed object igp_power_info (dpm.priv) (0x8004c1500, 32 bytes) has 8 bytes != 0xb0 -> object WRITTEN AFTER FREE (UAF WRITE) ***
*** SIGNAL 10 at address 0x0: abort ***      exit=99
```

## Why the kernel behaves the same

* radeon.h:2793 `#define radeon_dpm_init(rdev) rdev->asic->dpm.init((rdev))`
  — radeon_pm.c:1448 calls it as a statement; no error path exists.
* radeon_pm.c:1705 `radeon_dpm_fini(rdev);` runs at teardown regardless.
* The kernel `kfree()` of the dangling pointer at :959 is the classic
  double free into the DRM slab; the freed `igp_power_info` /
  `radeon_ps[]` sit in small-object slabs that are trivially re-groomed
  between probe failure and module unload — the write-after-free at :57
  corrupts whatever now occupies the slot (memcorrupt bucket).

The ENOMEM arm requires **no crafted VBIOS** — ordinary memory pressure at
the per-state kzalloc during probe on stock RS780 firmware is enough
(same reasoning as family row DF-4188, PR:L).

## Fix validation

`fix.diff`:

* ENOMEM path: frees `ps_priv[0..i-1]`, kfrees and NULLs `dpm.ps`,
  `boot_ps`, `uvd_ps` (single, consistent cleanup; teardown `kfree(NULL)`)
* `rs780_dpm_init()` no longer calls `radeon_dpm_fini()` itself — the
  swallowed error leaves a valid-or-NULL state, teardown owns the free
* `rs780_dpm_enable()` refuses NULL boot_ps (defense in depth)

Fixed harness (run.log):

```
rdev->pm.dpm.ps is NULL after the failed parse / boot_ps = 0x0
rs780_dpm_fini() returned; canary_violations so far = 0      exit=0
DRM_ERROR: no boot power state, refusing to enable DPM       exit=0   (4494u)
DRM_ERROR: No integrated system info for your GPU
rs780_dpm_enable() returned 0 ... canary_violations = 0      exit=0   (4494w)
```

Applied to the guest `/usr/src`: radeon_fix_apply.log (7 hunks, Plan A),
radeon_module_build.log — `radeon.ko` links under **-Werror**, MAKE_RC=0.
Guest source reverted (md5 verified), guest reset with-src afterwards.

## Attempts

1. First run: enable-arm "NO CRASH" (dead stub outputs let GCC fold the
   wild loads). Fixed with `-fno-delete-null-pointer-checks` + volatile
   clock sink.
2. Decisive run in run.log.

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