Commit 4a1cca7ddc3 for php.net

commit 4a1cca7ddc3deb615aa8e9cc66a9b4c9cf3d8f35
Author: Arnaud Le Blanc <arnaud.lb@gmail.com>
Date:   Wed Oct 15 16:08:43 2025 +0200

    Revert lazy proxy state after failed initialization

    We don't expect the lazy proxy to be modified during initialization, but
    this is allowed. The modification may set a property, still marked LAZY,
    without removing the LAZY flag. This causes an assertion failure in GH-20174.

    Both the RFC and the documentation specify that after an initialization
    failure, the state of the object is reset to its pre-initialization state:

        If the initializer throws an exception, the object state is reverted to
        its pre-initialization state and the object is marked as lazy again. In
        other words, all effects on the object itself are reverted. Other side
        effects, such as effects on other objects, are not reverted. This prevents
        exposing a partially initialized instance in case of failure.

    This behavior would have prevented this issue, but it was not implemented
    for lazy proxies (only for ghosts).

    Fix by implementing the missing behavior.

    Fixes GH-20174
    Closes GH-20181

diff --git a/NEWS b/NEWS
index 7f3d46f7885..e26c9eca2fd 100644
--- a/NEWS
+++ b/NEWS
@@ -13,6 +13,9 @@ PHP                                                                        NEWS
   . Fixed GH-20564 (Don't call autoloaders with pending exception). (ilutov)
   . Fix deprecation now showing when accessing null key of an array with JIT.
     (alexandre-daubois)
+  . Fixed bug GH-20174 (Assertion failure in
+    ReflectionProperty::skipLazyInitialization after failed LazyProxy
+    initialization). (Arnaud)

 - Date:
   . Update timelib to 2022.16. (Derick)
diff --git a/Zend/tests/lazy_objects/gh20174.phpt b/Zend/tests/lazy_objects/gh20174.phpt
new file mode 100644
index 00000000000..2bce09b4dc7
--- /dev/null
+++ b/Zend/tests/lazy_objects/gh20174.phpt
@@ -0,0 +1,33 @@
+--TEST--
+GH-20174: Assertion failure in ReflectionProperty::skipLazyInitialization after failed LazyProxy skipLazyInitialization
+--CREDITS--
+vi3tL0u1s
+--FILE--
+<?php
+
+class C {
+    public $b;
+    public $c;
+}
+
+$reflector = new ReflectionClass(C::class);
+
+$obj = $reflector->newLazyProxy(function ($obj) {
+    $obj->b = 4;
+    throw new Exception();
+});
+
+try {
+    $reflector->initializeLazyObject($obj);
+} catch (Exception $e) {
+    $reflector->getProperty('b')->skipLazyInitialization($obj);
+}
+
+var_dump($obj);
+
+?>
+--EXPECTF--
+lazy proxy object(C)#%d (1) {
+  ["b"]=>
+  NULL
+}
diff --git a/Zend/tests/lazy_objects/init_exception_reverts_initializer_changes.phpt b/Zend/tests/lazy_objects/init_exception_reverts_initializer_changes.phpt
index 4edf9481ebc..695da60229c 100644
--- a/Zend/tests/lazy_objects/init_exception_reverts_initializer_changes.phpt
+++ b/Zend/tests/lazy_objects/init_exception_reverts_initializer_changes.phpt
@@ -46,7 +46,6 @@ function test(string $name, object $obj) {
     throw new Exception('initializer exception');
 });

-// Initializer effects on the proxy are not reverted
 test('Proxy', $obj);

 --EXPECTF--
@@ -63,12 +62,10 @@ function test(string $name, object $obj) {
 # Proxy:
 string(11) "initializer"
 initializer exception
-lazy proxy object(C)#%d (3) {
-  ["a"]=>
-  int(3)
+lazy proxy object(C)#%d (1) {
   ["b"]=>
-  int(4)
+  uninitialized(int)
   ["c"]=>
-  int(5)
+  int(0)
 }
 Is lazy: 1
diff --git a/Zend/tests/lazy_objects/init_exception_reverts_initializer_changes_dyn_props.phpt b/Zend/tests/lazy_objects/init_exception_reverts_initializer_changes_dyn_props.phpt
index ce94fc8b2ab..19d3eac7a9f 100644
--- a/Zend/tests/lazy_objects/init_exception_reverts_initializer_changes_dyn_props.phpt
+++ b/Zend/tests/lazy_objects/init_exception_reverts_initializer_changes_dyn_props.phpt
@@ -49,7 +49,6 @@ function test(string $name, object $obj) {
     throw new Exception('initializer exception');
 });

-// Initializer effects on the proxy are not reverted
 test('Proxy', $obj);

 --EXPECTF--
@@ -66,14 +65,10 @@ function test(string $name, object $obj) {
 # Proxy:
 string(11) "initializer"
 initializer exception
-lazy proxy object(C)#%d (4) {
-  ["a"]=>
-  int(3)
+lazy proxy object(C)#%d (1) {
   ["b"]=>
-  int(4)
+  uninitialized(int)
   ["c"]=>
-  int(5)
-  ["d"]=>
-  int(6)
+  int(0)
 }
 Is lazy: 1
diff --git a/Zend/tests/lazy_objects/init_exception_reverts_initializer_changes_dyn_props_and_ht.phpt b/Zend/tests/lazy_objects/init_exception_reverts_initializer_changes_dyn_props_and_ht.phpt
index 1bc3eb2cea8..c11a0eda7aa 100644
--- a/Zend/tests/lazy_objects/init_exception_reverts_initializer_changes_dyn_props_and_ht.phpt
+++ b/Zend/tests/lazy_objects/init_exception_reverts_initializer_changes_dyn_props_and_ht.phpt
@@ -52,7 +52,6 @@ function test(string $name, object $obj) {
     throw new Exception('initializer exception');
 });

-// Initializer effects on the proxy are not reverted
 test('Proxy', $obj);

 --EXPECTF--
@@ -73,14 +72,10 @@ function test(string $name, object $obj) {
 }
 string(11) "initializer"
 initializer exception
-lazy proxy object(C)#%d (4) {
-  ["a"]=>
-  int(3)
+lazy proxy object(C)#%d (1) {
   ["b"]=>
-  int(4)
+  uninitialized(int)
   ["c"]=>
-  int(5)
-  ["d"]=>
-  int(6)
+  int(0)
 }
 Is lazy: 1
diff --git a/Zend/tests/lazy_objects/init_exception_reverts_initializer_changes_props_ht.phpt b/Zend/tests/lazy_objects/init_exception_reverts_initializer_changes_props_ht.phpt
index c4f0bd98773..7419a4a31da 100644
--- a/Zend/tests/lazy_objects/init_exception_reverts_initializer_changes_props_ht.phpt
+++ b/Zend/tests/lazy_objects/init_exception_reverts_initializer_changes_props_ht.phpt
@@ -49,7 +49,6 @@ function test(string $name, object $obj) {
     throw new Exception('initializer exception');
 });

-// Initializer effects on the proxy are not reverted
 test('Proxy', $obj);

 --EXPECTF--
@@ -74,12 +73,10 @@ function test(string $name, object $obj) {
 }
 string(11) "initializer"
 initializer exception
-lazy proxy object(C)#%d (3) {
-  ["a"]=>
-  int(3)
+lazy proxy object(C)#%d (1) {
   ["b"]=>
-  int(4)
+  uninitialized(int)
   ["c"]=>
-  int(5)
+  int(0)
 }
 Is lazy: 1
diff --git a/Zend/tests/lazy_objects/init_exception_reverts_initializer_changes_props_ht_ref.phpt b/Zend/tests/lazy_objects/init_exception_reverts_initializer_changes_props_ht_ref.phpt
index 094f5c9b809..cea95c3c8a7 100644
--- a/Zend/tests/lazy_objects/init_exception_reverts_initializer_changes_props_ht_ref.phpt
+++ b/Zend/tests/lazy_objects/init_exception_reverts_initializer_changes_props_ht_ref.phpt
@@ -55,7 +55,6 @@ function test(string $name, object $obj) {
     throw new Exception('initializer exception');
 });

-// Initializer effects on the proxy are not reverted
 test('Proxy', $obj);

 --EXPECTF--
@@ -83,13 +82,11 @@ function test(string $name, object $obj) {
 }
 string(11) "initializer"
 initializer exception
-lazy proxy object(C)#%d (3) {
-  ["a"]=>
-  int(3)
+lazy proxy object(C)#%d (1) {
   ["b"]=>
-  int(4)
+  uninitialized(int)
   ["c"]=>
-  int(5)
+  int(0)
 }
 Is lazy: 1

diff --git a/Zend/zend_lazy_objects.c b/Zend/zend_lazy_objects.c
index 7d3d7f584ba..e88ecf9fbe6 100644
--- a/Zend/zend_lazy_objects.c
+++ b/Zend/zend_lazy_objects.c
@@ -479,6 +479,24 @@ static zend_object *zend_lazy_object_init_proxy(zend_object *obj)
 	/* prevent reentrant initialization */
 	OBJ_EXTRA_FLAGS(obj) &= ~(IS_OBJ_LAZY_UNINITIALIZED|IS_OBJ_LAZY_PROXY);

+	zval *properties_table_snapshot = NULL;
+
+	/* Snapshot dynamic properties */
+	HashTable *properties_snapshot = obj->properties;
+	if (properties_snapshot) {
+		GC_TRY_ADDREF(properties_snapshot);
+	}
+
+	/* Snapshot declared properties */
+	if (obj->ce->default_properties_count) {
+		zval *properties_table = obj->properties_table;
+		properties_table_snapshot = emalloc(sizeof(*properties_table_snapshot) * obj->ce->default_properties_count);
+
+		for (int i = 0; i < obj->ce->default_properties_count; i++) {
+			ZVAL_COPY_PROP(&properties_table_snapshot[i], &properties_table[i]);
+		}
+	}
+
 	/* Call factory */
 	zval retval;
 	int argc = 1;
@@ -492,33 +510,29 @@ static zend_object *zend_lazy_object_init_proxy(zend_object *obj)
 	zend_call_known_fcc(initializer, &retval, argc, &zobj, named_params);

 	if (UNEXPECTED(EG(exception))) {
-		OBJ_EXTRA_FLAGS(obj) |= IS_OBJ_LAZY_UNINITIALIZED|IS_OBJ_LAZY_PROXY;
-		goto exit;
+		goto fail;
 	}

 	if (UNEXPECTED(Z_TYPE(retval) != IS_OBJECT)) {
-		OBJ_EXTRA_FLAGS(obj) |= IS_OBJ_LAZY_UNINITIALIZED|IS_OBJ_LAZY_PROXY;
 		zend_type_error("Lazy proxy factory must return an instance of a class compatible with %s, %s returned",
 				ZSTR_VAL(obj->ce->name),
 				zend_zval_value_name(&retval));
 		zval_ptr_dtor(&retval);
-		goto exit;
+		goto fail;
 	}

 	if (UNEXPECTED(Z_TYPE(retval) != IS_OBJECT || !zend_lazy_object_compatible(Z_OBJ(retval), obj))) {
-		OBJ_EXTRA_FLAGS(obj) |= IS_OBJ_LAZY_UNINITIALIZED|IS_OBJ_LAZY_PROXY;
 		zend_type_error("The real instance class %s is not compatible with the proxy class %s. The proxy must be a instance of the same class as the real instance, or a sub-class with no additional properties, and no overrides of the __destructor or __clone methods.",
 				zend_zval_value_name(&retval),
 				ZSTR_VAL(obj->ce->name));
 		zval_ptr_dtor(&retval);
-		goto exit;
+		goto fail;
 	}

 	if (UNEXPECTED(Z_OBJ(retval) == obj || zend_object_is_lazy(Z_OBJ(retval)))) {
-		OBJ_EXTRA_FLAGS(obj) |= IS_OBJ_LAZY_UNINITIALIZED|IS_OBJ_LAZY_PROXY;
 		zend_throw_error(NULL, "Lazy proxy factory must return a non-lazy object");
 		zval_ptr_dtor(&retval);
-		goto exit;
+		goto fail;
 	}

 	zend_fcc_dtor(&info->u.initializer.fcc);
@@ -542,6 +556,21 @@ static zend_object *zend_lazy_object_init_proxy(zend_object *obj)
 		}
 	}

+	if (properties_table_snapshot) {
+		for (int i = 0; i < obj->ce->default_properties_count; i++) {
+			zval *p = &properties_table_snapshot[i];
+			/* Use zval_ptr_dtor directly here (not zend_object_dtor_property),
+			 * as any reference type_source will have already been deleted in
+			 * case the prop is not bound to this value anymore. */
+			i_zval_ptr_dtor(p);
+		}
+		efree(properties_table_snapshot);
+	}
+
+	if (properties_snapshot) {
+		zend_release_properties(properties_snapshot);
+	}
+
 	instance = Z_OBJ(retval);

 exit:
@@ -554,6 +583,11 @@ static zend_object *zend_lazy_object_init_proxy(zend_object *obj)
 	}

 	return instance;
+
+fail:
+	OBJ_EXTRA_FLAGS(obj) |= IS_OBJ_LAZY_UNINITIALIZED|IS_OBJ_LAZY_PROXY;
+	zend_lazy_object_revert_init(obj, properties_table_snapshot, properties_snapshot);
+	goto exit;
 }

 /* Initialize a lazy object. */