From 7311ab6744264c9428ae381b5c89370d80c4365c Mon Sep 17 00:00:00 2001 From: Julien Voisin Date: Thu, 3 Sep 2026 12:32:12 +0200 Subject: [PATCH 1/2] Sanity check huge block sizes before using them as munmap() lengths (#23378) zend_mm_huge_list nodes are allocated with zend_mm_alloc_heap(), from the very heap they describe, so a heap overflow can reach them. Their size field is then handed to munmap() in three places: - zend_mm_free_huge(), directly via zend_mm_chunk_free(), - the huge block loop in zend_mm_shutdown(), likewise, - zend_mm_realloc_huge(), which takes it as old_size and passes it to zend_mm_chunk_truncate(), which unmaps the tail with munmap(addr + new_size, old_size - new_size). The ptr is constrained a bit, as it has to match the pointer being freed and is checked for chunk alignment, but size is used as-is. Corrupting it turns a free of a legitimate huge block into an unmap of an arbitrary amount of adjacent address space, which a later mmap() can then occupy. This commit bounds it before use: A live huge block has to satisfy three cheap invariants: its size is not zero, it is a multiple of REAL_PAGE_SIZE (since it was produced by ZEND_MM_ALIGNED_SIZE_EX(size, REAL_PAGE_SIZE)), and it is still accounted for in heap->real_size, which is only decremented after the block has been freed. This narrows the primitive rather than removing it, as doing so would be more invasive. This commit was validated under gdb by tampering with size on a live 4MB block and freeing it: 0, 0x400001 (unaligned) and 0x800000 (exceeding a 6MB real_size) all abort with "zend_mm_heap corrupted", where all three were previously passed to munmap(). --- Zend/zend_alloc.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c index 02de1a543da9..97cd2f895d3c 100644 --- a/Zend/zend_alloc.c +++ b/Zend/zend_alloc.c @@ -1841,6 +1841,20 @@ static zend_always_inline void *zend_mm_realloc_heap(zend_mm_heap *heap, void *p /* Huge Runs (again) */ /*********************/ +/* Huge block metadata is allocated from the very heap it describes, so a heap + * overflow can reach it. size ends up as a munmap() length, where a corrupted + * value would unmap unrelated mappings, so bound it before use: a live block is + * page aligned and is still accounted for in real_size. */ +static zend_always_inline void zend_mm_check_huge_block_size(const zend_mm_heap *heap, size_t size) +{ + ZEND_MM_CHECK(size != 0 && ZEND_MM_ALIGNED_OFFSET(size, REAL_PAGE_SIZE) == 0, "zend_mm_heap corrupted"); +#if ZEND_MM_STAT || ZEND_MM_LIMIT + ZEND_MM_CHECK(size <= heap->real_size, "zend_mm_heap corrupted"); +#else + (void)heap; +#endif +} + #if ZEND_DEBUG static void zend_mm_add_huge_block(zend_mm_heap *heap, void *ptr, size_t size, size_t dbg_size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC) #else @@ -1890,6 +1904,7 @@ static size_t zend_mm_get_huge_block_size(zend_mm_heap *heap, void *ptr ZEND_FIL zend_mm_huge_list *list = heap->huge_list; while (list != NULL) { if (list->ptr == ptr) { + zend_mm_check_huge_block_size(heap, list->size); return list->size; } list = list->next; @@ -2000,6 +2015,7 @@ static void zend_mm_free_huge(zend_mm_heap *heap, void *ptr ZEND_FILE_LINE_DC ZE ZEND_MM_CHECK(ZEND_MM_ALIGNED_OFFSET(ptr, ZEND_MM_CHUNK_SIZE) == 0, "zend_mm_heap corrupted"); size = zend_mm_del_huge_block(heap, ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC); + zend_mm_check_huge_block_size(heap, size); zend_mm_chunk_free(heap, ptr, size); #if ZEND_MM_STAT || ZEND_MM_LIMIT heap->real_size -= size; @@ -2492,6 +2508,7 @@ ZEND_API void zend_mm_shutdown(zend_mm_heap *heap, bool full, bool silent) while (list) { zend_mm_huge_list *q = list; list = list->next; + zend_mm_check_huge_block_size(heap, q->size); zend_mm_chunk_free(heap, q->ptr, q->size); } From 3c6a57189e299b139bded2ff12dd25877e1bf7f8 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Thu, 3 Sep 2026 08:36:53 -0400 Subject: [PATCH 2/2] [skip ci] Note the ZipArchive serialization BC break in UPGRADING adc5f8da339 (GH-21708) made serialize() and unserialize() throw for ZipArchive instances, which is user-visible and belongs in the BC section rather than only in NEWS. --- UPGRADING | 3 +++ 1 file changed, 3 insertions(+) diff --git a/UPGRADING b/UPGRADING index 42ee8f4228d4..596db8fb4267 100644 --- a/UPGRADING +++ b/UPGRADING @@ -319,6 +319,9 @@ PHP 8.6 UPGRADE NOTES when the "remove_all_path" option is not of type bool, or when the "comp_method", "comp_flags", or "enc_method" options are not of type int (numeric strings are coerced), instead of emitting a warning. + . Serializing or unserializing a ZipArchive now throws an Exception. A + subclass can override __serialize() and __unserialize() to round-trip + through ZipArchive::closeString() and ZipArchive::openString(). - Zlib: . deflate_init() now raises a TypeError when the value for option "level",