Commit 6d6d013d79f for php.net

commit 6d6d013d79fba1fd8f73fce995be73f1c531d183
Author: Arnaud Le Blanc <arnaud.lb@gmail.com>
Date:   Fri Jan 30 16:57:36 2026 +0100

    Mark object non-lazy before deleting info in zend_lazy_object_realize()

    A lazy object is marked non-lazy when all its properties are
    initialized. Before doing so we delete the object info, resulting in a
    temporarily invalid state. In GH-20657 the GC is triggered at this moment.

    Fix by deleting the object info _after_ marking it non lazy.

    Fixes GH-20657
    Closes GH-21094

diff --git a/NEWS b/NEWS
index cb2206ec1f5..343d71202de 100644
--- a/NEWS
+++ b/NEWS
@@ -4,6 +4,8 @@ PHP                                                                        NEWS

 - Core:
   . Fixed bug GH-21029 (zend_mm_heap corrupted on Aarch64, LTO builds). (Arnaud)
+  . Fixed bug GH-20657 (Assertion failure in zend_lazy_object_get_info triggered
+    by setRawValueWithoutLazyInitialization() and newLazyGhost()). (Arnaud)

 - Curl:
   . Fixed bug GH-21023 (CURLOPT_XFERINFOFUNCTION crash with a null callback).
diff --git a/Zend/tests/lazy_objects/gh20657-001.phpt b/Zend/tests/lazy_objects/gh20657-001.phpt
new file mode 100644
index 00000000000..ca3c70febca
--- /dev/null
+++ b/Zend/tests/lazy_objects/gh20657-001.phpt
@@ -0,0 +1,32 @@
+--TEST--
+GH-20657: GC during zend_lazy_object_realize()
+--CREDITS--
+vi3tL0u1s
+--FILE--
+<?php
+
+class C {
+    public $a;
+}
+
+$reflector = new ReflectionClass(C::class);
+
+for ($i = 0; $i < 10000; $i++) {
+    $obj = $reflector->newLazyGhost(function ($obj) {});
+
+    // Add to roots
+    $obj2 = $obj;
+    unset($obj2);
+
+    // Initialize all props to mark object non-lazy. Also create a cycle.
+    $reflector->getProperty('a')->setRawValueWithoutLazyInitialization($obj, $obj);
+}
+
+var_dump($obj);
+
+?>
+--EXPECTF--
+object(C)#%d (1) {
+  ["a"]=>
+  *RECURSION*
+}
diff --git a/Zend/tests/lazy_objects/gh20657-002.phpt b/Zend/tests/lazy_objects/gh20657-002.phpt
new file mode 100644
index 00000000000..daf9b767ba9
--- /dev/null
+++ b/Zend/tests/lazy_objects/gh20657-002.phpt
@@ -0,0 +1,43 @@
+--TEST--
+GH-20657 002: GC during zend_lazy_object_realize() - reset as lazy during realize()
+--FILE--
+<?php
+
+class C {
+    public $a;
+}
+
+class D {
+    public $self;
+    public function __construct() {
+        $this->self = $this;
+    }
+    public function __destruct() {
+        global $obj, $reflector;
+        $reflector->resetAsLazyGhost($obj, function () {});
+    }
+}
+
+new D();
+
+$reflector = new ReflectionClass(C::class);
+
+for ($i = 0; $i < 10000; $i++) {
+    $obj = $reflector->newLazyGhost(function ($obj) {});
+
+    // Add to roots
+    $obj2 = $obj;
+    unset($obj2);
+
+    // Initialize all props to mark object non-lazy. Also create a cycle.
+    $reflector->getProperty('a')->setRawValueWithoutLazyInitialization($obj, $obj);
+}
+
+var_dump($obj);
+
+?>
+--EXPECTF--
+object(C)#%d (1) {
+  ["a"]=>
+  *RECURSION*
+}
diff --git a/Zend/zend_lazy_objects.c b/Zend/zend_lazy_objects.c
index bf76f6e88fe..59c8ec36a9b 100644
--- a/Zend/zend_lazy_objects.c
+++ b/Zend/zend_lazy_objects.c
@@ -678,8 +678,6 @@ void zend_lazy_object_realize(zend_object *obj)
 	ZEND_ASSERT(zend_object_is_lazy(obj));
 	ZEND_ASSERT(!zend_lazy_object_initialized(obj));

-	zend_lazy_object_del_info(obj);
-
 #if ZEND_DEBUG
 	for (int i = 0; i < obj->ce->default_properties_count; i++) {
 		ZEND_ASSERT(!(Z_PROP_FLAG_P(&obj->properties_table[i]) & IS_PROP_LAZY));
@@ -687,6 +685,7 @@ void zend_lazy_object_realize(zend_object *obj)
 #endif

 	OBJ_EXTRA_FLAGS(obj) &= ~(IS_OBJ_LAZY_UNINITIALIZED | IS_OBJ_LAZY_PROXY);
+	zend_lazy_object_del_info(obj);
 }

 ZEND_API HashTable *zend_lazy_object_get_properties(zend_object *object)