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;