Commit f752eddff8a for php
commit f752eddff8a46c2281bc8bae70ce3a83a5b38b4c
Author: David Carlier <devnexen@gmail.com>
Date: Fri Oct 30 12:07:42 2026 +0000
Fix GH-21768: Reflection assertion failure on internal virtual properties
ReflectionProperty::isReadable() and isWritable() asserted prop->hooks
being set whenever ZEND_ACC_VIRTUAL is set. Internal classes such as
DOMDocument declare virtual properties backed by the object read/write
handlers without hooks, tripping the assertion. Virtual properties that
also carry ZEND_ACC_READONLY are reported as not writable.
Assertion introduced in e4f727d61eb.
Close GH-21769
diff --git a/NEWS b/NEWS
index 95f3867aee8..043842f2138 100644
--- a/NEWS
+++ b/NEWS
@@ -129,6 +129,10 @@ PHP NEWS
. Fixed bug GH-24740 (Oversized tar metadata exhausts the memory limit).
(Lazizbek Ergashev)
+- Reflection:
+ . Fixed bug GH-21768 (Assertion failure in ReflectionProperty::isReadable()
+ and isWritable() on internal virtual properties). (David Carlier)
+
- SAPI:
. Fixed infinite loop in sapi_header_op() for headers of 4 GiB or more.
(Martin Mirchev)
diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c
index 7552b00c115..5bdd5e18bf2 100644
--- a/ext/reflection/php_reflection.c
+++ b/ext/reflection/php_reflection.c
@@ -6742,8 +6742,7 @@ ZEND_METHOD(ReflectionProperty, isReadable)
RETURN_FALSE;
}
- if (prop->flags & ZEND_ACC_VIRTUAL) {
- ZEND_ASSERT(prop->hooks);
+ if ((prop->flags & ZEND_ACC_VIRTUAL) && prop->hooks) {
if (!prop->hooks[ZEND_PROPERTY_HOOK_GET]) {
RETURN_FALSE;
}
@@ -6835,26 +6834,31 @@ ZEND_METHOD(ReflectionProperty, isWritable)
RETURN_FALSE;
}
- if (prop->flags & ZEND_ACC_VIRTUAL) {
- ZEND_ASSERT(prop->hooks);
+ if ((prop->flags & ZEND_ACC_VIRTUAL) && prop->hooks) {
if (!prop->hooks[ZEND_PROPERTY_HOOK_SET]) {
RETURN_FALSE;
}
- } else if (obj && (prop->flags & ZEND_ACC_READONLY)) {
+ } else if (prop->flags & ZEND_ACC_VIRTUAL) {
+ if (prop->flags & ZEND_ACC_READONLY) {
+ RETURN_FALSE;
+ }
+ } else if ((prop->flags & ZEND_ACC_READONLY)) {
+ if (obj) {
retry:;
- zval *prop_val = OBJ_PROP(obj, prop->offset);
- if (Z_TYPE_P(prop_val) == IS_UNDEF
- && zend_lazy_object_must_init(obj)
- && (Z_PROP_FLAG_P(prop_val) & IS_PROP_LAZY)
- ) {
- obj = zend_lazy_object_init(obj);
- if (!obj) {
- RETURN_THROWS();
+ zval *prop_val = OBJ_PROP(obj, prop->offset);
+ if (Z_TYPE_P(prop_val) == IS_UNDEF
+ && zend_lazy_object_must_init(obj)
+ && (Z_PROP_FLAG_P(prop_val) & IS_PROP_LAZY)
+ ) {
+ obj = zend_lazy_object_init(obj);
+ if (!obj) {
+ RETURN_THROWS();
+ }
+ goto retry;
+ }
+ if (Z_TYPE_P(prop_val) != IS_UNDEF && !(Z_PROP_FLAG_P(prop_val) & IS_PROP_REINITABLE)) {
+ RETURN_FALSE;
}
- goto retry;
- }
- if (Z_TYPE_P(prop_val) != IS_UNDEF && !(Z_PROP_FLAG_P(prop_val) & IS_PROP_REINITABLE)) {
- RETURN_FALSE;
}
}
diff --git a/ext/reflection/tests/gh21768.phpt b/ext/reflection/tests/gh21768.phpt
new file mode 100644
index 00000000000..eabc7f89f12
--- /dev/null
+++ b/ext/reflection/tests/gh21768.phpt
@@ -0,0 +1,60 @@
+--TEST--
+GH-21768: Assertion failure in ReflectionProperty::is{Readable,Writable}() on internal virtual properties
+--EXTENSIONS--
+dom
+--FILE--
+<?php
+
+$rc = new ReflectionClass('DOMDocument');
+foreach ($rc->getProperties() as $rp) {
+ if (!$rp->isVirtual()) {
+ continue;
+ }
+ echo $rp->getName(), ': readable=', var_export($rp->isReadable(null), true),
+ ', writable=', var_export($rp->isWritable(null), true), PHP_EOL;
+}
+echo "done", PHP_EOL;
+
+?>
+--EXPECT--
+nodeName: readable=true, writable=false
+nodeValue: readable=true, writable=true
+nodeType: readable=true, writable=false
+parentNode: readable=true, writable=false
+parentElement: readable=true, writable=false
+childNodes: readable=true, writable=false
+firstChild: readable=true, writable=false
+lastChild: readable=true, writable=false
+previousSibling: readable=true, writable=false
+nextSibling: readable=true, writable=false
+attributes: readable=true, writable=false
+isConnected: readable=true, writable=false
+ownerDocument: readable=true, writable=false
+namespaceURI: readable=true, writable=false
+prefix: readable=true, writable=true
+localName: readable=true, writable=false
+baseURI: readable=true, writable=false
+textContent: readable=true, writable=true
+doctype: readable=true, writable=false
+implementation: readable=true, writable=false
+documentElement: readable=true, writable=false
+actualEncoding: readable=true, writable=false
+encoding: readable=true, writable=true
+xmlEncoding: readable=true, writable=false
+standalone: readable=true, writable=true
+xmlStandalone: readable=true, writable=true
+version: readable=true, writable=true
+xmlVersion: readable=true, writable=true
+strictErrorChecking: readable=true, writable=true
+documentURI: readable=true, writable=true
+config: readable=true, writable=false
+formatOutput: readable=true, writable=true
+validateOnParse: readable=true, writable=true
+resolveExternals: readable=true, writable=true
+preserveWhiteSpace: readable=true, writable=true
+recover: readable=true, writable=true
+substituteEntities: readable=true, writable=true
+firstElementChild: readable=true, writable=false
+lastElementChild: readable=true, writable=false
+childElementCount: readable=true, writable=false
+done