From ef029693dbd002988ae87ed1eef02adfdbbe466a Mon Sep 17 00:00:00 2001 From: Cas Ebbers <617080+CasEbb@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:21:03 +0200 Subject: [PATCH 1/3] Change `pcre.backtrack_limit` in the ini templates to the default value (#22868) --- php.ini-development | 2 +- php.ini-production | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/php.ini-development b/php.ini-development index afabe74ba0e4..8663f1c21de7 100644 --- a/php.ini-development +++ b/php.ini-development @@ -1033,7 +1033,7 @@ cli_server.color = On [Pcre] ; PCRE library backtracking limit. ; https://php.net/pcre.backtrack-limit -;pcre.backtrack_limit=100000 +;pcre.backtrack_limit=1000000 ; PCRE library recursion limit. ; Please note that if you set this value to a high number you may consume all diff --git a/php.ini-production b/php.ini-production index 04a7b699dadd..497a3774b8ed 100644 --- a/php.ini-production +++ b/php.ini-production @@ -1035,7 +1035,7 @@ cli_server.color = On [Pcre] ; PCRE library backtracking limit. ; https://php.net/pcre.backtrack-limit -;pcre.backtrack_limit=100000 +;pcre.backtrack_limit=1000000 ; PCRE library recursion limit. ; Please note that if you set this value to a high number you may consume all From 889ae478bddaf123a219ab7db183b3acedb8c61f Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Thu, 23 Jul 2026 14:37:09 -0400 Subject: [PATCH 2/3] Guard var_dump()/debug_zval_dump() against native stack overflow php_object_property_dump() and serialize() already check ZEND_CHECK_STACK_LIMIT before recursing, but php_array_element_dump() and both debug_zval_dump() element helpers do not, so a deeply nested array or object crashes the process instead of printing "nesting level too deep". Add the same guard to those three helpers. var_export() overflows the same way but needs a throw-based guard, left for a separate change. Closes GH-22871 --- .../var_dump_stack_limit.phpt | 40 +++++++++++++++++++ ext/standard/var.c | 18 +++++++++ 2 files changed, 58 insertions(+) create mode 100644 ext/standard/tests/general_functions/var_dump_stack_limit.phpt diff --git a/ext/standard/tests/general_functions/var_dump_stack_limit.phpt b/ext/standard/tests/general_functions/var_dump_stack_limit.phpt new file mode 100644 index 000000000000..a124e811fc9c --- /dev/null +++ b/ext/standard/tests/general_functions/var_dump_stack_limit.phpt @@ -0,0 +1,40 @@ +--TEST-- +var_dump() and debug_zval_dump() guard against native stack overflow on deep structures +--SKIPIF-- + +--INI-- +zend.max_allowed_stack_size=256K +--FILE-- +next = $newNode; $node = $newNode; } + +function guarded(callable $fn): string { + ob_start(); + $fn(); + return str_contains(ob_get_clean(), 'nesting level too deep') ? "guarded\n" : "NO GUARD\n"; +} + +echo 'var_dump array: ', guarded(fn() => var_dump($a)); +echo 'debug_zval_dump array: ', guarded(fn() => debug_zval_dump($a)); +echo 'debug_zval_dump object: ', guarded(fn() => debug_zval_dump($firstNode)); + +while (is_array($a) && isset($a[0])) { $a = $a[0]; } +while ($next = $firstNode->next) { $firstNode->next = $next->next; } +?> +--EXPECT-- +var_dump array: guarded +debug_zval_dump array: guarded +debug_zval_dump object: guarded diff --git a/ext/standard/var.c b/ext/standard/var.c index acb605d2eabe..99f28366ffdb 100644 --- a/ext/standard/var.c +++ b/ext/standard/var.c @@ -41,6 +41,12 @@ struct php_serialize_data { static void php_array_element_dump(zval *zv, zend_ulong index, zend_string *key, int level) /* {{{ */ { +#ifdef ZEND_CHECK_STACK_LIMIT + if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) { + php_printf("%*cnesting level too deep", level + 1, ' '); + return; + } +#endif if (key == NULL) { /* numeric key */ php_printf("%*c[" ZEND_LONG_FMT "]=>\n", level + 1, ' ', index); } else { /* string key */ @@ -255,6 +261,12 @@ PHP_FUNCTION(var_dump) static void zval_array_element_dump(zval *zv, zend_ulong index, zend_string *key, int level) /* {{{ */ { +#ifdef ZEND_CHECK_STACK_LIMIT + if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) { + php_printf("%*cnesting level too deep", level + 1, ' '); + return; + } +#endif if (key == NULL) { /* numeric key */ php_printf("%*c[" ZEND_LONG_FMT "]=>\n", level + 1, ' ', index); } else { /* string key */ @@ -270,6 +282,12 @@ static void zval_object_property_dump(zend_property_info *prop_info, zval *zv, z { const char *prop_name, *class_name; +#ifdef ZEND_CHECK_STACK_LIMIT + if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) { + php_printf("%*cnesting level too deep", level + 1, ' '); + return; + } +#endif if (key == NULL) { /* numeric key */ php_printf("%*c[" ZEND_LONG_FMT "]=>\n", level + 1, ' ', index); } else { /* string key */ From 1da60a2a392ab8fb608a47a62cb6b4eaffc51b59 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Fri, 24 Jul 2026 15:01:40 +0100 Subject: [PATCH 3/3] streams: php_stream_copy_to_stream_ex() drops progress notifications. The fd-level copy fast path hands the transfer to the kernel, so the bytes bypassed php_sockop_read() and neither the progress increments nor the completion event were emitted. Skip the fast path when either stream carries a context notifier. Fix GH-22841 Close GH-22863 --- NEWS | 2 + ext/standard/tests/streams/gh22841.phpt | 68 +++++++++++++++++++++++++ main/streams/streams.c | 10 +++- 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 ext/standard/tests/streams/gh22841.phpt diff --git a/NEWS b/NEWS index 63e4fdf0c244..79a15820a902 100644 --- a/NEWS +++ b/NEWS @@ -63,6 +63,8 @@ PHP NEWS . Added a new IO copy API used by php_stream_copy_to_stream_ex() that leverages platform primitives (sendfile, splice, copy_file_range, TransmitFile) for faster stream copying. (Jakub Zelenka, David Carlier) + . Fixed bug GH-22841 (php_stream_copy_to_stream_ex() drops progress + notifications when using the copy fast path). (David Carlier) 16 Jul 2026, PHP 8.6.0alpha2 diff --git a/ext/standard/tests/streams/gh22841.phpt b/ext/standard/tests/streams/gh22841.phpt new file mode 100644 index 000000000000..0f03bf83da26 --- /dev/null +++ b/ext/standard/tests/streams/gh22841.phpt @@ -0,0 +1,68 @@ +--TEST-- +GH-22841 (php_stream_copy_to_stream_ex() drops progress notifications) +--EXTENSIONS-- +zlib +--SKIPIF-- + +--INI-- +allow_url_fopen=1 +--CONFLICTS-- +server +--FILE-- + "CONNECT", + STREAM_NOTIFY_MIME_TYPE_IS => "MIME_TYPE_IS", + STREAM_NOTIFY_FILE_SIZE_IS => "FILE_SIZE_IS", + STREAM_NOTIFY_PROGRESS => "PROGRESS", + STREAM_NOTIFY_COMPLETED => "COMPLETED", +]; + +$events = []; +$context = stream_context_create(); +stream_context_set_params($context, ["notification" => + function ($code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) + use (&$events, $names) { + $name = $names[$code] ?? $code; + $event = $name . " " . ($code === STREAM_NOTIFY_FILE_SIZE_IS ? $bytes_max : $bytes_transferred); + /* The body may arrive in more than one read, so collapse consecutive + * progress events into the latest one to keep the output stable. */ + if ($name === "PROGRESS" && str_starts_with(end($events) ?: "", "PROGRESS ")) { + array_pop($events); + } + $events[] = $event; + } +]); + +/* compress.zlib:// opens the inner stream with STREAM_MUST_SEEK, which copies + * the HTTP stream into a temporary file through php_stream_copy_to_stream_ex(). */ +$fp = fopen("compress.zlib://http://" . PHP_CLI_SERVER_ADDRESS . "/", "r", false, $context); +var_dump(stream_get_contents($fp)); +fclose($fp); + +echo implode(PHP_EOL, $events), PHP_EOL; + +?> +--EXPECT-- +string(13) "Hello World! +" +CONNECT 0 +MIME_TYPE_IS 0 +FILE_SIZE_IS 43 +PROGRESS 43 +COMPLETED 43 diff --git a/main/streams/streams.c b/main/streams/streams.c index 3d8830d7291f..7d0171c1ca45 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -1302,7 +1302,12 @@ static zend_result php_stream_filters_seek_all(php_stream *stream, bool is_start return SUCCESS; } - +static bool php_stream_has_notifier(php_stream *stream) +{ + php_stream_context *context = PHP_STREAM_CONTEXT(stream); + /* The fd-level copy cannot emit progress notifications. */ + return context && context->notifier; +} PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence) { @@ -1655,7 +1660,8 @@ PHPAPI zend_result _php_stream_copy_to_stream_ex(php_stream *src, php_stream *de * are empty, so the fd offsets match the logical stream positions */ if (!php_stream_is(src, PHP_STREAM_IS_USERSPACE) && !php_stream_is(dest, PHP_STREAM_IS_USERSPACE) && src->writepos == src->readpos && dest->writepos == dest->readpos && - !php_stream_is_filtered(src) && !php_stream_is_filtered(dest)) { + !php_stream_is_filtered(src) && !php_stream_is_filtered(dest) && + !php_stream_has_notifier(src) && !php_stream_has_notifier(dest)) { php_io_fd src_copy_fd, dest_copy_fd; if (php_stream_cast(src, PHP_STREAM_AS_FD_FOR_COPY, (void *) &src_copy_fd, 0) == SUCCESS &&