# DF-4144 — VERDICT

**REPRODUCED** via the verbatim-code rig on the DragonFly guest (same tree
fidelity proof as DF-4143: 68 blocks identical to `/usr/src/sys`).

## Root cause (confirmed)

`trinity_parse_power_table` (trinity_dpm.c:1734-1821):

- On per-state `kzalloc(sizeof(struct sumo_ps))` failure (:1780) the parser
  executes `kfree(rdev->pm.dpm.ps); return -ENOMEM;` (:1781-1782) WITHOUT
  NULLing `rdev->pm.dpm.ps`, WITHOUT freeing the already-attached
  `ps_priv[0..i-1]`, and with `num_ps` unset (:1806 runs only on success).
  If a BOOT-classified state was parsed before the failure, `boot_ps`
  (set at :1705) points into the freed array.
- `trinity_dpm_init` propagates the error, but `radeon_pm_init_dpm`
  discards it (`radeon_dpm_init(rdev);` radeon_pm.c:1448) and keeps the
  device alive.
- At unload `radeon_pm_fini_dpm` calls `radeon_dpm_fini(rdev)`
  UNCONDITIONALLY (radeon_pm.c:1705) → `trinity_dpm_fini` (:2080-2092):
  the num_ps loop frees nothing (num_ps==0), then `kfree(rdev->pm.dpm.ps)`
  at :2089 frees the already-freed pointer → **double free**; `ps_priv[0]`
  is leaked.

## Rig observations (baseline, decisive lines)

- `4144-doublefree`: `RIG: init rc=-12; dpm.ps=0x80047e000 (kfreed at :1781,
  pointer NOT NULLed), num_ps=0, boot_ps=0x80047e000 (points INTO the freed
  array)`; `RIG: allocator confirms dpm.ps ... is FREED while rdev still
  holds it`; after the verbatim fini: `RESULT: DOUBLEFREE ptr=0x80047e000
  (allocation of 96 bytes freed a 2nd time)` (96 = 2 × sizeof(struct
  radeon_ps)=48, both verbatim-verified). Allocator leak report shows the
  128-byte `ps_priv[0]` allocation live at exit (leaked).
- `4144-uaf`: with the swallow simulated (radeon_pm.c:1448-1453), the
  verbatim enable's `trinity_update_current_ps(rdev, boot_ps)` (:1129) reads
  `boot_ps->ps_priv`: `RESULT: FAULT addr=0x80047e028 USE-AFTER-FREE in
  freed allocation 0x80047e000..0x80047e060 (+40 bytes in, alloc size 96)` —
  +40 is exactly offsetof(struct radeon_ps, ps_priv), i.e. the literal
  `rps->ps_priv` read of trinity_get_ps over freed heap.

In kernel context: the double free corrupts the slab freelist (panic or
worse on the next allocation of that bucket), and the UAF read feeds
`pi->current_ps = *new_ps` — a 92-byte copy from freed memory. Both trigger
under the crafted-VBIOS / memory-pressure (kzalloc failure) threat model;
the injector models the kernel's genuine allocation-failure path.

## Fix validation (A/B, identical image + identical injection)

`fix.diff`: (1) error unwinds at the three failure sites — free
`ps_priv[i-1..0]` while `dpm.ps` is still valid, then NULL `dpm.ps`,
`boot_ps`, `uvd_ps` after the production kfree; (2) idempotent fini
(`dpm.ps = NULL; num_ps = 0; priv = NULL;` after the frees). Fixed run:
`RESULT: OK (fixed) unwind + idempotent fini, no double free, no leak`
(allocator report: zero live allocations), and the follow-on enable returns
-EINVAL (DF-4143 guard + NULLed boot_ps) — no UAF. The checker proves the
`APPLY_DF4144_FIX` lines are exactly the fix.diff additions (build.log, 27
lines). Kernel-rebuild validation not performed (path executes only on AMD
ASICs, absent from the guest); validated in the rig per the established
protocol. fix.diff authored against pristine `sys/`, never applied.

## Honest limitations

- kzalloc-failure injection models memory pressure; on the target hardware
  the same code path is reachable with a large crafted `ucNumEntries`
  (255 × 128 B per state) at attach.
- The `num_ps==0` leak-report is rig-allocator evidence; in-kernel the leak
  is 128 B per parsed state per failed probe (re-probe loops make it
  cumulative).

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