Change:
While updating my code base from 1.49 to 1.50 we noticed a failures in a program that rebuilds files from small chunks. We are opening a file in append mode, one in read mode, and we stream one into the other (as for the snippet below). Testing this out confirmed that the error happens on the reader file when the writer is in append mode, whenever we use a / or not. If we load the whole file into a string and then use on the same data it works as intended, as does setting the file in write mode (while it becomes necessary to seek to the end). Checking what got shipped into 1.50 we suspect the probable culprit to be What we are guessing is that this change is using the syscall that doesn't support files in append mode even when the file is in append mode, triggering the Bad File Descriptor error I tried this code (): I expected to see this happen: the files to be concatenated Instead, this happened: returns Strace for the code snipped: Example that works with loading the file into a string before copy (): It most recently worked on: 1.49 50 : no backtrace <!-- If you know when this regression occurred, please add a line like below, replacing with one of stable, beta, or nightly. modify labels: +regression-from-stable-to-stable-regression-untriaged --<!-- TRIAGEBOTSTART --<!-- TRIAGEBOTASSIGNSTART --<!-- TRIAGEBOTASSIGNDATASTARTTRIAGEBOTASSIGNDATAEND --<!-- TRIAGEBOTASSIGNEND --<!-- TRIAGEBOT_END --
syscall326 is copyfilerange. And indeed, it has this error code overloading the more common meaning of EBADF. assign.
Error: Parsing assign command in failed: ...'ot assign.' ''... Please let know if you're having trouble with this bot.
claim
If you need a workaround for the moment you can do which should defeat the incorrect optimizations by going through a trait object so it can't see it's dealing with a file.
Nice, this is awesome. We will try this tomorrow which should allow to migrate our codebase to 1.50! Thanks a lot for the quick help on this..
Test out the workaround and it works, while we wait for the actual fix to come out, thanks a lot!
label +T-libs-impl +I-prioritize +regression-from-stable-to-stable +C-bug +O-linux workaround is available. fix needs review.
Assigning as discussed as part of the and removing . label -I-prioritize +P-critical
Not sure if this is related, but I have a smaller code which also fails with EBADF: Tested with Rust 1.41.1, stable, nightly on Debian 9 and Rust 1.41.1 on Debian 10. Also, the trick with doesn't work in this case.
Totally unrelated and not a bug. opens a file in write-only mode, then you try to read from it later. Use to create read-write files.
lol, just figured it out and wanted to post. Thanks anyway! It's still strange it's EBADF instead of EPERM.

Discussion:
use crate::ptr; use crate::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use crate::sys::cvt; <ins> use libc::{EBADF, EINVAL, ENOSYS, EOPNOTSUPP, EOVERFLOW, EPERM, EXDEV}; </ins> #[cfg(test)] mod tests;

cvt(copy_file_range(INVALID_FD, ptr::null_mut(), INVALID_FD, ptr::null_mut(), 1, 0)) }; <del> if matches!(result.map_err(|e| e.raw_os_error()), Err(Some(libc::EBADF))) { </del> <ins> if matches!(result.map_err(|e| e.raw_os_error()), Err(Some(EBADF))) { </ins> HAS_COPY_FILE_RANGE.store(AVAILABLE, Ordering::Relaxed); } else { HAS_COPY_FILE_RANGE.store(UNAVAILABLE, Ordering::Relaxed);

Err(err) => { return match err.raw_os_error() { // when file offset + max_length > u64::MAX <del> Some(libc::EOVERFLOW) => CopyResult::Fallback(written), Some( libc::ENOSYS | libc::EXDEV | libc::EINVAL | libc::EPERM | libc::EOPNOTSUPP, ) => { </del> <ins> Some(EOVERFLOW) => CopyResult::Fallback(written), Some(ENOSYS | EXDEV | EINVAL | EPERM | EOPNOTSUPP | EBADF) => { </ins> // 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 file is immutable or syscall is blocked by seccomp¹ (EPERM) // - copy_file_range cannot be used with pipes or device nodes (EINVAL) <ins> // - the writer fd was opened with O_APPEND (EBADF²) </ins> // // ¹ these cases should be detected by the initial probe but we handle them here //   anyway in case syscall interception changes during runtime <ins> // ² actually invalid file descriptors would cause this too, but in that case //   the fallback code path is expected to encounter the same error again </ins> assert_eq!(written, 0); CopyResult::Fallback(0) }

Ok(ret) => written += ret as u64, Err(err) => { return match err.raw_os_error() { <del> Some(libc::ENOSYS | libc::EPERM) => { </del> <ins> Some(ENOSYS | EPERM) => { </ins> // syscall not supported (ENOSYS) // syscall is disallowed, e.g. by seccomp (EPERM) match mode {

assert_eq!(written, 0); CopyResult::Fallback(0) } <del> Some(libc::EINVAL) => { </del> <ins> Some(EINVAL) => { </ins> // splice/sendfile do not support this particular file descriptor (EINVAL) assert_eq!(written, 0); CopyResult::Fallback(0) } <del> Some(os_err) if mode == SpliceMode::Sendfile && os_err == libc::EOVERFLOW => { </del> <ins> Some(os_err) if mode == SpliceMode::Sendfile && os_err == EOVERFLOW => { </ins> CopyResult::Fallback(written) } _ => CopyResult::Error(err, written),

result.and(rm1).and(rm2) } <ins> #[test] fn copies_append_mode_sink() -> Result<()> { let tmp_path = tmpdir(); let source_path = tmp_path.join("copies_append_mode.source"); let sink_path = tmp_path.join("copies_append_mode.sink"); let mut source = OpenOptions::new().create(true).truncate(true).write(true).read(true).open(&source_path)?; write!(source, "not empty")?; source.seek(SeekFrom::Start(0))?; let mut sink = OpenOptions::new().create(true).append(true).open(&sink_path)?; let copied = crate::io::copy(&mut source, &mut sink)?; assert_eq!(copied, 9); Ok(()) } </ins> #[bench] fn bench_file_to_file_copy(b: &mut test::Bencher) { const BYTES: usize = 128 * 1024;