From 3a21b1f827de7f0fefa9e70447f02bf173245559 Mon Sep 17 00:00:00 2001 From: lazerg Date: Mon, 26 Oct 2026 11:02:00 +0500 Subject: [PATCH 1/2] Fix GH-24715: Lazy ghost reset by a destructor while releasing its initializer --- NEWS | 3 ++ Zend/tests/lazy_objects/gh24715.phpt | 44 ++++++++++++++++++++++++++++ Zend/zend_lazy_objects.c | 13 +++++++- 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 Zend/tests/lazy_objects/gh24715.phpt diff --git a/NEWS b/NEWS index 5ea9ecbafb0b..075825c2ba53 100644 --- a/NEWS +++ b/NEWS @@ -30,6 +30,9 @@ PHP NEWS for grouping. (Klaas Skelte van der Werf) . Fixed bug GH-24265 (Unwind/GracefulExit discarded in zend_fiber_execute()). (David Carlier) + . Fixed bug GH-24715 (Assertion failure in zend_lazy_object_get_properties() + when releasing the initializer resets the object as lazy). + (Lazizbek Ergashev) - Date: . Fixed bug GH-20503 (Assertion failure with ext/date DateInterval property diff --git a/Zend/tests/lazy_objects/gh24715.phpt b/Zend/tests/lazy_objects/gh24715.phpt new file mode 100644 index 000000000000..4d7cefa1857e --- /dev/null +++ b/Zend/tests/lazy_objects/gh24715.phpt @@ -0,0 +1,44 @@ +--TEST-- +GH-24715: Object reset as lazy by a destructor triggered when releasing the initializer +--FILE-- +self = $this; + } + public function __destruct() { + global $obj, $reflector; + try { + $reflector->resetAsLazyGhost($obj, function () {}); + } catch (Error $e) { + printf("%s: %s\n", $e::class, $e->getMessage()); + } + } +} + +new D(); + +$reflector = new ReflectionClass(C::class); +$obj = $reflector->newLazyGhost(new class { + public function __invoke($obj) { + $obj->a = 1; + } + public function __destruct() { + gc_collect_cycles(); + } +}); + +var_dump(serialize($obj)); +var_dump($reflector->isUninitializedLazyObject($obj)); + +?> +--EXPECT-- +Error: Can not reset an object while it is being initialized +string(24) "O:1:"C":1:{s:1:"a";i:1;}" +bool(false) diff --git a/Zend/zend_lazy_objects.c b/Zend/zend_lazy_objects.c index eb8b5f300eeb..bc43053a5d88 100644 --- a/Zend/zend_lazy_objects.c +++ b/Zend/zend_lazy_objects.c @@ -76,7 +76,9 @@ static void zend_lazy_object_info_dtor_func(zval *pElement) zend_object_release(info->u.instance); } else { zval_ptr_dtor(&info->u.initializer.zv); - zend_fcc_dtor(&info->u.initializer.fcc); + if (ZEND_FCC_INITIALIZED(info->u.initializer.fcc)) { + zend_fcc_dtor(&info->u.initializer.fcc); + } } efree(info); @@ -670,6 +672,15 @@ ZEND_API zend_object *zend_lazy_object_init(zend_object *obj) zend_release_properties(properties_snapshot); } + /* Release the initializer while the info is still registered, so that + * destructors triggered by this can not reset the object */ + zend_lazy_object_info *info = zend_hash_index_find_ptr(&EG(lazy_objects_store).infos, obj->handle); + zval initializer_zv; + ZVAL_COPY_VALUE(&initializer_zv, &info->u.initializer.zv); + ZVAL_UNDEF(&info->u.initializer.zv); + zend_fcc_dtor(&info->u.initializer.fcc); + zval_ptr_dtor(&initializer_zv); + /* Must be very last in this function, for the * zend_lazy_object_has_stale_info() check */ zend_lazy_object_del_info(obj); From e19d671204cb421b614dcc3e1e8937de1fdf055e Mon Sep 17 00:00:00 2001 From: lazerg Date: Mon, 26 Oct 2026 11:53:44 +0500 Subject: [PATCH 2/2] Release the initializer before deleting the info in markLazyObjectAsInitialized() too --- .../{gh24715.phpt => gh24715-001.phpt} | 0 Zend/tests/lazy_objects/gh24715-002.phpt | 33 +++++++++++++++++++ Zend/zend_lazy_objects.c | 24 +++++++++----- 3 files changed, 49 insertions(+), 8 deletions(-) rename Zend/tests/lazy_objects/{gh24715.phpt => gh24715-001.phpt} (100%) create mode 100644 Zend/tests/lazy_objects/gh24715-002.phpt diff --git a/Zend/tests/lazy_objects/gh24715.phpt b/Zend/tests/lazy_objects/gh24715-001.phpt similarity index 100% rename from Zend/tests/lazy_objects/gh24715.phpt rename to Zend/tests/lazy_objects/gh24715-001.phpt diff --git a/Zend/tests/lazy_objects/gh24715-002.phpt b/Zend/tests/lazy_objects/gh24715-002.phpt new file mode 100644 index 000000000000..a7aa165a4633 --- /dev/null +++ b/Zend/tests/lazy_objects/gh24715-002.phpt @@ -0,0 +1,33 @@ +--TEST-- +GH-24715 002: Object reset as lazy by a destructor triggered when releasing the initializer in markLazyObjectAsInitialized() +--FILE-- +resetAsLazyGhost($obj, function () {}); + } catch (Error $e) { + printf("%s: %s\n", $e::class, $e->getMessage()); + } + } +} + +$reflector = new ReflectionClass(C::class); +$d = new D(); +$obj = $reflector->newLazyGhost(function ($obj) use ($d) {}); +unset($d); + +var_dump($reflector->markLazyObjectAsInitialized($obj) === $obj); +var_dump($reflector->isUninitializedLazyObject($obj)); + +?> +--EXPECT-- +Error: Can not reset an object while it is being initialized +bool(true) +bool(false) diff --git a/Zend/zend_lazy_objects.c b/Zend/zend_lazy_objects.c index bc43053a5d88..5a9800da3d14 100644 --- a/Zend/zend_lazy_objects.c +++ b/Zend/zend_lazy_objects.c @@ -161,6 +161,20 @@ zend_lazy_object_flags_t zend_lazy_object_get_flags(zend_object *obj) return zend_lazy_object_get_info(obj)->flags; } +/* Release the initializer while the info is still registered, so that + * destructors triggered by this can not reset the object */ +static void zend_lazy_object_release_initializer(zend_object *obj) +{ + zend_lazy_object_info *info = zend_hash_index_find_ptr(&EG(lazy_objects_store).infos, obj->handle); + ZEND_ASSERT(info && !(info->flags & ZEND_LAZY_OBJECT_INITIALIZED)); + + zval initializer_zv; + ZVAL_COPY_VALUE(&initializer_zv, &info->u.initializer.zv); + ZVAL_UNDEF(&info->u.initializer.zv); + zend_fcc_dtor(&info->u.initializer.fcc); + zval_ptr_dtor(&initializer_zv); +} + void zend_lazy_object_del_info(zend_object *obj) { zend_result res = zend_hash_index_del(&EG(lazy_objects_store).infos, obj->handle); @@ -412,6 +426,7 @@ ZEND_API zend_object *zend_lazy_object_mark_as_initialized(zend_object *obj) } } + zend_lazy_object_release_initializer(obj); zend_lazy_object_del_info(obj); return obj; @@ -672,14 +687,7 @@ ZEND_API zend_object *zend_lazy_object_init(zend_object *obj) zend_release_properties(properties_snapshot); } - /* Release the initializer while the info is still registered, so that - * destructors triggered by this can not reset the object */ - zend_lazy_object_info *info = zend_hash_index_find_ptr(&EG(lazy_objects_store).infos, obj->handle); - zval initializer_zv; - ZVAL_COPY_VALUE(&initializer_zv, &info->u.initializer.zv); - ZVAL_UNDEF(&info->u.initializer.zv); - zend_fcc_dtor(&info->u.initializer.fcc); - zval_ptr_dtor(&initializer_zv); + zend_lazy_object_release_initializer(obj); /* Must be very last in this function, for the * zend_lazy_object_has_stale_info() check */