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

dal_vector_destruct() leaves container dangling β€” second destruct is a double kfree (latent; double-free reproduced at runtime, fix semantics runtime-validated)

Summary

destruct kfrees container but does not NULL it (unlike dal_vector_destroy) - a second destruct double-frees; a later reserve kreallocs the dangling pointer. Both in-tree callers destruct once and NULL the wrapper - latent. RUNTIME-REPRODUCED on guest: second destruct panics 'memory chunk already free'; NULL-out (the exact fix state change) makes destruct #2 a clean no-op with distinct subsequent allocations. One-line container=NULL fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5052 Β· 10 files
FileTypeDescriptionSize
README.md β€” 1.5 KB ↓ raw
VERDICT.md β€” 3.4 KB ↓ raw
kv.c β€” 15.1 KB view raw
mkpatch.c β€” 2.6 KB view raw
vectorpoc.c β€” 6.4 KB view raw
run-baseline.sh β€” 1.4 KB view raw
run-fixcheck.sh β€” 1.3 KB view raw
panic-baseline-doublefree.log β€” 13.6 KB view raw
fix-semantics-validation.log β€” 691 B view raw
fix.diff β€” 1.2 KB view raw

DF-5052 β€” dal_vector_destruct() leaves container dangling β†’ double kfree (runtime-demonstrated)

sys/dev/drm/amd/display/dc/basics/vector.c:131-137.

Build / Run / Expected

Same /dev/kmem call facility as DF-5051 (see ../DF-5051/README.md and kv.c).

cc -O2 -o /root/kv kv.c
kldload amdgpu
V=$(/root/kv call <base+11d070> 0 4 10)    # dal_vector_create(NULL,4,16)
/root/kv call <base+11d120> $V             # dal_vector_destruct  #1 (frees container)
KV_BCOPY=<bcopy> /root/kv kread $V 32      # container STILL set (dangling), count/capacity 0
/root/kv call <base+11d120> $V             # dal_vector_destruct  #2 -> kfree(dangling)
# EXPECTED (unfixed): panic "memory chunk 0x... is already free!" with trace
#   chunk_mark_free() -> _kfree() -> dal_vector_destruct() -> (trampoline) -> syscall2

Fix-semantics check (run-fixcheck.sh): after destruct #1, write NULL over vector->container (exactly the state change fix.diff makes the function perform itself), then destruct #2 must return cleanly (kfree(NULL) is a no-op via the kfree macro's NULL guard, sys/dev/drm/include/linux/slab.h:36-39) and two subsequent 64-byte allocations must return distinct pointers.

Results

Baseline: REPRODUCED β€” panic: memory chunk 0xfffff80116c07ac0 is already free! attributed to dal_vector_destruct+0x20 (panic-baseline-doublefree.log).

Fix semantics: CONFIRMED β€” with container=NULL injected between the calls, destruct #2 returned without any panic and the re-allocations were distinct (fix-semantics-validation.log).

VERDICT.md
↓ download raw

DF-5052 β€” VERDICT

Finding: dal_vector_destruct() (vector.c:131-137) frees container and resets count/capacity to zero, but leaves container pointing at the freed chunk (contrast dal_vector_destroy, vector.c:139-147, which NULLs the outer pointer). Any second dal_vector_destruct() on the same vector β€” or any later dal_vector_reserve()/dal_vector_at_index() on it β€” operates on freed memory. dal_vector_reserve in particular would krealloc() the dangling pointer (vector.c:293) and then write through it.

Source analysis (pass 2): the two in-tree destruct callers (dal_ddc_i2c_payloads_destroy / dal_ddc_aux_payloads_destroy, dc_link_ddc.c:160-168, 197-205) each destruct once and NULL their wrapper β€” no in-tree double-destruct exists, so like DF-5051 this is a latent library defect (the exported API cannot defend itself because destruct does not clear the one field that makes re-use dangerous).

Runtime verification (same guest/facility as DF-5051):

  1. dal_vector_create(NULL, 4, 16) β†’ valid vector (container=0xfffff80116c07ac0, struct_size=16, count=0, capacity=4).
  2. dal_vector_destruct(V) β€” bcopy-read of the vector afterwards shows container still 0xfffff80116c07ac0 (dangling), count=0, capacity=0 β€” the exact unfixed state from the source.
  3. dal_vector_destruct(V) again:
panic: memory chunk 0xfffff80116c07ac0 is already free!
Trace: chunk_mark_free() at chunk_mark_free+0xae
       _kfree() at _kfree+0x262
       dal_vector_destruct() at dal_vector_destruct+0x20
       (call trampoline in sys_sbrk body) -> syscall2()

(panic-baseline-doublefree.log β€” serial console capture.)

Primitive: confirmed double-free of a 64-byte DRM-slab chunk, attributed to the vendored function itself. On this KKASSERT-enabled dev kernel the slab catches it; on release kernels (no INVARIANTS) the chunk would re-enter the per-CPU magazine twice β€” the classic aliasing primitive (two kmallocs of the same size returning the same pointer). Latent (no in-tree second destruct), same severity class as DF-2068/DF-5051: Low.

Fix validation (fix-semantics-validation.log): - baseline (this run): destruct #2 β†’ panic "already free" (reproduced). - with the fix's state change emulated (kmem-write NULL over container between the destructs β€” precisely what vector->container = NULL; in fix.diff makes the function do): destruct #2 returns rc=0, no panic, and two fresh 64-byte allocations return DISTINCT pointers (0xfffff80116c07b00 vs 0xfffff80116c07b40). Guest stays up. The kfree(NULL) no-op relies on the DRM kfree macro's existing NULL guard (sys/dev/drm/include/linux/slab.h:36-39), verified in the disassembly of the running dal_vector_destruct (test %rdi,%rdi; je at +0x12). fix_status: fixed (semantics runtime-validated; source diff included).

Artifacts: panic-baseline-doublefree.log, fix-semantics-validation.log, run-baseline.sh, run-fixcheck.sh, kv.c, mkpatch.c, vectorpoc.c, fix.diff.

Guest: DragonFly 6.5-DEV #0 Jul 2 2026 (X86_64_GENERIC), QEMU/KVM; left clean (with-src snapshot).

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

fixed
baseline reproduced→ patch + rebuild →patched reproduced

Baseline double-free panic reproduced; with the fix's state change (container=NULL) applied between the destructs, destruct #2 returns cleanly with no panic and no allocator aliasing β€” the exact behavior of the fixed source. Full fixed-module rebuild impossible on this harness (kmod loader broken for fresh builds), hence semantic-level validation

fix-semantics-validation.log; fix.diff
↓ fix.diffper-fix-DF-5052

Confirmed kernel references

Detail

Evidence (decisive lines)

panic-baseline-doublefree.log (decisive panic + attribution trace); fix-semantics-validation.log (kread showing dangling container after destruct #1; container=NULL; destruct #2 rc=0; distinct reallocs; guest up)

PoC changes

Rebuilt the kmod sketch as the /dev/kmem sysent[534]+trampoline call facility (kv.c) because freshly-linked kmods cannot load on this guest snapshot

Verified recommended fix

dal_vector_destruct: set vector->container = NULL after kfree (one line), mirroring dal_vector_destroy's hygiene

Verdict

REPRODUCED on the QEMU guest via the /dev/kmem call facility: calling the real dal_vector_destruct() from amdgpu.ko twice on the same vector panics with 'panic: memory chunk 0xfffff80116c07ac0 is already free!' attributed to dal_vector_destruct+0x20 (trace: chunk_mark_free <- _kfree <- dal_vector_destruct). Between the two calls a bcopy-read of the vector shows the exact unfixed state from the source: container still pointing at the freed chunk while count/capacity are already zeroed. FIX SEMANTICS RUNTIME-VALIDATED: emulating the fix's exact state change (kmem-write NULL over vector->container after destruct #1 β€” precisely what 'vector->container = NULL;' in fix.diff makes the function do) makes destruct #2 a clean no-op (kfree(NULL) guard, slab.h:36-39, visible in the function's own test/je at +0x12), no panic, and two subsequent 64-byte allocations return distinct pointers. LATENT: no in-tree double-destruct exists (both destruct wrappers in dc_link_ddc.c NULL their outer pointer), so severity Low like DF-2068/DF-5051.