From 17e783efe3205a70f5f97c0556e2cacf5fc80767 Mon Sep 17 00:00:00 2001 From: lazerg Date: Tue, 6 Oct 2026 05:53:52 +0500 Subject: [PATCH 1/2] Fix GH-24147: Reflection name/class can be modified by compound assignment --- NEWS | 4 ++++ UPGRADING | 5 ++++ ext/reflection/php_reflection.c | 21 +++++++++++++++-- ext/reflection/tests/gh24147.phpt | 39 +++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 ext/reflection/tests/gh24147.phpt diff --git a/NEWS b/NEWS index e027d6c139ee..a6557f8a25ec 100644 --- a/NEWS +++ b/NEWS @@ -81,6 +81,10 @@ PHP NEWS . Fixed a heap over-read in the interactive shell prompt when cli.prompt is set to an empty string. (Ilia Alshanetsky) +- Reflection: + . Fixed bug GH-24147 (ReflectionProperty class and name properties can be + modified through compound assignment or by reference). (lazerg) + - Session: . Fixed session_start() continuing after a failed create_sid() when session.use_strict_mode rejects the supplied ID. (Ilia Alshanetsky) diff --git a/UPGRADING b/UPGRADING index 46cf4cafab12..c08a175fdbb3 100644 --- a/UPGRADING +++ b/UPGRADING @@ -27,6 +27,11 @@ PHP 8.7 UPGRADE NOTES not present in the result set. It previously reported the condition through PDO::ATTR_ERRMODE and returned false. +- Reflection: + . Modifying the $name or $class property of a Reflection object through + compound assignment, increment/decrement or by reference now throws a + ReflectionException, like a plain assignment already did. + - Standard: . The number of filters that can be chained in a php://filter URL is limited to 16 by default. Set the stream context option max_filter_count to change diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 0c1a6ba721ce..cab6d8d35561 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -7936,11 +7936,16 @@ ZEND_METHOD(ReflectionFiber, getCallable) RETURN_COPY(&fiber->fci.function_name); } +static bool reflection_is_read_only_property(zend_object *object, zend_string *name) +{ + return zend_hash_exists(&object->ce->properties_info, name) + && (zend_string_equals(name, ZSTR_KNOWN(ZEND_STR_NAME)) || zend_string_equals(name, ZSTR_KNOWN(ZEND_STR_CLASS))); +} + /* {{{ _reflection_write_property */ static zval *_reflection_write_property(zend_object *object, zend_string *name, zval *value, void **cache_slot) { - if (zend_hash_exists(&object->ce->properties_info, name) - && (zend_string_equals(name, ZSTR_KNOWN(ZEND_STR_NAME)) || zend_string_equals(name, ZSTR_KNOWN(ZEND_STR_CLASS)))) + if (reflection_is_read_only_property(object, name)) { zend_throw_exception_ex(reflection_exception_ptr, 0, "Cannot set read-only property %pS::$%pS", object->ce->name, name); @@ -7951,6 +7956,17 @@ static zval *_reflection_write_property(zend_object *object, zend_string *name, } /* }}} */ +static zval *_reflection_get_property_ptr_ptr(zend_object *object, zend_string *name, int type, void **cache_slot) +{ + if (reflection_is_read_only_property(object, name)) { + zend_throw_exception_ex(reflection_exception_ptr, 0, + "Cannot set read-only property %s::$%s", ZSTR_VAL(object->ce->name), ZSTR_VAL(name)); + return &EG(error_zval); + } + + return zend_std_get_property_ptr_ptr(object, name, type, cache_slot); +} + ZEND_METHOD(ReflectionConstant, __construct) { zend_string *name; @@ -8172,6 +8188,7 @@ PHP_MINIT_FUNCTION(reflection) /* {{{ */ reflection_object_handlers.free_obj = reflection_free_objects_storage; reflection_object_handlers.clone_obj = NULL; reflection_object_handlers.write_property = _reflection_write_property; + reflection_object_handlers.get_property_ptr_ptr = _reflection_get_property_ptr_ptr; reflection_object_handlers.get_gc = reflection_get_gc; reflection_exception_ptr = register_class_ReflectionException(zend_ce_exception); diff --git a/ext/reflection/tests/gh24147.phpt b/ext/reflection/tests/gh24147.phpt new file mode 100644 index 000000000000..d2e32191e2dc --- /dev/null +++ b/ext/reflection/tests/gh24147.phpt @@ -0,0 +1,39 @@ +--TEST-- +GH-24147 (ReflectionProperty class and name properties can be changed) +--FILE-- +class .= 'Z'; +} catch (ReflectionException $e) { + echo $e->getMessage(), "\n"; +} + +try { + $prop->name++; +} catch (ReflectionException $e) { + echo $e->getMessage(), "\n"; +} + +try { + $ref = &$prop->name; + $ref = 'y'; +} catch (ReflectionException $e) { + echo $e->getMessage(), "\n"; +} + +var_dump($prop->class, $prop->name); + +?> +--EXPECT-- +Cannot set read-only property ReflectionProperty::$class +Cannot set read-only property ReflectionProperty::$name +Cannot set read-only property ReflectionProperty::$name +string(1) "B" +string(1) "x" From 978e2b1e0981b2deab287af67c473c305b363003 Mon Sep 17 00:00:00 2001 From: lazerg Date: Tue, 6 Oct 2026 05:59:32 +0500 Subject: [PATCH 2/2] Prevent by-ref func arg fetch from using a cached name/class offset --- ext/reflection/php_reflection.c | 12 ++++++++++++ ext/reflection/tests/gh24147.phpt | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index cab6d8d35561..e5157c575349 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -7956,6 +7956,17 @@ static zval *_reflection_write_property(zend_object *object, zend_string *name, } /* }}} */ +static zval *_reflection_read_property(zend_object *object, zend_string *name, int type, void **cache_slot, zval *rv) +{ + /* Don't cache the offset of read-only properties: a by-reference fetch + * sharing the cache slot (FETCH_OBJ_FUNC_ARG) would skip get_property_ptr_ptr. */ + if (reflection_is_read_only_property(object, name)) { + cache_slot = NULL; + } + + return zend_std_read_property(object, name, type, cache_slot, rv); +} + static zval *_reflection_get_property_ptr_ptr(zend_object *object, zend_string *name, int type, void **cache_slot) { if (reflection_is_read_only_property(object, name)) { @@ -8187,6 +8198,7 @@ PHP_MINIT_FUNCTION(reflection) /* {{{ */ reflection_object_handlers.offset = offsetof(reflection_object, zo); reflection_object_handlers.free_obj = reflection_free_objects_storage; reflection_object_handlers.clone_obj = NULL; + reflection_object_handlers.read_property = _reflection_read_property; reflection_object_handlers.write_property = _reflection_write_property; reflection_object_handlers.get_property_ptr_ptr = _reflection_get_property_ptr_ptr; reflection_object_handlers.get_gc = reflection_get_gc; diff --git a/ext/reflection/tests/gh24147.phpt b/ext/reflection/tests/gh24147.phpt index d2e32191e2dc..c69b9c904189 100644 --- a/ext/reflection/tests/gh24147.phpt +++ b/ext/reflection/tests/gh24147.phpt @@ -7,6 +7,9 @@ class B { public string $x; } +function byVal($x) {} +function byRef(&$x) { $x .= 'Z'; } + $prop = new ReflectionProperty('B', 'x'); try { @@ -28,6 +31,14 @@ try { echo $e->getMessage(), "\n"; } +foreach (['byVal', 'byRef'] as $fn) { + try { + $fn($prop->class); + } catch (ReflectionException $e) { + echo $e->getMessage(), "\n"; + } +} + var_dump($prop->class, $prop->name); ?> @@ -35,5 +46,6 @@ var_dump($prop->class, $prop->name); Cannot set read-only property ReflectionProperty::$class Cannot set read-only property ReflectionProperty::$name Cannot set read-only property ReflectionProperty::$name +Cannot set read-only property ReflectionProperty::$class string(1) "B" string(1) "x"