From 20b1436365ba0cc93774f1cb414bb2b1140d1673 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 17 Jul 2026 09:43:26 -0400 Subject: [PATCH] Check partial-application bound arguments in the creating scope 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. --- .../bound_arg_strict_types.phpt | 26 +++++++++++++++++++ Zend/zend_execute.c | 18 ++++++------- Zend/zend_execute.h | 4 +-- Zend/zend_partial.c | 10 +++++-- ext/opcache/jit/zend_jit_helpers.c | 4 +-- 5 files changed, 47 insertions(+), 15 deletions(-) create mode 100644 Zend/tests/partial_application/bound_arg_strict_types.phpt 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 000000000000..d7e2ddcae5c4 --- /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-- +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 c51a55bac4c9..f5b70d74aa61 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 48f7e7a72530..eb73b3f3de9e 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 4095daa1f1a3..c6ab44830dde 100644 --- a/Zend/zend_partial.c +++ b/Zend/zend_partial.c @@ -1098,8 +1098,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); return; diff --git a/ext/opcache/jit/zend_jit_helpers.c b/ext/opcache/jit/zend_jit_helpers.c index 64a48068f378..660a17c05687 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); } }