Commit cbda101fe30 for php
commit cbda101fe3008d61e8474da41beff876daa82dcb
Author: Martin Mirchev <mmirchev@comp.nus.edu.sg>
Date: Tue Oct 27 00:30:51 2026 +0800
Fix unmapping of an unrelated range when the aligned chunk mmap fails (#24542)
When the first mmap() in zend_mm_chunk_alloc_int() is not suitably
aligned, it is released and a larger mapping is requested. If that
second mmap() failed, the POSIX code carried on with ptr == NULL and
called munmap() on [size, size + alignment - page size), an absolute
low address range. In a non-PIE binary this range can contain the
executable itself, so the process crashed instead of reporting
"Out of memory". Return NULL when the retry fails.
diff --git a/NEWS b/NEWS
index 5ea9ecbafb0..480df545c24 100644
--- a/NEWS
+++ b/NEWS
@@ -30,7 +30,9 @@ PHP NEWS
for grouping. (Klaas Skelte van der Werf)
. Fixed bug GH-24265 (Unwind/GracefulExit discarded in
zend_fiber_execute()). (David Carlier)
-
+ . Fixed unmapping of an unrelated range when the aligned chunk mmap fails
+ (Marti2203).
+
- Date:
. Fixed bug GH-20503 (Assertion failure with ext/date DateInterval property
hash construction). (heathdutton)
diff --git a/Zend/tests/zend_mm_chunk_alloc_mmap_failure.phpt b/Zend/tests/zend_mm_chunk_alloc_mmap_failure.phpt
new file mode 100644
index 00000000000..b137f25620c
--- /dev/null
+++ b/Zend/tests/zend_mm_chunk_alloc_mmap_failure.phpt
@@ -0,0 +1,32 @@
+--TEST--
+Failed retry mmap() in zend_mm_chunk_alloc_int() must not unmap an unrelated range
+--EXTENSIONS--
+posix
+--SKIPIF--
+<?php
+if (PHP_OS_FAMILY !== 'Linux') die('skip Linux only');
+if (PHP_INT_SIZE != 8) die('skip this test is for 64bit platforms only');
+if (getenv('USE_ZEND_ALLOC') === '0') die('skip Zend MM disabled');
+$maps = @file_get_contents('/proc/self/maps');
+if ($maps === false) die('skip /proc/self/maps not available');
+if (!preg_match('/^([0-9a-f]+)-[0-9a-f]+ r-xp /m', $maps, $m) || hexdec($m[1]) > 0x40000000) {
+ die('skip only for non-PIE builds');
+}
+?>
+--INI--
+memory_limit=-1
+--FILE--
+<?php
+// A huge block whose size equals the start of the executable mapping. If the
+// first mmap() is not 2 MiB aligned and the retry fails under RLIMIT_AS, the
+// missing NULL check used to unmap [size, size + 2 MiB - 4 KiB), i.e. our text.
+preg_match('/^([0-9a-f]+)-[0-9a-f]+ r-xp /m', file_get_contents('/proc/self/maps'), $m);
+$size = hexdec($m[1]);
+preg_match('/VmSize:\s+(\d+)/', file_get_contents('/proc/self/status'), $m);
+// Room for the first mmap() of $size bytes, not for the aligned retry.
+var_dump(posix_setrlimit(POSIX_RLIMIT_AS, $m[1] * 1024 + $size + 1024 * 1024, POSIX_RLIMIT_INFINITY));
+$s = str_repeat('A', $size - 4096);
+echo "allocation succeeded\n";
+?>
+--EXPECTREGEX--
+bool\(true\)\s+(.*Out of memory|allocation succeeded)\s*
diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c
index 3cbdd1e5f5d..2b0b2d62a19 100644
--- a/Zend/zend_alloc.c
+++ b/Zend/zend_alloc.c
@@ -905,6 +905,9 @@ static void *zend_mm_chunk_alloc_int(size_t size, size_t alignment)
/* chunk has to be aligned */
zend_mm_munmap(ptr, size);
ptr = zend_mm_mmap(size + alignment - REAL_PAGE_SIZE);
+ if (ptr == NULL) {
+ return NULL;
+ }
#ifdef _WIN32
offset = ZEND_MM_ALIGNED_OFFSET(ptr, alignment);
if (offset != 0) {