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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3955 Β· 11 files
FileTypeDescriptionSize
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).

VERDICT.md
↓ download raw

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

  1. kv_parse_power_table allocates rdev->pm.dpm.ps (ucNumEntries radeon_ps structs, kv_dpm.c:2660-2663) then per-state ps_priv kv_ps structs (:2673). If the i-th kzalloc fails: kfree(rdev->pm.dpm.ps) (:2675), return -ENOMEM. The pointer is left dangling; rdev->pm.dpm.num_ps is only assigned AFTER the loop (:2700) so it stays 0 β€” hiding the i already parsed ps_priv objects from kv_dpm_fini's loop (leak).

  2. 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).

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

  4. Also note: even before fini, any dereference of rdev->pm.dpm.ps between 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 = 0 and 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_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-3955

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.