From c82acefe470e0444540c63d2329d8cc675d5777c Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Sun, 19 Jul 2026 18:16:22 +0100 Subject: [PATCH 01/18] main: convert unserialize_callback_func global to zend_string* This prevents a reallocation --- ext/standard/var_unserializer.re | 4 ++-- main/main.c | 2 +- main/php_globals.h | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re index a9f1f96f366f..458ad2f12baa 100644 --- a/ext/standard/var_unserializer.re +++ b/ext/standard/var_unserializer.re @@ -1249,14 +1249,14 @@ object ":" uiv ":" ["] { } /* Check for unserialize callback */ - if ((PG(unserialize_callback_func) == NULL) || (PG(unserialize_callback_func)[0] == '\0')) { + if (PG(unserialize_callback_func) == NULL) { incomplete_class = 1; ce = PHP_IC_ENTRY; break; } /* Call unserialize callback */ - ZVAL_STRING(&user_func, PG(unserialize_callback_func)); + ZVAL_STR_COPY(&user_func, PG(unserialize_callback_func)); ZVAL_STR(&args[0], class_name); BG(serialize_lock)++; diff --git a/main/main.c b/main/main.c index 0a1d67358649..48cdc4e05d37 100644 --- a/main/main.c +++ b/main/main.c @@ -823,7 +823,7 @@ PHP_INI_BEGIN() STD_PHP_INI_BOOLEAN("auto_globals_jit", "1", PHP_INI_PERDIR|PHP_INI_SYSTEM, OnUpdateBool, auto_globals_jit, php_core_globals, core_globals) STD_PHP_INI_BOOLEAN("short_open_tag", DEFAULT_SHORT_OPEN_TAG, PHP_INI_SYSTEM|PHP_INI_PERDIR, OnUpdateBool, short_tags, zend_compiler_globals, compiler_globals) - STD_PHP_INI_ENTRY("unserialize_callback_func", NULL, PHP_INI_ALL, OnUpdateString, unserialize_callback_func, php_core_globals, core_globals) + STD_PHP_INI_ENTRY("unserialize_callback_func", NULL, PHP_INI_ALL, OnUpdateStrNotEmpty, unserialize_callback_func, php_core_globals, core_globals) STD_PHP_INI_ENTRY("serialize_precision", "-1", PHP_INI_ALL, OnSetSerializePrecision, serialize_precision, php_core_globals, core_globals) STD_PHP_INI_ENTRY("arg_separator.output", "&", PHP_INI_ALL, OnUpdateStrNotEmpty, arg_separator.output, php_core_globals, core_globals) STD_PHP_INI_ENTRY("arg_separator.input", "&", PHP_INI_SYSTEM|PHP_INI_PERDIR, OnUpdateStrNotEmpty, arg_separator.input, php_core_globals, core_globals) diff --git a/main/php_globals.h b/main/php_globals.h index 0bab9fc95c14..537e1cfd2dc7 100644 --- a/main/php_globals.h +++ b/main/php_globals.h @@ -67,7 +67,7 @@ struct _php_core_globals { char *output_handler; - char *unserialize_callback_func; + zend_string *unserialize_callback_func; zend_long serialize_precision; zend_long memory_limit; From 6877f77c628c09899af2daecea33cc0fb127e58a Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Sun, 19 Jul 2026 18:22:38 +0100 Subject: [PATCH 02/18] main: convert last_error_lineno global to type uint32_t As this is the expected type for lineno everywhere else in the engine --- ext/standard/basic_functions.c | 2 +- main/main.c | 2 +- main/php_globals.h | 2 +- sapi/cli/php_cli_server.c | 2 +- sapi/phpdbg/phpdbg_info.c | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index e81050323754..f8fcc01bf5bb 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -1434,7 +1434,7 @@ PHP_FUNCTION(error_get_last) ZVAL_STR_COPY(&tmp, PG(last_error_file)); zend_hash_update(Z_ARR_P(return_value), ZSTR_KNOWN(ZEND_STR_FILE), &tmp); - ZVAL_LONG(&tmp, PG(last_error_lineno)); + ZVAL_LONG(&tmp, (zend_long)PG(last_error_lineno)); zend_hash_update(Z_ARR_P(return_value), ZSTR_KNOWN(ZEND_STR_LINE), &tmp); if (!Z_ISUNDEF(EG(last_fatal_error_backtrace))) { diff --git a/main/main.c b/main/main.c index 48cdc4e05d37..3c079cb7fd77 100644 --- a/main/main.c +++ b/main/main.c @@ -1315,7 +1315,7 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c * be NULL if PG(last_error_message) is not NULL */ if (!zend_string_equals(PG(last_error_message), message) || (!PG(ignore_repeated_source) - && ((PG(last_error_lineno) != (int)error_lineno) + && ((PG(last_error_lineno) != error_lineno) || !zend_string_equals(PG(last_error_file), error_filename)))) { display = 1; } else { diff --git a/main/php_globals.h b/main/php_globals.h index 537e1cfd2dc7..5e6b7a30e85b 100644 --- a/main/php_globals.h +++ b/main/php_globals.h @@ -136,7 +136,7 @@ struct _php_core_globals { bool report_zend_debug; int last_error_type; - int last_error_lineno; + uint32_t last_error_lineno; zend_string *last_error_message; zend_string *last_error_file; diff --git a/sapi/cli/php_cli_server.c b/sapi/cli/php_cli_server.c index e06f30b9e2d2..85028cc03f7c 100644 --- a/sapi/cli/php_cli_server.c +++ b/sapi/cli/php_cli_server.c @@ -1219,7 +1219,7 @@ static void php_cli_server_log_response(php_cli_server_client *client, int statu /* error */ if (append_error_message) { - spprintf(&error_buf, 0, " - %s in %s on line %d", + spprintf(&error_buf, 0, " - %s in %s on line %" PRIu32, ZSTR_VAL(PG(last_error_message)), ZSTR_VAL(PG(last_error_file)), PG(last_error_lineno)); if (!error_buf) { efree(basic_buf); diff --git a/sapi/phpdbg/phpdbg_info.c b/sapi/phpdbg/phpdbg_info.c index ee579e81df02..2100d6e7dca4 100644 --- a/sapi/phpdbg/phpdbg_info.c +++ b/sapi/phpdbg/phpdbg_info.c @@ -81,7 +81,7 @@ PHPDBG_INFO(error) /* {{{ */ { if (PG(last_error_message)) { phpdbg_try_access { - phpdbg_writeln("Last error: %s at %s line %d", + phpdbg_writeln("Last error: %s at %s line %" PRIu32, ZSTR_VAL(PG(last_error_message)), ZSTR_VAL(PG(last_error_file)), PG(last_error_lineno)); From 1de8f4f16d71d33f432e7f24903aba53d9738256 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Sun, 19 Jul 2026 18:23:52 +0100 Subject: [PATCH 03/18] main: group Windows globals together --- main/php_globals.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/main/php_globals.h b/main/php_globals.h index 5e6b7a30e85b..333daab69ff6 100644 --- a/main/php_globals.h +++ b/main/php_globals.h @@ -158,12 +158,10 @@ struct _php_core_globals { bool in_error_log; bool allow_url_include; -#ifdef PHP_WIN32 - bool com_initialized; -#endif bool in_user_include; #ifdef PHP_WIN32 + bool com_initialized; bool windows_show_crt_warning; #endif From 1b204626c5eb9dc6b15cb4d156cc14a2b633c7f9 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Sun, 19 Jul 2026 18:48:03 +0100 Subject: [PATCH 04/18] main: convert doc_root global to zend_string* This remove a strlen() computation. While at it clarify path concatenation code by using the zend_string_concat{2|3} APIs rather than a memcpy and strncpy calls --- main/fopen_wrappers.c | 47 ++++++++++++++++++++++++++++------------- main/main.c | 2 +- main/php_globals.h | 2 +- sapi/cgi/cgi_main.c | 2 +- sapi/fpm/fpm/fpm_main.c | 2 +- 5 files changed, 36 insertions(+), 19 deletions(-) diff --git a/main/fopen_wrappers.c b/main/fopen_wrappers.c index c0f88ad3b5ba..562cf1c63b96 100644 --- a/main/fopen_wrappers.c +++ b/main/fopen_wrappers.c @@ -359,7 +359,6 @@ PHPAPI int php_fopen_primary_script(zend_file_handle *file_handle) char *path_info; zend_string *filename = NULL; zend_string *resolved_path = NULL; - size_t length; bool orig_display_errors; memset(file_handle, 0, sizeof(zend_file_handle)); @@ -372,7 +371,7 @@ PHPAPI int php_fopen_primary_script(zend_file_handle *file_handle) if (s) { /* if there is no path name after the file, do not bother */ char user[32]; /* to try open the directory */ - length = s - (path_info + 2); + size_t length = s - (path_info + 2); if (length > sizeof(user) - 1) { length = sizeof(user) - 1; } @@ -421,19 +420,37 @@ PHPAPI int php_fopen_primary_script(zend_file_handle *file_handle) } } else #endif - if (PG(doc_root) && path_info && (length = strlen(PG(doc_root))) && - IS_ABSOLUTE_PATH(PG(doc_root), length)) { - size_t path_len = strlen(path_info); - filename = zend_string_alloc(length + path_len + 2, 0); - memcpy(ZSTR_VAL(filename), PG(doc_root), length); - if (!IS_SLASH(ZSTR_VAL(filename)[length - 1])) { /* length is never 0 */ - ZSTR_VAL(filename)[length++] = PHP_DIR_SEPARATOR; - } - if (IS_SLASH(path_info[0])) { - length--; - } - strncpy(ZSTR_VAL(filename) + length, path_info, path_len + 1); - ZSTR_LEN(filename) = length + path_len; + if (PG(doc_root) && path_info && IS_ABSOLUTE_PATH(ZSTR_VAL(PG(doc_root)), ZSTR_LEN(PG(doc_root)))) { + const size_t path_len = strlen(path_info); + + /* We need to concatenate two paths together, there are 3 situations: + * - No trailing slash AND no leading slash + * - Trailing slash AND leading slash + * - Either a trailing slash OR a leading slash + * In the first case we need to add a slash, in the second one we need to skip the leading slash, + * and in the third we can just concatenate them together */ + const unsigned int nb_slashes = IS_SLASH(ZSTR_VAL(PG(doc_root))[ZSTR_LEN(PG(doc_root)) - 1]) + IS_SLASH(path_info[0]); + switch (nb_slashes) { + case 0: + filename = zend_string_concat3( + ZSTR_VAL(PG(doc_root)), ZSTR_LEN(PG(doc_root)), + ZEND_STRL("/"), + path_info, path_len + ); + break; + case 1: + filename = zend_string_concat2( + ZSTR_VAL(PG(doc_root)), ZSTR_LEN(PG(doc_root)), + path_info, path_len + ); + break; + case 2: + filename = zend_string_concat2( + ZSTR_VAL(PG(doc_root)), ZSTR_LEN(PG(doc_root)), + path_info + 1, path_len -1 + ); + break; + } } else if (SG(request_info).path_translated) { filename = zend_string_init(SG(request_info).path_translated, strlen(SG(request_info).path_translated), 0); diff --git a/main/main.c b/main/main.c index 3c079cb7fd77..5d26de4edc5e 100644 --- a/main/main.c +++ b/main/main.c @@ -830,7 +830,7 @@ PHP_INI_BEGIN() STD_PHP_INI_ENTRY("auto_append_file", NULL, PHP_INI_SYSTEM|PHP_INI_PERDIR, OnUpdateString, auto_append_file, php_core_globals, core_globals) STD_PHP_INI_ENTRY("auto_prepend_file", NULL, PHP_INI_SYSTEM|PHP_INI_PERDIR, OnUpdateString, auto_prepend_file, php_core_globals, core_globals) - STD_PHP_INI_ENTRY("doc_root", NULL, PHP_INI_SYSTEM, OnUpdateStringUnempty, doc_root, php_core_globals, core_globals) + STD_PHP_INI_ENTRY("doc_root", NULL, PHP_INI_SYSTEM, OnUpdateStrNotEmpty, doc_root, php_core_globals, core_globals) STD_PHP_INI_ENTRY("default_charset", PHP_DEFAULT_CHARSET, PHP_INI_ALL, OnUpdateDefaultCharset, default_charset, sapi_globals_struct, sapi_globals) STD_PHP_INI_ENTRY("default_mimetype", SAPI_DEFAULT_MIMETYPE, PHP_INI_ALL, OnUpdateDefaultMimeTye, default_mimetype, sapi_globals_struct, sapi_globals) STD_PHP_INI_ENTRY("internal_encoding", NULL, PHP_INI_ALL, OnUpdateInternalEncoding, internal_encoding, php_core_globals, core_globals) diff --git a/main/php_globals.h b/main/php_globals.h index 333daab69ff6..74639ffe0d47 100644 --- a/main/php_globals.h +++ b/main/php_globals.h @@ -76,7 +76,7 @@ struct _php_core_globals { char *error_log; - char *doc_root; + zend_string *doc_root; char *user_dir; char *include_path; char *open_basedir; diff --git a/sapi/cgi/cgi_main.c b/sapi/cgi/cgi_main.c index 841f45b207a8..a79eada2728b 100644 --- a/sapi/cgi/cgi_main.c +++ b/sapi/cgi/cgi_main.c @@ -1221,7 +1221,7 @@ static void init_request_info(fcgi_request *request) size_t script_path_translated_len; if (!env_document_root && PG(doc_root)) { - env_document_root = CGI_PUTENV("DOCUMENT_ROOT", PG(doc_root)); + env_document_root = CGI_PUTENV("DOCUMENT_ROOT", ZSTR_VAL(PG(doc_root))); /* fix docroot */ TRANSLATE_SLASHES(env_document_root); } diff --git a/sapi/fpm/fpm/fpm_main.c b/sapi/fpm/fpm/fpm_main.c index 9b7cbcf16f06..655d66ce5f06 100644 --- a/sapi/fpm/fpm/fpm_main.c +++ b/sapi/fpm/fpm/fpm_main.c @@ -1078,7 +1078,7 @@ static void init_request_info(void) int script_path_translated_len; if (!env_document_root && PG(doc_root)) { - env_document_root = FCGI_PUTENV(request, "DOCUMENT_ROOT", PG(doc_root)); + env_document_root = FCGI_PUTENV(request, "DOCUMENT_ROOT", ZSTR_VAL(PG(doc_root))); } if (!apache_was_here && env_path_translated != NULL && env_redirect_url != NULL && From 9362fc793cc17ff01efddbfbb2bb2b716daad0fc Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Sun, 19 Jul 2026 18:29:28 +0100 Subject: [PATCH 05/18] main: convert error_log global to zend_string* Allows us to convert a strcmp() call to zend_string_equals_literal() which is less confusing --- main/main.c | 9 +++++---- main/php_globals.h | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/main/main.c b/main/main.c index 5d26de4edc5e..7c5f8baae304 100644 --- a/main/main.c +++ b/main/main.c @@ -706,8 +706,9 @@ static PHP_INI_MH(OnUpdateErrorLog) return FAILURE; } } - char **p = ZEND_INI_GET_ADDR(); - *p = new_value && ZSTR_LEN(new_value) > 0 ? ZSTR_VAL(new_value) : NULL; + + zend_string **p = ZEND_INI_GET_ADDR(); + *p = new_value && ZSTR_LEN(new_value) > 0 ? new_value : NULL; return SUCCESS; } /* }}} */ @@ -940,7 +941,7 @@ PHPAPI ZEND_COLD void php_log_err_with_severity(const char *log_message, int sys int error_log_mode; #ifdef HAVE_SYSLOG_H - if (!strcmp(PG(error_log), "syslog")) { + if (zend_string_equals_literal(PG(error_log), "syslog")) { php_syslog(syslog_type_int, "%s", log_message); PG(in_error_log) = 0; return; @@ -953,7 +954,7 @@ PHPAPI ZEND_COLD void php_log_err_with_severity(const char *log_message, int sys error_log_mode = PG(error_log_mode); } - fd = VCWD_OPEN_MODE(PG(error_log), O_CREAT | O_APPEND | O_WRONLY, error_log_mode); + fd = VCWD_OPEN_MODE(ZSTR_VAL(PG(error_log)), O_CREAT | O_APPEND | O_WRONLY, error_log_mode); if (fd != -1) { char *tmp; size_t len; diff --git a/main/php_globals.h b/main/php_globals.h index 74639ffe0d47..45a1b0e44bf5 100644 --- a/main/php_globals.h +++ b/main/php_globals.h @@ -74,7 +74,7 @@ struct _php_core_globals { zend_long max_memory_limit; zend_long max_input_time; - char *error_log; + zend_string *error_log; zend_string *doc_root; char *user_dir; From e0221be81e39860e83867fadd67115e1d2c992c1 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Sun, 19 Jul 2026 18:14:44 +0100 Subject: [PATCH 06/18] main: convert output_handler global to zend_string* This prevents a reallocation --- main/main.c | 6 +++--- main/php_globals.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/main/main.c b/main/main.c index 7c5f8baae304..4fa34258b983 100644 --- a/main/main.c +++ b/main/main.c @@ -819,7 +819,7 @@ PHP_INI_BEGIN() STD_PHP_INI_BOOLEAN("report_memleaks", "1", PHP_INI_ALL, OnUpdateReportMemleaks, report_memleaks, php_core_globals, core_globals) STD_PHP_INI_BOOLEAN("report_zend_debug", "0", PHP_INI_ALL, OnUpdateBool, report_zend_debug, php_core_globals, core_globals) STD_PHP_INI_ENTRY("output_buffering", "0", PHP_INI_PERDIR|PHP_INI_SYSTEM, OnUpdateLong, output_buffering, php_core_globals, core_globals) - STD_PHP_INI_ENTRY("output_handler", NULL, PHP_INI_PERDIR|PHP_INI_SYSTEM, OnUpdateString, output_handler, php_core_globals, core_globals) + STD_PHP_INI_ENTRY("output_handler", NULL, PHP_INI_PERDIR|PHP_INI_SYSTEM, OnUpdateStrNotEmpty, output_handler, php_core_globals, core_globals) STD_PHP_INI_BOOLEAN("register_argc_argv", "0", PHP_INI_PERDIR|PHP_INI_SYSTEM, OnUpdateBool, register_argc_argv, php_core_globals, core_globals) STD_PHP_INI_BOOLEAN("auto_globals_jit", "1", PHP_INI_PERDIR|PHP_INI_SYSTEM, OnUpdateBool, auto_globals_jit, php_core_globals, core_globals) STD_PHP_INI_BOOLEAN("short_open_tag", DEFAULT_SHORT_OPEN_TAG, PHP_INI_SYSTEM|PHP_INI_PERDIR, OnUpdateBool, short_tags, zend_compiler_globals, compiler_globals) @@ -1886,10 +1886,10 @@ zend_result php_request_startup(void) sapi_add_header(SAPI_PHP_VERSION_HEADER, sizeof(SAPI_PHP_VERSION_HEADER)-1, 1); } - if (PG(output_handler) && PG(output_handler)[0]) { + if (PG(output_handler)) { zval oh; - ZVAL_STRING(&oh, PG(output_handler)); + ZVAL_STR_COPY(&oh, PG(output_handler)); php_output_start_user(&oh, 0, PHP_OUTPUT_HANDLER_STDFLAGS); zval_ptr_dtor(&oh); } else if (PG(output_buffering)) { diff --git a/main/php_globals.h b/main/php_globals.h index 45a1b0e44bf5..e3cfe38a63fd 100644 --- a/main/php_globals.h +++ b/main/php_globals.h @@ -65,7 +65,7 @@ struct _php_core_globals { bool ignore_repeated_source; bool report_memleaks; - char *output_handler; + zend_string *output_handler; zend_string *unserialize_callback_func; zend_long serialize_precision; From d75f79e2acfb28511923f90647ae7908933feb04 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Mon, 20 Jul 2026 22:17:04 +0100 Subject: [PATCH 07/18] streams: refactor _php_stream_open_wrapper_ex() to use early returns --- main/streams/streams.c | 140 ++++++++++++++++++++++++----------------- 1 file changed, 83 insertions(+), 57 deletions(-) diff --git a/main/streams/streams.c b/main/streams/streams.c index 740a1d7a6e06..3ec92b2a221f 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -2118,14 +2118,28 @@ PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mod path_to_open = path; wrapper = php_stream_locate_url_wrapper(path, &path_to_open, options); - if ((options & STREAM_USE_URL) && (!wrapper || !wrapper->is_url)) { - if (wrapper) { - php_stream_wrapper_warn(wrapper, context, options, - ProtocolUnsupported, - "This function may only be used against URLs"); - } else { - php_error_docref(NULL, E_WARNING, "This function may only be used against URLs"); + if (UNEXPECTED(!wrapper)) { + php_stream_wrapper_warn_name(PHP_STREAM_ERROR_WRAPPER_DEFAULT_NAME, context, options, OpenFailed, + "Failed to open stream: no suitable wrapper could be found"); + if (resolved_path) { + zend_string_release_ex(resolved_path, 0); + } + return NULL; + } + if ((options & STREAM_USE_URL) && !wrapper->is_url) { + php_stream_wrapper_warn(wrapper, context, options, + ProtocolUnsupported, + "This function may only be used against URLs"); + if (resolved_path) { + zend_string_release_ex(resolved_path, 0); } + return NULL; + } + + if (!wrapper->wops->stream_opener) { + php_stream_wrapper_warn(wrapper, context, options, + NoOpener, + "wrapper does not support stream open"); if (resolved_path) { zend_string_release_ex(resolved_path, 0); } @@ -2134,56 +2148,74 @@ PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mod /* wrapper name needs to be stored as wrapper can be removed in opener (user stream) */ char *wrapper_name = pestrdup(PHP_STREAM_ERROR_WRAPPER_NAME(wrapper), persistent); - if (wrapper) { - if (!wrapper->wops->stream_opener) { - php_stream_wrapper_log_warn(wrapper, context, options & ~REPORT_ERRORS, - NoOpener, - "wrapper does not support stream open"); - } else { - stream = wrapper->wops->stream_opener(wrapper, - path_to_open, mode, options & ~REPORT_ERRORS, - opened_path, context STREAMS_REL_CC); - } + stream = wrapper->wops->stream_opener(wrapper, + path_to_open, mode, options & ~REPORT_ERRORS, + opened_path, context STREAMS_REL_CC); - /* if the caller asked for a persistent stream but the wrapper did not - * return one, force an error here */ - if (stream && persistent && !stream->is_persistent) { - php_stream_wrapper_log_warn(wrapper, context, options & ~REPORT_ERRORS, - PersistentNotSupported, - "wrapper does not support persistent streams"); - php_stream_close(stream); - stream = NULL; + if (UNEXPECTED(!stream)) { + if (options & REPORT_ERRORS) { + php_stream_display_wrapper_name_errors(wrapper_name, context, PHP_STREAM_EC(OpenFailed), + "Failed to open stream"); + if (opened_path && *opened_path) { + zend_string_release_ex(*opened_path, 0); + *opened_path = NULL; + } } - - if (stream) { - stream->wrapper = wrapper; + php_stream_tidy_wrapper_name_error_log(wrapper_name); + pefree(wrapper_name, persistent); + if (resolved_path) { + zend_string_release_ex(resolved_path, 0); } + return NULL; } - if (stream) { - if (opened_path && !*opened_path && resolved_path) { - *opened_path = resolved_path; - resolved_path = NULL; + /* if the caller asked for a persistent stream but the wrapper did not + * return one, force an error here */ + if (persistent && !stream->is_persistent) { + php_stream_wrapper_log_warn(wrapper, context, options & ~REPORT_ERRORS, + PersistentNotSupported, + "wrapper does not support persistent streams"); + php_stream_close(stream); + if (options & REPORT_ERRORS) { + php_stream_display_wrapper_name_errors(wrapper_name, context, PHP_STREAM_EC(OpenFailed), + "Failed to open stream"); + if (opened_path && *opened_path) { + zend_string_release_ex(*opened_path, 0); + *opened_path = NULL; + } } - if (stream->orig_path) { - pefree(stream->orig_path, persistent); + php_stream_tidy_wrapper_name_error_log(wrapper_name); + pefree(wrapper_name, persistent); + if (resolved_path) { + zend_string_release_ex(resolved_path, 0); } - stream->orig_path = pestrdup(path, persistent); + return NULL; + } + + stream->wrapper = wrapper; + + if (opened_path && !*opened_path && resolved_path) { + *opened_path = resolved_path; + resolved_path = NULL; + } + if (stream->orig_path) { + pefree(stream->orig_path, persistent); + } + stream->orig_path = pestrdup(path, persistent); #if ZEND_DEBUG stream->open_filename = __zend_orig_filename ? __zend_orig_filename : __zend_filename; stream->open_lineno = __zend_orig_lineno ? __zend_orig_lineno : __zend_lineno; #endif - /* Attach an explicitly provided context to the stream, but never the - * default context: sharing it by reference would let a later - * stream_context_set_option() on the stream mutate the global default - * context, leaking options into every other stream. Stream errors fall - * back to the default context on their own when the stream has none. */ - if (stream->ctx == NULL && context != NULL && context != FG(default_context) && !persistent) { - php_stream_context_set(stream, context); - } + /* Attach an explicitly provided context to the stream, but never the + * default context: sharing it by reference would let a later + * stream_context_set_option() on the stream mutate the global default + * context, leaking options into every other stream. Stream errors fall + * back to the default context on their own when the stream has none. */ + if (stream->ctx == NULL && context != NULL && context != FG(default_context) && !persistent) { + php_stream_context_set(stream, context); } - if (stream != NULL && (options & STREAM_MUST_SEEK)) { + if (options & STREAM_MUST_SEEK) { php_stream *newstream; switch(php_stream_make_seekable_rel(stream, &newstream, @@ -2207,16 +2239,19 @@ PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mod return newstream; default: php_stream_close(stream); - stream = NULL; php_stream_wrapper_warn(wrapper, context, options, SeekNotSupported, "could not make seekable - %s", path); - /* We do not want multiple errors so we negate it */ - options &= ~REPORT_ERRORS; + php_stream_tidy_wrapper_name_error_log(wrapper_name); + pefree(wrapper_name, persistent); + if (resolved_path) { + zend_string_release_ex(resolved_path, 0); + } + return NULL; } } - if (stream && stream->ops->seek && (stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0 && strchr(mode, 'a') && stream->position == 0) { + if (stream->ops->seek && (stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0 && strchr(mode, 'a') && stream->position == 0) { zend_off_t newpos = 0; /* if opened for append, we need to revise our idea of the initial file position */ @@ -2225,15 +2260,6 @@ PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mod } } - if (stream == NULL && (options & REPORT_ERRORS)) { - php_stream_display_wrapper_name_errors(wrapper_name, context, PHP_STREAM_EC(OpenFailed), - "Failed to open stream"); - if (opened_path && *opened_path) { - zend_string_release_ex(*opened_path, 0); - *opened_path = NULL; - } - } - php_stream_tidy_wrapper_name_error_log(wrapper_name); pefree(wrapper_name, persistent); if (resolved_path) { zend_string_release_ex(resolved_path, 0); From f9ac6c13c427ead81d26f068e6b00ca891443054 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Mon, 20 Jul 2026 22:23:52 +0100 Subject: [PATCH 08/18] streams: use php_stream_wrapper_warn() in _php_stream_open_wrapper_ex() Instead of the old way of logging errors and displaying them use the new APIs. --- main/streams/streams.c | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/main/streams/streams.c b/main/streams/streams.c index 3ec92b2a221f..506a23b3bf74 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -2137,9 +2137,8 @@ PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mod } if (!wrapper->wops->stream_opener) { - php_stream_wrapper_warn(wrapper, context, options, - NoOpener, - "wrapper does not support stream open"); + php_stream_wrapper_warn(wrapper, context, options, NoOpener, + "Failed to open stream: wrapper does not support stream open"); if (resolved_path) { zend_string_release_ex(resolved_path, 0); } @@ -2172,19 +2171,15 @@ PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mod /* if the caller asked for a persistent stream but the wrapper did not * return one, force an error here */ if (persistent && !stream->is_persistent) { - php_stream_wrapper_log_warn(wrapper, context, options & ~REPORT_ERRORS, - PersistentNotSupported, - "wrapper does not support persistent streams"); + php_stream_wrapper_warn(wrapper, context, options, PersistentNotSupported, + "Failed to open stream: wrapper does not support persistent streams"); php_stream_close(stream); if (options & REPORT_ERRORS) { - php_stream_display_wrapper_name_errors(wrapper_name, context, PHP_STREAM_EC(OpenFailed), - "Failed to open stream"); if (opened_path && *opened_path) { zend_string_release_ex(*opened_path, 0); *opened_path = NULL; } } - php_stream_tidy_wrapper_name_error_log(wrapper_name); pefree(wrapper_name, persistent); if (resolved_path) { zend_string_release_ex(resolved_path, 0); @@ -2242,7 +2237,6 @@ PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mod php_stream_wrapper_warn(wrapper, context, options, SeekNotSupported, "could not make seekable - %s", path); - php_stream_tidy_wrapper_name_error_log(wrapper_name); pefree(wrapper_name, persistent); if (resolved_path) { zend_string_release_ex(resolved_path, 0); From 605ff6e11bc94b4bb35211827b39358910c9242d Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Tue, 21 Jul 2026 00:39:19 +0100 Subject: [PATCH 09/18] streams: use common cleanup code in _php_stream_open_wrapper_ex() --- main/streams/streams.c | 73 ++++++++++++------------------------------ 1 file changed, 21 insertions(+), 52 deletions(-) diff --git a/main/streams/streams.c b/main/streams/streams.c index 506a23b3bf74..192689ce036f 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -2108,10 +2108,7 @@ PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mod options &= ~USE_PATH; } if (EG(exception)) { - if (resolved_path) { - zend_string_release_ex(resolved_path, false); - } - return NULL; + goto cleanup_no_wrapper_name; } } @@ -2121,28 +2118,19 @@ PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mod if (UNEXPECTED(!wrapper)) { php_stream_wrapper_warn_name(PHP_STREAM_ERROR_WRAPPER_DEFAULT_NAME, context, options, OpenFailed, "Failed to open stream: no suitable wrapper could be found"); - if (resolved_path) { - zend_string_release_ex(resolved_path, 0); - } - return NULL; + goto cleanup_no_wrapper_name; } if ((options & STREAM_USE_URL) && !wrapper->is_url) { php_stream_wrapper_warn(wrapper, context, options, ProtocolUnsupported, "This function may only be used against URLs"); - if (resolved_path) { - zend_string_release_ex(resolved_path, 0); - } - return NULL; + goto cleanup_no_wrapper_name; } if (!wrapper->wops->stream_opener) { php_stream_wrapper_warn(wrapper, context, options, NoOpener, "Failed to open stream: wrapper does not support stream open"); - if (resolved_path) { - zend_string_release_ex(resolved_path, 0); - } - return NULL; + goto cleanup_no_wrapper_name; } /* wrapper name needs to be stored as wrapper can be removed in opener (user stream) */ @@ -2155,17 +2143,9 @@ PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mod if (options & REPORT_ERRORS) { php_stream_display_wrapper_name_errors(wrapper_name, context, PHP_STREAM_EC(OpenFailed), "Failed to open stream"); - if (opened_path && *opened_path) { - zend_string_release_ex(*opened_path, 0); - *opened_path = NULL; - } } php_stream_tidy_wrapper_name_error_log(wrapper_name); - pefree(wrapper_name, persistent); - if (resolved_path) { - zend_string_release_ex(resolved_path, 0); - } - return NULL; + goto cleanup; } /* if the caller asked for a persistent stream but the wrapper did not @@ -2174,17 +2154,8 @@ PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mod php_stream_wrapper_warn(wrapper, context, options, PersistentNotSupported, "Failed to open stream: wrapper does not support persistent streams"); php_stream_close(stream); - if (options & REPORT_ERRORS) { - if (opened_path && *opened_path) { - zend_string_release_ex(*opened_path, 0); - *opened_path = NULL; - } - } - pefree(wrapper_name, persistent); - if (resolved_path) { - zend_string_release_ex(resolved_path, 0); - } - return NULL; + stream = NULL; + goto cleanup; } stream->wrapper = wrapper; @@ -2217,31 +2188,21 @@ PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mod (options & STREAM_WILL_CAST) ? PHP_STREAM_PREFER_STDIO : PHP_STREAM_NO_PREFERENCE)) { case PHP_STREAM_UNCHANGED: - if (resolved_path) { - zend_string_release_ex(resolved_path, 0); - } - pefree(wrapper_name, persistent); - return stream; + goto cleanup; case PHP_STREAM_RELEASED: if (newstream->orig_path) { pefree(newstream->orig_path, persistent); } newstream->orig_path = pestrdup(path, persistent); - if (resolved_path) { - zend_string_release_ex(resolved_path, 0); - } - pefree(wrapper_name, persistent); - return newstream; + stream = newstream; + goto cleanup; default: - php_stream_close(stream); php_stream_wrapper_warn(wrapper, context, options, SeekNotSupported, "could not make seekable - %s", path); - pefree(wrapper_name, persistent); - if (resolved_path) { - zend_string_release_ex(resolved_path, 0); - } - return NULL; + php_stream_close(stream); + stream = NULL; + goto cleanup; } } @@ -2254,10 +2215,18 @@ PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mod } } +cleanup: pefree(wrapper_name, persistent); +cleanup_no_wrapper_name: if (resolved_path) { zend_string_release_ex(resolved_path, 0); } + if (stream == NULL) { + if (opened_path && *opened_path) { + zend_string_release_ex(*opened_path, 0); + *opened_path = NULL; + } + } return stream; } /* }}} */ From 28d321646f991f115e2d146c60d87e02feb77d38 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Thu, 30 Jul 2026 12:15:23 +0200 Subject: [PATCH 10/18] [skip ci] Mark another preloading test as opcache.file_cache_only=0 shm is mandatory for preloading. --- Zend/tests/partial_application/constexpr_015.phpt | 1 + 1 file changed, 1 insertion(+) diff --git a/Zend/tests/partial_application/constexpr_015.phpt b/Zend/tests/partial_application/constexpr_015.phpt index 2449adb9e4b7..22bc8dc720e8 100644 --- a/Zend/tests/partial_application/constexpr_015.phpt +++ b/Zend/tests/partial_application/constexpr_015.phpt @@ -5,6 +5,7 @@ opcache.enable=1 opcache.enable_cli=1 opcache.optimization_level=-1 opcache.preload={PWD}/constexpr_015.inc +opcache.file_cache_only=0 --SKIPIF-- Date: Thu, 30 Jul 2026 18:05:42 +0700 Subject: [PATCH 11/18] ext/mysqli: applied fixers to improve test robustness (#22929) --- ext/mysqli/tests/bug36802.phpt | 8 ++++---- ext/mysqli/tests/bug62885.phpt | 6 +++--- ext/mysqli/tests/gh17900.phpt | 4 ++-- ext/mysqli/tests/ghsa-h35g-vwh6-m678-auth-message.phpt | 4 ++-- ext/mysqli/tests/mysqli_driver/write_property.phpt | 8 ++++---- ext/mysqli/tests/mysqli_driver/write_property_strict.phpt | 8 ++++---- ext/mysqli/tests/mysqli_driver_unclonable.phpt | 2 +- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/ext/mysqli/tests/bug36802.phpt b/ext/mysqli/tests/bug36802.phpt index 47642087acb3..fdfc37d862be 100644 --- a/ext/mysqli/tests/bug36802.phpt +++ b/ext/mysqli/tests/bug36802.phpt @@ -18,7 +18,7 @@ mysqli try { $mysql->set_charset('utf8'); } catch (Error $exception) { - echo $exception->getMessage() . "\n"; + echo $exception::class, ': ', $exception->getMessage(), "\n"; } } else { $x[0] = false; @@ -27,7 +27,7 @@ mysqli try { $mysql->query("SELECT 'foo' FROM DUAL"); } catch (Error $exception) { - echo $exception->getMessage() . "\n"; + echo $exception::class, ': ', $exception->getMessage(), "\n"; } /* following operations should work */ @@ -39,8 +39,8 @@ mysqli var_dump($x); ?> --EXPECT-- -mysqli object is not fully initialized -mysqli object is not fully initialized +Error: mysqli object is not fully initialized +Error: mysqli object is not fully initialized array(2) { [1]=> string(0) "" diff --git a/ext/mysqli/tests/bug62885.phpt b/ext/mysqli/tests/bug62885.phpt index 8666795bd87c..9d2585b2fce7 100644 --- a/ext/mysqli/tests/bug62885.phpt +++ b/ext/mysqli/tests/bug62885.phpt @@ -13,19 +13,19 @@ $tablica = array(); try { $test1 = mysqli_poll($test2, $test3, $tablica, 0); } catch (\ValueError $e) { - echo $e->getMessage() . \PHP_EOL; + echo $e::class, ': ', $e->getMessage(), PHP_EOL; } $test2 = array(); try { $test1 = mysqli_poll($test2, $test3, $tablica, 0); } catch (\ValueError $e) { - echo $e->getMessage() . \PHP_EOL; + echo $e::class, ': ', $e->getMessage(), PHP_EOL; } echo "okey"; ?> --EXPECTF-- -No stream arrays were passed +ValueError: No stream arrays were passed Warning: mysqli_poll(): No stream arrays were passed in %s on line %d okey diff --git a/ext/mysqli/tests/gh17900.phpt b/ext/mysqli/tests/gh17900.phpt index ed099fa7e852..90aa3d7edfe0 100644 --- a/ext/mysqli/tests/gh17900.phpt +++ b/ext/mysqli/tests/gh17900.phpt @@ -9,8 +9,8 @@ $mysqli = new mysqli(); try { $mysqli->__construct('doesnotexist'); } catch (Error $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), "\n"; } ?> --EXPECT-- -Cannot call constructor twice +Error: Cannot call constructor twice diff --git a/ext/mysqli/tests/ghsa-h35g-vwh6-m678-auth-message.phpt b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-auth-message.phpt index 666f47f4199f..84c38dd862db 100644 --- a/ext/mysqli/tests/ghsa-h35g-vwh6-m678-auth-message.phpt +++ b/ext/mysqli/tests/ghsa-h35g-vwh6-m678-auth-message.phpt @@ -18,7 +18,7 @@ try { $info = mysqli_info($conn); var_dump($info); } catch (Exception $e) { - echo $e->getMessage() . PHP_EOL; + echo $e::class, ': ', $e->getMessage(), PHP_EOL; } $process->terminate(); @@ -33,5 +33,5 @@ print "done!"; [*] Sending - Malicious OK Auth Response [Extract heap through buffer over-read]: 0900000200000002000000fcff Warning: mysqli::__construct(): OK packet message length is past the packet size in %s on line %d -Unknown error while trying to connect via tcp://127.0.0.1:%d +mysqli_sql_exception: Unknown error while trying to connect via tcp://127.0.0.1:%d done! diff --git a/ext/mysqli/tests/mysqli_driver/write_property.phpt b/ext/mysqli/tests/mysqli_driver/write_property.phpt index 456dd40c81df..7bb41440b39f 100644 --- a/ext/mysqli/tests/mysqli_driver/write_property.phpt +++ b/ext/mysqli/tests/mysqli_driver/write_property.phpt @@ -10,7 +10,7 @@ try { /* Read-only property */ $driver->client_info = 'test'; } catch (Error $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), "\n"; } $driver->report_mode = "1"; @@ -18,11 +18,11 @@ var_dump($driver->report_mode); try { $driver->report_mode = []; } catch (Error $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), "\n"; } ?> --EXPECT-- -Cannot write read-only property mysqli_driver::$client_info +Error: Cannot write read-only property mysqli_driver::$client_info int(1) -Cannot assign array to property mysqli_driver::$report_mode of type int +TypeError: Cannot assign array to property mysqli_driver::$report_mode of type int diff --git a/ext/mysqli/tests/mysqli_driver/write_property_strict.phpt b/ext/mysqli/tests/mysqli_driver/write_property_strict.phpt index 41645ff61c28..996048ac1f34 100644 --- a/ext/mysqli/tests/mysqli_driver/write_property_strict.phpt +++ b/ext/mysqli/tests/mysqli_driver/write_property_strict.phpt @@ -12,16 +12,16 @@ try { /* Read-only property */ $driver->client_info = 42; } catch (Error $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), "\n"; } try { $driver->report_mode = "1"; } catch (Error $e) { - echo $e->getMessage(), "\n"; + echo $e::class, ': ', $e->getMessage(), "\n"; } ?> --EXPECT-- -Cannot write read-only property mysqli_driver::$client_info -Cannot assign string to property mysqli_driver::$report_mode of type int +Error: Cannot write read-only property mysqli_driver::$client_info +TypeError: Cannot assign string to property mysqli_driver::$report_mode of type int diff --git a/ext/mysqli/tests/mysqli_driver_unclonable.phpt b/ext/mysqli/tests/mysqli_driver_unclonable.phpt index 7041a024f67c..94065a1379df 100644 --- a/ext/mysqli/tests/mysqli_driver_unclonable.phpt +++ b/ext/mysqli/tests/mysqli_driver_unclonable.phpt @@ -9,7 +9,7 @@ try { $driver = new mysqli_driver; $driver_clone = clone $driver; } catch (Throwable $e) { - echo $e::class, ": ", $e->getMessage(), PHP_EOL; + echo $e::class, ': ', $e->getMessage(), PHP_EOL; } ?> From d3c95d1745de99b39034f8147f4659182143e6e9 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 17 Jul 2025 22:33:01 +0100 Subject: [PATCH 12/18] ext/standard: Throw ValueError for filenames with null bytes This should never happen in the first place --- NEWS | 7 +++++++ UPGRADING | 20 +++++++++++++++++++ ext/standard/filestat.c | 2 +- ext/standard/tests/file/bug39863.phpt | 8 ++++++-- .../tests/file/filegroup_null_byte.phpt | 14 +++++++++++++ .../tests/file/filegroup_variation3.phpt | 12 ----------- .../tests/file/fileinode_null_byte.phpt | 14 +++++++++++++ .../tests/file/fileinode_variation3.phpt | 12 ----------- .../tests/file/fileowner_null_byte.phpt | 14 +++++++++++++ .../tests/file/fileowner_variation3.phpt | 12 ----------- .../tests/file/fileperms_null_byte.phpt | 14 +++++++++++++ .../tests/file/fileperms_variation3.phpt | 12 ----------- ext/standard/tests/file/is_dir_null_byte.phpt | 14 +++++++++++++ .../tests/file/is_dir_variation4.phpt | 10 ---------- .../tests/file/is_executable_null_byte.phpt | 14 +++++++++++++ .../tests/file/is_executable_variation1.phpt | 13 +----------- .../tests/file/is_file_null_byte.phpt | 14 +++++++++++++ .../tests/file/is_file_variation4.phpt | 8 -------- .../tests/file/is_readable_null_byte.phpt | 14 +++++++++++++ .../tests/file/is_readable_variation1.phpt | 13 +----------- .../tests/file/is_writable_null_byte.phpt | 14 +++++++++++++ .../tests/file/is_writable_variation1.phpt | 16 +-------------- 22 files changed, 163 insertions(+), 108 deletions(-) create mode 100644 ext/standard/tests/file/filegroup_null_byte.phpt create mode 100644 ext/standard/tests/file/fileinode_null_byte.phpt create mode 100644 ext/standard/tests/file/fileowner_null_byte.phpt create mode 100644 ext/standard/tests/file/fileperms_null_byte.phpt create mode 100644 ext/standard/tests/file/is_dir_null_byte.phpt create mode 100644 ext/standard/tests/file/is_executable_null_byte.phpt create mode 100644 ext/standard/tests/file/is_file_null_byte.phpt create mode 100644 ext/standard/tests/file/is_readable_null_byte.phpt create mode 100644 ext/standard/tests/file/is_writable_null_byte.phpt diff --git a/NEWS b/NEWS index cb70bda237ff..d90af7f5e64d 100644 --- a/NEWS +++ b/NEWS @@ -13,6 +13,13 @@ PHP NEWS error handler instead of emitting a warning and continuing with an empty string. (Weilin Du) +- Standard: + . The following functions now raise a ValueError when the $filename argument + contains NUL bytes: fileperms(), fileinode(), filesize(), fileowner(), + filegroup(), fileatime(), filemtime(), filectime(), filetype(), + is_writable(), is_readable(), is_executable(), is_file(), is_dir(), + is_link(), file_exists(), lstat(), stat(). (Girgias) + 30 Jul 2026, PHP 8.6.0alpha3 - Core: diff --git a/UPGRADING b/UPGRADING index 599ae2290d6e..b516f24db97f 100644 --- a/UPGRADING +++ b/UPGRADING @@ -228,6 +228,26 @@ PHP 8.6 UPGRADE NOTES bytes. . base_convert(), bindex(), hexdec() and octdec() now raise a notice when they cannot precisely convert the given number. + . The following functions now raise a ValueError when the $filename argument + contains NUL bytes: + - fileperms() + - fileinode() + - filesize() + - fileowner() + - filegroup() + - fileatime() + - filemtime() + - filectime() + - filetype() + - is_writable() + - is_readable() + - is_executable() + - is_file() + - is_dir() + - is_link() + - file_exists() + - lstat() + - stat() - Sysvshm: . shm_attach() now raises a ValueError when the $key argument is outside the diff --git a/ext/standard/filestat.c b/ext/standard/filestat.c index 7dcfecf1f872..de4fcd82a620 100644 --- a/ext/standard/filestat.c +++ b/ext/standard/filestat.c @@ -996,7 +996,7 @@ ZEND_NAMED_FUNCTION(name) { \ zend_string *filename; \ \ ZEND_PARSE_PARAMETERS_START(1, 1) \ - Z_PARAM_STR(filename) \ + Z_PARAM_PATH_STR(filename) \ ZEND_PARSE_PARAMETERS_END(); \ \ php_stat(filename, funcnum, return_value); \ diff --git a/ext/standard/tests/file/bug39863.phpt b/ext/standard/tests/file/bug39863.phpt index 88c569473a76..2a0ca01ea65a 100644 --- a/ext/standard/tests/file/bug39863.phpt +++ b/ext/standard/tests/file/bug39863.phpt @@ -6,7 +6,11 @@ Andrew van der Stock, vanderaj @ owasp.org getMessage(), PHP_EOL; +} ?> --EXPECT-- -bool(false) +ValueError: file_exists(): Argument #1 ($filename) must not contain any null bytes diff --git a/ext/standard/tests/file/filegroup_null_byte.phpt b/ext/standard/tests/file/filegroup_null_byte.phpt new file mode 100644 index 000000000000..4cb80399c61d --- /dev/null +++ b/ext/standard/tests/file/filegroup_null_byte.phpt @@ -0,0 +1,14 @@ +--TEST-- +filegroup() with filenames with null bytes +--FILE-- +getMessage(), "\n"; +} + +?> +--EXPECT-- +ValueError: filegroup(): Argument #1 ($filename) must not contain any null bytes diff --git a/ext/standard/tests/file/filegroup_variation3.phpt b/ext/standard/tests/file/filegroup_variation3.phpt index e844be077212..94562850f58d 100644 --- a/ext/standard/tests/file/filegroup_variation3.phpt +++ b/ext/standard/tests/file/filegroup_variation3.phpt @@ -26,10 +26,6 @@ $files_arr = array( "//filegroup_variation3//filegroup_variation3.tmp", "/filegroup_variation3/*.tmp", "filegroup_variation3/filegroup*.tmp", - - /* Testing Binary safe */ - "/filegroup_variation3/filegroup_variation3.tmp".chr(0), - "/filegroup_variation3/filegroup_variation3.tmp\0" ); $count = 1; @@ -74,13 +70,5 @@ bool(false) Warning: filegroup(): stat failed for %s/filegroup_variation3/filegroup*.tmp in %s on line %d bool(false) -- Iteration 7 - - -Warning: filegroup(): Filename contains null byte in %s on line %d -bool(false) -- Iteration 8 - - -Warning: filegroup(): Filename contains null byte in %s on line %d -bool(false) *** Done *** diff --git a/ext/standard/tests/file/fileinode_null_byte.phpt b/ext/standard/tests/file/fileinode_null_byte.phpt new file mode 100644 index 000000000000..48b8272d97cf --- /dev/null +++ b/ext/standard/tests/file/fileinode_null_byte.phpt @@ -0,0 +1,14 @@ +--TEST-- +fileinode() with filenames with null bytes +--FILE-- +getMessage(), "\n"; +} + +?> +--EXPECT-- +ValueError: fileinode(): Argument #1 ($filename) must not contain any null bytes diff --git a/ext/standard/tests/file/fileinode_variation3.phpt b/ext/standard/tests/file/fileinode_variation3.phpt index 92357a3619e8..506984d41775 100644 --- a/ext/standard/tests/file/fileinode_variation3.phpt +++ b/ext/standard/tests/file/fileinode_variation3.phpt @@ -25,10 +25,6 @@ $files_arr = array( "//fileinode_variation3//fileinode_variation3.tmp", "/fileinode_variation3/*.tmp", "fileinode_variation3/fileinode*.tmp", - - /* Testing Binary safe */ - "/fileinode_variation3/fileinode_variation3.tmp".chr(0), - "/fileinode_variation3/fileinode_variation3.tmp\0" ); $count = 1; @@ -73,13 +69,5 @@ bool(false) Warning: fileinode(): stat failed for %s/fileinode_variation3/fileinode*.tmp in %s on line %d bool(false) -- Iteration 7 - - -Warning: fileinode(): Filename contains null byte in %s on line %d -bool(false) -- Iteration 8 - - -Warning: fileinode(): Filename contains null byte in %s on line %d -bool(false) *** Done *** diff --git a/ext/standard/tests/file/fileowner_null_byte.phpt b/ext/standard/tests/file/fileowner_null_byte.phpt new file mode 100644 index 000000000000..909ac0fe3109 --- /dev/null +++ b/ext/standard/tests/file/fileowner_null_byte.phpt @@ -0,0 +1,14 @@ +--TEST-- +fileowner() with filenames with null bytes +--FILE-- +getMessage(), "\n"; +} + +?> +--EXPECT-- +ValueError: fileowner(): Argument #1 ($filename) must not contain any null bytes diff --git a/ext/standard/tests/file/fileowner_variation3.phpt b/ext/standard/tests/file/fileowner_variation3.phpt index 63ba6936aaa8..e6360b475713 100644 --- a/ext/standard/tests/file/fileowner_variation3.phpt +++ b/ext/standard/tests/file/fileowner_variation3.phpt @@ -26,10 +26,6 @@ $files_arr = array( "//fileowner_variation3//fileowner_variation3.tmp", "/fileowner_variation3/*.tmp", "fileowner_variation3/fileowner*.tmp", - - /* Testing Binary safe */ - "/fileowner_variation3/fileowner_variation3.tmp".chr(0), - "/fileowner_variation3/fileowner_variation3.tmp\0" ); $count = 1; @@ -74,13 +70,5 @@ bool(false) Warning: fileowner(): stat failed for %s/fileowner_variation3/fileowner*.tmp in %s on line %d bool(false) -- Iteration 7 - - -Warning: fileowner(): Filename contains null byte in %s on line %d -bool(false) -- Iteration 8 - - -Warning: fileowner(): Filename contains null byte in %s on line %d -bool(false) *** Done *** diff --git a/ext/standard/tests/file/fileperms_null_byte.phpt b/ext/standard/tests/file/fileperms_null_byte.phpt new file mode 100644 index 000000000000..1530d0e5bf9f --- /dev/null +++ b/ext/standard/tests/file/fileperms_null_byte.phpt @@ -0,0 +1,14 @@ +--TEST-- +fileperms() with filenames with null bytes +--FILE-- +getMessage(), "\n"; +} + +?> +--EXPECT-- +ValueError: fileperms(): Argument #1 ($filename) must not contain any null bytes diff --git a/ext/standard/tests/file/fileperms_variation3.phpt b/ext/standard/tests/file/fileperms_variation3.phpt index 5e981e9b8632..6c1da17d6bfd 100644 --- a/ext/standard/tests/file/fileperms_variation3.phpt +++ b/ext/standard/tests/file/fileperms_variation3.phpt @@ -25,10 +25,6 @@ $files_arr = array( "//fileperms_variation3//fileperms_variation3.tmp", "/fileperms_variation3/*.tmp", "fileperms_variation3/fileperms*.tmp", - - /* Testing Binary safe */ - "/fileperms_variation3/fileperms_variation3.tmp".chr(0), - "/fileperms_variation3/fileperms_variation3.tmp\0" ); $count = 1; @@ -73,13 +69,5 @@ bool(false) Warning: fileperms(): stat failed for %s/fileperms_variation3/fileperms*.tmp in %s on line %d bool(false) -- Iteration 7 - - -Warning: fileperms(): Filename contains null byte in %s on line %d -bool(false) -- Iteration 8 - - -Warning: fileperms(): Filename contains null byte in %s on line %d -bool(false) *** Done *** diff --git a/ext/standard/tests/file/is_dir_null_byte.phpt b/ext/standard/tests/file/is_dir_null_byte.phpt new file mode 100644 index 000000000000..a49ccc0e5c45 --- /dev/null +++ b/ext/standard/tests/file/is_dir_null_byte.phpt @@ -0,0 +1,14 @@ +--TEST-- +is_dir() with filenames with null bytes +--FILE-- +getMessage(), "\n"; +} + +?> +--EXPECT-- +ValueError: is_dir(): Argument #1 ($filename) must not contain any null bytes diff --git a/ext/standard/tests/file/is_dir_variation4.phpt b/ext/standard/tests/file/is_dir_variation4.phpt index d6e64efe211c..30285804a33c 100644 --- a/ext/standard/tests/file/is_dir_variation4.phpt +++ b/ext/standard/tests/file/is_dir_variation4.phpt @@ -23,10 +23,6 @@ $dirs_arr = array( "./is_dir_variation4//", ".//is_dir_variation4//", "is_dir_vari*", - - /* Testing Binary safe */ - "./is_dir_variation4/".chr(0), - "is_dir_variation4\0" ); $count = 1; @@ -75,10 +71,4 @@ bool(true) -- Iteration 8 -- bool(false) --- Iteration 9 -- -bool(false) - --- Iteration 10 -- -bool(false) - *** Done *** diff --git a/ext/standard/tests/file/is_executable_null_byte.phpt b/ext/standard/tests/file/is_executable_null_byte.phpt new file mode 100644 index 000000000000..7c26d53061d1 --- /dev/null +++ b/ext/standard/tests/file/is_executable_null_byte.phpt @@ -0,0 +1,14 @@ +--TEST-- +is_executable() with filenames with null bytes +--FILE-- +getMessage(), "\n"; +} + +?> +--EXPECT-- +ValueError: is_executable(): Argument #1 ($filename) must not contain any null bytes diff --git a/ext/standard/tests/file/is_executable_variation1.phpt b/ext/standard/tests/file/is_executable_variation1.phpt index a85b9a2ef028..289abb1a265f 100644 --- a/ext/standard/tests/file/is_executable_variation1.phpt +++ b/ext/standard/tests/file/is_executable_variation1.phpt @@ -33,11 +33,6 @@ $files_arr = array( "$file_path/is_executable_variation1/*.tmp", "$file_path/is_executable_variation1/b*.tmp", - /* Testing Binary safe */ - "$file_path/is_executable_variation1".chr(0)."bar.temp", - "$file_path".chr(0)."is_executable_variation1/bar.temp", - "$file_path/is_executable_variation1x000/", - /* Testing directories */ ".", // current directory, exp: bool(true) "$file_path/is_executable_variation1" // temp directory, exp: bool(true) @@ -76,13 +71,7 @@ bool(false) -- Iteration 5 -- bool(false) -- Iteration 6 -- -bool(false) --- Iteration 7 -- -bool(false) --- Iteration 8 -- -bool(false) --- Iteration 9 -- bool(true) --- Iteration 10 -- +-- Iteration 7 -- bool(true) Done diff --git a/ext/standard/tests/file/is_file_null_byte.phpt b/ext/standard/tests/file/is_file_null_byte.phpt new file mode 100644 index 000000000000..f544d7bcf589 --- /dev/null +++ b/ext/standard/tests/file/is_file_null_byte.phpt @@ -0,0 +1,14 @@ +--TEST-- +is_file() with filenames with null bytes +--FILE-- +getMessage(), "\n"; +} + +?> +--EXPECT-- +ValueError: is_file(): Argument #1 ($filename) must not contain any null bytes diff --git a/ext/standard/tests/file/is_file_variation4.phpt b/ext/standard/tests/file/is_file_variation4.phpt index 1afc34dd03be..9d98b7fd90d7 100644 --- a/ext/standard/tests/file/is_file_variation4.phpt +++ b/ext/standard/tests/file/is_file_variation4.phpt @@ -23,10 +23,6 @@ $files_arr = array( "//is_file_variation4//is_file_variation4.tmp", "/is_file_variation4/*.tmp", "is_file_variation4/is_file*.tmp", - - /* Testing Binary safe */ - "/is_file_variation4/is_file_variation4.tmp".chr(0), - "/is_file_variation4/is_file_variation4.tmp\0" ); $count = 1; @@ -65,9 +61,5 @@ bool(true) bool(false) - Iteration 6 - bool(false) -- Iteration 7 - -bool(false) -- Iteration 8 - -bool(false) *** Done *** diff --git a/ext/standard/tests/file/is_readable_null_byte.phpt b/ext/standard/tests/file/is_readable_null_byte.phpt new file mode 100644 index 000000000000..711700f4e7ec --- /dev/null +++ b/ext/standard/tests/file/is_readable_null_byte.phpt @@ -0,0 +1,14 @@ +--TEST-- +is_readable() with filenames with null bytes +--FILE-- +getMessage(), "\n"; +} + +?> +--EXPECT-- +ValueError: is_readable(): Argument #1 ($filename) must not contain any null bytes diff --git a/ext/standard/tests/file/is_readable_variation1.phpt b/ext/standard/tests/file/is_readable_variation1.phpt index 9c25213f8abb..8548a840ccbe 100644 --- a/ext/standard/tests/file/is_readable_variation1.phpt +++ b/ext/standard/tests/file/is_readable_variation1.phpt @@ -32,11 +32,6 @@ $files_arr = array( "$file_path/is_readable_variation1/*.tmp", "$file_path/is_readable_variation1/b*.tmp", - /* Testing Binary safe */ - "$file_path/is_readable_variation1".chr(0)."bar.tmp", - "$file_path".chr(0)."is_readable_variation1/bar.tmp", - "$file_path".chr(0)."is_readable_variation1/bar.tmp", - /* Testing directories */ ".", // current directory, exp: bool(true) "$file_path/is_readable_variation1" // temp directory, exp: bool(true) @@ -77,13 +72,7 @@ bool(false) -- Iteration 6 -- bool(false) -- Iteration 7 -- -bool(false) --- Iteration 8 -- -bool(false) --- Iteration 9 -- -bool(false) --- Iteration 10 -- bool(true) --- Iteration 11 -- +-- Iteration 8 -- bool(true) Done diff --git a/ext/standard/tests/file/is_writable_null_byte.phpt b/ext/standard/tests/file/is_writable_null_byte.phpt new file mode 100644 index 000000000000..319db04cc151 --- /dev/null +++ b/ext/standard/tests/file/is_writable_null_byte.phpt @@ -0,0 +1,14 @@ +--TEST-- +is_writable() with filenames with null bytes +--FILE-- +getMessage(), "\n"; +} + +?> +--EXPECT-- +ValueError: is_writable(): Argument #1 ($filename) must not contain any null bytes diff --git a/ext/standard/tests/file/is_writable_variation1.phpt b/ext/standard/tests/file/is_writable_variation1.phpt index 80695d6d45a2..181b1ba4e268 100644 --- a/ext/standard/tests/file/is_writable_variation1.phpt +++ b/ext/standard/tests/file/is_writable_variation1.phpt @@ -31,11 +31,6 @@ $files_arr = array( "$file_path/is_writable_variation1/*.tmp", "$file_path/is_writable_variation1/b*.tmp", - /* Testing Binary safe */ - "$file_path/is_writable_variation1".chr(0)."bar.tmp", - "$file_path".chr(0)."is_writable_variation1/bar.tmp", - "$file_path".chr(0)."is_writable_variation1/bar.tmp", - /* Testing directories */ ".", // current directory, exp: bool(true) "$file_path/is_writable_variation1" // temp directory, exp: bool(true) @@ -87,18 +82,9 @@ bool(false) bool(false) bool(false) -- Iteration 7 -- -bool(false) -bool(false) --- Iteration 8 -- -bool(false) -bool(false) --- Iteration 9 -- -bool(false) -bool(false) --- Iteration 10 -- bool(true) bool(true) --- Iteration 11 -- +-- Iteration 8 -- bool(true) bool(true) Done From 732243fb662b17daed566eb7b72ee79900158329 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 17 Jul 2025 22:40:05 +0100 Subject: [PATCH 13/18] ext/standard: Refactor php_stat() API --- ext/standard/filestat.c | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/ext/standard/filestat.c b/ext/standard/filestat.c index de4fcd82a620..96669a809346 100644 --- a/ext/standard/filestat.c +++ b/ext/standard/filestat.c @@ -730,14 +730,12 @@ PHPAPI void php_stat(zend_string *filename, int type, zval *return_value) const char *local = NULL; php_stream_wrapper *wrapper = NULL; + ZEND_ASSERT(!zend_str_has_nul_byte(filename)); + /* Quick check for empty file paths */ + if (!ZSTR_LEN(filename)) { + RETURN_FALSE; + } if (IS_ACCESS_CHECK(type)) { - 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"); - } - RETURN_FALSE; - } - if ((wrapper = php_stream_locate_url_wrapper(ZSTR_VAL(filename), &local, 0)) == &php_plain_files_wrapper && php_check_open_basedir(local)) { RETURN_FALSE; @@ -799,13 +797,6 @@ PHPAPI void php_stat(zend_string *filename, int type, zval *return_value) } if (!wrapper) { - 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"); - } - RETURN_FALSE; - } - if ((wrapper = php_stream_locate_url_wrapper(ZSTR_VAL(filename), &local, 0)) == &php_plain_files_wrapper && php_check_open_basedir(local)) { RETURN_FALSE; From c35253d364c1c1c75ad9dbc4c8acbbdf239ffec1 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Wed, 29 Jul 2026 07:28:39 -0400 Subject: [PATCH 14/18] Free the hooked property value when json_encode() sees an exception php_json_encode_array() reads a hooked property into a local zval and bails out when an exception is pending. The get hook can publish its return value into that zval and then throw while its frame is freed, for example from the destructor of one of its locals, so the bailout leaks the value until request shutdown. Destroy it on the way out, the way the other failure exit already does. Closes GH-22919 --- ext/json/json_encoder.c | 1 + ...ncode_hooked_property_throwing_getter.phpt | 44 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 ext/json/tests/json_encode_hooked_property_throwing_getter.phpt diff --git a/ext/json/json_encoder.c b/ext/json/json_encoder.c index 4145bc2b6154..cefee25de90a 100644 --- a/ext/json/json_encoder.c +++ b/ext/json/json_encoder.c @@ -287,6 +287,7 @@ static zend_result php_json_encode_array(smart_str *buf, zval *val, int options, if (EG(exception)) { PHP_JSON_HASH_UNPROTECT_RECURSION(recursion_rc); zend_release_properties(prop_ht); + zval_ptr_dtor(&tmp); return FAILURE; } } diff --git a/ext/json/tests/json_encode_hooked_property_throwing_getter.phpt b/ext/json/tests/json_encode_hooked_property_throwing_getter.phpt new file mode 100644 index 000000000000..63fc8cf42c0e --- /dev/null +++ b/ext/json/tests/json_encode_hooked_property_throwing_getter.phpt @@ -0,0 +1,44 @@ +--TEST-- +json_encode() releases the hooked property value when the get hook throws +--FILE-- +getMessage(), "\n"; +} + +echo "done\n"; + +?> +--EXPECT-- +Value::__destruct +Exception: thrown while freeing the get hook frame +done From 346227583da55ed8b4494dd8033ff26dd725bae1 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 30 Jul 2026 13:28:00 +0100 Subject: [PATCH 15/18] Zend: reuse zend_parse_arg_func() in zend_parse_arg_impl() (#22937) No need to duplicate the logic --- Zend/zend_API.c | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 72f4727eeddf..33128e4a7e37 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -1058,21 +1058,8 @@ static const char *zend_parse_arg_impl(zval *arg, va_list *va, const char **spec zend_fcall_info *fci = va_arg(*va, zend_fcall_info *); zend_fcall_info_cache *fcc = va_arg(*va, zend_fcall_info_cache *); char *is_callable_error = NULL; - - if (check_null && Z_TYPE_P(arg) == IS_NULL) { - fci->size = 0; - fcc->function_handler = 0; - break; - } - - if (zend_fcall_info_init(arg, 0, fci, fcc, NULL, &is_callable_error) == SUCCESS) { + if (EXPECTED(zend_parse_arg_func(arg, fci, fcc, check_null, &is_callable_error, c == 'f'))) { ZEND_ASSERT(!is_callable_error); - if (c == 'f') { - /* Release call trampolines: The function may not get called, in which case - * the trampoline will leak. Force it to be refetched during - * zend_call_function instead. */ - zend_release_fcall_info_cache(fcc); - } break; } From 5795e330a6ac521bfa2e0e9486e9516369860208 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 30 Jul 2026 13:30:10 +0100 Subject: [PATCH 16/18] sapi: convert current_user field to zend_string* (#22902) Prevents some reallocations and strlen() recomputations --- Zend/Optimizer/zend_func_infos.h | 1 - ext/soap/php_sdl.c | 11 +++++------ ext/standard/basic_functions.c | 2 +- ext/standard/basic_functions.stub.php | 1 - ext/standard/basic_functions_arginfo.h | 2 +- ext/standard/basic_functions_decl.h | 8 ++++---- main/SAPI.c | 4 +--- main/SAPI.h | 3 +-- main/main.c | 21 ++++++++------------- main/php.h | 2 +- 10 files changed, 22 insertions(+), 33 deletions(-) diff --git a/Zend/Optimizer/zend_func_infos.h b/Zend/Optimizer/zend_func_infos.h index 2a9353c9b86e..88d012149beb 100644 --- a/Zend/Optimizer/zend_func_infos.h +++ b/Zend/Optimizer/zend_func_infos.h @@ -419,7 +419,6 @@ static const func_info_t func_infos[] = { #if defined(HAVE_NANOSLEEP) F1("time_nanosleep", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_LONG|MAY_BE_BOOL), #endif - F1("get_current_user", MAY_BE_STRING), FN("get_cfg_var", MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_STRING|MAY_BE_ARRAY_OF_ARRAY|MAY_BE_FALSE), F1("error_get_last", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_LONG|MAY_BE_ARRAY_OF_STRING|MAY_BE_ARRAY_OF_ARRAY|MAY_BE_NULL), F1("highlight_file", MAY_BE_STRING|MAY_BE_BOOL), diff --git a/ext/soap/php_sdl.c b/ext/soap/php_sdl.c index 77d73a2326d7..c6e53c408aa8 100644 --- a/ext/soap/php_sdl.c +++ b/ext/soap/php_sdl.c @@ -3179,8 +3179,7 @@ sdlPtr get_sdl(zval *this_ptr, char *uri, zend_long cache_wsdl) unsigned char digest[16]; size_t len = strlen(SOAP_GLOBAL(cache_dir)); time_t cached; - char *user = php_get_current_user(); - size_t user_len = user ? strlen(user) + 1 : 0; + zend_string *user = php_get_current_user(); /* System architecture identification (see bug #70951) */ static const char ids[] = {SIZEOF_ZEND_LONG, SOAP_BIG_ENDIAN}; @@ -3191,13 +3190,13 @@ sdlPtr get_sdl(zval *this_ptr, char *uri, zend_long cache_wsdl) PHP_MD5Update(&md5_context, ids, sizeof(ids)); PHP_MD5Final(digest, &md5_context); make_digest(md5str, digest); - key = emalloc(len+sizeof("/wsdl-")-1+user_len+2+sizeof(md5str)); + key = emalloc(len+sizeof("/wsdl-")-1+ZSTR_LEN(user)+1+2+sizeof(md5str)); memcpy(key,SOAP_GLOBAL(cache_dir),len); memcpy(key+len,"/wsdl-",sizeof("/wsdl-")-1); len += sizeof("/wsdl-")-1; - if (user_len) { - memcpy(key+len, user, user_len-1); - len += user_len-1; + if (ZSTR_LEN(user)) { + memcpy(key+len, ZSTR_VAL(user), ZSTR_LEN(user)); + len += ZSTR_LEN(user); key[len++] = '-'; } if (WSDL_CACHE_VERSION <= 0x9f) { diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index f8fcc01bf5bb..a87ab1b5ad62 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -1255,7 +1255,7 @@ PHP_FUNCTION(get_current_user) { ZEND_PARSE_PARAMETERS_NONE(); - RETURN_STRING(php_get_current_user()); + RETURN_STR_COPY(php_get_current_user()); } /* }}} */ diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index 7fc31e7d80ca..07e61957d191 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -1976,7 +1976,6 @@ function time_nanosleep(int $seconds, int $nanoseconds): array|bool {} function time_sleep_until(float $timestamp): bool {} #endif -/** @refcount 1 */ function get_current_user(): string {} /** @return string|array|false */ diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index c5266c5a877c..d24de1fef99d 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit basic_functions.stub.php instead. - * Stub hash: 3b1649a3abb3cfb5cb39d93f30a97765fe862d67 + * Stub hash: 0c8af3b78b3d6e1ee3e59c60363d0d941bea9faa * Has decl header: yes */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) diff --git a/ext/standard/basic_functions_decl.h b/ext/standard/basic_functions_decl.h index 630a4b7e656b..97d896e8bb6e 100644 --- a/ext/standard/basic_functions_decl.h +++ b/ext/standard/basic_functions_decl.h @@ -1,8 +1,8 @@ /* This is a generated file, edit basic_functions.stub.php instead. - * Stub hash: 3b1649a3abb3cfb5cb39d93f30a97765fe862d67 */ + * Stub hash: 0c8af3b78b3d6e1ee3e59c60363d0d941bea9faa */ -#ifndef ZEND_BASIC_FUNCTIONS_DECL_3b1649a3abb3cfb5cb39d93f30a97765fe862d67_H -#define ZEND_BASIC_FUNCTIONS_DECL_3b1649a3abb3cfb5cb39d93f30a97765fe862d67_H +#ifndef ZEND_BASIC_FUNCTIONS_DECL_0c8af3b78b3d6e1ee3e59c60363d0d941bea9faa_H +#define ZEND_BASIC_FUNCTIONS_DECL_0c8af3b78b3d6e1ee3e59c60363d0d941bea9faa_H typedef enum zend_enum_SortDirection { ZEND_ENUM_SortDirection_Ascending = 1, @@ -20,4 +20,4 @@ typedef enum zend_enum_RoundingMode { ZEND_ENUM_RoundingMode_PositiveInfinity = 8, } zend_enum_RoundingMode; -#endif /* ZEND_BASIC_FUNCTIONS_DECL_3b1649a3abb3cfb5cb39d93f30a97765fe862d67_H */ +#endif /* ZEND_BASIC_FUNCTIONS_DECL_0c8af3b78b3d6e1ee3e59c60363d0d941bea9faa_H */ diff --git a/main/SAPI.c b/main/SAPI.c index 31cf5da18a1d..3daa88e07f25 100644 --- a/main/SAPI.c +++ b/main/SAPI.c @@ -370,7 +370,6 @@ SAPI_API void sapi_activate_headers_only(void) SG(read_post_bytes) = 0; SG(request_info).request_body = NULL; SG(request_info).current_user = NULL; - SG(request_info).current_user_length = 0; SG(request_info).no_headers = 0; SG(request_info).post_entry = NULL; SG(global_request_time) = 0; @@ -413,7 +412,6 @@ SAPI_API void sapi_activate(void) SG(read_post_bytes) = 0; SG(request_info).request_body = NULL; SG(request_info).current_user = NULL; - SG(request_info).current_user_length = 0; SG(request_info).no_headers = 0; SG(request_info).post_entry = NULL; SG(request_info).proto_num = 1000; /* Default to HTTP 1.0 */ @@ -499,7 +497,7 @@ SAPI_API void sapi_deactivate_module(void) efree(SG(request_info).content_type_dup); } if (SG(request_info).current_user) { - efree(SG(request_info).current_user); + zend_string_release_ex(SG(request_info).current_user, false); } if (sapi_module.deactivate) { sapi_module.deactivate(); diff --git a/main/SAPI.h b/main/SAPI.h index 29532a019335..2621b9184d1b 100644 --- a/main/SAPI.h +++ b/main/SAPI.h @@ -97,8 +97,7 @@ typedef struct { /* this is necessary for the CGI SAPI module */ char *argv0; - char *current_user; - int current_user_length; + zend_string *current_user; /* this is necessary for CLI module */ int argc; diff --git a/main/main.c b/main/main.c index 4fa34258b983..d0ffe1895e9c 100644 --- a/main/main.c +++ b/main/main.c @@ -1509,7 +1509,7 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c /* }}} */ /* {{{ php_get_current_user */ -PHPAPI char *php_get_current_user(void) +PHPAPI zend_string *php_get_current_user(void) { zend_stat_t *pstat = NULL; @@ -1524,19 +1524,15 @@ PHPAPI char *php_get_current_user(void) pstat = sapi_get_stat(); if (!pstat) { - return ""; + return ZSTR_EMPTY_ALLOC(); } else { #ifdef PHP_WIN32 char *name = php_win32_get_username(); - int len; if (!name) { - return ""; + return ZSTR_EMPTY_ALLOC(); } - len = (int)strlen(name); - name[len] = '\0'; - SG(request_info).current_user_length = len; - SG(request_info).current_user = estrndup(name, len); + SG(request_info).current_user = zend_string_init(name, strlen(name), false); free(name); return SG(request_info).current_user; #else @@ -1566,20 +1562,19 @@ PHPAPI char *php_get_current_user(void) goto try_again; } efree(pwbuf); - return ""; + return ZSTR_EMPTY_ALLOC(); } if (retpwptr == NULL) { efree(pwbuf); - return ""; + return ZSTR_EMPTY_ALLOC(); } pwd = &_pw; #else if ((pwd=getpwuid(pstat->st_uid))==NULL) { - return ""; + return ZSTR_EMPTY_ALLOC(); } #endif - SG(request_info).current_user_length = strlen(pwd->pw_name); - SG(request_info).current_user = estrndup(pwd->pw_name, SG(request_info).current_user_length); + SG(request_info).current_user = zend_string_init(pwd->pw_name, strlen(pwd->pw_name), false); #if defined(ZTS) && defined(HAVE_GETPWUID_R) && defined(_SC_GETPW_R_SIZE_MAX) efree(pwbuf); #endif diff --git a/main/php.h b/main/php.h index fb81a4b47455..5186868e612f 100644 --- a/main/php.h +++ b/main/php.h @@ -323,7 +323,7 @@ PHPAPI extern int (*php_register_internal_extensions_func)(void); PHPAPI int php_register_internal_extensions(void); PHPAPI void php_register_pre_request_shutdown(void (*func)(void *), void *userdata); PHPAPI void php_com_initialize(void); -PHPAPI char *php_get_current_user(void); +PHPAPI zend_string *php_get_current_user(void); PHPAPI const char *php_get_internal_encoding(void); PHPAPI const char *php_get_input_encoding(void); From 955e2ea14b09681dc84f6a6a5f8dadff857ddf08 Mon Sep 17 00:00:00 2001 From: NickSdot <32384907+NickSdot@users.noreply.github.com> Date: Thu, 30 Jul 2026 20:44:07 +0700 Subject: [PATCH 17/18] Run scandir overflow test on Windows only (#22934) Avoids creating 70,000 files on platforms unaffected by the Win32 DIR_W32.offset regression. Shaves ~9s off non-Windows test runs. --- ext/standard/tests/file/bug36365.phpt | 1 + 1 file changed, 1 insertion(+) diff --git a/ext/standard/tests/file/bug36365.phpt b/ext/standard/tests/file/bug36365.phpt index be1a6c516555..c3aeb8045d6e 100644 --- a/ext/standard/tests/file/bug36365.phpt +++ b/ext/standard/tests/file/bug36365.phpt @@ -2,6 +2,7 @@ Bug #36365 (scandir duplicates file name at every 65535th file) --SKIPIF-- --FILE-- From 61a190cdd2e0020cbecf46c4b42525a8dca0cb4c Mon Sep 17 00:00:00 2001 From: NickSdot <32384907+NickSdot@users.noreply.github.com> Date: Thu, 30 Jul 2026 20:50:14 +0700 Subject: [PATCH 18/18] Prevents tests from leaking System V IPC objects (#22912) * Clean up IPC objects in arginfo mismatch test * Fix GH-16592 test leaking a message queue --- Zend/tests/arginfo_zpp_mismatch.phpt | 34 ++++++++++++++++++++++++++++ ext/sysvmsg/tests/gh16592.phpt | 5 +++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/Zend/tests/arginfo_zpp_mismatch.phpt b/Zend/tests/arginfo_zpp_mismatch.phpt index d7aefc6f374f..94f6b070900e 100644 --- a/Zend/tests/arginfo_zpp_mismatch.phpt +++ b/Zend/tests/arginfo_zpp_mismatch.phpt @@ -10,6 +10,36 @@ if (getenv('SKIP_MSAN')) die("skip msan misses interceptors for some functions") require __DIR__ . "/arginfo_zpp_mismatch.inc"; +function testWeakSysvIpcFunction($function): bool { + if (!in_array($function, ['msg_queue_exists', 'msg_get_queue', 'sem_get', 'shm_attach'], true)) { + return false; + } + + for ($argumentCount = 0; $argumentCount <= 8; $argumentCount++) { + $arguments = array_fill(0, $argumentCount, null); + if ($function === 'msg_queue_exists' && $argumentCount >= 1) { + $arguments[0] = 1; + } + if (($function === 'sem_get' || $function === 'shm_attach') && $argumentCount >= 3) { + $arguments[2] = 0600; + } + + try { + $result = @$function(...$arguments); + if ($result instanceof SysvMessageQueue) { + msg_remove_queue($result); + } elseif ($result instanceof SysvSemaphore) { + sem_remove($result); + } elseif ($result instanceof SysvSharedMemory) { + shm_remove($result); + } + } catch (Throwable) { + } + } + + return true; +} + function test($function) { if (skipFunction($function)) { return; @@ -21,6 +51,10 @@ function test($function) { } else { echo "Testing " . get_class($function[0]) . "::$function[1]\n"; } + if (testWeakSysvIpcFunction($function)) { + ob_end_clean(); + return; + } try { @$function(); } catch (Throwable) { diff --git a/ext/sysvmsg/tests/gh16592.phpt b/ext/sysvmsg/tests/gh16592.phpt index 8490d000f891..d48927b4dfee 100644 --- a/ext/sysvmsg/tests/gh16592.phpt +++ b/ext/sysvmsg/tests/gh16592.phpt @@ -8,11 +8,14 @@ class Test { function __serialize() {} } -$q = msg_get_queue(1); +// use a private queue, so we only remove our own. +$q = msg_get_queue(0, 0600); try { msg_send($q, 1, new Test, true); } catch (\TypeError $e) { echo $e->getMessage(); +} finally { + msg_remove_queue($q); } ?> --EXPECT--