β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4525

amdgpu_dpm: error-path double/triple free of all dyn_state power-table pointers - amdgpu_free_extended_power_table() frees without NULLing and every caller teardown frees the dangling pointers again

Summary

free_extended_power_table kfrees all 13 dyn_state pointers without NULLing; parse_extended_power_table calls it on each of its 12 internal error paths then returns the error, leaving every already-parsed pointer dangling - and the CI/SI/KV init-failure paths free again (CI: ci_dpm_fini called directly AND via dpm_failed = triple free). VERIFIED on guest via KLD against the real shipped amdgpu.ko symbols: dangling pointer observed, then 'panic: memory chunk already free!' with the chunk_mark_free<-amdgpu_free_extended_power_table stack; fix (NULL-after-free, kfree(NULL) no-op) rebuilt into amdgpu.ko and validated - identical sequence, no panic. Trigger: kmalloc failure mid-parse under induced memory pressure at attach on CI/SI/KV hardware. NULL-after-every-free fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4525 Β· 16 files
FileTypeDescriptionSize
README.md β€” 1.7 KB ↓ raw
VERDICT.md β€” 4.4 KB ↓ raw
df4525demo.c β€” 4.3 KB view raw
Makefile β€” 1.1 KB ↓ download
build.sh β€” 65 B view raw
run.sh β€” 89 B view raw
fix_validate.sh β€” 844 B view raw
demo_build.log β€” 6.9 KB view raw
fix_build.log β€” 1.2 MB ↓ download
run.log β€” 1.6 KB view raw
fix_run.log β€” 434 B view raw
panic.txt β€” 1.1 KB view raw
env.txt β€” 356 B view raw
fix.diff β€” 1.6 KB view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 5.7 KB view raw

DF-4525 evidence pack

Finding: amdgpu_dpm.c error-path double/triple free of all pm.dpm.dyn_state power-table pointers (amdgpu_free_extended_power_table frees without NULLing; internal parse error paths + ci/si/kv *_dpm_fini teardowns free again).

Reproduce

# guest: DragonFly 6.5-DEVELOPMENT QEMU snapshot (dfbsd-qemu/vm.sh up)
./build.sh        # scp + build df4525demo.ko against guest /usr/src
./run.sh          # baseline: stock amdgpu.ko  ->  kernel PANIC (serial console)
./fix_validate.sh # apply fix.diff to guest /usr/src, rebuild amdgpu.ko, rerun -> clean

Expected

  • Baseline: panic: memory chunk 0xfffff8008976ba00 is already free! with amdgpu_free_extended_power_table() in the trace; vm.sh status β‡’ down. (run.log, panic.txt)
  • Patched: after 1st free: vddc=0 vddci=0, second call is kfree(NULL), 4 distinct re-allocs, MODULES LOADED, NO PANIC, guest stays up. (fix_run.log)

Files

file what
df4525demo.c KLD performing the driver's exact double-free call sequence against the real shipped amdgpu_free_extended_power_table()
Makefile module build (include set mirrors sys/dev/drm/amd/amdgpu/Makefile so struct layouts match the shipped .ko)
build.sh / run.sh / fix_validate.sh exact commands
run.log, panic.txt baseline panic (serial console capture)
demo_build.log, fix_build.log untrimmed module + patched-amdgpu.ko build logs
fix_run.log patched-module rerun (no panic)
fix.diff NULL-after-free for all 13 dyn_state pointers (git-apply-able)
VERDICT.md full narrative
manifest.json / verdict.json machine-readable
VERDICT.md
↓ download raw

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Rebuilt amdgpu.ko from patched /usr/src (fix.diff = NULL-after-free for all 13 pointers; verified movq $0x0 after each kfree in the shipped-symbol disassembly). Identical demo: baseline stock module panics in amdgpu_free_extended_power_table; patched module leaves vddc/vddci NULL after the first free, the second call is a kfree(NULL) no-op, 4 fresh allocations are all distinct, no panic, guest stays up. The double-free behavior is gone.

fix_run.log: 'DF-4525: after 1st free: vddc=0 vddci=0' + 'MODULES LOADED, NO PANIC'; fix_build.log: full patched amdgpu.ko build (DONE_RC=0); baseline A/B in run.log/panic.txt
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (amdgpu.ko rebuilt from guest /usr/src + fix.diff)

Confirmed kernel references

Detail

Exploit chain

kernel memory pressure during amdgpu attach on a Sea Islands (ci)/Southern Islands (si)/Kaveri (kv) GPU -> kzalloc failure mid power-table parse -> internal free-all leaves dangling pointers -> caller *_dpm_fini frees them again (double/triple kfree) -> slab freelist corruption (panic on allocator-checked builds such as this guest; silent double-alloc primitive on unchecked builds, the theoretical privesc ceiling - not practical here because the trigger cannot be timed deterministically by an unprivileged user)

Evidence (decisive lines)

panic.txt + run.log: 'after 1st free: vddc=0xfffff8008976ba00' (dangling, not NULLed) then 'panic: memory chunk 0xfffff8008976ba00 is already free!' with amdgpu_free_extended_power_table in the stack; fix_run.log: 'after 1st free: vddc=0 vddci=0', second call no-op, 4 distinct re-allocs, no panic, guest up; fix_build.log shows the patched amdgpu.ko link line; objdump of patched module shows movq $0x0 after each kfree

PoC changes

PoC authored fresh (no seed): KLD built with the same include set as the shipped amdgpu.ko so struct amdgpu_device (58656 bytes) layout matches; calls the REAL exported amdgpu_free_extended_power_table from stock amdgpu.ko twice on a fabricated adev; fixed two build/load issues (missing display include paths; MODULE_DEPEND version mismatch on amdgpu -> removed, stock amdgpu.ko kldload'ed first so the linker resolves the symbol)

Verified recommended fix

NULL all 13 dyn_state pointers in amdgpu_free_extended_power_table() after kfree() (kfree(NULL) is a no-op), making every error-path/fini double-call safe; optionally also make the vddgfx error path (amdgpu_dpm.c:765-768) call amdgpu_free_extended_power_table() like its siblings

Verdict

amdgpu_free_extended_power_table() (amdgpu_dpm.c:775-792) kfrees all 13 dyn_state power-table pointers without NULLing them; amdgpu_parse_extended_power_table() invokes it on each of its 12 internal error paths (amdgpu_dpm.c:393..727) and returns the error, after which the ASIC teardown layers free the same dangling pointers again (ci_dpm.c:5877 + ci_dpm.c:6321 = triple free on CI; si_dpm.c:7729 and kv_dpm.c:3035 = double free). The defect (dangling pointers + multi-call teardown) was reproduced on the guest against the real stock /boot/kernel/amdgpu.ko: the exact driver call sequence panics the kernel inside amdgpu_free_extended_power_table ('memory chunk ... is already free!'), and a rebuilt amdgpu.ko carrying fix.diff (NULL-after-free) runs the identical sequence cleanly. In-driver trigger is ENOMEM-gated (all parse error returns are allocation failures; kzalloc(0) returns ZERO_LENGTH_PTR on DragonFly, kern_slaballoc.c:888, so crafted-VBIOS zero counts do not fire it) and requires a CI/SI/KV ASIC, which the guest lacks - reachability is proven statically; no unpriv->root chain claimed.