From e3a79ed6079735743af7e5bb861d37c3acc124a5 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 31 Jul 2026 07:47:39 -0400 Subject: [PATCH] Free previous state when re-calling Reflection*::__construct() ReflectionMethod::__construct() overwrote intern->ptr without releasing the previous Closure::__invoke() trampoline, and ReflectionClassConstant::__construct() overwrote the $name and $class slots without dropping their previous references. ReflectionFunction, ReflectionParameter and ReflectionProperty already release prior state on re-entry. Closes GH-22968 --- ext/reflection/php_reflection.c | 11 +++++++ ...lectionClassConstant_double_construct.phpt | 33 +++++++++++++++++++ ...ectionMethod_double_construct_closure.phpt | 28 ++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 ext/reflection/tests/ReflectionClassConstant_double_construct.phpt create mode 100644 ext/reflection/tests/ReflectionMethod_double_construct_closure.phpt diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 0945c9574c62..d7352f4f40c1 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -3420,6 +3420,12 @@ static void instantiate_reflection_method(INTERNAL_FUNCTION_PARAMETERS, bool is_ } efree(lcname); + if (intern->ptr) { + _free_function(intern->ptr); + zval_ptr_dtor(reflection_prop_name(object)); + zval_ptr_dtor(reflection_prop_class(object)); + } + ZVAL_STR_COPY(reflection_prop_name(object), mptr->common.function_name); ZVAL_STR_COPY(reflection_prop_class(object), mptr->common.scope->name); intern->ptr = mptr; @@ -3932,6 +3938,11 @@ ZEND_METHOD(ReflectionClassConstant, __construct) RETURN_THROWS(); } + if (intern->ptr) { + zval_ptr_dtor(reflection_prop_name(object)); + zval_ptr_dtor(reflection_prop_class(object)); + } + intern->ptr = constant; intern->ref_type = REF_TYPE_CLASS_CONSTANT; intern->ce = constant->ce; diff --git a/ext/reflection/tests/ReflectionClassConstant_double_construct.phpt b/ext/reflection/tests/ReflectionClassConstant_double_construct.phpt new file mode 100644 index 000000000000..efd5472c4aff --- /dev/null +++ b/ext/reflection/tests/ReflectionClassConstant_double_construct.phpt @@ -0,0 +1,33 @@ +--TEST-- +ReflectionClassConstant double construct call does not leak $name and $class +--FILE-- +__construct(C::class, implode('', ['F', 'O', 'O'])); +} + +$r = new ReflectionClassConstant(C::class, 'FOO'); +for ($i = 0; $i < 10; $i++) { + test($r); +} + +$before = memory_get_usage(); +for ($i = 0; $i < 1000; $i++) { + test($r); +} +$after = memory_get_usage(); + +var_dump($before === $after); +var_dump($r->name, $r->class); + +?> +--EXPECT-- +bool(true) +string(3) "FOO" +string(1) "C" diff --git a/ext/reflection/tests/ReflectionMethod_double_construct_closure.phpt b/ext/reflection/tests/ReflectionMethod_double_construct_closure.phpt new file mode 100644 index 000000000000..3642974d9a6a --- /dev/null +++ b/ext/reflection/tests/ReflectionMethod_double_construct_closure.phpt @@ -0,0 +1,28 @@ +--TEST-- +ReflectionMethod double construct call on Closure::__invoke() does not leak +--FILE-- +__construct(function () {}, '__invoke'); +} + +$r = new ReflectionMethod(function () {}, '__invoke'); +for ($i = 0; $i < 10; $i++) { + test($r); +} + +$before = memory_get_usage(); +for ($i = 0; $i < 1000; $i++) { + test($r); +} +$after = memory_get_usage(); + +var_dump($before === $after); +var_dump($r->name, $r->class); + +?> +--EXPECT-- +bool(true) +string(8) "__invoke" +string(7) "Closure"