Fixed copy_file sendfile fallback if copy_file_range fails with ENOSYS.

The copy_file_range implementation of copy_file used to set incorrect sendfile
fallback if copy_file_range failed with ENOSYS. The fallback would skip
checking the filesystem type for whether it is supported by sendfile.

Also, wrapped sendfile and copy_file_range implementations in structs to
silence clang warnings about using C++11 feature in C++03 mode: the functions
are in an anonymous namespace and therefore have internal linkage, and
pointers to such functions are not allowed to be used in non-type template
parameters in C++03.
This commit is contained in:
Andrey Semashev 2022-07-17 03:46:04 +03:00
parent fd3af54208
commit 561b964c2e
2 changed files with 103 additions and 88 deletions

View File

@ -44,6 +44,7 @@
<li>On Windows, added a fallback implementation for querying file attributes in case if the file cannot be opened with <code>ERROR_ACCESS_DENIED</code> error. This may allow <code>status</code> and <code>symlink_status</code> to succeed for system files and directories that are not reparse points or symlinks. (<a href="https://github.com/boostorg/filesystem/issues/234">#234</a>)</li>
<li>On Windows, added a workaround for FAT/exFAT filesystems that produce <code>ERROR_INVALID_PARAMETER</code> when querying file attributes. This affected <code>status</code> and <code>symlink_status</code>, which reported that files do not exist, and directory iterators, which failed to construct, as well as other dependent operations. (<a href="https://github.com/boostorg/filesystem/issues/236">#236</a>, <a href="https://github.com/boostorg/filesystem/issues/237">#237</a>)</li>
<li>Worked around a compilation problem on <a href="https://www.rtems.org/">RTEMS</a>. (<a href="https://github.com/boostorg/filesystem/pull/240">#240</a>)</li>
<li>On Linux, corrected switching to <code>sendfile</code> <code>copy_file</code> implementation if <code>copy_file_range</code> failed with <code>ENOSYS</code> in runtime. The <code>sendfile</code> fallback implementation used to skip the filesystem type check and could fail for some filesystems.</li>
</ul>
<h2>1.79.0</h2>

View File

@ -619,130 +619,144 @@ typedef int copy_file_data_t(int infile, int outfile, uintmax_t size, std::size_
//! Pointer to the actual implementation of the copy_file_data implementation
copy_file_data_t* copy_file_data = &copy_file_data_read_write;
#if defined(BOOST_FILESYSTEM_USE_SENDFILE) || defined(BOOST_FILESYSTEM_USE_COPY_FILE_RANGE)
//! copy_file_data wrapper that tests if a read/write loop must be used for a given filesystem
template< typename CopyFileData >
int check_fs_type(int infile, int outfile, uintmax_t size, std::size_t blksize);
#endif // defined(BOOST_FILESYSTEM_USE_SENDFILE) || defined(BOOST_FILESYSTEM_USE_COPY_FILE_RANGE)
#if defined(BOOST_FILESYSTEM_USE_SENDFILE)
//! copy_file implementation that uses sendfile loop. Requires sendfile to support file descriptors.
int copy_file_data_sendfile(int infile, int outfile, uintmax_t size, std::size_t blksize)
struct copy_file_data_sendfile
{
// sendfile will not send more than this amount of data in one call
BOOST_CONSTEXPR_OR_CONST std::size_t max_batch_size = 0x7ffff000u;
uintmax_t offset = 0u;
while (offset < size)
//! copy_file implementation that uses sendfile loop. Requires sendfile to support file descriptors.
static int impl(int infile, int outfile, uintmax_t size, std::size_t blksize)
{
uintmax_t size_left = size - offset;
std::size_t size_to_copy = max_batch_size;
if (size_left < static_cast< uintmax_t >(max_batch_size))
size_to_copy = static_cast< std::size_t >(size_left);
ssize_t sz = ::sendfile(outfile, infile, NULL, size_to_copy);
if (BOOST_UNLIKELY(sz < 0))
// sendfile will not send more than this amount of data in one call
BOOST_CONSTEXPR_OR_CONST std::size_t max_batch_size = 0x7ffff000u;
uintmax_t offset = 0u;
while (offset < size)
{
int err = errno;
if (err == EINTR)
continue;
if (offset == 0u)
uintmax_t size_left = size - offset;
std::size_t size_to_copy = max_batch_size;
if (size_left < static_cast< uintmax_t >(max_batch_size))
size_to_copy = static_cast< std::size_t >(size_left);
ssize_t sz = ::sendfile(outfile, infile, NULL, size_to_copy);
if (BOOST_UNLIKELY(sz < 0))
{
// sendfile may fail with EINVAL if the underlying filesystem does not support it
if (err == EINVAL)
int err = errno;
if (err == EINTR)
continue;
if (offset == 0u)
{
fallback_to_read_write:
return copy_file_data_read_write(infile, outfile, size, blksize);
// sendfile may fail with EINVAL if the underlying filesystem does not support it
if (err == EINVAL)
{
fallback_to_read_write:
return copy_file_data_read_write(infile, outfile, size, blksize);
}
if (err == ENOSYS)
{
filesystem::detail::atomic_store_relaxed(copy_file_data, &copy_file_data_read_write);
goto fallback_to_read_write;
}
}
if (err == ENOSYS)
{
filesystem::detail::atomic_store_relaxed(copy_file_data, &copy_file_data_read_write);
goto fallback_to_read_write;
}
return err;
}
return err;
offset += sz;
}
offset += sz;
return 0;
}
return 0;
}
};
#endif // defined(BOOST_FILESYSTEM_USE_SENDFILE)
#if defined(BOOST_FILESYSTEM_USE_COPY_FILE_RANGE)
//! copy_file implementation that uses copy_file_range loop. Requires copy_file_range to support cross-filesystem copying.
int copy_file_data_copy_file_range(int infile, int outfile, uintmax_t size, std::size_t blksize)
struct copy_file_data_copy_file_range
{
// Although copy_file_range does not document any particular upper limit of one transfer, still use some upper bound to guarantee
// that size_t is not overflown in case if off_t is larger and the file size does not fit in size_t.
BOOST_CONSTEXPR_OR_CONST std::size_t max_batch_size = 0x7ffff000u;
uintmax_t offset = 0u;
while (offset < size)
//! copy_file implementation that uses copy_file_range loop. Requires copy_file_range to support cross-filesystem copying.
static int impl(int infile, int outfile, uintmax_t size, std::size_t blksize)
{
uintmax_t size_left = size - offset;
std::size_t size_to_copy = max_batch_size;
if (size_left < static_cast< uintmax_t >(max_batch_size))
size_to_copy = static_cast< std::size_t >(size_left);
// Note: Use syscall directly to avoid depending on libc version. copy_file_range is added in glibc 2.27.
// uClibc-ng does not have copy_file_range as of the time of this writing (the latest uClibc-ng release is 1.0.33).
loff_t sz = ::syscall(__NR_copy_file_range, infile, (loff_t*)NULL, outfile, (loff_t*)NULL, size_to_copy, (unsigned int)0u);
if (BOOST_UNLIKELY(sz < 0))
// Although copy_file_range does not document any particular upper limit of one transfer, still use some upper bound to guarantee
// that size_t is not overflown in case if off_t is larger and the file size does not fit in size_t.
BOOST_CONSTEXPR_OR_CONST std::size_t max_batch_size = 0x7ffff000u;
uintmax_t offset = 0u;
while (offset < size)
{
int err = errno;
if (err == EINTR)
continue;
if (offset == 0u)
uintmax_t size_left = size - offset;
std::size_t size_to_copy = max_batch_size;
if (size_left < static_cast< uintmax_t >(max_batch_size))
size_to_copy = static_cast< std::size_t >(size_left);
// Note: Use syscall directly to avoid depending on libc version. copy_file_range is added in glibc 2.27.
// uClibc-ng does not have copy_file_range as of the time of this writing (the latest uClibc-ng release is 1.0.33).
loff_t sz = ::syscall(__NR_copy_file_range, infile, (loff_t*)NULL, outfile, (loff_t*)NULL, size_to_copy, (unsigned int)0u);
if (BOOST_UNLIKELY(sz < 0))
{
// copy_file_range may fail with EINVAL if the underlying filesystem does not support it.
// In some RHEL/CentOS 7.7-7.8 kernel versions, copy_file_range on NFSv4 is also known to return EOPNOTSUPP
// if the remote server does not support COPY, despite that it is not a documented error code.
// See https://patchwork.kernel.org/project/linux-nfs/patch/20190411183418.4510-1-olga.kornievskaia@gmail.com/
// and https://bugzilla.redhat.com/show_bug.cgi?id=1783554.
if (err == EINVAL || err == EOPNOTSUPP)
int err = errno;
if (err == EINTR)
continue;
if (offset == 0u)
{
// copy_file_range may fail with EINVAL if the underlying filesystem does not support it.
// In some RHEL/CentOS 7.7-7.8 kernel versions, copy_file_range on NFSv4 is also known to return EOPNOTSUPP
// if the remote server does not support COPY, despite that it is not a documented error code.
// See https://patchwork.kernel.org/project/linux-nfs/patch/20190411183418.4510-1-olga.kornievskaia@gmail.com/
// and https://bugzilla.redhat.com/show_bug.cgi?id=1783554.
if (err == EINVAL || err == EOPNOTSUPP)
{
#if !defined(BOOST_FILESYSTEM_USE_SENDFILE)
fallback_to_read_write:
fallback_to_read_write:
#endif
return copy_file_data_read_write(infile, outfile, size, blksize);
return copy_file_data_read_write(infile, outfile, size, blksize);
}
if (err == EXDEV)
{
#if defined(BOOST_FILESYSTEM_USE_SENDFILE)
fallback_to_sendfile:
return copy_file_data_sendfile::impl(infile, outfile, size, blksize);
#else
goto fallback_to_read_write;
#endif
}
if (err == ENOSYS)
{
#if defined(BOOST_FILESYSTEM_USE_SENDFILE)
filesystem::detail::atomic_store_relaxed(copy_file_data, &check_fs_type< copy_file_data_sendfile >);
goto fallback_to_sendfile;
#else
filesystem::detail::atomic_store_relaxed(copy_file_data, &copy_file_data_read_write);
goto fallback_to_read_write;
#endif
}
}
if (err == EXDEV)
{
#if defined(BOOST_FILESYSTEM_USE_SENDFILE)
fallback_to_sendfile:
return copy_file_data_sendfile(infile, outfile, size, blksize);
#else
goto fallback_to_read_write;
#endif
}
if (err == ENOSYS)
{
#if defined(BOOST_FILESYSTEM_USE_SENDFILE)
filesystem::detail::atomic_store_relaxed(copy_file_data, &copy_file_data_sendfile);
goto fallback_to_sendfile;
#else
filesystem::detail::atomic_store_relaxed(copy_file_data, &copy_file_data_read_write);
goto fallback_to_read_write;
#endif
}
return err;
}
return err;
offset += sz;
}
offset += sz;
return 0;
}
return 0;
}
};
#endif // defined(BOOST_FILESYSTEM_USE_COPY_FILE_RANGE)
#if defined(BOOST_FILESYSTEM_USE_SENDFILE) || defined(BOOST_FILESYSTEM_USE_COPY_FILE_RANGE)
//! copy_file_data wrapper that tests if a read/write loop must be used for a given filesystem
template< copy_file_data_t* CopyFileData >
template< typename CopyFileData >
int check_fs_type(int infile, int outfile, uintmax_t size, std::size_t blksize)
{
{
@ -776,7 +790,7 @@ int check_fs_type(int infile, int outfile, uintmax_t size, std::size_t blksize)
}
}
return CopyFileData(infile, outfile, size, blksize);
return CopyFileData::impl(infile, outfile, size, blksize);
}
#endif // defined(BOOST_FILESYSTEM_USE_SENDFILE) || defined(BOOST_FILESYSTEM_USE_COPY_FILE_RANGE)
@ -792,14 +806,14 @@ inline void init_copy_file_data_impl(unsigned int major_ver, unsigned int minor_
#if defined(BOOST_FILESYSTEM_USE_SENDFILE)
// sendfile started accepting file descriptors as the target in Linux 2.6.33
if (major_ver > 2u || (major_ver == 2u && (minor_ver > 6u || (minor_ver == 6u && patch_ver >= 33u))))
cfd = &check_fs_type< &copy_file_data_sendfile >;
cfd = &check_fs_type< copy_file_data_sendfile >;
#endif
#if defined(BOOST_FILESYSTEM_USE_COPY_FILE_RANGE)
// Although copy_file_range appeared in Linux 4.5, it did not support cross-filesystem copying until 5.3.
// copy_file_data_copy_file_range will fallback to copy_file_data_sendfile if copy_file_range returns EXDEV.
if (major_ver > 4u || (major_ver == 4u && minor_ver >= 5u))
cfd = &check_fs_type< &copy_file_data_copy_file_range >;
cfd = &check_fs_type< copy_file_data_copy_file_range >;
#endif
filesystem::detail::atomic_store_relaxed(copy_file_data, cfd);