Repository navigation
8.4.27 / 8.5.12 regression: GC double-frees the SimpleXMLElement properties table (zend_mm_heap corrupted), introduced by the GH-24050 fix #24488
Description
Description
Description
The following code:
<?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, orjson_encode()).foreachor 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
$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 7b75d71 (GH-24050, PR #24061):
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
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)
Activity
evankford commentedon Oct 21, 2026
@devnexen
I have a proposed fix, one line in SimpleXML, replacing
zend_hash_release(sxe->properties)withzend_array_release(sxe->properties);I am working on the tests/repro off of 8.4, would you like to take it over or I can try to get a PR up?
devnexen commentedon Oct 21, 2026
I came to the same conclusion but I m still testing it, if confirmed I ll mention you in the NEWS.
Fix phpGH-24488: GC double-frees the SimpleXMLElement properties table
evankford commentedon Oct 21, 2026
I've opened #24494 with a proposed fix. The analysis below was written with an LLM (Claude) and checked by me.
evankford commentedon Oct 21, 2026
I got the repro and fix into a simple PR, I think.
No pressure to use my PR, obviously. Thanks for checking this out, and let me know if I can help in any way.
datasage commentedon Oct 21, 2026
I did see this start to fail in one of my test suites after the update today. I am not sure this has caused any production impacts yet, still investigating.
steven-actual commentedon Oct 22, 2026
Confirming this regression from a second, unrelated code base, on a different architecture and build than the original report.
Our production CIs on our self-hosted runners (Ubuntu 24.04) are impacted
zend_mm_heap corrupted In Process.php line 498: The process has been signaled with signal "6".Environment
php8.5-cli 8.5.12-1+0~20261021.27+debian13~1.gbp1a3e8a, built Oct 21 2026 08:09:42opcache.enable_cli=0), JIT disabled; also reproduced withopcache.enable_cli=1+opcache.jit=tracingpcov.enabled=0) and with Xdebug 3.5.3 loaded (xdebug.mode=offandcoverage)(object)anduseshare an array #24050, commit 7b75d71, merged Oct 2 2026) shipped in 8.5.12 and 8.4.27 only.Results
gh24488.phptreproducer from #24494donezend_mm_heap corrupted, exit 139zend_mm_heap corrupted, SIGABRTzend.enable_gc=0USE_ZEND_ALLOC=0Real-world trigger
No
get_object_vars()orgc_collect_cycles()in user code. The chain is:SimpleXMLElementfrom a mocked SOAP client (theSimpleXMLElementis held by the mock's expectation object, which sits in a cycle: container -> mock -> expectation -> return value).SimpleXMLElementas the HTTP response; the framework does(array)/json_encode()on it, which materializessxe->propertiesthroughzend_std_get_properties_for().gc_possible_root_when_full()runszend_gc_collect_cycles(), theSimpleXMLElementis destroyed inside the collection andsxe_object_free_storage()releases the properties table withzend_hash_release(). The table was also added to the GC buffer by the newgc_collect_white()hunk, so the collector frees it a second time.zend_fetch_debug_backtrace->_emalloc_56).So any application that casts or JSON-encodes a
SimpleXMLElementthat later dies inside a GC run is affected, not only code callingget_object_vars().gdb backtrace of the abort (8.5.12, pcov disabled,
php8.5-cli-dbgsym)valgrind (8.5.12,
USE_ZEND_ALLOC=0 ZEND_DONT_UNLOAD_MODULES=1, pcov disabled)First invalid access, the collector touching the properties table after SimpleXML released it:
Followed by an
Invalid write of size 4atzend_gc_collect_cycles (zend_gc.c:2150)on the same block, and the double free of the table's bucket storage:(The
simplexml.soframes are unresolved because DEB.SURY.ORG ships no dbgsym forphp8.5-xml; the.cframes come fromphp8.5-cli-dbgsym.)Note on the fix
The one-line change in #24494 (
zend_hash_release->zend_array_releaseinsxe_object_free_storage) fixes SimpleXML, but the engine-level hardening suggested there seems worth doing as well: after the GH-24050 hunk, everyget_gchandler that returns a table it owns and later frees withzend_hash_release()has the same double free. This also affects production, not just test suites: any request that JSON-encodes aSimpleXMLElementthat ends up in a collected cycle can abort the worker (FPM child), which is a trivial crash vector for 8.4.27 / 8.5.12 deployments.jogoossens commentedon Oct 22, 2026
@steven-actual in case it helps until a release ships the fix from #24494 (disclosure: I'm with Hosted Power, we maintain this): our open-source extension php_uploadprogress_guard v3.2.1 has the same effect as that one-line change, without rebuilding PHP. It wraps SimpleXML's
free_objhandler sosxe->propertiesis released withzend_array_release()instead ofzend_hash_release(), and only does so on 8.4.27 and 8.5.12. On an 8.4.27 staging host, a SimpleXML script that crashed withzend_mm_heap corruptedcompletes with it loaded. It is meant as a bridge until the fixed release is out.Fix phpGH-24488: GC double-frees the SimpleXMLElement properties table
Merge branch 'PHP-8.4' into PHP-8.5
Merge branch 'PHP-8.5' into PHP-8.6
Merge branch 'PHP-8.6'