From 1316c786a08344c965a97b1b67c76a300a479eec Mon Sep 17 00:00:00 2001 From: The8472 Date: Wed, 12 Aug 2020 01:15:08 +0200 Subject: [PATCH 1/3] Workaround for copy_file_range spuriously returning EOPNOTSUPP when attemted on a NFS mount under RHEL/CentOS 7. The syscall is supposed to return ENOSYS in most cases but when calling it on NFS it may leak through EOPNOTSUPP even though that's supposed to be handled by the kernel and not returned to userspace. Since it returns ENOSYS in some cases anyway this will trip the HAS_COPY_FILE_RANGE detection anyway, so treat EOPNOTSUPP as if it were a ENOSYS. https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/7.8_release_notes/deprecated_functionality#the_literal_copy_file_range_literal_call_has_been_disabled_on_local_file_systems_and_in_nfs https://bugzilla.redhat.com/show_bug.cgi?id=1783554 --- library/std/src/sys/unix/fs.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/std/src/sys/unix/fs.rs b/library/std/src/sys/unix/fs.rs index acb18e6d064e6..bcdc36a516e3a 100644 --- a/library/std/src/sys/unix/fs.rs +++ b/library/std/src/sys/unix/fs.rs @@ -1162,7 +1162,7 @@ pub fn copy(from: &Path, to: &Path) -> io::Result { }; if let Err(ref copy_err) = copy_result { match copy_err.raw_os_error() { - Some(libc::ENOSYS) | Some(libc::EPERM) => { + Some(libc::ENOSYS) | Some(libc::EPERM) | Some(libc::EOPNOTSUPP) => { HAS_COPY_FILE_RANGE.store(false, Ordering::Relaxed); } _ => {} @@ -1180,11 +1180,13 @@ pub fn copy(from: &Path, to: &Path) -> io::Result { if os_err == libc::ENOSYS || os_err == libc::EXDEV || os_err == libc::EINVAL - || os_err == libc::EPERM => + || os_err == libc::EPERM + || os_err == libc::EOPNOTSUPP => { // Try fallback io::copy if either: // - Kernel version is < 4.5 (ENOSYS) // - Files are mounted on different fs (EXDEV) + // - copy_file_range is broken in various ways on RHEL/CentOS 7 (EOPNOTSUPP) // - copy_file_range is disallowed, for example by seccomp (EPERM) // - copy_file_range cannot be used with pipes or device nodes (EINVAL) assert_eq!(written, 0); From f0783632d315db90c0ca34d79d56207d132f3764 Mon Sep 17 00:00:00 2001 From: The8472 Date: Wed, 12 Aug 2020 20:09:55 +0200 Subject: [PATCH 2/3] more concise error matching --- library/std/src/sys/unix/fs.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/library/std/src/sys/unix/fs.rs b/library/std/src/sys/unix/fs.rs index bcdc36a516e3a..63af25e309230 100644 --- a/library/std/src/sys/unix/fs.rs +++ b/library/std/src/sys/unix/fs.rs @@ -1162,7 +1162,7 @@ pub fn copy(from: &Path, to: &Path) -> io::Result { }; if let Err(ref copy_err) = copy_result { match copy_err.raw_os_error() { - Some(libc::ENOSYS) | Some(libc::EPERM) | Some(libc::EOPNOTSUPP) => { + Some(libc::ENOSYS | libc::EPERM | libc::EOPNOTSUPP) => { HAS_COPY_FILE_RANGE.store(false, Ordering::Relaxed); } _ => {} @@ -1176,13 +1176,9 @@ pub fn copy(from: &Path, to: &Path) -> io::Result { Ok(ret) => written += ret as u64, Err(err) => { match err.raw_os_error() { - Some(os_err) - if os_err == libc::ENOSYS - || os_err == libc::EXDEV - || os_err == libc::EINVAL - || os_err == libc::EPERM - || os_err == libc::EOPNOTSUPP => - { + Some( + libc::ENOSYS | libc::EXDEV | libc::EINVAL | libc::EPERM | libc::EOPNOTSUPP, + ) => { // Try fallback io::copy if either: // - Kernel version is < 4.5 (ENOSYS) // - Files are mounted on different fs (EXDEV) From 4ddedd521418d67e845ecb43dc02c09b0af53022 Mon Sep 17 00:00:00 2001 From: The8472 Date: Fri, 14 Aug 2020 22:39:04 +0200 Subject: [PATCH 3/3] perform copy_file_range until EOF is reached instead of basing things on file size This solves several problems - race conditions where a file is truncated while copying from it. if we blindly trusted the file size this would lead to an infinite loop - proc files appearing empty to copy_file_range but not to read/write https://github.com/coreutils/coreutils/commit/4b04a0c - copy_file_range returning 0 for some filesystems (overlay? bind mounts?) inside docker, again leading to an infinite loop --- library/std/src/sys/unix/fs.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/library/std/src/sys/unix/fs.rs b/library/std/src/sys/unix/fs.rs index 63af25e309230..566ac0920dc8f 100644 --- a/library/std/src/sys/unix/fs.rs +++ b/library/std/src/sys/unix/fs.rs @@ -1140,14 +1140,14 @@ pub fn copy(from: &Path, to: &Path) -> io::Result { } let (mut reader, reader_metadata) = open_from(from)?; - let len = reader_metadata.len(); + let max_len = u64::MAX; let (mut writer, _) = open_to_and_set_permissions(to, reader_metadata)?; let has_copy_file_range = HAS_COPY_FILE_RANGE.load(Ordering::Relaxed); let mut written = 0u64; - while written < len { + while written < max_len { let copy_result = if has_copy_file_range { - let bytes_to_copy = cmp::min(len - written, usize::MAX as u64) as usize; + let bytes_to_copy = cmp::min(max_len - written, usize::MAX as u64) as usize; let copy_result = unsafe { // We actually don't have to adjust the offsets, // because copy_file_range adjusts the file offset automatically @@ -1173,6 +1173,15 @@ pub fn copy(from: &Path, to: &Path) -> io::Result { Err(io::Error::from_raw_os_error(libc::ENOSYS)) }; match copy_result { + Ok(0) if written == 0 => { + // fallback to work around several kernel bugs where copy_file_range will fail to + // copy any bytes and return 0 instead of an error if + // - reading virtual files from the proc filesystem which appear to have 0 size + // but are not empty. noted in coreutils to affect kernels at least up to 5.6.19. + // - copying from an overlay filesystem in docker. reported to occur on fedora 32. + return io::copy(&mut reader, &mut writer); + } + Ok(0) => return Ok(written), // reached EOF Ok(ret) => written += ret as u64, Err(err) => { match err.raw_os_error() {