copy_file_range: file-to-file copies that never leave the kernel
Published by RodHat · Updated

Every cp you have ever run did the same thing: read() from the source into a userspace buffer, then write() that buffer to the destination. The page cache already had the data. You just hauled it into your process and hauled it back. Two copies, neither necessary.
sendfile(2) fixed this for the file-to-socket case and only that case. splice(2) did the general version but stuck you with a mandatory pipe in the middle: you cannot go file-to-file directly, only file-to-pipe and pipe-to-file. If you wanted to copy a file to another file without touching userspace, you were out of options until 2016.
copy_file_range(2) landed in Linux 4.5. File in, file out, no pipe, no userspace buffer.
The syscall
#define _GNU_SOURCE
#include <unistd.h>
ssize_t copy_file_range(int fd_in, off64_t *off_in,
int fd_out, off64_t *off_out,
size_t len, unsigned int flags);
glibc 2.27 added the libc wrapper. On older glibc, call it with syscall(SYS_copy_file_range, ...). flags must be 0; the kernel doesn’t define any.
The two offset pointers are nullable. Pass NULL for off_in and the read position advances like a normal read(). Pass a pointer and the kernel reads from that offset without touching fd_in’s file position, same as pread(2). Same logic for off_out. If you want cp-style sequential copies, pass both as NULL. If you want random-access copies, pass pointers and manage the offsets yourself.
The return value is bytes copied, not a boolean. It can return less than len, exactly like write(). Loop on it:
ssize_t copy_file(int fd_in, int fd_out, size_t total)
{
size_t left = total;
while (left > 0) {
ssize_t n = copy_file_range(fd_in, NULL, fd_out, NULL, left, 0);
if (n < 0) return -1;
if (n == 0) break; /* EOF or sparse hole */
left -= n;
}
return total - left;
}
You do not know the size upfront in the general case. Stat the source first, then loop with whatever size you got from st_size. The statx post covers querying sizes and file attributes in one call if you want more than just the size.
When it goes faster than “just the kernel”
On a plain local ext4 or xfs, copy_file_range copies within the page cache without touching userspace. That is the baseline benefit: one copy instead of two.
Two cases are better:
NFS 4.2. If both fd_in and fd_out are on an NFS 4.2 mount, the kernel sends a CLONE or COPY operation to the NFS server. The data never crosses the network at all; the server duplicates it. A 10 GB file copy that would otherwise transfer 20 GB over the wire (read + write) now takes roughly zero bandwidth and finishes at server-disk speed.
btrfs. If both fds are on the same btrfs filesystem, copy_file_range uses reflink: the destination file shares extents with the source at the filesystem level. The copy is instantaneous regardless of file size. Dirty writes to either file get copy-on-write treatment afterwards. Same behavior you get from cp --reflink=always on btrfs, but from a syscall instead of a subprocess.
The kernel selects the right path automatically. You don’t opt in; you just call copy_file_range and let it do what it can.
The cross-filesystem caveat
Early kernels (before 5.3) returned EXDEV on cross-filesystem copies and left you to fall back manually. Since Linux 5.3, the kernel handles cross-filesystem copies internally: if the fast path isn’t available, it does the read-into-page-cache-and-write work itself, staying in kernel space throughout. You still never go through userspace.
The practical implication: on a 5.3+ kernel, copy_file_range is always at least as good as a read/write loop, and often much better. On older kernels, check for EXDEV and fall back.
ssize_t n = copy_file_range(fd_in, NULL, fd_out, NULL, len, 0);
if (n < 0 && errno == EXDEV) {
/* kernel < 5.3: fall back to read/write */
n = fallback_copy(fd_in, fd_out, len);
}
What copy_file_range cannot do
It does not copy directory trees. It does not preserve ownership, permissions, or extended attributes; those are separate syscalls. It also cannot copy from fd_in == fd_out with overlapping ranges (you get EINVAL). For overlapping same-file moves, memmove-style, there is no single syscall that handles it.
It also does not help with sockets or pipes as either end. Both descriptors must be regular files. For file-to-socket zero-copy, sendfile(2) is the right call. For anything with a pipe in the middle, splice(2) is what you want. The splice and tee post covers those.
FreeBSD
copy_file_range(2) landed in FreeBSD 13.0. The interface is identical to Linux’s. On ZFS, it uses the same reflink mechanism when both files are on the same dataset. On UFS, it copies within the kernel buffer cache.
The man page is copy_file_range(2) on man7.org. The NFS 4.2 CLONE/COPY behavior comes from RFC 7862, which is worth a skim if you are doing NFS storage work. Most of this code is in fs/copy_file_range.c in the kernel tree, which is short enough to read in an afternoon.