[php-src] Issue #24488: 8.4.27 / 8.5.12 regression: GC double-frees the SimpleXMLElement properties table (`zend_mm_heap corrupted`), introduced by
the GH-24050 fix
| From: | evankford | Date: | Wed, 21 Oct 2026 17:42:49 +0000 |
| Subject: | [php-src] Issue #24488: 8.4.27 / 8.5.12 regression: GC double-frees the SimpleXMLElement properties table (`zend_mm_heap corrupted`), introduced by the GH-24050 fix |
||
| Groups: | php.bugs | ||
| Request: | Send a blank email to php-bugs+get-253055@lists.php.net to get a copy of this message | ||
Issue: https://github.com/php/php-src/issues/24488
Author: evankford
### Description
## Description
The following code:
```php
<?php
for ($i = 0; $i < 2; $i++) {
$xml = new SimpleXMLElement('<a><b/></a>');
get_object_vars($xml);
$holder = new stdClass;
$holder->xml = $xml;
$holder->self = $holder;
unset($xml, $holder);
gc_collect_cycles();
$junk[] = str_repeat('x', 100);
}
echo "done\n";
```
Resulted in this output:
```
zend_mm_heap corrupted
```
But I expected this output instead:
```
done
```
It fails every time on 8.4.27 and 8.5.12 (official
php:*-cli Docker images, aarch64)
and on a macOS NTS build of 8.4.27. It passes on 8.4.26.
Each of these is required to trigger it:
- Something that materializes the SimpleXML properties table (get_object_vars(), an
(array) cast, or json_encode()). foreach or plain property
access does not trigger it.
- The SimpleXMLElement must be reachable from a cycle that the GC collects.
With the system allocator, a single GC run is enough. Here is a one-shot version:
```php
<?php
$xml = new SimpleXMLElement('<a><b/></a>');
get_object_vars($xml);
$holder = new stdClass;
$holder->xml = $xml;
$holder->self = $holder;
unset($xml, $holder);
gc_collect_cycles();
```
```
$ USE_ZEND_ALLOC=0 php one-shot.php
free(): double free detected in tcache 2
```
Valgrind output (8.4.27 built with -g -O2, USE_ZEND_ALLOC=0):
```
Invalid read of size 4
at zend_gc_collect_cycles (zend_gc.c:2070)
by zif_gc_collect_cycles (zend_builtin_functions.c:124)
Address 0xb4b0324 is 4 bytes inside a block of size 56 free'd
at free
by zend_gc_collect_cycles (zend_gc.c:2081) <- obj->handlers->free_obj(obj)
Block was alloc'd at
by _zend_new_array_0 (zend_hash.c:327)
by sxe_get_prop_hash (simplexml.c:1096)
by zend_std_get_properties_for (zend_object_handlers.c:2643)
by zif_get_object_vars (zend_builtin_functions.c:801)
Invalid write of size 4
at zend_gc_collect_cycles (zend_gc.c:2089)
... (26 invalid accesses total)
```
**Bisected cause:** the hunk added to gc_collect_white() by 7b75d71ca3f6 (GH-24050, PR
#24061):
```c
if (GC_REF_CHECK_COLOR(ht, GC_WHITE)) {
GC_REF_SET_BLACK(ht);
+ if (!GC_INFO(ht)) {
+ gc_add_garbage((zend_refcounted *)ht);
+ }
```
I built 8.4.27 from the release tarball twice. The unmodified build crashes. The same build with
only those three lines removed prints done and is clean under valgrind.
The table that get_gc returns is now added to the garbage buffer.
sxe_get_gc() returns sxe->properties, a table that SimpleXML owns.
sxe_object_free_storage() releases it with zend_hash_release() while the
GC is freeing the object. The GC then reaches the table's own garbage slot and reads, destroys
and frees memory that has already been freed.
Using the same pattern with stdClass (dynamic properties), ArrayObject and
DateTime is clean under valgrind. Any other extension whose get_gc returns
a table it owns and releases itself may be affected the same way.
**Real-world impact:** our Laravel/Pest suite crashes paratest workers with zend_mm_heap
corrupted (exit 134) on 8.4.27. Mockery expectations return collections of
SimpleXMLElements, the mock and its expectations form a cycle, and PHPUnit's
comparator/exporter reads all properties of the elements. We have pinned CI to 8.4.26.
### PHP Version
```plain
PHP 8.4.27 (cli) (built: Oct 21 2026 15:54:26) (NTS)
Copyright (c) The PHP Group
Built by https://github.com/docker-library/php
Zend Engine v4.4.27, Copyright (c) Zend Technologies
with Zend OPcache v8.4.27, Copyright (c), by Zend Technologies
Also reproduces on 8.5.12 (php:8.5-cli). Not reproducible on 8.4.26.
```
### Operating System
Debian (official php:8.4.27-cli Docker image, aarch64); also macOS (Laravel Herd build)