From deb6ef1db127ca9e1e475f96d08b06149a8dcaf9 Mon Sep 17 00:00:00 2001 From: Gina Peter Banyard Date: Fri, 31 Jul 2026 10:33:16 +0100 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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)++;