[php-src] Issue #24050: GC frees an object still held by a resurrected closure when `(object)` and `use` share an array

From: Date: Thu, 01 Oct 2026 16:14:39 +0000
Subject: [php-src] Issue #24050: GC frees an object still held by a resurrected closure when `(object)` and `use` share an array
Groups: php.bugs 
Request: Send a blank email to php-bugs+get-252869@lists.php.net to get a copy of this message
Issue: https://github.com/php/php-src/issues/24050 Author: OllieCrook ### Description ### Description I found my test suite suddenly crashing after a simple change. Claude first traced it back to a change made many days ago. I then had it git bisect until it found the exact time it broke which was on an AWS SDK upgrade. I then had it git bisect each change made to the SDK. It then realised that it wasn't an issue with that and instead found this. Everything else below is written by Claude Opus 5.5 Medium. I've pasted the reproduction script into eval and hopefully read the contributing guidelines correctly. 3v4l: https://3v4l.org/LkhFS#v The following code: ```php <?php // Its destructor brings the closure back to life by storing it in a global. class Resurrector { public $closure; public $self; public function __destruct() { $GLOBALS['resurrected'] = $this->closure; } } class Holder { public $castObject; public $resurrector; public $self; } $array = ['victim' => new stdClass(), 'key' => 'value']; $holder = new Holder(); $resurrector = new Resurrector(); // With only string keys, the cast object shares $array's hashtable. $holder->castObject = (object) $array; // The closure holds the same hashtable through its static vars. $resurrector->closure = function () use ($array) { return $array; }; // Self-references make both objects collectable only by the cycle collector. $holder->resurrector = $resurrector; $holder->self = $holder; $resurrector->self = $resurrector; unset($array); gc_collect_cycles(); unset($holder, $resurrector); gc_collect_cycles(); // Frees 'victim', though the resurrected closure still holds it. gc_collect_cycles(); // Reads the freed object. var_dump($resurrected()['victim']); ``` Resulted in this output on 8.4 and 8.5: ``` zend_mm_heap corrupted ``` On 8.2 and 8.3: ``` *RECURSION* ``` With USE_ZEND_ALLOC=0, all four versions: ``` malloc(): unaligned tcache chunk detected ``` But I expected this output instead: ``` object(stdClass)#1 (0) { } ``` > The notes and analysis below were written with an LLM. I checked them against an ASan debug > build and GC debug traces of 8.4.26. > > An ASan debug build of 8.4.26 reports heap-use-after-free in > gc_mark_grey. > > Casting a copy, (object) [...$array], prints the expected > output on every version, because the object and the closure no longer share a hashtable. The first > gc_collect_cycles() call also matters. Without it, the script runs > fine. > > **What I think happens** (Zend/zend_gc.c): > > 1. (object) $array with only string keys reuses the > array's hashtable as the object's property table. > 2. In the second GC run, gc_collect_white reaches the hashtable > through the cast object first. It marks it black but doesn't give it a slot in the garbage > buffer. > 3. Resurrector has a destructor, so GC rescues the nested data > of objects with destructors. gc_remove_nested_data_from_buffer walks > from the closure into its static vars and reaches the hashtable. Its > GC_REF_ADDRESS is 0, so the walk stops there and never reaches > victim. > 4. The destructor resurrects the closure, but GC still frees > victim. The third GC run reads the freed memory. > > We hit this in a test suite. A Mockery andReturnUsing closure > captured a JWT payload array with use and returned > (object) $payload. Whether it crashed depended on GC timing, so > unrelated changes (one more test, a slightly bigger vendor file) turned it on and off. > > Possibly related: #20001 (delaying destructors and GC until a safepoint). ### PHP Version ```plain PHP 8.4.26 (cli) (built: Sep 24 2026 19:11:07) (NTS) Copyright (c) The PHP Group Built by https://github.com/docker-library/php Zend Engine v4.4.26, Copyright (c) Zend Technologies with Zend OPcache v8.4.26, Copyright (c), by Zend Technologies Also reproduced on 8.2.34, 8.3.35 and 8.5.11 (official php:<version>-cli Docker images). ``` ### Operating System Debian GNU/Linux 13 (trixie), aarch64

« previous php.bugs (#252869) next »