Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
40 changes: 40 additions & 0 deletions ext/standard/tests/general_functions/var_dump_stack_limit.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
--TEST--
var_dump() and debug_zval_dump() guard against native stack overflow on deep structures
--SKIPIF--
<?php
if (ini_get('zend.max_allowed_stack_size') === false) {
die('skip No stack limit support');
}
if (getenv('SKIP_ASAN')) {
die('skip ASAN needs different stack limit setting due to more stack space usage');
}
?>
--INI--
zend.max_allowed_stack_size=256K
--FILE--
<?php
$a = [];
for ($i = 0; $i < 50000; $i++) { $a = [$a]; }

class Node { public $next; }
$firstNode = new Node();
$node = $firstNode;
for ($i = 0; $i < 50000; $i++) { $newNode = new Node(); $node->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
68 changes: 68 additions & 0 deletions ext/standard/tests/streams/gh22841.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
--TEST--
GH-22841 (php_stream_copy_to_stream_ex() drops progress notifications)
--EXTENSIONS--
zlib
--SKIPIF--
<?php
if (!function_exists("proc_open")) die("skip proc_open() is not available");
?>
--INI--
allow_url_fopen=1
--CONFLICTS--
server
--FILE--
<?php

/* A 43 byte gzip stream decompressing to "Hello World!\n". Kept as a literal so
* that the server, which is started with -n, does not need zlib itself. */
$serverCode = <<<'CODE'
$data = base64_decode("H4sICPQfX2oAA2luZGV4LnBocADzSM3JyVcIzy/KSVHkAgDd3RR9DQAAAA==");
header("Content-Type: application/gzip");
header("Content-Length: " . strlen($data));
echo $data;
CODE;

include __DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc";
php_cli_server_start($serverCode, null, []);

$names = [
STREAM_NOTIFY_CONNECT => "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
18 changes: 18 additions & 0 deletions ext/standard/var.c
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,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 */
Expand Down Expand Up @@ -257,6 +263,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 */
Expand All @@ -272,6 +284,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 */
Expand Down
10 changes: 8 additions & 2 deletions main/streams/streams.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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 &&
Expand Down
2 changes: 1 addition & 1 deletion php.ini-development
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion php.ini-production
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down