Skip to content

Commit b732d80

Browse files
committed
Fix bug GH-9779: stream_copy_to_stream fail when dest in append mode
1 parent dbedb69 commit b732d80

File tree

4 files changed

+93
-74
lines changed

4 files changed

+93
-74
lines changed

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ PHP NEWS
2828
. Fixed bug GH-9720 (Null pointer dereference while serializing the response).
2929
(cmb)
3030

31+
- Streams:
32+
. Fixed bug GH-9779 (stream_copy_to_stream fails if dest in append mode).
33+
(Jakub Zelenka)
34+
3135
13 Oct 2022, PHP 8.2.0RC4
3236

3337
- Core:

configure.ac

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,8 @@ if test "$ac_cv_func_getaddrinfo" = yes; then
690690
AC_DEFINE(HAVE_GETADDRINFO,1,[Define if you have the getaddrinfo function])
691691
fi
692692

693+
dnl on FreeBSD, copy_file_range() works only with the undocumented flag 0x01000000;
694+
dnl until the problem is fixed properly, copy_file_range() is used only on Linux
693695
AC_CACHE_CHECK([for copy_file_range], ac_cv_copy_file_range,
694696
[AC_RUN_IFELSE([AC_LANG_SOURCE([[
695697
#ifdef __linux__

ext/standard/tests/file/gh9779.phpt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
--TEST--
2+
Bug GH-9779 (stream_copy_to_stream doesn't work anymore with resource opened in a append mode)
3+
--FILE--
4+
<?php
5+
6+
$src = __DIR__.'/gh9779_src.txt';
7+
$dest = __DIR__.'/gh9779_dest.txt';
8+
9+
file_put_contents($src, "bar");
10+
file_put_contents($dest, "foo");
11+
$sourceHandle = fopen($src, "r");
12+
$destHandle = fopen($dest, "a");
13+
stream_copy_to_stream($sourceHandle, $destHandle);
14+
fclose($sourceHandle);
15+
fclose($destHandle);
16+
var_dump(file_get_contents($dest));
17+
?>
18+
--CLEAN--
19+
<?php
20+
$src = __DIR__.'/gh9779_src.txt';
21+
$dest = __DIR__.'/gh9779_dest.txt';
22+
23+
@unlink($src);
24+
@unlink($dest);
25+
?>
26+
--EXPECTF--
27+
string(6) "foobar"

main/streams/streams.c

Lines changed: 60 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1557,87 +1557,73 @@ PHPAPI zend_result _php_stream_copy_to_stream_ex(php_stream *src, php_stream *de
15571557
}
15581558

15591559
#ifdef HAVE_COPY_FILE_RANGE
1560-
1561-
/* TODO: on FreeBSD, copy_file_range() works only with the
1562-
undocumented flag 0x01000000; until the problem is fixed
1563-
properly, copy_file_range() is not used on FreeBSD */
1564-
#ifndef __FreeBSD__
15651560
if (php_stream_is(src, PHP_STREAM_IS_STDIO) &&
1566-
php_stream_is(dest, PHP_STREAM_IS_STDIO) &&
1567-
src->writepos == src->readpos &&
1568-
php_stream_can_cast(src, PHP_STREAM_AS_FD) == SUCCESS &&
1569-
php_stream_can_cast(dest, PHP_STREAM_AS_FD) == SUCCESS) {
1570-
/* both php_stream instances are backed by a file
1571-
descriptor, are not filtered and the read buffer is
1572-
empty: we can use copy_file_range() */
1573-
1574-
int src_fd, dest_fd;
1575-
1576-
php_stream_cast(src, PHP_STREAM_AS_FD, (void*)&src_fd, 0);
1577-
php_stream_cast(dest, PHP_STREAM_AS_FD, (void*)&dest_fd, 0);
1578-
1579-
/* clamp to INT_MAX to avoid EOVERFLOW */
1580-
const size_t cfr_max = MIN(maxlen, (size_t)SSIZE_MAX);
1581-
1582-
/* copy_file_range() is a Linux-specific system call
1583-
which allows efficient copying between two file
1584-
descriptors, eliminating the need to transfer data
1585-
from the kernel to userspace and back. For
1586-
networking file systems like NFS and Ceph, it even
1587-
eliminates copying data to the client, and local
1588-
filesystems like Btrfs and XFS can create shared
1589-
extents. */
1590-
1591-
ssize_t result = copy_file_range(src_fd, NULL,
1592-
dest_fd, NULL,
1593-
cfr_max, 0);
1594-
if (result > 0) {
1595-
size_t nbytes = (size_t)result;
1596-
haveread += nbytes;
1597-
1598-
src->position += nbytes;
1599-
dest->position += nbytes;
1600-
1601-
if ((maxlen != PHP_STREAM_COPY_ALL && nbytes == maxlen) ||
1602-
php_stream_eof(src)) {
1603-
/* the whole request was satisfied or
1604-
end-of-file reached - done */
1561+
php_stream_is(dest, PHP_STREAM_IS_STDIO) &&
1562+
src->writepos == src->readpos) {
1563+
/* both php_stream instances are backed by a file descriptor, are not filtered and the
1564+
* read buffer is empty: we can use copy_file_range() */
1565+
int src_fd, dest_fd, dest_open_flags = 0;
1566+
1567+
/* get dest open flags to check if the stream is open in append mode */
1568+
php_stream_parse_fopen_modes(dest->mode, &dest_open_flags);
1569+
1570+
/* copy_file_range does not work with O_APPEND */
1571+
if (php_stream_cast(src, PHP_STREAM_AS_FD, (void*)&src_fd, 0) == SUCCESS &&
1572+
php_stream_cast(dest, PHP_STREAM_AS_FD, (void*)&dest_fd, 0) == SUCCESS &&
1573+
php_stream_parse_fopen_modes(dest->mode, &dest_open_flags) == SUCCESS &&
1574+
!(dest_open_flags & O_APPEND)) {
1575+
1576+
/* clamp to INT_MAX to avoid EOVERFLOW */
1577+
const size_t cfr_max = MIN(maxlen, (size_t)SSIZE_MAX);
1578+
1579+
/* copy_file_range() is a Linux-specific system call which allows efficient copying
1580+
* between two file descriptors, eliminating the need to transfer data from the kernel
1581+
* to userspace and back. For networking file systems like NFS and Ceph, it even
1582+
* eliminates copying data to the client, and local filesystems like Btrfs and XFS can
1583+
* create shared extents. */
1584+
ssize_t result = copy_file_range(src_fd, NULL, dest_fd, NULL, cfr_max, 0);
1585+
if (result > 0) {
1586+
size_t nbytes = (size_t)result;
1587+
haveread += nbytes;
1588+
1589+
src->position += nbytes;
1590+
dest->position += nbytes;
1591+
1592+
if ((maxlen != PHP_STREAM_COPY_ALL && nbytes == maxlen) || php_stream_eof(src)) {
1593+
/* the whole request was satisfied or end-of-file reached - done */
1594+
*len = haveread;
1595+
return SUCCESS;
1596+
}
1597+
1598+
/* there may be more data; continue copying using the fallback code below */
1599+
} else if (result == 0) {
1600+
/* end of file */
16051601
*len = haveread;
16061602
return SUCCESS;
1607-
}
1608-
1609-
/* there may be more data; continue copying
1610-
using the fallback code below */
1611-
} else if (result == 0) {
1612-
/* end of file */
1613-
*len = haveread;
1614-
return SUCCESS;
1615-
} else if (result < 0) {
1616-
switch (errno) {
1617-
case EINVAL:
1618-
/* some formal error, e.g. overlapping
1619-
file ranges */
1620-
break;
1621-
1622-
case EXDEV:
1623-
/* pre Linux 5.3 error */
1624-
break;
1625-
1626-
case ENOSYS:
1627-
/* not implemented by this Linux kernel */
1628-
break;
1603+
} else if (result < 0) {
1604+
switch (errno) {
1605+
case EINVAL:
1606+
/* some formal error, e.g. overlapping file ranges */
1607+
break;
1608+
1609+
case EXDEV:
1610+
/* pre Linux 5.3 error */
1611+
break;
1612+
1613+
case ENOSYS:
1614+
/* not implemented by this Linux kernel */
1615+
break;
1616+
1617+
default:
1618+
/* unexpected I/O error - give up, no fallback */
1619+
*len = haveread;
1620+
return FAILURE;
1621+
}
16291622

1630-
default:
1631-
/* unexpected I/O error - give up, no
1632-
fallback */
1633-
*len = haveread;
1634-
return FAILURE;
1623+
/* fall back to classic copying */
16351624
}
1636-
1637-
/* fall back to classic copying */
16381625
}
16391626
}
1640-
#endif // __FreeBSD__
16411627
#endif // HAVE_COPY_FILE_RANGE
16421628

16431629
if (maxlen == PHP_STREAM_COPY_ALL) {

0 commit comments

Comments
 (0)