[php-src] PR #23774: Bound the LRUN page count in the realloc shrink path
| From: | jvoisin | Date: | Sat, 19 Sep 2026 16:51:15 +0000 |
| Subject: | [php-src] PR #23774: Bound the LRUN page count in the realloc shrink path | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-38592@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/23774
Author: jvoisin
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.