Commit 0c87849da5a for php.net

commit 0c87849da5a67a6f4cac4b6225cf026a7db5377b
Author: Julien Voisin <jvoisin@users.noreply.github.com>
Date:   Wed Aug 26 18:31:00 2026 +0200

    Validate large run map entries instead of assuming them (#23366)

    Three places dispatch on the page map entry of a pointer, and reach the
    large-run case by elimination, with the assumption written down as a
    comment rather than checked:

        if (EXPECTED(info & ZEND_MM_IS_SRUN)) {
            ...
        } else /* if (info & ZEND_MM_IS_LRUN) */ {

    The assumption does not always hold: when ZEND_MM_IS_FRUN is 0 and
    zend_mm_free_pages_ex() zeroes chunk->map[page_num], a pointer to a
    large run that has already been freed has info == 0, so it fails the SRUN test,
    and falls into the large-run branch. There, ZEND_MM_LRUN_PAGES(0) is 0,
    and the three callers quietly degrade:

     - zend_mm_free_heap() frees a run of zero pages, i.e. a double free of a
       large block is accepted and does nothing at all.
     - zend_mm_size() reports a block size of 0.
     - zend_mm_realloc_heap() takes old_size 0 and reallocates from there.

    A large-block double free or a use of a freed pointer is silently absorbed by
    the allocator instead of being a hard failure. This commit promotes the comment
    to a real ZEND_MM_CHECK() in all three. The value is already in a register at
    that point, so it costs a test and a branch.

    This was checked under GDB by allocating a large block, freeing it, and then
    reusing the pointer. Before, _efree() returned normally,
    _zend_mem_block_size() returned 0 and _erealloc() returned a new pointer.
    After this commit, each of the three aborts with "zend_mm_heap corrupted".

    Amusingly, the two comments naming ZEND_MM_IS_LARGE_RUN referred to a
    macro that does not exist: the real name is ZEND_MM_IS_LRUN.

diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c
index fc7bc1f4d9d..575b54b11a2 100644
--- a/Zend/zend_alloc.c
+++ b/Zend/zend_alloc.c
@@ -1527,9 +1527,11 @@ static zend_always_inline void zend_mm_free_heap(zend_mm_heap *heap, void *ptr Z
 		ZEND_MM_CHECK(chunk->heap == heap, "zend_mm_heap corrupted");
 		if (EXPECTED(info & ZEND_MM_IS_SRUN)) {
 			zend_mm_free_small(heap, ptr, ZEND_MM_SRUN_BIN_NUM(info));
-		} else /* if (info & ZEND_MM_IS_LRUN) */ {
-			int pages_count = ZEND_MM_LRUN_PAGES(info);
+		} else {
+			/* A freed large run has a zeroed map entry, so this also rejects double frees. */
+			ZEND_MM_CHECK(info & ZEND_MM_IS_LRUN, "zend_mm_heap corrupted");

+			int pages_count = ZEND_MM_LRUN_PAGES(info);
 			ZEND_MM_CHECK(ZEND_MM_ALIGNED_OFFSET(page_offset, ZEND_MM_PAGE_SIZE) == 0, "zend_mm_heap corrupted");
 			zend_mm_free_large(heap, chunk, page_num, pages_count);
 		}
@@ -1557,7 +1559,8 @@ static size_t zend_mm_size(zend_mm_heap *heap, void *ptr ZEND_FILE_LINE_DC ZEND_
 		ZEND_MM_CHECK(chunk->heap == heap, "zend_mm_heap corrupted");
 		if (EXPECTED(info & ZEND_MM_IS_SRUN)) {
 			return bin_data_size[ZEND_MM_SRUN_BIN_NUM(info)];
-		} else /* if (info & ZEND_MM_IS_LARGE_RUN) */ {
+		} else {
+			ZEND_MM_CHECK(info & ZEND_MM_IS_LRUN, "zend_mm_heap corrupted");
 			return ZEND_MM_LRUN_PAGES(info) * ZEND_MM_PAGE_SIZE;
 		}
 #endif
@@ -1752,7 +1755,8 @@ static zend_always_inline void *zend_mm_realloc_heap(zend_mm_heap *heap, void *p
 				return ret;
 			}  while (0);

-		} else /* if (info & ZEND_MM_IS_LARGE_RUN) */ {
+		} else {
+			ZEND_MM_CHECK(info & ZEND_MM_IS_LRUN, "zend_mm_heap corrupted");
 			ZEND_MM_CHECK(ZEND_MM_ALIGNED_OFFSET(page_offset, ZEND_MM_PAGE_SIZE) == 0, "zend_mm_heap corrupted");
 			old_size = ZEND_MM_LRUN_PAGES(info) * ZEND_MM_PAGE_SIZE;
 			if (size > ZEND_MM_MAX_SMALL_SIZE && size <= ZEND_MM_MAX_LARGE_SIZE) {