From 8b3dea4c3b471a09684cd04003179ea67ae76693 Mon Sep 17 00:00:00 2001 From: jvoisin Date: Sat, 19 Sep 2026 18:34:35 +0200 Subject: [PATCH] Bound the page count in the large-run free path (Zend/zend_alloc.c) zend_mm_free_pages_ex() is the common sink for freeing large runs. It uses pages_count to reset bits in chunk->free_map via zend_mm_bitset_reset_range() and to adjust chunk->free_pages, without ever bounding page_num + pages_count to ZEND_MM_PAGES. That count is not trustworthy: - In zend_mm_free_large(), it is decoded from the 10-bit ZEND_MM_LRUN_PAGES field of the page map entry, so it can reach 1023 while a chunk only has 512 pages. The map lives in the chunk header, right next to free_map, and is reachable by a heap overflow. - In _efree_large(), it is computed straight from the caller-supplied size. The only cross-check against the map is a pair of ZEND_ASSERT()s, which are compiled out under NDEBUG, so release builds validate nothing beyond page alignment. The realloc growth path already enforces this very invariant via page_num + new_pages_count <= ZEND_MM_PAGES. --- Zend/zend_alloc.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c index 4fc8926c0fcc..a2500de3e8b7 100644 --- a/Zend/zend_alloc.c +++ b/Zend/zend_alloc.c @@ -1239,6 +1239,11 @@ static zend_always_inline void zend_mm_delete_chunk(zend_mm_heap *heap, zend_mm_ static zend_always_inline void zend_mm_free_pages_ex(zend_mm_heap *heap, zend_mm_chunk *chunk, uint32_t page_num, uint32_t pages_count, int free_chunk) { + /* pages_count is decoded from a page map entry or a caller-supplied size, both of which + * may be corrupted. An out-of-range run would make the updates below reach past free_map + * into the rest of the chunk header. */ + ZEND_MM_CHECK(page_num >= ZEND_MM_FIRST_PAGE && pages_count != 0 + && page_num + pages_count <= ZEND_MM_PAGES, "zend_mm_heap corrupted"); chunk->free_pages += pages_count; zend_mm_bitset_reset_range(chunk->free_map, page_num, pages_count); chunk->map[page_num] = 0;