Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e8e2809
io: add initial php_io copy structure and implementation
bukka Sep 28, 2025
207d27a
io: refactore and simplify the io API
bukka Nov 4, 2025
b20d20f
io: refactore fd types and add io copy function
bukka Nov 4, 2025
1bdbe31
io: modify and use io api in stream copy and add build
bukka Nov 5, 2025
b0553ac
io: fix file copy when dest open in append mode
bukka Nov 5, 2025
e538246
io: skip io copying for userspace streams
bukka Nov 5, 2025
837648a
io: use libc copy_file_range instead of syscall
bukka Nov 5, 2025
758c237
io: fix copying of large files
bukka Nov 5, 2025
945098a
io: add missing header new lines
bukka Nov 5, 2025
9cfb82a
io: try to use loff_t for 32bit in copy_file_range
bukka Nov 5, 2025
7f316a2
io: remove broken copy_file_range offset handling
bukka Nov 26, 2025
0e1de66
io: fix windows io build
bukka Nov 26, 2025
6682f8e
io: fix zend_test copy_file_range wrapper len condition
bukka Dec 31, 2025
35fad37
io: rewrite and fix linux splice handling and add more tests
bukka Dec 31, 2025
d105af6
io: fix some sendfile issues and test it
bukka Dec 31, 2025
1148271
io: add Mswsock.lib to win32 confutils libs for TransmitFile
bukka Dec 31, 2025
40c16a9
io: remove wrong pipe drain in splice
bukka Feb 28, 2026
73f22de
io: remove sendfile use in bsd, macos and solaris
bukka Feb 28, 2026
6cab44b
io: do not use generic fallback on win
bukka Feb 28, 2026
f19f292
io: refactore to use single op and special cast
bukka Mar 7, 2026
3751a91
io: add missing io copy skip for userspace streams
bukka Mar 8, 2026
75ef87e
io: use ssize_t for total_copied in copy fallback
bukka Mar 8, 2026
a8140e9
io: rewrite streams socket copy tests to use client / server
bukka Mar 8, 2026
965c987
io: remove left over io headers
bukka Mar 8, 2026
a653e80
io: add cast to xp_ssl
bukka Mar 15, 2026
35d7f91
io: revert some formatting chages in php_stdiop_cast
bukka Mar 15, 2026
8b9770a
io: fix php_stream_copy_fallback return type
bukka Mar 15, 2026
0d57bfa
io: add stream copy socket to stdout test
bukka Mar 17, 2026
e2f2d15
io: refactore win copy and add timeout polling
bukka Mar 18, 2026
c49902b
io: fix linux timeout handling for splice
bukka Mar 18, 2026
85daa01
io: fix win copy issue related to TransmitFile not advancing file pos
bukka Mar 23, 2026
ab5abe0
io: handle partial writes in splice copy pipe draining
bukka Mar 23, 2026
3970129
freebsd code path optimisation attempt
devnexen May 23, 2026
6dfff79
various freebsd fixes
devnexen May 23, 2026
ba872b1
solaris/illumos implementation
devnexen Jun 6, 2026
01f7629
enable freebsd code path for dragonfly
devnexen Jun 6, 2026
1c57d5c
macos implementation attempt
devnexen Jun 6, 2026
4c30e3e
optimise copy paths with larger buffers and zero-copy hints
devnexen Jun 11, 2026
e214146
io: add stream copy tests for pipe, ssl, append and maxlen paths
devnexen Jun 12, 2026
8b0bc14
respect socket timeouts in generic copy path
devnexen Jun 12, 2026
727f637
io: test bounded sendfile and pipe-to-socket copy paths
devnexen Jun 16, 2026
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
14 changes: 14 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,9 @@ AC_SEARCH_LIBS([Pgrab], [proc])
dnl Haiku does not have network api in libc.
AC_SEARCH_LIBS([setsockopt], [network])

dnl Solaris/illumos provide sendfile() in libsendfile; libc on Linux/FreeBSD.
AC_SEARCH_LIBS([sendfile], [sendfile])

dnl Check for openpty. It may require linking against libutil or libbsd.
AC_CHECK_FUNCS([openpty],,
[AC_SEARCH_LIBS([openpty], [util bsd], [AC_DEFINE([HAVE_OPENPTY], [1])])])
Expand Down Expand Up @@ -587,10 +590,12 @@ AC_CHECK_FUNCS(m4_normalize([
putenv
reallocarray
scandir
sendfile
setenv
setitimer
shutdown
sigprocmask
splice
statfs
statvfs
std_syslog
Expand Down Expand Up @@ -1682,6 +1687,15 @@ PHP_ADD_SOURCES_X([main],
[PHP_FASTCGI_OBJS],
[no])

PHP_ADD_SOURCES([main/io], m4_normalize([
php_io.c
php_io_copy_linux.c
php_io_copy_freebsd.c
php_io_copy_solaris.c
php_io_copy_macos.c
]),
[-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1])

PHP_ADD_SOURCES([main/streams], m4_normalize([
cast.c
filter.c
Expand Down
71 changes: 71 additions & 0 deletions ext/openssl/tests/stream_copy_to_stream_ssl_to_file.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
--TEST--
stream_copy_to_stream() from a TLS stream copies decrypted data (no fd fast-path)
--EXTENSIONS--
openssl
--SKIPIF--
<?php
if (!function_exists("proc_open")) die("skip no proc_open");
?>
--FILE--
<?php

$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'stream_copy_ssl.pem.tmp';
$cacertFile = __DIR__ . DIRECTORY_SEPARATOR . 'stream_copy_ssl-ca.pem.tmp';

$serverCode = <<<'CODE'
$serverCtx = stream_context_create(['ssl' => [
'local_cert' => '%s',
]]);
$flags = STREAM_SERVER_BIND | STREAM_SERVER_LISTEN;
$server = stream_socket_server("ssl://127.0.0.1:0", $errno, $errstr, $flags, $serverCtx);
phpt_notify_server_start($server);

$conn = stream_socket_accept($server, 5);
fwrite($conn, str_repeat("secret-", 1000));
fclose($conn);
fclose($server);
CODE;
$serverCode = sprintf($serverCode, $certFile);

$peerName = 'stream_copy_ssl_peer';
$clientCode = <<<'CODE'
$clientCtx = stream_context_create(['ssl' => [
'verify_peer' => true,
'cafile' => '%s',
'peer_name' => '%s',
]]);
$client = stream_socket_client("ssl://{{ ADDR }}", $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $clientCtx);

$tmp = tmpfile();
/* If the copy offloaded the raw socket fd it would write ciphertext; the
* decrypted plaintext proves it correctly fell back to the userspace loop. */
$copied = stream_copy_to_stream($client, $tmp);
var_dump($copied);

fseek($tmp, 0, SEEK_SET);
$content = stream_get_contents($tmp);
var_dump(strlen($content));
var_dump($content === str_repeat("secret-", 1000));

fclose($tmp);
fclose($client);
CODE;
$clientCode = sprintf($clientCode, $cacertFile, $peerName);

include 'CertificateGenerator.inc';
$certificateGenerator = new CertificateGenerator();
$certificateGenerator->saveCaCert($cacertFile);
$certificateGenerator->saveNewCertAsFileWithKey($peerName, $certFile);

include 'ServerClientTestCase.inc';
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
?>
--CLEAN--
<?php
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'stream_copy_ssl.pem.tmp');
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'stream_copy_ssl-ca.pem.tmp');
?>
--EXPECT--
int(7000)
int(7000)
bool(true)
14 changes: 13 additions & 1 deletion ext/openssl/xp_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
#include "zend_exceptions.h"
#include "php_openssl.h"
#include "php_openssl_backend.h"
#include "php_network.h"
#include "php_io.h"
#include <openssl/ssl.h>
#include <openssl/rsa.h>
#include <openssl/x509.h>
Expand Down Expand Up @@ -3571,6 +3571,18 @@ static int php_openssl_sockop_cast(php_stream *stream, int castas, void **ret)
*(php_socket_t *)ret = sslsock->s.socket;
}
return SUCCESS;
case PHP_STREAM_AS_FD_FOR_COPY:
if (sslsock->ssl_active) {
return FAILURE;
}
if (ret) {
php_io_fd *copy_fd = (php_io_fd *) ret;
copy_fd->socket = sslsock->s.socket;
copy_fd->fd_type = PHP_IO_FD_SOCKET;
copy_fd->timeout = sslsock->s.timeout;
copy_fd->is_blocked = sslsock->s.is_blocked;
}
return SUCCESS;
default:
return FAILURE;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
--TEST--
stream_copy_to_stream() file to file with an append-mode destination
--FILE--
<?php

$srcFile = __DIR__ . '/stream_copy_append_src.txt';
$dstFile = __DIR__ . '/stream_copy_append_dst.txt';

file_put_contents($srcFile, str_repeat("b", 3000));
file_put_contents($dstFile, "PREFIX-");

$src = fopen($srcFile, 'r');
/* O_APPEND must disable the fd-level copy fast-path and still append correctly. */
$dst = fopen($dstFile, 'a');

$copied = stream_copy_to_stream($src, $dst);
var_dump($copied);

fclose($src);
fclose($dst);

$result = file_get_contents($dstFile);
var_dump(strlen($result));
var_dump($result === "PREFIX-" . str_repeat("b", 3000));
?>
--CLEAN--
<?php
@unlink(__DIR__ . '/stream_copy_append_src.txt');
@unlink(__DIR__ . '/stream_copy_append_dst.txt');
?>
--EXPECT--
int(3000)
int(3007)
bool(true)
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
--TEST--
stream_copy_to_stream() file to socket with a maxlength shorter than the file (bounded sendfile + source offset)
--SKIPIF--
<?php
if (!function_exists("proc_open")) die("skip no proc_open");
?>
--FILE--
<?php

$serverCode = <<<'CODE'
$server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr);
phpt_notify_server_start($server);

$conn = stream_socket_accept($server);
$result = stream_get_contents($conn);

phpt_notify(message: strlen($result));
phpt_notify(message: $result === str_repeat("A", 8192) ? "match" : "mismatch");

fclose($conn);
fclose($server);
CODE;

$clientCode = <<<'CODE'
$src = tmpfile();
fwrite($src, str_repeat("A", 8192) . str_repeat("B", 8192));
rewind($src);

$dest = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 10);

/* Only the first 8192 bytes must be sent: the bounded sendfile path has to
* stop at maxlen rather than streaming to EOF. */
$copied = stream_copy_to_stream($src, $dest, 8192);
var_dump($copied);

/* The source position must have advanced by exactly maxlen, so the kernel
* offload restored the descriptor offset to the maxlen boundary. */
$rest = fread($src, 8192);
var_dump(strlen($rest));
var_dump($rest === str_repeat("B", 8192));

fclose($dest);
fclose($src);

var_dump((int) trim(phpt_wait()));
var_dump(trim(phpt_wait()) === "match");
CODE;

include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
?>
--EXPECT--
int(8192)
int(8192)
bool(true)
int(8192)
bool(true)
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
--TEST--
stream_copy_to_stream() 16k with file as $source and socket as $dest
--SKIPIF--
<?php
if (!function_exists("proc_open")) die("skip no proc_open");
?>
--FILE--
<?php

$serverCode = <<<'CODE'
$server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr);
phpt_notify_server_start($server);

$conn = stream_socket_accept($server);
$data = str_repeat('data', 4096);
$result = stream_get_contents($conn);

phpt_notify(message: strlen($result));
phpt_notify(message: $result === $data ? "match" : "mismatch");

fclose($conn);
fclose($server);
CODE;

$clientCode = <<<'CODE'
$src = tmpfile();
$data = str_repeat('data', 4096);
fwrite($src, $data);
rewind($src);

$dest = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 10);
$copied = stream_copy_to_stream($src, $dest);
var_dump($copied);

fclose($dest);
fclose($src);

var_dump((int) trim(phpt_wait()));
var_dump(trim(phpt_wait()) === "match");
CODE;

include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
?>
--EXPECT--
int(16384)
int(16384)
bool(true)
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
--TEST--
stream_copy_to_stream() file to socket with a maxlength larger than the file (sendfile stops at EOF)
--SKIPIF--
<?php
if (!function_exists("proc_open")) die("skip no proc_open");
?>
--FILE--
<?php

$serverCode = <<<'CODE'
$server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr);
phpt_notify_server_start($server);

$conn = stream_socket_accept($server);
$result = stream_get_contents($conn);

phpt_notify(message: strlen($result));
phpt_notify(message: $result === str_repeat("A", 4096) ? "match" : "mismatch");

fclose($conn);
fclose($server);
CODE;

$clientCode = <<<'CODE'
$src = tmpfile();
fwrite($src, str_repeat("A", 4096));
rewind($src);

$dest = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 10);

/* maxlen exceeds the file size: sendfile must stop at EOF and report only
* the bytes actually available rather than blocking for the full maxlen. */
$copied = stream_copy_to_stream($src, $dest, 100000);
var_dump($copied);

/* Nothing left to read once the whole file has been consumed. */
var_dump(strlen(fread($src, 4096)));

fclose($dest);
fclose($src);

var_dump((int) trim(phpt_wait()));
var_dump(trim(phpt_wait()) === "match");
CODE;

include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
?>
--EXPECT--
int(4096)
int(0)
int(4096)
bool(true)
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
--TEST--
stream_copy_to_stream() with a pipe as $source and file as $dest
--SKIPIF--
<?php
if (!function_exists("proc_open")) die("skip no proc_open");
?>
--FILE--
<?php

$descriptors = [1 => ['pipe', 'w']];
$proc = proc_open(
[PHP_BINARY, '-n', '-r', 'echo str_repeat("p", 5000);'],
$descriptors,
$pipes
);
var_dump(is_resource($proc));

$source = $pipes[1];
$tmp = tmpfile();

$copied = stream_copy_to_stream($source, $tmp);
var_dump($copied);

fseek($tmp, 0, SEEK_SET);
$content = stream_get_contents($tmp);
var_dump(strlen($content));
var_dump($content === str_repeat("p", 5000));

fclose($tmp);
fclose($source);
proc_close($proc);
?>
--EXPECT--
bool(true)
int(5000)
int(5000)
bool(true)
Loading