From 400cba26e242fe565520d10fd9941612acf02042 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 16 Mar 2026 20:20:52 -0400 Subject: [PATCH 1/3] Fix GH-20875: Propagate IN_GET guard in get_property_ptr_ptr for lazy proxies zend_std_get_property_ptr_ptr() was the only property handler that did not propagate the IN_GET guard to the underlying object when forwarding from a lazy proxy after initialization. This caused __get to be called on the underlying object when it shouldn't be, leading to assertion failures. The same guard-copying pattern already existed in read_property, write_property, unset_property, and has_property since commit 26f5009e91 (GH-18039). Also fixes GH-20873 and GH-20854. Closes GH-20875 --- Zend/tests/lazy_objects/gh20854.phpt | 24 ++++++++++++ Zend/tests/lazy_objects/gh20873.phpt | 32 ++++++++++++++++ Zend/tests/lazy_objects/gh20875.phpt | 55 ++++++++++++++++++++++++++++ Zend/zend_object_handlers.c | 35 +++++++++++++++--- 4 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 Zend/tests/lazy_objects/gh20854.phpt create mode 100644 Zend/tests/lazy_objects/gh20873.phpt create mode 100644 Zend/tests/lazy_objects/gh20875.phpt diff --git a/Zend/tests/lazy_objects/gh20854.phpt b/Zend/tests/lazy_objects/gh20854.phpt new file mode 100644 index 0000000000000..57880d89ad7e8 --- /dev/null +++ b/Zend/tests/lazy_objects/gh20854.phpt @@ -0,0 +1,24 @@ +--TEST-- +GH-20854 (Assertion in ZEND_RETURN_BY_REF with lazy proxy and return-by-ref __get) +--FILE-- +x; + } +} + +$rc = new ReflectionClass(C::class); +$obj = $rc->newLazyProxy(function () { + return new C; +}); +$obj->x; +echo "Done\n"; +?> +--EXPECTF-- +Deprecated: Creation of dynamic property C::$x is deprecated in %s on line %d + +Deprecated: Creation of dynamic property C::$x is deprecated in %s on line %d +Done diff --git a/Zend/tests/lazy_objects/gh20873.phpt b/Zend/tests/lazy_objects/gh20873.phpt new file mode 100644 index 0000000000000..e831c568cc153 --- /dev/null +++ b/Zend/tests/lazy_objects/gh20873.phpt @@ -0,0 +1,32 @@ +--TEST-- +GH-20873 (Assertion failure in _zendi_try_convert_scalar_to_number with lazy proxy) +--FILE-- +x =& $this->_; + static $a = $a; + $e =& $this->_ - $a; + } +} +$rc = new ReflectionClass(A::class); +$obj = $rc->newLazyProxy(fn() => new A); +$rc->initializeLazyObject($obj); +var_dump($obj->p); +?> +--EXPECTF-- +Deprecated: Creation of dynamic property A::$x is deprecated in %s on line %d + +Deprecated: Creation of dynamic property A::$x is deprecated in %s on line %d + +Warning: Undefined variable $a in %s on line %d + +Notice: Indirect modification of overloaded property A::$x has no effect in %s on line %d + +Fatal error: Uncaught Error: Cannot assign by reference to overloaded object in %s:%d +Stack trace: +#0 %s(%d): A->__get('p') +#1 {main} + thrown in %s on line %d diff --git a/Zend/tests/lazy_objects/gh20875.phpt b/Zend/tests/lazy_objects/gh20875.phpt new file mode 100644 index 0000000000000..b453e3bde9034 --- /dev/null +++ b/Zend/tests/lazy_objects/gh20875.phpt @@ -0,0 +1,55 @@ +--TEST-- +GH-20875 (Assertion failure in _get_zval_ptr_tmp with lazy proxy) +--FILE-- +f =& $this->b - $x > $y = new StdClass; + static $a = $a; + $t = 'x'; + foreach (get_defined_vars() as $key => $e) {} + if ($v ==!1) $x = $a ?: $t = "ok"; + } +} +$rc = new ReflectionClass(A::class); +$obj = $rc->newLazyProxy(function () { return new A; }); +$real = $rc->initializeLazyObject($obj); +var_dump($real->prop); +?> +--EXPECTF-- +Deprecated: Creation of dynamic property A::$b is deprecated in %s on line %d + +Deprecated: Creation of dynamic property A::$b is deprecated in %s on line %d + +Deprecated: Creation of dynamic property A::$f is deprecated in %s on line %d + +Deprecated: Creation of dynamic property A::$f is deprecated 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 $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 + +Deprecated: Creation of dynamic property A::$f is deprecated 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 +Stack trace: +#0 %s(%d): A->__get('b') +#1 %s(%d): A->__get('prop') +#2 {main} + thrown in %s on line %d diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c index ef5fb29751b8e..276c56a7fb088 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -1401,12 +1401,24 @@ ZEND_API zval *zend_std_get_property_ptr_ptr(zend_object *zobj, zend_string *nam UNEXPECTED((*zend_get_property_guard(zobj, name)) & IN_GET) || UNEXPECTED(prop_info && (Z_PROP_FLAG_P(retval) & IS_PROP_UNINIT))) { if (UNEXPECTED(zend_lazy_object_must_init(zobj) && (Z_PROP_FLAG_P(retval) & IS_PROP_LAZY))) { - zobj = zend_lazy_object_init(zobj); - if (!zobj) { + bool guarded = zobj->ce->__get + && (*zend_get_property_guard(zobj, name) & IN_GET); + zend_object *instance = zend_lazy_object_init(zobj); + if (!instance) { return &EG(error_zval); } - return zend_std_get_property_ptr_ptr(zobj, name, type, cache_slot); + if (guarded && (instance->ce->ce_flags & ZEND_ACC_USE_GUARDS)) { + uint32_t *guard = zend_get_property_guard(instance, name); + if (!(*guard & IN_GET)) { + (*guard) |= IN_GET; + retval = zend_std_get_property_ptr_ptr(instance, name, type, cache_slot); + (*guard) &= ~IN_GET; + return retval; + } + } + + return zend_std_get_property_ptr_ptr(instance, name, type, cache_slot); } if (UNEXPECTED(type == BP_VAR_RW || type == BP_VAR_R)) { if (prop_info) { @@ -1459,12 +1471,23 @@ ZEND_API zval *zend_std_get_property_ptr_ptr(zend_object *zobj, zend_string *nam } } if (UNEXPECTED(zend_lazy_object_must_init(zobj))) { - zobj = zend_lazy_object_init(zobj); - if (!zobj) { + bool guarded = (zobj->ce->__get != NULL); + zend_object *instance = zend_lazy_object_init(zobj); + if (!instance) { return &EG(error_zval); } - return zend_std_get_property_ptr_ptr(zobj, name, type, cache_slot); + if (guarded && (instance->ce->ce_flags & ZEND_ACC_USE_GUARDS)) { + uint32_t *guard = zend_get_property_guard(instance, name); + if (!(*guard & IN_GET)) { + (*guard) |= IN_GET; + retval = zend_std_get_property_ptr_ptr(instance, name, type, cache_slot); + (*guard) &= ~IN_GET; + return retval; + } + } + + return zend_std_get_property_ptr_ptr(instance, name, type, cache_slot); } if (UNEXPECTED(!zobj->properties)) { rebuild_object_properties_internal(zobj); From 13c633139cd3d34772c6a8805d482e5868f83531 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Wed, 18 Mar 2026 13:06:20 -0400 Subject: [PATCH 2/3] fix: hoist lazy init check before dynamic property deprecation Move the zend_lazy_object_must_init check in the dynamic property branch of get_property_ptr_ptr above the deprecation/error handling, so that a lazy proxy delegates to the real instance before emitting deprecation notices. This eliminates duplicate deprecation warnings that fired once on the proxy and once on the underlying object. --- Zend/tests/lazy_objects/gh20854.phpt | 2 -- Zend/tests/lazy_objects/gh20873.phpt | 2 -- Zend/tests/lazy_objects/gh20875.phpt | 14 ---------- Zend/zend_object_handlers.c | 39 ++++++++++++++-------------- 4 files changed, 20 insertions(+), 37 deletions(-) diff --git a/Zend/tests/lazy_objects/gh20854.phpt b/Zend/tests/lazy_objects/gh20854.phpt index 57880d89ad7e8..26a69cfcbbf56 100644 --- a/Zend/tests/lazy_objects/gh20854.phpt +++ b/Zend/tests/lazy_objects/gh20854.phpt @@ -18,7 +18,5 @@ $obj->x; echo "Done\n"; ?> --EXPECTF-- -Deprecated: Creation of dynamic property C::$x is deprecated in %s on line %d - Deprecated: Creation of dynamic property C::$x is deprecated in %s on line %d Done diff --git a/Zend/tests/lazy_objects/gh20873.phpt b/Zend/tests/lazy_objects/gh20873.phpt index e831c568cc153..e5b8c31e9130c 100644 --- a/Zend/tests/lazy_objects/gh20873.phpt +++ b/Zend/tests/lazy_objects/gh20873.phpt @@ -19,8 +19,6 @@ var_dump($obj->p); --EXPECTF-- Deprecated: Creation of dynamic property A::$x is deprecated in %s on line %d -Deprecated: Creation of dynamic property A::$x is deprecated in %s on line %d - Warning: Undefined variable $a in %s on line %d Notice: Indirect modification of overloaded property A::$x has no effect in %s on line %d diff --git a/Zend/tests/lazy_objects/gh20875.phpt b/Zend/tests/lazy_objects/gh20875.phpt index b453e3bde9034..ff036edabd596 100644 --- a/Zend/tests/lazy_objects/gh20875.phpt +++ b/Zend/tests/lazy_objects/gh20875.phpt @@ -21,10 +21,6 @@ var_dump($real->prop); --EXPECTF-- Deprecated: Creation of dynamic property A::$b is deprecated in %s on line %d -Deprecated: Creation of dynamic property A::$b is deprecated in %s on line %d - -Deprecated: Creation of dynamic property A::$f is deprecated in %s on line %d - Deprecated: Creation of dynamic property A::$f is deprecated in %s on line %d Warning: Undefined variable $x in %s on line %d @@ -35,16 +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 - -Deprecated: Creation of dynamic property A::$f is deprecated 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/zend_object_handlers.c b/Zend/zend_object_handlers.c index 276c56a7fb088..813f14891c993 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -1459,6 +1459,26 @@ ZEND_API zval *zend_std_get_property_ptr_ptr(zend_object *zobj, zend_string *nam return retval; } } + if (UNEXPECTED(zend_lazy_object_must_init(zobj))) { + bool guarded = zobj->ce->__get + && (*zend_get_property_guard(zobj, name) & IN_GET); + zend_object *instance = zend_lazy_object_init(zobj); + if (!instance) { + return &EG(error_zval); + } + + if (guarded && (instance->ce->ce_flags & ZEND_ACC_USE_GUARDS)) { + uint32_t *guard = zend_get_property_guard(instance, name); + if (!(*guard & IN_GET)) { + (*guard) |= IN_GET; + retval = zend_std_get_property_ptr_ptr(instance, name, type, cache_slot); + (*guard) &= ~IN_GET; + return retval; + } + } + + return zend_std_get_property_ptr_ptr(instance, name, type, cache_slot); + } if (EXPECTED(!zobj->ce->__get) || UNEXPECTED((*zend_get_property_guard(zobj, name)) & IN_GET)) { if (UNEXPECTED(zobj->ce->ce_flags & ZEND_ACC_NO_DYNAMIC_PROPERTIES)) { @@ -1470,25 +1490,6 @@ ZEND_API zval *zend_std_get_property_ptr_ptr(zend_object *zobj, zend_string *nam return &EG(error_zval); } } - if (UNEXPECTED(zend_lazy_object_must_init(zobj))) { - bool guarded = (zobj->ce->__get != NULL); - zend_object *instance = zend_lazy_object_init(zobj); - if (!instance) { - return &EG(error_zval); - } - - if (guarded && (instance->ce->ce_flags & ZEND_ACC_USE_GUARDS)) { - uint32_t *guard = zend_get_property_guard(instance, name); - if (!(*guard & IN_GET)) { - (*guard) |= IN_GET; - retval = zend_std_get_property_ptr_ptr(instance, name, type, cache_slot); - (*guard) &= ~IN_GET; - return retval; - } - } - - return zend_std_get_property_ptr_ptr(instance, name, type, cache_slot); - } if (UNEXPECTED(!zobj->properties)) { rebuild_object_properties_internal(zobj); } From a87b6980f12e044861342ed39d7a0ed6ec375829 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Wed, 18 Mar 2026 13:43:27 -0400 Subject: [PATCH 3/3] fix: keep lazy init inside __get guard, move above deprecation checks The previous commit hoisted the lazy init check above the __get guard conditional, which caused proxy initialization even when __get should handle the property access. Move the check back inside the guard but before the deprecation/error handling so dynamic property deprecation still only fires once (on the instance, not the proxy). Add regression test for proxy-with-__get not initializing on dynamic property access (from Ilija's review). --- Zend/tests/lazy_objects/gh20875.phpt | 8 ++++ .../gh20875_proxy_get_no_init.phpt | 23 ++++++++++++ Zend/zend_object_handlers.c | 37 +++++++++---------- 3 files changed, 49 insertions(+), 19 deletions(-) create mode 100644 Zend/tests/lazy_objects/gh20875_proxy_get_no_init.phpt diff --git a/Zend/tests/lazy_objects/gh20875.phpt b/Zend/tests/lazy_objects/gh20875.phpt index ff036edabd596..72e16011320c3 100644 --- a/Zend/tests/lazy_objects/gh20875.phpt +++ b/Zend/tests/lazy_objects/gh20875.phpt @@ -31,6 +31,14 @@ 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/gh20875_proxy_get_no_init.phpt b/Zend/tests/lazy_objects/gh20875_proxy_get_no_init.phpt new file mode 100644 index 0000000000000..d5d9754864b46 --- /dev/null +++ b/Zend/tests/lazy_objects/gh20875_proxy_get_no_init.phpt @@ -0,0 +1,23 @@ +--TEST-- +GH-20875 (Lazy proxy should not initialize when __get handles dynamic property access) +--FILE-- +newLazyProxy(function () { + echo "init\n"; + return new Foo(); +}); +$x = &$proxy->x; + +?> +--EXPECT-- +__get diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c index 813f14891c993..648e57dfe7e71 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -1459,28 +1459,27 @@ ZEND_API zval *zend_std_get_property_ptr_ptr(zend_object *zobj, zend_string *nam return retval; } } - if (UNEXPECTED(zend_lazy_object_must_init(zobj))) { - bool guarded = zobj->ce->__get - && (*zend_get_property_guard(zobj, name) & IN_GET); - zend_object *instance = zend_lazy_object_init(zobj); - if (!instance) { - return &EG(error_zval); - } + if (EXPECTED(!zobj->ce->__get) || + UNEXPECTED((*zend_get_property_guard(zobj, name)) & IN_GET)) { + if (UNEXPECTED(zend_lazy_object_must_init(zobj))) { + bool guarded = (zobj->ce->__get != NULL); + zend_object *instance = zend_lazy_object_init(zobj); + if (!instance) { + return &EG(error_zval); + } - if (guarded && (instance->ce->ce_flags & ZEND_ACC_USE_GUARDS)) { - uint32_t *guard = zend_get_property_guard(instance, name); - if (!(*guard & IN_GET)) { - (*guard) |= IN_GET; - retval = zend_std_get_property_ptr_ptr(instance, name, type, cache_slot); - (*guard) &= ~IN_GET; - return retval; + if (guarded && (instance->ce->ce_flags & ZEND_ACC_USE_GUARDS)) { + uint32_t *guard = zend_get_property_guard(instance, name); + if (!(*guard & IN_GET)) { + (*guard) |= IN_GET; + retval = zend_std_get_property_ptr_ptr(instance, name, type, cache_slot); + (*guard) &= ~IN_GET; + return retval; + } } - } - return zend_std_get_property_ptr_ptr(instance, name, type, cache_slot); - } - if (EXPECTED(!zobj->ce->__get) || - UNEXPECTED((*zend_get_property_guard(zobj, name)) & IN_GET)) { + return zend_std_get_property_ptr_ptr(instance, name, type, cache_slot); + } if (UNEXPECTED(zobj->ce->ce_flags & ZEND_ACC_NO_DYNAMIC_PROPERTIES)) { zend_forbidden_dynamic_property(zobj->ce, name); return &EG(error_zval);