diff --git a/configure.ac b/configure.ac index 07d64902ebf0b..15fc7e4951649 100644 --- a/configure.ac +++ b/configure.ac @@ -580,10 +580,12 @@ AC_CHECK_FUNCS(m4_normalize([ putenv reallocarray scandir + sendfile setenv setitimer shutdown sigprocmask + splice statfs statvfs std_syslog @@ -1688,6 +1690,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/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/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..41838144b9fd0 --- /dev/null +++ b/main/io/php_io_copy_linux.c @@ -0,0 +1,249 @@ +/* + +----------------------------------------------------------------------+ + | 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 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(int pipe_fd, 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) { + size_t to_copy = (remaining < SSIZE_MAX) ? remaining : SSIZE_MAX; + ssize_t result = splice(pipe_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(pipe_fd, dest_fd, maxlen); + } + break; + } + } + + return total_copied > 0 ? (ssize_t) total_copied : -1; +} + +static ssize_t php_io_linux_splice_via_pipe(int src_fd, 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) { + 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) { + /* drain pipe before closing */ + 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); + } + + /* sendfile also works for file to pipe on Linux */ + 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->fd, dest->fd, maxlen); + } + + if (src->fd_type == PHP_IO_FD_SOCKET) { + return php_io_linux_splice_via_pipe(src->fd, 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..cb22a58784103 --- /dev/null +++ b/main/io/php_io_copy_windows.c @@ -0,0 +1,211 @@ +/* + +----------------------------------------------------------------------+ + | 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 + +typedef ssize_t (*php_io_win_read_fn)(void *handle, char *buf, size_t len); +typedef ssize_t (*php_io_win_write_fn)(void *handle, const char *buf, size_t len); + +static ssize_t php_io_win_read_handle(void *handle, char *buf, size_t len) +{ + DWORD to_read = (len > MAXDWORD) ? MAXDWORD : (DWORD) len; + DWORD bytes_read; + if (!ReadFile((HANDLE) handle, buf, to_read, &bytes_read, NULL)) { + return -1; + } + return (ssize_t) bytes_read; +} + +static ssize_t php_io_win_write_handle(void *handle, const char *buf, size_t len) +{ + DWORD to_write = (len > MAXDWORD) ? MAXDWORD : (DWORD) len; + DWORD bytes_written; + if (!WriteFile((HANDLE) handle, buf, to_write, &bytes_written, NULL)) { + return -1; + } + return (ssize_t) bytes_written; +} + +static ssize_t php_io_win_read_socket(void *handle, char *buf, size_t len) +{ + int to_recv = (len > INT_MAX) ? INT_MAX : (int) len; + int result = recv((SOCKET)(uintptr_t) handle, buf, to_recv, 0); + if (result == SOCKET_ERROR) { + return -1; + } + return (ssize_t) result; +} + +static ssize_t php_io_win_write_socket(void *handle, const char *buf, size_t len) +{ + int to_send = (len > INT_MAX) ? INT_MAX : (int) len; + int result = send((SOCKET)(uintptr_t) handle, buf, to_send, 0); + if (result == SOCKET_ERROR) { + return -1; + } + return (ssize_t) result; +} + +static ssize_t php_io_win_copy_loop( + void *src_handle, php_io_win_read_fn read_fn, + void *dest_handle, php_io_win_write_fn write_fn, + 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) { + size_t to_read = (remaining < sizeof(buf)) ? remaining : sizeof(buf); + ssize_t bytes_read = read_fn(src_handle, 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 = write_fn(dest_handle, writeptr, 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_copy_handle_to_handle(int src_fd, int dest_fd, size_t maxlen) +{ + HANDLE src_handle = (HANDLE) _get_osfhandle(src_fd); + HANDLE dest_handle = (HANDLE) _get_osfhandle(dest_fd); + + if (src_handle == INVALID_HANDLE_VALUE || dest_handle == INVALID_HANDLE_VALUE) { + return -1; + } + + return php_io_win_copy_loop( + (void *) src_handle, php_io_win_read_handle, + (void *) dest_handle, php_io_win_write_handle, + maxlen); +} + +static ssize_t php_io_win_copy_handle_to_socket(int src_fd, SOCKET dest_sock, size_t maxlen) +{ + HANDLE file_handle = (HANDLE) _get_osfhandle(src_fd); + + if (file_handle == INVALID_HANDLE_VALUE) { + return -1; + } + + /* Try TransmitFile for file to socket */ + if (dest_sock != INVALID_SOCKET) { + LARGE_INTEGER file_pos; + file_pos.QuadPart = 0; + if (SetFilePointerEx(file_handle, file_pos, &file_pos, FILE_CURRENT)) { + DWORD bytes_to_send; + + if (maxlen == PHP_IO_COPY_ALL) { + LARGE_INTEGER file_size; + if (GetFileSizeEx(file_handle, &file_size)) { + LONGLONG available = file_size.QuadPart - file_pos.QuadPart; + bytes_to_send = (available > MAXDWORD) ? 0 : (DWORD) available; + } else { + bytes_to_send = 0; + } + } else { + bytes_to_send = (DWORD) min(maxlen, MAXDWORD); + } + + if (TransmitFile(dest_sock, file_handle, bytes_to_send, 0, NULL, NULL, 0)) { + if (bytes_to_send == 0 && maxlen == PHP_IO_COPY_ALL) { + 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 0; + } + return (ssize_t) bytes_to_send; + } + + if (WSAGetLastError() == WSAENOTSOCK) { + SetFilePointerEx(file_handle, file_pos, NULL, FILE_BEGIN); + } + } + } + + return php_io_win_copy_loop( + (void *) file_handle, php_io_win_read_handle, + (void *)(uintptr_t) dest_sock, php_io_win_write_socket, + maxlen); +} + +static ssize_t php_io_win_copy_socket_to_handle(SOCKET src_sock, int dest_fd, size_t maxlen) +{ + HANDLE dest_handle = (HANDLE) _get_osfhandle(dest_fd); + + if (dest_handle == INVALID_HANDLE_VALUE) { + return -1; + } + + return php_io_win_copy_loop( + (void *)(uintptr_t) src_sock, php_io_win_read_socket, + (void *) dest_handle, php_io_win_write_handle, + maxlen); +} + +static ssize_t php_io_win_copy_socket_to_socket(SOCKET src_sock, SOCKET dest_sock, size_t maxlen) +{ + return php_io_win_copy_loop( + (void *)(uintptr_t) src_sock, php_io_win_read_socket, + (void *)(uintptr_t) dest_sock, php_io_win_write_socket, + maxlen); +} + +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) { + return php_io_win_copy_socket_to_socket(src->socket, dest->socket, maxlen); + } + + if (src->fd_type == PHP_IO_FD_SOCKET) { + return php_io_win_copy_socket_to_handle(src->socket, dest->fd, maxlen); + } + + if (dest->fd_type == PHP_IO_FD_SOCKET) { + return php_io_win_copy_handle_to_socket(src->fd, dest->socket, maxlen); + } + + return php_io_win_copy_handle_to_handle(src->fd, dest->fd, 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..dce3ea4a76553 --- /dev/null +++ b/main/php_io.h @@ -0,0 +1,49 @@ +/* + +----------------------------------------------------------------------+ + | 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; +} 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 688d271db8147..ea8c143bb02a7 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" @@ -641,7 +642,6 @@ static int php_stdiop_seek(php_stream *stream, zend_off_t offset, int whence, ze return ret; } } - static int php_stdiop_cast(php_stream *stream, int castas, void **ret) { php_socket_t fd; @@ -649,16 +649,10 @@ static int php_stdiop_cast(php_stream *stream, int castas, void **ret) assert(data != NULL); - /* as soon as someone touches the stdio layer, buffering may ensue, - * so we need to stop using the fd directly in that case */ - switch (castas) { case PHP_STREAM_AS_STDIO: if (ret) { - if (data->file == NULL) { - /* we were opened as a plain file descriptor, so we - * need fdopen now */ char fixed_mode[5]; php_stream_mode_sanitize_fdopen_fopencookie(stream, fixed_mode); data->file = fdopen(data->fd, fixed_mode); @@ -666,7 +660,6 @@ static int php_stdiop_cast(php_stream *stream, int castas, void **ret) return FAILURE; } } - *(FILE**)ret = data->file; data->fd = SOCK_ERR; } @@ -684,7 +677,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 +687,22 @@ 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; + } + return SUCCESS; + default: return FAILURE; } diff --git a/main/streams/streams.c b/main/streams/streams.c index 85d2947c28a6c..e0f7a5485c987 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) */ @@ -1635,165 +1636,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 ssize_t 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; @@ -1808,14 +1661,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; } @@ -1832,6 +1685,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 edf1751ec33b5..92b949c21369a 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,17 @@ 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; + } + 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 e516fd410bcd5..be5d026e1b1b1 100644 --- a/win32/build/confutils.js +++ b/win32/build/confutils.js @@ -3445,7 +3445,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()