Double free of adev->pm.dpm.ps (and leak of parsed ps_priv states) on the kzalloc-failure path of kv_parse_power_table
Summary
kv_parse_power_table frees adev->pm.dpm.ps when a per-state kzalloc fails, without clearing the pointer and before num_ps is ever assigned. The -ENOMEM propagates to dpm_failed which calls kv_dpm_fini: the priv loop runs 0 iterations (leaking every already-parsed ps_priv) and kfree(adev->pm.dpm.ps) frees the same block a second time: allocator freelist corruption (silent; classic aliasing primitive). Same code exists in Linux amdgpu. Requires engineered allocation failure mid-parse (OOM during driver init). VERIFIED: exact sequence harness; the second free is accepted silently by the guest allocator matching kfree's silent freelist re-insertion.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3891 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| double_free.c | β | 3.8 KB | view raw | |
| build.sh | β | 95 B | view raw | |
| run.sh | β | 116 B | view raw | |
| build.log | β | 107 B | view raw | |
| run.log | β | 367 B | view raw | |
| run.2.log | β | 613 B | view raw | |
| env.txt | β | 561 B | view raw | |
| fix.diff | β | 451 B | view raw | |
| VERDICT.md | β | 3.4 KB | β raw | |
| README.md | β | 1.7 KB | β raw | |
| verdict.json | β | 3.1 KB | view raw |
DF-3891 β double free of adev->pm.dpm.ps on the allocation-failure path of kv_parse_power_table
File: sys/dev/drm/amd/amdgpu/kv_dpm.c:2750 (first free, pointer not cleared) + :2919 (kv_dpm_fini frees again); related leak of ps_priv allocations at :2753
Class: CWE-415 double free (+ CWE-401 leak of the already-parsed state privs)
Verdict: inconclusive at runtime on this guest (no AMD GPU); source-certain call sequence; harness shows the second free() accepted silently by the allocator.
Build (on the DragonFly guest, as root)
mkdir -p /tmp/DF-3891 && cp double_free.c /tmp/DF-3891/h.c cd /tmp/DF-3891 && cc -Wall -O0 -o t h.c
Run
./t # accounting β exact call sequence ./t crash # performs the verbatim free(); free() sequence
Expected
- accounting:
kv_parse_power_table: kfree(ps) at :2750 (pointer left dangling; num_ps still 0)thenkv_dpm_fini: kfree(ps) at :2919on the same pointer. - crash: the second
free()is accepted silently (no allocator detection, exit=0) β in the kernel the samekfree()re-inserts an already-freed block into the kmalloc freelist, i.e. latent heap corruption (two future allocations may alias); the userspace allocator did not hand out aliased blocks in this short run, which is reported honestly.
Kernel trigger
kzalloc failure for a struct kv_ps while parsing state i (memory pressure
during driver init) β kfree(adev->pm.dpm.ps) at :2750, return -ENOMEM with
num_ps still 0 β kv_dpm_sw_init dpm_failed β kv_dpm_fini:
0-iteration priv loop (states 0..i-1 leak) + kfree(adev->pm.dpm.ps) again
at :2919 β double free.
DF-3891 β verdict narrative
Finding
kv_parse_power_table (kv_dpm.c:2702-2790) allocates the power-state array once
and then per-state privs:
adev->pm.dpm.ps = kcalloc(state_array->ucNumEntries, sizeof(struct amdgpu_ps), GFP_KERNEL); /* :2736 */
...
for (i = 0; i < state_array->ucNumEntries; i++) {
...
ps = kzalloc(sizeof(struct kv_ps), GFP_KERNEL); /* :2748 */
if (ps == NULL) {
kfree(adev->pm.dpm.ps); /* :2750 <-- freed, NOT nulled */
return -ENOMEM; /* :2751 <-- num_ps still 0 (:2775 not reached) */
}
adev->pm.dpm.ps[i].ps_priv = ps; /* :2753 */
}
adev->pm.dpm.num_ps = state_array->ucNumEntries; /* :2775 */
kv_dpm_init propagates the error; kv_dpm_sw_init's dpm_failed path calls
kv_dpm_fini (kv_dpm.c:2912-2922):
for (i = 0; i < adev->pm.dpm.num_ps; i++) /* 0 iterations: states 0..k-1 ps_priv LEAK */
kfree(adev->pm.dpm.ps[i].ps_priv);
kfree(adev->pm.dpm.ps); /* :2919 <-- DOUBLE FREE of :2750's free */
Two defects on this path: (1) double free of adev->pm.dpm.ps β the second
kfree re-inserts a freed block into the allocator freelist (silent heap
corruption; classic exploitation primitive for freelist-based allocators, though
the trigger requires engineering an allocation failure at a precise point during
driver init); (2) leak of every already-parsed ps_priv.
Preconditions: KV-family APU + kzalloc failure mid-parse (severe memory
pressure during amdgpu load). Not attacker-timed to order; robustness bug.
Root cause (kernel references)
sys/dev/drm/amd/amdgpu/kv_dpm.c:2750β free without clearing/num_ps updatesys/dev/drm/amd/amdgpu/kv_dpm.c:2919β second free in kv_dpm_finisys/dev/drm/amd/amdgpu/kv_dpm.c:3034-3035β dpm_failed β kv_dpm_fini
What was verified, and how
- Source-level certainty. Full-file pass-2 audit; error path traced through
kv_dpm_init(:2855-2857) intokv_dpm_sw_initdpm_failed (:3034-3035);num_psisint(amdgpu_dpm.h:370) and is only assigned on success (:2775), sokv_dpm_fini's loop cannot free the privs. - Harness on the guest (DragonFly 6.5-DEVELOPMENT x86_64, base cc 8.3):
double_free.creplays the exact sequence withcalloc/malloc/free. The secondfree()is accepted silently (exit=0; DF's libc allocator did not detect the double free, and no aliasing surfaced in a 6-allocation probe β reported honestly). The kernel'skfreewould likewise accept it and corrupt the freelist. Userspace cannot faithfully emulate kmalloc zone behavior; the call-sequence proof is the deliverable. - Hardware absence. Guest is virtio-only; the in-kernel path additionally needs a KV APU. Recorded inconclusive, impact none on this guest; source certain.
Fix validation
fix.diff sets adev->pm.dpm.num_ps = i instead of freeing, so kv_dpm_fini
releases exactly the parsed privs and the array once. Not built on the guest β
fix_status: not_testable.
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
not_testablefix.diff authored against the read-only sys/ tree after source+harness verification; not built (no AMD hardware on guest).
findings/poc/DF-3891/fix.diff
Confirmed kernel references
Detail
Exploit chain
engineer kzalloc failure for a struct kv_ps during amdgpu load (memory pressure) -> kfree(dpm.ps) at :2750 -> return -ENOMEM -> kv_dpm_fini -> kfree(dpm.ps) again at :2919 -> same block twice on the freelist -> two future kernel allocations alias -> heap corruption (timing-dependent, not guest-verifiable).
Evidence (decisive lines)
run.log: 'kfree(ps) at :2750 (pointer left dangling; num_ps still 0)' + 'kv_dpm_fini: kfree(ps) at :2919 on 0x8004904c0'; run.2.log: second free() accepted silently (exit=0), 'no aliasing observed this run'; VERDICT.md; fix.diff
PoC changes
No seed (pass-2 self-authored). Fixed a nested block-comment typo that broke the first guest build; extended crash mode with an aliasing probe after observing the silent second free.
Verified recommended fix
On kzalloc failure in kv_parse_power_table set adev->pm.dpm.num_ps = i and return -ENOMEM without freeing, so kv_dpm_fini releases the privs and the array exactly once.
Verdict
Source-certain double free + leak on the allocation-failure path; runtime-inconclusive on this guest (no AMD GPU, and the trigger needs a kzalloc failure mid-parse under memory pressure). kv_parse_power_table (kv_dpm.c:2748-2752) frees adev->pm.dpm.ps at :2750 without clearing the pointer or setting num_ps (assigned only at :2775), then returns -ENOMEM; kv_dpm_sw_init dpm_failed (:3034-3035) calls kv_dpm_fini which kfrees the same pointer again at :2919 (double free -> allocator freelist corruption) while the 0-iteration priv loop leaks every already-parsed ps_priv (:2916-2918). Harness replays the exact sequence: the second free() is accepted silently by the guest allocator (exit=0, no detection, no aliasing in a short probe - reported honestly); in-kernel kfree of a freed block is the classic latent freelist-corruption primitive. Robustness bug: requires OOM timing during driver init on a KV APU, hence Low.
No comments yet.