[php-src] PR #23869: Fix alignment in `zend_string_safe_alloc()`/`zend_string_safe_realloc()`
| From: | realFlowControl | 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.