From 8239dd39f9864377ebda7e47777e0a06b6bd94d1 Mon Sep 17 00:00:00 2001 From: jvoisin Date: Sat, 19 Sep 2026 18:50:05 +0200 Subject: [PATCH] Bound the LRUN page count in the realloc shrink path The large-run branch of zend_mm_realloc_heap() decodes the run length from the page map entry and, in the in-place shrink case, releases the tail pages with a direct zend_mm_bitset_reset_range(): old_size = ZEND_MM_LRUN_PAGES(info) * ZEND_MM_PAGE_SIZE; ... zend_mm_bitset_reset_range(chunk->free_map, page_num + new_pages_count, rest_pages_count); The reset reaches up to page_num + old_pages_count, and old_pages_count comes straight from the 10-bit ZEND_MM_LRUN_PAGES field, so it can be as large as 1023 while a chunk only holds 512 pages. free_map is a 512-bit (8-word) bitset followed in the chunk header by the map[] array, so an out-of-range count makes the reset write past free_map into the header, turning a corrupted map entry into an out-of-bounds write. This is the same class as the bound recently added to zend_mm_free_pages_ex(), but this path resets the bitset directly and never goes through that sink, so it was left unprotected. The growth sub-branch just below already guards page_num + new_pages_count <= ZEND_MM_PAGES before touching the bitset; the shrink branch was simply asymmetric. Add the check where the run length is decoded, so it covers the in-place, shrink and grow sub-branches at once. A real large run always fits in its chunk, so page_num + ZEND_MM_LRUN_PAGES(info) <= ZEND_MM_PAGES holds and the check only fires on a corrupted heap. page_num and info are likely already in registers, so it costs a test and a branch. --- Zend/zend_alloc.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c index 4fc8926c0fcc..cdd26adf9c7e 100644 --- a/Zend/zend_alloc.c +++ b/Zend/zend_alloc.c @@ -1831,6 +1831,9 @@ static zend_always_inline void *zend_mm_realloc_heap(zend_mm_heap *heap, void *p } 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"); + /* The decoded run length drives a free_map reset in the shrink path + * below; an out-of-range count would reach past it into the chunk header. */ + ZEND_MM_CHECK(page_num + ZEND_MM_LRUN_PAGES(info) <= ZEND_MM_PAGES, "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) { new_size = ZEND_MM_ALIGNED_SIZE_EX(size, ZEND_MM_PAGE_SIZE);