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)
PoC verification
Evidence pack
findings/poc/DF-4525 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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!withamdgpu_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 iskfree(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 |
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
fixedRebuilt 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
Confirmed kernel references
- sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:775
- sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:393
- sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:404
- sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:415
- sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:426
- sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:460
- sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:504
- sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:559
- sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:614
- sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:646
- sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:669
- sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:704
- sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:727
- sys/dev/drm/amd/amdgpu/amdgpu_dpm.c:765
- sys/dev/drm/amd/amdgpu/ci_dpm.c:5777
- sys/dev/drm/amd/amdgpu/ci_dpm.c:5877
- sys/dev/drm/amd/amdgpu/ci_dpm.c:6321
- sys/dev/drm/amd/amdgpu/si_dpm.c:7352
- sys/dev/drm/amd/amdgpu/si_dpm.c:7729
- sys/dev/drm/amd/amdgpu/kv_dpm.c:3035
- sys/kern/kern_slaballoc.c:888
- sys/kern/kern_slaballoc.c:947
- sys/kern/kern_slaballoc.c:1158
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.
No comments yet.