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