# DF-3755 — Verdict

## Verdict: REPRODUCED (source-level + harness) — dangling-pointer UAF + teardown double free + ps_priv leak; kernel-heap corruption potential, escalation not demonstrated

radeon (Northern Islands) twin of si **DF-3611** / ci **DF-3645** (amdgpu
DF-3588 / DF-3624).

## Bug confirmation

`ni_parse_power_table()` frees the state array when a per-state `kzalloc`
fails, and leaves every published pointer dangling:

```c
ni_dpm.c:4020           ps = kzalloc(sizeof(struct ni_ps), GFP_KERNEL);
ni_dpm.c:4021           if (ps == NULL) {
ni_dpm.c:4022                   kfree(rdev->pm.dpm.ps);   // freed, NOT nulled
ni_dpm.c:4023                   return -ENOMEM;           // num_ps never set
ni_dpm.c:4041           rdev->pm.dpm.num_ps = ...;       // only on success
```

Three consequences:

1. **Ignored-error amplification.** `radeon_pm_init_dpm()` discards the
   `dpm.init` return (radeon_pm.c:1448 — `radeon_dpm_init(rdev);` is a
   bare statement, macro at radeon.h:2793) and immediately runs
   radeon_pm.c:1449 `current_ps = requested_ps = boot_ps`. If a
   BOOT-classified state was parsed before the failing iteration,
   `boot_ps` points INTO the freed array (`ni_parse_pplib_non_clock_info`
   assigned `&rdev->pm.dpm.ps[i]` at ni_dpm.c:3914-3915), so the published
   current/requested/boot pointers all dangle. `radeon_dpm_enable()`
   (radeon_pm.c:1453) then executes the UAF read chain
   `ni_dpm_enable:3589/3641 -> ni_init_smc_table:1943/1975 ->
   ni_get_ps(freed)->ps_priv -> ni_ps through the stale ps_priv pointer`
   (the `struct ni_ps` privs themselves stay allocated, so the read chain
   usually returns stale-but-mapped data — until the slab is reused, then
   attacker-influenced garbage drives the DPM state machine).
   `radeon_pm_init`'s error is swallowed a second time by the asic init
   (`ni.c:2432 radeon_pm_init(rdev);` — value ignored), so the driver
   finishes loading in the broken state.

2. **Leak.** `num_ps` stays 0, so the k already-parsed `ps_priv`
   allocations are never freed (`ni_dpm_fini`'s loop runs 0 times).

3. **Double free at teardown.** Driver removal/shutdown runs
   `radeon_device_fini -> radeon_fini -> ni.c:2489 radeon_pm_fini ->
   radeon_pm.c:1705 radeon_dpm_fini` (NOT guarded by `dpm_enabled`) ->
   `ni_dpm_fini`:

   ```c
   ni_dpm.c:4270          for (i = 0; i < rdev->pm.dpm.num_ps; i++)   // 0 iterations
   ni_dpm.c:4273          kfree(rdev->pm.dpm.ps);                      // SECOND free
   ```

   Between the first free (driver load) and the second (teardown) the slab
   object can be reallocated by any kernel subsystem, so this `kfree`
   releases a live foreign object — classic exploitable double-free shape.
   The chunk size is attacker-influenced:
   `kzalloc(sizeof(struct radeon_ps) * ucNumStates)` with `ucNumStates`
   a VBIOS byte (2..255 -> ~96 B..~12 KB, selectable kmalloc bucket).

## Harness confirmation

run.log decisive lines:

```
[A] current_ps = requested_ps = boot_ps = 0x8004a3120  (DANGLING: ps array freed at ni_dpm.c:4022)
    num_ps = 0 (never set -> ps_priv[0..4] leak permanently)
[B] victim alloc (384 bytes) = 0x8004a30c0  (reuse: SAME chunk)
    ni_dpm_enable consumed reused-chunk data: mclk=0xdeadbee
[C] post-corruption allocations: x1=0x8004a30c0 x2=0x8004a30c0 -> ALIAS (same chunk handed out twice)
[C2] victim = 0x8004a30c0 == freed dpm.ps = 0x8004a30c0: REUSED
     after kfree(victim): next malloc returns 0x8004a30c0 == victim: YES (double-alloc)
[D] rc=-ENOMEM propagated; dpm.ps = NULL; ps_privs freed; no double free, no leak
```

The harness replicates the exact lifetime sequence (kzalloc-failure
injection at parse iteration 5 of a BOOT-bearing 8-state table) and proves
each consequence with DragonFly's own allocator: the dangling publication
[A], attacker-controlled data flowing through the UAF read after slab
reuse [B], the teardown double free as *silent* bin corruption — the same
chunk handed out twice to live allocations [C] and a live victim freed
while in use [C2] — and the fully corrected behavior with fix.diff [D].

## Trigger conditions (not met on this guest)

1. AMD NI GPU + radeon DPM (default on) — QEMU guest is virtio-only
   (env.txt: no 0x1002 PCI device; radeon.ko + radeonkmsfw_* present).
2. Crafted VBIOS PowerPlayInfo table with >= 2 states (ucNumStates >= 2)
   so at least one `ps_priv` kzalloc succeeds, and kernel heap pressure
   making a later `kzalloc(sizeof(struct ni_ps))` fail mid-parse.
3. For the double free: driver unload/reboot after the failed load.

The kzalloc-failure requirement is what keeps this at Medium (family
precedent: si twin DF-3611 Low without the ignored-init multiplier; ci
twin DF-3645 Medium *with* it — ni has the same multiplier as ci:
radeon_pm.c:1448 + ni.c:2432).

## Exploit chain

Demonstrated to the allocator-corruption boundary (live-alias double
allocation / foreign-object release) in the harness. A kernel-heap
exploit chain would additionally need: real-HW trigger timing (heap
pressure at exactly the right kzalloc), a reclaiming victim object in the
chosen `sizeof(struct radeon_ps)*ucNumStates` bucket, and a follow-up
allocation pattern — plausible on real hardware but NOT demonstrated
here (no NI GPU on the audit guest). Realistic impact ceiling: kernel
heap corruption -> potential local privilege escalation; realistic
likelihood: low (dual requirement of crafted vBIOS + timed allocation
failure).

## Fix validation

`fix.diff`:
- parse error path frees the already-parsed `ps_priv` states (kills the
  leak), then NULLs `dpm.ps` / `boot_ps` / `uvd_ps` (kills the dangling
  publication AND the double free);
- `ni_dpm_fini` becomes NULL-safe and re-clears the pointers.

`patch -p1` clean against pristine source alone and stacked with
DF-3754's fix; compiles `-Werror` clean as a kernel object
(`make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC ni_dpm.o`,
fix_build.log); harness `[D]` shows the corrected lifetime. Guest
/usr/src restored to pristine afterwards (verified by md5 + grep).

`fix_status: fixed` (compile + harness-level); live HW 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.
