From 880312bfe15340a5e822f8f263957f9d4087ee01 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Thu, 16 Apr 2026 06:08:16 +0100 Subject: [PATCH 1/3] Fixes GH-21768: ext/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. Assertion introduced in e4f727d61eb. --- ext/reflection/php_reflection.c | 6 ++---- ext/reflection/tests/gh21768.phpt | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 ext/reflection/tests/gh21768.phpt diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index e74b7fcb27a78..860b4049a2fb4 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -6763,8 +6763,7 @@ ZEND_METHOD(ReflectionProperty, isReadable) } if (prop->flags & ZEND_ACC_VIRTUAL) { - ZEND_ASSERT(prop->hooks); - if (!prop->hooks[ZEND_PROPERTY_HOOK_GET]) { + if (prop->hooks && !prop->hooks[ZEND_PROPERTY_HOOK_GET]) { RETURN_FALSE; } } else if (obj && (!prop->hooks || !prop->hooks[ZEND_PROPERTY_HOOK_GET])) { @@ -6855,8 +6854,7 @@ ZEND_METHOD(ReflectionProperty, isWritable) } if (prop->flags & ZEND_ACC_VIRTUAL) { - ZEND_ASSERT(prop->hooks); - if (!prop->hooks[ZEND_PROPERTY_HOOK_SET]) { + if (prop->hooks && !prop->hooks[ZEND_PROPERTY_HOOK_SET]) { RETURN_FALSE; } } else if (obj && (prop->flags & ZEND_ACC_READONLY)) { diff --git a/ext/reflection/tests/gh21768.phpt b/ext/reflection/tests/gh21768.phpt new file mode 100644 index 0000000000000..422239120dbe4 --- /dev/null +++ b/ext/reflection/tests/gh21768.phpt @@ -0,0 +1,21 @@ +--TEST-- +GH-21768: Assertion failure in ReflectionProperty::is{Readable,Writable}() on internal virtual properties +--EXTENSIONS-- +dom +--FILE-- +getProperties() as $rp) { + if (!$rp->isVirtual()) + continue; + if (!$rp->isReadable(null)) + die("$rp should be readable"); + if (!$rp->isWritable(null)) + die("$rp should be writable"); +} +echo "done\n"; + +?> +--EXPECT-- +done From 0a5140729f8afee3d1efa97d62fcb72d1669dbf6 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Fri, 17 Apr 2026 19:56:19 +0100 Subject: [PATCH 2/3] ext/reflection: handle virtual+readonly in ReflectionProperty::isWritable(). Follow-up to review feedback from @iluuu1994 on GH-21769. Gate the ZEND_ACC_VIRTUAL branch on prop->hooks being set, and short-circuit virtual properties that also carry ZEND_ACC_READONLY to RETURN_FALSE (no backing slot, treat as always initialized). Covers legitimate virtual+readonly uses outside of DOM. --- ext/reflection/php_reflection.c | 38 +++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 860b4049a2fb4..e560077502eff 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -6762,8 +6762,8 @@ ZEND_METHOD(ReflectionProperty, isReadable) RETURN_FALSE; } - if (prop->flags & ZEND_ACC_VIRTUAL) { - if (prop->hooks && !prop->hooks[ZEND_PROPERTY_HOOK_GET]) { + if ((prop->flags & ZEND_ACC_VIRTUAL) && prop->hooks) { + if (!prop->hooks[ZEND_PROPERTY_HOOK_GET]) { RETURN_FALSE; } } else if (obj && (!prop->hooks || !prop->hooks[ZEND_PROPERTY_HOOK_GET])) { @@ -6853,24 +6853,30 @@ ZEND_METHOD(ReflectionProperty, isWritable) RETURN_FALSE; } - if (prop->flags & ZEND_ACC_VIRTUAL) { - if (prop->hooks && !prop->hooks[ZEND_PROPERTY_HOOK_SET]) { + if ((prop->flags & ZEND_ACC_VIRTUAL) && prop->hooks) { + if (!prop->hooks[ZEND_PROPERTY_HOOK_SET]) { + RETURN_FALSE; + } + } else if (prop->flags & ZEND_ACC_VIRTUAL) { + if (prop->flags & ZEND_ACC_READONLY) { RETURN_FALSE; } - } else if (obj && (prop->flags & ZEND_ACC_READONLY)) { + } 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; } } From 4d624cdf5c62b945ff0dc31342cb2a76f8b1ca3e Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sat, 18 Apr 2026 17:37:21 +0100 Subject: [PATCH 3/3] update test --- ext/reflection/tests/gh21768.phpt | 86 +++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 4 deletions(-) diff --git a/ext/reflection/tests/gh21768.phpt b/ext/reflection/tests/gh21768.phpt index 422239120dbe4..69c50c5a4482d 100644 --- a/ext/reflection/tests/gh21768.phpt +++ b/ext/reflection/tests/gh21768.phpt @@ -9,13 +9,91 @@ $rc = new ReflectionClass('DOMDocument'); foreach ($rc->getProperties() as $rp) { if (!$rp->isVirtual()) continue; - if (!$rp->isReadable(null)) - die("$rp should be readable"); - if (!$rp->isWritable(null)) - die("$rp should be writable"); + var_dump($rp->isReadable(null)); + var_dump($rp->isWritable(null)); } echo "done\n"; ?> --EXPECT-- +bool(true) +bool(false) +bool(true) +bool(true) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(true) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(true) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(true) +bool(true) +bool(false) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) done