From deb6ef1db127ca9e1e475f96d08b06149a8dcaf9 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Fri, 31 Jul 2026 10:33:16 +0100 Subject: [PATCH 01/17] main: duplicate PG(output_handler) zstr As it may originated from persistent storage if it was defined in an INI file --- main/main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/main.c b/main/main.c index d0ffe1895e9c..753625643508 100644 --- a/main/main.c +++ b/main/main.c @@ -1884,7 +1884,7 @@ zend_result php_request_startup(void) if (PG(output_handler)) { zval oh; - ZVAL_STR_COPY(&oh, PG(output_handler)); + ZVAL_STR(&oh, zend_string_dup(PG(output_handler), false)); php_output_start_user(&oh, 0, PHP_OUTPUT_HANDLER_STDFLAGS); zval_ptr_dtor(&oh); } else if (PG(output_buffering)) { From 4568be02cf0223e318950269b893cd15f50fc9a1 Mon Sep 17 00:00:00 2001 From: Volker Dusch <247397+edorian@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:36:41 +0200 Subject: [PATCH 02/17] RM docs: Update security release procedure (#22953) --- docs/release-process.md | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/docs/release-process.md b/docs/release-process.md index 4215002a7b4d..e65a85c101a2 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -105,6 +105,7 @@ releases. * php-8.4.0 (initial GA) * php-8.4.9 (periodic bugfix or security release) +12. Ensure you are familiar with our procedure for [merging upwards][]. ## Packaging a non-stable release (alpha/beta/RC) @@ -321,7 +322,7 @@ slightly different steps. We'll call attention where the steps differ. git add main/php_version.h Zend/zend.h configure.ac git merge --continue ``` - + Be sure to set up a merge driver for the `NEWS` file as described in the [Git FAQ page on the PHP wiki][gitfaq-mandatory]. @@ -543,25 +544,38 @@ slightly different steps. We'll call attention where the steps differ. ## Packaging a stable release -1. Check out the *patch-level version branch* for the release - (e.g., `PHP-8.1.7`). +1. Check out the *patch-level version branch* for the release. + + ``` + git switch PHP-X.Y.Z + ``` > 💬 **Hint** \ > You should have created this branch when packaging the non-stable release > candidate for this version. If it is for a PHP-X.Y.0 version, then the branch > was created as part of the final planned release candidate, PHP-X.Y.0RC4. -2. If a CVE commit needs to be merged to the release, have it committed to - the base branches and [merged upwards as usual][] (e.g. commit the CVE fix - to 7.2, merge to 7.3, 7.4, etc.). Then, you can cherry-pick it into the - patch-level version branch for this release. +2. If the upcoming release is a security release, you will have been informed + about it by the security release manager (SRM) by Tuesday noon (UTC). - Commit these changes and push the patch-level version branch. Ensure - that CI is still passing (see above). + > 💬 **Hint** \ + > If you haven't set up a git remote for the security repo yet, do so: + > ```bash + > git remote add security git@github.com:php/php-src-security.git + > ``` - > 💡 **Tip** \ - > Don't forget to update `NEWS` manually in an extra commit to the - > patch-level version branch. + The SRM will provide you with a branch to merge in your + *patch-level version branch*. + + ```bash + git fetch security + git merge security/PHP-X.Y.Z-security + git push upstream PHP-X.Y.Z + ``` + + > 💬 **Hint** \ + > You do not need to merge this back into PHP-X.Y; the SRM will take care + > of it. 3. Run the `./scripts/dev/credits` script in the patch-level version branch, and commit the changes in the credits files in `ext/standard`. @@ -1182,7 +1196,7 @@ volunteers to begin the selection process for the next release managers. [Update NEWS for PHP 8.2.0RC6]: https://github.com/php/php-src/commit/4ccc414961a70200d638ca281a35f893226d74e2 [PHP 8.3 is now for PHP 8.3.21-dev]: https://github.com/php/php-src/commit/b57f425cfe20a11003253427424cc0517483550b [GitHub command line tool]: https://cli.github.com -[merged upwards as usual]: https://wiki.php.net/vcs/gitworkflow +[merging upwards]: https://wiki.php.net/vcs/gitworkflow [Update versions for PHP 8.1.7]: https://github.com/php/php-src/commit/d35e577a1bd0b35b9386cea97cddc73fd98eed6d [Update NEWS for PHP 8.1.7]: https://github.com/php/php-src/commit/b241f07f52ca9f87bf52be81817f475e6e727439 [Announce PHP 8.1.6]: https://github.com/php/web-php/commit/9f796a96c65f07e45845ec248933bfb0010b94a9 From 9be36f0cd474d7d66b4f42935af26fd514a8b5ac Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Fri, 31 Jul 2026 10:37:19 +0100 Subject: [PATCH 03/17] output: only use FCC for storing user output handler (#22815) --- main/output.c | 37 +++++++++++++++++-------------------- main/php_output.h | 8 +------- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/main/output.c b/main/output.c index 664adf7f1688..8221425f5cf6 100644 --- a/main/output.c +++ b/main/output.c @@ -468,7 +468,6 @@ PHPAPI php_output_handler *php_output_handler_create_user(zval *output_handler, char *error = NULL; php_output_handler *handler = NULL; php_output_handler_alias_ctor_t alias = NULL; - php_output_handler_user_func_t *user = NULL; switch (Z_TYPE_P(output_handler)) { case IS_NULL: @@ -480,22 +479,23 @@ PHPAPI php_output_handler *php_output_handler_create_user(zval *output_handler, break; } ZEND_FALLTHROUGH; - default: - user = ecalloc(1, sizeof(php_output_handler_user_func_t)); - if (SUCCESS == zend_fcall_info_init(output_handler, 0, &user->fci, &user->fcc, &handler_name, &error)) { + default: { + zend_fcall_info_cache *fcc = ecalloc(1, sizeof(*fcc)); + + if (zend_is_callable_ex(output_handler, NULL, 0, &handler_name, fcc, &error)) { handler = php_output_handler_init(handler_name, chunk_size, PHP_OUTPUT_HANDLER_ABILITY_FLAGS(flags) | PHP_OUTPUT_HANDLER_USER); - ZVAL_COPY(&user->zoh, output_handler); - handler->func.user = user; + zend_fcc_addref(fcc); + handler->func.user_fcc = fcc; } else { - efree(user); - } - if (error) { + efree(fcc); + ZEND_ASSERT(error); php_error_docref("ref.outcontrol", E_WARNING, "%s", error); efree(error); } if (handler_name) { zend_string_release_ex(handler_name, 0); } + } } return handler; @@ -707,8 +707,8 @@ PHPAPI void php_output_handler_dtor(php_output_handler *handler) efree(handler->buffer.data); } if (handler->flags & PHP_OUTPUT_HANDLER_USER) { - zval_ptr_dtor(&handler->func.user->zoh); - efree(handler->func.user); + zend_fcc_dtor(handler->func.user_fcc); + efree(handler->func.user_fcc); } if (handler->dtor && handler->opaq) { handler->dtor(handler->opaq); @@ -966,13 +966,12 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl /* ob_mode */ ZVAL_LONG(&ob_args[1], (zend_long) context->op); - /* Set FCI info */ - handler->func.user->fci.param_count = 2; - handler->func.user->fci.params = ob_args; - handler->func.user->fci.retval = &retval; - handler->func.user->fci.consumed_args = zend_fci_consumed_arg(0); + zend_call_known_fcc_ex(handler->func.user_fcc, &retval, 2, ob_args, NULL, zend_fci_consumed_arg(0)); - if (SUCCESS == zend_call_function(&handler->func.user->fci, &handler->func.user->fcc) && Z_TYPE(retval) != IS_UNDEF) { + zval_ptr_dtor(&ob_args[0]); + zval_ptr_dtor(&ob_args[1]); + + if (Z_TYPE(retval) != IS_UNDEF) { if (handler->flags & PHP_OUTPUT_HANDLER_PRODUCED_OUTPUT) { // Make sure that we don't get lost in the current output buffer // by disabling it @@ -1026,9 +1025,7 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl status = PHP_OUTPUT_HANDLER_FAILURE; } - /* Free arguments and return value */ - zval_ptr_dtor(&ob_args[0]); - zval_ptr_dtor(&ob_args[1]); + /* Free return value */ zval_ptr_dtor(&retval); } else { diff --git a/main/php_output.h b/main/php_output.h index f0a26824936a..4db6809d4936 100644 --- a/main/php_output.h +++ b/main/php_output.h @@ -112,12 +112,6 @@ typedef zend_result (*php_output_handler_conflict_check_t)(const char *handler_n /* ctor for aliases */ typedef struct _php_output_handler *(*php_output_handler_alias_ctor_t)(const char *handler_name, size_t handler_name_len, size_t chunk_size, int flags); -typedef struct _php_output_handler_user_func_t { - zend_fcall_info fci; - zend_fcall_info_cache fcc; - zval zoh; -} php_output_handler_user_func_t; - typedef struct _php_output_handler { zend_string *name; int flags; @@ -129,7 +123,7 @@ typedef struct _php_output_handler { void (*dtor)(void *opaq); union { - php_output_handler_user_func_t *user; + zend_fcall_info_cache *user_fcc; php_output_handler_context_func_t internal; } func; } php_output_handler; From ec804614315c41d6c7048dcd381316068d1f98ee Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 23 Jul 2026 14:50:01 +0100 Subject: [PATCH 04/17] standard/fpt_fopen_wrapper: use size_t instead of int --- ext/standard/ftp_fopen_wrapper.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/ext/standard/ftp_fopen_wrapper.c b/ext/standard/ftp_fopen_wrapper.c index 2597269bad41..4944310fad60 100644 --- a/ext/standard/ftp_fopen_wrapper.c +++ b/ext/standard/ftp_fopen_wrapper.c @@ -127,8 +127,6 @@ static php_stream *php_ftp_fopen_connect(php_stream_wrapper *wrapper, const char php_uri *resource = NULL; int result, use_ssl, use_ssl_on_data = 0; char tmp_line[512]; - char *transport; - int transport_len; const php_uri_parser *uri_parser = php_stream_context_get_uri_parser("ftp", context); if (uri_parser == NULL) { @@ -150,7 +148,8 @@ static php_stream *php_ftp_fopen_connect(php_stream_wrapper *wrapper, const char if (resource->port == 0) resource->port = 21; - transport_len = (int)spprintf(&transport, 0, "tcp://%s:" ZEND_LONG_FMT, ZSTR_VAL(resource->host), resource->port); + char *transport; + size_t transport_len = spprintf(&transport, 0, "tcp://%s:" ZEND_LONG_FMT, ZSTR_VAL(resource->host), resource->port); stream = php_stream_xport_create(transport, transport_len, REPORT_ERRORS, STREAM_XPORT_CLIENT | STREAM_XPORT_CONNECT, NULL, NULL, context, NULL, NULL); efree(transport); if (stream == NULL) { @@ -420,8 +419,6 @@ php_stream * php_stream_url_wrap_ftp(php_stream_wrapper *wrapper, const char *pa zval *tmpzval; bool allow_overwrite = false; int8_t read_write = 0; - char *transport; - int transport_len; zend_string *error_message = NULL; tmp_line[0] = '\0'; @@ -554,7 +551,9 @@ php_stream * php_stream_url_wrap_ftp(php_stream_wrapper *wrapper, const char *pa if (hoststart == NULL) { hoststart = ZSTR_VAL(resource->host); } - transport_len = (int)spprintf(&transport, 0, "tcp://%s:%d", hoststart, portno); + + char *transport; + size_t transport_len = spprintf(&transport, 0, "tcp://%s:%d", hoststart, portno); datastream = php_stream_xport_create(transport, transport_len, REPORT_ERRORS, STREAM_XPORT_CLIENT | STREAM_XPORT_CONNECT, NULL, NULL, context, &error_message, NULL); efree(transport); if (datastream == NULL) { From 9658dfc1f302f2c9e2c4918af3171ae68fc298e3 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 23 Jul 2026 15:07:17 +0100 Subject: [PATCH 05/17] standard/http_fopen_wrapper: add const qualifiers --- ext/standard/http_fopen_wrapper.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c index 4b2aa7116a24..9301536458a6 100644 --- a/ext/standard/http_fopen_wrapper.c +++ b/ext/standard/http_fopen_wrapper.c @@ -206,7 +206,7 @@ static zend_string *php_stream_http_response_headers_parse(php_stream_wrapper *w /* Process folding headers if starting with a space or a tab. */ if (header_line && (*header_line == ' ' || *header_line == '\t')) { - char *http_folded_header_line = header_line; + const char *http_folded_header_line = header_line; size_t http_folded_header_line_length = *header_line_length; /* Remove the leading white spaces. */ while (*http_folded_header_line == ' ' || *http_folded_header_line == '\t') { @@ -232,7 +232,7 @@ static zend_string *php_stream_http_response_headers_parse(php_stream_wrapper *w char *last_header_value = memchr(last_header_line, ':', last_header_line_length); if (last_header_value) { /* Verify there is no space in header name */ - char *last_header_name = last_header_line + 1; + const char *last_header_name = last_header_line + 1; while (last_header_name < last_header_value) { if (*last_header_name == ' ' || *last_header_name == '\t') { header_info->error = true; From 1b061f851c5b9b8604a8b72339c2c89aac0fffff Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 30 Jul 2026 10:20:44 +0100 Subject: [PATCH 06/17] serializer: move unserialize_callback_func INI tests to subfolder --- .../autoload_must_be_called_twice.phpt} | 0 .../tests/serialize/{ => unserialize_callback_func}/bug26762.phpt | 0 .../tests/serialize/{ => unserialize_callback_func}/bug70213.phpt | 0 .../non_existing.phpt} | 0 .../unserialize_callback_func_INI_modifications.phpt | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename ext/standard/tests/serialize/{serialization_objects_007.phpt => unserialize_callback_func/autoload_must_be_called_twice.phpt} (100%) rename ext/standard/tests/serialize/{ => unserialize_callback_func}/bug26762.phpt (100%) rename ext/standard/tests/serialize/{ => unserialize_callback_func}/bug70213.phpt (100%) rename ext/standard/tests/serialize/{serialization_objects_008.phpt => unserialize_callback_func/non_existing.phpt} (100%) rename ext/standard/tests/serialize/{ => unserialize_callback_func}/unserialize_callback_func_INI_modifications.phpt (100%) diff --git a/ext/standard/tests/serialize/serialization_objects_007.phpt b/ext/standard/tests/serialize/unserialize_callback_func/autoload_must_be_called_twice.phpt similarity index 100% rename from ext/standard/tests/serialize/serialization_objects_007.phpt rename to ext/standard/tests/serialize/unserialize_callback_func/autoload_must_be_called_twice.phpt diff --git a/ext/standard/tests/serialize/bug26762.phpt b/ext/standard/tests/serialize/unserialize_callback_func/bug26762.phpt similarity index 100% rename from ext/standard/tests/serialize/bug26762.phpt rename to ext/standard/tests/serialize/unserialize_callback_func/bug26762.phpt diff --git a/ext/standard/tests/serialize/bug70213.phpt b/ext/standard/tests/serialize/unserialize_callback_func/bug70213.phpt similarity index 100% rename from ext/standard/tests/serialize/bug70213.phpt rename to ext/standard/tests/serialize/unserialize_callback_func/bug70213.phpt diff --git a/ext/standard/tests/serialize/serialization_objects_008.phpt b/ext/standard/tests/serialize/unserialize_callback_func/non_existing.phpt similarity index 100% rename from ext/standard/tests/serialize/serialization_objects_008.phpt rename to ext/standard/tests/serialize/unserialize_callback_func/non_existing.phpt diff --git a/ext/standard/tests/serialize/unserialize_callback_func_INI_modifications.phpt b/ext/standard/tests/serialize/unserialize_callback_func/unserialize_callback_func_INI_modifications.phpt similarity index 100% rename from ext/standard/tests/serialize/unserialize_callback_func_INI_modifications.phpt rename to ext/standard/tests/serialize/unserialize_callback_func/unserialize_callback_func_INI_modifications.phpt From ec562d07d29c57889aed178cbe78c0188ed49553 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 30 Jul 2026 11:30:29 +0100 Subject: [PATCH 07/17] serializer: more unserialize_callback_func INI tests with different types of string callables --- ...precated_partially_supported_callable.phpt | 51 +++++++++++++++++++ .../fn_with_fqn.phpt | 22 ++++++++ .../fn_with_fqn_namespace.phpt | 26 ++++++++++ .../fn_with_null_bytes.phpt | 25 +++++++++ .../non_static_method_private.phpt | 42 +++++++++++++++ .../non_static_method_public.phpt | 42 +++++++++++++++ .../static_method.phpt | 26 ++++++++++ .../static_method_private.phpt | 41 +++++++++++++++ 8 files changed, 275 insertions(+) create mode 100644 ext/standard/tests/serialize/unserialize_callback_func/deprecated_partially_supported_callable.phpt create mode 100644 ext/standard/tests/serialize/unserialize_callback_func/fn_with_fqn.phpt create mode 100644 ext/standard/tests/serialize/unserialize_callback_func/fn_with_fqn_namespace.phpt create mode 100644 ext/standard/tests/serialize/unserialize_callback_func/fn_with_null_bytes.phpt create mode 100644 ext/standard/tests/serialize/unserialize_callback_func/non_static_method_private.phpt create mode 100644 ext/standard/tests/serialize/unserialize_callback_func/non_static_method_public.phpt create mode 100644 ext/standard/tests/serialize/unserialize_callback_func/static_method.phpt create mode 100644 ext/standard/tests/serialize/unserialize_callback_func/static_method_private.phpt diff --git a/ext/standard/tests/serialize/unserialize_callback_func/deprecated_partially_supported_callable.phpt b/ext/standard/tests/serialize/unserialize_callback_func/deprecated_partially_supported_callable.phpt new file mode 100644 index 000000000000..a7806581abe5 --- /dev/null +++ b/ext/standard/tests/serialize/unserialize_callback_func/deprecated_partially_supported_callable.phpt @@ -0,0 +1,51 @@ +--TEST-- +unserialize_callback_func with partially deprecated callable string +--INI-- +unserialize_callback_func=parent::my_unserialize +--FILE-- +getMessage(), PHP_EOL; +} + +try { + $tester = new TesterChild(); + $o = $tester->unserialize($s); + var_dump($o); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +echo "Done"; + +?> +--EXPECTF-- +Error: Invalid callback parent::my_unserialize, cannot access "parent" when no class scope is active + +Deprecated: Use of "parent" in callables is deprecated in %s on line %d +callback_called in TesterParent +object(Foo)#3 (0) { +} +Done diff --git a/ext/standard/tests/serialize/unserialize_callback_func/fn_with_fqn.phpt b/ext/standard/tests/serialize/unserialize_callback_func/fn_with_fqn.phpt new file mode 100644 index 000000000000..67ce94c00dad --- /dev/null +++ b/ext/standard/tests/serialize/unserialize_callback_func/fn_with_fqn.phpt @@ -0,0 +1,22 @@ +--TEST-- +unserialize_callback_func with fully qualified name function +--INI-- +unserialize_callback_func=\my_global_fn +--FILE-- + +--EXPECT-- +callback_called +object(Foo)#1 (0) { +} +Done diff --git a/ext/standard/tests/serialize/unserialize_callback_func/fn_with_fqn_namespace.phpt b/ext/standard/tests/serialize/unserialize_callback_func/fn_with_fqn_namespace.phpt new file mode 100644 index 000000000000..28718bdb2417 --- /dev/null +++ b/ext/standard/tests/serialize/unserialize_callback_func/fn_with_fqn_namespace.phpt @@ -0,0 +1,26 @@ +--TEST-- +unserialize_callback_func with fully qualified named namespaced function +--INI-- +unserialize_callback_func=\php\test\my_global_fn +--FILE-- + +--EXPECT-- +callback_called +object(Foo)#1 (0) { +} +Done diff --git a/ext/standard/tests/serialize/unserialize_callback_func/fn_with_null_bytes.phpt b/ext/standard/tests/serialize/unserialize_callback_func/fn_with_null_bytes.phpt new file mode 100644 index 000000000000..5341d4bb6dd8 --- /dev/null +++ b/ext/standard/tests/serialize/unserialize_callback_func/fn_with_null_bytes.phpt @@ -0,0 +1,25 @@ +--TEST-- +unserialize_callback_func with function name containing null bytes +--FILE-- +getMessage(), PHP_EOL; +} + +echo "Done"; +?> +--EXPECT-- +Error: Invalid callback foo, function "foo" not found or invalid function name +Done diff --git a/ext/standard/tests/serialize/unserialize_callback_func/non_static_method_private.phpt b/ext/standard/tests/serialize/unserialize_callback_func/non_static_method_private.phpt new file mode 100644 index 000000000000..2b3fb93f3d4a --- /dev/null +++ b/ext/standard/tests/serialize/unserialize_callback_func/non_static_method_private.phpt @@ -0,0 +1,42 @@ +--TEST-- +unserialize_callback_func with private non-static method +--INI-- +unserialize_callback_func=Tester::my_unserialize +--FILE-- +getMessage(), PHP_EOL; +} + +try { + $tester = new Tester(); + $o = $tester->unserialize($s); + var_dump($o); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +echo "Done"; + +?> +--EXPECT-- +Error: Invalid callback Tester::my_unserialize, non-static method Tester::my_unserialize() cannot be called statically +callback_called +object(Foo)#3 (0) { +} +Done diff --git a/ext/standard/tests/serialize/unserialize_callback_func/non_static_method_public.phpt b/ext/standard/tests/serialize/unserialize_callback_func/non_static_method_public.phpt new file mode 100644 index 000000000000..ec09c8285a5e --- /dev/null +++ b/ext/standard/tests/serialize/unserialize_callback_func/non_static_method_public.phpt @@ -0,0 +1,42 @@ +--TEST-- +unserialize_callback_func with public non-static method +--INI-- +unserialize_callback_func=Tester::my_unserialize +--FILE-- +getMessage(), PHP_EOL; +} + +try { + $tester = new Tester(); + $o = $tester->unserialize($s); + var_dump($o); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +echo "Done"; + +?> +--EXPECT-- +Error: Invalid callback Tester::my_unserialize, non-static method Tester::my_unserialize() cannot be called statically +callback_called +object(Foo)#3 (0) { +} +Done diff --git a/ext/standard/tests/serialize/unserialize_callback_func/static_method.phpt b/ext/standard/tests/serialize/unserialize_callback_func/static_method.phpt new file mode 100644 index 000000000000..a96d998cd8c6 --- /dev/null +++ b/ext/standard/tests/serialize/unserialize_callback_func/static_method.phpt @@ -0,0 +1,26 @@ +--TEST-- +unserialize_callback_func with public static method +--INI-- +unserialize_callback_func=Tester::my_unserialize +--FILE-- + +--EXPECT-- +callback_called +object(Foo)#1 (0) { +} +Done diff --git a/ext/standard/tests/serialize/unserialize_callback_func/static_method_private.phpt b/ext/standard/tests/serialize/unserialize_callback_func/static_method_private.phpt new file mode 100644 index 000000000000..4bc4e760bed7 --- /dev/null +++ b/ext/standard/tests/serialize/unserialize_callback_func/static_method_private.phpt @@ -0,0 +1,41 @@ +--TEST-- +unserialize_callback_func with private static method +--INI-- +unserialize_callback_func=Tester::my_unserialize +--FILE-- +getMessage(), PHP_EOL; +} + +try { + $o = Tester::unserialize($s); + var_dump($o); +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +echo "Done"; + +?> +--EXPECT-- +Error: Invalid callback Tester::my_unserialize, cannot access private method Tester::my_unserialize() +callback_called +object(Foo)#2 (0) { +} +Done From 79d76a560a5f646881dc45a4b1b76332304e345f Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 30 Jul 2026 11:38:22 +0100 Subject: [PATCH 08/17] serializer: must duplicate unserialize_callback_func global zstr --- ext/standard/var_unserializer.re | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re index 458ad2f12baa..27647c907d3a 100644 --- a/ext/standard/var_unserializer.re +++ b/ext/standard/var_unserializer.re @@ -1256,7 +1256,7 @@ object ":" uiv ":" ["] { } /* Call unserialize callback */ - ZVAL_STR_COPY(&user_func, PG(unserialize_callback_func)); + ZVAL_STR(&user_func, zend_string_dup(PG(unserialize_callback_func), false)); ZVAL_STR(&args[0], class_name); BG(serialize_lock)++; From a66ffb80fd7779dea8928ce6eafbad5932c91529 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Fri, 31 Jul 2026 12:42:43 +0100 Subject: [PATCH 09/17] Zend: move '\' and lowercase handling to zend_fetch_function() (#22966) Rather than having the logic be repeated over and over at the call sites. --- Zend/zend_API.c | 23 +------------------- Zend/zend_ast.c | 11 +++++----- Zend/zend_execute.c | 38 +++++++++++++++++---------------- ext/reflection/php_reflection.c | 29 +++---------------------- 4 files changed, 30 insertions(+), 71 deletions(-) diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 33128e4a7e37..714fa156522e 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -3830,32 +3830,11 @@ static zend_always_inline bool zend_is_callable_check_func(const zval *callable, int call_via_handler = 0; zend_class_entry *scope; zval *zv; - ALLOCA_FLAG(use_heap) fcc->calling_scope = NULL; if (!ce_org) { - zend_function *func; - zend_string *lmname; - - /* Check if function with given name exists. - * This may be a compound name that includes namespace name */ - if (UNEXPECTED(Z_STRVAL_P(callable)[0] == '\\')) { - /* Skip leading \ */ - ZSTR_ALLOCA_ALLOC(lmname, Z_STRLEN_P(callable) - 1, use_heap); - zend_str_tolower_copy(ZSTR_VAL(lmname), Z_STRVAL_P(callable) + 1, Z_STRLEN_P(callable) - 1); - func = zend_fetch_function(lmname); - ZSTR_ALLOCA_FREE(lmname, use_heap); - } else { - lmname = Z_STR_P(callable); - func = zend_fetch_function(lmname); - if (!func) { - ZSTR_ALLOCA_ALLOC(lmname, Z_STRLEN_P(callable), use_heap); - zend_str_tolower_copy(ZSTR_VAL(lmname), Z_STRVAL_P(callable), Z_STRLEN_P(callable)); - func = zend_fetch_function(lmname); - ZSTR_ALLOCA_FREE(lmname, use_heap); - } - } + zend_function *func = zend_fetch_function(Z_STR_P(callable)); if (EXPECTED(func != NULL)) { fcc->function_handler = func; return 1; diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index a91f0d2c933a..4b070d9d5d58 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -1241,15 +1241,16 @@ static zend_result ZEND_FASTCALL zend_ast_evaluate_inner( if (!fptr) { zend_string *function_name = zend_ast_get_str(ast->child[0]); - zend_string *function_name_lc = zend_string_tolower(function_name); - fptr = zend_fetch_function(function_name_lc); + fptr = zend_fetch_function(function_name); + + /* Search for global function of the same name */ if (!fptr && ast->child[0]->attr != ZEND_NAME_FQ) { - const char *backslash = zend_memrchr(ZSTR_VAL(function_name_lc), '\\', ZSTR_LEN(function_name_lc)); + const char *backslash = zend_memrchr(ZSTR_VAL(function_name), '\\', ZSTR_LEN(function_name)); if (backslash) { - fptr = zend_fetch_function_str(backslash + 1, ZSTR_LEN(function_name_lc) - (backslash - ZSTR_VAL(function_name_lc) + 1)); + fptr = zend_fetch_function_str(backslash + 1, ZSTR_LEN(function_name) - (backslash - ZSTR_VAL(function_name) + 1)); } } - zend_string_release(function_name_lc); + if (!fptr) { zend_throw_error(NULL, "Call to undefined function %s()", ZSTR_VAL(function_name)); return FAILURE; diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 91cab1b63d2a..bc93aa8892ac 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -4498,32 +4498,34 @@ static zend_never_inline void ZEND_FASTCALL init_func_run_time_cache(zend_op_arr ZEND_API zend_function * ZEND_FASTCALL zend_fetch_function(zend_string *name) /* {{{ */ { - zval *zv = zend_hash_find(EG(function_table), name); - - if (EXPECTED(zv != NULL)) { - zend_function *fbc = Z_FUNC_P(zv); + zend_function *fbc; + if (UNEXPECTED(ZSTR_VAL(name)[0] == '\\')) { + /* Ignore leading "\" */ + fbc = zend_hash_str_find_ptr_lc(EG(function_table), ZSTR_VAL(name) + 1, ZSTR_LEN(name) - 1); + } else { + fbc = zend_hash_find_ptr_lc(EG(function_table), name); + } - if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { - init_func_run_time_cache_i(&fbc->op_array); - } - return fbc; + if (EXPECTED(fbc && fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { + init_func_run_time_cache_i(&fbc->op_array); } - return NULL; + return fbc; } /* }}} */ ZEND_API zend_function * ZEND_FASTCALL zend_fetch_function_str(const char *name, size_t len) /* {{{ */ { - const zval *zv = zend_hash_str_find(EG(function_table), name, len); - - if (EXPECTED(zv != NULL)) { - zend_function *fbc = Z_FUNC_P(zv); + zend_function *fbc; + if (UNEXPECTED(name[0] == '\\')) { + /* Ignore leading "\" */ + fbc = zend_hash_str_find_ptr_lc(EG(function_table), name + 1, len - 1); + } else { + fbc = zend_hash_str_find_ptr_lc(EG(function_table), name, len); + } - if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { - init_func_run_time_cache_i(&fbc->op_array); - } - return fbc; + if (EXPECTED(fbc && fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) { + init_func_run_time_cache_i(&fbc->op_array); } - return NULL; + return fbc; } /* }}} */ ZEND_API void ZEND_FASTCALL zend_init_func_run_time_cache(zend_op_array *op_array) /* {{{ */ diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 7fa76c121cac..6682346a5b08 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -1684,7 +1684,7 @@ ZEND_METHOD(ReflectionFunction, __construct) { zend_object *closure_obj = NULL; zend_function *fptr; - zend_string *fname, *lcname; + zend_string *fname; zval *object = ZEND_THIS; reflection_object *intern = Z_REFLECTION_P(object); @@ -1696,18 +1696,7 @@ ZEND_METHOD(ReflectionFunction, __construct) if (closure_obj) { fptr = (zend_function*)zend_get_closure_method_def(closure_obj); } else { - if (UNEXPECTED(ZSTR_VAL(fname)[0] == '\\')) { - /* Ignore leading "\" */ - ALLOCA_FLAG(use_heap) - ZSTR_ALLOCA_ALLOC(lcname, ZSTR_LEN(fname) - 1, use_heap); - zend_str_tolower_copy(ZSTR_VAL(lcname), ZSTR_VAL(fname) + 1, ZSTR_LEN(fname) - 1); - fptr = zend_fetch_function(lcname); - ZSTR_ALLOCA_FREE(lcname, use_heap); - } else { - lcname = zend_string_tolower(fname); - fptr = zend_fetch_function(lcname); - zend_string_release(lcname); - } + fptr = zend_fetch_function(fname); if (fptr == NULL) { zend_throw_exception_ex(reflection_exception_ptr, 0, @@ -2429,19 +2418,7 @@ ZEND_METHOD(ReflectionParameter, __construct) switch (Z_TYPE_P(reference)) { case IS_STRING: { zend_string *fname = Z_STR_P(reference); - zend_string *lcname; - if (UNEXPECTED(ZSTR_VAL(fname)[0] == '\\')) { - /* Ignore leading "\" */ - ALLOCA_FLAG(use_heap) - ZSTR_ALLOCA_ALLOC(lcname, ZSTR_LEN(fname) - 1, use_heap); - zend_str_tolower_copy(ZSTR_VAL(lcname), ZSTR_VAL(fname) + 1, ZSTR_LEN(fname) - 1); - fptr = zend_fetch_function(lcname); - ZSTR_ALLOCA_FREE(lcname, use_heap); - } else { - lcname = zend_string_tolower(fname); - fptr = zend_fetch_function(lcname); - zend_string_release(lcname); - } + fptr = zend_fetch_function(fname); if (!fptr) { zend_throw_exception_ex(reflection_exception_ptr, 0, "Function %s() does not exist", Z_STRVAL_P(reference)); From e2d54c22c245bb959a0b3c72cf3973b0dd0e9182 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Fri, 31 Jul 2026 13:55:42 +0100 Subject: [PATCH 10/17] streams: call php_stream_wrapper_warn calls checking REPORT_ERRORS is set (#22965) As the underlying function already checks that REPORT_ERRORS is set in the options parameter. --- ext/standard/ftp_fopen_wrapper.c | 72 +++++++++++--------------------- ext/standard/php_fopen_wrapper.c | 32 ++++++-------- main/streams/plain_wrapper.c | 2 +- main/streams/streams.c | 17 ++++---- 4 files changed, 44 insertions(+), 79 deletions(-) diff --git a/ext/standard/ftp_fopen_wrapper.c b/ext/standard/ftp_fopen_wrapper.c index 4944310fad60..cf529e40085b 100644 --- a/ext/standard/ftp_fopen_wrapper.c +++ b/ext/standard/ftp_fopen_wrapper.c @@ -915,18 +915,14 @@ static int php_stream_ftp_unlink(php_stream_wrapper *wrapper, const char *url, i stream = php_ftp_fopen_connect(wrapper, url, "r", 0, NULL, context, NULL, &resource, NULL, NULL); if (!stream) { - if (options & REPORT_ERRORS) { - php_stream_wrapper_warn(wrapper, context, options, AuthFailed, - "Unable to connect to %s", url); - } + php_stream_wrapper_warn(wrapper, context, options, AuthFailed, + "Unable to connect to %s", url); goto unlink_errexit; } if (resource->path == NULL) { - if (options & REPORT_ERRORS) { - php_stream_wrapper_warn(wrapper, context, options, InvalidPath, - "Invalid path provided in %s", url); - } + php_stream_wrapper_warn(wrapper, context, options, InvalidPath, + "Invalid path provided in %s", url); goto unlink_errexit; } @@ -935,10 +931,8 @@ static int php_stream_ftp_unlink(php_stream_wrapper *wrapper, const char *url, i result = GET_FTP_RESULT(stream); if (result < 200 || result > 299) { - if (options & REPORT_ERRORS) { - php_stream_wrapper_warn(wrapper, context, options, UnlinkFailed, - "Error Deleting file: %s", tmp_line); - } + php_stream_wrapper_warn(wrapper, context, options, UnlinkFailed, + "Error Deleting file: %s", tmp_line); goto unlink_errexit; } @@ -1000,10 +994,8 @@ static int php_stream_ftp_rename(php_stream_wrapper *wrapper, const char *url_fr stream = php_ftp_fopen_connect(wrapper, url_from, "r", 0, NULL, context, NULL, NULL, NULL, NULL); if (!stream) { - if (options & REPORT_ERRORS) { - php_stream_wrapper_warn(wrapper, context, options, AuthFailed, - "Unable to connect to %s", ZSTR_VAL(resource_from->host)); - } + php_stream_wrapper_warn(wrapper, context, options, AuthFailed, + "Unable to connect to %s", ZSTR_VAL(resource_from->host)); goto rename_errexit; } @@ -1012,10 +1004,8 @@ static int php_stream_ftp_rename(php_stream_wrapper *wrapper, const char *url_fr result = GET_FTP_RESULT(stream); if (result < 300 || result > 399) { - if (options & REPORT_ERRORS) { - php_stream_wrapper_warn(wrapper, context, options, RenameFailed, - "Error Renaming file: %s", tmp_line); - } + php_stream_wrapper_warn(wrapper, context, options, RenameFailed, + "Error Renaming file: %s", tmp_line); goto rename_errexit; } @@ -1024,10 +1014,8 @@ static int php_stream_ftp_rename(php_stream_wrapper *wrapper, const char *url_fr result = GET_FTP_RESULT(stream); if (result < 200 || result > 299) { - if (options & REPORT_ERRORS) { - php_stream_wrapper_warn(wrapper, context, options, RenameFailed, - "Error Renaming file: %s", tmp_line); - } + php_stream_wrapper_warn(wrapper, context, options, RenameFailed, + "Error Renaming file: %s", tmp_line); goto rename_errexit; } @@ -1058,18 +1046,14 @@ static int php_stream_ftp_mkdir(php_stream_wrapper *wrapper, const char *url, in stream = php_ftp_fopen_connect(wrapper, url, "r", 0, NULL, context, NULL, &resource, NULL, NULL); if (!stream) { - if (options & REPORT_ERRORS) { - php_stream_wrapper_warn(wrapper, context, options, AuthFailed, - "Unable to connect to %s", url); - } + php_stream_wrapper_warn(wrapper, context, options, AuthFailed, + "Unable to connect to %s", url); goto mkdir_errexit; } if (resource->path == NULL) { - if (options & REPORT_ERRORS) { - php_stream_wrapper_warn(wrapper, context, options, InvalidPath, - "Invalid path provided in %s", url); - } + php_stream_wrapper_warn(wrapper, context, options, InvalidPath, + "Invalid path provided in %s", url); goto mkdir_errexit; } @@ -1108,10 +1092,8 @@ static int php_stream_ftp_mkdir(php_stream_wrapper *wrapper, const char *url, in php_stream_printf(stream, "MKD %s\r\n", buf); result = GET_FTP_RESULT(stream); if (result < 200 || result > 299) { - if (options & REPORT_ERRORS) { - php_stream_wrapper_warn(wrapper, context, options, MkdirFailed, - "%s", tmp_line); - } + php_stream_wrapper_warn(wrapper, context, options, MkdirFailed, + "%s", tmp_line); break; } } @@ -1153,18 +1135,14 @@ static int php_stream_ftp_rmdir(php_stream_wrapper *wrapper, const char *url, in stream = php_ftp_fopen_connect(wrapper, url, "r", 0, NULL, context, NULL, &resource, NULL, NULL); if (!stream) { - if (options & REPORT_ERRORS) { - php_stream_wrapper_warn(wrapper, context, options, AuthFailed, - "Unable to connect to %s", url); - } + php_stream_wrapper_warn(wrapper, context, options, AuthFailed, + "Unable to connect to %s", url); goto rmdir_errexit; } if (resource->path == NULL) { - if (options & REPORT_ERRORS) { - php_stream_wrapper_warn(wrapper, context, options, InvalidPath, - "Invalid path provided in %s", url); - } + php_stream_wrapper_warn(wrapper, context, options, InvalidPath, + "Invalid path provided in %s", url); goto rmdir_errexit; } @@ -1172,10 +1150,8 @@ static int php_stream_ftp_rmdir(php_stream_wrapper *wrapper, const char *url, in result = GET_FTP_RESULT(stream); if (result < 200 || result > 299) { - if (options & REPORT_ERRORS) { - php_stream_wrapper_warn(wrapper, context, options, RmdirFailed, - "%s", tmp_line); - } + php_stream_wrapper_warn(wrapper, context, options, RmdirFailed, + "%s", tmp_line); goto rmdir_errexit; } diff --git a/ext/standard/php_fopen_wrapper.c b/ext/standard/php_fopen_wrapper.c index 3f062bf2ea1e..b18832ebe410 100644 --- a/ext/standard/php_fopen_wrapper.c +++ b/ext/standard/php_fopen_wrapper.c @@ -220,11 +220,9 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c php_stream_input_t *input; if ((options & STREAM_OPEN_FOR_INCLUDE) && !PG(allow_url_include) ) { - if (options & REPORT_ERRORS) { - php_stream_wrapper_warn(wrapper, context, options, - Disabled, - "URL file-access is disabled in the server configuration"); - } + php_stream_wrapper_warn(wrapper, context, options, + Disabled, + "URL file-access is disabled in the server configuration"); return NULL; } @@ -241,11 +239,9 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c if (!strcasecmp(path, "stdin")) { if ((options & STREAM_OPEN_FOR_INCLUDE) && !PG(allow_url_include) ) { - if (options & REPORT_ERRORS) { - php_stream_wrapper_warn(wrapper, context, options, - Disabled, - "URL file-access is disabled in the server configuration"); - } + php_stream_wrapper_warn(wrapper, context, options, + Disabled, + "URL file-access is disabled in the server configuration"); return NULL; } if (!strcmp(sapi_module.name, "cli")) { @@ -302,20 +298,16 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c int dtablesize; if (strcmp(sapi_module.name, "cli")) { - if (options & REPORT_ERRORS) { - php_stream_wrapper_warn(wrapper, context, options, - Disabled, - "Direct access to file descriptors is only available from command-line PHP"); - } + php_stream_wrapper_warn(wrapper, context, options, + Disabled, + "Direct access to file descriptors is only available from command-line PHP"); return NULL; } if ((options & STREAM_OPEN_FOR_INCLUDE) && !PG(allow_url_include) ) { - if (options & REPORT_ERRORS) { - php_stream_wrapper_warn(wrapper, context, options, - Disabled, - "URL file-access is disabled in the server configuration"); - } + php_stream_wrapper_warn(wrapper, context, options, + Disabled, + "URL file-access is disabled in the server configuration"); return NULL; } diff --git a/main/streams/plain_wrapper.c b/main/streams/plain_wrapper.c index 4db810b8b5e2..eb9b81b6e2c5 100644 --- a/main/streams/plain_wrapper.c +++ b/main/streams/plain_wrapper.c @@ -1482,7 +1482,7 @@ static int php_plain_files_mkdir(php_stream_wrapper *wrapper, const char *dir, i } int ret = VCWD_MKDIR(dir, (mode_t)mode); - if (ret < 0 && (options & REPORT_ERRORS)) { + if (ret < 0) { php_stream_wrapper_warn(wrapper, context, options, MkdirFailed, "%s", strerror(errno)); return 0; diff --git a/main/streams/streams.c b/main/streams/streams.c index 192689ce036f..cb75e322d928 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -1909,11 +1909,10 @@ PHPAPI php_stream_wrapper *php_stream_locate_url_wrapper(const char *path, const #else if (!localhost && path[n+3] != '\0' && path[n+3] != '/') { #endif - if (options & REPORT_ERRORS) { - php_stream_wrapper_warn(plain_files_wrapper, NULL, options, - ProtocolUnsupported, - "Remote host file access not supported, %s", path); - } + php_stream_wrapper_warn(plain_files_wrapper, NULL, options, + ProtocolUnsupported, + "Remote host file access not supported, %s", path); + return NULL; } @@ -1950,11 +1949,9 @@ PHPAPI php_stream_wrapper *php_stream_locate_url_wrapper(const char *path, const return wrapper; } - if (options & REPORT_ERRORS) { - php_stream_wrapper_warn(plain_files_wrapper, NULL, options, - Disabled, - "file:// wrapper is disabled in the server configuration"); - } + php_stream_wrapper_warn(plain_files_wrapper, NULL, options, + Disabled, + "file:// wrapper is disabled in the server configuration"); return NULL; } From 65fe066d0f4faf9db5a2030418b22f2d980a2e53 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Wed, 29 Jul 2026 15:51:30 +0100 Subject: [PATCH 11/17] Zend: pass callable as zend_string* in zend_is_callable_check_func() --- Zend/zend_API.c | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 714fa156522e..c8959328d370 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -3818,7 +3818,7 @@ ZEND_API void zend_release_fcall_info_cache(zend_fcall_info_cache *fcc) { } } -static zend_always_inline bool zend_is_callable_check_func(const zval *callable, const zend_execute_data *frame, zend_fcall_info_cache *fcc, bool strict_class, char **error, bool suppress_deprecation) /* {{{ */ +static zend_always_inline bool zend_is_callable_check_func(zend_string *callable, const zend_execute_data *frame, zend_fcall_info_cache *fcc, bool strict_class, char **error, bool suppress_deprecation) /* {{{ */ { zend_class_entry *ce_org = fcc->calling_scope; bool retval = false; @@ -3834,7 +3834,7 @@ static zend_always_inline bool zend_is_callable_check_func(const zval *callable, fcc->calling_scope = NULL; if (!ce_org) { - zend_function *func = zend_fetch_function(Z_STR_P(callable)); + zend_function *func = zend_fetch_function(callable); if (EXPECTED(func != NULL)) { fcc->function_handler = func; return 1; @@ -3842,17 +3842,17 @@ static zend_always_inline bool zend_is_callable_check_func(const zval *callable, } /* Split name into class/namespace and method/function names */ - if ((colon = zend_memrchr(Z_STRVAL_P(callable), ':', Z_STRLEN_P(callable))) != NULL && - colon > Z_STRVAL_P(callable) && + if ((colon = zend_memrchr(ZSTR_VAL(callable), ':', ZSTR_LEN(callable))) != NULL && + colon > ZSTR_VAL(callable) && *(colon-1) == ':' ) { size_t mlen; colon--; - clen = colon - Z_STRVAL_P(callable); - mlen = Z_STRLEN_P(callable) - clen - 2; + clen = colon - ZSTR_VAL(callable); + mlen = ZSTR_LEN(callable) - clen - 2; - if (colon == Z_STRVAL_P(callable)) { + if (colon == ZSTR_VAL(callable)) { if (error) *error = estrdup("invalid function name"); return 0; } @@ -3865,7 +3865,7 @@ static zend_always_inline bool zend_is_callable_check_func(const zval *callable, scope = get_scope(frame); } - cname = zend_string_init_interned(Z_STRVAL_P(callable), clen, 0); + cname = zend_string_init_interned(ZSTR_VAL(callable), clen, 0); if (ZSTR_HAS_CE_CACHE(cname) && ZSTR_GET_CE_CACHE(cname)) { fcc->calling_scope = ZSTR_GET_CE_CACHE(cname); if (scope && !fcc->object) { @@ -3897,19 +3897,19 @@ static zend_always_inline bool zend_is_callable_check_func(const zval *callable, if (ce_org && !suppress_deprecation) { zend_error(E_DEPRECATED, "Callables of the form [\"%s\", \"%s\"] are deprecated", - ZSTR_VAL(ce_org->name), Z_STRVAL_P(callable)); + ZSTR_VAL(ce_org->name), ZSTR_VAL(callable)); } - mname = zend_string_init(Z_STRVAL_P(callable) + clen + 2, mlen, 0); + mname = zend_string_init(ZSTR_VAL(callable) + clen + 2, mlen, 0); } else if (ce_org) { /* Try to fetch find static method of given class. */ - mname = Z_STR_P(callable); + mname = callable; zend_string_addref(mname); ftable = &ce_org->function_table; fcc->calling_scope = ce_org; } else { /* We already checked for plain function before. */ if (error) { - zend_spprintf(error, 0, "function \"%s\" not found or invalid function name", Z_STRVAL_P(callable)); + zend_spprintf(error, 0, "function \"%s\" not found or invalid function name", ZSTR_VAL(callable)); } return 0; } @@ -4188,7 +4188,7 @@ ZEND_API bool zend_is_callable_at_frame( } check_func: - ret = zend_is_callable_check_func(callable, frame, fcc, strict_class, error, check_flags & IS_CALLABLE_SUPPRESS_DEPRECATIONS); + ret = zend_is_callable_check_func(Z_STR_P(callable), frame, fcc, strict_class, error, check_flags & IS_CALLABLE_SUPPRESS_DEPRECATIONS); if (fcc == &fcc_local) { zend_release_fcall_info_cache(fcc); } From 46ab6952715e76954eebc548ed885b1f381fd8af Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 30 Jul 2026 21:21:25 +0100 Subject: [PATCH 12/17] Zend: rename zend_is_callable_check_func function Rename it to zend_is_string_callable as it is more descriptive --- Zend/zend_API.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Zend/zend_API.c b/Zend/zend_API.c index c8959328d370..261c74eac39d 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -3818,7 +3818,7 @@ ZEND_API void zend_release_fcall_info_cache(zend_fcall_info_cache *fcc) { } } -static zend_always_inline bool zend_is_callable_check_func(zend_string *callable, const zend_execute_data *frame, zend_fcall_info_cache *fcc, bool strict_class, char **error, bool suppress_deprecation) /* {{{ */ +static zend_always_inline bool zend_is_string_callable(zend_string *callable, const zend_execute_data *frame, zend_fcall_info_cache *fcc, bool strict_class, char **error, bool suppress_deprecation) /* {{{ */ { zend_class_entry *ce_org = fcc->calling_scope; bool retval = false; @@ -4188,7 +4188,7 @@ ZEND_API bool zend_is_callable_at_frame( } check_func: - ret = zend_is_callable_check_func(Z_STR_P(callable), frame, fcc, strict_class, error, check_flags & IS_CALLABLE_SUPPRESS_DEPRECATIONS); + ret = zend_is_string_callable(Z_STR_P(callable), frame, fcc, strict_class, error, check_flags & IS_CALLABLE_SUPPRESS_DEPRECATIONS); if (fcc == &fcc_local) { zend_release_fcall_info_cache(fcc); } From c309ad8124221c17f171a4b98c3259ee86dca39e Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Wed, 29 Jul 2026 15:53:15 +0100 Subject: [PATCH 13/17] Zend: use bool type for call_via_handler variable --- Zend/zend_API.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 261c74eac39d..4c38b5447fbe 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -3827,7 +3827,7 @@ static zend_always_inline bool zend_is_string_callable(zend_string *callable, co const char *colon; size_t clen; HashTable *ftable; - int call_via_handler = 0; + bool call_via_handler = false; zend_class_entry *scope; zval *zv; @@ -3959,7 +3959,7 @@ static zend_always_inline bool zend_is_string_callable(zend_string *callable, co if (fcc->object && fcc->calling_scope == ce_org) { if (strict_class && ce_org->__call) { fcc->function_handler = zend_get_call_trampoline_func(ce_org->__call, mname); - call_via_handler = 1; + call_via_handler = true; retval = true; } else { fcc->function_handler = fcc->object->handlers->get_method(&fcc->object, mname, NULL); From 4c3bd1a9156092c677fcea36f4aa11f30372ef29 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Wed, 29 Jul 2026 16:07:16 +0100 Subject: [PATCH 14/17] Zend: move and rename some zend_string definitions --- Zend/zend_API.c | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 4c38b5447fbe..5e5f39213f39 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -3822,10 +3822,8 @@ static zend_always_inline bool zend_is_string_callable(zend_string *callable, co { zend_class_entry *ce_org = fcc->calling_scope; bool retval = false; - zend_string *mname, *cname; - zend_string *lmname; + zend_string *mname; const char *colon; - size_t clen; HashTable *ftable; bool call_via_handler = false; zend_class_entry *scope; @@ -3849,8 +3847,9 @@ static zend_always_inline bool zend_is_string_callable(zend_string *callable, co size_t mlen; colon--; - clen = colon - ZSTR_VAL(callable); - mlen = ZSTR_LEN(callable) - clen - 2; + + size_t class_name_len = colon - ZSTR_VAL(callable); + mlen = ZSTR_LEN(callable) - class_name_len - 2; if (colon == ZSTR_VAL(callable)) { if (error) *error = estrdup("invalid function name"); @@ -3865,9 +3864,9 @@ static zend_always_inline bool zend_is_string_callable(zend_string *callable, co scope = get_scope(frame); } - cname = zend_string_init_interned(ZSTR_VAL(callable), clen, 0); - if (ZSTR_HAS_CE_CACHE(cname) && ZSTR_GET_CE_CACHE(cname)) { - fcc->calling_scope = ZSTR_GET_CE_CACHE(cname); + zend_string *class_name = zend_string_init_interned(ZSTR_VAL(callable), class_name_len, 0); + if (ZSTR_HAS_CE_CACHE(class_name) && ZSTR_GET_CE_CACHE(class_name)) { + fcc->calling_scope = ZSTR_GET_CE_CACHE(class_name); if (scope && !fcc->object) { zend_object *object = zend_get_this_object(frame); @@ -3883,11 +3882,11 @@ static zend_always_inline bool zend_is_string_callable(zend_string *callable, co fcc->called_scope = fcc->object ? fcc->object->ce : fcc->calling_scope; } strict_class = true; - } else if (!zend_is_callable_check_class(cname, scope, frame, fcc, &strict_class, error, suppress_deprecation || ce_org != NULL)) { - zend_string_release_ex(cname, 0); + } else if (!zend_is_callable_check_class(class_name, scope, frame, fcc, &strict_class, error, suppress_deprecation || ce_org != NULL)) { + zend_string_release_ex(class_name, 0); return 0; } - zend_string_release_ex(cname, 0); + zend_string_release_ex(class_name, 0); ftable = &fcc->calling_scope->function_table; if (ce_org && !instanceof_function(ce_org, fcc->calling_scope)) { @@ -3899,7 +3898,7 @@ static zend_always_inline bool zend_is_string_callable(zend_string *callable, co "Callables of the form [\"%s\", \"%s\"] are deprecated", ZSTR_VAL(ce_org->name), ZSTR_VAL(callable)); } - mname = zend_string_init(ZSTR_VAL(callable) + clen + 2, mlen, 0); + mname = zend_string_init(ZSTR_VAL(callable) + class_name_len + 2, mlen, 0); } else if (ce_org) { /* Try to fetch find static method of given class. */ mname = callable; @@ -3914,7 +3913,7 @@ static zend_always_inline bool zend_is_string_callable(zend_string *callable, co return 0; } - lmname = zend_string_tolower(mname); + zend_string *lmname = zend_string_tolower(mname); if (strict_class && fcc->calling_scope && zend_string_equals_literal(lmname, ZEND_CONSTRUCTOR_FUNC_NAME)) { From af7a6c704a2125ba8fb5b6fb2d9590622074133a Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 30 Jul 2026 21:33:41 +0100 Subject: [PATCH 15/17] Zend: remove goto in zend_is_callable_at_frame() But moving the call into the switch statement rather than trying to understand this code flow. --- Zend/zend_API.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 5e5f39213f39..28f24774b71c 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -4186,7 +4186,6 @@ ZEND_API bool zend_is_callable_at_frame( return 1; } -check_func: ret = zend_is_string_callable(Z_STR_P(callable), frame, fcc, strict_class, error, check_flags & IS_CALLABLE_SUPPRESS_DEPRECATIONS); if (fcc == &fcc_local) { zend_release_fcall_info_cache(fcc); @@ -4238,10 +4237,13 @@ ZEND_API bool zend_is_callable_at_frame( } } - callable = method; - goto check_func; + ret = zend_is_string_callable(Z_STR_P(method), frame, fcc, strict_class, error, check_flags & IS_CALLABLE_SUPPRESS_DEPRECATIONS); + if (fcc == &fcc_local) { + zend_release_fcall_info_cache(fcc); + } + return ret; } - return 0; + case IS_OBJECT: if (Z_OBJ_HANDLER_P(callable, get_closure) && Z_OBJ_HANDLER_P(callable, get_closure)(Z_OBJ_P(callable), &fcc->calling_scope, &fcc->function_handler, &fcc->object, 1) == SUCCESS) { fcc->called_scope = fcc->calling_scope; From 76e0734ec5641b3ce5749a6ad7da28099da8a80b Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 30 Jul 2026 21:43:47 +0100 Subject: [PATCH 16/17] Zend: unify cleanup in zend_is_callable_at_frame() --- Zend/zend_API.c | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 28f24774b71c..83c1ed312c78 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -4187,10 +4187,7 @@ ZEND_API bool zend_is_callable_at_frame( } ret = zend_is_string_callable(Z_STR_P(callable), frame, fcc, strict_class, error, check_flags & IS_CALLABLE_SUPPRESS_DEPRECATIONS); - if (fcc == &fcc_local) { - zend_release_fcall_info_cache(fcc); - } - return ret; + break; case IS_ARRAY: { @@ -4238,23 +4235,18 @@ ZEND_API bool zend_is_callable_at_frame( } ret = zend_is_string_callable(Z_STR_P(method), frame, fcc, strict_class, error, check_flags & IS_CALLABLE_SUPPRESS_DEPRECATIONS); - if (fcc == &fcc_local) { - zend_release_fcall_info_cache(fcc); - } - return ret; + break; } case IS_OBJECT: - if (Z_OBJ_HANDLER_P(callable, get_closure) && Z_OBJ_HANDLER_P(callable, get_closure)(Z_OBJ_P(callable), &fcc->calling_scope, &fcc->function_handler, &fcc->object, 1) == SUCCESS) { - fcc->called_scope = fcc->calling_scope; - fcc->closure = Z_OBJ_P(callable); - if (fcc == &fcc_local) { - zend_release_fcall_info_cache(fcc); - } - return 1; + if (Z_OBJ_HANDLER_P(callable, get_closure) && Z_OBJ_HANDLER_P(callable, get_closure)(Z_OBJ_P(callable), &fcc->calling_scope, &fcc->function_handler, &fcc->object, 1) == FAILURE) { + if (error) *error = estrdup("no array or string given"); + return 0; } - if (error) *error = estrdup("no array or string given"); - return 0; + fcc->called_scope = fcc->calling_scope; + fcc->closure = Z_OBJ_P(callable); + ret = true; + break; case IS_REFERENCE: callable = Z_REFVAL_P(callable); goto again; @@ -4262,6 +4254,11 @@ ZEND_API bool zend_is_callable_at_frame( if (error) *error = estrdup("no array or string given"); return 0; } + + if (fcc == &fcc_local) { + zend_release_fcall_info_cache(fcc); + } + return ret; } /* }}} */ From 0dc8a54f6296bd6738571ef116c23143724611cf Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Thu, 30 Jul 2026 22:21:32 +0100 Subject: [PATCH 17/17] Zend: move true function search out of main callability check And rename C function to zend_is_method_callable --- Zend/zend_API.c | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 83c1ed312c78..299d5d57a249 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -3818,7 +3818,7 @@ ZEND_API void zend_release_fcall_info_cache(zend_fcall_info_cache *fcc) { } } -static zend_always_inline bool zend_is_string_callable(zend_string *callable, const zend_execute_data *frame, zend_fcall_info_cache *fcc, bool strict_class, char **error, bool suppress_deprecation) /* {{{ */ +static zend_always_inline bool zend_is_method_callable(zend_string *callable, const zend_execute_data *frame, zend_fcall_info_cache *fcc, bool strict_class, char **error, bool suppress_deprecation) /* {{{ */ { zend_class_entry *ce_org = fcc->calling_scope; bool retval = false; @@ -3831,14 +3831,6 @@ static zend_always_inline bool zend_is_string_callable(zend_string *callable, co fcc->calling_scope = NULL; - if (!ce_org) { - zend_function *func = zend_fetch_function(callable); - if (EXPECTED(func != NULL)) { - fcc->function_handler = func; - return 1; - } - } - /* Split name into class/namespace and method/function names */ if ((colon = zend_memrchr(ZSTR_VAL(callable), ':', ZSTR_LEN(callable))) != NULL && colon > ZSTR_VAL(callable) && @@ -4176,17 +4168,29 @@ ZEND_API bool zend_is_callable_at_frame( again: switch (Z_TYPE_P(callable)) { case IS_STRING: - if (object) { + /* First check for a normal function */ + if (!object) { + if (check_flags & IS_CALLABLE_CHECK_SYNTAX_ONLY) { + return true; + } + + zend_function *func = zend_fetch_function(Z_STR_P(callable)); + if (EXPECTED(func != NULL)) { + fcc->function_handler = func; + return true; + } + /* Might be a static method */ + } else { fcc->object = object; fcc->calling_scope = object->ce; } if (check_flags & IS_CALLABLE_CHECK_SYNTAX_ONLY) { fcc->called_scope = fcc->calling_scope; - return 1; + return true; } - ret = zend_is_string_callable(Z_STR_P(callable), frame, fcc, strict_class, error, check_flags & IS_CALLABLE_SUPPRESS_DEPRECATIONS); + ret = zend_is_method_callable(Z_STR_P(callable), frame, fcc, strict_class, error, check_flags & IS_CALLABLE_SUPPRESS_DEPRECATIONS); break; case IS_ARRAY: @@ -4234,7 +4238,7 @@ ZEND_API bool zend_is_callable_at_frame( } } - ret = zend_is_string_callable(Z_STR_P(method), frame, fcc, strict_class, error, check_flags & IS_CALLABLE_SUPPRESS_DEPRECATIONS); + ret = zend_is_method_callable(Z_STR_P(method), frame, fcc, strict_class, error, check_flags & IS_CALLABLE_SUPPRESS_DEPRECATIONS); break; }