diff --git a/Zend/tests/lazy_objects/gh18038-002.phpt b/Zend/tests/lazy_objects/gh18038-002.phpt index 4c12f21de8115..d363731c62a09 100644 --- a/Zend/tests/lazy_objects/gh18038-002.phpt +++ b/Zend/tests/lazy_objects/gh18038-002.phpt @@ -34,5 +34,4 @@ var_dump($obj->prop); --EXPECT-- init string(19) "RealInstance::__set" -string(12) "Proxy::__set" int(2) 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/gh18038-009.phpt b/Zend/tests/lazy_objects/gh18038-009.phpt index 3c165a71ccffe..11067cdb970bd 100644 --- a/Zend/tests/lazy_objects/gh18038-009.phpt +++ b/Zend/tests/lazy_objects/gh18038-009.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-isset.phpt b/Zend/tests/lazy_objects/gh21478-isset.phpt new file mode 100644 index 0000000000000..9138984af01bf --- /dev/null +++ b/Zend/tests/lazy_objects/gh21478-isset.phpt @@ -0,0 +1,30 @@ +--TEST-- +GH-21478: __isset on lazy proxy should not double-invoke when real instance guard is set +--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); +isset($real->x); + +?> +--EXPECT-- +Init +__isset($x) on Foo 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-proxy-get-ref-forward.phpt b/Zend/tests/lazy_objects/gh21478-proxy-get-ref-forward.phpt new file mode 100644 index 0000000000000..fa737cf18f2e2 --- /dev/null +++ b/Zend/tests/lazy_objects/gh21478-proxy-get-ref-forward.phpt @@ -0,0 +1,32 @@ +--TEST-- +GH-21478: No assertion failure when &__get forwards through initialized lazy proxy +--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); +$a = &$real->x; +var_dump($a); +?> +--EXPECTF-- +Init +Foo::__get($x) on Foo + +Warning: Undefined property: Foo::$x in %s on line %d +NULL diff --git a/Zend/tests/lazy_objects/gh21478-set.phpt b/Zend/tests/lazy_objects/gh21478-set.phpt new file mode 100644 index 0000000000000..0b2f872de11a6 --- /dev/null +++ b/Zend/tests/lazy_objects/gh21478-set.phpt @@ -0,0 +1,32 @@ +--TEST-- +GH-21478: __set on lazy proxy should not double-invoke when real instance guard is set +--FILE-- +{$name} = $value; + } +} + +#[AllowDynamicProperties] +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 = 1; + +?> +--EXPECT-- +Init +__set($x) on Foo diff --git a/Zend/tests/lazy_objects/gh21478-unset.phpt b/Zend/tests/lazy_objects/gh21478-unset.phpt new file mode 100644 index 0000000000000..5febbd235d824 --- /dev/null +++ b/Zend/tests/lazy_objects/gh21478-unset.phpt @@ -0,0 +1,30 @@ +--TEST-- +GH-21478: __unset on lazy proxy should not double-invoke when real instance guard is set +--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); +unset($real->x); + +?> +--EXPECT-- +Init +__unset($x) on Foo 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_atomic.c b/Zend/zend_atomic.c index 5f6a7793beeab..4151e5e539de2 100644 --- a/Zend/zend_atomic.c +++ b/Zend/zend_atomic.c @@ -57,7 +57,7 @@ ZEND_API void zend_atomic_int_store(zend_atomic_int *obj, int desired) { zend_atomic_int_store_ex(obj, desired); } -#if defined(ZEND_WIN32) || defined(HAVE_SYNC_ATOMICS) +#if (defined(ZEND_WIN32) || defined(HAVE_SYNC_ATOMICS)) && !defined(HAVE_C11_ATOMICS) /* On these platforms it is non-const due to underlying APIs. */ ZEND_API bool zend_atomic_bool_load(zend_atomic_bool *obj) { return zend_atomic_bool_load_ex(obj); diff --git a/Zend/zend_atomic.h b/Zend/zend_atomic.h index 56422a721fdba..bcbc6681c9289 100644 --- a/Zend/zend_atomic.h +++ b/Zend/zend_atomic.h @@ -39,7 +39,7 @@ * and alignment purposes. */ -#if defined(ZEND_WIN32) || defined(HAVE_SYNC_ATOMICS) +#if (defined(ZEND_WIN32) || defined(HAVE_SYNC_ATOMICS)) && !defined(HAVE_C11_ATOMICS) typedef struct zend_atomic_bool_s { volatile char value; } zend_atomic_bool; @@ -68,7 +68,7 @@ typedef struct zend_atomic_int_s { BEGIN_EXTERN_C() -#ifdef ZEND_WIN32 +#if defined(ZEND_WIN32) && !defined(HAVE_C11_ATOMICS) #ifndef InterlockedExchange8 #define InterlockedExchange8 _InterlockedExchange8 @@ -123,7 +123,7 @@ static zend_always_inline bool zend_atomic_int_compare_exchange_ex(zend_atomic_i } } -/* On this platform it is non-const due to Iterlocked API*/ +/* On this platform it is non-const due to Interlocked API */ static zend_always_inline bool zend_atomic_bool_load_ex(zend_atomic_bool *obj) { /* Or'ing with false won't change the value. */ return InterlockedOr8(&obj->value, false); @@ -376,7 +376,7 @@ ZEND_API bool zend_atomic_int_compare_exchange(zend_atomic_int *obj, int *expect ZEND_API void zend_atomic_bool_store(zend_atomic_bool *obj, bool desired); ZEND_API void zend_atomic_int_store(zend_atomic_int *obj, int desired); -#if defined(ZEND_WIN32) || defined(HAVE_SYNC_ATOMICS) +#if (defined(ZEND_WIN32) && !defined(HAVE_C11_ATOMICS)) || defined(HAVE_SYNC_ATOMICS) /* On these platforms it is non-const due to underlying APIs. */ ZEND_API bool zend_atomic_bool_load(zend_atomic_bool *obj); ZEND_API int zend_atomic_int_load(zend_atomic_int *obj); diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c index 7e03139dc426b..90f0e1099619b 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -893,6 +893,28 @@ 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) { + retval = zend_std_read_property(instance, name, type, cache_slot, rv); + if (retval == &EG(uninitialized_zval)) { + ZVAL_NULL(rv); + retval = rv; + } + return retval; + } + } + } + /* magic isset */ if ((type == BP_VAR_IS) && zobj->ce->__isset) { zval tmp_result; @@ -1209,6 +1231,20 @@ found:; goto exit; } + /* For initialized lazy proxies: if the real instance's __set guard + * is already set, we are inside a recursive call from the real + * instance's __set. Forward directly to avoid double invocation. */ + 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); + if ((*instance_guard) & IN_SET) { + return zend_std_write_property(instance, name, value, cache_slot); + } + } + } + /* magic set */ if (zobj->ce->__set) { if (!guard) { @@ -1603,6 +1639,21 @@ ZEND_API void zend_std_unset_property(zend_object *zobj, zend_string *name, void return; } + /* For initialized lazy proxies: if the real instance's __unset guard + * is already set, we are inside a recursive call from the real + * instance's __unset. Forward directly to avoid double invocation. */ + 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); + if ((*instance_guard) & IN_UNSET) { + zend_std_unset_property(instance, name, cache_slot); + return; + } + } + } + /* magic unset */ if (zobj->ce->__unset) { if (!guard) { @@ -2399,6 +2450,20 @@ ZEND_API int zend_std_has_property(zend_object *zobj, zend_string *name, int has goto exit; } + /* For initialized lazy proxies: if the real instance's __isset guard + * is already set, we are inside a recursive call from the real + * instance's __isset. Forward directly to avoid double invocation. */ + 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); + if ((*instance_guard) & IN_ISSET) { + return zend_std_has_property(instance, name, has_set_exists, cache_slot); + } + } + } + if (!zobj->ce->__isset) { goto lazy_init; }