Commit b1a471aa018 for php.net
commit b1a471aa018a9fdf23fbc9e98e5db089963a77bf
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Mon Jul 20 06:56:13 2026 -0400
Check partial-application bound arguments in the creating scope (#22789)
Bound-argument type checks in zp_bind() resolved strict_types from
ZEND_ARG_USES_STRICT_TYPES(), which reads the caller of the frame
creating the partial rather than the file where the partial is written,
so the same f("3", ?) threw at one call depth and silently coerced at
another, and the TypeError named the enclosing function instead of the
target. Resolve the check against the creating frame's strict_types and
attribute the error to the target function.
diff --git a/Zend/tests/partial_application/bound_arg_strict_types.phpt b/Zend/tests/partial_application/bound_arg_strict_types.phpt
new file mode 100644
index 00000000000..d7e2ddcae5c
--- /dev/null
+++ b/Zend/tests/partial_application/bound_arg_strict_types.phpt
@@ -0,0 +1,26 @@
+--TEST--
+Partial application checks bound arguments with the creating scope's strict_types
+--FILE--
+<?php
+declare(strict_types=1);
+
+function f(int $a, $b) { return $a; }
+
+try {
+ f("3", ?);
+} catch (\TypeError $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+function make() {
+ try {
+ f("3", ?);
+ } catch (\TypeError $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+ }
+}
+make();
+?>
+--EXPECT--
+TypeError: f(): Argument #1 ($a) must be of type int, string given
+TypeError: f(): Argument #1 ($a) must be of type int, string given
diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c
index c51a55bac4c..f5b70d74aa6 100644
--- a/Zend/zend_execute.c
+++ b/Zend/zend_execute.c
@@ -1153,7 +1153,7 @@ static bool zend_check_intersection_type_from_list(
static zend_always_inline bool zend_check_type_slow(
const zend_type *type, zval *arg, const zend_reference *ref,
- bool is_return_type, bool is_internal)
+ bool current_frame, bool is_internal)
{
if (ZEND_TYPE_IS_COMPLEX(*type) && EXPECTED(Z_TYPE_P(arg) == IS_OBJECT)) {
zend_class_entry *ce;
@@ -1199,7 +1199,7 @@ static zend_always_inline bool zend_check_type_slow(
/* We cannot have conversions for typed refs. */
return 0;
}
- if (is_internal && is_return_type) {
+ if (is_internal && current_frame) {
/* For internal returns, the type has to match exactly, because we're not
* going to check it for non-debug builds, and there will be no chance to
* apply coercions. */
@@ -1207,7 +1207,7 @@ static zend_always_inline bool zend_check_type_slow(
}
return zend_verify_scalar_type_hint(type_mask, arg,
- is_return_type ? ZEND_RET_USES_STRICT_TYPES() : ZEND_ARG_USES_STRICT_TYPES(),
+ current_frame ? ZEND_RET_USES_STRICT_TYPES() : ZEND_ARG_USES_STRICT_TYPES(),
is_internal);
/* Special handling for IS_VOID is not necessary (for return types),
@@ -1215,7 +1215,7 @@ static zend_always_inline bool zend_check_type_slow(
}
static zend_always_inline bool zend_check_type(
- const zend_type *type, zval *arg, bool is_return_type, bool is_internal)
+ const zend_type *type, zval *arg, bool current_frame, bool is_internal)
{
const zend_reference *ref = NULL;
ZEND_ASSERT(ZEND_TYPE_IS_SET(*type));
@@ -1229,21 +1229,21 @@ static zend_always_inline bool zend_check_type(
return 1;
}
- return zend_check_type_slow(type, arg, ref, is_return_type, is_internal);
+ return zend_check_type_slow(type, arg, ref, current_frame, is_internal);
}
/* We can not expose zend_check_type() directly because it's inline and uses static functions */
ZEND_API bool zend_check_type_ex(
- const zend_type *type, zval *arg, bool is_return_type, bool is_internal)
+ const zend_type *type, zval *arg, bool current_frame, bool is_internal)
{
- return zend_check_type(type, arg, is_return_type, is_internal);
+ return zend_check_type(type, arg, current_frame, is_internal);
}
ZEND_API bool zend_check_user_type_slow(
- const zend_type *type, zval *arg, const zend_reference *ref, bool is_return_type)
+ const zend_type *type, zval *arg, const zend_reference *ref, bool current_frame)
{
return zend_check_type_slow(
- type, arg, ref, is_return_type, /* is_internal */ false);
+ type, arg, ref, current_frame, /* is_internal */ false);
}
static zend_always_inline bool zend_verify_recv_arg_type(const zend_function *zf, uint32_t arg_num, zval *arg)
diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h
index 48f7e7a7253..eb73b3f3de9 100644
--- a/Zend/zend_execute.h
+++ b/Zend/zend_execute.h
@@ -108,9 +108,9 @@ ZEND_API zend_never_inline ZEND_COLD void zend_verify_never_error(
const zend_function *zf);
ZEND_API bool zend_verify_ref_array_assignable(zend_reference *ref);
ZEND_API bool zend_check_user_type_slow(
- const zend_type *type, zval *arg, const zend_reference *ref, bool is_return_type);
+ const zend_type *type, zval *arg, const zend_reference *ref, bool current_frame);
ZEND_API bool zend_check_type_ex(
- const zend_type *type, zval *arg, bool is_return_type, bool is_internal);
+ const zend_type *type, zval *arg, bool current_frame, bool is_internal);
#if ZEND_DEBUG
ZEND_API bool zend_internal_call_should_throw(const zend_function *fbc, zend_execute_data *call);
diff --git a/Zend/zend_partial.c b/Zend/zend_partial.c
index 2b5b5f4d3b4..ef335cd805c 100644
--- a/Zend/zend_partial.c
+++ b/Zend/zend_partial.c
@@ -1105,8 +1105,14 @@ static void zp_bind(zval *result, zend_function *function, uint32_t argc, zval *
arg_info = NULL;
}
if (arg_info && ZEND_TYPE_IS_SET(arg_info->type)
- && UNEXPECTED(!zend_check_type_ex(&arg_info->type, var, 0, 0))) {
- zend_verify_arg_error(function, arg_info, offset+1, var);
+ && UNEXPECTED(!zend_check_type_ex(&arg_info->type, var,
+ /* 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(var));
+ zend_string_release(need_msg);
zval_ptr_dtor(result);
ZVAL_NULL(result);
zp_free_unbound_args(offset, argc, argv);
diff --git a/ext/opcache/jit/zend_jit_helpers.c b/ext/opcache/jit/zend_jit_helpers.c
index 64a48068f37..660a17c0568 100644
--- a/ext/opcache/jit/zend_jit_helpers.c
+++ b/ext/opcache/jit/zend_jit_helpers.c
@@ -1974,7 +1974,7 @@ static bool ZEND_FASTCALL zend_jit_verify_arg_slow(zval *arg, zend_arg_info *arg
zend_execute_data *execute_data = EG(current_execute_data);
const zend_op *opline = EX(opline);
bool ret = zend_check_user_type_slow(
- &arg_info->type, arg, /* ref */ NULL, /* is_return_type */ false);
+ &arg_info->type, arg, /* ref */ NULL, /* current_frame */ false);
if (UNEXPECTED(!ret)) {
zend_verify_arg_error(EX(func), arg_info, opline->op1.num, arg);
return false;
@@ -1991,7 +1991,7 @@ static void ZEND_FASTCALL zend_jit_verify_return_slow(zval *arg, const zend_op_a
}
}
if (UNEXPECTED(!zend_check_user_type_slow(
- &arg_info->type, arg, /* ref */ NULL, /* is_return_type */ true))) {
+ &arg_info->type, arg, /* ref */ NULL, /* current_frame */ true))) {
zend_verify_return_error((zend_function*)op_array, arg);
}
}