From 5e54440c24a650cb8bdac5e43ee92da43a3f2148 Mon Sep 17 00:00:00 2001 From: jvoisin Date: Tue, 18 Aug 2026 23:17:12 +0200 Subject: [PATCH] Detect immediate double-frees of zend_mm small slots Freeing the same small pointer twice in a row pushed it onto the freelist twice, so the next two allocations of that bin returned the same address. That's a nifty primitive to obtain two live pointers of different types to the same object. The shadow-pointer check does not catch it, as both links are consistent. This commit adds a simple check for when the freed pointer already is the head of the freelist. heap->free_slot[bin_num] is loaded by the very next line, so the check costs a single comparison on an already-hot value. This only catches consecutive double-frees, not a free after other activity on the same bin, but it doesn't cost ~anything performance wise, and catches real bugs like error/cleanup paths freeing the same value twice. A quick look at `git log --grep='double.free'` shows that this is a popular bug pattern. --- Zend/zend_alloc.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c index fc7bc1f4d9d4..d934346acf7a 100644 --- a/Zend/zend_alloc.c +++ b/Zend/zend_alloc.c @@ -1430,6 +1430,12 @@ static zend_always_inline void zend_mm_free_small(zend_mm_heap *heap, void *ptr, #endif p = (zend_mm_free_slot*)ptr; +#if ZEND_MM_HEAP_PROTECTION + /* Catch the most common double-free pattern for free. */ + if (UNEXPECTED(p == heap->free_slot[bin_num])) { + zend_mm_panic("zend_mm_heap corrupted (double free)"); + } +#endif zend_mm_set_next_free_slot(heap, bin_num, p, heap->free_slot[bin_num]); heap->free_slot[bin_num] = p; }