# DF-3611 — Verdict

## Verdict: REPRODUCED (source-level + harness) — double free of a recycled heap block (cross-object UAF), OOM-gated

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

## Bug confirmation

```c
si_dpm.c:6840  rdev->pm.dpm.ps = kzalloc(sizeof(struct radeon_ps) * ucNumEntries);
si_dpm.c:6845  for (i = 0; i < state_array->ucNumEntries; i++) {
...
si_dpm.c:6853      ps = kzalloc(sizeof(struct ni_ps), GFP_KERNEL);
si_dpm.c:6854      if (ps == NULL) {
si_dpm.c:6855          kfree(rdev->pm.dpm.ps);      // freed, NOT NULLed,
si_dpm.c:6856          return -ENOMEM;              //  num_ps still 0,
                                                    //  ps_priv[0..i-1] LEAK
```

`si_dpm_init` propagates the -ENOMEM, but the caller ignores it:

```c
radeon_pm.c:1448     radeon_dpm_init(rdev);          // rc discarded
radeon_pm.c:1453     ret = radeon_dpm_enable(rdev);  // fails later, ignored path
```

and teardown frees the same pointer unconditionally:

```c
si.c:6945             radeon_pm_fini(rdev)
radeon_pm.c:1723-1724   pm_method == PM_METHOD_DPM -> radeon_pm_fini_dpm
radeon_pm.c:1705          radeon_dpm_fini(rdev);     // no dpm_enabled check
si_dpm.c:7085               kfree(rdev->pm.dpm.ps);  // SECOND free
```

Between init failure and fini, ordinary kernel allocation churn recycles the
freed block (same size class). The fini-time kfree then frees whatever object
now occupies the block -> next allocation returns it to a second owner ->
use-after-free with cross-object type confusion.

## Harness confirmation

`run.log` (fault-injected kzalloc failure at state 2 of 4):

```
    [heap churn] victim object now lives at      0x8004904a0 (SAME block - recycled)
    [unload] si_dpm_fini: for(i<num_ps=0) skipped; kfree(dpm.ps=0x8004904a0)
    >>> DOUBLE FREE of a LIVE object <<<
    next allocation gets same block at 0x8004904a0; write via 2nd owner:
      victim->marker  = 0x4141414141414141 (owner #1 view)
      other->marker   = 0x4141414141414141 (owner #2 view)
      victim->marker was silently CLOBBERED -> use-after-free
```

The harness also prints the secondary defect: 2 leaked `ni_ps` allocations
(the already-parsed states) because `num_ps` is only set after the loop.

## Trigger conditions

1. SI GPU + radeon DPM (absent on the audit guest).
2. `kzalloc(sizeof(struct ni_ps))` failure mid-parse while the surrounding
   allocations succeed — OOM / kmalloc fault injection (`vm.zone_allocate`
   limits, KMEM_DEBUG). Low practical likelihood => severity Low, matching
   the amdgpu twin (DF-3588).

## Exploit chain

If triggered under controlled memory pressure, the primitive is a same-size-
class cross-object UAF; the freed block sits inside rdev->pm.dpm lifecycle,
freeable again at driver unload (or GPU reset -> radeon_pm_fini). No
demonstrated uid=0 chain (OOM-gated trigger, unprivileged users do not
control it directly).

## Fix

`fix.diff`:
- error path frees the already-parsed `ps_priv` states, frees `dpm.ps` and
  **NULLs** it before returning -ENOMEM;
- `si_dpm_fini` guards on `rdev->pm.dpm.ps` and re-NULLs after free.

Validation: `git apply --check` OK; `make si_dpm.o` -Werror clean with all
five fixes stacked; harness `[B]` shows the fixed error path (0 leaked bytes,
dangling pointer gone, fini no-op).

`fix_status: fixed` (compile + harness-level).

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