]> git.lizzy.rs Git - rust.git/blob - library/std/src/sys/unix/kernel_copy.rs
Rollup merge of #90704 - ijackson:exitstatus-comments, r=joshtriplett
[rust.git] / library / std / src / sys / unix / kernel_copy.rs
1 //! This module contains specializations that can offload `io::copy()` operations on file descriptor
2 //! containing types (`File`, `TcpStream`, etc.) to more efficient syscalls than `read(2)` and `write(2)`.
3 //!
4 //! Specialization is only applied to wholly std-owned types so that user code can't observe
5 //! that the `Read` and `Write` traits are not used.
6 //!
7 //! Since a copy operation involves a reader and writer side where each can consist of different types
8 //! and also involve generic wrappers (e.g. `Take`, `BufReader`) it is not practical to specialize
9 //! a single method on all possible combinations.
10 //!
11 //! Instead readers and writers are handled separately by the `CopyRead` and `CopyWrite` specialization
12 //! traits and then specialized on by the `Copier::copy` method.
13 //!
14 //! `Copier` uses the specialization traits to unpack the underlying file descriptors and
15 //! additional prerequisites and constraints imposed by the wrapper types.
16 //!
17 //! Once it has obtained all necessary pieces and brought any wrapper types into a state where they
18 //! can be safely bypassed it will attempt to use the `copy_file_range(2)`,
19 //! `sendfile(2)` or `splice(2)` syscalls to move data directly between file descriptors.
20 //! Since those syscalls have requirements that cannot be fully checked in advance and
21 //! gathering additional information about file descriptors would require additional syscalls
22 //! anyway it simply attempts to use them one after another (guided by inaccurate hints) to
23 //! figure out which one works and and falls back to the generic read-write copy loop if none of them
24 //! does.
25 //! Once a working syscall is found for a pair of file descriptors it will be called in a loop
26 //! until the copy operation is completed.
27 //!
28 //! Advantages of using these syscalls:
29 //!
30 //! * fewer context switches since reads and writes are coalesced into a single syscall
31 //!   and more bytes are transferred per syscall. This translates to higher throughput
32 //!   and fewer CPU cycles, at least for sufficiently large transfers to amortize the initial probing.
33 //! * `copy_file_range` creates reflink copies on CoW filesystems, thus moving less data and
34 //!   consuming less disk space
35 //! * `sendfile` and `splice` can perform zero-copy IO under some circumstances while
36 //!   a naive copy loop would move every byte through the CPU.
37 //!
38 //! Drawbacks:
39 //!
40 //! * copy operations smaller than the default buffer size can under some circumstances, especially
41 //!   on older kernels, incur more syscalls than the naive approach would. As mentioned above
42 //!   the syscall selection is guided by hints to minimize this possibility but they are not perfect.
43 //! * optimizations only apply to std types. If a user adds a custom wrapper type, e.g. to report
44 //!   progress, they can hit a performance cliff.
45 //! * complexity
46
47 use crate::cmp::min;
48 use crate::convert::TryInto;
49 use crate::fs::{File, Metadata};
50 use crate::io::copy::generic_copy;
51 use crate::io::{
52     BufRead, BufReader, BufWriter, Error, Read, Result, StderrLock, StdinLock, StdoutLock, Take,
53     Write,
54 };
55 use crate::mem::ManuallyDrop;
56 use crate::net::TcpStream;
57 use crate::os::unix::fs::FileTypeExt;
58 use crate::os::unix::io::{AsRawFd, FromRawFd, RawFd};
59 use crate::os::unix::net::UnixStream;
60 use crate::process::{ChildStderr, ChildStdin, ChildStdout};
61 use crate::ptr;
62 use crate::sync::atomic::{AtomicBool, AtomicU8, Ordering};
63 use crate::sys::cvt;
64 use crate::sys::weak::syscall;
65 use libc::{EBADF, EINVAL, ENOSYS, EOPNOTSUPP, EOVERFLOW, EPERM, EXDEV};
66
67 #[cfg(test)]
68 mod tests;
69
70 pub(crate) fn copy_spec<R: Read + ?Sized, W: Write + ?Sized>(
71     read: &mut R,
72     write: &mut W,
73 ) -> Result<u64> {
74     let copier = Copier { read, write };
75     SpecCopy::copy(copier)
76 }
77
78 /// This type represents either the inferred `FileType` of a `RawFd` based on the source
79 /// type from which it was extracted or the actual metadata
80 ///
81 /// The methods on this type only provide hints, due to `AsRawFd` and `FromRawFd` the inferred
82 /// type may be wrong.
83 enum FdMeta {
84     /// We obtained the FD from a type that can contain any type of `FileType` and queried the metadata
85     /// because it is cheaper than probing all possible syscalls (reader side)
86     Metadata(Metadata),
87     Socket,
88     Pipe,
89     /// We don't have any metadata, e.g. because the original type was `File` which can represent
90     /// any `FileType` and we did not query the metadata either since it did not seem beneficial
91     /// (writer side)
92     NoneObtained,
93 }
94
95 impl FdMeta {
96     fn maybe_fifo(&self) -> bool {
97         match self {
98             FdMeta::Metadata(meta) => meta.file_type().is_fifo(),
99             FdMeta::Socket => false,
100             FdMeta::Pipe => true,
101             FdMeta::NoneObtained => true,
102         }
103     }
104
105     fn potential_sendfile_source(&self) -> bool {
106         match self {
107             // procfs erronously shows 0 length on non-empty readable files.
108             // and if a file is truly empty then a `read` syscall will determine that and skip the write syscall
109             // thus there would be benefit from attempting sendfile
110             FdMeta::Metadata(meta)
111                 if meta.file_type().is_file() && meta.len() > 0
112                     || meta.file_type().is_block_device() =>
113             {
114                 true
115             }
116             _ => false,
117         }
118     }
119
120     fn copy_file_range_candidate(&self) -> bool {
121         match self {
122             // copy_file_range will fail on empty procfs files. `read` can determine whether EOF has been reached
123             // without extra cost and skip the write, thus there is no benefit in attempting copy_file_range
124             FdMeta::Metadata(meta) if meta.is_file() && meta.len() > 0 => true,
125             FdMeta::NoneObtained => true,
126             _ => false,
127         }
128     }
129 }
130
131 struct CopyParams(FdMeta, Option<RawFd>);
132
133 struct Copier<'a, 'b, R: Read + ?Sized, W: Write + ?Sized> {
134     read: &'a mut R,
135     write: &'b mut W,
136 }
137
138 trait SpecCopy {
139     fn copy(self) -> Result<u64>;
140 }
141
142 impl<R: Read + ?Sized, W: Write + ?Sized> SpecCopy for Copier<'_, '_, R, W> {
143     default fn copy(self) -> Result<u64> {
144         generic_copy(self.read, self.write)
145     }
146 }
147
148 impl<R: CopyRead, W: CopyWrite> SpecCopy for Copier<'_, '_, R, W> {
149     fn copy(self) -> Result<u64> {
150         let (reader, writer) = (self.read, self.write);
151         let r_cfg = reader.properties();
152         let w_cfg = writer.properties();
153
154         // before direct operations on file descriptors ensure that all source and sink buffers are empty
155         let mut flush = || -> crate::io::Result<u64> {
156             let bytes = reader.drain_to(writer, u64::MAX)?;
157             // BufWriter buffered bytes have already been accounted for in earlier write() calls
158             writer.flush()?;
159             Ok(bytes)
160         };
161
162         let mut written = 0u64;
163
164         if let (CopyParams(input_meta, Some(readfd)), CopyParams(output_meta, Some(writefd))) =
165             (r_cfg, w_cfg)
166         {
167             written += flush()?;
168             let max_write = reader.min_limit();
169
170             if input_meta.copy_file_range_candidate() && output_meta.copy_file_range_candidate() {
171                 let result = copy_regular_files(readfd, writefd, max_write);
172                 result.update_take(reader);
173
174                 match result {
175                     CopyResult::Ended(bytes_copied) => return Ok(bytes_copied + written),
176                     CopyResult::Error(e, _) => return Err(e),
177                     CopyResult::Fallback(bytes) => written += bytes,
178                 }
179             }
180
181             // on modern kernels sendfile can copy from any mmapable type (some but not all regular files and block devices)
182             // to any writable file descriptor. On older kernels the writer side can only be a socket.
183             // So we just try and fallback if needed.
184             // If current file offsets + write sizes overflow it may also fail, we do not try to fix that and instead
185             // fall back to the generic copy loop.
186             if input_meta.potential_sendfile_source() {
187                 let result = sendfile_splice(SpliceMode::Sendfile, readfd, writefd, max_write);
188                 result.update_take(reader);
189
190                 match result {
191                     CopyResult::Ended(bytes_copied) => return Ok(bytes_copied + written),
192                     CopyResult::Error(e, _) => return Err(e),
193                     CopyResult::Fallback(bytes) => written += bytes,
194                 }
195             }
196
197             if input_meta.maybe_fifo() || output_meta.maybe_fifo() {
198                 let result = sendfile_splice(SpliceMode::Splice, readfd, writefd, max_write);
199                 result.update_take(reader);
200
201                 match result {
202                     CopyResult::Ended(bytes_copied) => return Ok(bytes_copied + written),
203                     CopyResult::Error(e, _) => return Err(e),
204                     CopyResult::Fallback(0) => { /* use the fallback below */ }
205                     CopyResult::Fallback(_) => {
206                         unreachable!("splice should not return > 0 bytes on the fallback path")
207                     }
208                 }
209             }
210         }
211
212         // fallback if none of the more specialized syscalls wants to work with these file descriptors
213         match generic_copy(reader, writer) {
214             Ok(bytes) => Ok(bytes + written),
215             err => err,
216         }
217     }
218 }
219
220 #[rustc_specialization_trait]
221 trait CopyRead: Read {
222     /// Implementations that contain buffers (i.e. `BufReader`) must transfer data from their internal
223     /// buffers into `writer` until either the buffers are emptied or `limit` bytes have been
224     /// transferred, whichever occurs sooner.
225     /// If nested buffers are present the outer buffers must be drained first.
226     ///
227     /// This is necessary to directly bypass the wrapper types while preserving the data order
228     /// when operating directly on the underlying file descriptors.
229     fn drain_to<W: Write>(&mut self, _writer: &mut W, _limit: u64) -> Result<u64> {
230         Ok(0)
231     }
232
233     /// Updates `Take` wrappers to remove the number of bytes copied.
234     fn taken(&mut self, _bytes: u64) {}
235
236     /// The minimum of the limit of all `Take<_>` wrappers, `u64::MAX` otherwise.
237     /// This method does not account for data `BufReader` buffers and would underreport
238     /// the limit of a `Take<BufReader<Take<_>>>` type. Thus its result is only valid
239     /// after draining the buffers via `drain_to`.
240     fn min_limit(&self) -> u64 {
241         u64::MAX
242     }
243
244     /// Extracts the file descriptor and hints/metadata, delegating through wrappers if necessary.
245     fn properties(&self) -> CopyParams;
246 }
247
248 #[rustc_specialization_trait]
249 trait CopyWrite: Write {
250     /// Extracts the file descriptor and hints/metadata, delegating through wrappers if necessary.
251     fn properties(&self) -> CopyParams;
252 }
253
254 impl<T> CopyRead for &mut T
255 where
256     T: CopyRead,
257 {
258     fn drain_to<W: Write>(&mut self, writer: &mut W, limit: u64) -> Result<u64> {
259         (**self).drain_to(writer, limit)
260     }
261
262     fn taken(&mut self, bytes: u64) {
263         (**self).taken(bytes);
264     }
265
266     fn min_limit(&self) -> u64 {
267         (**self).min_limit()
268     }
269
270     fn properties(&self) -> CopyParams {
271         (**self).properties()
272     }
273 }
274
275 impl<T> CopyWrite for &mut T
276 where
277     T: CopyWrite,
278 {
279     fn properties(&self) -> CopyParams {
280         (**self).properties()
281     }
282 }
283
284 impl CopyRead for File {
285     fn properties(&self) -> CopyParams {
286         CopyParams(fd_to_meta(self), Some(self.as_raw_fd()))
287     }
288 }
289
290 impl CopyRead for &File {
291     fn properties(&self) -> CopyParams {
292         CopyParams(fd_to_meta(*self), Some(self.as_raw_fd()))
293     }
294 }
295
296 impl CopyWrite for File {
297     fn properties(&self) -> CopyParams {
298         CopyParams(FdMeta::NoneObtained, Some(self.as_raw_fd()))
299     }
300 }
301
302 impl CopyWrite for &File {
303     fn properties(&self) -> CopyParams {
304         CopyParams(FdMeta::NoneObtained, Some(self.as_raw_fd()))
305     }
306 }
307
308 impl CopyRead for TcpStream {
309     fn properties(&self) -> CopyParams {
310         // avoid the stat syscall since we can be fairly sure it's a socket
311         CopyParams(FdMeta::Socket, Some(self.as_raw_fd()))
312     }
313 }
314
315 impl CopyRead for &TcpStream {
316     fn properties(&self) -> CopyParams {
317         // avoid the stat syscall since we can be fairly sure it's a socket
318         CopyParams(FdMeta::Socket, Some(self.as_raw_fd()))
319     }
320 }
321
322 impl CopyWrite for TcpStream {
323     fn properties(&self) -> CopyParams {
324         // avoid the stat syscall since we can be fairly sure it's a socket
325         CopyParams(FdMeta::Socket, Some(self.as_raw_fd()))
326     }
327 }
328
329 impl CopyWrite for &TcpStream {
330     fn properties(&self) -> CopyParams {
331         // avoid the stat syscall since we can be fairly sure it's a socket
332         CopyParams(FdMeta::Socket, Some(self.as_raw_fd()))
333     }
334 }
335
336 impl CopyRead for UnixStream {
337     fn properties(&self) -> CopyParams {
338         // avoid the stat syscall since we can be fairly sure it's a socket
339         CopyParams(FdMeta::Socket, Some(self.as_raw_fd()))
340     }
341 }
342
343 impl CopyRead for &UnixStream {
344     fn properties(&self) -> CopyParams {
345         // avoid the stat syscall since we can be fairly sure it's a socket
346         CopyParams(FdMeta::Socket, Some(self.as_raw_fd()))
347     }
348 }
349
350 impl CopyWrite for UnixStream {
351     fn properties(&self) -> CopyParams {
352         // avoid the stat syscall since we can be fairly sure it's a socket
353         CopyParams(FdMeta::Socket, Some(self.as_raw_fd()))
354     }
355 }
356
357 impl CopyWrite for &UnixStream {
358     fn properties(&self) -> CopyParams {
359         // avoid the stat syscall since we can be fairly sure it's a socket
360         CopyParams(FdMeta::Socket, Some(self.as_raw_fd()))
361     }
362 }
363
364 impl CopyWrite for ChildStdin {
365     fn properties(&self) -> CopyParams {
366         CopyParams(FdMeta::Pipe, Some(self.as_raw_fd()))
367     }
368 }
369
370 impl CopyRead for ChildStdout {
371     fn properties(&self) -> CopyParams {
372         CopyParams(FdMeta::Pipe, Some(self.as_raw_fd()))
373     }
374 }
375
376 impl CopyRead for ChildStderr {
377     fn properties(&self) -> CopyParams {
378         CopyParams(FdMeta::Pipe, Some(self.as_raw_fd()))
379     }
380 }
381
382 impl CopyRead for StdinLock<'_> {
383     fn drain_to<W: Write>(&mut self, writer: &mut W, outer_limit: u64) -> Result<u64> {
384         let buf_reader = self.as_mut_buf();
385         let buf = buf_reader.buffer();
386         let buf = &buf[0..min(buf.len(), outer_limit.try_into().unwrap_or(usize::MAX))];
387         let bytes_drained = buf.len();
388         writer.write_all(buf)?;
389         buf_reader.consume(bytes_drained);
390
391         Ok(bytes_drained as u64)
392     }
393
394     fn properties(&self) -> CopyParams {
395         CopyParams(fd_to_meta(self), Some(self.as_raw_fd()))
396     }
397 }
398
399 impl CopyWrite for StdoutLock<'_> {
400     fn properties(&self) -> CopyParams {
401         CopyParams(FdMeta::NoneObtained, Some(self.as_raw_fd()))
402     }
403 }
404
405 impl CopyWrite for StderrLock<'_> {
406     fn properties(&self) -> CopyParams {
407         CopyParams(FdMeta::NoneObtained, Some(self.as_raw_fd()))
408     }
409 }
410
411 impl<T: CopyRead> CopyRead for Take<T> {
412     fn drain_to<W: Write>(&mut self, writer: &mut W, outer_limit: u64) -> Result<u64> {
413         let local_limit = self.limit();
414         let combined_limit = min(outer_limit, local_limit);
415         let bytes_drained = self.get_mut().drain_to(writer, combined_limit)?;
416         // update limit since read() was bypassed
417         self.set_limit(local_limit - bytes_drained);
418
419         Ok(bytes_drained)
420     }
421
422     fn taken(&mut self, bytes: u64) {
423         self.set_limit(self.limit() - bytes);
424         self.get_mut().taken(bytes);
425     }
426
427     fn min_limit(&self) -> u64 {
428         min(Take::limit(self), self.get_ref().min_limit())
429     }
430
431     fn properties(&self) -> CopyParams {
432         self.get_ref().properties()
433     }
434 }
435
436 impl<T: CopyRead> CopyRead for BufReader<T> {
437     fn drain_to<W: Write>(&mut self, writer: &mut W, outer_limit: u64) -> Result<u64> {
438         let buf = self.buffer();
439         let buf = &buf[0..min(buf.len(), outer_limit.try_into().unwrap_or(usize::MAX))];
440         let bytes = buf.len();
441         writer.write_all(buf)?;
442         self.consume(bytes);
443
444         let remaining = outer_limit - bytes as u64;
445
446         // in case of nested bufreaders we also need to drain the ones closer to the source
447         let inner_bytes = self.get_mut().drain_to(writer, remaining)?;
448
449         Ok(bytes as u64 + inner_bytes)
450     }
451
452     fn taken(&mut self, bytes: u64) {
453         self.get_mut().taken(bytes);
454     }
455
456     fn min_limit(&self) -> u64 {
457         self.get_ref().min_limit()
458     }
459
460     fn properties(&self) -> CopyParams {
461         self.get_ref().properties()
462     }
463 }
464
465 impl<T: CopyWrite> CopyWrite for BufWriter<T> {
466     fn properties(&self) -> CopyParams {
467         self.get_ref().properties()
468     }
469 }
470
471 fn fd_to_meta<T: AsRawFd>(fd: &T) -> FdMeta {
472     let fd = fd.as_raw_fd();
473     let file: ManuallyDrop<File> = ManuallyDrop::new(unsafe { File::from_raw_fd(fd) });
474     match file.metadata() {
475         Ok(meta) => FdMeta::Metadata(meta),
476         Err(_) => FdMeta::NoneObtained,
477     }
478 }
479
480 pub(super) enum CopyResult {
481     Ended(u64),
482     Error(Error, u64),
483     Fallback(u64),
484 }
485
486 impl CopyResult {
487     fn update_take(&self, reader: &mut impl CopyRead) {
488         match *self {
489             CopyResult::Fallback(bytes)
490             | CopyResult::Ended(bytes)
491             | CopyResult::Error(_, bytes) => reader.taken(bytes),
492         }
493     }
494 }
495
496 /// Invalid file descriptor.
497 ///
498 /// Valid file descriptors are guaranteed to be positive numbers (see `open()` manpage)
499 /// while negative values are used to indicate errors.
500 /// Thus -1 will never be overlap with a valid open file.
501 const INVALID_FD: RawFd = -1;
502
503 /// Linux-specific implementation that will attempt to use copy_file_range for copy offloading.
504 /// As the name says, it only works on regular files.
505 ///
506 /// Callers must handle fallback to a generic copy loop.
507 /// `Fallback` may indicate non-zero number of bytes already written
508 /// if one of the files' cursor +`max_len` would exceed u64::MAX (`EOVERFLOW`).
509 pub(super) fn copy_regular_files(reader: RawFd, writer: RawFd, max_len: u64) -> CopyResult {
510     use crate::cmp;
511
512     const NOT_PROBED: u8 = 0;
513     const UNAVAILABLE: u8 = 1;
514     const AVAILABLE: u8 = 2;
515
516     // Kernel prior to 4.5 don't have copy_file_range
517     // We store the availability in a global to avoid unnecessary syscalls
518     static HAS_COPY_FILE_RANGE: AtomicU8 = AtomicU8::new(NOT_PROBED);
519
520     syscall! {
521         fn copy_file_range(
522             fd_in: libc::c_int,
523             off_in: *mut libc::loff_t,
524             fd_out: libc::c_int,
525             off_out: *mut libc::loff_t,
526             len: libc::size_t,
527             flags: libc::c_uint
528         ) -> libc::ssize_t
529     }
530
531     match HAS_COPY_FILE_RANGE.load(Ordering::Relaxed) {
532         NOT_PROBED => {
533             // EPERM can indicate seccomp filters or an immutable file.
534             // To distinguish these cases we probe with invalid file descriptors which should result in EBADF if the syscall is supported
535             // and some other error (ENOSYS or EPERM) if it's not available
536             let result = unsafe {
537                 cvt(copy_file_range(INVALID_FD, ptr::null_mut(), INVALID_FD, ptr::null_mut(), 1, 0))
538             };
539
540             if matches!(result.map_err(|e| e.raw_os_error()), Err(Some(EBADF))) {
541                 HAS_COPY_FILE_RANGE.store(AVAILABLE, Ordering::Relaxed);
542             } else {
543                 HAS_COPY_FILE_RANGE.store(UNAVAILABLE, Ordering::Relaxed);
544                 return CopyResult::Fallback(0);
545             }
546         }
547         UNAVAILABLE => return CopyResult::Fallback(0),
548         _ => {}
549     };
550
551     let mut written = 0u64;
552     while written < max_len {
553         let bytes_to_copy = cmp::min(max_len - written, usize::MAX as u64);
554         // cap to 1GB chunks in case u64::MAX is passed as max_len and the file has a non-zero seek position
555         // this allows us to copy large chunks without hitting EOVERFLOW,
556         // unless someone sets a file offset close to u64::MAX - 1GB, in which case a fallback would be required
557         let bytes_to_copy = cmp::min(bytes_to_copy as usize, 0x4000_0000usize);
558         let copy_result = unsafe {
559             // We actually don't have to adjust the offsets,
560             // because copy_file_range adjusts the file offset automatically
561             cvt(copy_file_range(reader, ptr::null_mut(), writer, ptr::null_mut(), bytes_to_copy, 0))
562         };
563
564         match copy_result {
565             Ok(0) if written == 0 => {
566                 // fallback to work around several kernel bugs where copy_file_range will fail to
567                 // copy any bytes and return 0 instead of an error if
568                 // - reading virtual files from the proc filesystem which appear to have 0 size
569                 //   but are not empty. noted in coreutils to affect kernels at least up to 5.6.19.
570                 // - copying from an overlay filesystem in docker. reported to occur on fedora 32.
571                 return CopyResult::Fallback(0);
572             }
573             Ok(0) => return CopyResult::Ended(written), // reached EOF
574             Ok(ret) => written += ret as u64,
575             Err(err) => {
576                 return match err.raw_os_error() {
577                     // when file offset + max_length > u64::MAX
578                     Some(EOVERFLOW) => CopyResult::Fallback(written),
579                     Some(ENOSYS | EXDEV | EINVAL | EPERM | EOPNOTSUPP | EBADF) => {
580                         // Try fallback io::copy if either:
581                         // - Kernel version is < 4.5 (ENOSYS¹)
582                         // - Files are mounted on different fs (EXDEV)
583                         // - copy_file_range is broken in various ways on RHEL/CentOS 7 (EOPNOTSUPP)
584                         // - copy_file_range file is immutable or syscall is blocked by seccomp¹ (EPERM)
585                         // - copy_file_range cannot be used with pipes or device nodes (EINVAL)
586                         // - the writer fd was opened with O_APPEND (EBADF²)
587                         //
588                         // ¹ these cases should be detected by the initial probe but we handle them here
589                         //   anyway in case syscall interception changes during runtime
590                         // ² actually invalid file descriptors would cause this too, but in that case
591                         //   the fallback code path is expected to encounter the same error again
592                         assert_eq!(written, 0);
593                         CopyResult::Fallback(0)
594                     }
595                     _ => CopyResult::Error(err, written),
596                 };
597             }
598         }
599     }
600     CopyResult::Ended(written)
601 }
602
603 #[derive(PartialEq)]
604 enum SpliceMode {
605     Sendfile,
606     Splice,
607 }
608
609 /// performs splice or sendfile between file descriptors
610 /// Does _not_ fall back to a generic copy loop.
611 fn sendfile_splice(mode: SpliceMode, reader: RawFd, writer: RawFd, len: u64) -> CopyResult {
612     static HAS_SENDFILE: AtomicBool = AtomicBool::new(true);
613     static HAS_SPLICE: AtomicBool = AtomicBool::new(true);
614
615     syscall! {
616         fn splice(
617             srcfd: libc::c_int,
618             src_offset: *const i64,
619             dstfd: libc::c_int,
620             dst_offset: *const i64,
621             len: libc::size_t,
622             flags: libc::c_int
623         ) -> libc::ssize_t
624     }
625
626     match mode {
627         SpliceMode::Sendfile if !HAS_SENDFILE.load(Ordering::Relaxed) => {
628             return CopyResult::Fallback(0);
629         }
630         SpliceMode::Splice if !HAS_SPLICE.load(Ordering::Relaxed) => {
631             return CopyResult::Fallback(0);
632         }
633         _ => (),
634     }
635
636     let mut written = 0u64;
637     while written < len {
638         // according to its manpage that's the maximum size sendfile() will copy per invocation
639         let chunk_size = crate::cmp::min(len - written, 0x7ffff000_u64) as usize;
640
641         let result = match mode {
642             SpliceMode::Sendfile => {
643                 cvt(unsafe { libc::sendfile(writer, reader, ptr::null_mut(), chunk_size) })
644             }
645             SpliceMode::Splice => cvt(unsafe {
646                 splice(reader, ptr::null_mut(), writer, ptr::null_mut(), chunk_size, 0)
647             }),
648         };
649
650         match result {
651             Ok(0) => break, // EOF
652             Ok(ret) => written += ret as u64,
653             Err(err) => {
654                 return match err.raw_os_error() {
655                     Some(ENOSYS | EPERM) => {
656                         // syscall not supported (ENOSYS)
657                         // syscall is disallowed, e.g. by seccomp (EPERM)
658                         match mode {
659                             SpliceMode::Sendfile => HAS_SENDFILE.store(false, Ordering::Relaxed),
660                             SpliceMode::Splice => HAS_SPLICE.store(false, Ordering::Relaxed),
661                         }
662                         assert_eq!(written, 0);
663                         CopyResult::Fallback(0)
664                     }
665                     Some(EINVAL) => {
666                         // splice/sendfile do not support this particular file descriptor (EINVAL)
667                         assert_eq!(written, 0);
668                         CopyResult::Fallback(0)
669                     }
670                     Some(os_err) if mode == SpliceMode::Sendfile && os_err == EOVERFLOW => {
671                         CopyResult::Fallback(written)
672                     }
673                     _ => CopyResult::Error(err, written),
674                 };
675             }
676         }
677     }
678     CopyResult::Ended(written)
679 }