diff --git a/configure.ac b/configure.ac index 214c0ab91b2a5..8b925c94a1b59 100644 --- a/configure.ac +++ b/configure.ac @@ -587,10 +587,12 @@ AC_CHECK_FUNCS(m4_normalize([ putenv reallocarray scandir + sendfile setenv setitimer shutdown sigprocmask + splice statfs statvfs std_syslog @@ -1682,6 +1684,12 @@ PHP_ADD_SOURCES_X([main], [PHP_FASTCGI_OBJS], [no]) +PHP_ADD_SOURCES([main/io], m4_normalize([ + php_io.c + php_io_copy_linux.c + ]), + [-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1]) + PHP_ADD_SOURCES([main/streams], m4_normalize([ cast.c filter.c diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index bd174f30095c6..428962c709b9c 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -29,7 +29,7 @@ #include "zend_exceptions.h" #include "php_openssl.h" #include "php_openssl_backend.h" -#include "php_network.h" +#include "php_io.h" #include #include #include @@ -2605,6 +2605,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; } diff --git a/ext/standard/tests/streams/stream_copy_to_stream_file_to_socket_medium.phpt b/ext/standard/tests/streams/stream_copy_to_stream_file_to_socket_medium.phpt new file mode 100644 index 0000000000000..c7bd9afeedacf --- /dev/null +++ b/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-- + +--FILE-- +run($clientCode, $serverCode); +?> +--EXPECT-- +int(16384) +int(16384) +bool(true) diff --git a/ext/standard/tests/streams/stream_copy_to_stream_socket.phpt b/ext/standard/tests/streams/stream_copy_to_stream_socket.phpt deleted file mode 100644 index dafe90e40c405..0000000000000 --- a/ext/standard/tests/streams/stream_copy_to_stream_socket.phpt +++ /dev/null @@ -1,30 +0,0 @@ ---TEST-- -stream_copy_to_stream() with socket as $source ---SKIPIF-- - ---FILE-- - ---EXPECT-- -string(1) "a" -string(1) "a" diff --git a/ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_large.phpt b/ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_large.phpt new file mode 100644 index 0000000000000..6d66bf4f81ba7 --- /dev/null +++ b/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-- + +--FILE-- +run($clientCode, $serverCode); +?> +--EXPECT-- +int(200000) diff --git a/ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_single.phpt b/ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_single.phpt new file mode 100644 index 0000000000000..a5b694ce56507 --- /dev/null +++ b/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-- + +--FILE-- +run($clientCode, $serverCode); +?> +--EXPECT-- +string(1) "a" +string(1) "a" diff --git a/ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_small.phpt b/ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_small.phpt new file mode 100644 index 0000000000000..9b4f8befce936 --- /dev/null +++ b/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-- + +--FILE-- +run($clientCode, $serverCode); +?> +--EXPECTF-- +string(2048) "aaaaa%saaa" diff --git a/ext/standard/tests/streams/stream_copy_to_stream_socket_to_socket.phpt b/ext/standard/tests/streams/stream_copy_to_stream_socket_to_socket.phpt new file mode 100644 index 0000000000000..d462d51170912 --- /dev/null +++ b/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-- + +--FILE-- +run($clientCode, [ + 'source' => $sourceCode, + 'dest' => $destCode, +]); +?> +--EXPECT-- +int(10000) +int(10000) +bool(true) diff --git a/ext/standard/tests/streams/stream_copy_to_stream_socket_to_stdout.phpt b/ext/standard/tests/streams/stream_copy_to_stream_socket_to_stdout.phpt new file mode 100644 index 0000000000000..f378207f4ef35 --- /dev/null +++ b/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-- + +--FILE-- +run($clientCode, $serverCode); +?> +--EXPECT-- +data to stdout diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c index 0faf65f36437f..f0509d94ea54f 100644 --- a/ext/zend_test/test.c +++ b/ext/zend_test/test.c @@ -1820,7 +1820,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); diff --git a/main/io/php_io.c b/main/io/php_io.c new file mode 100644 index 0000000000000..4f172983d27fa --- /dev/null +++ b/main/io/php_io.c @@ -0,0 +1,81 @@ +/* + +----------------------------------------------------------------------+ + | 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 . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php.h" +#include "php_io.h" +#include "php_io_internal.h" + +#ifdef PHP_WIN32 +#include +#include +#else +#include +#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 ssize_t php_io_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen) +{ + return php_io_get()->copy(src, dest, maxlen); +} + +ssize_t php_io_generic_copy_fallback(int src_fd, int dest_fd, size_t maxlen) +{ + char buf[8192]; + ssize_t total_copied = 0; + size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; + + while (remaining > 0) { + size_t to_read = (remaining < sizeof(buf)) ? remaining : sizeof(buf); + ssize_t bytes_read = read(src_fd, buf, to_read); + + if (bytes_read < 0) { + return total_copied > 0 ? total_copied : -1; + } else if (bytes_read == 0) { + return total_copied; + } + + char *writeptr = buf; + size_t to_write = (size_t) bytes_read; + + while (to_write > 0) { + ssize_t bytes_written = write(dest_fd, writeptr, to_write); + if (bytes_written <= 0) { + return total_copied > 0 ? total_copied : -1; + } + total_copied += bytes_written; + writeptr += bytes_written; + to_write -= bytes_written; + } + + if (maxlen != PHP_IO_COPY_ALL) { + remaining -= bytes_read; + } + } + + return total_copied; +} + +ssize_t php_io_generic_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen) +{ + return php_io_generic_copy_fallback(src->fd, dest->fd, maxlen); +} diff --git a/main/io/php_io_copy_linux.c b/main/io/php_io_copy_linux.c new file mode 100644 index 0000000000000..d39f15bddef01 --- /dev/null +++ b/main/io/php_io_copy_linux.c @@ -0,0 +1,294 @@ +/* + +----------------------------------------------------------------------+ + | 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 . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#ifdef __linux__ + +#include "php_io_internal.h" +#include +#include +#include + +#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 +#endif + +#ifdef HAVE_SPLICE +#include +#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 ssize_t php_io_linux_copy_file_to_file(int src_fd, int dest_fd, size_t maxlen) +{ +#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); + } + break; + default: + return total_copied > 0 ? (ssize_t) total_copied : -1; + } + break; + } + } + + if (total_copied > 0) { + return (ssize_t) total_copied; + } +#endif + + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); +} + +static ssize_t php_io_linux_sendfile(int src_fd, int dest_fd, size_t maxlen) +{ +#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 EINVAL: + case ENOSYS: + if (total_copied == 0) { + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); + } + break; + case EAGAIN: + break; + default: + if (total_copied == 0) { + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); + } + break; + } + break; + } + } + + if (total_copied > 0) { + return (ssize_t) total_copied; + } +#endif + + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); +} + +#ifdef HAVE_SPLICE +static ssize_t php_io_linux_splice_from_pipe(php_io_fd *src, int dest_fd, size_t maxlen) +{ + size_t total_copied = 0; + size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; + + while (remaining > 0) { + int ready = php_io_linux_wait_for_data(src); + if (ready == 0) { + break; + } else if (ready < 0) { + return total_copied > 0 ? (ssize_t) total_copied : -1; + } + + size_t to_copy = (remaining < SSIZE_MAX) ? remaining : SSIZE_MAX; + ssize_t result = splice(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 { + if (total_copied == 0) { + return php_io_generic_copy_fallback(src->fd, dest_fd, maxlen); + } + break; + } + } + + return total_copied > 0 ? (ssize_t) total_copied : -1; +} + +static ssize_t php_io_linux_splice_via_pipe(php_io_fd *src, int dest_fd, size_t maxlen) +{ + int pipefd[2]; + if (pipe(pipefd) == -1) { + return php_io_generic_copy_fallback(src->fd, dest_fd, maxlen); + } + + size_t total_copied = 0; + size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; + + while (remaining > 0) { + int ready = php_io_linux_wait_for_data(src); + if (ready == 0) { + /* timeout */ + break; + } else if (ready < 0) { + if (total_copied == 0) { + close(pipefd[0]); + close(pipefd[1]); + return -1; + } + 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) { + close(pipefd[0]); + close(pipefd[1]); + if (total_copied == 0) { + return php_io_generic_copy_fallback(src->fd, dest_fd, maxlen); + } + return (ssize_t) total_copied; + } + 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, 0); + if (out <= 0) { + 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 = read(pipefd[0], drain_buf, to_drain); + if (drained <= 0) { + break; + } + ssize_t written = write(dest_fd, drain_buf, drained); + if (written <= 0) { + close(pipefd[0]); + close(pipefd[1]); + return total_copied > 0 ? (ssize_t) total_copied : -1; + } + pipe_remaining -= written; + total_copied += written; + } + close(pipefd[0]); + close(pipefd[1]); + return total_copied > 0 ? (ssize_t) total_copied : -1; + } + pipe_remaining -= out; + total_copied += out; + } + + if (maxlen != PHP_IO_COPY_ALL) { + remaining -= in_pipe; + } + } + + close(pipefd[0]); + close(pipefd[1]); + return total_copied > 0 ? (ssize_t) total_copied : -1; +} +#endif /* HAVE_SPLICE */ + +ssize_t php_io_linux_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen) +{ + 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); + } + + 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); + } + + 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); + } + +#ifdef HAVE_SPLICE + if (src->fd_type == PHP_IO_FD_PIPE) { + return php_io_linux_splice_from_pipe(src, dest->fd, maxlen); + } + + if (src->fd_type == PHP_IO_FD_SOCKET) { + return php_io_linux_splice_via_pipe(src, dest->fd, maxlen); + } +#endif + + return php_io_generic_copy_fallback(src->fd, dest->fd, maxlen); +} + +#endif /* __linux__ */ diff --git a/main/io/php_io_copy_windows.c b/main/io/php_io_copy_windows.c new file mode 100644 index 0000000000000..c0cb758ce9b87 --- /dev/null +++ b/main/io/php_io_copy_windows.c @@ -0,0 +1,154 @@ +/* + +----------------------------------------------------------------------+ + | 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 . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php_io_internal.h" + +#ifdef PHP_WIN32 + +#include +#include +#include + +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 ssize_t php_io_win_copy_loop(php_io_fd *src, php_io_fd *dest, size_t maxlen) +{ + char buf[8192]; + size_t total_copied = 0; + size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; + + while (remaining > 0) { + int ready = php_io_win_wait_for_data(src); + if (ready == 0) { + break; + } else if (ready < 0) { + return total_copied > 0 ? (ssize_t) total_copied : -1; + } + + 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) { + return total_copied > 0 ? (ssize_t) total_copied : -1; + } else if (bytes_read == 0) { + return (ssize_t) total_copied; + } + + 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) { + return total_copied > 0 ? (ssize_t) total_copied : -1; + } + total_copied += bytes_written; + writeptr += bytes_written; + to_write -= bytes_written; + } + + if (maxlen != PHP_IO_COPY_ALL) { + remaining -= bytes_read; + } + } + + return (ssize_t) total_copied; +} + +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 -1; + } + + LARGE_INTEGER file_pos; + file_pos.QuadPart = 0; + if (!SetFilePointerEx(file_handle, file_pos, &file_pos, FILE_CURRENT)) { + return -1; + } + + DWORD bytes_to_send = (maxlen == PHP_IO_COPY_ALL) + ? 0 : (DWORD) min(maxlen, MAXDWORD); + + if (!TransmitFile(dest_sock, file_handle, bytes_to_send, 0, NULL, NULL, 0)) { + SetFilePointerEx(file_handle, file_pos, NULL, FILE_BEGIN); + return -1; + } + + LARGE_INTEGER new_pos; + LARGE_INTEGER zero = {0}; + if (SetFilePointerEx(file_handle, zero, &new_pos, FILE_CURRENT)) { + return (ssize_t)(new_pos.QuadPart - file_pos.QuadPart); + } + return (ssize_t) bytes_to_send; +} + +ssize_t php_io_windows_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen) +{ + 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) { + return result; + } + } + + return php_io_win_copy_loop(src, dest, maxlen); +} + +#endif /* PHP_WIN32 */ diff --git a/main/io/php_io_generic.h b/main/io/php_io_generic.h new file mode 100644 index 0000000000000..f5a9478e3dbce --- /dev/null +++ b/main/io/php_io_generic.h @@ -0,0 +1,23 @@ +/* + +----------------------------------------------------------------------+ + | 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 | + +----------------------------------------------------------------------+ +*/ + +#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 */ diff --git a/main/io/php_io_internal.h b/main/io/php_io_internal.h new file mode 100644 index 0000000000000..69a1db5dee19f --- /dev/null +++ b/main/io/php_io_internal.h @@ -0,0 +1,33 @@ +/* + +----------------------------------------------------------------------+ + | 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 | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_IO_INTERNAL_H +#define PHP_IO_INTERNAL_H + +#include "php_io.h" + +ssize_t php_io_generic_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen); +ssize_t php_io_generic_copy_fallback(int src_fd, int dest_fd, size_t maxlen); + +#ifdef __linux__ +#include "php_io_linux.h" +#elif defined(PHP_WIN32) +#include "php_io_windows.h" +#else +#include "php_io_generic.h" +#endif + +#endif /* PHP_IO_INTERNAL_H */ diff --git a/main/io/php_io_linux.h b/main/io/php_io_linux.h new file mode 100644 index 0000000000000..40eedf014f055 --- /dev/null +++ b/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 . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_IO_LINUX_H +#define PHP_IO_LINUX_H + +ssize_t php_io_linux_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen); + +#define PHP_IO_PLATFORM_COPY php_io_linux_copy +#define PHP_IO_PLATFORM_NAME "linux" + +#endif /* PHP_IO_LINUX_H */ diff --git a/main/io/php_io_windows.h b/main/io/php_io_windows.h new file mode 100644 index 0000000000000..a80a8af63564c --- /dev/null +++ b/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 . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_IO_WINDOWS_H +#define PHP_IO_WINDOWS_H + +ssize_t php_io_windows_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen); + +#define PHP_IO_PLATFORM_COPY php_io_windows_copy +#define PHP_IO_PLATFORM_NAME "windows" + +#endif /* PHP_IO_WINDOWS_H */ diff --git a/main/php_io.h b/main/php_io.h new file mode 100644 index 0000000000000..a760d80a2cba3 --- /dev/null +++ b/main/php_io.h @@ -0,0 +1,51 @@ +/* + +----------------------------------------------------------------------+ + | 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 | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_IO_H +#define PHP_IO_H + +#include "php.h" +#include "php_network.h" + +#define PHP_IO_COPY_ALL SIZE_MAX + +#define PHP_IO_FD_FILE 1 +#define PHP_IO_FD_SOCKET 2 +#define PHP_IO_FD_PIPE 3 + +typedef struct { + union { + int fd; + php_socket_t socket; + }; + int fd_type; + struct timeval timeout; + unsigned is_blocked:1; +} php_io_fd; + +typedef ssize_t (*php_io_copy_fn)(php_io_fd *src, php_io_fd *dest, size_t maxlen); + +typedef struct php_io { + php_io_copy_fn copy; + const char *platform_name; +} php_io; + +PHPAPI php_io *php_io_get(void); + +/* Returns bytes copied on success, -1 on error */ +PHPAPI ssize_t php_io_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen); + +#endif /* PHP_IO_H */ diff --git a/main/php_streams.h b/main/php_streams.h index 1c52539cfcaee..d017a1d76b22f 100644 --- a/main/php_streams.h +++ b/main/php_streams.h @@ -554,6 +554,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 diff --git a/main/streams/cast.c b/main/streams/cast.c index 4dc8ddb5f6a30..a29a10ef323bb 100644 --- a/main/streams/cast.c +++ b/main/streams/cast.c @@ -197,7 +197,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; @@ -207,6 +207,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) { diff --git a/main/streams/plain_wrapper.c b/main/streams/plain_wrapper.c index 74d8877a7f381..cc7660528709d 100644 --- a/main/streams/plain_wrapper.c +++ b/main/streams/plain_wrapper.c @@ -34,6 +34,7 @@ #endif #include "SAPI.h" +#include "php_io.h" #include "php_streams_int.h" #ifdef PHP_WIN32 # include "win32/winutil.h" @@ -684,7 +685,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; } @@ -695,6 +695,25 @@ 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: + PHP_STDIOP_GET_FD(fd, data); + if (SOCK_ERR == fd) { + return FAILURE; + } + if (data->file) { + fflush(data->file); + } + 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; } diff --git a/main/streams/streams.c b/main/streams/streams.c index 32c7ba99f58c2..2ade64aff2cc8 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -24,6 +24,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) */ @@ -1637,164 +1638,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; @@ -1809,14 +1663,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; } @@ -1833,6 +1687,55 @@ 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 read buffer is empty */ + if (!php_stream_is(src, PHP_STREAM_IS_USERSPACE) && !php_stream_is(dest, PHP_STREAM_IS_USERSPACE) && + src->writepos == src->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; + ssize_t result = php_io_copy(&src_copy_fd, &dest_copy_fd, io_maxlen); + + if (result >= 0) { + src->position += result; + dest->position += result; + *len = result; + return SUCCESS; + } + + *len = 0; + return FAILURE; + } + } + +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() */ diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c index f21944313d32a..2a7213160cb15 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -17,7 +17,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 @@ -527,11 +527,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 diff --git a/win32/build/config.w32 b/win32/build/config.w32 index aefcfb5f82474..fa75404748b0d 100644 --- a/win32/build/config.w32 +++ b/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/streams", "streams.c cast.c memory.c filter.c plain_wrapper.c \ userspace.c transports.c xp_socket.c mmap.c glob_wrapper.c"); ADD_FLAG("CFLAGS_BD_MAIN_STREAMS", "/D ZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); @@ -309,7 +312,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); diff --git a/win32/build/confutils.js b/win32/build/confutils.js index 3acb9ad203cc8..4885025da9c77 100644 --- a/win32/build/confutils.js +++ b/win32/build/confutils.js @@ -3502,7 +3502,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()