[PHP-CVS] [php-src] PHP-8.5: Free previous state when re-calling Reflection*::__construct()

[email protected] (Ilia Alshanetsky) Fri, 31 Jul 2026 15:53:21 +0000
Newsgroups php.cvs
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Date: 2026-07-31T11:39:17-04:00

Commit: https://github.com/php/php-src/commit/87f8d1742664c677fa12bd7e146fafe431885cdf
Raw diff: https://github.com/php/php-src/commit/87f8d1742664c677fa12bd7e146fafe431885cdf.diff

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

Changed paths:
  A  ext/reflection/tests/ReflectionClassConstant_double_construct.phpt
  A  ext/reflection/tests/ReflectionMethod_double_construct_closure.phpt
  M  ext/reflection/php_reflection.c


Diff:

diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c
index 0945c9574c62..65462bb8652f 100644
--- a/ext/reflection/php_reflection.c
+++ b/ext/reflection/php_reflection.c
@@ -3420,6 +3420,13 @@ static void instantiate_reflection_method(INTERNAL_FUNCTION_PARAMETERS, bool is_
 	}
 	efree(lcname);
 
+	if (intern->ptr) {
+		ZEND_ASSERT(is_constructor);
+		_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 +3939,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--
+<?php
+
+class C {
+    const FOO = 1;
+}
+
+function test(ReflectionClassConstant $r) {
+    /* implode() so that the name is not an interned string. */
+    $r->__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--
+<?php
+
+function test(ReflectionMethod $r) {
+    $r->__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"