Skip to content

Commit 1da60a2

Browse files
committed
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
1 parent fd796bb commit 1da60a2

3 files changed

Lines changed: 78 additions & 2 deletions

File tree

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ PHP NEWS
6363
. Added a new IO copy API used by php_stream_copy_to_stream_ex() that
6464
leverages platform primitives (sendfile, splice, copy_file_range,
6565
TransmitFile) for faster stream copying. (Jakub Zelenka, David Carlier)
66+
. Fixed bug GH-22841 (php_stream_copy_to_stream_ex() drops progress
67+
notifications when using the copy fast path). (David Carlier)
6668

6769
16 Jul 2026, PHP 8.6.0alpha2
6870

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
--TEST--
2+
GH-22841 (php_stream_copy_to_stream_ex() drops progress notifications)
3+
--EXTENSIONS--
4+
zlib
5+
--SKIPIF--
6+
<?php
7+
if (!function_exists("proc_open")) die("skip proc_open() is not available");
8+
?>
9+
--INI--
10+
allow_url_fopen=1
11+
--CONFLICTS--
12+
server
13+
--FILE--
14+
<?php
15+
16+
/* A 43 byte gzip stream decompressing to "Hello World!\n". Kept as a literal so
17+
* that the server, which is started with -n, does not need zlib itself. */
18+
$serverCode = <<<'CODE'
19+
$data = base64_decode("H4sICPQfX2oAA2luZGV4LnBocADzSM3JyVcIzy/KSVHkAgDd3RR9DQAAAA==");
20+
header("Content-Type: application/gzip");
21+
header("Content-Length: " . strlen($data));
22+
echo $data;
23+
CODE;
24+
25+
include __DIR__ . "/../../../../sapi/cli/tests/php_cli_server.inc";
26+
php_cli_server_start($serverCode, null, []);
27+
28+
$names = [
29+
STREAM_NOTIFY_CONNECT => "CONNECT",
30+
STREAM_NOTIFY_MIME_TYPE_IS => "MIME_TYPE_IS",
31+
STREAM_NOTIFY_FILE_SIZE_IS => "FILE_SIZE_IS",
32+
STREAM_NOTIFY_PROGRESS => "PROGRESS",
33+
STREAM_NOTIFY_COMPLETED => "COMPLETED",
34+
];
35+
36+
$events = [];
37+
$context = stream_context_create();
38+
stream_context_set_params($context, ["notification" =>
39+
function ($code, $severity, $message, $message_code, $bytes_transferred, $bytes_max)
40+
use (&$events, $names) {
41+
$name = $names[$code] ?? $code;
42+
$event = $name . " " . ($code === STREAM_NOTIFY_FILE_SIZE_IS ? $bytes_max : $bytes_transferred);
43+
/* The body may arrive in more than one read, so collapse consecutive
44+
* progress events into the latest one to keep the output stable. */
45+
if ($name === "PROGRESS" && str_starts_with(end($events) ?: "", "PROGRESS ")) {
46+
array_pop($events);
47+
}
48+
$events[] = $event;
49+
}
50+
]);
51+
52+
/* compress.zlib:// opens the inner stream with STREAM_MUST_SEEK, which copies
53+
* the HTTP stream into a temporary file through php_stream_copy_to_stream_ex(). */
54+
$fp = fopen("compress.zlib://http://" . PHP_CLI_SERVER_ADDRESS . "/", "r", false, $context);
55+
var_dump(stream_get_contents($fp));
56+
fclose($fp);
57+
58+
echo implode(PHP_EOL, $events), PHP_EOL;
59+
60+
?>
61+
--EXPECT--
62+
string(13) "Hello World!
63+
"
64+
CONNECT 0
65+
MIME_TYPE_IS 0
66+
FILE_SIZE_IS 43
67+
PROGRESS 43
68+
COMPLETED 43

main/streams/streams.c

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,7 +1302,12 @@ static zend_result php_stream_filters_seek_all(php_stream *stream, bool is_start
13021302
return SUCCESS;
13031303
}
13041304

1305-
1305+
static bool php_stream_has_notifier(php_stream *stream)
1306+
{
1307+
php_stream_context *context = PHP_STREAM_CONTEXT(stream);
1308+
/* The fd-level copy cannot emit progress notifications. */
1309+
return context && context->notifier;
1310+
}
13061311

13071312
PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence)
13081313
{
@@ -1655,7 +1660,8 @@ PHPAPI zend_result _php_stream_copy_to_stream_ex(php_stream *src, php_stream *de
16551660
* are empty, so the fd offsets match the logical stream positions */
16561661
if (!php_stream_is(src, PHP_STREAM_IS_USERSPACE) && !php_stream_is(dest, PHP_STREAM_IS_USERSPACE) &&
16571662
src->writepos == src->readpos && dest->writepos == dest->readpos &&
1658-
!php_stream_is_filtered(src) && !php_stream_is_filtered(dest)) {
1663+
!php_stream_is_filtered(src) && !php_stream_is_filtered(dest) &&
1664+
!php_stream_has_notifier(src) && !php_stream_has_notifier(dest)) {
16591665
php_io_fd src_copy_fd, dest_copy_fd;
16601666

16611667
if (php_stream_cast(src, PHP_STREAM_AS_FD_FOR_COPY, (void *) &src_copy_fd, 0) == SUCCESS &&

0 commit comments

Comments
 (0)