Commit 7b75d71ca3f for php
commit 7b75d71ca3f6c3226f7feffcc25247091b4d8c9e
Author: David Carlier <devnexen@gmail.com>
Date: Fri Oct 2 11:16:06 2026 +0100
Fix GH-24050: gc_collect_white() frees data reachable from a resurrected object
A properties table shared through a cast got no garbage slot, so nested
data removal stopped at it before the destructor ran.
Close GH-24061
diff --git a/NEWS b/NEWS
index edeca96eaeb..7b24c94899c 100644
--- a/NEWS
+++ b/NEWS
@@ -30,6 +30,8 @@ PHP NEWS
function and a later request calls it. (Ilia Alshanetsky)
. Fixed System V shared memory emulation for Windows attaching segments past
the end of their mapping. (Ilia Alshanetsky)
+ . Fixed bug GH-24050 (GC frees an object still held by a resurrected closure
+ when (object) and use share an array). (David Carlier)
- DOM:
. Fixed use-after-free when re-constructing a DOMXPath whose php:function
diff --git a/Zend/tests/gh24050.phpt b/Zend/tests/gh24050.phpt
new file mode 100644
index 00000000000..d5c77fa43dd
--- /dev/null
+++ b/Zend/tests/gh24050.phpt
@@ -0,0 +1,49 @@
+--TEST--
+GH-24050 (GC frees an object still held by a resurrected closure when (object) and use share an array)
+--CREDITS--
+OllieCrook
+--FILE--
+<?php
+
+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();
+$holder->castObject = (object) $array;
+$resurrector->closure = function () use ($array) {
+ return $array;
+};
+$holder->resurrector = $resurrector;
+$holder->self = $holder;
+$resurrector->self = $resurrector;
+
+unset($array);
+gc_collect_cycles();
+
+unset($holder, $resurrector);
+gc_collect_cycles();
+gc_collect_cycles();
+
+var_dump($resurrected()['victim']);
+?>
+--EXPECT--
+object(stdClass)#1 (0) {
+}
diff --git a/Zend/tests/gh24050_2.phpt b/Zend/tests/gh24050_2.phpt
new file mode 100644
index 00000000000..a4f49d96a4e
--- /dev/null
+++ b/Zend/tests/gh24050_2.phpt
@@ -0,0 +1,49 @@
+--TEST--
+GH-24050 (GC frees an object still held by a resurrected closure when (array) and use share a properties table)
+--FILE--
+<?php
+
+class Resurrector
+{
+ public $closure;
+ public $self;
+
+ public function __destruct()
+ {
+ $GLOBALS['resurrected'] = $this->closure;
+ }
+}
+
+class Holder
+{
+ public $obj;
+ public $resurrector;
+ public $self;
+}
+
+$obj = new stdClass();
+$obj->victim = new stdClass();
+
+$holder = new Holder();
+$resurrector = new Resurrector();
+$holder->obj = $obj;
+$array = (array) $obj;
+$resurrector->closure = function () use ($array) {
+ return $array;
+};
+$holder->resurrector = $resurrector;
+$holder->self = $holder;
+$resurrector->self = $resurrector;
+
+unset($array, $obj);
+gc_collect_cycles();
+
+unset($holder, $resurrector);
+gc_collect_cycles();
+gc_collect_cycles();
+
+var_dump($resurrected()['victim']);
+?>
+--EXPECT--
+object(stdClass)#2 (0) {
+}
diff --git a/Zend/zend_gc.c b/Zend/zend_gc.c
index ffd427eac5b..669a009defe 100644
--- a/Zend/zend_gc.c
+++ b/Zend/zend_gc.c
@@ -1517,6 +1517,9 @@ static int gc_collect_white(zend_refcounted *ref, uint32_t *flags, gc_stack *sta
GC_ADDREF(ht);
if (GC_REF_CHECK_COLOR(ht, GC_WHITE)) {
GC_REF_SET_BLACK(ht);
+ if (!GC_INFO(ht)) {
+ gc_add_garbage((zend_refcounted *)ht);
+ }
for (; n != 0; n--) {
if (Z_REFCOUNTED_P(zv)) {
ref = Z_COUNTED_P(zv);