Commit 383b5bb7c57 for php.net

commit 383b5bb7c579709fbb635fbea992b89307ec47cf
Author: jvoisin <julien.voisin@dustri.org>
Date:   Tue Aug 18 23:17:12 2026 +0200

    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.

diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c
index 575b54b11a2..02de1a543da 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;
 }