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)
PoC verification
Evidence pack
findings/poc/DF-5052 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
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):
dal_vector_create(NULL, 4, 16)β valid vector (container=0xfffff80116c07ac0, struct_size=16, count=0, capacity=4).dal_vector_destruct(V)β bcopy-read of the vector afterwards showscontainerstill 0xfffff80116c07ac0 (dangling), count=0, capacity=0 β the exact unfixed state from the source.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
fixedBaseline 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
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.
No comments yet.