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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3891 Β· 11 files
FileTypeDescriptionSize
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) then kv_dpm_fini: kfree(ps) at :2919 on the same pointer.
  • crash: the second free() is accepted silently (no allocator detection, exit=0) β€” in the kernel the same kfree() 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.

VERDICT.md
↓ download raw

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)

What was verified, and how

  1. Source-level certainty. Full-file pass-2 audit; error path traced through kv_dpm_init (:2855-2857) into kv_dpm_sw_init dpm_failed (:3034-3035); num_ps is int (amdgpu_dpm.h:370) and is only assigned on success (:2775), so kv_dpm_fini's loop cannot free the privs.
  2. Harness on the guest (DragonFly 6.5-DEVELOPMENT x86_64, base cc 8.3): double_free.c replays the exact sequence with calloc/malloc/free. The second free() 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's kfree would likewise accept it and corrupt the freelist. Userspace cannot faithfully emulate kmalloc zone behavior; the call-sequence proof is the deliverable.
  3. 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_testable
baseline no→ patch + rebuild →patched clean

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

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.