From 8254e8de31e7798e66c1f4537fe518677502ea7d Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Fri, 7 Mar 2025 23:30:45 +0100 Subject: [PATCH] Fix lazy proxy calling set hook twice Writing to an uninitialized lazy proxy will initialize the underlying object and then call zend_std_write_property() on it. If this happens inside a hook, zend_std_write_property() should not call the hook again but directly write to the property slot. This didn't previously work because zend_should_call_hook() would compare the parent frame containing the proxy to the underlying object. This is now handled explicitly. Fixes GH-18000 Closes GH-18001 --- NEWS | 2 ++ Zend/tests/property_hooks/gh18000.phpt | 33 ++++++++++++++++++++++++++ Zend/zend_object_handlers.c | 20 +++++++++++++--- 3 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 Zend/tests/property_hooks/gh18000.phpt diff --git a/NEWS b/NEWS index 87c99560ec3..0ba759b4de8 100644 --- a/NEWS +++ b/NEWS @@ -17,6 +17,8 @@ PHP NEWS get_object_vars()). (ilutov) . Fixed bug GH-17998 (Skipped lazy object initialization on primed SIMPLE_WRITE cache). (ilutov) + . Fixed bug GH-17998 (Assignment to backing value in set hook of lazy proxy + calls hook again). (ilutov) - DOM: . Fixed bug GH-17991 (Assertion failure dom_attr_value_write). (nielsdos) diff --git a/Zend/tests/property_hooks/gh18000.phpt b/Zend/tests/property_hooks/gh18000.phpt new file mode 100644 index 00000000000..61b36034671 --- /dev/null +++ b/Zend/tests/property_hooks/gh18000.phpt @@ -0,0 +1,33 @@ +--TEST-- +GH-18000: Lazy proxy calls set hook twice +--FILE-- +prop = $value * 2; + } + } +} + +$rc = new ReflectionClass(C::class); + +$obj = $rc->newLazyProxy(function () { + echo "init\n"; + return new C; +}); + +function foo(C $c) { + $c->prop = 1; + var_dump($c->prop); +} + +foo($obj); + +?> +--EXPECT-- +set +init +int(2) diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c index bba40c6bd41..a4dba771e3e 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -673,9 +673,23 @@ static bool zend_is_in_hook(const zend_property_info *prop_info) static bool zend_should_call_hook(const zend_property_info *prop_info, const zend_object *obj) { - return !zend_is_in_hook(prop_info) - /* execute_data and This are guaranteed to be set if zend_is_in_hook() returns true. */ - || Z_OBJ(EG(current_execute_data)->This) != obj; + if (!zend_is_in_hook(prop_info)) { + return true; + } + + /* execute_data and This are guaranteed to be set if zend_is_in_hook() returns true. */ + zend_object *parent_obj = Z_OBJ(EG(current_execute_data)->This); + if (parent_obj == obj) { + return false; + } + + if (zend_object_is_lazy_proxy(parent_obj) + && zend_lazy_object_initialized(parent_obj) + && zend_lazy_object_get_instance(parent_obj) == obj) { + return false; + } + + return true; } static ZEND_COLD void zend_throw_no_prop_backing_value_access(zend_string *class_name, zend_string *prop_name, bool is_read)