Commit 626f3c3c7c6 for php.net

commit 626f3c3c7c6834f884286e454dbe5a870d125299
Author: Arnaud Le Blanc <arnaud.lb@gmail.com>
Date:   Wed Jun 18 18:01:57 2025 +0200

    Unify arg info representation for internal and user functions

    The arg_info member of zend_function is now always a zend_arg_info*. Before,
    it was a zend_internal_arg_info* on internal functions, unless the
    ZEND_ACC_USER_ARG_INFO flag was set.

    Closes GH-19022

diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS
index 71b6e1fd01e..3ec54f49391 100644
--- a/UPGRADING.INTERNALS
+++ b/UPGRADING.INTERNALS
@@ -49,6 +49,9 @@ PHP 8.6 INTERNALS UPGRADE NOTES
     removed. Call zend_wrong_param_count(); followed by RETURN_THROWS();
     instead.
   . PHP_HAVE_STREAMS macro removed from <php.h>.
+  . zend_function.arg_info is now always a zend_arg_info*. Before, it was a
+    zend_internal_arg_info on internal functions, unless the
+    ZEND_ACC_USER_ARG_INFO flag was set.

 ========================
 2. Build system changes
diff --git a/Zend/tests/closures/gh19653_3.phpt b/Zend/tests/closures/gh19653_3.phpt
new file mode 100644
index 00000000000..7a9ac589182
--- /dev/null
+++ b/Zend/tests/closures/gh19653_3.phpt
@@ -0,0 +1,21 @@
+--TEST--
+GH-19653 (Closure named argument unpacking between temporary closures can cause a crash) - temporary method variation
+--EXTENSIONS--
+zend_test
+--FILE--
+<?php
+
+function usage1($f) {
+    $f(tmpMethodParamName: null);
+}
+
+usage1([new _ZendTestClass(), 'testTmpMethodWithArgInfo']);
+usage1(eval('return function (string $a, string $b): string { return $a.$b; };'));
+
+?>
+--EXPECTF--
+Fatal error: Uncaught Error: Unknown named parameter $tmpMethodParamName in %s:%d
+Stack trace:
+#0 %s(%d): usage1(Object(Closure))
+#1 {main}
+  thrown in %s on line %d
diff --git a/Zend/tests/unified_arg_infos_001.phpt b/Zend/tests/unified_arg_infos_001.phpt
new file mode 100644
index 00000000000..d6699aa8d8c
--- /dev/null
+++ b/Zend/tests/unified_arg_infos_001.phpt
@@ -0,0 +1,21 @@
+--TEST--
+Declaring non persistent method with arg info
+--EXTENSIONS--
+zend_test
+--FILE--
+<?php
+
+$o = new _ZendTestClass();
+$o->testTmpMethodWithArgInfo(null);
+
+echo new ReflectionFunction($o->testTmpMethodWithArgInfo(...));
+
+?>
+--EXPECT--
+Closure [ <internal> public method testTmpMethodWithArgInfo ] {
+
+  - Parameters [2] {
+    Parameter #0 [ <optional> Foo|Bar|null $tmpMethodParamName = null ]
+    Parameter #1 [ <optional> string $tmpMethodParamWithStringDefaultValue = "tmpMethodParamWithStringDefaultValue" ]
+  }
+}
diff --git a/Zend/zend.c b/Zend/zend.c
index 4d024444a4b..c46c8e9ada8 100644
--- a/Zend/zend.c
+++ b/Zend/zend.c
@@ -38,6 +38,8 @@
 #include "zend_call_stack.h"
 #include "zend_max_execution_timer.h"
 #include "zend_hrtime.h"
+#include "zend_enum.h"
+#include "zend_closures.h"
 #include "Optimizer/zend_optimizer.h"
 #include "php.h"
 #include "php_globals.h"
@@ -1054,6 +1056,7 @@ void zend_startup(zend_utility_functions *utility_functions) /* {{{ */
 	ZVAL_UNDEF(&EG(last_fatal_error_backtrace));

 	zend_interned_strings_init();
+	zend_object_handlers_startup();
 	zend_startup_builtin_functions();
 	zend_register_standard_constants();
 	zend_register_auto_global(zend_string_init_interned("GLOBALS", sizeof("GLOBALS") - 1, 1), 1, php_auto_globals_create_globals);
@@ -1077,6 +1080,9 @@ void zend_startup(zend_utility_functions *utility_functions) /* {{{ */
 	tsrm_set_new_thread_end_handler(zend_new_thread_end_handler);
 	tsrm_set_shutdown_handler(zend_interned_strings_dtor);
 #endif
+
+    zend_enum_startup();
+    zend_closure_startup();
 }
 /* }}} */

diff --git a/Zend/zend_API.c b/Zend/zend_API.c
index ca001f0e0c4..0881a169dfa 100644
--- a/Zend/zend_API.c
+++ b/Zend/zend_API.c
@@ -20,6 +20,7 @@
 */

 #include "zend.h"
+#include "zend_compile.h"
 #include "zend_execute.h"
 #include "zend_API.h"
 #include "zend_hash.h"
@@ -2927,6 +2928,80 @@ static zend_always_inline void zend_normalize_internal_type(zend_type *type) {
 	} ZEND_TYPE_FOREACH_END();
 }

+static void zend_convert_internal_arg_info_type(zend_type *type, bool persistent)
+{
+	if (ZEND_TYPE_HAS_LITERAL_NAME(*type)) {
+		// gen_stubs.php does not support codegen for compound types. As a
+		// temporary workaround, we support union types by splitting
+		// the type name on `|` characters if necessary.
+		const char *class_name = ZEND_TYPE_LITERAL_NAME(*type);
+		type->type_mask &= ~_ZEND_TYPE_LITERAL_NAME_BIT;
+
+		size_t num_types = 1;
+		const char *p = class_name;
+		while ((p = strchr(p, '|'))) {
+			num_types++;
+			p++;
+		}
+
+		if (num_types == 1) {
+			/* Simple class type */
+			zend_string *str = zend_string_init_interned(class_name, strlen(class_name), persistent);
+			zend_alloc_ce_cache(str);
+			ZEND_TYPE_SET_PTR(*type, str);
+			type->type_mask |= _ZEND_TYPE_NAME_BIT;
+		} else {
+			/* Union type */
+			zend_type_list *list = pemalloc(ZEND_TYPE_LIST_SIZE(num_types), persistent);
+			list->num_types = num_types;
+			ZEND_TYPE_SET_LIST(*type, list);
+			ZEND_TYPE_FULL_MASK(*type) |= _ZEND_TYPE_UNION_BIT;
+
+			const char *start = class_name;
+			uint32_t j = 0;
+			while (true) {
+				const char *end = strchr(start, '|');
+				zend_string *str = zend_string_init_interned(start, end ? end - start : strlen(start), persistent);
+				zend_alloc_ce_cache(str);
+				list->types[j] = (zend_type) ZEND_TYPE_INIT_CLASS(str, 0, 0);
+				if (!end) {
+					break;
+				}
+				start = end + 1;
+				j++;
+			}
+		}
+	}
+	if (ZEND_TYPE_IS_ITERABLE_FALLBACK(*type)) {
+		/* Warning generated an extension load warning which is emitted for every test
+		   zend_error(E_CORE_WARNING, "iterable type is now a compile time alias for array|Traversable,"
+		   " regenerate the argument info via the php-src gen_stub build script");
+		   */
+		zend_type legacy_iterable = ZEND_TYPE_INIT_CLASS_MASK(
+			ZSTR_KNOWN(ZEND_STR_TRAVERSABLE),
+			(type->type_mask | MAY_BE_ARRAY)
+		);
+		*type = legacy_iterable;
+	}
+}
+
+ZEND_API void zend_convert_internal_arg_info(zend_arg_info *new_arg_info, const zend_internal_arg_info *arg_info, bool is_return_info, bool persistent)
+{
+	if (!is_return_info) {
+		new_arg_info->name = zend_string_init_interned(arg_info->name, strlen(arg_info->name), persistent);
+		if (arg_info->default_value) {
+			new_arg_info->default_value = zend_string_init_interned(arg_info->default_value, strlen(arg_info->default_value), persistent);
+		} else {
+			new_arg_info->default_value = NULL;
+		}
+	} else {
+		new_arg_info->name = NULL;
+		new_arg_info->default_value = NULL;
+	}
+	new_arg_info->type = arg_info->type;
+	zend_convert_internal_arg_info_type(&new_arg_info->type, persistent);
+}
+
 /* registers all functions in *library_functions in the function hash */
 ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend_function_entry *functions, HashTable *function_table, int type) /* {{{ */
 {
@@ -2938,6 +3013,7 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
 	int error_type;
 	zend_string *lowercase_name;
 	size_t fname_len;
+	const zend_internal_arg_info *internal_arg_info;

 	if (type==MODULE_PERSISTENT) {
 		error_type = E_CORE_WARNING;
@@ -2994,7 +3070,7 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend

 		if (ptr->arg_info) {
 			zend_internal_function_info *info = (zend_internal_function_info*)ptr->arg_info;
-			internal_function->arg_info = (zend_internal_arg_info*)ptr->arg_info+1;
+			internal_arg_info = ptr->arg_info+1;
 			internal_function->num_args = ptr->num_args;
 			/* Currently you cannot denote that the function can accept less arguments than num_args */
 			if (info->required_num_args == (uintptr_t)-1) {
@@ -3024,7 +3100,7 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
 			zend_error(E_CORE_WARNING, "Missing arginfo for %s%s%s()",
 				 scope ? ZSTR_VAL(scope->name) : "", scope ? "::" : "", ptr->fname);

-			internal_function->arg_info = NULL;
+			internal_arg_info = NULL;
 			internal_function->num_args = 0;
 			internal_function->required_num_args = 0;
 		}
@@ -3035,13 +3111,11 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
 				!(internal_function->fn_flags & ZEND_ACC_HAS_RETURN_TYPE)) {
 			zend_error(E_CORE_WARNING, "%s::__toString() implemented without string return type",
 				ZSTR_VAL(scope->name));
-			internal_function->arg_info = (zend_internal_arg_info *) arg_info_toString + 1;
+			internal_arg_info = (zend_internal_arg_info *) arg_info_toString + 1;
 			internal_function->fn_flags |= ZEND_ACC_HAS_RETURN_TYPE;
 			internal_function->num_args = internal_function->required_num_args = 0;
 		}

-
-		zend_set_function_arg_flags((zend_function*)internal_function);
 		if (ptr->flags & ZEND_ACC_ABSTRACT) {
 			if (scope) {
 				/* This is a class that must be abstract itself. Here we set the check info. */
@@ -3106,17 +3180,17 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
 		}

 		/* If types of arguments have to be checked */
-		if (reg_function->arg_info && num_args) {
+		if (internal_arg_info && num_args) {
 			uint32_t i;
 			for (i = 0; i < num_args; i++) {
-				zend_internal_arg_info *arg_info = &reg_function->arg_info[i];
+				const zend_internal_arg_info *arg_info = &internal_arg_info[i];
 				ZEND_ASSERT(arg_info->name && "Parameter must have a name");
 				if (ZEND_TYPE_IS_SET(arg_info->type)) {
 				    reg_function->fn_flags |= ZEND_ACC_HAS_TYPE_HINTS;
 				}
 #if ZEND_DEBUG
 				for (uint32_t j = 0; j < i; j++) {
-					if (!strcmp(arg_info->name, reg_function->arg_info[j].name)) {
+					if (!strcmp(arg_info->name, internal_arg_info[j].name)) {
 						zend_error_noreturn(E_CORE_ERROR,
 							"Duplicate parameter name $%s for function %s%s%s()", arg_info->name,
 							scope ? ZSTR_VAL(scope->name) : "", scope ? "::" : "", ptr->fname);
@@ -3126,78 +3200,24 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
 			}
 		}

-		/* Rebuild arginfos if parameter/property types and/or a return type are used */
-		if (reg_function->arg_info &&
-		    (reg_function->fn_flags & (ZEND_ACC_HAS_RETURN_TYPE|ZEND_ACC_HAS_TYPE_HINTS))) {
-			/* convert "const char*" class type names into "zend_string*" */
+		/* Convert zend_internal_arg_info to zend_arg_info */
+		if (internal_arg_info) {
 			uint32_t i;
-			zend_internal_arg_info *arg_info = reg_function->arg_info - 1;
-			zend_internal_arg_info *new_arg_info;
+			const zend_internal_arg_info *arg_info = internal_arg_info - 1;
+			zend_arg_info *new_arg_info;

 			/* Treat return type as an extra argument */
 			num_args++;
-			new_arg_info = malloc(sizeof(zend_internal_arg_info) * num_args);
-			memcpy(new_arg_info, arg_info, sizeof(zend_internal_arg_info) * num_args);
+			new_arg_info = malloc(sizeof(zend_arg_info) * num_args);
 			reg_function->arg_info = new_arg_info + 1;
 			for (i = 0; i < num_args; i++) {
-				if (ZEND_TYPE_HAS_LITERAL_NAME(new_arg_info[i].type)) {
-					// gen_stubs.php does not support codegen for DNF types in arg infos.
-					// As a temporary workaround, we split the type name on `|` characters,
-					// converting it to an union type if necessary.
-					const char *class_name = ZEND_TYPE_LITERAL_NAME(new_arg_info[i].type);
-					new_arg_info[i].type.type_mask &= ~_ZEND_TYPE_LITERAL_NAME_BIT;
-
-					size_t num_types = 1;
-					const char *p = class_name;
-					while ((p = strchr(p, '|'))) {
-						num_types++;
-						p++;
-					}
-
-					if (num_types == 1) {
-						/* Simple class type */
-						zend_string *str = zend_string_init_interned(class_name, strlen(class_name), 1);
-						zend_alloc_ce_cache(str);
-						ZEND_TYPE_SET_PTR(new_arg_info[i].type, str);
-						new_arg_info[i].type.type_mask |= _ZEND_TYPE_NAME_BIT;
-					} else {
-						/* Union type */
-						zend_type_list *list = malloc(ZEND_TYPE_LIST_SIZE(num_types));
-						list->num_types = num_types;
-						ZEND_TYPE_SET_LIST(new_arg_info[i].type, list);
-						ZEND_TYPE_FULL_MASK(new_arg_info[i].type) |= _ZEND_TYPE_UNION_BIT;
-
-						const char *start = class_name;
-						uint32_t j = 0;
-						while (true) {
-							const char *end = strchr(start, '|');
-							zend_string *str = zend_string_init_interned(start, end ? end - start : strlen(start), 1);
-							zend_alloc_ce_cache(str);
-							list->types[j] = (zend_type) ZEND_TYPE_INIT_CLASS(str, 0, 0);
-							if (!end) {
-								break;
-							}
-							start = end + 1;
-							j++;
-						}
-					}
-				}
-				if (ZEND_TYPE_IS_ITERABLE_FALLBACK(new_arg_info[i].type)) {
-					/* Warning generated an extension load warning which is emitted for every test
-					zend_error(E_CORE_WARNING, "iterable type is now a compile time alias for array|Traversable,"
-						" regenerate the argument info via the php-src gen_stub build script");
-					*/
-					zend_type legacy_iterable = ZEND_TYPE_INIT_CLASS_MASK(
-						ZSTR_KNOWN(ZEND_STR_TRAVERSABLE),
-						(new_arg_info[i].type.type_mask | MAY_BE_ARRAY)
-					);
-					new_arg_info[i].type = legacy_iterable;
-				}
-
-				zend_normalize_internal_type(&new_arg_info[i].type);
+				zend_convert_internal_arg_info(&new_arg_info[i], &arg_info[i],
+						i == 0, true);
 			}
 		}

+		zend_set_function_arg_flags((zend_function*)reg_function);
+
 		if (scope) {
 			zend_check_magic_method_implementation(
 				scope, (zend_function *)reg_function, lowercase_name, E_CORE_ERROR);
@@ -5242,48 +5262,43 @@ static zend_string *try_parse_string(const char *str, size_t len, char quote) {
 }

 ZEND_API zend_result zend_get_default_from_internal_arg_info(
-		zval *default_value_zval, const zend_internal_arg_info *arg_info)
+		zval *default_value_zval, const zend_arg_info *arg_info)
 {
-	const char *default_value = arg_info->default_value;
+	const zend_string *default_value = arg_info->default_value;
 	if (!default_value) {
 		return FAILURE;
 	}

 	/* Avoid going through the full AST machinery for some simple and common cases. */
-	size_t default_value_len = strlen(default_value);
 	zend_ulong lval;
-	if (default_value_len == sizeof("null")-1
-			&& !memcmp(default_value, "null", sizeof("null")-1)) {
+	if (zend_string_equals_literal(default_value, "null")) {
 		ZVAL_NULL(default_value_zval);
 		return SUCCESS;
-	} else if (default_value_len == sizeof("true")-1
-			&& !memcmp(default_value, "true", sizeof("true")-1)) {
+	} else if (zend_string_equals_literal(default_value, "true")) {
 		ZVAL_TRUE(default_value_zval);
 		return SUCCESS;
-	} else if (default_value_len == sizeof("false")-1
-			&& !memcmp(default_value, "false", sizeof("false")-1)) {
+	} else if (zend_string_equals_literal(default_value, "false")) {
 		ZVAL_FALSE(default_value_zval);
 		return SUCCESS;
-	} else if (default_value_len >= 2
-			&& (default_value[0] == '\'' || default_value[0] == '"')
-			&& default_value[default_value_len - 1] == default_value[0]) {
+	} else if (ZSTR_LEN(default_value) >= 2
+			&& (ZSTR_VAL(default_value)[0] == '\'' || ZSTR_VAL(default_value)[0] == '"')
+			&& ZSTR_VAL(default_value)[ZSTR_LEN(default_value) - 1] == ZSTR_VAL(default_value)[0]) {
 		zend_string *str = try_parse_string(
-			default_value + 1, default_value_len - 2, default_value[0]);
+			ZSTR_VAL(default_value) + 1, ZSTR_LEN(default_value) - 2, ZSTR_VAL(default_value)[0]);
 		if (str) {
 			ZVAL_STR(default_value_zval, str);
 			return SUCCESS;
 		}
-	} else if (default_value_len == sizeof("[]")-1
-			&& !memcmp(default_value, "[]", sizeof("[]")-1)) {
+	} else if (zend_string_equals_literal(default_value, "[]")) {
 		ZVAL_EMPTY_ARRAY(default_value_zval);
 		return SUCCESS;
-	} else if (ZEND_HANDLE_NUMERIC_STR(default_value, default_value_len, lval)) {
+	} else if (ZEND_HANDLE_NUMERIC(default_value, lval)) {
 		ZVAL_LONG(default_value_zval, lval);
 		return SUCCESS;
 	}

 #if 0
-	fprintf(stderr, "Evaluating %s via AST\n", default_value);
+	fprintf(stderr, "Evaluating %s via AST\n", ZSTR_VAL(default_value));
 #endif
-	return get_default_via_ast(default_value_zval, default_value);
+	return get_default_via_ast(default_value_zval, ZSTR_VAL(default_value));
 }
diff --git a/Zend/zend_API.h b/Zend/zend_API.h
index 1bb47a2e870..c1ccbf13666 100644
--- a/Zend/zend_API.h
+++ b/Zend/zend_API.h
@@ -922,8 +922,12 @@ ZEND_API bool zend_is_iterable(const zval *iterable);

 ZEND_API bool zend_is_countable(const zval *countable);

+ZEND_API void zend_convert_internal_arg_info(zend_arg_info *new_arg_info,
+		const zend_internal_arg_info *arg_info, bool is_return_info,
+		bool permanent);
+
 ZEND_API zend_result zend_get_default_from_internal_arg_info(
-		zval *default_value_zval, const zend_internal_arg_info *arg_info);
+		zval *default_value_zval, const zend_arg_info *arg_info);

 END_EXTERN_C()

diff --git a/Zend/zend_closures.c b/Zend/zend_closures.c
index 4ecb6b2c493..05b68620448 100644
--- a/Zend/zend_closures.c
+++ b/Zend/zend_closures.c
@@ -510,7 +510,7 @@ ZEND_API zend_function *zend_get_closure_invoke_method(zend_object *object) /* {
 	 * ZEND_ACC_USER_ARG_INFO flag to prevent invalid usage by Reflection */
 	invoke->type = ZEND_INTERNAL_FUNCTION;
 	invoke->internal_function.fn_flags =
-		ZEND_ACC_PUBLIC | ZEND_ACC_CALL_VIA_HANDLER | (closure->func.common.fn_flags & keep_flags);
+		ZEND_ACC_PUBLIC | ZEND_ACC_CALL_VIA_HANDLER | ZEND_ACC_NEVER_CACHE | (closure->func.common.fn_flags & keep_flags);
 	if (closure->func.type != ZEND_INTERNAL_FUNCTION || (closure->func.common.fn_flags & ZEND_ACC_USER_ARG_INFO)) {
 		invoke->internal_function.fn_flags |=
 			ZEND_ACC_USER_ARG_INFO;
@@ -618,7 +618,6 @@ static HashTable *zend_closure_get_debug_info(zend_object *object, int *is_temp)
 	zval val;
 	struct _zend_arg_info *arg_info = closure->func.common.arg_info;
 	HashTable *debug_info;
-	bool zstr_args = (closure->func.type == ZEND_USER_FUNCTION) || (closure->func.common.fn_flags & ZEND_ACC_USER_ARG_INFO);

 	*is_temp = 1;

@@ -694,15 +693,9 @@ static HashTable *zend_closure_get_debug_info(zend_object *object, int *is_temp)
 			zend_string *name;
 			zval info;
 			ZEND_ASSERT(arg_info->name && "Argument should have name");
-			if (zstr_args) {
-				name = zend_strpprintf(0, "%s$%s",
-						ZEND_ARG_SEND_MODE(arg_info) ? "&" : "",
-						ZSTR_VAL(arg_info->name));
-			} else {
-				name = zend_strpprintf(0, "%s$%s",
-						ZEND_ARG_SEND_MODE(arg_info) ? "&" : "",
-						((zend_internal_arg_info*)arg_info)->name);
-			}
+			name = zend_strpprintf(0, "%s$%s",
+					ZEND_ARG_SEND_MODE(arg_info) ? "&" : "",
+					ZSTR_VAL(arg_info->name));
 			ZVAL_NEW_STR(&info, zend_strpprintf(0, "%s", i >= required ? "<optional>" : "<required>"));
 			zend_hash_update(Z_ARRVAL(val), name, &info);
 			zend_string_release_ex(name, 0);
@@ -885,8 +878,7 @@ ZEND_API void zend_create_fake_closure(zval *res, zend_function *func, zend_clas
 }
 /* }}} */

-/* __call and __callStatic name the arguments "$arguments" in the docs. */
-static zend_internal_arg_info trampoline_arg_info[] = {ZEND_ARG_VARIADIC_TYPE_INFO(false, arguments, IS_MIXED, false)};
+static zend_arg_info trampoline_arg_info[1];

 void zend_closure_from_frame(zval *return_value, const zend_execute_data *call) { /* {{{ */
 	zval instance;
@@ -951,3 +943,11 @@ void zend_closure_bind_var_ex(zval *closure_zv, uint32_t offset, zval *val) /* {
 	ZVAL_COPY_VALUE(var, val);
 }
 /* }}} */
+
+void zend_closure_startup(void)
+{
+	/* __call and __callStatic name the arguments "$arguments" in the docs. */
+	trampoline_arg_info[0].name = zend_string_init_interned("arguments", strlen("arguments"), true);
+	trampoline_arg_info[0].type = (zend_type)ZEND_TYPE_INIT_CODE(IS_MIXED, false, _ZEND_ARG_INFO_FLAGS(false, 1, 0));
+	trampoline_arg_info[0].default_value = NULL;
+}
diff --git a/Zend/zend_closures.h b/Zend/zend_closures.h
index 8bea4ffb051..a118044c6e2 100644
--- a/Zend/zend_closures.h
+++ b/Zend/zend_closures.h
@@ -28,6 +28,7 @@ BEGIN_EXTERN_C()
 #define ZEND_CLOSURE_OBJECT(op_array) \
 	((zend_object*)((char*)(op_array) - sizeof(zend_object)))

+void zend_closure_startup(void);
 void zend_register_closure_ce(void);
 void zend_closure_bind_var(zval *closure_zv, zend_string *var_name, zval *var);
 void zend_closure_bind_var_ex(zval *closure_zv, uint32_t offset, zval *val);
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index 50ba8029873..5eba2ec1366 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -3688,21 +3688,10 @@ static void zend_compile_compound_assign(znode *result, zend_ast *ast) /* {{{ */

 static uint32_t zend_get_arg_num(const zend_function *fn, const zend_string *arg_name) {
 	// TODO: Caching?
-	if (fn->type == ZEND_USER_FUNCTION) {
-		for (uint32_t i = 0; i < fn->common.num_args; i++) {
-			const zend_arg_info *arg_info = &fn->op_array.arg_info[i];
-			if (zend_string_equals(arg_info->name, arg_name)) {
-				return i + 1;
-			}
-		}
-	} else {
-		ZEND_ASSERT(fn->common.num_args == 0 || fn->internal_function.arg_info);
-		for (uint32_t i = 0; i < fn->common.num_args; i++) {
-			const zend_internal_arg_info *arg_info = &fn->internal_function.arg_info[i];
-			size_t len = strlen(arg_info->name);
-			if (zend_string_equals_cstr(arg_name, arg_info->name, len)) {
-				return i + 1;
-			}
+	for (uint32_t i = 0; i < fn->common.num_args; i++) {
+		zend_arg_info *arg_info = &fn->op_array.arg_info[i];
+		if (zend_string_equals(arg_info->name, arg_name)) {
+			return i + 1;
 		}
 	}

@@ -4702,7 +4691,7 @@ static uint32_t zend_compile_frameless_icall_ex(znode *result, const zend_ast_li
 		if (i < args->children) {
 			zend_compile_expr(&arg_zvs[i], args->child[i]);
 		} else {
-			const zend_internal_arg_info *arg_info = (const zend_internal_arg_info *)&fbc->common.arg_info[i];
+			const zend_arg_info *arg_info = &fbc->common.arg_info[i];
 			arg_zvs[i].op_type = IS_CONST;
 			if (zend_get_default_from_internal_arg_info(&arg_zvs[i].u.constant, arg_info) == FAILURE) {
 				ZEND_UNREACHABLE();
@@ -7934,6 +7923,7 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32
 		arg_info = &arg_infos[i];
 		arg_info->name = zend_string_copy(name);
 		arg_info->type = (zend_type) ZEND_TYPE_INIT_NONE(0);
+		arg_info->default_value = NULL;

 		if (attributes_ast) {
 			zend_compile_attributes(
diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h
index 86fab4b57de..d2a3b47bf92 100644
--- a/Zend/zend_compile.h
+++ b/Zend/zend_compile.h
@@ -592,7 +592,7 @@ typedef struct _zend_internal_function {
 	zend_function *prototype;
 	uint32_t num_args;
 	uint32_t required_num_args;
-	zend_internal_arg_info *arg_info;
+	zend_arg_info *arg_info;
 	HashTable *attributes;
 	ZEND_MAP_PTR_DEF(void **, run_time_cache);
 	zend_string *doc_comment;
@@ -976,7 +976,8 @@ ZEND_API ZEND_COLD void zend_user_exception_handler(void);
 		} \
 	} while (0)

-void zend_free_internal_arg_info(zend_internal_function *function);
+ZEND_API void zend_free_internal_arg_info(zend_internal_function *function,
+		bool permanent);
 ZEND_API void destroy_zend_function(zend_function *function);
 ZEND_API void zend_function_dtor(zval *zv);
 ZEND_API void destroy_zend_class(zval *zv);
diff --git a/Zend/zend_enum.c b/Zend/zend_enum.c
index ae9e7b70121..00fbab22a05 100644
--- a/Zend/zend_enum.c
+++ b/Zend/zend_enum.c
@@ -36,6 +36,10 @@ ZEND_API zend_class_entry *zend_ce_unit_enum;
 ZEND_API zend_class_entry *zend_ce_backed_enum;
 ZEND_API zend_object_handlers zend_enum_object_handlers;

+static zend_arg_info zarginfo_class_UnitEnum_cases[sizeof(arginfo_class_UnitEnum_cases)/sizeof(zend_internal_arg_info)];
+static zend_arg_info zarginfo_class_BackedEnum_from[sizeof(arginfo_class_BackedEnum_from)/sizeof(zend_internal_arg_info)];
+static zend_arg_info zarginfo_class_BackedEnum_tryFrom[sizeof(arginfo_class_BackedEnum_tryFrom)/sizeof(zend_internal_arg_info)];
+
 zend_object *zend_enum_new(zval *result, zend_class_entry *ce, zend_string *case_name, zval *backing_value_zv)
 {
 	zend_object *zobj = zend_objects_new(ce);
@@ -446,7 +450,7 @@ void zend_enum_register_funcs(zend_class_entry *ce)
 	cases_function->function_name = ZSTR_KNOWN(ZEND_STR_CASES);
 	cases_function->fn_flags = fn_flags;
 	cases_function->doc_comment = NULL;
-	cases_function->arg_info = (zend_internal_arg_info *) (arginfo_class_UnitEnum_cases + 1);
+	cases_function->arg_info = zarginfo_class_UnitEnum_cases + 1;
 	zend_enum_register_func(ce, ZEND_STR_CASES, cases_function);

 	if (ce->enum_backing_type != IS_UNDEF) {
@@ -457,7 +461,7 @@ void zend_enum_register_funcs(zend_class_entry *ce)
 		from_function->doc_comment = NULL;
 		from_function->num_args = 1;
 		from_function->required_num_args = 1;
-		from_function->arg_info = (zend_internal_arg_info *) (arginfo_class_BackedEnum_from + 1);
+		from_function->arg_info = zarginfo_class_BackedEnum_from + 1;
 		zend_enum_register_func(ce, ZEND_STR_FROM, from_function);

 		zend_internal_function *try_from_function = zend_arena_calloc(&CG(arena), sizeof(zend_internal_function), 1);
@@ -467,7 +471,7 @@ void zend_enum_register_funcs(zend_class_entry *ce)
 		try_from_function->doc_comment = NULL;
 		try_from_function->num_args = 1;
 		try_from_function->required_num_args = 1;
-		try_from_function->arg_info = (zend_internal_arg_info *) (arginfo_class_BackedEnum_tryFrom + 1);
+		try_from_function->arg_info = zarginfo_class_BackedEnum_tryFrom + 1;
 		zend_enum_register_func(ce, ZEND_STR_TRYFROM_LOWERCASE, try_from_function);
 	}
 }
@@ -633,3 +637,16 @@ ZEND_API zend_object *zend_enum_get_case_cstr(zend_class_entry *ce, const char *
 	zend_class_constant *c = zend_hash_str_find_ptr(CE_CONSTANTS_TABLE(ce), name, strlen(name));
 	return zend_enum_case_from_class_constant(c);
 }
+
+void zend_enum_startup(void)
+{
+	for (size_t i = 0; i < sizeof(zarginfo_class_UnitEnum_cases)/sizeof(zend_arg_info); i++) {
+		zend_convert_internal_arg_info(&zarginfo_class_UnitEnum_cases[i], &arginfo_class_UnitEnum_cases[i], i == 0, true);
+	}
+	for (size_t i = 0; i < sizeof(zarginfo_class_BackedEnum_from)/sizeof(zend_arg_info); i++) {
+		zend_convert_internal_arg_info(&zarginfo_class_BackedEnum_from[i], &arginfo_class_BackedEnum_from[i], i == 0, true);
+	}
+	for (size_t i = 0; i < sizeof(zarginfo_class_BackedEnum_tryFrom)/sizeof(zend_arg_info); i++) {
+		zend_convert_internal_arg_info(&zarginfo_class_BackedEnum_tryFrom[i], &arginfo_class_BackedEnum_tryFrom[i], i == 0, true);
+	}
+}
diff --git a/Zend/zend_enum.h b/Zend/zend_enum.h
index 7b3b0184b4e..d6c82018947 100644
--- a/Zend/zend_enum.h
+++ b/Zend/zend_enum.h
@@ -30,6 +30,7 @@ extern ZEND_API zend_class_entry *zend_ce_unit_enum;
 extern ZEND_API zend_class_entry *zend_ce_backed_enum;
 extern ZEND_API zend_object_handlers zend_enum_object_handlers;

+void zend_enum_startup(void);
 void zend_register_enum_ce(void);
 void zend_enum_add_interfaces(zend_class_entry *ce);
 zend_result zend_enum_build_backed_enum_table(zend_class_entry *ce);
diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c
index d411dcbc3b9..518cbb98fc0 100644
--- a/Zend/zend_execute.c
+++ b/Zend/zend_execute.c
@@ -136,8 +136,7 @@ static ZEND_FUNCTION(pass)
 {
 }

-ZEND_BEGIN_ARG_INFO_EX(zend_pass_function_arg_info, 0, 0, 0)
-ZEND_END_ARG_INFO()
+static zend_arg_info zend_pass_function_arg_info[1] = {0};

 ZEND_API const zend_internal_function zend_pass_function = {
 	ZEND_INTERNAL_FUNCTION, /* type              */
@@ -148,7 +147,7 @@ ZEND_API const zend_internal_function zend_pass_function = {
 	NULL,                   /* prototype         */
 	0,                      /* num_args          */
 	0,                      /* required_num_args */
-	(zend_internal_arg_info *) zend_pass_function_arg_info + 1, /* arg_info */
+	zend_pass_function_arg_info + 1, /* arg_info */
 	NULL,                   /* attributes        */
 	NULL,                   /* run_time_cache    */
 	NULL,                   /* doc_comment       */
@@ -1480,7 +1479,7 @@ static ZEND_COLD void zend_verify_void_return_error(const zend_function *zf, con

 ZEND_API bool zend_verify_internal_return_type(const zend_function *zf, zval *ret)
 {
-	const zend_internal_arg_info *ret_info = zf->internal_function.arg_info - 1;
+	const zend_arg_info *ret_info = zf->internal_function.arg_info - 1;

 	if (ZEND_TYPE_FULL_MASK(ret_info->type) & MAY_BE_VOID) {
 		if (UNEXPECTED(Z_TYPE_P(ret) != IS_NULL)) {
@@ -5474,28 +5473,17 @@ static zend_always_inline uint32_t zend_get_arg_offset_by_name(

 	// TODO: Use a hash table?
 	uint32_t num_args = fbc->common.num_args;
-	if (EXPECTED(fbc->type == ZEND_USER_FUNCTION)
-			|| EXPECTED(fbc->common.fn_flags & ZEND_ACC_USER_ARG_INFO)) {
-		for (uint32_t i = 0; i < num_args; i++) {
-			const zend_arg_info *arg_info = &fbc->common.arg_info[i];
-			if (zend_string_equals(arg_name, arg_info->name)) {
-				if (fbc->type == ZEND_USER_FUNCTION && (!fbc->op_array.refcount || !(fbc->op_array.fn_flags & ZEND_ACC_CLOSURE))) {
-					*cache_slot = unique_id;
-					*(uintptr_t *)(cache_slot + 1) = i;
-				}
-				return i;
-			}
-		}
-	} else {
-		ZEND_ASSERT(num_args == 0 || fbc->internal_function.arg_info);
-		for (uint32_t i = 0; i < num_args; i++) {
-			const zend_internal_arg_info *arg_info = &fbc->internal_function.arg_info[i];
-			size_t len = strlen(arg_info->name);
-			if (zend_string_equals_cstr(arg_name, arg_info->name, len)) {
+	for (uint32_t i = 0; i < num_args; i++) {
+		const zend_arg_info *arg_info = &fbc->common.arg_info[i];
+		if (zend_string_equals(arg_name, arg_info->name)) {
+			if ((fbc->type == ZEND_USER_FUNCTION
+			  && (!fbc->op_array.refcount || !(fbc->op_array.fn_flags & ZEND_ACC_CLOSURE)))
+			 || (fbc->type == ZEND_INTERNAL_FUNCTION
+			  && !(fbc->common.fn_flags & ZEND_ACC_NEVER_CACHE))) {
 				*cache_slot = unique_id;
 				*(uintptr_t *)(cache_slot + 1) = i;
-				return i;
 			}
+			return i;
 		}
 	}

@@ -5503,7 +5491,7 @@ static zend_always_inline uint32_t zend_get_arg_offset_by_name(
 		if ((fbc->type == ZEND_USER_FUNCTION
 		  && (!fbc->op_array.refcount || !(fbc->op_array.fn_flags & ZEND_ACC_CLOSURE)))
 		 || (fbc->type == ZEND_INTERNAL_FUNCTION
-		  && !(fbc->common.fn_flags & ZEND_ACC_USER_ARG_INFO))) {
+		  && !(fbc->common.fn_flags & ZEND_ACC_NEVER_CACHE))) {
 			*cache_slot = unique_id;
 			*(uintptr_t *)(cache_slot + 1) = fbc->common.num_args;
 		}
@@ -5661,7 +5649,7 @@ ZEND_API zend_result ZEND_FASTCALL zend_handle_undef_args(zend_execute_data *cal
 				continue;
 			}

-			zend_internal_arg_info *arg_info = &fbc->internal_function.arg_info[i];
+			zend_arg_info *arg_info = &fbc->internal_function.arg_info[i];
 			if (i < fbc->common.required_num_args) {
 				zend_execute_data *old = start_fake_frame(call, NULL);
 				zend_argument_error(zend_ce_argument_count_error, i + 1, "not passed");
diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c
index 660975f9bc1..e134d3d496b 100644
--- a/Zend/zend_execute_API.c
+++ b/Zend/zend_execute_API.c
@@ -652,11 +652,7 @@ ZEND_API const char *get_function_arg_name(const zend_function *func, uint32_t a
 		return NULL;
 	}

-	if (func->type == ZEND_USER_FUNCTION || (func->common.fn_flags & ZEND_ACC_USER_ARG_INFO)) {
-		return ZSTR_VAL(func->common.arg_info[arg_num - 1].name);
-	} else {
-		return ((zend_internal_arg_info*) func->common.arg_info)[arg_num - 1].name;
-	}
+	return ZSTR_VAL(func->common.arg_info[arg_num - 1].name);
 }
 /* }}} */

diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c
index 64448bc5355..3c3931cdca1 100644
--- a/Zend/zend_inheritance.c
+++ b/Zend/zend_inheritance.c
@@ -953,18 +953,14 @@ static ZEND_COLD zend_string *zend_get_function_declaration(
 			}

 			smart_str_appendc(&str, '$');
-			if (fptr->type == ZEND_INTERNAL_FUNCTION) {
-				smart_str_appends(&str, ((zend_internal_arg_info*)arg_info)->name);
-			} else {
-				smart_str_appendl(&str, ZSTR_VAL(arg_info->name), ZSTR_LEN(arg_info->name));
-			}
+			smart_str_append(&str, arg_info->name);

 			if (i >= required && !ZEND_ARG_IS_VARIADIC(arg_info)) {
 				smart_str_appends(&str, " = ");

 				if (fptr->type == ZEND_INTERNAL_FUNCTION) {
-					if (((zend_internal_arg_info*)arg_info)->default_value) {
-						smart_str_appends(&str, ((zend_internal_arg_info*)arg_info)->default_value);
+					if (arg_info->default_value) {
+						smart_str_append(&str, arg_info->default_value);
 					} else {
 						smart_str_appends(&str, "<default>");
 					}
diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c
index 470fb76ec14..88b7b1112d7 100644
--- a/Zend/zend_object_handlers.c
+++ b/Zend/zend_object_handlers.c
@@ -46,6 +46,9 @@
 #define IN_ISSET	ZEND_GUARD_PROPERTY_ISSET
 #define IN_HOOK		ZEND_GUARD_PROPERTY_HOOK

+static zend_arg_info zend_call_trampoline_arginfo[1] = {{0}};
+static zend_arg_info zend_property_hook_arginfo[1] = {{0}};
+
 static zend_always_inline bool zend_objects_check_stack_limit(void)
 {
 #ifdef ZEND_CHECK_STACK_LIMIT
@@ -1782,9 +1785,6 @@ ZEND_API zend_function *zend_get_property_hook_trampoline(
 	const zend_property_info *prop_info,
 	zend_property_hook_kind kind, zend_string *prop_name)
 {
-	static const zend_internal_arg_info arg_info[2] = {
-		{ .name = "value" }
-	};
 	zend_function *func;
 	if (EXPECTED(EG(trampoline).common.function_name == NULL)) {
 		func = &EG(trampoline);
@@ -1810,7 +1810,7 @@ ZEND_API zend_function *zend_get_property_hook_trampoline(
 	func->common.scope = prop_info->ce;
 	func->common.prototype = NULL;
 	func->common.prop_info = prop_info;
-	func->common.arg_info = (zend_arg_info *) arg_info;
+	func->common.arg_info = zend_property_hook_arginfo;
 	func->internal_function.handler = kind == ZEND_PROPERTY_HOOK_GET
 		? ZEND_FN(zend_parent_hook_get_trampoline)
 		: ZEND_FN(zend_parent_hook_set_trampoline);
@@ -2574,3 +2574,8 @@ ZEND_API const zend_object_handlers std_object_handlers = {
 	zend_std_compare_objects,				/* compare */
 	NULL,									/* get_properties_for */
 };
+
+void zend_object_handlers_startup(void) {
+	zend_call_trampoline_arginfo[0].name = ZSTR_KNOWN(ZEND_STR_ARGS);
+	zend_property_hook_arginfo[0].name = ZSTR_KNOWN(ZEND_STR_VALUE);
+}
diff --git a/Zend/zend_object_handlers.h b/Zend/zend_object_handlers.h
index 59277c09d80..3e922343eb1 100644
--- a/Zend/zend_object_handlers.h
+++ b/Zend/zend_object_handlers.h
@@ -334,6 +334,8 @@ ZEND_API zend_function *zend_get_property_hook_trampoline(

 ZEND_API bool ZEND_FASTCALL zend_asymmetric_property_has_set_access(const zend_property_info *prop_info);

+void zend_object_handlers_startup(void);
+
 #define zend_release_properties(ht) do { \
 	if (ht) { \
 		zend_array_release(ht); \
diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c
index 1962c7b5a56..5e9e7b20d86 100644
--- a/Zend/zend_opcode.c
+++ b/Zend/zend_opcode.c
@@ -124,21 +124,32 @@ ZEND_API void zend_type_release(zend_type type, bool persistent) {
 	}
 }

-void zend_free_internal_arg_info(zend_internal_function *function) {
-	if ((function->fn_flags & (ZEND_ACC_HAS_RETURN_TYPE|ZEND_ACC_HAS_TYPE_HINTS)) &&
-		function->arg_info) {
+ZEND_API void zend_free_internal_arg_info(zend_internal_function *function,
+		bool persistent) {
+	if (function->arg_info) {
+		ZEND_ASSERT((persistent || (function->fn_flags & ZEND_ACC_NEVER_CACHE))
+				&& "Functions with non-persistent arg_info must be flagged ZEND_ACC_NEVER_CACHE");

 		uint32_t i;
 		uint32_t num_args = function->num_args + 1;
-		zend_internal_arg_info *arg_info = function->arg_info - 1;
+		zend_arg_info *arg_info = function->arg_info - 1;

 		if (function->fn_flags & ZEND_ACC_VARIADIC) {
 			num_args++;
 		}
 		for (i = 0 ; i < num_args; i++) {
-			zend_type_release(arg_info[i].type, /* persistent */ true);
+			bool is_return_info = i == 0;
+			if (!is_return_info) {
+				zend_string_release_ex(arg_info[i].name, persistent);
+				if (arg_info[i].default_value) {
+					zend_string_release_ex(arg_info[i].default_value,
+							persistent);
+				}
+			}
+			zend_type_release(arg_info[i].type, persistent);
 		}
-		free(arg_info);
+
+		pefree(arg_info, persistent);
 	}
 }

@@ -157,7 +168,7 @@ ZEND_API void zend_function_dtor(zval *zv)

 		/* For methods this will be called explicitly. */
 		if (!function->common.scope) {
-			zend_free_internal_arg_info(&function->internal_function);
+			zend_free_internal_arg_info(&function->internal_function, true);

 			if (function->common.attributes) {
 				zend_hash_release(function->common.attributes);
@@ -474,12 +485,9 @@ ZEND_API void destroy_zend_class(zval *zv)
 			zend_hash_destroy(&ce->properties_info);
 			zend_string_release_ex(ce->name, 1);

-			/* TODO: eliminate this loop for classes without functions with arg_info / attributes */
 			ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, fn) {
 				if (fn->common.scope == ce) {
-					if (fn->common.fn_flags & (ZEND_ACC_HAS_RETURN_TYPE|ZEND_ACC_HAS_TYPE_HINTS)) {
-						zend_free_internal_arg_info(&fn->internal_function);
-					}
+					zend_free_internal_arg_info(&fn->internal_function, true);

 					if (fn->common.attributes) {
 						zend_hash_release(fn->common.attributes);
diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c
index d1d49ca391a..7acb14b778f 100644
--- a/ext/opcache/ZendAccelerator.c
+++ b/ext/opcache/ZendAccelerator.c
@@ -663,8 +663,7 @@ static void accel_copy_permanent_strings(zend_new_interned_string_func_t new_int
 		if (Z_FUNC(p->val)->common.function_name) {
 			Z_FUNC(p->val)->common.function_name = new_interned_string(Z_FUNC(p->val)->common.function_name);
 		}
-		if (Z_FUNC(p->val)->common.arg_info &&
-		    (Z_FUNC(p->val)->common.fn_flags & (ZEND_ACC_HAS_RETURN_TYPE|ZEND_ACC_HAS_TYPE_HINTS))) {
+		if (Z_FUNC(p->val)->common.arg_info) {
 			uint32_t i;
 			uint32_t num_args = Z_FUNC(p->val)->common.num_args + 1;
 			zend_arg_info *arg_info = Z_FUNC(p->val)->common.arg_info - 1;
@@ -673,6 +672,12 @@ static void accel_copy_permanent_strings(zend_new_interned_string_func_t new_int
 				num_args++;
 			}
 			for (i = 0 ; i < num_args; i++) {
+				if (i > 0) {
+					arg_info[i].name = new_interned_string(arg_info[i].name);
+					if (arg_info[i].default_value) {
+						arg_info[i].default_value = new_interned_string(arg_info[i].default_value);
+					}
+				}
 				accel_copy_permanent_list_types(new_interned_string, arg_info[i].type);
 			}
 		}
@@ -714,6 +719,24 @@ static void accel_copy_permanent_strings(zend_new_interned_string_func_t new_int
 			if (Z_FUNC(q->val)->common.function_name) {
 				Z_FUNC(q->val)->common.function_name = new_interned_string(Z_FUNC(q->val)->common.function_name);
 			}
+			if (Z_FUNC(q->val)->common.scope == ce) {
+				uint32_t i;
+				uint32_t num_args = Z_FUNC(q->val)->common.num_args + 1;
+				zend_arg_info *arg_info = Z_FUNC(q->val)->common.arg_info - 1;
+
+				if (Z_FUNC(q->val)->common.fn_flags & ZEND_ACC_VARIADIC) {
+					num_args++;
+				}
+				for (i = 0 ; i < num_args; i++) {
+					if (i > 0) {
+						arg_info[i].name = new_interned_string(arg_info[i].name);
+						if (arg_info[i].default_value) {
+							arg_info[i].default_value = new_interned_string(arg_info[i].default_value);
+						}
+					}
+					accel_copy_permanent_list_types(new_interned_string, arg_info[i].type);
+				}
+			}
 		} ZEND_HASH_FOREACH_END();

 		ZEND_HASH_MAP_FOREACH_BUCKET(&ce->constants_table, q) {
diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c
index 9dc6ea1c3b0..10be237d06b 100644
--- a/ext/opcache/jit/zend_jit_trace.c
+++ b/ext/opcache/jit/zend_jit_trace.c
@@ -493,7 +493,7 @@ static bool zend_jit_needs_arg_dtor(const zend_function *func, uint32_t arg_num,
 	 && func->type == ZEND_INTERNAL_FUNCTION
 	 && (func->internal_function.fn_flags & ZEND_ACC_HAS_TYPE_HINTS) != 0
 	 && arg_num < func->internal_function.num_args) {
-		const zend_internal_arg_info *arg_info = &func->internal_function.arg_info[arg_num];
+		const zend_arg_info *arg_info = &func->internal_function.arg_info[arg_num];

 		if (ZEND_ARG_SEND_MODE(arg_info) == ZEND_SEND_BY_VAL
 		 && ZEND_TYPE_IS_SET(arg_info->type)
diff --git a/ext/pdo/pdo_dbh.c b/ext/pdo/pdo_dbh.c
index beb3665c4d9..34f19e364fa 100644
--- a/ext/pdo/pdo_dbh.c
+++ b/ext/pdo/pdo_dbh.c
@@ -1321,6 +1321,7 @@ static void cls_method_dtor(zval *el) /* {{{ */ {
 	if (func->common.attributes) {
 		zend_hash_release(func->common.attributes);
 	}
+	zend_free_internal_arg_info(&func->internal_function, false);
 	efree(func);
 }
 /* }}} */
@@ -1336,6 +1337,7 @@ static void cls_method_pdtor(zval *el) /* {{{ */ {
 	if (func->common.attributes) {
 		zend_hash_release(func->common.attributes);
 	}
+	zend_free_internal_arg_info(&func->internal_function, true);
 	pefree(func, 1);
 }
 /* }}} */
@@ -1408,7 +1410,19 @@ bool pdo_hash_methods(pdo_dbh_object_t *dbh_obj, int kind)
 		if (funcs->arg_info) {
 			zend_internal_function_info *info = (zend_internal_function_info*)funcs->arg_info;

-			func.arg_info = (zend_internal_arg_info*)funcs->arg_info + 1;
+			uint32_t num_arg_info = 1 + funcs->num_args;
+			if (func.fn_flags & ZEND_ACC_VARIADIC) {
+				num_arg_info++;
+			}
+
+			zend_arg_info *arg_info = safe_pemalloc(num_arg_info,
+					sizeof(zend_arg_info), 0, dbh->is_persistent);
+			for (uint32_t i = 0; i < num_arg_info; i++) {
+				zend_convert_internal_arg_info(&arg_info[i],
+						&funcs->arg_info[i], i == 0, dbh->is_persistent);
+			}
+
+			func.arg_info = arg_info + 1;
 			func.num_args = funcs->num_args;
 			if (info->required_num_args == (uint32_t)-1) {
 				func.required_num_args = funcs->num_args;
diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c
index d6e55c982b4..c6f681ee227 100644
--- a/ext/reflection/php_reflection.c
+++ b/ext/reflection/php_reflection.c
@@ -763,11 +763,6 @@ static void format_default_value(smart_str *str, zval *value) {
 	}
 }

-static inline bool has_internal_arg_info(const zend_function *fptr) {
-	return fptr->type == ZEND_INTERNAL_FUNCTION
-		&& !(fptr->common.fn_flags & ZEND_ACC_USER_ARG_INFO);
-}
-
 /* {{{ _parameter_string */
 static void _parameter_string(smart_str *str, zend_function *fptr, struct _zend_arg_info *arg_info, uint32_t offset, bool required, char* indent)
 {
@@ -789,17 +784,15 @@ static void _parameter_string(smart_str *str, zend_function *fptr, struct _zend_
 	if (ZEND_ARG_IS_VARIADIC(arg_info)) {
 		smart_str_appends(str, "...");
 	}
-	smart_str_append_printf(str, "$%s", has_internal_arg_info(fptr)
-		? ((zend_internal_arg_info*)arg_info)->name : ZSTR_VAL(arg_info->name));
+	smart_str_append_printf(str, "$%s", ZSTR_VAL(arg_info->name));

 	if (!required && !ZEND_ARG_IS_VARIADIC(arg_info)) {
 		if (fptr->type == ZEND_INTERNAL_FUNCTION) {
 			smart_str_appends(str, " = ");
 			/* TODO: We don't have a way to fetch the default value for an internal function
 			 * with userland arg info. */
-			if (has_internal_arg_info(fptr)
-					&& ((zend_internal_arg_info*)arg_info)->default_value) {
-				smart_str_appends(str, ((zend_internal_arg_info*)arg_info)->default_value);
+			if (arg_info->default_value) {
+				smart_str_append(str, arg_info->default_value);
 			} else {
 				smart_str_appends(str, "<default>");
 			}
@@ -1463,11 +1456,7 @@ static void reflection_parameter_factory(zend_function *fptr, zval *closure_obje
 	}

 	prop_name = reflection_prop_name(object);
-	if (has_internal_arg_info(fptr)) {
-		ZVAL_STRING(prop_name, ((zend_internal_arg_info*)arg_info)->name);
-	} else {
-		ZVAL_STR_COPY(prop_name, arg_info->name);
-	}
+	ZVAL_STR_COPY(prop_name, arg_info->name);
 }
 /* }}} */

@@ -1653,8 +1642,7 @@ static zend_result get_parameter_default(zval *result, parameter_reference *para
 			/* We don't have a way to determine the default value for this case right now. */
 			return FAILURE;
 		}
-		return zend_get_default_from_internal_arg_info(
-			result, (zend_internal_arg_info *) param->arg_info);
+		return zend_get_default_from_internal_arg_info(result, param->arg_info);
 	} else {
 		zval *default_value = get_default_from_recv((zend_op_array *) param->fptr, param->offset);
 		if (!default_value) {
@@ -2590,22 +2578,11 @@ ZEND_METHOD(ReflectionParameter, __construct)
 		uint32_t i;
 		position = -1;

-		if (has_internal_arg_info(fptr)) {
-			for (i = 0; i < num_args; i++) {
-				if (arg_info[i].name) {
-					if (strcmp(((zend_internal_arg_info*)arg_info)[i].name, ZSTR_VAL(arg_name)) == 0) {
-						position = i;
-						break;
-					}
-				}
-			}
-		} else {
-			for (i = 0; i < num_args; i++) {
-				if (arg_info[i].name) {
-					if (zend_string_equals(arg_name, arg_info[i].name)) {
-						position = i;
-						break;
-					}
+		for (i = 0; i < num_args; i++) {
+			if (arg_info[i].name) {
+				if (zend_string_equals(arg_name, arg_info[i].name)) {
+					position = i;
+					break;
 				}
 			}
 		}
@@ -2646,11 +2623,7 @@ ZEND_METHOD(ReflectionParameter, __construct)

 	prop_name = reflection_prop_name(object);
 	zval_ptr_dtor(prop_name);
-	if (has_internal_arg_info(fptr)) {
-		ZVAL_STRING(prop_name, ((zend_internal_arg_info*)arg_info)[position].name);
-	} else {
-		ZVAL_STR_COPY(prop_name, arg_info[position].name);
-	}
+	ZVAL_STR_COPY(prop_name, arg_info[position].name);
 	return;

 failure:
@@ -2689,11 +2662,7 @@ ZEND_METHOD(ReflectionParameter, getName)
 	ZEND_PARSE_PARAMETERS_NONE();

 	GET_REFLECTION_OBJECT_PTR(param);
-	if (has_internal_arg_info(param->fptr)) {
-		RETURN_STRING(((zend_internal_arg_info *) param->arg_info)->name);
-	} else {
-		RETURN_STR_COPY(param->arg_info->name);
-	}
+	RETURN_STR_COPY(param->arg_info->name);
 }
 /* }}} */

@@ -2947,8 +2916,7 @@ ZEND_METHOD(ReflectionParameter, isDefaultValueAvailable)
 	GET_REFLECTION_OBJECT_PTR(param);

 	if (param->fptr->type == ZEND_INTERNAL_FUNCTION) {
-		RETURN_BOOL(!(param->fptr->common.fn_flags & ZEND_ACC_USER_ARG_INFO)
-			&& ((zend_internal_arg_info*) (param->arg_info))->default_value);
+		RETURN_BOOL(param->arg_info->default_value);
 	} else {
 		zval *default_value = get_default_from_recv((zend_op_array *)param->fptr, param->offset);
 		RETURN_BOOL(default_value != NULL);
diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c
index 4e06b2106ce..fd1a51c5776 100644
--- a/ext/zend_test/test.c
+++ b/ext/zend_test/test.c
@@ -37,10 +37,12 @@
 #include "Zend/Optimizer/zend_optimizer.h"
 #include "Zend/zend_alloc.h"
 #include "test_arginfo.h"
+#include "tmp_methods_arginfo.h"
 #include "zend_call_stack.h"
 #include "zend_exceptions.h"
 #include "zend_mm_custom_handlers.h"
 #include "ext/uri/php_uri.h"
+#include "zend_observer.h"

 #if defined(HAVE_LIBXML) && !defined(PHP_WIN32)
 # include <libxml/globals.h>
@@ -1030,16 +1032,38 @@ static ZEND_FUNCTION(zend_test_log_err_debug)
 	php_log_err_with_severity(ZSTR_VAL(str), LOG_DEBUG);
 }

+typedef struct _zend_test_object {
+	zend_internal_function *tmp_method;
+	zend_object std;
+} zend_test_object;
+
 static zend_object *zend_test_class_new(zend_class_entry *class_type)
 {
-	zend_object *obj = zend_objects_new(class_type);
-	object_properties_init(obj, class_type);
-	obj->handlers = &zend_test_class_handlers;
-	return obj;
+	zend_test_object *intern = zend_object_alloc(sizeof(zend_test_object), class_type);
+	zend_object_std_init(&intern->std, class_type);
+	object_properties_init(&intern->std, class_type);
+	return &intern->std;
+}
+
+static void zend_test_class_free_obj(zend_object *object)
+{
+	zend_test_object *intern = (zend_test_object*)((char*)object - XtOffsetOf(zend_test_object, std));
+
+	if (intern->tmp_method) {
+		zend_internal_function *func = intern->tmp_method;
+		intern->tmp_method = NULL;
+		zend_string_release_ex(func->function_name, 0);
+		zend_free_internal_arg_info(func, false);
+		efree(func);
+	}
+
+	zend_object_std_dtor(object);
 }

 static zend_function *zend_test_class_method_get(zend_object **object, zend_string *name, const zval *key)
 {
+	zend_test_object *intern = (zend_test_object*)((char*)(*object) - XtOffsetOf(zend_test_object, std));
+
 	if (zend_string_equals_literal_ci(name, "test")) {
 		zend_internal_function *fptr;

@@ -1058,6 +1082,41 @@ static zend_function *zend_test_class_method_get(zend_object **object, zend_stri
 		fptr->handler = ZEND_FN(zend_test_func);
 		fptr->doc_comment = NULL;

+		return (zend_function*)fptr;
+	} else if (zend_string_equals_literal_ci(name, "testTmpMethodWithArgInfo")) {
+		if (intern->tmp_method) {
+			return (zend_function*)intern->tmp_method;
+		}
+
+		const zend_function_entry *entry = &class_ZendTestTmpMethods_methods[0];
+		zend_internal_function *fptr = emalloc(sizeof(zend_internal_function));
+		memset(fptr, 0, sizeof(zend_internal_function));
+		fptr->type = ZEND_INTERNAL_FUNCTION;
+		fptr->handler = entry->handler;
+		fptr->function_name = zend_string_init(entry->fname, strlen(entry->fname), false);
+		fptr->scope = intern->std.ce;
+		fptr->prototype = NULL;
+		fptr->T = ZEND_OBSERVER_ENABLED;
+		fptr->fn_flags = ZEND_ACC_PUBLIC | ZEND_ACC_NEVER_CACHE;
+
+		zend_internal_function_info *info = (zend_internal_function_info*)entry->arg_info;
+
+		uint32_t num_arg_info = 1 + entry->num_args;
+		zend_arg_info *arg_info = safe_emalloc(num_arg_info, sizeof(zend_arg_info), 0);
+		for (uint32_t i = 0; i < num_arg_info; i++) {
+			zend_convert_internal_arg_info(&arg_info[i], &entry->arg_info[i], i == 0, false);
+		}
+
+		fptr->arg_info = arg_info + 1;
+		fptr->num_args = entry->num_args;
+		if (info->required_num_args == (uint32_t)-1) {
+			fptr->required_num_args = entry->num_args;
+		} else {
+			fptr->required_num_args = info->required_num_args;
+		}
+
+		intern->tmp_method = fptr;
+
 		return (zend_function*)fptr;
 	}
 	return zend_std_get_method(object, name, key);
@@ -1145,6 +1204,18 @@ static ZEND_METHOD(_ZendTestClass, variadicTest) {
 	object_init_ex(return_value, zend_get_called_scope(execute_data));
 }

+ZEND_METHOD(ZendTestTmpMethods, testTmpMethodWithArgInfo)
+{
+	zend_object *obj;
+	zend_string *str;
+
+	ZEND_PARSE_PARAMETERS_START(0, 2);
+		Z_PARAM_OPTIONAL;
+		Z_PARAM_OBJ_OR_NULL(obj);
+		Z_PARAM_STR(str);
+	ZEND_PARSE_PARAMETERS_END();
+}
+
 static ZEND_METHOD(_ZendTestChildClass, returnsThrowable)
 {
 	ZEND_PARSE_PARAMETERS_NONE();
@@ -1450,11 +1521,14 @@ PHP_MINIT_FUNCTION(zend_test)
 	register_ZendTestClass_dnf_property(zend_test_class);
 	zend_test_class->create_object = zend_test_class_new;
 	zend_test_class->get_static_method = zend_test_class_static_method_get;
+	zend_test_class->default_object_handlers = &zend_test_class_handlers;

 	zend_test_child_class = register_class__ZendTestChildClass(zend_test_class);

 	memcpy(&zend_test_class_handlers, &std_object_handlers, sizeof(zend_object_handlers));
 	zend_test_class_handlers.get_method = zend_test_class_method_get;
+	zend_test_class_handlers.free_obj = zend_test_class_free_obj;
+	zend_test_class_handlers.offset = XtOffsetOf(zend_test_object, std);

 	zend_test_gen_stub_flag_compatibility_test = register_class_ZendTestGenStubFlagCompatibilityTest();

diff --git a/ext/zend_test/tmp_methods.stub.php b/ext/zend_test/tmp_methods.stub.php
new file mode 100644
index 00000000000..af479c7d542
--- /dev/null
+++ b/ext/zend_test/tmp_methods.stub.php
@@ -0,0 +1,11 @@
+<?php
+
+/** @generate-function-entries */
+
+/**
+ * These methods will be added to a class at runtime
+ * @undocumentable
+ */
+class ZendTestTmpMethods {
+    public function testTmpMethodWithArgInfo(Foo|Bar|null $tmpMethodParamName = null, string $tmpMethodParamWithStringDefaultValue = "tmpMethodParamWithStringDefaultValue"): void {}
+}
diff --git a/ext/zend_test/tmp_methods_arginfo.h b/ext/zend_test/tmp_methods_arginfo.h
new file mode 100644
index 00000000000..92598a34ad8
Binary files /dev/null and b/ext/zend_test/tmp_methods_arginfo.h differ
diff --git a/sapi/phpdbg/phpdbg_frame.c b/sapi/phpdbg/phpdbg_frame.c
index a8f1bf01433..01b19de80a9 100644
--- a/sapi/phpdbg/phpdbg_frame.c
+++ b/sapi/phpdbg/phpdbg_frame.c
@@ -34,11 +34,7 @@ static inline void phpdbg_append_individual_arg(smart_str *s, uint32_t i, zend_f
 	}
 	if (i < func->common.num_args) {
 		if (arginfo) {
-			if (func->type == ZEND_INTERNAL_FUNCTION) {
-				arg_name = (char *) ((zend_internal_arg_info *) &arginfo[i])->name;
-			} else {
-				arg_name = ZSTR_VAL(arginfo[i].name);
-			}
+			arg_name = ZSTR_VAL(arginfo[i].name);
 		}
 		smart_str_appends(s, arg_name ? arg_name : "?");
 		smart_str_appendc(s, '=');
@@ -213,11 +209,7 @@ static void phpdbg_dump_prototype(zval *tmp) /* {{{ */
 				char *arg_name = NULL;

 				if (arginfo) {
-					if (func->type == ZEND_INTERNAL_FUNCTION) {
-						arg_name = (char *)((zend_internal_arg_info *)&arginfo[j])->name;
-					} else {
-						arg_name = ZSTR_VAL(arginfo[j].name);
-					}
+					arg_name = ZSTR_VAL(arginfo[j].name);
 				}

 				if (!is_variadic) {