Commit de268272756 for php.net

commit de2682727564324186e5a61f20d8d04dbc252be8
Author: Arnaud Le Blanc <arnaud.lb@gmail.com>
Date:   Fri Jan 30 15:56:34 2026 +0100

    Real instance of lazy proxy may have less magic methods

    In GH-18039 we guard the underlying property before forwarding access
    to the real instance of a lazy proxy. When the real instance lacks magic
    methods, the assertion zobj->ce->ce_flags & ZEND_ACC_USE_GUARDS fails in
    zend_get_property_guard().

    Fix by checking that the real instance uses guards.

    Fixes GH-20504
    Closes GH-21093

diff --git a/NEWS b/NEWS
index 343d71202de..a2fec5115dd 100644
--- a/NEWS
+++ b/NEWS
@@ -6,6 +6,8 @@ PHP                                                                        NEWS
   . Fixed bug GH-21029 (zend_mm_heap corrupted on Aarch64, LTO builds). (Arnaud)
   . Fixed bug GH-20657 (Assertion failure in zend_lazy_object_get_info triggered
     by setRawValueWithoutLazyInitialization() and newLazyGhost()). (Arnaud)
+  . Fixed bug GH-20504 (Assertion failure in zend_get_property_guard when
+    accessing properties on Reflection LazyProxy via isset()). (Arnaud)

 - Curl:
   . Fixed bug GH-21023 (CURLOPT_XFERINFOFUNCTION crash with a null callback).
diff --git a/Zend/tests/lazy_objects/gh20504-001.phpt b/Zend/tests/lazy_objects/gh20504-001.phpt
new file mode 100644
index 00000000000..c092e0f337f
--- /dev/null
+++ b/Zend/tests/lazy_objects/gh20504-001.phpt
@@ -0,0 +1,24 @@
+--TEST--
+GH-20504: Assertion failure in zend_get_property_guard() when lazy proxy adds magic method - isset
+--CREDITS--
+vi3tL0u1s
+--FILE--
+<?php
+
+class RealInstance {
+    public $_;
+}
+class Proxy extends RealInstance {
+    public function __isset($name) {
+        return isset($this->$name['']);
+    }
+}
+$rc = new ReflectionClass(Proxy::class);
+$obj = $rc->newLazyProxy(function () {
+    return new RealInstance;
+});
+var_dump(isset($obj->name['']));
+
+?>
+--EXPECT--
+bool(false)
diff --git a/Zend/tests/lazy_objects/gh20504-002.phpt b/Zend/tests/lazy_objects/gh20504-002.phpt
new file mode 100644
index 00000000000..c9cb7e743af
--- /dev/null
+++ b/Zend/tests/lazy_objects/gh20504-002.phpt
@@ -0,0 +1,23 @@
+--TEST--
+GH-20504: Assertion failure in zend_get_property_guard() when lazy proxy adds magic method - get
+--FILE--
+<?php
+
+class RealInstance {
+    public $_;
+}
+class Proxy extends RealInstance {
+    public function __get($name) {
+        return $this->$name;
+    }
+}
+$rc = new ReflectionClass(Proxy::class);
+$obj = $rc->newLazyProxy(function () {
+    return new RealInstance;
+});
+var_dump($obj->name);
+
+?>
+--EXPECTF--
+Warning: Undefined property: RealInstance::$name in %s on line %d
+NULL
diff --git a/Zend/tests/lazy_objects/gh20504-003.phpt b/Zend/tests/lazy_objects/gh20504-003.phpt
new file mode 100644
index 00000000000..df66e43a875
--- /dev/null
+++ b/Zend/tests/lazy_objects/gh20504-003.phpt
@@ -0,0 +1,33 @@
+--TEST--
+GH-20504: Assertion failure in zend_get_property_guard() when lazy proxy adds magic method - set
+--FILE--
+<?php
+
+#[AllowDynamicProperties]
+class RealInstance {
+    public $_;
+}
+class Proxy extends RealInstance {
+    public function __set($name, $value) {
+        $this->$name = $value;
+    }
+}
+$rc = new ReflectionClass(Proxy::class);
+$obj = $rc->newLazyProxy(function () {
+    return new RealInstance;
+});
+$obj->name = 0;
+
+var_dump($obj);
+
+?>
+--EXPECTF--
+lazy proxy object(Proxy)#%d (1) {
+  ["instance"]=>
+  object(RealInstance)#%d (2) {
+    ["_"]=>
+    NULL
+    ["name"]=>
+    int(0)
+  }
+}
diff --git a/Zend/tests/lazy_objects/gh20504-004.phpt b/Zend/tests/lazy_objects/gh20504-004.phpt
new file mode 100644
index 00000000000..a80964a9ae9
--- /dev/null
+++ b/Zend/tests/lazy_objects/gh20504-004.phpt
@@ -0,0 +1,28 @@
+--TEST--
+GH-20504: Assertion failure in zend_get_property_guard() when lazy proxy adds magic method - proxy defines __isset(), both have guards
+--FILE--
+<?php
+
+class RealInstance {
+    public $_;
+    public function __get($name) {
+        printf("%s::%s\n", static::class, __FUNCTION__);
+    }
+}
+class Proxy extends RealInstance {
+    public function __isset($name) {
+        printf("%s::%s\n", static::class, __FUNCTION__);
+        return isset($this->$name['']);
+    }
+}
+$rc = new ReflectionClass(Proxy::class);
+$obj = $rc->newLazyProxy(function () {
+    return new RealInstance;
+});
+var_dump(isset($obj->name['']));
+
+?>
+--EXPECT--
+Proxy::__isset
+Proxy::__get
+bool(false)
diff --git a/Zend/tests/lazy_objects/gh20504-005.phpt b/Zend/tests/lazy_objects/gh20504-005.phpt
new file mode 100644
index 00000000000..8a2519bde11
--- /dev/null
+++ b/Zend/tests/lazy_objects/gh20504-005.phpt
@@ -0,0 +1,30 @@
+--TEST--
+GH-20504: Assertion failure in zend_get_property_guard() when lazy proxy adds magic method - unset
+--FILE--
+<?php
+
+class RealInstance {
+    public $_;
+}
+class Proxy extends RealInstance {
+    public function __unset($name) {
+        unset($this->$name);
+    }
+}
+$rc = new ReflectionClass(Proxy::class);
+$obj = $rc->newLazyProxy(function () {
+    return new RealInstance;
+});
+unset($obj->name);
+
+var_dump($obj);
+
+?>
+--EXPECTF--
+lazy proxy object(Proxy)#%d (1) {
+  ["instance"]=>
+  object(RealInstance)#%d (1) {
+    ["_"]=>
+    NULL
+  }
+}
diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c
index ccedb79acc0..ef5fb29751b 100644
--- a/Zend/zend_object_handlers.c
+++ b/Zend/zend_object_handlers.c
@@ -956,25 +956,27 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int
 uninit_error:
 	if (UNEXPECTED(zend_lazy_object_must_init(zobj))) {
 		if (!prop_info || (Z_PROP_FLAG_P(retval) & IS_PROP_LAZY)) {
-			zobj = zend_lazy_object_init(zobj);
-			if (!zobj) {
+			zend_object *instance = zend_lazy_object_init(zobj);
+			if (!instance) {
 				retval = &EG(uninitialized_zval);
 				goto exit;
 			}

-			if (UNEXPECTED(guard)) {
+			if (UNEXPECTED(guard && (instance->ce->ce_flags & ZEND_ACC_USE_GUARDS))) {
+				/* Find which guard was used on zobj, so we can set the same
+				 * guard on instance. */
 				uint32_t guard_type = (type == BP_VAR_IS) && zobj->ce->__isset
 					? IN_ISSET : IN_GET;
-				guard = zend_get_property_guard(zobj, name);
+				guard = zend_get_property_guard(instance, name);
 				if (!((*guard) & guard_type)) {
 					(*guard) |= guard_type;
-					retval = zend_std_read_property(zobj, name, type, cache_slot, rv);
+					retval = zend_std_read_property(instance, name, type, cache_slot, rv);
 					(*guard) &= ~guard_type;
 					return retval;
 				}
 			}

-			return zend_std_read_property(zobj, name, type, cache_slot, rv);
+			return zend_std_read_property(instance, name, type, cache_slot, rv);
 		}
 	}
 	if (type != BP_VAR_IS) {
@@ -1013,7 +1015,7 @@ static zval *forward_write_to_lazy_object(zend_object *zobj,
 		return &EG(error_zval);
 	}

-	if (UNEXPECTED(guarded)) {
+	if (UNEXPECTED(guarded && (instance->ce->ce_flags & ZEND_ACC_USE_GUARDS))) {
 		uint32_t *guard = zend_get_property_guard(instance, name);
 		if (!((*guard) & IN_SET)) {
 			(*guard) |= IN_SET;
@@ -1597,7 +1599,7 @@ ZEND_API void zend_std_unset_property(zend_object *zobj, zend_string *name, void
 			return;
 		}

-		if (UNEXPECTED(guard)) {
+		if (UNEXPECTED(guard && zobj->ce->ce_flags & ZEND_ACC_USE_GUARDS)) {
 			guard = zend_get_property_guard(zobj, name);
 			if (!((*guard) & IN_UNSET)) {
 				(*guard) |= IN_UNSET;