copy_file_range() asks Linux to copy bytes between regular files without requiring the application to shuttle those bytes through a user-space buffer. The call defines a byte-range operation, but it does not prescribe the physical transfer mechanism. A filesystem can perform ordinary data movement, use a copy-on-write sharing mechanism such as reflink, or employ another supported acceleration path while preserving the visible file contents required by the operation.

That separation is the central API boundary. Applications specify source and destination ranges and observe the returned byte count. The kernel and filesystem retain latitude over the mechanism used to realize the copy.

A successful call can copy fewer bytes than requested

The size argument is an upper bound, not a promise that one call transfers exactly that many bytes. A successful copy_file_range() returns the number of bytes actually copied, and that value can be smaller than size. A return value of zero indicates that no bytes were copied; when the source position is at or beyond end of file, zero is the documented result.

Code that requires a complete range therefore needs a progress loop rather than a single-call equality assumption:

size_t remaining = length;

while (remaining > 0) {
    ssize_t n = copy_file_range(src, NULL, dst, NULL, remaining, 0);
    if (n > 0) {
        remaining -= (size_t)n;
        continue;
    }
    if (n == 0) {
        break;
    }
    if (errno == EINTR) {
        continue;
    }
    /* handle error */
    break;
}

The loop still needs an application-level policy for an early zero result. If the requested logical contract says that length bytes must exist in the source, reaching end of file before remaining becomes zero is an incomplete copy even though the final system call itself did not fail.

This distinction resembles other byte-oriented I/O interfaces: syscall success reports progress according to the syscall contract, while completeness belongs to the operation assembled by the caller.

Offset pointers select who owns position state

off_in and off_out determine whether the operation uses each open file description’s current file offset or explicit caller-managed positions.

When off_in is NULL, copying begins at the current source file offset and advances that offset by the number of bytes copied. The destination side behaves analogously when off_out is NULL.

When a non-null pointer is supplied, the pointed-to offset selects the byte position. The syscall updates that pointed-to value as bytes are copied, but it does not change the corresponding file offset.

The two modes establish different concurrency contracts:

NULL offset
fd -> open file description -> shared current position

explicit offset
caller state -> off_t value -> selected byte range
fd current position remains unchanged

A duplicated descriptor can share an open file description with another descriptor. In the NULL form, operations using that shared description interact through its current offset. Explicit offsets avoid using that mutable position for the range selection, which can make ownership clearer when independent components operate on distinct ranges.

Explicit offsets do not make arbitrary concurrent file mutation safe. They only change position accounting. If another actor modifies source or destination contents during the operation, the resulting application semantics still depend on the filesystem and on whatever synchronization contract surrounds those mutations.

The destination range is overwritten, not appended

The destination position identifies where copied bytes replace existing bytes. copy_file_range() is not an append primitive, and Linux rejects a destination open file description carrying O_APPEND.

This constraint prevents the API from having two competing destination-position rules. A caller either uses the destination file offset or an explicit off_out; append mode would instead require writes to be placed according to end-of-file semantics.

If the selected destination range extends beyond the existing end of file and the operation succeeds, the file can grow accordingly. Existing bytes outside the written range are not implied to change merely because a range copy occurred.

The range model also permits source and destination descriptors to refer to the same file, but source and destination ranges must not overlap. Linux rejects overlapping ranges in that case with EINVAL. The syscall is therefore not a general in-place range-moving primitive comparable to a memory operation that explicitly defines overlap behavior.

In-kernel copying does not imply one physical implementation

The user-visible distinction from a conventional read() plus write() loop is that data need not make the extra round trip from kernel space into a user buffer and back. That does not mean every successful call follows the same internal data path.

Linux deliberately gives filesystems an opportunity to accelerate the operation. A filesystem can use reflink-style block sharing, in which source and destination initially refer to shared physical storage and later writes trigger copy-on-write behavior. A network filesystem can potentially use server-side copying. Another filesystem may perform a more conventional copy.

Consequently, an application should not derive a storage-layout guarantee from syscall success:

visible contract:
source bytes -> destination bytes

possible implementation paths:
physical copy
reflink / copy-on-write sharing
filesystem-specific acceleration

The destination’s logical contents are the interface. Whether physical blocks were duplicated immediately is an implementation property unless a separate API supplies a stronger storage-layout contract.

This also means performance characteristics are not portable invariants. A range copy that is metadata-heavy on one filesystem can require substantial data I/O on another. The API enables acceleration; it does not guarantee a particular acceleration technique or cost profile.

Sparse source layout is not preserved automatically

A sparse file contains logical regions that read as zeroes without necessarily having physical data blocks allocated for every byte. copy_file_range() operates on byte ranges, not on an abstract promise to reproduce the source’s hole map.

The Linux documentation explicitly notes that copying a sparse file can expand holes in the copied range. Software for which sparse allocation is part of the required result can inspect data and hole regions with lseek() using SEEK_DATA and SEEK_HOLE, where supported, and copy data extents selectively.

That creates two separate notions of equivalence:

content equivalence:
reads return the intended bytes

allocation equivalence:
holes and allocated regions retain intended structure

copy_file_range() directly serves the first notion. The second requires additional filesystem-aware handling when it matters to the application.

A byte-for-byte verification can therefore succeed even when disk-space consumption differs from the source. Storage accounting and logical contents are related properties, but they are not identical.

Cross-filesystem support is conditional

The syscall’s cross-filesystem behavior has changed across Linux versions. Current documented semantics for Linux 5.19 and later permit cross-filesystem copying when the source and destination filesystems are of the same type and that filesystem implements the operation. Unsupported combinations can fail with EXDEV or EOPNOTSUPP according to the applicable condition.

This makes support a capability that software should treat as conditional rather than universal. A program that needs broad copy compatibility can attempt copy_file_range() and use another copy path when the failure indicates that the optimized operation is unavailable.

Such fallback logic needs to preserve the same offset and partial-progress semantics as the primary path. If an initial call has already copied bytes before a later call fails, blindly restarting a fallback from the original positions can duplicate or overwrite data incorrectly. The transition must begin from the remaining source and destination positions.

The libc boundary also matters. Modern glibc does not emulate the operation in user space when the kernel lacks the system call; on such a kernel the wrapper can fail with ENOSYS. Application fallback remains an application responsibility in that environment.

Copy completion is separate from persistence

A positive return value says that bytes were copied according to the system call’s I/O semantics. It does not by itself establish crash durability for the destination file or for directory metadata associated with a newly created destination pathname.

If an application requires persistence across a power loss or kernel crash, it needs the relevant synchronization protocol for the target filesystem and update pattern. That can include synchronization of file data and, when namespace changes are involved, directory metadata. The exact durability contract is separate from the range-copy mechanism.

This boundary matters for publication workflows. A process can copy data into a temporary file, validate it, synchronize it as required, and then publish it through a namespace operation. copy_file_range() can implement the data-transfer phase without absorbing the durability or publication guarantees of the surrounding protocol.

Range-copy APIs expose mechanism without owning the larger transaction

copy_file_range() is narrow by design. It moves up to a requested number of bytes, reports actual progress, and gives the kernel and filesystem room to select an efficient implementation. Offset arguments let the caller choose between shared file-position state and explicit range state.

Properties outside that boundary remain separate: complete-range enforcement, sparse-layout preservation, fallback behavior, concurrent mutation policy, crash durability, and atomic publication all require their own contracts.

That separation is useful precisely because storage systems have multiple layers of observable state. A file can have correct bytes yet different allocation, a completed copy can still be non-durable, and an accelerated copy can expose the same logical contents as a physical one. Treating those dimensions independently keeps the syscall’s guarantee precise while allowing filesystem-specific optimization beneath it.