[PHP-CVS] [php-src] master: PFA: Fix strict_types checking when a const arg is inlined (#23256)

[email protected] (Arnaud Le Blanc via GitHub)
Newsgroups php.cvs
Message-ID <[email protected]>
Author: Arnaud Le Blanc (arnaud-lb)
Committer: GitHub (web-flow)
Pusher: arnaud-lb
Date: 2026-08-14T16:20:03+02:00

Commit: https://github.com/php/php-src/commit/8b00475b1806d575ab3bd55abab971a89034e33c
Raw diff: https://github.com/php/php-src/commit/8b00475b1806d575ab3bd55abab971a89034e33c.diff

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.

Changed paths:
  A  Zend/tests/partial_application/const_arg_opt_002.phpt
  A  Zend/tests/partial_application/const_arg_opt_003.phpt
  M  Zend/zend_partial.c


Diff:

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 000000000000..5f3351276673
--- /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 000000000000..0841bff9e6f0
--- /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 b50512c238ff..ce1604ddc67a 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,
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.