From b1a471aa018a9fdf23fbc9e98e5db089963a77bf Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 20 Jul 2026 06:56:13 -0400 Subject: [PATCH 1/5] 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. --- .../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 2b5b5f4d3b44..ef335cd805c3 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 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); } } From d37a820d59fa2a870f90f52fc545c27f734525ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Mon, 20 Jul 2026 12:58:40 +0200 Subject: [PATCH 2/5] zend_string: Add `zend_string_ends_with*()` (#22819) --- UPGRADING.INTERNALS | 1 + Zend/zend_string.h | 26 ++++++++++++++++++++++++++ ext/standard/string.c | 8 +------- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index 79d5def33823..ae58e7ba8d5b 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -145,6 +145,7 @@ PHP 8.6 INTERNALS UPGRADE NOTES (sendfile, splice, copy_file_range, TransmitFile), and is now used by php_stream_copy_to_stream_ex(). The mmap-based copy fallback was removed. . Added zend_string_equals_cstr_ci(). + . Added zend_string_ends_with() and related variants. ======================== 2. Build system changes diff --git a/Zend/zend_string.h b/Zend/zend_string.h index cb0502891efa..d9efea00e7bd 100644 --- a/Zend/zend_string.h +++ b/Zend/zend_string.h @@ -493,6 +493,32 @@ static zend_always_inline bool zend_string_starts_with_ci(const zend_string *str #define zend_string_starts_with_literal_ci(str, prefix) \ zend_string_starts_with_cstr_ci(str, "" prefix, sizeof(prefix) - 1) +static zend_always_inline bool zend_string_ends_with_cstr(const zend_string *str, const char *suffix, size_t suffix_length) +{ + return ZSTR_LEN(str) >= suffix_length && !memcmp(ZSTR_VAL(str) + ZSTR_LEN(str) - suffix_length, suffix, suffix_length); +} + +static zend_always_inline bool zend_string_ends_with(const zend_string *str, const zend_string *suffix) +{ + return zend_string_ends_with_cstr(str, ZSTR_VAL(suffix), ZSTR_LEN(suffix)); +} + +#define zend_string_ends_with_literal(str, suffix) \ + zend_string_ends_with_cstr(str, "" suffix, sizeof(suffix) - 1) + +static zend_always_inline bool zend_string_ends_with_cstr_ci(const zend_string *str, const char *suffix, size_t suffix_length) +{ + return ZSTR_LEN(str) >= suffix_length && !strncasecmp(ZSTR_VAL(str) + ZSTR_LEN(str) - suffix_length, suffix, suffix_length); +} + +static zend_always_inline bool zend_string_ends_with_ci(const zend_string *str, const zend_string *suffix) +{ + return zend_string_ends_with_cstr_ci(str, ZSTR_VAL(suffix), ZSTR_LEN(suffix)); +} + +#define zend_string_ends_with_literal_ci(str, suffix) \ + zend_string_ends_with_cstr_ci(str, "" suffix, sizeof(suffix) - 1) + /* * DJBX33A (Daniel J. Bernstein, Times 33 with Addition) * diff --git a/ext/standard/string.c b/ext/standard/string.c index c6e89dcca2e5..823c32a8cf1e 100644 --- a/ext/standard/string.c +++ b/ext/standard/string.c @@ -1861,13 +1861,7 @@ PHP_FUNCTION(str_ends_with) Z_PARAM_STR(needle) ZEND_PARSE_PARAMETERS_END(); - if (ZSTR_LEN(needle) > ZSTR_LEN(haystack)) { - RETURN_FALSE; - } - - RETURN_BOOL(memcmp( - ZSTR_VAL(haystack) + ZSTR_LEN(haystack) - ZSTR_LEN(needle), - ZSTR_VAL(needle), ZSTR_LEN(needle)) == 0); + RETURN_BOOL(zend_string_ends_with(haystack, needle)); } /* }}} */ From af43113e1e81e172aa7d53141efd44ef3639d651 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Mon, 20 Jul 2026 15:29:02 +0200 Subject: [PATCH 3/5] Remove compat hack for curl_httppost (#22821) HttpPost was renamed to curl_httppost in curl 7.9.6. Since the minimum version for PHP is 7.61.0, we can safely depend on curl_httppost. https://github.com/curl/curl/commit/579985242455741d34df17eea94174772bbe4e32 --- ext/curl/interface.c | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ext/curl/interface.c b/ext/curl/interface.c index 2571eb60e42d..d5d20d825652 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -32,11 +32,6 @@ #include #include -/* As of curl 7.11.1 this is no longer defined inside curl.h */ -#ifndef HttpPost -#define HttpPost curl_httppost -#endif - #include "zend_smart_str.h" #include "ext/standard/info.h" #include "ext/standard/file.h" @@ -1110,7 +1105,7 @@ void init_curl_handle(php_curl *ch) memset(&ch->err, 0, sizeof(struct _php_curl_error)); - zend_llist_init(&ch->to_free->post, sizeof(struct HttpPost *), (llist_dtor_func_t)curl_free_post, 0); + zend_llist_init(&ch->to_free->post, sizeof(struct curl_httppost *), (llist_dtor_func_t)curl_free_post, 0); zend_llist_init(&ch->to_free->stream, sizeof(struct mime_data_cb_arg *), (llist_dtor_func_t)curl_free_cb_arg, 0); zend_hash_init(&ch->to_free->slist, 4, NULL, curl_free_slist, 0); From 55183b9d127bb8eba2d5e4b13e0c1536ff371d30 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Mon, 20 Jul 2026 14:32:29 +0100 Subject: [PATCH 4/5] poll: fetch method ptr directly (#22816) Rather than going through a whole callability check via call_user_function() we can grab the known method pointer and call it directly. This removes some allocations --- main/poll/poll_handle.c | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/main/poll/poll_handle.c b/main/poll/poll_handle.c index 0c0628ac49dc..228d44e1e420 100644 --- a/main/poll/poll_handle.c +++ b/main/poll/poll_handle.c @@ -19,26 +19,22 @@ static php_socket_t php_poll_handle_default_get_fd(php_poll_handle_object *handle) { zval retval; - zval obj; - zval func_name; - ZVAL_OBJ(&obj, &handle->std); - - /* Prepare function name as zval */ - ZVAL_STRING(&func_name, "getFileDescriptor"); + /* Grab getFileDescriptor() method pointer which is stored in lowercase in the function table */ + zend_function *method = zend_hash_str_find_ptr_lc(&handle->std.ce->function_table, ZEND_STRL("getfiledescriptor")); + ZEND_ASSERT(method && "no default method???"); /* Call getFileDescriptor() method */ - if (EXPECTED(call_user_function(NULL, &obj, &func_name, &retval, 0, NULL) == SUCCESS)) { - if (Z_TYPE(retval) == IS_LONG) { - php_socket_t fd = Z_LVAL(retval) < 0 ? SOCK_ERR : (php_socket_t) Z_LVAL(retval); - zval_ptr_dtor(&retval); - zval_ptr_dtor(&func_name); /* Clean up function name */ - return fd; - } - zval_ptr_dtor(&retval); + zend_call_known_function(method, &handle->std, handle->std.ce, &retval, 0, NULL, NULL); + + /* No need to deref the return value as the class is final and thus the method cannot be changed to return by-ref */ + if (EXPECTED(Z_TYPE(retval) == IS_LONG)) { + php_socket_t fd = Z_LVAL(retval) < 0 ? SOCK_ERR : (php_socket_t) Z_LVAL(retval); + /* No need to clean the retval as we know it is an integer, and thus it's just on the stack */ + return fd; } - zval_ptr_dtor(&func_name); /* Clean up function name */ + zval_ptr_dtor(&retval); return SOCK_ERR; /* Invalid socket */ } From 94e8c54ef27877ab58bece03b8b260a4f32a518f Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Mon, 20 Jul 2026 21:55:54 +0800 Subject: [PATCH 5/5] ext/intl: Fix various error messages (#22828) Fixed grammatical issues in Normalizer invalid form and IntlCalendar time zone offset error messages. --- NEWS | 4 ++++ ext/intl/common/common_date.cpp | 2 +- ext/intl/normalizer/normalizer_normalize.cpp | 4 ++-- ext/intl/tests/calendar_fromDateTime_error.phpt | 2 +- ext/intl/tests/calendar_setTimeZone_error2.phpt | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/NEWS b/NEWS index af7d45ecd087..7dd8530af905 100644 --- a/NEWS +++ b/NEWS @@ -11,6 +11,10 @@ PHP NEWS . Fixed GMP integer string parsing to reject strings containing NUL bytes instead of silently truncating them. (Weilin Du) +- Intl: + . Fixed grammatical issues in Normalizer invalid form and IntlCalendar time + zone offset error messages. (Weilin Du) + - ODBC: . Fixed bug GH-22668 (Heap buffer over-read when a column value exceeds the driver-reported display size). (iliaal) diff --git a/ext/intl/common/common_date.cpp b/ext/intl/common/common_date.cpp index ed18bba70b9f..c9eb45b0f2d9 100644 --- a/ext/intl/common/common_date.cpp +++ b/ext/intl/common/common_date.cpp @@ -57,7 +57,7 @@ U_CFUNC TimeZone *timezone_convert_datetimezone( if (UNEXPECTED(offset_mins <= -24 * 60 || offset_mins >= 24 * 60)) { intl_errors_set(outside_error, U_ILLEGAL_ARGUMENT_ERROR, - "object has an time zone offset that's too large"); + "object has a time zone offset that is too large"); return NULL; } diff --git a/ext/intl/normalizer/normalizer_normalize.cpp b/ext/intl/normalizer/normalizer_normalize.cpp index 8a77d37ad4d4..e5b8db14bafb 100644 --- a/ext/intl/normalizer/normalizer_normalize.cpp +++ b/ext/intl/normalizer/normalizer_normalize.cpp @@ -115,7 +115,7 @@ U_CFUNC PHP_FUNCTION( normalizer_normalize ) case NORMALIZER_FORM_KC_CF: break; default: - zend_argument_value_error(2, "must be a a valid normalization form"); + zend_argument_value_error(2, "must be a valid normalization form"); RETURN_THROWS(); } @@ -232,7 +232,7 @@ U_CFUNC PHP_FUNCTION( normalizer_is_normalized ) case NORMALIZER_FORM_KC_CF: break; default: - zend_argument_value_error(2, "must be a a valid normalization form"); + zend_argument_value_error(2, "must be a valid normalization form"); RETURN_THROWS(); } diff --git a/ext/intl/tests/calendar_fromDateTime_error.phpt b/ext/intl/tests/calendar_fromDateTime_error.phpt index b0a6a6d0fe0c..33d9f9cf1259 100644 --- a/ext/intl/tests/calendar_fromDateTime_error.phpt +++ b/ext/intl/tests/calendar_fromDateTime_error.phpt @@ -35,6 +35,6 @@ DateMalformedStringException: Failed to parse time string (foobar) at position 0 NULL string(88) "IntlCalendar::fromDateTime(): DateTime object is unconstructed: U_ILLEGAL_ARGUMENT_ERROR" NULL -string(103) "IntlCalendar::fromDateTime(): object has an time zone offset that's too large: U_ILLEGAL_ARGUMENT_ERROR" +string(103) "IntlCalendar::fromDateTime(): object has a time zone offset that is too large: U_ILLEGAL_ARGUMENT_ERROR" NULL string(127) "IntlCalendar::fromDateTime(): time zone id 'WEST' extracted from ext/date DateTimeZone not recognized: U_ILLEGAL_ARGUMENT_ERROR" diff --git a/ext/intl/tests/calendar_setTimeZone_error2.phpt b/ext/intl/tests/calendar_setTimeZone_error2.phpt index 6b65aa1a0e7c..277311e0c6b6 100644 --- a/ext/intl/tests/calendar_setTimeZone_error2.phpt +++ b/ext/intl/tests/calendar_setTimeZone_error2.phpt @@ -25,5 +25,5 @@ bool(false) string(126) "IntlCalendar::setTimeZone(): time zone id 'WEST' extracted from ext/date DateTimeZone not recognized: U_ILLEGAL_ARGUMENT_ERROR" string(16) "Europe/Amsterdam" bool(false) -string(102) "IntlCalendar::setTimeZone(): object has an time zone offset that's too large: U_ILLEGAL_ARGUMENT_ERROR" +string(102) "IntlCalendar::setTimeZone(): object has a time zone offset that is too large: U_ILLEGAL_ARGUMENT_ERROR" string(16) "Europe/Amsterdam"