# DF-4525 — amdgpu_dpm error-path double free (`amdgpu_free_extended_power_table`)

**File:** `sys/dev/drm/amd/amdgpu/amdgpu_dpm.c`
**Severity:** Medium (CWE-415) · **Confidence:** certain (statically proven chain + guest-reproduced primitive)
**Verdict: reproduced (kernel panic), fix validated (fixed)**

## What the bug is

`amdgpu_parse_extended_power_table()` frees **all 13** `dyn_state` table
pointers on each of its 12 internal error paths
(amdgpu_dpm.c:393, 404, 415, 426, 460, 504, 559, 614, 646, 669, 704, 727)
by calling `amdgpu_free_extended_power_table()` — which
(amdgpu_dpm.c:775-792) performs 13 bare `kfree()`s and **never NULLs the
pointers** — and then returns an error.

Every caller then frees the same pointers again:

| ASIC family | 2nd free | 3rd free |
|---|---|---|
| CI (Bonaire/Hawaii) | `ci_dpm_init` → `ci_dpm_fini` (ci_dpm.c:5877) | `ci_dpm_sw_init` dpm_failed → `ci_dpm_fini` (ci_dpm.c:6321) |
| SI (Tahiti/Pitcairn) | `si_dpm_sw_init` dpm_failed → `si_dpm_fini` (si_dpm.c:7729) | — |
| KV (Kaveri/Kabini) | `kv_dpm_sw_init` dpm_failed → `kv_dpm_fini` (kv_dpm.c:3035) | — |

(si_dpm.c:7352 and ci_dpm.c:5934 hit the same multi-free with the internal
free done directly in the caller.)

**Trigger:** any `kzalloc`/`kcalloc` failure inside the parse after at least
one table was allocated — i.e. kernel memory/KVA pressure during driver
attach. All error returns in the parse are allocation failures
(`kzalloc(0)` returns `ZERO_LENGTH_PTR`, not NULL, on DragonFly —
kern_slaballoc.c:888 — so a zero-count VBIOS table does *not* trigger this;
ENOMEM-only). `_kmalloc` can return NULL via kern_slaballoc.c:947-949 and
1158-1161 (`fail:`).

**Impact:** double (CI: triple) `kfree()` of M_DRM slab objects → on this
guest's allocator config: immediate `panic: memory chunk ... is already
free!`; on builds without the free-state check: silent slab-freelist
corruption (double-alloc primitive → classic memcorrupt ceiling).

## Reproduction on the guest (no AMD hardware needed)

The PoC calls the **real** `amdgpu_free_extended_power_table()` exported by
the stock `/boot/kernel/amdgpu.ko` on a fabricated `struct amdgpu_device`
whose two dep-table entries were allocated exactly the way
`amdgpu_parse_clk_voltage_dep_table()` allocates them. It then performs the
driver's exact error-path call sequence (free-all → fini → fini). It does
not copy any vulnerable code — the panic comes from the shipped binary.

```
./build.sh         # scp module + make on guest (matches shipped amdgpu.ko layout)
./run.sh           # kldload amdgpu; kldload df4525demo.ko  -> PANIC (see run.log / panic.txt)
./fix_validate.sh  # patch /usr/src with fix.diff, rebuild amdgpu.ko, rerun -> NO panic
```

Baseline (stock module, decisive lines — full text in run.log/panic.txt):
```
DF-4525: allocated vddc_entries=0xfffff8008976ba00 vddci_entries=0xfffff8008976ba80
DF-4525: after 1st free: vddc=0xfffff8008976ba00 vddci=0xfffff8008976ba80   <- dangling
panic: memory chunk 0xfffff8008976ba00 is already free!
chunk_mark_free() → _kfree() → amdgpu_free_extended_power_table() → df4525_event()
```

Patched (`fix.diff`, NULL-after-free; verified in the binary: `movq $0x0`
after each `kfree`):
```
DF-4525: after 1st free: vddc=0 vddci=0
DF-4525: after 2nd free: DOUBLE KFREE PERFORMED      <- now 13x kfree(NULL), no-op
DF-4525: re-alloc a=0xfffff8008976ba80 b=0xfffff8008976ba00 c=... d=...  (4 distinct)
MODULES LOADED, NO PANIC  — guest stays up
```

## Honest scope note

The guest has no AMD GPU, so the *in-driver* trigger (attach-time ENOMEM on
a CI/SI/KV card) could not be exercised end-to-end; its reachability is
proven statically (path:line chain above; allocator NULL paths cited). The
function-level defect (free-without-NULL + multi-call teardown) was
reproduced against the real shipped binary, and the proposed fix was
built and validated on the guest. Exploitability beyond DoS requires
(a) the specific ASIC, (b) reliably forcing a mid-parse allocation failure,
(c) a kernel without the double-free trip — not a practical unpriv→root
chain; rated Medium.

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