Double free of rdev->pm.dpm.ps (and leak of parsed ps_priv states) on the kzalloc-failure path of kv_parse_power_table
Summary
On mid-loop kzalloc failure the error path kfrees dpm.ps without NULLing it while num_ps stays 0, hiding the already-parsed ps_priv objects from fini's loop (leak). The swallowed -ENOMEM means the dangling pointer persists; kv_dpm_fini at detach kfrees it a second time -> silent heap corruption. VERIFIED: exact-sequence replay with injected OOM on the guest - dangling pointer + leaked ps_priv count + silent second free (DFly libc, like the kernel heap, does not abort). amdgpu twin DF-3891.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3955 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.0 KB | β raw | |
| VERDICT.md | β | 2.9 KB | β raw | |
| trigger_df3955.c | β | 4.1 KB | view raw | |
| build.sh | β | 92 B | view raw | |
| run.sh | β | 116 B | view raw | |
| build.log | β | 89 B | view raw | |
| run.log | β | 1.1 KB | view raw | |
| env.txt | β | 725 B | view raw | |
| fix.diff | β | 1.6 KB | view raw | |
| manifest.json | β | 1.6 KB | view raw | |
| verdict.json | β | 3.2 KB | view raw |
DF-3955 β radeon kv_dpm double-free of rdev->pm.dpm.ps on the parse-OOM path
File: sys/dev/drm/radeon/kv_dpm.c (pass 2, GLM 5.3)
Severity: Low Β· CWE: CWE-415 (double free) / CWE-401 (leak)
Confidence: likely Β· Status: inconclusive on guest (no AMD GPU); the
free/free-again sequence replayed in userland (completes silently β DragonFly
userland malloc, like the kernel heap, does not abort).
What
kv_parse_power_table() kv_dpm.c:2673-2677:
ps = kzalloc(sizeof(struct kv_ps), GFP_KERNEL);
if (ps == NULL) {
kfree(rdev->pm.dpm.ps); /* freed, pointer NOT NULLed */
return -ENOMEM; /* num_ps (set only at :2700) stays 0;
ps_priv of states 0..i-1 LEAK */
}
kv_dpm_init() propagates the error (:2787-2789) β but radeon_pm_init_dpm()
radeon_pm.c:1448 discards it, so the device lives on with a dangling
rdev->pm.dpm.ps until teardown:
kv_dpm_fini() kv_dpm.c:2861-2871:
for (i = 0; i < rdev->pm.dpm.num_ps; i++) /* num_ps == 0: ps_priv leak */
kfree(rdev->pm.dpm.ps[i].ps_priv);
kfree(rdev->pm.dpm.ps); /* SECOND free => heap corruption */
Kernel impact: double free of a sizeof(struct radeon_ps) Γ ucNumEntries
kmalloc block β silent free-list corruption, potentially adjacent to other
live radeon objects. Trigger: kzalloc(sizeof(struct kv_ps)) (small) failing
mid-parse at boot under memory pressure on KV hardware β rare by nature, hence
Low. amdgpu twin: DF-3891 (Low).
Reproduce
cd findings/poc/DF-3955 ./build.sh # cc -O2 -Wall -o trigger_df3955 trigger_df3955.c ./run.sh
Expected: analysis mode stops before the second free (reports dangling pointer + leaked count); crash mode performs the identical free/free-again sequence with the OOM injected at state #2 β note it completes silently (no libc abort on DFly), mirroring the kernel heap's non-defensive behavior.
Fix
NULL the pointer, free already-parsed ps_privs, and count them (fix.diff).
DF-3955 VERDICT
Verdict: INCONCLUSIVE at kernel level β source-certain error-path defect (free + later free-again + leak); the exact sequence replayed in userland completes silently on DragonFly libc, which β like the kernel's kmalloc/kmem heap β performs no double-free detection. Confidence: likely (the mid-loop kzalloc failure itself is memory-pressure-gated and cannot be forced without hardware).
Trace
-
kv_parse_power_table allocates
rdev->pm.dpm.ps(ucNumEntries radeon_ps structs, kv_dpm.c:2660-2663) then per-stateps_privkv_ps structs (:2673). If the i-thkzallocfails:kfree(rdev->pm.dpm.ps)(:2675), return -ENOMEM. The pointer is left dangling;rdev->pm.dpm.num_psis only assigned AFTER the loop (:2700) so it stays 0 β hiding the i already parsedps_privobjects from kv_dpm_fini's loop (leak). -
kv_dpm_init returns the error (kv_dpm.c:2787-2789) β and radeon_pm_init_dpm (radeon_pm.c:1448) throws it away, so the driver proceeds with DPM (enable fails later on real hardware paths, but the dangling pointer persists regardless of that outcome).
-
On module unload / device detach: radeon_pm_fini β radeon_pm_fini_dpm (radeon_pm.c:1705) β radeon_dpm_fini β kv_dpm_fini (kv_dpm.c:2861-2871): num_ps==0 skips the ps_priv loop (leak confirmed), then
kfree(rdev->pm.dpm.ps)frees the already-freed block a second time. -
Also note: even before fini, any dereference of
rdev->pm.dpm.psbetween the failed parse and teardown hits freed memory (UAF read); in the enable path the DF-3953 NULL boot_ps deref usually fires first (panic), which is why this is Low rather than Medium.
Harness result
- Analysis mode: prints the dangling pointer,
leaked ps_priv count = 2,num_ps = 0and stops before the second free (exit 0). - Crash mode: performs free β free-again with the failure injected; DragonFly userland malloc does not abort ("SECOND FREE COMPLETED SILENTLY") β the kernel heap likewise corrupts silently rather than panicking, which is the dangerous property being demonstrated.
Why Low
Trigger requires a small kzalloc to fail during boot-time VBIOS parse on KV hardware (memory pressure). Double-free impact in the kernel is heap corruption (potentially exploitable in principle), but the gating makes practical exploitation negligible; matches amdgpu DF-3891 (Low).
Fix validation
fix.diff NULLs the pointer, frees the already-parsed ps_priv states, and
counts them into num_ps before returning. Not built on the guest (no AMD GPU):
fix_status: not_testable. By inspection the fix removes both the double
free and the leak.
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; not built - no AMD hardware on the guest. Removes both the double free and the leak by construction.
findings/poc/DF-3955/fix.diff
Confirmed kernel references
Detail
Exploit chain
boot-time memory pressure on KV APU -> kzalloc(sizeof(struct kv_ps)) fails mid-parse -> dangling rdev->pm.dpm.ps -> driver detach/unload -> kv_dpm_fini double free -> silent kernel heap corruption. Principled exploitability of a heap double-free exists but the gating makes it negligible; primarily DoS/robustness.
Evidence (decisive lines)
run.log: 'state 2: kzalloc(kv_ps) failed -> kfree(dpm.ps); return -ENOMEM', 'dangling dpm_ps = 0x8004a16c0 ; leaked ps_priv count = 2 ; num_ps = 0', 'SECOND FREE COMPLETED SILENTLY (userland no-abort; kernel: silent heap corruption)'; VERDICT.md; fix.diff
PoC changes
No seed (pass-2 self-authored). One fix cycle: naive replay expected malloc(72) to fail on demand (it never does) - replaced with an explicit injected failure at state #2; also discovered DFly libc does NOT abort on double free, so the harness now reports the silent completion explicitly instead of implying a libc abort.
Verified recommended fix
On the parse error path free the already-parsed ps_priv states, NULL rdev->pm.dpm.ps, and track num_ps incrementally (fix.diff); honor kv_dpm_init()'s return value at radeon_pm.c:1448.
Verdict
Source-certain error-path defect, amdgpu twin DF-3891 (Low). kv_parse_power_table (kv_dpm.c:2673-2677) on mid-loop kzalloc failure: kfree(rdev->pm.dpm.ps) without NULLing, num_ps (assigned only at :2700) stays 0, ps_priv objects of states 0..i-1 leak. radeon_pm.c:1448 discards kv_dpm_init()'s -ENOMEM, so the device persists with the dangling pointer; kv_dpm_fini (:2861-2871) at detach then kfree's rdev->pm.dpm.ps a second time (silent kernel heap corruption) and its num_ps==0 loop skips the leaked ps_privs. Harness replays the exact sequence with the OOM injected: dangling pointer + 'leaked ps_priv count = 2' + silent second free on DFly libc. Runtime-inconclusive: no AMD GPU, and the trigger itself needs a small kzalloc to fail at boot under memory pressure on KV hardware (rare) - hence Low.
No comments yet.