From 9ce51dad8bc0847f64b32fbbe623d79d49f6a5f0 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Mon, 21 Jul 2025 22:48:15 +0200 Subject: [PATCH 01/12] Add missing hooks JIT restart code Closes GH-19207. --- NEWS | 1 + ext/opcache/jit/zend_jit.c | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/NEWS b/NEWS index 409c613f6ebb0..aa2961a6ec352 100644 --- a/NEWS +++ b/NEWS @@ -46,6 +46,7 @@ PHP NEWS - Opcache: . Reset global pointers to prevent use-after-free in zend_jit_status(). (Florian Engelhardt) + . Fix issue with JIT restart and hooks. (nielsdos) - OpenSSL: . Fixed bug GH-18986 (OpenSSL backend: incorrect RAND_{load,write}_file() diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c index fd1203f086ee1..b0423dc06bd1e 100644 --- a/ext/opcache/jit/zend_jit.c +++ b/ext/opcache/jit/zend_jit.c @@ -3836,6 +3836,24 @@ static void zend_jit_restart_preloaded_script(zend_persistent_script *script) zend_jit_restart_preloaded_op_array(op_array); } } ZEND_HASH_FOREACH_END(); + + if (ce->num_hooked_props > 0) { + zend_property_info *prop; + + ZEND_HASH_MAP_FOREACH_PTR(&ce->properties_info, prop) { + if (prop->hooks) { + for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) { + if (prop->hooks[i]) { + op_array = &prop->hooks[i]->op_array; + ZEND_ASSERT(op_array->type == ZEND_USER_FUNCTION); + if (!(op_array->fn_flags & ZEND_ACC_TRAIT_CLONE)) { + zend_jit_restart_preloaded_op_array(op_array); + } + } + } + } + } ZEND_HASH_FOREACH_END(); + } } ZEND_HASH_FOREACH_END(); } From 771bfaf34d6de1260ee9b02ff7145ea4fc081301 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Mon, 21 Jul 2025 22:44:03 +0200 Subject: [PATCH 02/12] Remove dynamic defs from property hooks Otherwise this hits an assertion failure in pass2 reversal and causes a subsequent crash. Closes GH-19206. --- NEWS | 1 + ext/opcache/ZendAccelerator.c | 18 ++++++++++++++++++ .../tests/preload_dynamic_def_removal.inc | 10 ++++++++++ .../tests/preload_dynamic_def_removal.phpt | 2 ++ 4 files changed, 31 insertions(+) diff --git a/NEWS b/NEWS index aa2961a6ec352..84006fc58f2cd 100644 --- a/NEWS +++ b/NEWS @@ -47,6 +47,7 @@ PHP NEWS . Reset global pointers to prevent use-after-free in zend_jit_status(). (Florian Engelhardt) . Fix issue with JIT restart and hooks. (nielsdos) + . Fix crash with dynamic function defs in hooks during preload. (nielsdos) - OpenSSL: . Fixed bug GH-18986 (OpenSSL backend: incorrect RAND_{load,write}_file() diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c index 83dedb1e74272..1e661314f2044 100644 --- a/ext/opcache/ZendAccelerator.c +++ b/ext/opcache/ZendAccelerator.c @@ -4132,6 +4132,24 @@ static void preload_link(void) preload_remove_declares(op_array); } } ZEND_HASH_FOREACH_END(); + + if (ce->num_hooked_props > 0) { + zend_property_info *info; + + ZEND_HASH_MAP_FOREACH_PTR(&ce->properties_info, info) { + if (info->hooks) { + for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) { + if (info->hooks[i]) { + op_array = &info->hooks[i]->op_array; + ZEND_ASSERT(op_array->type == ZEND_USER_FUNCTION); + if (!(op_array->fn_flags & ZEND_ACC_TRAIT_CLONE)) { + preload_remove_declares(op_array); + } + } + } + } + } ZEND_HASH_FOREACH_END(); + } } ZEND_HASH_FOREACH_END(); } diff --git a/ext/opcache/tests/preload_dynamic_def_removal.inc b/ext/opcache/tests/preload_dynamic_def_removal.inc index 27ec69120eac5..2a6a44ef509a2 100644 --- a/ext/opcache/tests/preload_dynamic_def_removal.inc +++ b/ext/opcache/tests/preload_dynamic_def_removal.inc @@ -5,6 +5,15 @@ class Test { echo "dynamic\n"; } } + + public int $hook { + get { + function dynamic_in_hook() { + echo "dynamic in hook\n"; + } + return 1; + } + } } function func() { @@ -16,3 +25,4 @@ function func() { $test = new Test; $test->method(); func(); +$test->hook; diff --git a/ext/opcache/tests/preload_dynamic_def_removal.phpt b/ext/opcache/tests/preload_dynamic_def_removal.phpt index d4f2bb070ccd5..acc26873e1867 100644 --- a/ext/opcache/tests/preload_dynamic_def_removal.phpt +++ b/ext/opcache/tests/preload_dynamic_def_removal.phpt @@ -15,7 +15,9 @@ if (PHP_OS_FAMILY == 'Windows') die('skip Preloading is not supported on Windows --EXPECT-- dynamic dynamic2 +dynamic in hook From dad28a30f3b4cfa3734734d22a24286b694f348e Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 31 Jul 2025 19:56:38 +0100 Subject: [PATCH 03/12] main: pack _php_netstream_data_t and use bool instead of int type (#19331) Fix use sites at the same time --- ext/openssl/xp_ssl.c | 28 ++++++++++++++-------------- main/network.c | 2 +- main/php_network.h | 4 ++-- main/streams/xp_socket.c | 14 +++++++------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index d53e55aa15547..65d28f210c5f7 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -237,7 +237,7 @@ static int php_openssl_handle_ssl_error(php_stream *stream, int nr_bytes, bool i char esbuf[512]; smart_str ebuf = {0}; unsigned long ecode; - int retry = 1; + bool retry = true; switch(err) { case SSL_ERROR_ZERO_RETURN: @@ -249,7 +249,7 @@ static int php_openssl_handle_ssl_error(php_stream *stream, int nr_bytes, bool i /* re-negotiation, or perhaps the SSL layer needs more * packets: retry in next iteration */ errno = EAGAIN; - retry = is_init ? 1 : sslsock->s.is_blocked; + retry = is_init ? true : sslsock->s.is_blocked; break; case SSL_ERROR_SYSCALL: if (ERR_peek_error() == 0) { @@ -1806,7 +1806,7 @@ static int php_openssl_enable_crypto(php_stream *stream, if (cparam->inputs.activate && !sslsock->ssl_active) { struct timeval start_time, *timeout; - int blocked = sslsock->s.is_blocked, has_timeout = 0; + bool blocked = sslsock->s.is_blocked, has_timeout = false; #ifdef HAVE_TLS_SNI if (sslsock->is_client) { @@ -1946,8 +1946,8 @@ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, si int retry = 1; struct timeval start_time; struct timeval *timeout = NULL; - int began_blocked = sslsock->s.is_blocked; - int has_timeout = 0; + bool began_blocked = sslsock->s.is_blocked; + bool has_timeout = false; int nr_bytes = 0; /* prevent overflow in openssl */ @@ -1965,7 +1965,7 @@ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, si } if (!sslsock->s.is_blocked && timeout && (timeout->tv_sec > 0 || (timeout->tv_sec == 0 && timeout->tv_usec))) { - has_timeout = 1; + has_timeout = true; /* gettimeofday is not monotonic; using it here is not strictly correct */ gettimeofday(&start_time, NULL); } @@ -1987,7 +1987,7 @@ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, si if (began_blocked) { php_openssl_set_blocking(sslsock, 1); } - sslsock->s.timeout_event = 1; + sslsock->s.timeout_event = true; return -1; } } @@ -2248,7 +2248,7 @@ static inline int php_openssl_tcp_sockop_accept(php_stream *stream, php_openssl_ clisockdata->s.socket = clisock; #ifdef __linux__ /* O_NONBLOCK is not inherited on Linux */ - clisockdata->s.is_blocked = 1; + clisockdata->s.is_blocked = true; #endif xparam->outputs.client = php_stream_alloc_rel(stream->ops, clisockdata, NULL, "r+"); @@ -2379,8 +2379,8 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val int retry = 1; struct timeval start_time; struct timeval *timeout = NULL; - int began_blocked = sslsock->s.is_blocked; - int has_timeout = 0; + bool began_blocked = sslsock->s.is_blocked; + bool has_timeout = false; /* never use a timeout with non-blocking sockets */ if (began_blocked) { @@ -2392,7 +2392,7 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val } if (!sslsock->s.is_blocked && timeout && (timeout->tv_sec > 0 || (timeout->tv_sec == 0 && timeout->tv_usec))) { - has_timeout = 1; + has_timeout = true; /* gettimeofday is not monotonic; using it here is not strictly correct */ gettimeofday(&start_time, NULL); } @@ -2414,7 +2414,7 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val if (began_blocked) { php_openssl_set_blocking(sslsock, 1); } - sslsock->s.timeout_event = 1; + sslsock->s.timeout_event = true; return PHP_STREAM_OPTION_RETURN_ERR; } } @@ -2438,7 +2438,7 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val } /* Don't loop indefinitely in non-blocking mode if no data is available */ - if (began_blocked == 0 || !has_timeout) { + if (!began_blocked || !has_timeout) { alive = retry; break; } @@ -2668,7 +2668,7 @@ php_stream *php_openssl_ssl_socket_factory(const char *proto, size_t protolen, sslsock = pemalloc(sizeof(php_openssl_netstream_data_t), persistent_id ? 1 : 0); memset(sslsock, 0, sizeof(*sslsock)); - sslsock->s.is_blocked = 1; + sslsock->s.is_blocked = true; /* this timeout is used by standard stream funcs, therefore it should use the default value */ #ifdef _WIN32 sslsock->s.timeout.tv_sec = (long)FG(default_socket_timeout); diff --git a/main/network.c b/main/network.c index 14f4ca4dff987..70dc505582868 100644 --- a/main/network.c +++ b/main/network.c @@ -1109,7 +1109,7 @@ PHPAPI php_stream *_php_stream_sock_open_from_socket(php_socket_t socket, const sock = pemalloc(sizeof(php_netstream_data_t), persistent_id ? 1 : 0); memset(sock, 0, sizeof(php_netstream_data_t)); - sock->is_blocked = 1; + sock->is_blocked = true; sock->timeout.tv_sec = FG(default_socket_timeout); sock->timeout.tv_usec = 0; sock->socket = socket; diff --git a/main/php_network.h b/main/php_network.h index 94a2508c89e52..1d941265bd9a4 100644 --- a/main/php_network.h +++ b/main/php_network.h @@ -313,9 +313,9 @@ END_EXTERN_C() struct _php_netstream_data_t { php_socket_t socket; - char is_blocked; + bool is_blocked; + bool timeout_event; struct timeval timeout; - char timeout_event; size_t ownsize; }; typedef struct _php_netstream_data_t php_netstream_data_t; diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c index 8623c11be004c..f843fe83e27ff 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -80,13 +80,13 @@ static ssize_t php_sockop_write(php_stream *stream, const char *buf, size_t coun if (sock->is_blocked) { int retval; - sock->timeout_event = 0; + sock->timeout_event = false; do { retval = php_pollfd_for(sock->socket, POLLOUT, ptimeout); if (retval == 0) { - sock->timeout_event = 1; + sock->timeout_event = true; break; } @@ -129,7 +129,7 @@ static void php_sock_stream_wait_for_data(php_stream *stream, php_netstream_data return; } - sock->timeout_event = 0; + sock->timeout_event = false; if (has_buffered_data) { /* If there is already buffered data, use no timeout. */ @@ -146,7 +146,7 @@ static void php_sock_stream_wait_for_data(php_stream *stream, php_netstream_data retval = php_pollfd_for(sock->socket, PHP_POLLREADABLE, ptimeout); if (retval == 0) - sock->timeout_event = 1; + sock->timeout_event = true; if (retval >= 0) break; @@ -399,7 +399,7 @@ static int php_sockop_set_option(php_stream *stream, int option, int value, void case PHP_STREAM_OPTION_READ_TIMEOUT: sock->timeout = *(struct timeval*)ptrparam; - sock->timeout_event = 0; + sock->timeout_event = false; return PHP_STREAM_OPTION_RETURN_OK; case PHP_STREAM_OPTION_META_DATA_API: @@ -885,7 +885,7 @@ static inline int php_tcp_sockop_accept(php_stream *stream, php_netstream_data_t clisockdata->socket = clisock; #ifdef __linux__ /* O_NONBLOCK is not inherited on Linux */ - clisockdata->is_blocked = 1; + clisockdata->is_blocked = true; #endif xparam->outputs.client = php_stream_alloc_rel(stream->ops, clisockdata, NULL, "r+"); @@ -963,7 +963,7 @@ PHPAPI php_stream *php_stream_generic_socket_factory(const char *proto, size_t p sock = pemalloc(sizeof(php_netstream_data_t), persistent_id ? 1 : 0); memset(sock, 0, sizeof(php_netstream_data_t)); - sock->is_blocked = 1; + sock->is_blocked = true; sock->timeout.tv_sec = FG(default_socket_timeout); sock->timeout.tv_usec = 0; From e43074a1d8ad5834af46a2cdddfb5ea10481e6b1 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois <2144837+alexandre-daubois@users.noreply.github.com> Date: Thu, 31 Jul 2025 20:58:01 +0200 Subject: [PATCH 04/12] Rename poorly named tests in `Zend/tests` (#19332) And move some into their relevant folders --- Zend/tests/014.inc | 3 --- .../{022.phpt => abstract_method_optional_params.phpt} | 0 Zend/tests/{032.phpt => array_append_by_reference.phpt} | 0 Zend/tests/{031.phpt => array_append_reading_error.phpt} | 0 Zend/tests/{029.phpt => assign_array_object_property.phpt} | 0 Zend/tests/{026.phpt => assign_property_null_object.phpt} | 0 Zend/tests/{017.phpt => builtin_functions_basic.phpt} | 0 Zend/tests/{012.phpt => class_exists_basic.phpt} | 0 .../{038.phpt => closures/closure_array_key_error.phpt} | 0 .../{036.phpt => closures/closure_array_offset_error.phpt} | 0 .../closure_static_property_error.phpt} | 0 .../{025.phpt => dynamic_call/dynamic_method_calls.phpt} | 0 .../variable_variables_curly_syntax.phpt} | 0 .../variable_variables_function_names.phpt} | 0 Zend/tests/{002.phpt => func_get_arg_basic.phpt} | 2 +- Zend/tests/{020.phpt => func_get_arg_invalid.phpt} | 0 Zend/tests/{003.phpt => func_get_args_basic.phpt} | 4 ++-- Zend/tests/{001.phpt => func_num_args_basic.phpt} | 0 Zend/tests/{028.phpt => function_call_array_item.phpt} | 0 Zend/tests/{009.phpt => get_class_basic.phpt} | 0 Zend/tests/get_included_files_basic.inc | 3 +++ Zend/tests/{014.phpt => get_included_files_basic.phpt} | 6 +++--- Zend/tests/{010.phpt => get_parent_class_basic.phpt} | 0 Zend/tests/{013.phpt => interface_exists_basic.phpt} | 0 Zend/tests/{016.phpt => isset_non_object.phpt} | 0 Zend/tests/{011.phpt => property_exists_basic.phpt} | 0 Zend/tests/{035.phpt => static_global_scope.phpt} | 0 Zend/tests/{005.phpt => strcasecmp_basic.phpt} | 0 Zend/tests/{006.phpt => strncasecmp_basic.phpt} | 0 Zend/tests/{004.phpt => strncmp_basic.phpt} | 0 Zend/tests/{021.phpt => ternary_operator_basic.phpt} | 0 Zend/tests/{015.phpt => trigger_error_basic.phpt} | 0 .../{033.phpt => undefined_multidimensional_array.phpt} | 0 .../tests/{024.phpt => undefined_variables_operations.phpt} | 0 .../{019.phpt => unset_empty_isset_comprehensive.phpt} | 0 35 files changed, 9 insertions(+), 9 deletions(-) delete mode 100644 Zend/tests/014.inc rename Zend/tests/{022.phpt => abstract_method_optional_params.phpt} (100%) rename Zend/tests/{032.phpt => array_append_by_reference.phpt} (100%) rename Zend/tests/{031.phpt => array_append_reading_error.phpt} (100%) rename Zend/tests/{029.phpt => assign_array_object_property.phpt} (100%) rename Zend/tests/{026.phpt => assign_property_null_object.phpt} (100%) rename Zend/tests/{017.phpt => builtin_functions_basic.phpt} (100%) rename Zend/tests/{012.phpt => class_exists_basic.phpt} (100%) rename Zend/tests/{038.phpt => closures/closure_array_key_error.phpt} (100%) rename Zend/tests/{036.phpt => closures/closure_array_offset_error.phpt} (100%) rename Zend/tests/{037.phpt => closures/closure_static_property_error.phpt} (100%) rename Zend/tests/{025.phpt => dynamic_call/dynamic_method_calls.phpt} (100%) rename Zend/tests/{027.phpt => dynamic_call/variable_variables_curly_syntax.phpt} (100%) rename Zend/tests/{023.phpt => dynamic_call/variable_variables_function_names.phpt} (100%) rename Zend/tests/{002.phpt => func_get_arg_basic.phpt} (98%) rename Zend/tests/{020.phpt => func_get_arg_invalid.phpt} (100%) rename Zend/tests/{003.phpt => func_get_args_basic.phpt} (93%) rename Zend/tests/{001.phpt => func_num_args_basic.phpt} (100%) rename Zend/tests/{028.phpt => function_call_array_item.phpt} (100%) rename Zend/tests/{009.phpt => get_class_basic.phpt} (100%) create mode 100644 Zend/tests/get_included_files_basic.inc rename Zend/tests/{014.phpt => get_included_files_basic.phpt} (74%) rename Zend/tests/{010.phpt => get_parent_class_basic.phpt} (100%) rename Zend/tests/{013.phpt => interface_exists_basic.phpt} (100%) rename Zend/tests/{016.phpt => isset_non_object.phpt} (100%) rename Zend/tests/{011.phpt => property_exists_basic.phpt} (100%) rename Zend/tests/{035.phpt => static_global_scope.phpt} (100%) rename Zend/tests/{005.phpt => strcasecmp_basic.phpt} (100%) rename Zend/tests/{006.phpt => strncasecmp_basic.phpt} (100%) rename Zend/tests/{004.phpt => strncmp_basic.phpt} (100%) rename Zend/tests/{021.phpt => ternary_operator_basic.phpt} (100%) rename Zend/tests/{015.phpt => trigger_error_basic.phpt} (100%) rename Zend/tests/{033.phpt => undefined_multidimensional_array.phpt} (100%) rename Zend/tests/{024.phpt => undefined_variables_operations.phpt} (100%) rename Zend/tests/{019.phpt => unset_empty_isset_comprehensive.phpt} (100%) diff --git a/Zend/tests/014.inc b/Zend/tests/014.inc deleted file mode 100644 index 69c9bc0790257..0000000000000 --- a/Zend/tests/014.inc +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/Zend/tests/022.phpt b/Zend/tests/abstract_method_optional_params.phpt similarity index 100% rename from Zend/tests/022.phpt rename to Zend/tests/abstract_method_optional_params.phpt diff --git a/Zend/tests/032.phpt b/Zend/tests/array_append_by_reference.phpt similarity index 100% rename from Zend/tests/032.phpt rename to Zend/tests/array_append_by_reference.phpt diff --git a/Zend/tests/031.phpt b/Zend/tests/array_append_reading_error.phpt similarity index 100% rename from Zend/tests/031.phpt rename to Zend/tests/array_append_reading_error.phpt diff --git a/Zend/tests/029.phpt b/Zend/tests/assign_array_object_property.phpt similarity index 100% rename from Zend/tests/029.phpt rename to Zend/tests/assign_array_object_property.phpt diff --git a/Zend/tests/026.phpt b/Zend/tests/assign_property_null_object.phpt similarity index 100% rename from Zend/tests/026.phpt rename to Zend/tests/assign_property_null_object.phpt diff --git a/Zend/tests/017.phpt b/Zend/tests/builtin_functions_basic.phpt similarity index 100% rename from Zend/tests/017.phpt rename to Zend/tests/builtin_functions_basic.phpt diff --git a/Zend/tests/012.phpt b/Zend/tests/class_exists_basic.phpt similarity index 100% rename from Zend/tests/012.phpt rename to Zend/tests/class_exists_basic.phpt diff --git a/Zend/tests/038.phpt b/Zend/tests/closures/closure_array_key_error.phpt similarity index 100% rename from Zend/tests/038.phpt rename to Zend/tests/closures/closure_array_key_error.phpt diff --git a/Zend/tests/036.phpt b/Zend/tests/closures/closure_array_offset_error.phpt similarity index 100% rename from Zend/tests/036.phpt rename to Zend/tests/closures/closure_array_offset_error.phpt diff --git a/Zend/tests/037.phpt b/Zend/tests/closures/closure_static_property_error.phpt similarity index 100% rename from Zend/tests/037.phpt rename to Zend/tests/closures/closure_static_property_error.phpt diff --git a/Zend/tests/025.phpt b/Zend/tests/dynamic_call/dynamic_method_calls.phpt similarity index 100% rename from Zend/tests/025.phpt rename to Zend/tests/dynamic_call/dynamic_method_calls.phpt diff --git a/Zend/tests/027.phpt b/Zend/tests/dynamic_call/variable_variables_curly_syntax.phpt similarity index 100% rename from Zend/tests/027.phpt rename to Zend/tests/dynamic_call/variable_variables_curly_syntax.phpt diff --git a/Zend/tests/023.phpt b/Zend/tests/dynamic_call/variable_variables_function_names.phpt similarity index 100% rename from Zend/tests/023.phpt rename to Zend/tests/dynamic_call/variable_variables_function_names.phpt diff --git a/Zend/tests/002.phpt b/Zend/tests/func_get_arg_basic.phpt similarity index 98% rename from Zend/tests/002.phpt rename to Zend/tests/func_get_arg_basic.phpt index 7c2ff7b4d389d..ec06a5c20f4df 100644 --- a/Zend/tests/002.phpt +++ b/Zend/tests/func_get_arg_basic.phpt @@ -104,7 +104,7 @@ int(10) func_get_arg(): Argument #1 ($position) must be less than the number of the arguments passed to the currently executed function int(1) func_get_arg(): Argument #1 ($position) must be less than the number of the arguments passed to the currently executed function -Exception: Too few arguments to function test2(), 0 passed in %s002.php on line %d and exactly 1 expected +Exception: Too few arguments to function test2(), 0 passed in %s on line %d and exactly 1 expected int(1) int(2) func_get_arg(): Argument #1 ($position) must be less than the number of the arguments passed to the currently executed function diff --git a/Zend/tests/020.phpt b/Zend/tests/func_get_arg_invalid.phpt similarity index 100% rename from Zend/tests/020.phpt rename to Zend/tests/func_get_arg_invalid.phpt diff --git a/Zend/tests/003.phpt b/Zend/tests/func_get_args_basic.phpt similarity index 93% rename from Zend/tests/003.phpt rename to Zend/tests/func_get_args_basic.phpt index 3931628e9a9ca..54ddf1b40c921 100644 --- a/Zend/tests/003.phpt +++ b/Zend/tests/func_get_args_basic.phpt @@ -59,7 +59,7 @@ array(1) { [0]=> int(1) } -Exception: Too few arguments to function test2(), 0 passed in %s003.php on line %d and exactly 1 expected +Exception: Too few arguments to function test2(), 0 passed in %s on line %d and exactly 1 expected array(2) { [0]=> int(1) @@ -68,7 +68,7 @@ array(2) { } array(0) { } -Exception: Too few arguments to function test3(), 1 passed in %s003.php on line %d and exactly 2 expected +Exception: Too few arguments to function test3(), 1 passed in %s on line %d and exactly 2 expected array(2) { [0]=> int(1) diff --git a/Zend/tests/001.phpt b/Zend/tests/func_num_args_basic.phpt similarity index 100% rename from Zend/tests/001.phpt rename to Zend/tests/func_num_args_basic.phpt diff --git a/Zend/tests/028.phpt b/Zend/tests/function_call_array_item.phpt similarity index 100% rename from Zend/tests/028.phpt rename to Zend/tests/function_call_array_item.phpt diff --git a/Zend/tests/009.phpt b/Zend/tests/get_class_basic.phpt similarity index 100% rename from Zend/tests/009.phpt rename to Zend/tests/get_class_basic.phpt diff --git a/Zend/tests/get_included_files_basic.inc b/Zend/tests/get_included_files_basic.inc new file mode 100644 index 0000000000000..406acaee6e461 --- /dev/null +++ b/Zend/tests/get_included_files_basic.inc @@ -0,0 +1,3 @@ + diff --git a/Zend/tests/014.phpt b/Zend/tests/get_included_files_basic.phpt similarity index 74% rename from Zend/tests/014.phpt rename to Zend/tests/get_included_files_basic.phpt index c02fee93856f1..be595a592621a 100644 --- a/Zend/tests/014.phpt +++ b/Zend/tests/get_included_files_basic.phpt @@ -5,13 +5,13 @@ get_included_files() tests var_dump(get_included_files()); -include(__DIR__."/014.inc"); +include(__DIR__."/get_included_files_basic.inc"); var_dump(get_included_files()); -include_once(__DIR__."/014.inc"); +include_once(__DIR__."/get_included_files_basic.inc"); var_dump(get_included_files()); -include(__DIR__."/014.inc"); +include(__DIR__."/get_included_files_basic.inc"); var_dump(get_included_files()); echo "Done\n"; diff --git a/Zend/tests/010.phpt b/Zend/tests/get_parent_class_basic.phpt similarity index 100% rename from Zend/tests/010.phpt rename to Zend/tests/get_parent_class_basic.phpt diff --git a/Zend/tests/013.phpt b/Zend/tests/interface_exists_basic.phpt similarity index 100% rename from Zend/tests/013.phpt rename to Zend/tests/interface_exists_basic.phpt diff --git a/Zend/tests/016.phpt b/Zend/tests/isset_non_object.phpt similarity index 100% rename from Zend/tests/016.phpt rename to Zend/tests/isset_non_object.phpt diff --git a/Zend/tests/011.phpt b/Zend/tests/property_exists_basic.phpt similarity index 100% rename from Zend/tests/011.phpt rename to Zend/tests/property_exists_basic.phpt diff --git a/Zend/tests/035.phpt b/Zend/tests/static_global_scope.phpt similarity index 100% rename from Zend/tests/035.phpt rename to Zend/tests/static_global_scope.phpt diff --git a/Zend/tests/005.phpt b/Zend/tests/strcasecmp_basic.phpt similarity index 100% rename from Zend/tests/005.phpt rename to Zend/tests/strcasecmp_basic.phpt diff --git a/Zend/tests/006.phpt b/Zend/tests/strncasecmp_basic.phpt similarity index 100% rename from Zend/tests/006.phpt rename to Zend/tests/strncasecmp_basic.phpt diff --git a/Zend/tests/004.phpt b/Zend/tests/strncmp_basic.phpt similarity index 100% rename from Zend/tests/004.phpt rename to Zend/tests/strncmp_basic.phpt diff --git a/Zend/tests/021.phpt b/Zend/tests/ternary_operator_basic.phpt similarity index 100% rename from Zend/tests/021.phpt rename to Zend/tests/ternary_operator_basic.phpt diff --git a/Zend/tests/015.phpt b/Zend/tests/trigger_error_basic.phpt similarity index 100% rename from Zend/tests/015.phpt rename to Zend/tests/trigger_error_basic.phpt diff --git a/Zend/tests/033.phpt b/Zend/tests/undefined_multidimensional_array.phpt similarity index 100% rename from Zend/tests/033.phpt rename to Zend/tests/undefined_multidimensional_array.phpt diff --git a/Zend/tests/024.phpt b/Zend/tests/undefined_variables_operations.phpt similarity index 100% rename from Zend/tests/024.phpt rename to Zend/tests/undefined_variables_operations.phpt diff --git a/Zend/tests/019.phpt b/Zend/tests/unset_empty_isset_comprehensive.phpt similarity index 100% rename from Zend/tests/019.phpt rename to Zend/tests/unset_empty_isset_comprehensive.phpt From 5d40592fe28c87e027794a091d08f0d4cf280885 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 30 Jul 2025 23:57:21 +0200 Subject: [PATCH 05/12] Fix stale nInternalPosition on rehashing Since GH-13188 we're no longer immediately updating iterator positions when deleting array elements. zend_hash_rehash() needs to adapt accordingly by adjusting nInternalPosition for IS_UNDEF elements. This is already the case for array iterators. Fixes GH-19280 Closes GH-19323 --- NEWS | 1 + Zend/tests/gh19280.phpt | 81 +++++++++++++++++++++++++++++++++++++++++ Zend/zend_hash.c | 4 +- 3 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 Zend/tests/gh19280.phpt diff --git a/NEWS b/NEWS index 84006fc58f2cd..93f804bb5d982 100644 --- a/NEWS +++ b/NEWS @@ -21,6 +21,7 @@ PHP NEWS delegated Generator). (Arnaud) . Fixed bug GH-19326 (Calling Generator::throw() on a running generator with a non-Generator delegate crashes). (Arnaud) + . Fixed bug GH-19280 (Stale array iterator position on rehashing). (ilutov) - FTP: . Fix theoretical issues with hrtime() not being available. (nielsdos) diff --git a/Zend/tests/gh19280.phpt b/Zend/tests/gh19280.phpt new file mode 100644 index 0000000000000..d73fc91b623fa --- /dev/null +++ b/Zend/tests/gh19280.phpt @@ -0,0 +1,81 @@ +--TEST-- +GH-19280: Stale nInternalPosition on rehashing +--FILE-- += 0; $i--) { + $a[$i] = $i; + } + for ($i = 0; $i <= 47; $i++) { + next($a); + } + for ($i = 48; $i >= 2; $i--) { + unset($a[$i]); + } + var_dump(key($a)); + $a[64] = 64; + var_dump(key($a)); +} + +rehash_packed(); +rehash_packed_iterated(); +rehash_string(); +rehash_int(); + +?> +--EXPECT-- +int(62) +int(62) +int(62) +int(62) +string(32) "44f683a84163b3523afe57c2e008bc8c" +string(32) "44f683a84163b3523afe57c2e008bc8c" +int(1) +int(1) diff --git a/Zend/zend_hash.c b/Zend/zend_hash.c index a417290b82c49..07dad65f1d274 100644 --- a/Zend/zend_hash.c +++ b/Zend/zend_hash.c @@ -1379,7 +1379,7 @@ ZEND_API void ZEND_FASTCALL zend_hash_rehash(HashTable *ht) q->key = p->key; Z_NEXT(q->val) = HT_HASH(ht, nIndex); HT_HASH(ht, nIndex) = HT_IDX_TO_HASH(j); - if (UNEXPECTED(ht->nInternalPointer == i)) { + if (UNEXPECTED(ht->nInternalPointer > j && ht->nInternalPointer <= i)) { ht->nInternalPointer = j; } q++; @@ -1398,7 +1398,7 @@ ZEND_API void ZEND_FASTCALL zend_hash_rehash(HashTable *ht) q->key = p->key; Z_NEXT(q->val) = HT_HASH(ht, nIndex); HT_HASH(ht, nIndex) = HT_IDX_TO_HASH(j); - if (UNEXPECTED(ht->nInternalPointer == i)) { + if (UNEXPECTED(ht->nInternalPointer > j && ht->nInternalPointer <= i)) { ht->nInternalPointer = j; } if (UNEXPECTED(i >= iter_pos)) { From 15990de89e34ea07c8b27f8137db1de95788621a Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Thu, 31 Jul 2025 22:10:06 +0200 Subject: [PATCH 06/12] Refactor op array loops in JIT (#19335) Reuse the helper zend_foreach_op_array() that we move to the zend_optimizer.h header to be usable in opcache. Note that applying this to other op_array loops is not easy because they either: - start from EG(persistent_classes_count) - or only apply to classes --- Zend/Optimizer/zend_optimizer.h | 4 +++ Zend/Optimizer/zend_optimizer_internal.h | 3 -- ext/opcache/jit/zend_jit.c | 44 +++--------------------- 3 files changed, 8 insertions(+), 43 deletions(-) diff --git a/Zend/Optimizer/zend_optimizer.h b/Zend/Optimizer/zend_optimizer.h index 16bfd75520d89..43a0f60a23219 100644 --- a/Zend/Optimizer/zend_optimizer.h +++ b/Zend/Optimizer/zend_optimizer.h @@ -98,6 +98,10 @@ ZEND_API int zend_optimizer_register_pass(zend_optimizer_pass_t pass); ZEND_API void zend_optimizer_unregister_pass(int idx); zend_result zend_optimizer_startup(void); zend_result zend_optimizer_shutdown(void); + +typedef void (*zend_op_array_func_t)(zend_op_array *, void *context); +void zend_foreach_op_array(zend_script *script, zend_op_array_func_t func, void *context); + END_EXTERN_C() #endif diff --git a/Zend/Optimizer/zend_optimizer_internal.h b/Zend/Optimizer/zend_optimizer_internal.h index a1fc57092d179..896fe8fb47289 100644 --- a/Zend/Optimizer/zend_optimizer_internal.h +++ b/Zend/Optimizer/zend_optimizer_internal.h @@ -128,7 +128,4 @@ int sccp_optimize_op_array(zend_optimizer_ctx *ctx, zend_op_array *op_array, zen int dce_optimize_op_array(zend_op_array *op_array, zend_optimizer_ctx *optimizer_ctx, zend_ssa *ssa, bool reorder_dtor_effects); zend_result zend_ssa_escape_analysis(const zend_script *script, zend_op_array *op_array, zend_ssa *ssa); -typedef void (*zend_op_array_func_t)(zend_op_array *, void *context); -void zend_foreach_op_array(zend_script *script, zend_op_array_func_t func, void *context); - #endif diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c index ab425f037c81f..eadd72ff4c800 100644 --- a/ext/opcache/jit/zend_jit.c +++ b/ext/opcache/jit/zend_jit.c @@ -3928,8 +3928,10 @@ void zend_jit_deactivate(void) zend_jit_profile_counter = 0; } -static void zend_jit_restart_preloaded_op_array(zend_op_array *op_array) +static void zend_jit_restart_preloaded_op_array(zend_op_array *op_array, void *context) { + ZEND_IGNORE_VALUE(context); + zend_func_info *func_info = ZEND_FUNC_INFO(op_array); if (!func_info) { @@ -3959,49 +3961,11 @@ static void zend_jit_restart_preloaded_op_array(zend_op_array *op_array) } #endif } - if (op_array->num_dynamic_func_defs) { - for (uint32_t i = 0; i < op_array->num_dynamic_func_defs; i++) { - zend_jit_restart_preloaded_op_array(op_array->dynamic_func_defs[i]); - } - } } static void zend_jit_restart_preloaded_script(zend_persistent_script *script) { - zend_class_entry *ce; - zend_op_array *op_array; - - zend_jit_restart_preloaded_op_array(&script->script.main_op_array); - - ZEND_HASH_MAP_FOREACH_PTR(&script->script.function_table, op_array) { - zend_jit_restart_preloaded_op_array(op_array); - } ZEND_HASH_FOREACH_END(); - - ZEND_HASH_MAP_FOREACH_PTR(&script->script.class_table, ce) { - ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, op_array) { - if (op_array->type == ZEND_USER_FUNCTION) { - zend_jit_restart_preloaded_op_array(op_array); - } - } ZEND_HASH_FOREACH_END(); - - if (ce->num_hooked_props > 0) { - zend_property_info *prop; - - ZEND_HASH_MAP_FOREACH_PTR(&ce->properties_info, prop) { - if (prop->hooks) { - for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) { - if (prop->hooks[i]) { - op_array = &prop->hooks[i]->op_array; - ZEND_ASSERT(op_array->type == ZEND_USER_FUNCTION); - if (!(op_array->fn_flags & ZEND_ACC_TRAIT_CLONE)) { - zend_jit_restart_preloaded_op_array(op_array); - } - } - } - } - } ZEND_HASH_FOREACH_END(); - } - } ZEND_HASH_FOREACH_END(); + zend_foreach_op_array(&script->script, zend_jit_restart_preloaded_op_array, NULL); } void zend_jit_restart(void) From ada10741b4238be7eb4c187d50ea073d488d6649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1t=C3=A9=20Kocsis?= Date: Thu, 31 Jul 2025 22:51:38 +0200 Subject: [PATCH 07/12] Add support for manually running the real-time benchmark on PRs (#19265) [skip-ci] --- .github/workflows/real-time-benchmark.yml | 228 ++++++++++++++++++---- 1 file changed, 190 insertions(+), 38 deletions(-) diff --git a/.github/workflows/real-time-benchmark.yml b/.github/workflows/real-time-benchmark.yml index 5d4c455acc18d..5156afc37b5d4 100644 --- a/.github/workflows/real-time-benchmark.yml +++ b/.github/workflows/real-time-benchmark.yml @@ -2,14 +2,81 @@ name: Real-time Benchmark on: schedule: - cron: "30 0 * * *" + workflow_dispatch: + inputs: + pull_request: + description: 'PR number that is going to be benchmarked (e.g. "1234")' + required: true + type: number + jit: + description: 'Whether JIT is benchmarked' + required: false + type: choice + options: + - "0" + - "1" + opcache: + description: 'Whether opcache is enabled for the benchmarked commit' + required: true + default: "1" + type: choice + options: + - "0" + - "1" + - "2" + baseline_opcache: + description: 'Whether opcache is enabled for the baseline commit' + required: true + default: "1" + type: choice + options: + - "0" + - "1" + - "2" permissions: contents: read + pull-requests: write jobs: REAL_TIME_BENCHMARK: name: REAL_TIME_BENCHMARK - if: github.repository == 'php/php-src' + if: github.repository == 'php/php-src' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-22.04 + env: + REPOSITORY: ${{ github.repository }} + BRANCH: "master" + COMMIT: ${{ github.sha }} + BASELINE_COMMIT: "d5f6e56610c729710073350af318c4ea1b292cfe" + ID: "master" + OPCACHE: ${{ inputs.opcache || '1' }} + BASELINE_OPCACHE: ${{ inputs.baseline_opcache || '2' }} + JIT: ${{ inputs.jit || '1' }} + YEAR: "" steps: + - name: Setup benchmark environment + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + YEAR="$(date '+%Y')" + echo "YEAR=$YEAR" >> $GITHUB_ENV + + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + PR_INFO=$(gh pr view ${{ inputs.pull_request }} --json headRepositoryOwner,headRepository,headRefName,headRefOid,baseRefOid --repo ${{ github.repository }} | jq -r '.headRepositoryOwner.login, .headRepository.name, .headRefName, .headRefOid, .baseRefOid') + + REPOSITORY="$(echo "$PR_INFO" | sed -n '1p')/$(echo "$PR_INFO" | sed -n '2p')" + echo "REPOSITORY=$REPOSITORY" >> $GITHUB_ENV + + BRANCH=$(echo "$PR_INFO" | sed -n '3p') + echo "BRANCH=$BRANCH" >> $GITHUB_ENV + + COMMIT=$(echo "$PR_INFO" | sed -n '4p') + echo "COMMIT=$COMMIT" >> $GITHUB_ENV + + BASELINE_COMMIT=$(echo "$PR_INFO" | sed -n '5p') + echo "BASELINE_COMMIT=$BASELINE_COMMIT" >> $GITHUB_ENV + + echo "ID=benchmarked" >> $GITHUB_ENV + fi + - name: Install dependencies run: | set -ex @@ -29,73 +96,142 @@ jobs: ref: 'main' fetch-depth: 1 path: 'php-version-benchmarks' - - name: Checkout php-src + - name: Checkout php-src (benchmarked version) + uses: actions/checkout@v4 + with: + repository: '${{ env.REPOSITORY }}' + ref: '${{ env.COMMIT }}' + fetch-depth: 100 + path: 'php-version-benchmarks/tmp/php_${{ env.ID }}' + - name: Checkout php-src (baseline version) uses: actions/checkout@v4 with: - repository: 'php/php-src' - ref: '${{ github.sha }}' + repository: '${{ env.REPOSITORY }}' + ref: '${{ env.BASELINE_COMMIT }}' fetch-depth: 100 - path: 'php-version-benchmarks/tmp/php_master' + path: 'php-version-benchmarks/tmp/php_baseline' - name: Setup benchmark results run: | git config --global user.name "Benchmark" git config --global user.email "benchmark@php.net" - + rm -rf ./php-version-benchmarks/docs/results - name: Checkout benchmark data + if: github.event_name != 'workflow_dispatch' uses: actions/checkout@v4 with: repository: php/real-time-benchmark-data ssh-key: ${{ secrets.PHP_VERSION_BENCHMARK_RESULTS_DEPLOY_KEY }} path: 'php-version-benchmarks/docs/results' - - name: Set benchmark config + - name: Setup infra config run: | set -e - # Set infrastructure config cp ./php-version-benchmarks/config/infra/aws/x86_64-metal.ini.dist ./php-version-benchmarks/config/infra/aws/x86_64-metal.ini ESCAPED_DOCKER_REGISTRY=$(printf '%s\n' "${{ secrets.PHP_VERSION_BENCHMARK_DOCKER_REGISTRY }}" | sed -e 's/[\/&]/\\&/g') sed -i "s/INFRA_DOCKER_REGISTRY=public.ecr.aws\/abcdefgh/INFRA_DOCKER_REGISTRY=$ESCAPED_DOCKER_REGISTRY/g" ./php-version-benchmarks/config/infra/aws/x86_64-metal.ini cp ./php-version-benchmarks/build/infrastructure/config/aws.tfvars.dist ./php-version-benchmarks/build/infrastructure/config/aws.tfvars sed -i 's/access_key = ""/access_key = "${{ secrets.PHP_VERSION_BENCHMARK_AWS_ACCESS_KEY }}"/g' ./php-version-benchmarks/build/infrastructure/config/aws.tfvars sed -i 's/secret_key = ""/secret_key = "${{ secrets.PHP_VERSION_BENCHMARK_AWS_SECRET_KEY }}"/g' ./php-version-benchmarks/build/infrastructure/config/aws.tfvars + sed -i 's/github_token = ""/github_token = "${{ secrets.GITHUB_TOKEN }}"/g' ./php-version-benchmarks/build/infrastructure/config/aws.tfvars + - name: Setup PHP config - baseline PHP version + run: | + set -e - YEAR="$(date '+%Y')" - DATABASE="./php-version-benchmarks/docs/results/$YEAR/database.tsv" + BASELINE_SHORT_SHA="$(echo "${{ env.BASELINE_COMMIT }}" | cut -c1-4)" + + cat << EOF > ./php-version-benchmarks/config/php/baseline.ini + PHP_NAME="PHP - baseline@$BASELINE_SHORT_SHA" + PHP_ID=php_baseline + + PHP_REPO=https://github.com/${{ env.REPOSITORY }}.git + PHP_BRANCH=${{ env.BRANCH }} + PHP_COMMIT=${{ env.BASELINE_COMMIT }} + + PHP_OPCACHE=${{ env.BASELINE_OPCACHE }} + PHP_JIT=0 + EOF + - name: Setup PHP config - baseline PHP version with JIT + if: github.event_name == 'workflow_dispatch' && inputs.jit == '1' + run: | + set -e + + BASELINE_SHORT_SHA="$(echo "${{ env.BASELINE_COMMIT }}" | cut -c1-4)" + + cat << EOF > ./php-version-benchmarks/config/php/baseline_jit.ini + PHP_NAME="PHP - baseline@$BASELINE_SHORT_SHA (JIT)" + PHP_ID=php_baseline_jit + + PHP_REPO=https://github.com/${{ env.REPOSITORY }}.git + PHP_BRANCH=${{ env.BRANCH }} + PHP_COMMIT=${{ env.BASELINE_COMMIT }} + + PHP_OPCACHE=${{ env.BASELINE_OPCACHE }} + PHP_JIT=${{ env.JIT }} + EOF + + git clone ./php-version-benchmarks/tmp/php_baseline/ ./php-version-benchmarks/tmp/php_baseline_jit + - name: Setup PHP config - previous PHP version + if: github.event_name != 'workflow_dispatch' + run: | + set -e + + DATABASE="./php-version-benchmarks/docs/results/${{ env.YEAR }}/database.tsv" if [ -f "$DATABASE" ]; then LAST_RESULT_SHA="$(tail -n 2 "$DATABASE" | head -n 1 | cut -f 6)" else YESTERDAY="$(date -d "-2 day 23:59:59" '+%Y-%m-%d %H:%M:%S')" - LAST_RESULT_SHA="$(cd ./php-version-benchmarks/tmp/php_master/ && git --no-pager log --until="$YESTERDAY" -n 1 --pretty='%H')" + LAST_RESULT_SHA="$(cd ./php-version-benchmarks/tmp/php_${{ env.ID }}/ && git --no-pager log --until="$YESTERDAY" -n 1 --pretty='%H')" fi - BASELINE_SHA="d5f6e56610c729710073350af318c4ea1b292cfe" - BASELINE_SHORT_SHA="$(echo "$BASELINE_SHA" | cut -c1-4)" - - # Set config for the baseline PHP version - cp ./php-version-benchmarks/config/php/master.ini.dist ./php-version-benchmarks/config/php/master_baseline.ini - sed -i 's/PHP_NAME="PHP - master"/PHP_NAME="PHP - baseline@'"$BASELINE_SHORT_SHA"'"/g' ./php-version-benchmarks/config/php/master_baseline.ini - sed -i "s/PHP_ID=php_master/PHP_ID=php_master_baseline/g" ./php-version-benchmarks/config/php/master_baseline.ini - sed -i "s/PHP_COMMIT=/PHP_COMMIT=$BASELINE_SHA/g" ./php-version-benchmarks/config/php/master_baseline.ini - sed -i "s/PHP_OPCACHE=1/PHP_OPCACHE=2/g" ./php-version-benchmarks/config/php/master_baseline.ini - - # Set config for the previous PHP version - cp ./php-version-benchmarks/config/php/master.ini.dist ./php-version-benchmarks/config/php/master_last.ini - sed -i 's/PHP_NAME="PHP - master"/PHP_NAME="PHP - previous master"/g' ./php-version-benchmarks/config/php/master_last.ini - sed -i "s/PHP_ID=php_master/PHP_ID=php_master_previous/g" ./php-version-benchmarks/config/php/master_last.ini - sed -i "s/PHP_COMMIT=/PHP_COMMIT=$LAST_RESULT_SHA/g" ./php-version-benchmarks/config/php/master_last.ini - sed -i "s/PHP_OPCACHE=1/PHP_OPCACHE=2/g" ./php-version-benchmarks/config/php/master_last.ini - - # Set config for the current PHP version - cp ./php-version-benchmarks/config/php/master.ini.dist ./php-version-benchmarks/config/php/master_now.ini - sed -i "s/PHP_COMMIT=/PHP_COMMIT=${{ github.sha }}/g" ./php-version-benchmarks/config/php/master_now.ini - - # Set config for current PHP version with JIT - git clone ./php-version-benchmarks/tmp/php_master/ ./php-version-benchmarks/tmp/php_master_jit - cp ./php-version-benchmarks/config/php/master_jit.ini.dist ./php-version-benchmarks/config/php/master_now_jit.ini - sed -i "s/PHP_COMMIT=/PHP_COMMIT=${{ github.sha }}/g" ./php-version-benchmarks/config/php/master_now_jit.ini - - # Set test configs + cat << EOF > ./php-version-benchmarks/config/php/previous.ini + PHP_NAME="PHP - previous ${{ env.BRANCH }}" + PHP_ID=php_previous + + PHP_REPO=https://github.com/${{ env.REPOSITORY }}.git + PHP_BRANCH=${{ env.BRANCH }} + PHP_COMMIT=$LAST_RESULT_SHA + + PHP_OPCACHE=1 + PHP_JIT=0 + EOF + - name: Setup PHP config - benchmarked PHP version + run: | + set -e + + cat << EOF > ./php-version-benchmarks/config/php/this.ini + PHP_NAME="PHP - ${{ env.BRANCH }}" + PHP_ID=php_${{ env.ID }} + + PHP_REPO=https://github.com/${{ env.REPOSITORY }}.git + PHP_BRANCH=${{ env.BRANCH }} + PHP_COMMIT=${{ env.COMMIT }} + + PHP_OPCACHE=${{ env.OPCACHE }} + PHP_JIT=0 + EOF + - name: Setup PHP config - benchmarked PHP version with JIT + if: env.JIT == '1' + run: | + set -e + + cat << EOF > ./php-version-benchmarks/config/php/this_jit.ini + PHP_NAME="PHP - ${{ env.BRANCH }} (JIT)" + PHP_ID=php_${{ env.ID }}_jit + + PHP_REPO=https://github.com/${{ env.REPOSITORY }}.git + PHP_BRANCH=${{ env.BRANCH }} + PHP_COMMIT=${{ env.COMMIT }} + + PHP_OPCACHE=${{ env.OPCACHE }} + PHP_JIT=${{ env.JIT }} + EOF + + git clone ./php-version-benchmarks/tmp/php_${{ env.ID }}/ ./php-version-benchmarks/tmp/php_${{ env.ID }}_jit + - name: Setup test config + run: | + set -e + cp ./php-version-benchmarks/config/test/1_laravel.ini.dist ./php-version-benchmarks/config/test/1_laravel.ini cp ./php-version-benchmarks/config/test/2_symfony_main.ini.dist ./php-version-benchmarks/config/test/2_symfony_main.ini cp ./php-version-benchmarks/config/test/4_wordpress.ini.dist ./php-version-benchmarks/config/test/4_wordpress.ini @@ -104,6 +240,7 @@ jobs: - name: Run benchmark run: ./php-version-benchmarks/benchmark.sh run aws - name: Store results + if: github.repository == 'php/php-src' && github.event_name != 'workflow_dispatch' run: | set -ex @@ -119,6 +256,21 @@ jobs: fi git commit -m "Add result for ${{ github.repository }}@${{ github.sha }}" git push + - name: Upload artifact + if: github.event_name == 'workflow_dispatch' + uses: actions/upload-artifact@v4 + with: + name: results + path: ./php-version-benchmarks/docs/results/${{ env.YEAR }} + retention-days: 30 + - name: Comment results + if: github.event_name == 'workflow_dispatch' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + cd ./php-version-benchmarks/tmp/php_${{ env.ID }} + NEWEST_RESULT_DIRECTORY=$(ls -td ${{ github.workspace }}/php-version-benchmarks/docs/results/${{ env.YEAR }}/*/ | head -1) + gh pr comment ${{ inputs.pull_request }} --body-file "${NEWEST_RESULT_DIRECTORY}result.md" --repo ${{ env.REPOSITORY }} - name: Cleanup if: always() run: | From 65b9cf1db33f799f169cb2607108a44513bac292 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 31 Jul 2025 23:02:35 +0100 Subject: [PATCH 08/12] ext/standard/proc_open.c: Minor refactorings (#18085) Add const modifier Use unsigned types when the source type is unsigned Use HashTable instead of zvals where possible --- ext/standard/proc_open.c | 61 +++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c index 06a3f916a849c..690e23e0d3538 100644 --- a/ext/standard/proc_open.c +++ b/ext/standard/proc_open.c @@ -136,11 +136,11 @@ int openpty(int *master, int *slave, char *name, struct termios *termp, struct w static int le_proc_open; /* Resource number for `proc` resources */ -/* {{{ _php_array_to_envp +/* {{{ php_array_to_envp * Process the `environment` argument to `proc_open` * Convert into data structures which can be passed to underlying OS APIs like `exec` on POSIX or * `CreateProcessW` on Win32 */ -static php_process_env _php_array_to_envp(zval *environment) +ZEND_ATTRIBUTE_NONNULL static php_process_env php_array_to_envp(const HashTable *environment) { zval *element; php_process_env env; @@ -154,13 +154,9 @@ static php_process_env _php_array_to_envp(zval *environment) memset(&env, 0, sizeof(env)); - if (!environment) { - return env; - } - - uint32_t cnt = zend_hash_num_elements(Z_ARRVAL_P(environment)); + uint32_t cnt = zend_hash_num_elements(environment); - if (cnt < 1) { + if (cnt == 0) { #ifndef PHP_WIN32 env.envarray = (char **) ecalloc(1, sizeof(char *)); #endif @@ -172,7 +168,7 @@ static php_process_env _php_array_to_envp(zval *environment) zend_hash_init(env_hash, cnt, NULL, NULL, 0); /* first, we have to get the size of all the elements in the hash */ - ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(environment), key, element) { + ZEND_HASH_FOREACH_STR_KEY_VAL(environment, key, element) { str = zval_get_string(element); if (ZSTR_LEN(str) == 0) { @@ -221,7 +217,7 @@ static php_process_env _php_array_to_envp(zval *environment) /* }}} */ /* {{{ _php_free_envp - * Free the structures allocated by `_php_array_to_envp` */ + * Free the structures allocated by php_array_to_envp */ static void _php_free_envp(php_process_env env) { #ifndef PHP_WIN32 @@ -506,7 +502,7 @@ typedef struct _descriptorspec_item { int mode_flags; /* mode for opening FDs: r/o, r/w, binary (on Win32), etc */ } descriptorspec_item; -static zend_string *get_valid_arg_string(zval *zv, int elem_num) { +static zend_string *get_valid_arg_string(zval *zv, uint32_t elem_num) { zend_string *str = zval_get_string(zv); if (!str) { return NULL; @@ -518,7 +514,7 @@ static zend_string *get_valid_arg_string(zval *zv, int elem_num) { return NULL; } - if (strlen(ZSTR_VAL(str)) != ZSTR_LEN(str)) { + if (zend_str_has_nul_byte(str)) { zend_value_error("Command array element %d contains a null byte", elem_num); zend_string_release(str); return NULL; @@ -630,7 +626,7 @@ static zend_string *create_win_command_from_args(HashTable *args) zval *arg_zv; bool is_prog_name = true; bool is_cmd_execution = false; - int elem_num = 0; + uint32_t elem_num = 0; ZEND_HASH_FOREACH_VAL(args, arg_zv) { zend_string *arg_str = get_valid_arg_string(arg_zv, ++elem_num); @@ -778,11 +774,11 @@ static zend_result convert_command_to_use_shell(wchar_t **cmdw, size_t cmdw_len) #ifndef PHP_WIN32 /* Convert command parameter array passed as first argument to `proc_open` into command string */ -static zend_string* get_command_from_array(HashTable *array, char ***argv, int num_elems) +static zend_string* get_command_from_array(const HashTable *array, char ***argv, uint32_t num_elems) { zval *arg_zv; zend_string *command = NULL; - int i = 0; + uint32_t i = 0; *argv = safe_emalloc(sizeof(char *), num_elems + 1, 0); @@ -810,16 +806,16 @@ static zend_string* get_command_from_array(HashTable *array, char ***argv, int n } #endif -static descriptorspec_item* alloc_descriptor_array(HashTable *descriptorspec) +static descriptorspec_item* alloc_descriptor_array(const HashTable *descriptorspec) { uint32_t ndescriptors = zend_hash_num_elements(descriptorspec); return ecalloc(ndescriptors, sizeof(descriptorspec_item)); } -static zend_string* get_string_parameter(zval *array, int index, char *param_name) +static zend_string* get_string_parameter(const HashTable *ht, unsigned int index, const char *param_name) { zval *array_item; - if ((array_item = zend_hash_index_find(Z_ARRVAL_P(array), index)) == NULL) { + if ((array_item = zend_hash_index_find(ht, index)) == NULL) { zend_value_error("Missing %s", param_name); return NULL; } @@ -995,7 +991,7 @@ static zend_result dup_proc_descriptor(php_file_descriptor_t from, php_file_desc } static zend_result redirect_proc_descriptor(descriptorspec_item *desc, int target, - descriptorspec_item *descriptors, int ndesc, int nindex) + const descriptorspec_item *descriptors, int ndesc, int nindex) { php_file_descriptor_t redirect_to = PHP_INVALID_FD; @@ -1030,9 +1026,9 @@ static zend_result redirect_proc_descriptor(descriptorspec_item *desc, int targe } /* Process one item from `$descriptorspec` argument to `proc_open` */ -static zend_result set_proc_descriptor_from_array(zval *descitem, descriptorspec_item *descriptors, +static zend_result set_proc_descriptor_from_array(const HashTable *ht, descriptorspec_item *descriptors, int ndesc, int nindex, int *pty_master_fd, int *pty_slave_fd) { - zend_string *ztype = get_string_parameter(descitem, 0, "handle qualifier"); + zend_string *ztype = get_string_parameter(ht, 0, "handle qualifier"); if (!ztype) { return FAILURE; } @@ -1042,7 +1038,7 @@ static zend_result set_proc_descriptor_from_array(zval *descitem, descriptorspec if (zend_string_equals_literal(ztype, "pipe")) { /* Set descriptor to pipe */ - zmode = get_string_parameter(descitem, 1, "mode parameter for 'pipe'"); + zmode = get_string_parameter(ht, 1, "mode parameter for 'pipe'"); if (zmode == NULL) { goto finish; } @@ -1052,16 +1048,16 @@ static zend_result set_proc_descriptor_from_array(zval *descitem, descriptorspec retval = set_proc_descriptor_to_socket(&descriptors[ndesc]); } else if (zend_string_equals(ztype, ZSTR_KNOWN(ZEND_STR_FILE))) { /* Set descriptor to file */ - if ((zfile = get_string_parameter(descitem, 1, "file name parameter for 'file'")) == NULL) { + if ((zfile = get_string_parameter(ht, 1, "file name parameter for 'file'")) == NULL) { goto finish; } - if ((zmode = get_string_parameter(descitem, 2, "mode parameter for 'file'")) == NULL) { + if ((zmode = get_string_parameter(ht, 2, "mode parameter for 'file'")) == NULL) { goto finish; } retval = set_proc_descriptor_to_file(&descriptors[ndesc], zfile, zmode); } else if (zend_string_equals_literal(ztype, "redirect")) { /* Redirect descriptor to whatever another descriptor is set to */ - zval *ztarget = zend_hash_index_find_deref(Z_ARRVAL_P(descitem), 1); + zval *ztarget = zend_hash_index_find_deref(ht, 1); if (!ztarget) { zend_value_error("Missing redirection target"); goto finish; @@ -1116,7 +1112,7 @@ static zend_result set_proc_descriptor_from_resource(zval *resource, descriptors #ifndef PHP_WIN32 #if defined(USE_POSIX_SPAWN) -static zend_result close_parentends_of_pipes(posix_spawn_file_actions_t * actions, descriptorspec_item *descriptors, int ndesc) +static zend_result close_parentends_of_pipes(posix_spawn_file_actions_t * actions, const descriptorspec_item *descriptors, int ndesc) { int r; for (int i = 0; i < ndesc; i++) { @@ -1200,9 +1196,10 @@ PHP_FUNCTION(proc_open) HashTable *command_ht; HashTable *descriptorspec; /* Mandatory argument */ zval *pipes; /* Mandatory argument */ - char *cwd = NULL; /* Optional argument */ - size_t cwd_len = 0; /* Optional argument */ - zval *environment = NULL, *other_options = NULL; /* Optional arguments */ + char *cwd = NULL; /* Optional argument */ + size_t cwd_len = 0; /* Optional argument */ + HashTable *environment = NULL; /* Optional arguments */ + zval *other_options = NULL; /* Optional arguments */ php_process_env env; int ndesc = 0; @@ -1239,7 +1236,7 @@ PHP_FUNCTION(proc_open) Z_PARAM_ZVAL(pipes) Z_PARAM_OPTIONAL Z_PARAM_STRING_OR_NULL(cwd, cwd_len) - Z_PARAM_ARRAY_OR_NULL(environment) + Z_PARAM_ARRAY_HT_OR_NULL(environment) Z_PARAM_ARRAY_OR_NULL(other_options) ZEND_PARSE_PARAMETERS_END(); @@ -1282,7 +1279,7 @@ PHP_FUNCTION(proc_open) #endif if (environment) { - env = _php_array_to_envp(environment); + env = php_array_to_envp(environment); } descriptors = alloc_descriptor_array(descriptorspec); @@ -1302,7 +1299,7 @@ PHP_FUNCTION(proc_open) goto exit_fail; } } else if (Z_TYPE_P(descitem) == IS_ARRAY) { - if (set_proc_descriptor_from_array(descitem, descriptors, ndesc, (int)nindex, + if (set_proc_descriptor_from_array(Z_ARRVAL_P(descitem), descriptors, ndesc, (int)nindex, &pty_master_fd, &pty_slave_fd) == FAILURE) { goto exit_fail; } From d0fad34230eaeeac1ade4a9311aaf23ae2969fd6 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Fri, 18 Jul 2025 15:28:29 +0200 Subject: [PATCH 09/12] Fix circumvented type check with return by ref + finally Fixes GH-18736 Closes GH-19172 --- NEWS | 2 ++ Zend/tests/gh18736.phpt | 24 ++++++++++++++++++++++++ Zend/zend_compile.c | 12 ++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 Zend/tests/gh18736.phpt diff --git a/NEWS b/NEWS index e89bc61148d1f..727b25f0521f9 100644 --- a/NEWS +++ b/NEWS @@ -17,6 +17,8 @@ PHP NEWS delegated Generator). (Arnaud) . Fixed bug GH-19326 (Calling Generator::throw() on a running generator with a non-Generator delegate crashes). (Arnaud) + . Fixed bug GH-18736 (Circumvented type check with return by ref + finally). + (ilutov) - FTP: . Fix theoretical issues with hrtime() not being available. (nielsdos) diff --git a/Zend/tests/gh18736.phpt b/Zend/tests/gh18736.phpt new file mode 100644 index 0000000000000..f397ee39a310b --- /dev/null +++ b/Zend/tests/gh18736.phpt @@ -0,0 +1,24 @@ +--TEST-- +GH-18736: Circumvented type check with return by ref + finally +--FILE-- +getMessage(), "\n"; +} + +?> +--EXPECT-- +test(): Return value must be of type int, string returned diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 41113e2f0055b..6c8577cef5881 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -5201,8 +5201,20 @@ static void zend_compile_return(zend_ast *ast) /* {{{ */ expr_ast ? &expr_node : NULL, CG(active_op_array)->arg_info - 1, 0); } + uint32_t opnum_before_finally = get_next_op_number(); + zend_handle_loops_and_finally((expr_node.op_type & (IS_TMP_VAR | IS_VAR)) ? &expr_node : NULL); + /* Content of reference might have changed in finally, repeat type check. */ + if (by_ref + /* Check if any opcodes were emitted since the last return type check. */ + && opnum_before_finally != get_next_op_number() + && !is_generator + && (CG(active_op_array)->fn_flags & ZEND_ACC_HAS_RETURN_TYPE)) { + zend_emit_return_type_check( + expr_ast ? &expr_node : NULL, CG(active_op_array)->arg_info - 1, 0); + } + opline = zend_emit_op(NULL, by_ref ? ZEND_RETURN_BY_REF : ZEND_RETURN, &expr_node, NULL); From 18dee43e025ecb8407d56fb48d3d0fc7b689bf73 Mon Sep 17 00:00:00 2001 From: Calvin Buckley Date: Thu, 31 Jul 2025 19:52:04 -0300 Subject: [PATCH 10/12] Add `SAPI_HEADER_DELETE_PREFIX`, make ext/session use it (#18678) * Add SAPI_HEADER_DELETE_PREFIX operation The session ext currently munges into the linked list of headers itself, because the delete header API is given the key for headers to delete. The session ext wants to use a prefix past the colon separator, for i.e. "Set-Cookie: PHPSESSID=", to eliminate only the specific cookie rather than all cookies. This changes the SAPI code to add a new header op to take a prefix instead. Call sites are yet unchanged. Also fix some whitespace. * Simplify cookie setting code in ext/session Use the modern SAPI header ops API, including the remove prefix op we just added. * [ci skip] Remove redundant and unnecessary comment The purpose of this is clear, and after refactoring, the special case is no longer there, so it has no value. * Un-deprecate simple add/replace header API, use it Suggestion from Jakub. * Restore the optimization removing session cookies had I don't think this needs to be special cased with the parameter. * Move setting header length to caller Suggestion from Jakub. * [ci skip] adjust tab count It may be better to use spaces in here instead. * Use session_cookie_len rather than calling strlen --- ext/session/session.c | 38 ++++++-------------------------------- main/SAPI.c | 25 +++++++++++++++++-------- main/SAPI.h | 7 +++++-- 3 files changed, 28 insertions(+), 42 deletions(-) diff --git a/ext/session/session.c b/ext/session/session.c index edcfe9c433607..d466db2932ec3 100644 --- a/ext/session/session.c +++ b/ext/session/session.c @@ -1335,45 +1335,19 @@ static int php_session_cache_limiter(void) * Cookie Management * ********************* */ -/* - * Remove already sent session ID cookie. - * It must be directly removed from SG(sapi_header) because sapi_add_header_ex() - * removes all of matching cookie. i.e. It deletes all of Set-Cookie headers. - */ static void php_session_remove_cookie(void) { - sapi_header_struct *header; - zend_llist *l = &SG(sapi_headers).headers; - zend_llist_element *next; - zend_llist_element *current; char *session_cookie; size_t session_cookie_len; - size_t len = sizeof("Set-Cookie")-1; + sapi_header_line header_line = {0}; ZEND_ASSERT(strpbrk(ZSTR_VAL(PS(session_name)), SESSION_FORBIDDEN_CHARS) == NULL); session_cookie_len = spprintf(&session_cookie, 0, "Set-Cookie: %s=", ZSTR_VAL(PS(session_name))); - current = l->head; - while (current) { - header = (sapi_header_struct *)(current->data); - next = current->next; - if (header->header_len > len && header->header[len] == ':' - && !strncmp(header->header, session_cookie, session_cookie_len)) { - if (current->prev) { - current->prev->next = next; - } else { - l->head = next; - } - if (next) { - next->prev = current->prev; - } else { - l->tail = current->prev; - } - sapi_free_header(header); - efree(current); - --l->count; - } - current = next; - } + header_line.line = session_cookie; + header_line.line_len = session_cookie_len; + header_line.header_len = sizeof("Set-Cookie") - 1; + sapi_header_op(SAPI_HEADER_DELETE_PREFIX, &header_line); + efree(session_cookie); } diff --git a/main/SAPI.c b/main/SAPI.c index 866b44c3eac7d..169ae572fa967 100644 --- a/main/SAPI.c +++ b/main/SAPI.c @@ -597,7 +597,8 @@ static void sapi_update_response_code(int ncode) * since zend_llist_del_element only removes one matched item once, * we should remove them manually */ -static void sapi_remove_header(zend_llist *l, char *name, size_t len) { +static void sapi_remove_header(zend_llist *l, char *name, size_t len, size_t header_len) +{ sapi_header_struct *header; zend_llist_element *next; zend_llist_element *current=l->head; @@ -605,7 +606,8 @@ static void sapi_remove_header(zend_llist *l, char *name, size_t len) { while (current) { header = (sapi_header_struct *)(current->data); next = current->next; - if (header->header_len > len && header->header[len] == ':' + if (header->header_len > header_len + && (header->header[header_len] == ':' || len > header_len) && !strncasecmp(header->header, name, len)) { if (current->prev) { current->prev->next = next; @@ -653,7 +655,7 @@ static void sapi_header_add_op(sapi_header_op_enum op, sapi_header_struct *sapi_ char sav = *colon_offset; *colon_offset = 0; - sapi_remove_header(&SG(sapi_headers).headers, sapi_header->header, strlen(sapi_header->header)); + sapi_remove_header(&SG(sapi_headers).headers, sapi_header->header, strlen(sapi_header->header), 0); *colon_offset = sav; } } @@ -668,7 +670,7 @@ SAPI_API int sapi_header_op(sapi_header_op_enum op, void *arg) sapi_header_struct sapi_header; char *colon_offset; char *header_line; - size_t header_line_len; + size_t header_line_len, header_len; int http_response_code; if (SG(headers_sent) && !SG(request_info).no_headers) { @@ -691,6 +693,7 @@ SAPI_API int sapi_header_op(sapi_header_op_enum op, void *arg) case SAPI_HEADER_ADD: case SAPI_HEADER_REPLACE: + case SAPI_HEADER_DELETE_PREFIX: case SAPI_HEADER_DELETE: { sapi_header_line *p = arg; @@ -699,7 +702,13 @@ SAPI_API int sapi_header_op(sapi_header_op_enum op, void *arg) } header_line = estrndup(p->line, p->line_len); header_line_len = p->line_len; - http_response_code = p->response_code; + if (op == SAPI_HEADER_DELETE_PREFIX) { + header_len = p->header_len; + http_response_code = 0; + } else { + header_len = 0; + http_response_code = p->response_code; + } break; } @@ -722,8 +731,8 @@ SAPI_API int sapi_header_op(sapi_header_op_enum op, void *arg) header_line[header_line_len]='\0'; } - if (op == SAPI_HEADER_DELETE) { - if (strchr(header_line, ':')) { + if (op == SAPI_HEADER_DELETE || op == SAPI_HEADER_DELETE_PREFIX) { + if (op == SAPI_HEADER_DELETE && strchr(header_line, ':')) { efree(header_line); sapi_module.sapi_error(E_WARNING, "Header to delete may not contain colon."); return FAILURE; @@ -733,7 +742,7 @@ SAPI_API int sapi_header_op(sapi_header_op_enum op, void *arg) sapi_header.header_len = header_line_len; sapi_module.header_handler(&sapi_header, op, &SG(sapi_headers)); } - sapi_remove_header(&SG(sapi_headers).headers, header_line, header_line_len); + sapi_remove_header(&SG(sapi_headers).headers, header_line, header_line_len, header_len); efree(header_line); return SUCCESS; } else { diff --git a/main/SAPI.h b/main/SAPI.h index 284f4cb96f1fa..f7a64f243104e 100644 --- a/main/SAPI.h +++ b/main/SAPI.h @@ -185,13 +185,17 @@ END_EXTERN_C() typedef struct { const char *line; /* If you allocated this, you need to free it yourself */ size_t line_len; - zend_long response_code; /* long due to zend_parse_parameters compatibility */ + union { + zend_long response_code; /* long due to zend_parse_parameters compatibility */ + size_t header_len; /* the "Key" in "Key: Value", for optimization */ + }; } sapi_header_line; typedef enum { /* Parameter: */ SAPI_HEADER_REPLACE, /* sapi_header_line* */ SAPI_HEADER_ADD, /* sapi_header_line* */ SAPI_HEADER_DELETE, /* sapi_header_line* */ + SAPI_HEADER_DELETE_PREFIX, /* sapi_header_line* */ SAPI_HEADER_DELETE_ALL, /* void */ SAPI_HEADER_SET_STATUS /* int */ } sapi_header_op_enum; @@ -199,7 +203,6 @@ typedef enum { /* Parameter: */ BEGIN_EXTERN_C() SAPI_API int sapi_header_op(sapi_header_op_enum op, void *arg); -/* Deprecated functions. Use sapi_header_op instead. */ SAPI_API int sapi_add_header_ex(const char *header_line, size_t header_line_len, bool duplicate, bool replace); #define sapi_add_header(a, b, c) sapi_add_header_ex((a),(b),(c),1) From 2c4d4a6f185ff7b3f64c5380383ad37258a47366 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 31 Jul 2025 23:56:27 +0100 Subject: [PATCH 11/12] ext/odbc: various minor refactorings (#19337) --- ext/odbc/php_odbc.c | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/ext/odbc/php_odbc.c b/ext/odbc/php_odbc.c index 7bc3a8bc17d05..4b2cfa955a2f9 100644 --- a/ext/odbc/php_odbc.c +++ b/ext/odbc/php_odbc.c @@ -346,21 +346,21 @@ static void _close_odbc_pconn(zend_resource *rsrc) /* {{{ PHP_INI_DISP(display_link_nums) */ static PHP_INI_DISP(display_link_nums) { - char *value; + const zend_string *value; if (type == PHP_INI_DISPLAY_ORIG && ini_entry->modified) { - value = ZSTR_VAL(ini_entry->orig_value); + value = ini_entry->orig_value; } else if (ini_entry->value) { - value = ZSTR_VAL(ini_entry->value); + value = ini_entry->value; } else { value = NULL; } if (value) { - if (atoi(value) == -1) { + if (atoi(ZSTR_VAL(value)) == -1) { PUTS("Unlimited"); } else { - php_printf("%s", value); + php_output_write(ZSTR_VAL(value), ZSTR_LEN(value)); } } } @@ -670,7 +670,6 @@ void odbc_bindcols(odbc_result *result) SQLSMALLINT colnamelen; /* Not used */ SQLLEN displaysize; SQLUSMALLINT colfieldid; - int charextraalloc; result->values = (odbc_result_value *) safe_emalloc(sizeof(odbc_result_value), result->numcols, 0); @@ -678,7 +677,7 @@ void odbc_bindcols(odbc_result *result) result->binmode = ODBCG(defaultbinmode); for(i = 0; i < result->numcols; i++) { - charextraalloc = 0; + bool char_extra_alloc = false; colfieldid = SQL_COLUMN_DISPLAY_SIZE; rc = PHP_ODBC_SQLCOLATTRIBUTE(result->stmt, (SQLUSMALLINT)(i+1), PHP_ODBC_SQL_DESC_NAME, @@ -716,7 +715,7 @@ void odbc_bindcols(odbc_result *result) case SQL_WVARCHAR: colfieldid = SQL_DESC_OCTET_LENGTH; #else - charextraalloc = 1; + char_extra_alloc = true; #endif /* TODO: Check this is the intended behaviour */ ZEND_FALLTHROUGH; @@ -742,7 +741,7 @@ void odbc_bindcols(odbc_result *result) } /* This is a quirk for ODBC 2.0 compatibility for broken driver implementations. */ - charextraalloc = 1; + char_extra_alloc = true; rc = SQLColAttributes(result->stmt, (SQLUSMALLINT)(i+1), SQL_COLUMN_DISPLAY_SIZE, NULL, 0, NULL, &displaysize); if (rc != SQL_SUCCESS) { @@ -769,7 +768,7 @@ void odbc_bindcols(odbc_result *result) displaysize += 3; } - if (charextraalloc) { + if (char_extra_alloc) { /* Since we don't know the exact # of bytes, allocate extra */ displaysize *= 4; } @@ -1015,10 +1014,9 @@ PHP_FUNCTION(odbc_execute) zval *pv_res, *tmp; HashTable *pv_param_ht = (HashTable *) &zend_empty_array; odbc_params_t *params = NULL; - char *filename; SQLSMALLINT ctype; odbc_result *result; - int i, ne; + int i; RETCODE rc; if (zend_parse_parameters(ZEND_NUM_ARGS(), "O|h", &pv_res, odbc_result_ce, &pv_param_ht) == FAILURE) { @@ -1029,8 +1027,9 @@ PHP_FUNCTION(odbc_execute) CHECK_ODBC_RESULT(result); if (result->numparams > 0) { - if ((ne = zend_hash_num_elements(pv_param_ht)) < result->numparams) { - php_error_docref(NULL, E_WARNING, "Not enough parameters (%d should be %d) given", ne, result->numparams); + uint32_t ne = zend_hash_num_elements(pv_param_ht); + if (ne < result->numparams) { + php_error_docref(NULL, E_WARNING, "Not enough parameters (%" PRIu32 " should be %d) given", ne, result->numparams); RETURN_FALSE; } @@ -1067,7 +1066,7 @@ PHP_FUNCTION(odbc_execute) odbc_release_params(result, params); RETURN_FALSE; } - filename = estrndup(&ZSTR_VAL(tmpstr)[1], ZSTR_LEN(tmpstr) - 2); + char *filename = estrndup(&ZSTR_VAL(tmpstr)[1], ZSTR_LEN(tmpstr) - 2); /* Check the basedir */ if (php_check_open_basedir(filename)) { @@ -2185,8 +2184,7 @@ bool odbc_sqlconnect(zval *zv, char *db, char *uid, char *pwd, int cur_opt, bool int direct = 0; SQLCHAR dsnbuf[1024]; short dsnbuflen; - char *ldb = 0; - int ldb_len = 0; + char *ldb = NULL; /* a connection string may have = but not ; - i.e. "DSN=PHP" */ if (strstr((char*)db, "=")) { @@ -2248,7 +2246,7 @@ bool odbc_sqlconnect(zval *zv, char *db, char *uid, char *pwd, int cur_opt, bool efree(pwd_quoted); } } else { - ldb_len = strlen(db)+1; + size_t ldb_len = strlen(db)+1; ldb = (char*) emalloc(ldb_len); memcpy(ldb, db, ldb_len); } From 105c1e9896073083fc3f60b6a4a8840da39f8f64 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 31 Jul 2025 23:57:27 +0100 Subject: [PATCH 12/12] tree: use zend_str_has_nul_byte() API (#19336) --- Zend/zend_API.h | 2 +- Zend/zend_execute.c | 4 ++-- ext/mbstring/mbstring.c | 2 +- ext/odbc/php_odbc.c | 2 +- ext/standard/exec.c | 19 +++++++------------ ext/standard/filestat.c | 4 ++-- ext/standard/image.c | 2 +- ext/standard/mail.c | 2 +- ext/xsl/xsltprocessor.c | 6 +++--- main/main.c | 2 +- 10 files changed, 20 insertions(+), 25 deletions(-) diff --git a/Zend/zend_API.h b/Zend/zend_API.h index 8ab32a280b01d..78a30d630ae6e 100644 --- a/Zend/zend_API.h +++ b/Zend/zend_API.h @@ -2326,7 +2326,7 @@ static zend_always_inline bool zend_parse_arg_string(zval *arg, char **dest, siz static zend_always_inline bool zend_parse_arg_path_str(zval *arg, zend_string **dest, bool check_null, uint32_t arg_num) { if (!zend_parse_arg_str(arg, dest, check_null, arg_num) || - (*dest && UNEXPECTED(CHECK_NULL_PATH(ZSTR_VAL(*dest), ZSTR_LEN(*dest))))) { + (*dest && UNEXPECTED(zend_str_has_nul_byte(*dest)))) { return 0; } return 1; diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 734494d252da0..3908299a41c3f 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -5211,7 +5211,7 @@ static zend_never_inline zend_op_array* ZEND_FASTCALL zend_include_or_eval(zval } } else if (UNEXPECTED(EG(exception))) { break; - } else if (UNEXPECTED(strlen(ZSTR_VAL(inc_filename)) != ZSTR_LEN(inc_filename))) { + } else if (UNEXPECTED(zend_str_has_nul_byte(inc_filename))) { zend_message_dispatcher( (type == ZEND_INCLUDE_ONCE) ? ZMSG_FAILED_INCLUDE_FOPEN : ZMSG_FAILED_REQUIRE_FOPEN, @@ -5245,7 +5245,7 @@ static zend_never_inline zend_op_array* ZEND_FASTCALL zend_include_or_eval(zval break; case ZEND_INCLUDE: case ZEND_REQUIRE: - if (UNEXPECTED(strlen(ZSTR_VAL(inc_filename)) != ZSTR_LEN(inc_filename))) { + if (UNEXPECTED(zend_str_has_nul_byte(inc_filename))) { zend_message_dispatcher( (type == ZEND_INCLUDE) ? ZMSG_FAILED_INCLUDE_FOPEN : ZMSG_FAILED_REQUIRE_FOPEN, diff --git a/ext/mbstring/mbstring.c b/ext/mbstring/mbstring.c index 9c1759f05db63..f4e50fbd7c885 100644 --- a/ext/mbstring/mbstring.c +++ b/ext/mbstring/mbstring.c @@ -4506,7 +4506,7 @@ PHP_FUNCTION(mb_send_mail) ZEND_PARSE_PARAMETERS_END(); if (str_headers) { - if (strlen(ZSTR_VAL(str_headers)) != ZSTR_LEN(str_headers)) { + if (UNEXPECTED(zend_str_has_nul_byte(str_headers))) { zend_argument_value_error(4, "must not contain any null bytes"); RETURN_THROWS(); } diff --git a/ext/odbc/php_odbc.c b/ext/odbc/php_odbc.c index 4b2cfa955a2f9..55334fa15b1ce 100644 --- a/ext/odbc/php_odbc.c +++ b/ext/odbc/php_odbc.c @@ -1062,7 +1062,7 @@ PHP_FUNCTION(odbc_execute) ZSTR_VAL(tmpstr)[0] == '\'' && ZSTR_VAL(tmpstr)[ZSTR_LEN(tmpstr) - 1] == '\'') { - if (ZSTR_LEN(tmpstr) != strlen(ZSTR_VAL(tmpstr))) { + if (UNEXPECTED(zend_str_has_nul_byte(tmpstr))) { odbc_release_params(result, params); RETURN_FALSE; } diff --git a/ext/standard/exec.c b/ext/standard/exec.c index ce3e8565ad200..762d8bee13c22 100644 --- a/ext/standard/exec.c +++ b/ext/standard/exec.c @@ -199,13 +199,12 @@ PHPAPI int php_exec(int type, const char *cmd, zval *array, zval *return_value) static void php_exec_ex(INTERNAL_FUNCTION_PARAMETERS, int mode) /* {{{ */ { - char *cmd; - size_t cmd_len; + zend_string *cmd; zval *ret_code=NULL, *ret_array=NULL; int ret; ZEND_PARSE_PARAMETERS_START(1, (mode ? 2 : 3)) - Z_PARAM_STRING(cmd, cmd_len) + Z_PARAM_PATH_STR(cmd) Z_PARAM_OPTIONAL if (!mode) { Z_PARAM_ZVAL(ret_array) @@ -213,17 +212,13 @@ static void php_exec_ex(INTERNAL_FUNCTION_PARAMETERS, int mode) /* {{{ */ Z_PARAM_ZVAL(ret_code) ZEND_PARSE_PARAMETERS_END(); - if (!cmd_len) { + if (UNEXPECTED(!ZSTR_LEN(cmd))) { zend_argument_must_not_be_empty_error(1); RETURN_THROWS(); } - if (strlen(cmd) != cmd_len) { - zend_argument_value_error(1, "must not contain any null bytes"); - RETURN_THROWS(); - } if (!ret_array) { - ret = php_exec(mode, cmd, NULL, return_value); + ret = php_exec(mode, ZSTR_VAL(cmd), NULL, return_value); } else { if (Z_TYPE_P(Z_REFVAL_P(ret_array)) == IS_ARRAY) { ZVAL_DEREF(ret_array); @@ -235,7 +230,7 @@ static void php_exec_ex(INTERNAL_FUNCTION_PARAMETERS, int mode) /* {{{ */ } } - ret = php_exec(2, cmd, ret_array, return_value); + ret = php_exec(2, ZSTR_VAL(cmd), ret_array, return_value); } if (ret_code) { ZEND_TRY_ASSIGN_REF_LONG(ret_code, ret); @@ -280,7 +275,7 @@ PHPAPI zend_string *php_escape_shell_cmd(const zend_string *unescaped_cmd) char *p = NULL; #endif - ZEND_ASSERT(ZSTR_LEN(unescaped_cmd) == strlen(ZSTR_VAL(unescaped_cmd)) && "Must be a binary safe string"); + ZEND_ASSERT(!zend_str_has_nul_byte(unescaped_cmd) && "Must be a binary safe string"); size_t l = ZSTR_LEN(unescaped_cmd); const char *str = ZSTR_VAL(unescaped_cmd); @@ -387,7 +382,7 @@ PHPAPI zend_string *php_escape_shell_arg(const zend_string *unescaped_arg) size_t x, y = 0; zend_string *cmd; - ZEND_ASSERT(ZSTR_LEN(unescaped_arg) == strlen(ZSTR_VAL(unescaped_arg)) && "Must be a binary safe string"); + ZEND_ASSERT(!zend_str_has_nul_byte(unescaped_arg) && "Must be a binary safe string"); size_t l = ZSTR_LEN(unescaped_arg); const char *str = ZSTR_VAL(unescaped_arg); diff --git a/ext/standard/filestat.c b/ext/standard/filestat.c index 7cb54aa0aca49..85c2517ed91a0 100644 --- a/ext/standard/filestat.c +++ b/ext/standard/filestat.c @@ -749,7 +749,7 @@ PHPAPI void php_stat(zend_string *filename, int type, zval *return_value) php_stream_wrapper *wrapper = NULL; if (IS_ACCESS_CHECK(type)) { - if (!ZSTR_LEN(filename) || CHECK_NULL_PATH(ZSTR_VAL(filename), ZSTR_LEN(filename))) { + if (!ZSTR_LEN(filename) || zend_str_has_nul_byte(filename)) { if (ZSTR_LEN(filename) && !IS_EXISTS_CHECK(type)) { php_error_docref(NULL, E_WARNING, "Filename contains null byte"); } @@ -821,7 +821,7 @@ PHPAPI void php_stat(zend_string *filename, int type, zval *return_value) } if (!wrapper) { - if (!ZSTR_LEN(filename) || CHECK_NULL_PATH(ZSTR_VAL(filename), ZSTR_LEN(filename))) { + if (!ZSTR_LEN(filename) || zend_str_has_nul_byte(filename)) { if (ZSTR_LEN(filename) && !IS_EXISTS_CHECK(type)) { php_error_docref(NULL, E_WARNING, "Filename contains null byte"); } diff --git a/ext/standard/image.c b/ext/standard/image.c index 8556320e671a3..722f0a1edbd4d 100644 --- a/ext/standard/image.c +++ b/ext/standard/image.c @@ -1618,7 +1618,7 @@ static void php_getimagesize_from_any(INTERNAL_FUNCTION_PARAMETERS, int mode) { Z_PARAM_ZVAL(info) ZEND_PARSE_PARAMETERS_END(); - if (mode == FROM_PATH && CHECK_NULL_PATH(ZSTR_VAL(input), ZSTR_LEN(input))) { + if (mode == FROM_PATH && zend_str_has_nul_byte(input)) { zend_argument_value_error(1, "must not contain any null bytes"); RETURN_THROWS(); } diff --git a/ext/standard/mail.c b/ext/standard/mail.c index 41e2a02078e78..4263656a515e4 100644 --- a/ext/standard/mail.c +++ b/ext/standard/mail.c @@ -290,7 +290,7 @@ PHP_FUNCTION(mail) ZEND_PARSE_PARAMETERS_END(); if (headers_str) { - if (strlen(ZSTR_VAL(headers_str)) != ZSTR_LEN(headers_str)) { + if (UNEXPECTED(zend_str_has_nul_byte(headers_str))) { zend_argument_value_error(4, "must not contain any null bytes"); RETURN_THROWS(); } diff --git a/ext/xsl/xsltprocessor.c b/ext/xsl/xsltprocessor.c index ea0f9232aced4..95e2e2e8754be 100644 --- a/ext/xsl/xsltprocessor.c +++ b/ext/xsl/xsltprocessor.c @@ -609,7 +609,7 @@ PHP_METHOD(XSLTProcessor, setParameter) RETURN_THROWS(); } - if (UNEXPECTED(CHECK_NULL_PATH(ZSTR_VAL(string_key), ZSTR_LEN(string_key)))) { + if (UNEXPECTED(zend_str_has_nul_byte(string_key))) { zend_argument_value_error(3, "must not contain keys with any null bytes"); RETURN_THROWS(); } @@ -625,7 +625,7 @@ PHP_METHOD(XSLTProcessor, setParameter) RETURN_THROWS(); } - if (UNEXPECTED(CHECK_NULL_PATH(ZSTR_VAL(str), ZSTR_LEN(str)))) { + if (UNEXPECTED(zend_str_has_nul_byte(str))) { zend_string_release(str); zend_string_release_ex(ht_key, false); zend_argument_value_error(3, "must not contain values with any null bytes"); @@ -643,7 +643,7 @@ PHP_METHOD(XSLTProcessor, setParameter) RETURN_THROWS(); } - if (UNEXPECTED(CHECK_NULL_PATH(ZSTR_VAL(name), ZSTR_LEN(name)))) { + if (UNEXPECTED(zend_str_has_nul_byte(name))) { zend_argument_value_error(2, "must not contain any null bytes"); RETURN_THROWS(); } diff --git a/main/main.c b/main/main.c index e2973f17c248c..a1a2da56defd7 100644 --- a/main/main.c +++ b/main/main.c @@ -707,7 +707,7 @@ static PHP_INI_MH(OnUpdateMailLog) static PHP_INI_MH(OnChangeMailForceExtra) { /* Check that INI setting does not have any nul bytes */ - if (new_value && ZSTR_LEN(new_value) != strlen(ZSTR_VAL(new_value))) { + if (new_value && zend_str_has_nul_byte(new_value)) { /* TODO Emit warning? */ return FAILURE; }