[php-src] PR #23869: Fix alignment in `zend_string_safe_alloc()`/`zend_string_safe_realloc()`

From: Date: Wed, 23 Sep 2026 18:51:38 +0000
Subject: [php-src] PR #23869: Fix alignment in `zend_string_safe_alloc()`/`zend_string_safe_realloc()`
Groups: php.git-pulls 
Request: Send a blank email to git-pulls+get-38751@lists.php.net to get a copy of this message
Pull Request: https://github.com/php/php-src/pull/23869 Author: realFlowControl A zend_string is a 24-byte header, then the characters, then \0. zend_string_alloc() rounds that size up to a multiple of 8: ALIGN(24 + len + 1) zend_string_safe_alloc() and zend_string_safe_realloc() should do the same, but they round up only part of the size and add the rest on top: n * m + ALIGN(24 + l + 1) So these strings can be unaligned and up to 7 bytes too small. That matters because the x86_64 asm in [zend_string_equal_val()](https://github.com/php/php-src/blob/0a36b83ad2abff95fbeece3273047b5d8fdd8d89/Zend/zend_string.c#L422-L498) compares 8 bytes at a time. It expects the rounded-up size and reads up to that end. ## Example ```php implode(",", ["abc", "d", "efg"]); // "abc,d,efg", 9 chars ``` implode() calls zend_string_safe_alloc(2, 1, 7): 2 separators of 1 byte each, plus 7 bytes of pieces. So we need 34 bytes of memory (24 (header) + 9 (chars) + 1 (NULL)). In current master we do 2 * 1 + ALIGN(24 + 7 + 1) = 34. What we'd expect and what zend_string_equal_val() relies on is 40 bytes: ALIGN(2 * 1 + 24 + 9 + 1). So the asm in zend_string_equal_val() reads 6 bytes past the end of the block. ### Why did we not notice The extra bytes are almost always there anyway: - ZendMM puts 34 bytes in the 40-byte bin. - glibc malloc (USE_ZEND_ALLOC=0) gives 40 usable bytes for a 34-byte request. - ASAN does not check inline asm. - Valgrind replaces zend_string_equal_val() with a memcmp version. - On non-x86 CPUs the asm is not used. ### Reproducer Electric Fence puts each allocation at the end of a page, followed by a page you can't read. So reading past the end crashes. This works with the released PHP 8.5: ```sh docker run --rm -ti --platform linux/amd64 php:8.5-cli bash # inside the container apt-get update -qq && apt-get install -y -qq electric-fence cat > /t.php <<"EOF" <?php $a = implode(",", ["abc", "d", "efg"]); $b = implode(",", ["abc", "d", "efg"]); var_dump($a === $b); EOF USE_ZEND_ALLOC=0 EF_ALIGNMENT=1 LD_PRELOAD=/usr/lib/libefence.so php -n /t.php ``` Checking the resulting core file with gdb: ``` Core was generated by `/usr/local/bin/php '' '''. Program terminated with signal SIGSEGV, Segmentation fault. #0 0x0000004000820d1b in zend_string_equal_val () ``` ### Bonus Less memory: str_repeat("x", 25) to str_repeat("x", 31) now use a 56-byte block instead of 64.

« previous php.git-pulls (#38751) next »