From d68fcd88bd911f02120ef7ee227d4f4754966cac Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 20 Mar 2026 21:25:30 -0400 Subject: [PATCH] Fix GH-21478: Forward read_property to real instance for initialized lazy proxies For initialized lazy proxies, the proxy and real instance have separate magic method guard slots. When the real instance's __get/__isset is running and code inside it accesses the proxy, the proxy's guard is clear, causing __get/__isset to fire on the proxy too (double invocation). Check whether the real instance's guard is already set before invoking magic methods on the proxy. If it is, forward to the real instance (we're inside a recursive call). If not, the proxy was accessed directly and its own magic methods should run as normal. --- Zend/tests/lazy_objects/gh18038-004.phpt | 1 - Zend/tests/lazy_objects/gh18038-007.phpt | 1 - Zend/tests/lazy_objects/gh20875.phpt | 8 ----- .../gh21478-proxy-get-override.phpt | 30 +++++++++++++++++ Zend/tests/lazy_objects/gh21478.phpt | 32 +++++++++++++++++++ Zend/zend_object_handlers.c | 17 ++++++++++ 6 files changed, 79 insertions(+), 10 deletions(-) create mode 100644 Zend/tests/lazy_objects/gh21478-proxy-get-override.phpt create mode 100644 Zend/tests/lazy_objects/gh21478.phpt diff --git a/Zend/tests/lazy_objects/gh18038-004.phpt b/Zend/tests/lazy_objects/gh18038-004.phpt index 8810efb6bec2e..c1495c5a6d8d6 100644 --- a/Zend/tests/lazy_objects/gh18038-004.phpt +++ b/Zend/tests/lazy_objects/gh18038-004.phpt @@ -36,7 +36,6 @@ var_dump($real->prop); --EXPECTF-- init string(19) "RealInstance::__get" -string(12) "Proxy::__get" Warning: Undefined property: RealInstance::$prop in %s on line %d NULL diff --git a/Zend/tests/lazy_objects/gh18038-007.phpt b/Zend/tests/lazy_objects/gh18038-007.phpt index 9925190a19801..4c7c0d0b4b0a6 100644 --- a/Zend/tests/lazy_objects/gh18038-007.phpt +++ b/Zend/tests/lazy_objects/gh18038-007.phpt @@ -36,6 +36,5 @@ var_dump(isset($real->prop[''])); --EXPECT-- init string(21) "RealInstance::__isset" -string(14) "Proxy::__isset" bool(false) bool(false) diff --git a/Zend/tests/lazy_objects/gh20875.phpt b/Zend/tests/lazy_objects/gh20875.phpt index 72e16011320c3..ff036edabd596 100644 --- a/Zend/tests/lazy_objects/gh20875.phpt +++ b/Zend/tests/lazy_objects/gh20875.phpt @@ -31,14 +31,6 @@ Warning: Undefined variable $a in %s on line %d Warning: Undefined variable $v in %s on line %d -Notice: Indirect modification of overloaded property A::$b has no effect in %s on line %d - -Warning: Undefined variable $x in %s on line %d - -Notice: Object of class stdClass could not be converted to int in %s on line %d - -Warning: Undefined variable $v in %s on line %d - Notice: Indirect modification of overloaded property A::$f has no effect in %s on line %d Fatal error: Uncaught Error: Cannot assign by reference to overloaded object in %s:%d diff --git a/Zend/tests/lazy_objects/gh21478-proxy-get-override.phpt b/Zend/tests/lazy_objects/gh21478-proxy-get-override.phpt new file mode 100644 index 0000000000000..520c8f6623531 --- /dev/null +++ b/Zend/tests/lazy_objects/gh21478-proxy-get-override.phpt @@ -0,0 +1,30 @@ +--TEST-- +GH-21478: Proxy's own __get runs when accessed directly (not from real instance) +--FILE-- +newLazyProxy(function () { + return new Foo(); +}); +$rc->initializeLazyObject($proxy); + +$proxy->x; + +?> +--EXPECT-- +Bar x diff --git a/Zend/tests/lazy_objects/gh21478.phpt b/Zend/tests/lazy_objects/gh21478.phpt new file mode 100644 index 0000000000000..aaa226a9a09a7 --- /dev/null +++ b/Zend/tests/lazy_objects/gh21478.phpt @@ -0,0 +1,32 @@ +--TEST-- +GH-21478 (Property access on lazy proxy may invoke magic method despite real instance guards) +--FILE-- +{$name}; + } +} + +class Bar extends Foo {} + +$rc = new ReflectionClass(Bar::class); +$proxy = $rc->newLazyProxy(function () { + echo "Init\n"; + return new Foo(); +}); + +$real = $rc->initializeLazyObject($proxy); +$real->x; + +?> +--EXPECTF-- +Init +__get($x) on Foo + +Warning: Undefined property: Foo::$x in %s on line %d diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c index 7e03139dc426b..c41d76bf453a8 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -893,6 +893,23 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int retval = &EG(uninitialized_zval); + /* For initialized lazy proxies: if the real instance's magic method + * guard is already set for this property, we are inside a recursive + * call from the real instance's __get/__isset. Forward directly to + * the real instance to avoid double invocation. (GH-21478) */ + if (UNEXPECTED(zend_object_is_lazy_proxy(zobj) + && zend_lazy_object_initialized(zobj))) { + zend_object *instance = zend_lazy_object_get_instance(zobj); + if (instance->ce->ce_flags & ZEND_ACC_USE_GUARDS) { + uint32_t *instance_guard = zend_get_property_guard(instance, name); + uint32_t guard_type = ((type == BP_VAR_IS) && zobj->ce->__isset) + ? IN_ISSET : IN_GET; + if ((*instance_guard) & guard_type) { + return zend_std_read_property(instance, name, type, cache_slot, rv); + } + } + } + /* magic isset */ if ((type == BP_VAR_IS) && zobj->ce->__isset) { zval tmp_result;