Commit 8b00475b180 for php.net

commit 8b00475b1806d575ab3bd55abab971a89034e33c
Author: Arnaud Le Blanc <365207+arnaud-lb@users.noreply.github.com>
Date:   Fri Aug 14 16:20:03 2026 +0200

    PFA: Fix strict_types checking when a const arg is inlined (#23256)

    GH-22829 added an optimization to burn literal arguments into the generated closure. An overlooked side effect is that these arguments are not checked anymore by zp_bind() (GH-22789).

    Fix by checking these arguments earlier.

    Bug found by Ryan @ Calif.io.

diff --git a/Zend/tests/partial_application/const_arg_opt_002.phpt b/Zend/tests/partial_application/const_arg_opt_002.phpt
new file mode 100644
index 00000000000..5f335127667
--- /dev/null
+++ b/Zend/tests/partial_application/const_arg_opt_002.phpt
@@ -0,0 +1,42 @@
+--TEST--
+Constant argument optimization - strict_types bug
+--CREDITS--
+Ryan @ Calif.io
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.optimization_level=-1
+opcache.file_update_protection=0
+--FILE--
+<?php
+
+declare(strict_types=1);
+
+function cand_86014_typed(int $bound, mixed $placeholder): void
+{
+    echo 'CALLED:', get_debug_type($bound), ':', $bound, "\n";
+}
+
+function cand_86014_make_invalid(): Closure
+{
+    return cand_86014_typed('123', ?);
+}
+
+try {
+    $partial = cand_86014_make_invalid();
+    echo "CONSTRUCTED\n";
+} catch (Throwable $e) {
+    echo 'CREATE: ', get_class($e), ': ', $e->getMessage(), "\n";
+}
+
+if (isset($partial)) {
+    try {
+        $partial(null);
+    } catch (Throwable $e) {
+        echo 'CALL: ', get_class($e), ': ', $e->getMessage(), "\n";
+    }
+}
+
+?>
+--EXPECT--
+CREATE: TypeError: cand_86014_typed(): Argument #1 ($bound) must be of type int, string given
diff --git a/Zend/tests/partial_application/const_arg_opt_003.phpt b/Zend/tests/partial_application/const_arg_opt_003.phpt
new file mode 100644
index 00000000000..0841bff9e6f
--- /dev/null
+++ b/Zend/tests/partial_application/const_arg_opt_003.phpt
@@ -0,0 +1,32 @@
+--TEST--
+Constant argument optimization - strict_types
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.optimization_level=-1
+opcache.file_update_protection=0
+--FILE--
+<?php
+declare(strict_types=1);
+
+function f(int $a, $b) { return $a; }
+
+// Check that optimizations are enabled
+$f = f(3, ?);
+$usedVars = new ReflectionFunction($f)->getClosureUsedVariables();
+if ($usedVars !== []) {
+    echo "const arg optimization was not applied\n";
+    var_dump($usedVars);
+    exit;
+}
+
+var_dump($f(0));
+
+// Actual test. Not catching this as it disables optimizations
+f("3", ?)(0);
+
+?>
+--EXPECTF--
+int(3)
+
+Fatal error: Uncaught TypeError: f(): Argument #1 ($a) must be of type int, string given in %a
diff --git a/Zend/zend_partial.c b/Zend/zend_partial.c
index b50512c238f..ce1604ddc67 100644
--- a/Zend/zend_partial.c
+++ b/Zend/zend_partial.c
@@ -561,6 +561,30 @@ static zend_ast *zp_compile_forwarding_call(
 			args_ast = zend_ast_list_add(args_ast, default_value_ast);
 		} else if (zp_is_const_arg(const_args, offset)) {
 			ZEND_ASSERT(Z_TYPE(argv[offset]) < IS_OBJECT);
+
+			/* This argument never changes, so we can burn it into the op_array
+			 * and check its type ahead of time. */
+
+			zend_arg_info *arg_info;
+			if (offset < function->common.num_args) {
+				arg_info = &function->common.arg_info[offset];
+			} else if (function->common.fn_flags & ZEND_ACC_VARIADIC) {
+				arg_info = &function->common.arg_info[function->common.num_args];
+			} else {
+				arg_info = NULL;
+			}
+			if (arg_info && ZEND_TYPE_IS_SET(arg_info->type)
+					&& UNEXPECTED(!zend_check_type_ex(&arg_info->type, &argv[offset],
+						/* current_frame */ true, /* is_internal */ false))) {
+				zend_string *need_msg = zend_type_to_string_resolved(arg_info->type,
+						function->common.scope);
+				zend_argument_type_error_ex(function, offset + 1,
+						"must be of type %s, %s given",
+						ZSTR_VAL(need_msg), zend_zval_value_name(&argv[offset]));
+				zend_string_release(need_msg);
+				goto error;
+			}
+
 			args_ast = zend_ast_list_add(args_ast, zend_ast_create_zval(&argv[offset]));
 		} else {
 			args_ast = zend_ast_list_add(args_ast, zend_ast_create(ZEND_AST_VAR,