[php-src] PR #24542: Fix unmapping of an unrelated range when the aligned chunk mmap fails
| From: | Marti2203 | Date: | Thu, 22 Oct 2026 14:23:24 +0000 |
| Subject: | [php-src] PR #24542: Fix unmapping of an unrelated range when the aligned chunk mmap fails | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-39706@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/24542
Author: Marti2203
If the first
mmap() in zend_mm_chunk_alloc_int() is not suitably aligned,
the mapping is released and a larger one is requested so that it can be trimmed to alignment.
Nothing checks whether that second mmap() succeeded. On POSIX, when it fails, the code
carries on with ptr == NULL:
```c
zend_mm_munmap(ptr, size);
ptr = zend_mm_mmap(size + alignment - REAL_PAGE_SIZE); /* NULL on failure */
...
offset = ZEND_MM_ALIGNED_OFFSET(ptr, alignment); /* 0 */
...
if (alignment > REAL_PAGE_SIZE) {
zend_mm_munmap((char*)ptr + size, alignment - REAL_PAGE_SIZE);
}
return ptr;
```
That is a munmap() of the absolute range [size, size + 2 MiB - 4 KiB),
followed by an ordinary "Out of memory". In a PIE binary nothing is usually mapped there,
so the stray munmap() does no harm. In a non-PIE binary (for example a plain
./configure build with clang 12 on Ubuntu 20.04, which does not default to PIE) that
range can be PHP's own text, data or heap, and the process crashes with SIGSEGV instead of
reporting "Out of memory". The second mmap() can fail under an address-space
limit (RLIMIT_AS, ulimit -v), with strict overcommit, or when the address
space is exhausted.
The Windows path already has a NULL check for the later retry (added for GH-9650). The first retry
has none on either platform.
### Reproducer
Non-PIE release build (./configure --disable-all, clang), text segment at 0x600000:
```php
<?php
$s = str_repeat('A', 0x600000 - 4096); // huge block of exactly 0x600000 bytes
echo "allocation succeeded\n";
```
```
for v in 25000 25600 26000 27096 27352 28000; do
(ulimit -v $v; php -n -d memory_limit=-1 chunk-unmap.php)
done
```
| ulimit -v (KiB) | before | after |
|---|---|---|
| 25000 | Out of memory | Out of memory |
| 25600 - 27352 | Segmentation fault | Out of memory |
| 28000 | allocation succeeded | allocation succeeded |
In the crashing window the first mmap() succeeds and the aligned retry fails. gdb shows
the crash in zend_mm_munmap() called from zend_mm_chunk_alloc_int(), and
the r-x mapping at 0x600000 is gone from info proc mappings.
### Fix
Return NULL when the retry mmap() fails. The check sits before the #ifdef
_WIN32 split, so it also covers the first retry on Windows, which until now would have passed
NULL on to zend_mm_munmap() and zend_mm_mmap_fixed().
### Test
Zend/tests/zend_mm_chunk_alloc_mmap_failure.phpt sets up this situation from inside the
script. It reads the start of the executable mapping from /proc/self/maps and uses it
as the allocation size. It then sets RLIMIT_AS (via posix_setrlimit()) to
the current VmSize plus that size plus 1 MiB, which leaves room for the first mmap()
but not for the aligned retry. Finally it allocates the block. Either a clean "Out of
memory" or a successful allocation (if the first mapping happens to be aligned) is accepted.
The test needs posix, Linux and 64-bit. It is skipped for PIE builds (where the bug cannot be
observed) and with USE_ZEND_ALLOC=0.
- Without the fix (non-PIE build): fails with Termsig=11, deterministically in my runs.
- With the fix: passes (5/5 runs).
- Zend/tests: 4918 passed, 143 skipped, 1 expected fail, 0 failed.
The Windows part of the change is not tested; I have no Windows build environment.
This issue was found by an LLM-Enhanced Static Analysis Engine and verified manually.
🤖 Generated with [Claude Code](https://claude.com/claude-code)