diff --git a/ext/zend_test/php_test.h b/ext/zend_test/php_test.h index a5b11a6041d..87412ba34d3 100644 --- a/ext/zend_test/php_test.h +++ b/ext/zend_test/php_test.h @@ -53,6 +53,7 @@ ZEND_BEGIN_MODULE_GLOBALS(zend_test) int replace_zend_execute_ex; int register_passes; bool print_stderr_mshutdown; + zend_long limit_copy_file_range; zend_test_fiber *active_fiber; zend_long quantity_value; zend_string *str_test; diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c index 96898d2d71f..c6a9c7fb60a 100644 --- a/ext/zend_test/test.c +++ b/ext/zend_test/test.c @@ -650,6 +650,9 @@ PHP_INI_BEGIN() STD_PHP_INI_BOOLEAN("zend_test.replace_zend_execute_ex", "0", PHP_INI_SYSTEM, OnUpdateBool, replace_zend_execute_ex, zend_zend_test_globals, zend_test_globals) STD_PHP_INI_BOOLEAN("zend_test.register_passes", "0", PHP_INI_SYSTEM, OnUpdateBool, register_passes, zend_zend_test_globals, zend_test_globals) STD_PHP_INI_BOOLEAN("zend_test.print_stderr_mshutdown", "0", PHP_INI_SYSTEM, OnUpdateBool, print_stderr_mshutdown, zend_zend_test_globals, zend_test_globals) +#ifdef HAVE_COPY_FILE_RANGE + STD_PHP_INI_ENTRY("zend_test.limit_copy_file_range", "-1", PHP_INI_ALL, OnUpdateLong, limit_copy_file_range, zend_zend_test_globals, zend_test_globals) +#endif STD_PHP_INI_ENTRY("zend_test.quantity_value", "0", PHP_INI_ALL, OnUpdateLong, quantity_value, zend_zend_test_globals, zend_test_globals) STD_PHP_INI_ENTRY("zend_test.str_test", "", PHP_INI_ALL, OnUpdateStr, str_test, zend_zend_test_globals, zend_test_globals) STD_PHP_INI_ENTRY("zend_test.not_empty_str_test", "val", PHP_INI_ALL, OnUpdateStrNotEmpty, not_empty_str_test, zend_zend_test_globals, zend_test_globals) @@ -930,3 +933,17 @@ PHP_ZEND_TEST_API void bug_gh9090_void_int_char_var(int i, char *fmt, ...) { va_end(args); } + +#ifdef HAVE_COPY_FILE_RANGE +/** + * This function allows us to simulate early return of copy_file_range by setting the limit_copy_file_range ini setting. + */ +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)) { + len = ZT_G(limit_copy_file_range); + } + return original_copy_file_range(fd_in, off_in, fd_out, off_out, len, flags); +} +#endif diff --git a/ext/zend_test/tests/gh10370.tar b/ext/zend_test/tests/gh10370.tar new file mode 100644 index 00000000000..4dbb754430d Binary files /dev/null and b/ext/zend_test/tests/gh10370.tar differ diff --git a/ext/zend_test/tests/gh10370_1.phpt b/ext/zend_test/tests/gh10370_1.phpt new file mode 100644 index 00000000000..f594c3d70ec --- /dev/null +++ b/ext/zend_test/tests/gh10370_1.phpt @@ -0,0 +1,29 @@ +--TEST-- +GH-10370: File corruption in _php_stream_copy_to_stream_ex when using copy_file_range - partial copy +--EXTENSIONS-- +zend_test +phar +--SKIPIF-- + +--INI-- +zend_test.limit_copy_file_range=3584 +--FILE-- +extractTo(__DIR__ . DIRECTORY_SEPARATOR . 'gh10370', ['testfile'])); +var_dump(sha1_file(__DIR__ . DIRECTORY_SEPARATOR . 'gh10370' . DIRECTORY_SEPARATOR . 'testfile')); +?> +--EXPECT-- +bool(true) +string(40) "a723ae4ec7eababff73ca961a771b794be6388d2" +--CLEAN-- + diff --git a/ext/zend_test/tests/gh10370_2.phpt b/ext/zend_test/tests/gh10370_2.phpt new file mode 100644 index 00000000000..6f0d9da1207 --- /dev/null +++ b/ext/zend_test/tests/gh10370_2.phpt @@ -0,0 +1,30 @@ +--TEST-- +GH-10370: File corruption in _php_stream_copy_to_stream_ex when using copy_file_range - unlimited copy +--EXTENSIONS-- +zend_test +--SKIPIF-- + +--INI-- +zend_test.limit_copy_file_range=4096 +--FILE-- + +--EXPECT-- +string(40) "edcad8cd6c276f5e318c826ad77a5604d6a6e93d" +string(40) "edcad8cd6c276f5e318c826ad77a5604d6a6e93d" +--CLEAN-- + diff --git a/ext/zend_test/tests/gh10370_3.phpt b/ext/zend_test/tests/gh10370_3.phpt new file mode 100644 index 00000000000..2df35774287 --- /dev/null +++ b/ext/zend_test/tests/gh10370_3.phpt @@ -0,0 +1,36 @@ +--TEST-- +GH-10370: File corruption in _php_stream_copy_to_stream_ex when using copy_file_range - partial copy using stream_copy_to_stream +--EXTENSIONS-- +zend_test +--SKIPIF-- + +--INI-- +zend_test.limit_copy_file_range=3584 +--FILE-- + +--EXPECT-- +int(10240) +string(40) "a723ae4ec7eababff73ca961a771b794be6388d2" +--CLEAN-- + diff --git a/ext/zend_test/tests/gh10370_4.phpt b/ext/zend_test/tests/gh10370_4.phpt new file mode 100644 index 00000000000..69dce59ea22 --- /dev/null +++ b/ext/zend_test/tests/gh10370_4.phpt @@ -0,0 +1,38 @@ +--TEST-- +GH-10370: File corruption in _php_stream_copy_to_stream_ex when using copy_file_range - unlimited copy using stream_copy_to_stream +--EXTENSIONS-- +zend_test +--SKIPIF-- + +--INI-- +zend_test.limit_copy_file_range=4096 +--FILE-- + +--EXPECT-- +int(11776) +string(40) "edcad8cd6c276f5e318c826ad77a5604d6a6e93d" +string(40) "edcad8cd6c276f5e318c826ad77a5604d6a6e93d" +--CLEAN-- + diff --git a/main/streams/streams.c b/main/streams/streams.c index 20029fc73ee..de53e483c62 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -1634,8 +1634,21 @@ PHPAPI zend_result _php_stream_copy_to_stream_ex(php_stream *src, php_stream *de char *p; do { - size_t chunk_size = (maxlen == 0 || maxlen > PHP_STREAM_MMAP_MAX) ? PHP_STREAM_MMAP_MAX : maxlen; - size_t mapped; + /* 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); @@ -1650,6 +1663,7 @@ PHPAPI zend_result _php_stream_copy_to_stream_ex(php_stream *src, php_stream *de didwrite = php_stream_write(dest, p, mapped); if (didwrite < 0) { *len = haveread; + php_stream_mmap_unmap(src); return FAILURE; } @@ -1666,9 +1680,10 @@ PHPAPI zend_result _php_stream_copy_to_stream_ex(php_stream *src, php_stream *de if (mapped < chunk_size) { return SUCCESS; } + /* If we're not reading as much as possible, so a bounded read */ if (maxlen != 0) { - maxlen -= mapped; - if (maxlen == 0) { + must_read -= mapped; + if (must_read == 0) { return SUCCESS; } }