Browse Source

IO copy API for stream copying (#20399)

This introduces new API for fd copying and modifies
php_stream_copy_to_stream_ex to use it. The implementation is separated
for various platforms and the end result have couple of implications:

- sendfile is used for copying file to generic fd (e.g. sockets) on all
  platforms except Windows that use TransmitFile
- splice is used for copying between generic fds (e.g. sockets) on
  Linux
- copy_file_range should get used on alpine linux with directly using
  syscall (as musl does not seem to implement it)
- copy_file_range is used in the loop so it is used multiple times for
  files bigger than 2GB on Linux.
- file mmap for copying is removed as it allowed crashing PHP when
  another process modified mapped file - this was used as a fallback
  for file copying. Sendfile should partially replace it.
- File to file copying was optimized on Windows with use of ReadFile
  and WriteFile.

This also adds various tests including Linux unit tests.

Closes GH-20399

Co-authored-by: David Carlier <devnexen@gmail.com>
pull/22808/head
Jakub Zelenka 2 weeks ago
committed by GitHub
parent
commit
32a0f3b6cf
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      .github/workflows/unit-tests.yml
  2. 5
      NEWS
  3. 4
      UPGRADING.INTERNALS
  4. 14
      configure.ac
  5. 71
      ext/openssl/tests/stream_copy_to_stream_ssl_to_file.phpt
  6. 14
      ext/openssl/xp_ssl.c
  7. 34
      ext/standard/tests/streams/stream_copy_to_stream_file_to_file_append.phpt
  8. 38
      ext/standard/tests/streams/stream_copy_to_stream_file_to_file_dest_read_ahead.phpt
  9. 44
      ext/standard/tests/streams/stream_copy_to_stream_file_to_file_over_2gb.phpt
  10. 57
      ext/standard/tests/streams/stream_copy_to_stream_file_to_socket_maxlen.phpt
  11. 48
      ext/standard/tests/streams/stream_copy_to_stream_file_to_socket_medium.phpt
  12. 53
      ext/standard/tests/streams/stream_copy_to_stream_file_to_socket_over_eof.phpt
  13. 37
      ext/standard/tests/streams/stream_copy_to_stream_pipe_to_file.phpt
  14. 54
      ext/standard/tests/streams/stream_copy_to_stream_pipe_to_socket.phpt
  15. 30
      ext/standard/tests/streams/stream_copy_to_stream_socket.phpt
  16. 23
      ext/standard/tests/streams/stream_copy_to_stream_socket_empty.phpt
  17. 42
      ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_large.phpt
  18. 48
      ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_maxlen.phpt
  19. 49
      ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_single.phpt
  20. 42
      ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_small.phpt
  21. 69
      ext/standard/tests/streams/stream_copy_to_stream_socket_to_socket.phpt
  22. 32
      ext/standard/tests/streams/stream_copy_to_stream_socket_to_stdout.phpt
  23. 27
      ext/standard/tests/streams/stream_copy_to_stream_temp_source_partially_read.phpt
  24. 2
      ext/zend_test/test.c
  25. 185
      main/io/php_io.c
  26. 98
      main/io/php_io_copy_freebsd.c
  27. 354
      main/io/php_io_copy_linux.c
  28. 100
      main/io/php_io_copy_macos.c
  29. 104
      main/io/php_io_copy_solaris.c
  30. 189
      main/io/php_io_copy_windows.c
  31. 27
      main/io/php_io_freebsd.h
  32. 21
      main/io/php_io_generic.h
  33. 41
      main/io/php_io_internal.h
  34. 23
      main/io/php_io_linux.h
  35. 23
      main/io/php_io_macos.h
  36. 23
      main/io/php_io_solaris.h
  37. 23
      main/io/php_io_windows.h
  38. 53
      main/php_io.h
  39. 2
      main/php_streams.h
  40. 12
      main/streams/cast.c
  41. 5
      main/streams/memory.c
  42. 22
      main/streams/plain_wrapper.c
  43. 205
      main/streams/streams.c
  44. 12
      main/streams/xp_socket.c
  45. 4
      tests/unit/Makefile
  46. 293
      tests/unit/main/test_io_copy.c
  47. 5
      win32/build/config.w32
  48. 2
      win32/build/confutils.js

4
.github/workflows/unit-tests.yml

@ -3,6 +3,8 @@ on:
push:
paths:
- 'main/network.c'
- 'main/io/**'
- 'main/php_io.h'
- 'tests/unit/**'
- '.github/workflows/unit-tests.yml'
branches:
@ -10,6 +12,8 @@ on:
pull_request:
paths:
- 'main/network.c'
- 'main/io/**'
- 'main/php_io.h'
- 'tests/unit/**'
- '.github/workflows/unit-tests.yml'
branches:

5
NEWS

@ -35,6 +35,11 @@ PHP NEWS
TCP_USER_TIMEOUT, and SO_LINGER options. (Weilin Du)
. Fixed various memory related issues in ext/sockets. (David Carlier)
- Streams:
. 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)
16 Jul 2026, PHP 8.6.0alpha2
- Core:

4
UPGRADING.INTERNALS

@ -136,6 +136,10 @@ PHP 8.6 INTERNALS UPGRADE NOTES
. Added zend_compile_ast().
. Added zend_check_type_ex().
. Added zend_create_partial_closure().
. Added a new IO copy API in <php_io.h>. php_io_copy() copies bytes between
file descriptors using the most efficient platform primitive available
(sendfile, splice, copy_file_range, TransmitFile), and is now used by
php_stream_copy_to_stream_ex(). The mmap-based copy fallback was removed.
========================
2. Build system changes

14
configure.ac

@ -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])])])
@ -588,10 +591,12 @@ AC_CHECK_FUNCS(m4_normalize([
putenv
reallocarray
scandir
sendfile
setenv
setitimer
shutdown
sigprocmask
splice
statfs
statvfs
std_syslog
@ -1685,6 +1690,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/poll], m4_normalize([
poll_backend_epoll.c
poll_backend_eventport.c

71
ext/openssl/tests/stream_copy_to_stream_ssl_to_file.phpt

@ -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
ext/openssl/xp_ssl.c

@ -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>
@ -3627,6 +3627,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;
}

34
ext/standard/tests/streams/stream_copy_to_stream_file_to_file_append.phpt

@ -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)

38
ext/standard/tests/streams/stream_copy_to_stream_file_to_file_dest_read_ahead.phpt

@ -0,0 +1,38 @@
--TEST--
stream_copy_to_stream() file to file with a partially read destination
--FILE--
<?php
$srcFile = __DIR__ . '/stream_copy_dest_read_ahead_src.txt';
$dstFile = __DIR__ . '/stream_copy_dest_read_ahead_dst.txt';
file_put_contents($srcFile, str_repeat("N", 50));
file_put_contents($dstFile, str_repeat("O", 3000));
$src = fopen($srcFile, 'r');
$dst = fopen($dstFile, 'r+');
/* Buffered read-ahead moves the fd offset past the stream position; the copy
* must land at the stream position. */
fread($dst, 10);
$copied = stream_copy_to_stream($src, $dst);
var_dump($copied);
var_dump(ftell($dst));
fclose($src);
fclose($dst);
$result = file_get_contents($dstFile);
var_dump(strlen($result));
var_dump($result === str_repeat("O", 10) . str_repeat("N", 50) . str_repeat("O", 2940));
?>
--CLEAN--
<?php
@unlink(__DIR__ . '/stream_copy_dest_read_ahead_src.txt');
@unlink(__DIR__ . '/stream_copy_dest_read_ahead_dst.txt');
?>
--EXPECT--
int(50)
int(60)
int(3000)
bool(true)

44
ext/standard/tests/streams/stream_copy_to_stream_file_to_file_over_2gb.phpt

@ -0,0 +1,44 @@
--TEST--
stream_copy_to_stream() copies files larger than 2GB in full
--SKIPIF--
<?php
if (!getenv('RUN_RESOURCE_HEAVY_TESTS')) die('skip resource-heavy test');
if (PHP_INT_SIZE < 8) die('skip 64-bit only');
if (getenv('SKIP_SLOW_TESTS')) die('skip slow test');
$dir = sys_get_temp_dir();
if (disk_free_space($dir) < 4 * 1024 * 1024 * 1024) {
die('skip Reason: Insufficient disk space (less than 4GB)');
}
?>
--FILE--
<?php
$size = 3 * 1024 * 1024 * 1024; // exceeds the ~2GB per-call kernel copy limit
$src = sys_get_temp_dir() . DIRECTORY_SEPARATOR . "stream_copy_over_2gb_src.bin";
$dst = sys_get_temp_dir() . DIRECTORY_SEPARATOR . "stream_copy_over_2gb_dst.bin";
// Create a sparse 3GB source so the copy loop runs without using 3GB of data.
$fh = fopen($src, "wb");
fseek($fh, $size - 1);
fwrite($fh, "\0");
fclose($fh);
$in = fopen($src, "rb");
$out = fopen($dst, "wb");
$copied = stream_copy_to_stream($in, $out);
fclose($in);
fclose($out);
var_dump($copied === $size);
var_dump(filesize($dst) === $size);
unlink($src);
unlink($dst);
?>
--EXPECT--
bool(true)
bool(true)
--CLEAN--
<?php
@unlink(sys_get_temp_dir() . DIRECTORY_SEPARATOR . "stream_copy_over_2gb_src.bin");
@unlink(sys_get_temp_dir() . DIRECTORY_SEPARATOR . "stream_copy_over_2gb_dst.bin");
?>

57
ext/standard/tests/streams/stream_copy_to_stream_file_to_socket_maxlen.phpt

@ -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)

48
ext/standard/tests/streams/stream_copy_to_stream_file_to_socket_medium.phpt

@ -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)

53
ext/standard/tests/streams/stream_copy_to_stream_file_to_socket_over_eof.phpt

@ -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)

37
ext/standard/tests/streams/stream_copy_to_stream_pipe_to_file.phpt

@ -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)

54
ext/standard/tests/streams/stream_copy_to_stream_pipe_to_socket.phpt

@ -0,0 +1,54 @@
--TEST--
stream_copy_to_stream() with a pipe as $source and a socket as $dest (splice from pipe)
--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("p", 5000) ? "match" : "mismatch");
fclose($conn);
fclose($server);
CODE;
$clientCode = <<<'CODE'
/* A real pipe as the source exercises the splice-from-pipe path; with a
* socket destination it must use the corked (SPLICE_F_MORE) variant. */
$descriptors = [1 => ['pipe', 'w']];
$proc = proc_open(
[PHP_BINARY, '-n', '-r', 'echo str_repeat("p", 5000);'],
$descriptors,
$pipes
);
$source = $pipes[1];
$dest = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 10);
$copied = stream_copy_to_stream($source, $dest);
var_dump($copied);
fclose($dest);
fclose($source);
proc_close($proc);
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(5000)
int(5000)
bool(true)

30
ext/standard/tests/streams/stream_copy_to_stream_socket.phpt

@ -1,30 +0,0 @@
--TEST--
stream_copy_to_stream() with socket as $source
--SKIPIF--
<?php
$sockets = @stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0);
if (!$sockets) die("skip stream_socket_pair");
?>
--FILE--
<?php
$sockets = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0);
$tmp = tmpfile();
fwrite($sockets[0], "a");
stream_socket_shutdown($sockets[0], STREAM_SHUT_WR);
stream_copy_to_stream($sockets[1], $tmp);
fseek($tmp, 0, SEEK_SET);
var_dump(stream_get_contents($tmp));
stream_copy_to_stream($sockets[1], $tmp);
fseek($tmp, 0, SEEK_SET);
var_dump(stream_get_contents($tmp));
?>
--EXPECT--
string(1) "a"
string(1) "a"

23
ext/standard/tests/streams/stream_copy_to_stream_socket_empty.phpt

@ -0,0 +1,23 @@
--TEST--
stream_copy_to_stream() from a socket already at EOF returns 0, not false
--SKIPIF--
<?php
if (substr(PHP_OS, 0, 3) == 'WIN') die('skip not for Windows');
?>
--FILE--
<?php
$file = tempnam(sys_get_temp_dir(), 'sct');
$dest = fopen($file, 'wb');
[$a, $b] = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0);
fclose($b); // peer closed without sending; $a is at EOF
$copied = stream_copy_to_stream($a, $dest);
var_dump($copied);
fclose($a);
fclose($dest);
@unlink($file);
?>
--EXPECT--
int(0)

42
ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_large.phpt

@ -0,0 +1,42 @@
--TEST--
stream_copy_to_stream() 200k bytes with socket as $source and file 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);
fwrite($conn, str_repeat("a", 200000));
stream_socket_shutdown($conn, STREAM_SHUT_WR);
/* Keep alive until client is done reading. */
fread($conn, 1);
fclose($conn);
fclose($server);
CODE;
$clientCode = <<<'CODE'
$source = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 10);
$tmp = tmpfile();
stream_copy_to_stream($source, $tmp);
fseek($tmp, 0, SEEK_SET);
var_dump(strlen(stream_get_contents($tmp)));
fclose($tmp);
fclose($source);
CODE;
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
?>
--EXPECT--
int(200000)

48
ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_maxlen.phpt

@ -0,0 +1,48 @@
--TEST--
stream_copy_to_stream() socket to file with a maxlength shorter than the data
--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);
fwrite($conn, str_repeat("a", 10000));
stream_socket_shutdown($conn, STREAM_SHUT_WR);
/* Keep alive until client is done reading. */
fread($conn, 1);
fclose($conn);
fclose($server);
CODE;
$clientCode = <<<'CODE'
$source = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 10);
$tmp = tmpfile();
/* Only 4096 of the 10000 available bytes must be copied. */
$copied = stream_copy_to_stream($source, $tmp, 4096);
var_dump($copied);
fseek($tmp, 0, SEEK_SET);
$content = stream_get_contents($tmp);
var_dump(strlen($content));
var_dump($content === str_repeat("a", 4096));
fclose($tmp);
fclose($source);
CODE;
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
?>
--EXPECT--
int(4096)
int(4096)
bool(true)

49
ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_single.phpt

@ -0,0 +1,49 @@
--TEST--
stream_copy_to_stream() single byte with socket as $source and file 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);
fwrite($conn, "a");
stream_socket_shutdown($conn, STREAM_SHUT_WR);
/* Keep alive until client is done reading. */
fread($conn, 1);
fclose($conn);
fclose($server);
CODE;
$clientCode = <<<'CODE'
$source = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 10);
$tmp = tmpfile();
stream_copy_to_stream($source, $tmp);
fseek($tmp, 0, SEEK_SET);
var_dump(stream_get_contents($tmp));
/* Second copy after EOF should be a no-op. */
stream_copy_to_stream($source, $tmp);
fseek($tmp, 0, SEEK_SET);
var_dump(stream_get_contents($tmp));
fclose($tmp);
fclose($source);
CODE;
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
?>
--EXPECT--
string(1) "a"
string(1) "a"

42
ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_small.phpt

@ -0,0 +1,42 @@
--TEST--
stream_copy_to_stream() 2048 bytes with socket as $source and file 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);
fwrite($conn, str_repeat("a", 2048));
stream_socket_shutdown($conn, STREAM_SHUT_WR);
/* Keep alive until client is done reading. */
fread($conn, 1);
fclose($conn);
fclose($server);
CODE;
$clientCode = <<<'CODE'
$source = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 10);
$tmp = tmpfile();
stream_copy_to_stream($source, $tmp);
fseek($tmp, 0, SEEK_SET);
var_dump(stream_get_contents($tmp));
fclose($tmp);
fclose($source);
CODE;
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
?>
--EXPECTF--
string(2048) "aaaaa%saaa"

69
ext/standard/tests/streams/stream_copy_to_stream_socket_to_socket.phpt

@ -0,0 +1,69 @@
--TEST--
stream_copy_to_stream() socket to socket (splice both directions)
--SKIPIF--
<?php
if (!function_exists("proc_open")) die("skip no proc_open");
?>
--FILE--
<?php
$sourceCode = <<<'CODE'
$server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr);
phpt_notify_server_start($server);
/* Send address again so the client can read it via phpt_wait(). */
phpt_notify(message: stream_socket_get_name($server, false));
$conn = stream_socket_accept($server);
$data = str_repeat('test data ', 1000);
fwrite($conn, $data);
stream_socket_shutdown($conn, STREAM_SHUT_WR);
/* Keep alive until client is done reading. */
fread($conn, 1);
fclose($conn);
fclose($server);
CODE;
$destCode = <<<'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('test data ', 1000) ? "match" : "mismatch");
fclose($conn);
fclose($server);
CODE;
$clientCode = <<<'CODE'
$sourceAddr = trim(phpt_wait("source"));
$source = stream_socket_client("tcp://$sourceAddr", $errno, $errstr, 10);
$dest = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 10);
$copied = stream_copy_to_stream($source, $dest);
var_dump($copied);
stream_socket_shutdown($dest, STREAM_SHUT_WR);
fclose($source);
var_dump((int) trim(phpt_wait("dest")));
var_dump(trim(phpt_wait("dest")) === "match");
fclose($dest);
CODE;
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
ServerClientTestCase::getInstance()->run($clientCode, [
'source' => $sourceCode,
'dest' => $destCode,
]);
?>
--EXPECT--
int(10000)
int(10000)
bool(true)

32
ext/standard/tests/streams/stream_copy_to_stream_socket_to_stdout.phpt

@ -0,0 +1,32 @@
--TEST--
stream_copy_to_stream() with socket as $source and STDOUT 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, 5);
fwrite($conn, "data to stdout\n");
fclose($conn);
fclose($server);
CODE;
$clientCode = <<<'CODE'
$fd = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 10);
stream_copy_to_stream($fd, STDOUT);
fclose($fd);
CODE;
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__);
ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
?>
--EXPECT--
data to stdout

27
ext/standard/tests/streams/stream_copy_to_stream_temp_source_partially_read.phpt

@ -0,0 +1,27 @@
--TEST--
stream_copy_to_stream() with a partially read file-backed php://temp source
--FILE--
<?php
/* Spill php://temp to its tmpfile backing so the copy source delegates to an
* inner stdio stream whose buffer state the fd-level fast path cannot see. */
$src = fopen('php://temp/maxmemory:16', 'r+');
fwrite($src, str_repeat("A", 2000) . str_repeat("B", 2000));
rewind($src);
/* Read-ahead on the inner stream moves its fd offset past the position. */
var_dump(strlen(fread($src, 2000)));
$dst = tmpfile();
$copied = stream_copy_to_stream($src, $dst);
var_dump($copied);
rewind($dst);
var_dump(stream_get_contents($dst) === str_repeat("B", 2000));
fclose($src);
fclose($dst);
?>
--EXPECT--
int(2000)
int(2000)
bool(true)

2
ext/zend_test/test.c

@ -1892,7 +1892,7 @@ typedef off_t off64_t;
PHP_ZEND_TEST_API ssize_t copy_file_range(int fd_in, off64_t *off_in, int fd_out, off64_t *off_out, size_t len, unsigned int flags)
{
ssize_t (*original_copy_file_range)(int, off64_t *, int, off64_t *, size_t, unsigned int) = dlsym(RTLD_NEXT, "copy_file_range");
if (ZT_G(limit_copy_file_range) >= Z_L(0)) {
if (ZT_G(limit_copy_file_range) >= Z_L(0) && ZT_G(limit_copy_file_range) < len) {
len = ZT_G(limit_copy_file_range);
}
return original_copy_file_range(fd_in, off_in, fd_out, off_out, len, flags);

185
main/io/php_io.c

@ -0,0 +1,185 @@
/*
+----------------------------------------------------------------------+
| Copyright © The PHP Group and Contributors. |
+----------------------------------------------------------------------+
| This source file is subject to the Modified BSD License that is |
| bundled with this package in the file LICENSE, and is available |
| through the World Wide Web at <https://www.php.net/license/>. |
| |
| SPDX-License-Identifier: BSD-3-Clause |
+----------------------------------------------------------------------+
| Authors: Jakub Zelenka <bukka@php.net> |
+----------------------------------------------------------------------+
*/
#include "php.h"
#include "php_io.h"
#include "php_io_internal.h"
#include <errno.h>
#ifdef PHP_WIN32
#include <io.h>
#include <winsock2.h>
#else
#include <unistd.h>
#include <poll.h>
#endif
static php_io php_io_instance = {
.copy = PHP_IO_PLATFORM_COPY,
.platform_name = PHP_IO_PLATFORM_NAME,
};
PHPAPI php_io *php_io_get(void)
{
return &php_io_instance;
}
PHPAPI zend_result php_io_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied)
{
return php_io_get()->copy(src, dest, maxlen, copied);
}
zend_result php_io_generic_copy_fallback(int src_fd, int dest_fd, size_t maxlen, size_t *copied)
{
char buf[PHP_IO_COPY_BUFSIZE];
size_t total_copied = 0;
size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen;
zend_result result = SUCCESS;
while (remaining > 0) {
size_t to_read = (remaining < sizeof(buf)) ? remaining : sizeof(buf);
ssize_t bytes_read;
do {
bytes_read = read(src_fd, buf, to_read);
} while (bytes_read < 0 && errno == EINTR);
if (bytes_read < 0) {
result = FAILURE;
break;
} else if (bytes_read == 0) {
break;
}
char *writeptr = buf;
size_t to_write = (size_t) bytes_read;
while (to_write > 0) {
ssize_t bytes_written;
do {
bytes_written = write(dest_fd, writeptr, to_write);
} while (bytes_written < 0 && errno == EINTR);
if (bytes_written <= 0) {
result = FAILURE;
break;
}
total_copied += bytes_written;
writeptr += bytes_written;
to_write -= bytes_written;
}
if (result == FAILURE) {
break;
}
if (maxlen != PHP_IO_COPY_ALL) {
remaining -= bytes_read;
}
}
*copied = total_copied;
return result;
}
/* For a blocking socket source, wait until data is available (or the configured
* timeout elapses) before reading. Mirrors the per-platform wait_for_data
* helpers so the generic copy path honours stream timeouts on systems without a
* kernel offload (e.g. Haiku). Returns >0 ready, 0 timeout, <0 error. */
#ifndef PHP_WIN32
static int php_io_generic_wait_for_data(php_io_fd *fd)
{
if (fd->fd_type != PHP_IO_FD_SOCKET || !fd->is_blocked) {
return 1;
}
int timeout_ms = (fd->timeout.tv_sec == -1)
? -1
: (int) (fd->timeout.tv_sec * 1000 + fd->timeout.tv_usec / 1000);
struct pollfd pfd;
pfd.fd = fd->fd;
pfd.events = POLLIN;
int ret;
do {
ret = poll(&pfd, 1, timeout_ms);
} while (ret == -1 && errno == EINTR);
return ret;
}
#else
static int php_io_generic_wait_for_data(php_io_fd *fd)
{
(void) fd;
return 1;
}
#endif
zend_result php_io_generic_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied)
{
char buf[PHP_IO_COPY_BUFSIZE];
size_t total_copied = 0;
size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen;
zend_result result = SUCCESS;
while (remaining > 0) {
int ready = php_io_generic_wait_for_data(src);
if (ready == 0) {
/* timeout */
break;
} else if (ready < 0) {
result = FAILURE;
break;
}
size_t to_read = (remaining < sizeof(buf)) ? remaining : sizeof(buf);
ssize_t bytes_read;
do {
bytes_read = read(src->fd, buf, to_read);
} while (bytes_read < 0 && errno == EINTR);
if (bytes_read < 0) {
result = FAILURE;
break;
} else if (bytes_read == 0) {
break;
}
char *writeptr = buf;
size_t to_write = (size_t) bytes_read;
while (to_write > 0) {
ssize_t bytes_written;
do {
bytes_written = write(dest->fd, writeptr, to_write);
} while (bytes_written < 0 && errno == EINTR);
if (bytes_written <= 0) {
result = FAILURE;
break;
}
total_copied += bytes_written;
writeptr += bytes_written;
to_write -= bytes_written;
}
if (result == FAILURE) {
break;
}
if (maxlen != PHP_IO_COPY_ALL) {
remaining -= bytes_read;
}
}
*copied = total_copied;
return result;
}

98
main/io/php_io_copy_freebsd.c

@ -0,0 +1,98 @@
/*
+----------------------------------------------------------------------+
| Copyright © The PHP Group and Contributors. |
+----------------------------------------------------------------------+
| This source file is subject to the Modified BSD License that is |
| bundled with this package in the file LICENSE, and is available |
| through the World Wide Web at <https://www.php.net/license/>. |
| |
| SPDX-License-Identifier: BSD-3-Clause |
+----------------------------------------------------------------------+
| Authors: David Carlier <devnexen@gmail.com> |
+----------------------------------------------------------------------+
*/
#if defined(__FreeBSD__) || defined(__DragonFly__)
#include "php_io_internal.h"
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <unistd.h>
#include <errno.h>
/* Hint the kernel to read ahead a few pages from the source file so the disk
* I/O overlaps with the network send. SF_FLAGS() is FreeBSD-specific and may be
* absent on DragonFly, in which case we pass plain 0 (no readahead hint). */
#ifdef SF_FLAGS
# define PHP_IO_FREEBSD_SF_FLAGS SF_FLAGS(16, 0)
#else
# define PHP_IO_FREEBSD_SF_FLAGS 0
#endif
static zend_result php_io_freebsd_sendfile(int src_fd, int dest_fd, size_t maxlen, size_t *copied)
{
off_t start_offset = lseek(src_fd, 0, SEEK_CUR);
if (start_offset == (off_t) -1) {
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied);
}
off_t total_sent = 0;
size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? 0 : maxlen;
zend_result result = SUCCESS;
while (maxlen == PHP_IO_COPY_ALL || remaining > 0) {
off_t sent_in_this_call = 0;
int sendfile_result = sendfile(src_fd, dest_fd, start_offset + total_sent, remaining, NULL,
&sent_in_this_call, PHP_IO_FREEBSD_SF_FLAGS);
if (sent_in_this_call > 0) {
total_sent += sent_in_this_call;
if (maxlen != PHP_IO_COPY_ALL) {
remaining -= (size_t) sent_in_this_call;
}
}
if (sendfile_result == 0) {
if (sent_in_this_call == 0 || remaining == 0) {
break;
}
} else {
if (errno == EINTR) {
continue;
}
if (errno == EAGAIN) {
if (sent_in_this_call > 0) {
continue;
}
break;
}
if (total_sent == 0) {
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied);
}
result = FAILURE;
break;
}
}
if (total_sent > 0) {
/* best effort: keep reporting the delivered bytes even if this fails */
lseek(src_fd, start_offset + total_sent, SEEK_SET);
}
*copied = (size_t) total_sent;
return result;
}
zend_result php_io_freebsd_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied)
{
/* unlike linux, sendfile on freebsd works only under those conditions */
if (src->fd_type == PHP_IO_FD_FILE && dest->fd_type == PHP_IO_FD_SOCKET) {
return php_io_freebsd_sendfile(src->fd, dest->fd, maxlen, copied);
}
/* php_io_generic_copy honours the stream timeout for socket sources */
return php_io_generic_copy(src, dest, maxlen, copied);
}
#endif

354
main/io/php_io_copy_linux.c

@ -0,0 +1,354 @@
/*
+----------------------------------------------------------------------+
| Copyright © The PHP Group and Contributors. |
+----------------------------------------------------------------------+
| This source file is subject to the Modified BSD License that is |
| bundled with this package in the file LICENSE, and is available |
| through the World Wide Web at <https://www.php.net/license/>. |
| |
| SPDX-License-Identifier: BSD-3-Clause |
+----------------------------------------------------------------------+
| Authors: Jakub Zelenka <bukka@php.net> |
+----------------------------------------------------------------------+
*/
#ifdef __linux__
#include "php_io_internal.h"
#include <unistd.h>
#include <errno.h>
#include <sys/syscall.h>
#if !defined(HAVE_COPY_FILE_RANGE) && defined(__NR_copy_file_range)
#define HAVE_COPY_FILE_RANGE 1
static inline ssize_t copy_file_range(
int fd_in, off_t *off_in, int fd_out, off_t *off_out, size_t len, unsigned int flags)
{
return syscall(__NR_copy_file_range, fd_in, off_in, fd_out, off_out, len, flags);
}
#endif
#ifdef HAVE_SENDFILE
#include <sys/sendfile.h>
#endif
#ifdef HAVE_SPLICE
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#endif
static inline int php_io_linux_wait_for_data(php_io_fd *fd)
{
if (fd->fd_type != PHP_IO_FD_SOCKET || !fd->is_blocked) {
return 1;
}
struct timeval *ptimeout = (fd->timeout.tv_sec == -1) ? NULL : &fd->timeout;
int timeout_ms;
if (ptimeout == NULL) {
timeout_ms = -1;
} else {
timeout_ms = ptimeout->tv_sec * 1000 + ptimeout->tv_usec / 1000;
}
struct pollfd pfd;
pfd.fd = fd->fd;
pfd.events = POLLIN;
int ret;
do {
ret = poll(&pfd, 1, timeout_ms);
} while (ret == -1 && errno == EINTR);
return ret;
}
static zend_result php_io_linux_copy_file_to_file(int src_fd, int dest_fd, size_t maxlen, size_t *copied)
{
#ifdef HAVE_COPY_FILE_RANGE
size_t total_copied = 0;
size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen;
while (remaining > 0) {
size_t to_copy = (remaining < SSIZE_MAX) ? remaining : SSIZE_MAX;
ssize_t result = copy_file_range(src_fd, NULL, dest_fd, NULL, to_copy, 0);
if (result > 0) {
total_copied += result;
if (maxlen != PHP_IO_COPY_ALL) {
remaining -= result;
}
} else if (result == 0) {
break;
} else {
switch (errno) {
case EINVAL:
case EXDEV:
case ENOSYS:
case EIO:
if (total_copied == 0) {
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied);
}
break;
default:
*copied = total_copied;
return FAILURE;
}
break;
}
}
if (total_copied > 0) {
*copied = total_copied;
return SUCCESS;
}
#endif
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied);
}
static zend_result php_io_linux_sendfile(int src_fd, int dest_fd, size_t maxlen, size_t *copied)
{
#ifdef HAVE_SENDFILE
size_t total_copied = 0;
size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen;
while (remaining > 0) {
size_t to_send = (remaining < SSIZE_MAX) ? remaining : SSIZE_MAX;
ssize_t result = sendfile(dest_fd, src_fd, NULL, to_send);
if (result > 0) {
total_copied += result;
if (maxlen != PHP_IO_COPY_ALL) {
remaining -= result;
}
} else if (result == 0) {
break;
} else {
switch (errno) {
case EINTR:
continue;
case EINVAL:
case ENOSYS:
if (total_copied == 0) {
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied);
}
break;
case EAGAIN:
break;
default:
*copied = total_copied;
return FAILURE;
}
break;
}
}
if (total_copied > 0) {
*copied = total_copied;
return SUCCESS;
}
#endif
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied);
}
#ifdef HAVE_SPLICE
/* Enlarge the intermediate pipe so socket transfers move more data per splice
* round-trip. Capped by /proc/sys/fs/pipe-max-size (1 MiB by default); a failed
* fcntl() simply leaves the kernel default (64 KiB) in place. */
#define PHP_IO_PIPE_SIZE (1 << 20)
/* Kernel MAX_RW_COUNT; a larger len fails the pos + len overflow check with
* EINVAL once a file destination sits at a non-zero offset. */
#define PHP_IO_SPLICE_MAX ((size_t) 0x7ffff000)
/* SPLICE_F_MORE corks the socket the same way MSG_MORE does, letting the kernel
* coalesce splices into full segments. The final partial segment is not flushed
* by the kernel until the cork timer (<= 200 ms) expires or the socket is next
* written/closed, so the copy loops below clear the cork once they are done. */
static inline unsigned int php_io_linux_out_flags(const php_io_fd *dest)
{
return (dest->fd_type == PHP_IO_FD_SOCKET) ? SPLICE_F_MORE : 0;
}
/* Clearing TCP_CORK pushes out any segment still held by SPLICE_F_MORE;
* on non-TCP sockets the setsockopt() harmlessly fails. */
static inline void php_io_linux_socket_uncork(const php_io_fd *dest, size_t total_copied)
{
if (dest->fd_type == PHP_IO_FD_SOCKET && total_copied > 0) {
int off = 0;
setsockopt(dest->fd, IPPROTO_TCP, TCP_CORK, &off, sizeof(off));
}
}
static zend_result php_io_linux_splice_from_pipe(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied)
{
int dest_fd = dest->fd;
unsigned int out_flags = php_io_linux_out_flags(dest);
size_t total_copied = 0;
size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen;
zend_result result = SUCCESS;
while (remaining > 0) {
int ready = php_io_linux_wait_for_data(src);
if (ready == 0) {
break;
} else if (ready < 0) {
result = FAILURE;
break;
}
size_t to_copy = (remaining < PHP_IO_SPLICE_MAX) ? remaining : PHP_IO_SPLICE_MAX;
ssize_t spliced = splice(src->fd, NULL, dest_fd, NULL, to_copy, out_flags);
if (spliced > 0) {
total_copied += spliced;
if (maxlen != PHP_IO_COPY_ALL) {
remaining -= spliced;
}
} else if (spliced == 0) {
break;
} else {
if (total_copied == 0) {
return php_io_generic_copy_fallback(src->fd, dest_fd, maxlen, copied);
}
result = FAILURE;
break;
}
}
php_io_linux_socket_uncork(dest, total_copied);
*copied = total_copied;
return result;
}
static zend_result php_io_linux_splice_via_pipe(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied)
{
int dest_fd = dest->fd;
unsigned int out_flags = php_io_linux_out_flags(dest);
int pipefd[2];
if (pipe(pipefd) == -1) {
return php_io_generic_copy_fallback(src->fd, dest_fd, maxlen, copied);
}
#ifdef F_SETPIPE_SZ
fcntl(pipefd[1], F_SETPIPE_SZ, PHP_IO_PIPE_SIZE);
#endif
size_t total_copied = 0;
size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen;
zend_result result = SUCCESS;
while (remaining > 0) {
int ready = php_io_linux_wait_for_data(src);
if (ready == 0) {
/* timeout */
break;
} else if (ready < 0) {
result = FAILURE;
break;
}
size_t to_copy = (remaining < SSIZE_MAX) ? remaining : SSIZE_MAX;
ssize_t in_pipe = splice(src->fd, NULL, pipefd[1], NULL, to_copy, 0);
if (in_pipe < 0) {
if (total_copied == 0) {
close(pipefd[0]);
close(pipefd[1]);
return php_io_generic_copy_fallback(src->fd, dest_fd, maxlen, copied);
}
result = FAILURE;
break;
}
if (in_pipe == 0) {
break;
}
size_t pipe_remaining = in_pipe;
while (pipe_remaining > 0) {
ssize_t out = splice(pipefd[0], NULL, dest_fd, NULL, pipe_remaining, out_flags);
if (out <= 0) {
/* the dest refused the splice; salvage what already sits in
* the pipe with a plain read/write loop before failing */
char drain_buf[1024];
while (pipe_remaining > 0) {
size_t to_drain = (pipe_remaining < sizeof(drain_buf))
? pipe_remaining : sizeof(drain_buf);
ssize_t drained;
do {
drained = read(pipefd[0], drain_buf, to_drain);
} while (drained < 0 && errno == EINTR);
if (drained <= 0) {
break;
}
ssize_t drain_written = 0;
while (drain_written < drained) {
ssize_t written;
do {
written = write(dest_fd, drain_buf + drain_written, drained - drain_written);
} while (written < 0 && errno == EINTR);
if (written <= 0) {
total_copied += drain_written;
result = FAILURE;
goto out;
}
drain_written += written;
}
pipe_remaining -= drain_written;
total_copied += drain_written;
}
result = FAILURE;
goto out;
}
pipe_remaining -= out;
total_copied += out;
}
if (maxlen != PHP_IO_COPY_ALL) {
remaining -= in_pipe;
}
}
out:
close(pipefd[0]);
close(pipefd[1]);
php_io_linux_socket_uncork(dest, total_copied);
*copied = total_copied;
return result;
}
#endif /* HAVE_SPLICE */
zend_result php_io_linux_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied)
{
if (src->fd_type == PHP_IO_FD_FILE && dest->fd_type == PHP_IO_FD_FILE) {
return php_io_linux_copy_file_to_file(src->fd, dest->fd, maxlen, copied);
}
if (src->fd_type == PHP_IO_FD_FILE && dest->fd_type == PHP_IO_FD_SOCKET) {
return php_io_linux_sendfile(src->fd, dest->fd, maxlen, copied);
}
if (src->fd_type == PHP_IO_FD_FILE && dest->fd_type == PHP_IO_FD_PIPE) {
return php_io_linux_sendfile(src->fd, dest->fd, maxlen, copied);
}
#ifdef HAVE_SPLICE
if (src->fd_type == PHP_IO_FD_PIPE) {
return php_io_linux_splice_from_pipe(src, dest, maxlen, copied);
}
if (src->fd_type == PHP_IO_FD_SOCKET) {
return php_io_linux_splice_via_pipe(src, dest, maxlen, copied);
}
#endif
/* php_io_generic_copy honours the stream timeout for socket sources */
return php_io_generic_copy(src, dest, maxlen, copied);
}
#endif /* __linux__ */

100
main/io/php_io_copy_macos.c

@ -0,0 +1,100 @@
/*
+----------------------------------------------------------------------+
| Copyright © The PHP Group and Contributors. |
+----------------------------------------------------------------------+
| This source file is subject to the Modified BSD License that is |
| bundled with this package in the file LICENSE, and is available |
| through the World Wide Web at <https://www.php.net/license/>. |
| |
| SPDX-License-Identifier: BSD-3-Clause |
+----------------------------------------------------------------------+
| Authors: David Carlier <devnexen@gmail.com> |
+----------------------------------------------------------------------+
*/
#if defined(__APPLE__)
#include "php_io_internal.h"
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <unistd.h>
#include <errno.h>
static zend_result php_io_macos_sendfile(int src_fd, int dest_fd, size_t maxlen, size_t *copied)
{
#ifdef HAVE_SENDFILE
/* macOS sendfile() takes an explicit offset and does not advance the
* source descriptor, so remember the starting position and restore it
* afterwards to keep the streams-layer contract. */
off_t start_offset = lseek(src_fd, 0, SEEK_CUR);
if (start_offset == (off_t) -1) {
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied);
}
off_t total_sent = 0;
size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? 0 : maxlen;
zend_result result = SUCCESS;
while (maxlen == PHP_IO_COPY_ALL || remaining > 0) {
/* len is in/out: on input the number of bytes to send (0 means
* until end of file), on output the number of bytes actually sent
* (populated even when sendfile() reports EINTR/EAGAIN). */
off_t len = (maxlen == PHP_IO_COPY_ALL) ? 0 : (off_t) remaining;
int sendfile_result = sendfile(src_fd, dest_fd, start_offset + total_sent, &len, NULL, 0);
if (len > 0) {
total_sent += len;
if (maxlen != PHP_IO_COPY_ALL) {
remaining -= (size_t) len;
}
}
if (sendfile_result == 0) {
/* Reached EOF or sent the whole requested range. */
if (len == 0 || maxlen == PHP_IO_COPY_ALL || remaining == 0) {
break;
}
} else {
if (errno == EINTR) {
continue;
}
if (errno == EAGAIN) {
if (len > 0) {
continue;
}
break;
}
if (total_sent == 0) {
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied);
}
result = FAILURE;
break;
}
}
if (total_sent > 0) {
/* best effort: keep reporting the delivered bytes even if this fails */
lseek(src_fd, start_offset + total_sent, SEEK_SET);
}
*copied = (size_t) total_sent;
return result;
#else
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied);
#endif
}
zend_result php_io_macos_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied)
{
/* Like FreeBSD, macOS sendfile() works only with a regular file source
* and a socket destination. */
if (src->fd_type == PHP_IO_FD_FILE && dest->fd_type == PHP_IO_FD_SOCKET) {
return php_io_macos_sendfile(src->fd, dest->fd, maxlen, copied);
}
/* php_io_generic_copy honours the stream timeout for socket sources */
return php_io_generic_copy(src, dest, maxlen, copied);
}
#endif

104
main/io/php_io_copy_solaris.c

@ -0,0 +1,104 @@
/*
+----------------------------------------------------------------------+
| Copyright © The PHP Group and Contributors. |
+----------------------------------------------------------------------+
| This source file is subject to the Modified BSD License that is |
| bundled with this package in the file LICENSE, and is available |
| through the World Wide Web at <https://www.php.net/license/>. |
| |
| SPDX-License-Identifier: BSD-3-Clause |
+----------------------------------------------------------------------+
| Authors: David Carlier <devnexen@gmail.com> |
+----------------------------------------------------------------------+
*/
#if defined(__sun) && defined(__SVR4)
#include "php_io_internal.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/sendfile.h>
#include <unistd.h>
#include <errno.h>
static zend_result php_io_solaris_sendfile(int src_fd, int dest_fd, size_t maxlen, size_t *copied)
{
#ifdef HAVE_SENDFILE
/* Solaris/illumos sendfile() takes an explicit offset and does not
* advance the source descriptor, so remember the starting position and
* restore it afterwards to keep the streams-layer contract. */
off_t start_offset = lseek(src_fd, 0, SEEK_CUR);
if (start_offset == (off_t) -1) {
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied);
}
/* Unlike Linux, Solaris sendfile() returns -1/EINVAL (rather than 0) when
* called with an offset at or past EOF, so a trailing call issued after the
* source has been fully consumed would spuriously fail an otherwise complete
* copy. Bound the loop to the bytes actually available in the source file so
* we stop exactly at EOF and never call sendfile() past it. */
struct stat st;
if (fstat(src_fd, &st) != 0 || !S_ISREG(st.st_mode)) {
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied);
}
size_t available = (st.st_size > start_offset) ? (size_t) (st.st_size - start_offset) : 0;
size_t target = (maxlen == PHP_IO_COPY_ALL || maxlen > available) ? available : maxlen;
off_t offset = start_offset;
size_t total_copied = 0;
zend_result result = SUCCESS;
while (total_copied < target) {
size_t remaining = target - total_copied;
size_t to_send = (remaining < SSIZE_MAX) ? remaining : SSIZE_MAX;
/* offset is updated in place by sendfile() */
ssize_t sent = sendfile(dest_fd, src_fd, &offset, to_send);
if (sent > 0) {
total_copied += (size_t) sent;
} else if (sent == 0) {
/* Source shrank under us; stop with what we have. */
break;
} else {
if (errno == EINTR) {
continue;
}
if (errno == EAGAIN) {
break;
}
if (total_copied == 0) {
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied);
}
result = FAILURE;
break;
}
}
if (total_copied > 0) {
/* best effort: keep reporting the delivered bytes even if this fails */
lseek(src_fd, start_offset + (off_t) total_copied, SEEK_SET);
}
*copied = total_copied;
return result;
#else
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied);
#endif
}
zend_result php_io_solaris_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied)
{
/* Unlike FreeBSD, Solaris/illumos sendfile() accepts both a socket and a
* regular file as the output descriptor, so file->socket and file->file
* can both be offloaded. */
if (src->fd_type == PHP_IO_FD_FILE &&
(dest->fd_type == PHP_IO_FD_SOCKET || dest->fd_type == PHP_IO_FD_FILE)) {
return php_io_solaris_sendfile(src->fd, dest->fd, maxlen, copied);
}
/* php_io_generic_copy honours the stream timeout for socket sources */
return php_io_generic_copy(src, dest, maxlen, copied);
}
#endif

189
main/io/php_io_copy_windows.c

@ -0,0 +1,189 @@
/*
+----------------------------------------------------------------------+
| Copyright © The PHP Group and Contributors. |
+----------------------------------------------------------------------+
| This source file is subject to the Modified BSD License that is |
| bundled with this package in the file LICENSE, and is available |
| through the World Wide Web at <https://www.php.net/license/>. |
| |
| SPDX-License-Identifier: BSD-3-Clause |
+----------------------------------------------------------------------+
| Authors: Jakub Zelenka <bukka@php.net> |
+----------------------------------------------------------------------+
*/
#include "php_io_internal.h"
#ifdef PHP_WIN32
#include <io.h>
#include <winsock2.h>
#include <mswsock.h>
static inline ssize_t php_io_win_read(php_io_fd *fd, char *buf, int len)
{
if (fd->fd_type == PHP_IO_FD_SOCKET) {
int result = recv(fd->socket, buf, len, 0);
return (result == SOCKET_ERROR) ? -1 : (ssize_t) result;
}
return (ssize_t) _read(fd->fd, buf, len);
}
static inline ssize_t php_io_win_write(php_io_fd *fd, const char *buf, int len)
{
if (fd->fd_type == PHP_IO_FD_SOCKET) {
int result = send(fd->socket, buf, len, 0);
return (result == SOCKET_ERROR) ? -1 : (ssize_t) result;
}
return (ssize_t) _write(fd->fd, buf, len);
}
static inline int php_io_win_wait_for_data(php_io_fd *fd)
{
if (fd->fd_type != PHP_IO_FD_SOCKET || !fd->is_blocked) {
return 1;
}
int timeout_ms;
if (fd->timeout.tv_sec == -1) {
timeout_ms = -1;
} else {
timeout_ms = fd->timeout.tv_sec * 1000 + fd->timeout.tv_usec / 1000;
}
WSAPOLLFD pfd;
pfd.fd = fd->socket;
pfd.events = POLLIN;
int ret;
do {
ret = WSAPoll(&pfd, 1, timeout_ms);
} while (ret == SOCKET_ERROR && WSAGetLastError() == WSAEINTR);
return (ret == SOCKET_ERROR) ? -1 : ret;
}
static zend_result php_io_win_copy_loop(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied)
{
char buf[PHP_IO_COPY_BUFSIZE];
size_t total_copied = 0;
size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen;
zend_result result = SUCCESS;
while (remaining > 0) {
int ready = php_io_win_wait_for_data(src);
if (ready == 0) {
break;
} else if (ready < 0) {
result = FAILURE;
break;
}
int to_read = (remaining < sizeof(buf)) ? (int) remaining : (int) sizeof(buf);
ssize_t bytes_read = php_io_win_read(src, buf, to_read);
if (bytes_read < 0) {
result = FAILURE;
break;
} else if (bytes_read == 0) {
break;
}
const char *writeptr = buf;
size_t to_write = (size_t) bytes_read;
while (to_write > 0) {
ssize_t bytes_written = php_io_win_write(dest, writeptr, (int) to_write);
if (bytes_written <= 0) {
result = FAILURE;
break;
}
total_copied += bytes_written;
writeptr += bytes_written;
to_write -= bytes_written;
}
if (result == FAILURE) {
break;
}
if (maxlen != PHP_IO_COPY_ALL) {
remaining -= bytes_read;
}
}
*copied = total_copied;
return result;
}
/* Documented TransmitFile per-call max; larger requests get WSAEINVAL, so they
* fall back to the read/write loop. */
#define PHP_IO_WIN_TRANSMIT_MAX ((size_t) 2147483646)
/* Negative results from php_io_win_transmit_file(). */
#define PHP_IO_WIN_TRANSMIT_FALLBACK ((ssize_t) -1) /* nothing sent; retry via loop */
#define PHP_IO_WIN_TRANSMIT_ERROR ((ssize_t) -2) /* may have sent data; do not retry */
static ssize_t php_io_win_transmit_file(int src_fd, SOCKET dest_sock, size_t maxlen)
{
HANDLE file_handle = (HANDLE) _get_osfhandle(src_fd);
if (file_handle == INVALID_HANDLE_VALUE ||
dest_sock == INVALID_SOCKET ||
GetFileType(file_handle) != FILE_TYPE_DISK) {
return PHP_IO_WIN_TRANSMIT_FALLBACK;
}
LARGE_INTEGER file_pos, file_size;
file_pos.QuadPart = 0;
if (!SetFilePointerEx(file_handle, file_pos, &file_pos, FILE_CURRENT)) {
return PHP_IO_WIN_TRANSMIT_FALLBACK;
}
if (!GetFileSizeEx(file_handle, &file_size)) {
return PHP_IO_WIN_TRANSMIT_FALLBACK;
}
LONGLONG available = file_size.QuadPart - file_pos.QuadPart;
if (available <= 0) {
return 0;
}
/* 64-bit compare avoids truncating maxlen into the DWORD arg below */
ULONGLONG to_send = (maxlen == PHP_IO_COPY_ALL || (ULONGLONG) maxlen > (ULONGLONG) available)
? (ULONGLONG) available : (ULONGLONG) maxlen;
if (to_send > PHP_IO_WIN_TRANSMIT_MAX) {
return PHP_IO_WIN_TRANSMIT_FALLBACK;
}
if (!TransmitFile(dest_sock, file_handle, (DWORD) to_send, 0, NULL, NULL, 0)) {
/* may have sent data, so don't resend via the loop */
return PHP_IO_WIN_TRANSMIT_ERROR;
}
LARGE_INTEGER advance;
advance.QuadPart = (LONGLONG) to_send;
SetFilePointerEx(file_handle, advance, NULL, FILE_CURRENT);
return (ssize_t) to_send;
}
zend_result php_io_windows_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied)
{
if (src->fd_type != PHP_IO_FD_SOCKET && dest->fd_type == PHP_IO_FD_SOCKET) {
ssize_t result = php_io_win_transmit_file(src->fd, dest->socket, maxlen);
if (result >= 0) {
*copied = (size_t) result;
return SUCCESS;
}
if (result == PHP_IO_WIN_TRANSMIT_ERROR) {
*copied = 0;
return FAILURE;
}
/* PHP_IO_WIN_TRANSMIT_FALLBACK: nothing was sent, copy via the loop. */
}
return php_io_win_copy_loop(src, dest, maxlen, copied);
}
#endif /* PHP_WIN32 */

27
main/io/php_io_freebsd.h

@ -0,0 +1,27 @@
/*
+----------------------------------------------------------------------+
| Copyright (c) The PHP Group |
+----------------------------------------------------------------------+
| This source file is subject to the Modified BSD License that is |
| bundled with this package in the file LICENSE, and is available |
| through the World Wide Web at <https://www.php.net/license/>. |
| |
| SPDX-License-Identifier: BSD-3-Clause |
+----------------------------------------------------------------------+
| Authors: David Carlier <devnexen@gmail.com> |
+----------------------------------------------------------------------+
*/
#ifndef PHP_IO_FREEBSD_H
#define PHP_IO_FREEBSD_H
#define PHP_IO_PLATFORM_COPY php_io_freebsd_copy
#if defined(__DragonFly__)
# define PHP_IO_PLATFORM_NAME "dragonfly"
#else
# define PHP_IO_PLATFORM_NAME "freebsd"
#endif
zend_result php_io_freebsd_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied);
#endif /* PHP_IO_FREEBSD_H */

21
main/io/php_io_generic.h

@ -0,0 +1,21 @@
/*
+----------------------------------------------------------------------+
| Copyright (c) The PHP Group |
+----------------------------------------------------------------------+
| This source file is subject to the Modified BSD License that is |
| bundled with this package in the file LICENSE, and is available |
| through the World Wide Web at <https://www.php.net/license/>. |
| |
| SPDX-License-Identifier: BSD-3-Clause |
+----------------------------------------------------------------------+
| Authors: Jakub Zelenka <bukka@php.net> |
+----------------------------------------------------------------------+
*/
#ifndef PHP_IO_GENERIC_H
#define PHP_IO_GENERIC_H
#define PHP_IO_PLATFORM_COPY php_io_generic_copy
#define PHP_IO_PLATFORM_NAME "generic"
#endif /* PHP_IO_GENERIC_H */

41
main/io/php_io_internal.h

@ -0,0 +1,41 @@
/*
+----------------------------------------------------------------------+
| Copyright (c) The PHP Group |
+----------------------------------------------------------------------+
| This source file is subject to the Modified BSD License that is |
| bundled with this package in the file LICENSE, and is available |
| through the World Wide Web at <https://www.php.net/license/>. |
| |
| SPDX-License-Identifier: BSD-3-Clause |
+----------------------------------------------------------------------+
| Authors: Jakub Zelenka <bukka@php.net> |
+----------------------------------------------------------------------+
*/
#ifndef PHP_IO_INTERNAL_H
#define PHP_IO_INTERNAL_H
#include "php_io.h"
/* Buffer size for the userspace read/write copy loops. Larger than the stream
* layer CHUNK_SIZE (8 KiB) to cut the number of syscalls on bulk transfers. */
#define PHP_IO_COPY_BUFSIZE (64 * 1024)
zend_result php_io_generic_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied);
zend_result php_io_generic_copy_fallback(int src_fd, int dest_fd, size_t maxlen, size_t *copied);
#ifdef __linux__
#include "php_io_linux.h"
#elif defined(PHP_WIN32)
#include "php_io_windows.h"
#elif defined(__FreeBSD__) || defined(__DragonFly__)
#include "php_io_freebsd.h"
#elif defined(__sun) && defined(__SVR4)
#include "php_io_solaris.h"
#elif defined(__APPLE__)
#include "php_io_macos.h"
#else
#include "php_io_generic.h"
#endif
#endif /* PHP_IO_INTERNAL_H */

23
main/io/php_io_linux.h

@ -0,0 +1,23 @@
/*
+----------------------------------------------------------------------+
| Copyright © The PHP Group and Contributors. |
+----------------------------------------------------------------------+
| This source file is subject to the Modified BSD License that is |
| bundled with this package in the file LICENSE, and is available |
| through the World Wide Web at <https://www.php.net/license/>. |
| |
| SPDX-License-Identifier: BSD-3-Clause |
+----------------------------------------------------------------------+
| Authors: Jakub Zelenka <bukka@php.net> |
+----------------------------------------------------------------------+
*/
#ifndef PHP_IO_LINUX_H
#define PHP_IO_LINUX_H
zend_result php_io_linux_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied);
#define PHP_IO_PLATFORM_COPY php_io_linux_copy
#define PHP_IO_PLATFORM_NAME "linux"
#endif /* PHP_IO_LINUX_H */

23
main/io/php_io_macos.h

@ -0,0 +1,23 @@
/*
+----------------------------------------------------------------------+
| Copyright (c) The PHP Group |
+----------------------------------------------------------------------+
| This source file is subject to the Modified BSD License that is |
| bundled with this package in the file LICENSE, and is available |
| through the World Wide Web at <https://www.php.net/license/>. |
| |
| SPDX-License-Identifier: BSD-3-Clause |
+----------------------------------------------------------------------+
| Authors: David Carlier <devnexen@gmail.com> |
+----------------------------------------------------------------------+
*/
#ifndef PHP_IO_MACOS_H
#define PHP_IO_MACOS_H
#define PHP_IO_PLATFORM_COPY php_io_macos_copy
#define PHP_IO_PLATFORM_NAME "macos"
zend_result php_io_macos_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied);
#endif /* PHP_IO_MACOS_H */

23
main/io/php_io_solaris.h

@ -0,0 +1,23 @@
/*
+----------------------------------------------------------------------+
| Copyright © The PHP Group and Contributors. |
+----------------------------------------------------------------------+
| This source file is subject to the Modified BSD License that is |
| bundled with this package in the file LICENSE, and is available |
| through the World Wide Web at <https://www.php.net/license/>. |
| |
| SPDX-License-Identifier: BSD-3-Clause |
+----------------------------------------------------------------------+
| Authors: David Carlier <devnexen@gmail.com> |
+----------------------------------------------------------------------+
*/
#ifndef PHP_IO_SOLARIS_H
#define PHP_IO_SOLARIS_H
#define PHP_IO_PLATFORM_COPY php_io_solaris_copy
#define PHP_IO_PLATFORM_NAME "solaris"
zend_result php_io_solaris_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied);
#endif /* PHP_IO_SOLARIS_H */

23
main/io/php_io_windows.h

@ -0,0 +1,23 @@
/*
+----------------------------------------------------------------------+
| Copyright © The PHP Group and Contributors. |
+----------------------------------------------------------------------+
| This source file is subject to the Modified BSD License that is |
| bundled with this package in the file LICENSE, and is available |
| through the World Wide Web at <https://www.php.net/license/>. |
| |
| SPDX-License-Identifier: BSD-3-Clause |
+----------------------------------------------------------------------+
| Authors: Jakub Zelenka <bukka@php.net> |
+----------------------------------------------------------------------+
*/
#ifndef PHP_IO_WINDOWS_H
#define PHP_IO_WINDOWS_H
zend_result php_io_windows_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied);
#define PHP_IO_PLATFORM_COPY php_io_windows_copy
#define PHP_IO_PLATFORM_NAME "windows"
#endif /* PHP_IO_WINDOWS_H */

53
main/php_io.h

@ -0,0 +1,53 @@
/*
+----------------------------------------------------------------------+
| Copyright (c) The PHP Group |
+----------------------------------------------------------------------+
| This source file is subject to version 3.01 of the PHP license, |
| that is bundled with this package in the file LICENSE, and is |
| available through the world-wide-web at the following url: |
| https://www.php.net/license/3_01.txt |
| If you did not receive a copy of the PHP license and are unable to |
| obtain it through the world-wide-web, please send a note to |
| license@php.net so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
| Authors: Jakub Zelenka <bukka@php.net> |
+----------------------------------------------------------------------+
*/
#ifndef PHP_IO_H
#define PHP_IO_H
#include "php.h"
#include "php_network.h"
#define PHP_IO_COPY_ALL SIZE_MAX
typedef enum php_io_fd_type {
PHP_IO_FD_FILE = 1,
PHP_IO_FD_SOCKET,
PHP_IO_FD_PIPE,
} php_io_fd_type;
typedef struct php_io_fd {
union {
int fd;
php_socket_t socket;
};
php_io_fd_type fd_type;
struct timeval timeout;
unsigned is_blocked:1;
} php_io_fd;
typedef zend_result (*php_io_copy_fn)(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied);
typedef struct php_io {
php_io_copy_fn copy;
const char *platform_name;
} php_io;
PHPAPI php_io *php_io_get(void);
/* Copies up to maxlen bytes from src to dest; *copied is set even on FAILURE */
PHPAPI zend_result php_io_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied);
#endif /* PHP_IO_H */

2
main/php_streams.h

@ -555,6 +555,8 @@ END_EXTERN_C()
#define PHP_STREAM_AS_SOCKETD 2
/* cast as fd/socket for select purposes */
#define PHP_STREAM_AS_FD_FOR_SELECT 3
/* cast as fd/socket for copy purposes */
#define PHP_STREAM_AS_FD_FOR_COPY 4
/* try really, really hard to make sure the cast happens (avoid using this flag if possible) */
#define PHP_STREAM_CAST_TRY_HARD 0x80000000

12
main/streams/cast.c

@ -194,7 +194,7 @@ PHPAPI zend_result _php_stream_cast(php_stream *stream, int castas, void **ret,
castas &= ~PHP_STREAM_CAST_MASK;
/* synchronize our buffer (if possible) */
if (ret && castas != PHP_STREAM_AS_FD_FOR_SELECT) {
if (ret && castas != PHP_STREAM_AS_FD_FOR_SELECT && castas != PHP_STREAM_AS_FD_FOR_COPY) {
php_stream_flush(stream);
if (stream->ops->seek && (stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0) {
zend_off_t dummy;
@ -204,6 +204,16 @@ PHPAPI zend_result _php_stream_cast(php_stream *stream, int castas, void **ret,
}
}
if (castas == PHP_STREAM_AS_FD_FOR_COPY) {
if (php_stream_is_filtered(stream)) {
return FAILURE;
}
if (stream->ops->cast && stream->ops->cast(stream, castas, ret) == SUCCESS) {
return SUCCESS;
}
return FAILURE;
}
/* filtered streams can only be cast as stdio, and only when fopencookie is present */
if (castas == PHP_STREAM_AS_STDIO) {

5
main/streams/memory.c

@ -471,6 +471,11 @@ static int php_stream_temp_cast(php_stream *stream, int castas, void **ret)
if (!ts->innerstream) {
return FAILURE;
}
if (castas == PHP_STREAM_AS_FD_FOR_COPY) {
/* the fd would come from the inner stream whose buffer state and
* position the copy fast path cannot see, so use the stream fallback */
return FAILURE;
}
if (php_stream_is(ts->innerstream, PHP_STREAM_IS_STDIO)) {
return php_stream_cast(ts->innerstream, castas, ret, 0);
}

22
main/streams/plain_wrapper.c

@ -32,6 +32,7 @@
#endif
#include "SAPI.h"
#include "php_io.h"
#include "php_streams_int.h"
#ifdef PHP_WIN32
# include "win32/winutil.h"
@ -699,7 +700,6 @@ static int php_stdiop_cast(php_stream *stream, int castas, void **ret)
case PHP_STREAM_AS_FD:
PHP_STDIOP_GET_FD(fd, data);
if (SOCK_ERR == fd) {
return FAILURE;
}
@ -710,6 +710,26 @@ static int php_stdiop_cast(php_stream *stream, int castas, void **ret)
*(php_socket_t *)ret = fd;
}
return SUCCESS;
case PHP_STREAM_AS_FD_FOR_COPY:
/* stdio may read ahead, so use the buffered fallback for FILE* streams */
if (data->file) {
return FAILURE;
}
PHP_STDIOP_GET_FD(fd, data);
if (SOCK_ERR == fd) {
return FAILURE;
}
if (ret) {
php_io_fd *copy_fd = (php_io_fd *) ret;
copy_fd->fd = fd;
copy_fd->fd_type = data->is_pipe ? PHP_IO_FD_PIPE : PHP_IO_FD_FILE;
copy_fd->timeout.tv_sec = 0;
copy_fd->timeout.tv_usec = 0;
copy_fd->is_blocked = 0;
}
return SUCCESS;
default:
return FAILURE;
}

205
main/streams/streams.c

@ -22,6 +22,7 @@
#include "php_globals.h"
#include "php_memory_streams.h"
#include "php_network.h"
#include "php_io.h"
#include "php_open_temporary_file.h"
#include "ext/standard/file.h"
#include "ext/standard/basic_functions.h" /* for BG(CurrentStatFile) */
@ -1588,164 +1589,17 @@ PHPAPI zend_string *_php_stream_copy_to_mem(php_stream *src, size_t maxlen, bool
return result;
}
/* Returns SUCCESS/FAILURE and sets *len to the number of bytes moved */
PHPAPI zend_result _php_stream_copy_to_stream_ex(php_stream *src, php_stream *dest, size_t maxlen, size_t *len STREAMS_DC)
/* Fallback copy using stream read/write API */
static zend_result php_stream_copy_fallback(php_stream *src, php_stream *dest, size_t maxlen, size_t *len)
{
char buf[CHUNK_SIZE];
size_t haveread = 0;
size_t towrite;
size_t dummy;
if (!len) {
len = &dummy;
}
if (maxlen == 0) {
*len = 0;
return SUCCESS;
}
#ifdef HAVE_COPY_FILE_RANGE
if (php_stream_is(src, PHP_STREAM_IS_STDIO) &&
php_stream_is(dest, PHP_STREAM_IS_STDIO) &&
src->writepos == src->readpos) {
/* both php_stream instances are backed by a file descriptor, are not filtered and the
* read buffer is empty: we can use copy_file_range() */
int src_fd, dest_fd, dest_open_flags = 0;
/* copy_file_range does not work with O_APPEND */
if (php_stream_cast(src, PHP_STREAM_AS_FD, (void*)&src_fd, 0) == SUCCESS &&
php_stream_cast(dest, PHP_STREAM_AS_FD, (void*)&dest_fd, 0) == SUCCESS &&
/* get dest open flags to check if the stream is open in append mode */
php_stream_parse_fopen_modes(dest->mode, &dest_open_flags) == SUCCESS &&
!(dest_open_flags & O_APPEND)) {
/* clamp to INT_MAX to avoid EOVERFLOW */
const size_t cfr_max = MIN(maxlen, (size_t)SSIZE_MAX);
/* copy_file_range() is a Linux-specific system call which allows efficient copying
* between two file descriptors, eliminating the need to transfer data from the kernel
* to userspace and back. For networking file systems like NFS and Ceph, it even
* eliminates copying data to the client, and local filesystems like Btrfs and XFS can
* create shared extents. */
ssize_t result = copy_file_range(src_fd, NULL, dest_fd, NULL, cfr_max, 0);
if (result > 0) {
size_t nbytes = (size_t)result;
haveread += nbytes;
src->position += nbytes;
dest->position += nbytes;
if ((maxlen != PHP_STREAM_COPY_ALL && nbytes == maxlen) || php_stream_eof(src)) {
/* the whole request was satisfied or end-of-file reached - done */
*len = haveread;
return SUCCESS;
}
/* there may be more data; continue copying using the fallback code below */
} else if (result == 0) {
/* end of file */
*len = haveread;
return SUCCESS;
} else if (result < 0) {
switch (errno) {
case EINVAL:
/* some formal error, e.g. overlapping file ranges */
break;
case EXDEV:
/* pre Linux 5.3 error */
break;
case ENOSYS:
/* not implemented by this Linux kernel */
break;
case EIO:
/* Some filesystems will cause failures if the max length is greater than the file length
* in certain circumstances and configuration. In those cases the errno is EIO and we will
* fall back to other methods. We cannot use stat to determine the file length upfront because
* that is prone to races and outdated caching. */
break;
default:
/* unexpected I/O error - give up, no fallback */
*len = haveread;
return FAILURE;
}
/* fall back to classic copying */
}
}
}
#endif // HAVE_COPY_FILE_RANGE
if (maxlen == PHP_STREAM_COPY_ALL) {
maxlen = 0;
}
if (php_stream_mmap_possible(src)) {
char *p;
do {
/* We must not modify maxlen here, because otherwise the file copy fallback below can fail */
size_t chunk_size, must_read, mapped;
if (maxlen == 0) {
/* Unlimited read */
must_read = chunk_size = PHP_STREAM_MMAP_MAX;
} else {
must_read = maxlen - haveread;
if (must_read >= PHP_STREAM_MMAP_MAX) {
chunk_size = PHP_STREAM_MMAP_MAX;
} else {
/* In case the length we still have to read from the file could be smaller than the file size,
* chunk_size must not get bigger the size we're trying to read. */
chunk_size = must_read;
}
}
p = php_stream_mmap_range(src, php_stream_tell(src), chunk_size, PHP_STREAM_MAP_MODE_SHARED_READONLY, &mapped);
if (p) {
ssize_t didwrite;
if (php_stream_seek(src, mapped, SEEK_CUR) != 0) {
php_stream_mmap_unmap(src);
break;
}
didwrite = php_stream_write(dest, p, mapped);
if (didwrite < 0) {
*len = haveread;
php_stream_mmap_unmap(src);
return FAILURE;
}
php_stream_mmap_unmap(src);
*len = haveread += didwrite;
/* we've got at least 1 byte to read
* less than 1 is an error
* AND read bytes match written */
if (mapped == 0 || mapped != didwrite) {
return FAILURE;
}
if (mapped < chunk_size) {
return SUCCESS;
}
/* If we're not reading as much as possible, so a bounded read */
if (maxlen != 0) {
must_read -= mapped;
if (must_read == 0) {
return SUCCESS;
}
}
}
} while (p);
}
while(1) {
while (1) {
size_t readchunk = sizeof(buf);
ssize_t didread;
char *writeptr;
@ -1760,14 +1614,14 @@ PHPAPI zend_result _php_stream_copy_to_stream_ex(php_stream *src, php_stream *de
return didread < 0 ? FAILURE : SUCCESS;
}
towrite = didread;
size_t towrite = didread;
writeptr = buf;
haveread += didread;
while (towrite) {
ssize_t didwrite = php_stream_write(dest, writeptr, towrite);
if (didwrite <= 0) {
*len = haveread - (didread - towrite);
*len = haveread - towrite;
return FAILURE;
}
@ -1784,6 +1638,53 @@ PHPAPI zend_result _php_stream_copy_to_stream_ex(php_stream *src, php_stream *de
return SUCCESS;
}
PHPAPI zend_result _php_stream_copy_to_stream_ex(php_stream *src, php_stream *dest, size_t maxlen, size_t *len STREAMS_DC)
{
size_t dummy;
if (!len) {
len = &dummy;
}
if (maxlen == 0) {
*len = 0;
return SUCCESS;
}
/* Try optimized fd-level copy if both streams support it and their read buffers
* 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_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 &&
php_stream_cast(dest, PHP_STREAM_AS_FD_FOR_COPY, (void *) &dest_copy_fd, 0) == SUCCESS) {
/* copy_file_range does not work with O_APPEND */
if (src_copy_fd.fd_type == PHP_IO_FD_FILE && dest_copy_fd.fd_type == PHP_IO_FD_FILE) {
int dest_flags = 0;
if (php_stream_parse_fopen_modes(dest->mode, &dest_flags) == SUCCESS
&& (dest_flags & O_APPEND)) {
goto fallback;
}
}
size_t io_maxlen = (maxlen == PHP_STREAM_COPY_ALL) ? PHP_IO_COPY_ALL : maxlen;
size_t copied = 0;
zend_result result = php_io_copy(&src_copy_fd, &dest_copy_fd, io_maxlen, &copied);
src->position += copied;
dest->position += copied;
*len = copied;
return result;
}
}
fallback:
return php_stream_copy_fallback(src, dest, maxlen, len);
}
/* Returns the number of bytes moved.
* Returns 1 when source len is 0.
* Deprecated in favor of php_stream_copy_to_stream_ex() */

12
main/streams/xp_socket.c

@ -15,7 +15,7 @@
#include "php.h"
#include "ext/standard/file.h"
#include "php_streams.h"
#include "php_network.h"
#include "php_io.h"
#if defined(PHP_WIN32) || defined(__riscos__)
# undef AF_UNIX
@ -523,11 +523,19 @@ static int php_sockop_cast(php_stream *stream, int castas, void **ret)
if (ret)
*(php_socket_t *)ret = sock->socket;
return SUCCESS;
case PHP_STREAM_AS_FD_FOR_COPY:
if (ret) {
php_io_fd *copy_fd = (php_io_fd *) ret;
copy_fd->socket = sock->socket;
copy_fd->fd_type = PHP_IO_FD_SOCKET;
copy_fd->timeout = sock->timeout;
copy_fd->is_blocked = sock->is_blocked;
}
return SUCCESS;
default:
return FAILURE;
}
}
/* }}} */
/* These may look identical, but we need them this way so that
* we can determine which type of socket we are dealing with

4
tests/unit/Makefile

@ -3,9 +3,11 @@ CFLAGS = -g -Wall -I../../ -I../../Zend -I../../main -I../../TSRM -I. -I..
COMMON_LDFLAGS = ../../.libs/libphp.a -lcmocka -lpthread -lm -ldl -lresolv -lutil
# Update paths in .github/workflows/unit-tests.yml when adding new test to make it run in PR when such file changes
TESTS = main/test_network
TESTS = main/test_network main/test_io_copy
main/test_network_SRC = main/test_network.c
main/test_network_LDFLAGS = $(COMMON_LDFLAGS) -Wl,--wrap=connect,--wrap=poll,--wrap=getsockopt,--wrap=gettimeofday
main/test_io_copy_SRC = main/test_io_copy.c
main/test_io_copy_LDFLAGS = $(COMMON_LDFLAGS) -Wl,--wrap=copy_file_range,--wrap=sendfile,--wrap=splice,--wrap=read,--wrap=write,--wrap=poll,--wrap=setsockopt
# Build all tests

293
tests/unit/main/test_io_copy.c

@ -0,0 +1,293 @@
#include "php.h"
#include "io/php_io_internal.h"
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <cmocka.h>
/* Mocked syscalls return the value queued via will_return(); a negative value
* -E is translated to a -1 return with errno set to E. Must expand inside the
* wrapper body because cmocka keys the queued values by __func__. */
#define MOCK_IO_RESULT(result_var) \
ssize_t result_var = mock_type(ssize_t); \
if (result_var < 0) { \
errno = (int) -result_var; \
result_var = -1; \
}
ssize_t __wrap_copy_file_range(int fd_in, off_t *off_in, int fd_out, off_t *off_out, size_t len, unsigned int flags)
{
function_called();
MOCK_IO_RESULT(result);
return result;
}
ssize_t __wrap_sendfile(int out_fd, int in_fd, off_t *offset, size_t count)
{
function_called();
MOCK_IO_RESULT(result);
return result;
}
ssize_t __wrap_splice(int fd_in, off_t *off_in, int fd_out, off_t *off_out, size_t len, unsigned int flags)
{
function_called();
check_expected(len);
check_expected(flags);
MOCK_IO_RESULT(result);
return result;
}
ssize_t __wrap_read(int fd, void *buf, size_t count)
{
function_called();
MOCK_IO_RESULT(result);
if (result > 0) {
memset(buf, 'x', result);
}
return result;
}
ssize_t __wrap_write(int fd, const void *buf, size_t count)
{
function_called();
MOCK_IO_RESULT(result);
return result;
}
int __wrap_poll(struct pollfd *ufds, nfds_t nfds, int timeout)
{
function_called();
check_expected(timeout);
int n = mock_type(int);
if (n > 0) {
ufds->revents = POLLIN;
} else if (n < 0) {
errno = -n;
n = -1;
}
return n;
}
int __wrap_setsockopt(int fd, int level, int optname, const void *optval, socklen_t optlen)
{
function_called();
check_expected(optname);
return 0;
}
/* Kernel MAX_RW_COUNT, must match PHP_IO_SPLICE_MAX in php_io_copy_linux.c */
#define TEST_SPLICE_MAX ((size_t) 0x7ffff000)
static php_io_fd make_io_fd(int fd, php_io_fd_type fd_type)
{
php_io_fd io_fd = {
.fd = fd,
.fd_type = fd_type,
.timeout = { .tv_sec = -1, .tv_usec = 0 },
.is_blocked = 0,
};
return io_fd;
}
/* file -> file: a hard error after partial progress must report FAILURE
* together with the bytes already copied */
static void test_file_to_file_error_after_partial(void **state)
{
php_io_fd src = make_io_fd(10, PHP_IO_FD_FILE);
php_io_fd dest = make_io_fd(11, PHP_IO_FD_FILE);
size_t copied = 0;
expect_function_calls(__wrap_copy_file_range, 2);
will_return(__wrap_copy_file_range, 4096);
will_return(__wrap_copy_file_range, -ENOSPC);
assert_int_equal(php_io_linux_copy(&src, &dest, PHP_IO_COPY_ALL, &copied), FAILURE);
assert_int_equal(copied, 4096);
}
/* file -> file: EXDEV with no progress falls back to the read/write loop */
static void test_file_to_file_exdev_fallback(void **state)
{
php_io_fd src = make_io_fd(10, PHP_IO_FD_FILE);
php_io_fd dest = make_io_fd(11, PHP_IO_FD_FILE);
size_t copied = 0;
expect_function_call(__wrap_copy_file_range);
will_return(__wrap_copy_file_range, -EXDEV);
expect_function_call(__wrap_read);
will_return(__wrap_read, 100);
expect_function_call(__wrap_write);
will_return(__wrap_write, 100);
expect_function_call(__wrap_read);
will_return(__wrap_read, 0);
assert_int_equal(php_io_linux_copy(&src, &dest, PHP_IO_COPY_ALL, &copied), SUCCESS);
assert_int_equal(copied, 100);
}
/* the generic fallback loop must retry interrupted read() and write() */
static void test_generic_fallback_eintr_retry(void **state)
{
size_t copied = 0;
expect_function_calls(__wrap_read, 2);
will_return(__wrap_read, -EINTR);
will_return(__wrap_read, 50);
expect_function_calls(__wrap_write, 2);
will_return(__wrap_write, -EINTR);
will_return(__wrap_write, 50);
expect_function_call(__wrap_read);
will_return(__wrap_read, 0);
assert_int_equal(php_io_generic_copy_fallback(10, 11, PHP_IO_COPY_ALL, &copied), SUCCESS);
assert_int_equal(copied, 50);
}
/* the generic fallback loop must report FAILURE with the partial count when a
* write fails mid-copy */
static void test_generic_fallback_write_error_after_partial(void **state)
{
size_t copied = 0;
expect_function_call(__wrap_read);
will_return(__wrap_read, 50);
expect_function_call(__wrap_write);
will_return(__wrap_write, 50);
expect_function_call(__wrap_read);
will_return(__wrap_read, 50);
expect_function_call(__wrap_write);
will_return(__wrap_write, -ENOSPC);
assert_int_equal(php_io_generic_copy_fallback(10, 11, PHP_IO_COPY_ALL, &copied), FAILURE);
assert_int_equal(copied, 50);
}
/* file -> socket: a hard sendfile error after partial progress must report
* FAILURE together with the bytes already sent */
static void test_file_to_socket_sendfile_error_after_partial(void **state)
{
php_io_fd src = make_io_fd(10, PHP_IO_FD_FILE);
php_io_fd dest = make_io_fd(11, PHP_IO_FD_SOCKET);
size_t copied = 0;
expect_function_calls(__wrap_sendfile, 2);
will_return(__wrap_sendfile, 8192);
will_return(__wrap_sendfile, -EPIPE);
assert_int_equal(php_io_linux_copy(&src, &dest, PHP_IO_COPY_ALL, &copied), FAILURE);
assert_int_equal(copied, 8192);
}
/* file -> socket: sendfile EINVAL with no progress falls back to the
* read/write loop */
static void test_file_to_socket_sendfile_einval_fallback(void **state)
{
php_io_fd src = make_io_fd(10, PHP_IO_FD_FILE);
php_io_fd dest = make_io_fd(11, PHP_IO_FD_SOCKET);
size_t copied = 0;
expect_function_call(__wrap_sendfile);
will_return(__wrap_sendfile, -EINVAL);
expect_function_call(__wrap_read);
will_return(__wrap_read, 10);
expect_function_call(__wrap_write);
will_return(__wrap_write, 10);
expect_function_call(__wrap_read);
will_return(__wrap_read, 0);
assert_int_equal(php_io_linux_copy(&src, &dest, PHP_IO_COPY_ALL, &copied), SUCCESS);
assert_int_equal(copied, 10);
}
/* socket -> file: a blocking source socket must be polled with the stream
* timeout and a timeout is a clean stop, not an error */
static void test_socket_source_poll_timeout(void **state)
{
php_io_fd src = make_io_fd(10, PHP_IO_FD_SOCKET);
php_io_fd dest = make_io_fd(11, PHP_IO_FD_FILE);
size_t copied = 42;
src.is_blocked = 1;
src.timeout.tv_sec = 2;
src.timeout.tv_usec = 500000;
expect_function_call(__wrap_poll);
expect_value(__wrap_poll, timeout, 2500);
will_return(__wrap_poll, 0);
assert_int_equal(php_io_linux_copy(&src, &dest, PHP_IO_COPY_ALL, &copied), SUCCESS);
assert_int_equal(copied, 0);
}
/* pipe -> socket: splices are corked with SPLICE_F_MORE and capped at the
* kernel MAX_RW_COUNT; an error after partial progress must report FAILURE
* with the partial count and still clear the cork */
static void test_pipe_to_socket_splice_error_after_partial_uncorks(void **state)
{
php_io_fd src = make_io_fd(10, PHP_IO_FD_PIPE);
php_io_fd dest = make_io_fd(11, PHP_IO_FD_SOCKET);
size_t copied = 0;
expect_function_calls(__wrap_splice, 2);
expect_value_count(__wrap_splice, len, TEST_SPLICE_MAX, 2);
expect_value_count(__wrap_splice, flags, SPLICE_F_MORE, 2);
will_return(__wrap_splice, 1000);
will_return(__wrap_splice, -EPIPE);
expect_function_call(__wrap_setsockopt);
expect_value(__wrap_setsockopt, optname, TCP_CORK);
assert_int_equal(php_io_linux_copy(&src, &dest, PHP_IO_COPY_ALL, &copied), FAILURE);
assert_int_equal(copied, 1000);
}
/* socket -> file: when the outbound splice fails, the data already sitting in
* the intermediate pipe is salvaged with a read/write loop (retrying EINTR)
* and the copy reports FAILURE with the salvaged count */
static void test_socket_to_file_splice_out_error_drains_pipe(void **state)
{
php_io_fd src = make_io_fd(10, PHP_IO_FD_SOCKET);
php_io_fd dest = make_io_fd(11, PHP_IO_FD_FILE);
size_t copied = 0;
expect_function_calls(__wrap_splice, 2);
/* socket -> pipe */
expect_value(__wrap_splice, len, SSIZE_MAX);
expect_value(__wrap_splice, flags, 0);
will_return(__wrap_splice, 500);
/* pipe -> file */
expect_value(__wrap_splice, len, 500);
expect_value(__wrap_splice, flags, 0);
will_return(__wrap_splice, -EINVAL);
expect_function_calls(__wrap_read, 2);
will_return(__wrap_read, -EINTR);
will_return(__wrap_read, 500);
expect_function_call(__wrap_write);
will_return(__wrap_write, 500);
assert_int_equal(php_io_linux_copy(&src, &dest, PHP_IO_COPY_ALL, &copied), FAILURE);
assert_int_equal(copied, 500);
}
int main(void)
{
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_file_to_file_error_after_partial),
cmocka_unit_test(test_file_to_file_exdev_fallback),
cmocka_unit_test(test_generic_fallback_eintr_retry),
cmocka_unit_test(test_generic_fallback_write_error_after_partial),
cmocka_unit_test(test_file_to_socket_sendfile_error_after_partial),
cmocka_unit_test(test_file_to_socket_sendfile_einval_fallback),
cmocka_unit_test(test_socket_source_poll_timeout),
cmocka_unit_test(test_pipe_to_socket_splice_error_after_partial_uncorks),
cmocka_unit_test(test_socket_to_file_splice_out_error_drains_pipe),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}

5
win32/build/config.w32

@ -298,6 +298,9 @@ AC_DEFINE('HAVE_STRNLEN', 1);
AC_DEFINE('ZEND_CHECK_STACK_LIMIT', 1)
ADD_SOURCES("main/io", "php_io.c php_io_copy_windows.c");
ADD_FLAG("CFLAGS_BD_MAIN_IO", "/D ZEND_ENABLE_STATIC_TSRMLS_CACHE=1");
ADD_SOURCES("main/poll", "poll_backend_wsapoll.c poll_core.c poll_fd_table.c poll_handle.c");
ADD_FLAG("CFLAGS_BD_MAIN_POLL", "/D ZEND_ENABLE_STATIC_TSRMLS_CACHE=1");
@ -312,7 +315,7 @@ ADD_SOURCES("win32", "dllmain.c readdir.c \
ADD_FLAG("CFLAGS_BD_WIN32", "/D ZEND_ENABLE_STATIC_TSRMLS_CACHE=1");
PHP_INSTALL_HEADERS("", "Zend/ TSRM/ main/ main/streams/ win32/");
PHP_INSTALL_HEADERS("", "Zend/ TSRM/ main/ main/io main/streams/ win32/");
PHP_INSTALL_HEADERS("Zend/Optimizer", "zend_call_graph.h zend_cfg.h zend_dfg.h zend_dump.h zend_func_info.h zend_inference.h zend_optimizer.h zend_ssa.h zend_worklist.h");
STDOUT.WriteBlankLines(1);

2
win32/build/confutils.js

@ -3523,7 +3523,7 @@ function toolset_setup_common_ldflags()
function toolset_setup_common_libs()
{
// urlmon.lib ole32.lib oleaut32.lib uuid.lib gdi32.lib winspool.lib comdlg32.lib
DEFINE("LIBS", "kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib Dnsapi.lib psapi.lib bcrypt.lib Pathcch.lib");
DEFINE("LIBS", "kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib Dnsapi.lib psapi.lib bcrypt.lib Pathcch.lib Mswsock.lib");
}
function toolset_setup_build_mode()

Loading…
Cancel
Save