Linux copy_file_range Separates Copy Semantics From Copy Implementation

A successful copy_file_range() call reports a byte count, not a promise about the physical path those bytes took. Linux can satisfy the request through filesystem-specific acceleration, an in-kernel transfer path, or another implementation permitted by the active filesystem interfaces. The application receives a range-copy operation with defined offset and return-value semantics; it does not receive a guarantee that storage blocks were physically duplicated.

That boundary matters in file replication, backup staging, cache population, and local object movement. Code that treats the call as merely a faster read() plus write() loop can miss properties that are visible at the API boundary: short copies are valid, sparse holes can become allocated data, cross-filesystem support is conditional, and filesystem implementations may exploit copy-on-write sharing.

The contract is a range operation, not a transfer topology

copy_file_range() accepts an input descriptor, an optional input offset pointer, an output descriptor, an optional output offset pointer, a requested byte count, and flags. Current Linux interfaces require flags to be zero.

When an offset pointer is null, the corresponding open file description’s current offset participates in the operation and advances by the number of bytes copied. When a non-null offset pointer is supplied, the pointed-to offset advances instead, while the descriptor’s file offset remains unchanged.

This distinction makes the operation usable in two different coordination models. A caller can deliberately consume shared file position, or it can carry explicit range state without mutating that position. Those models have different concurrency consequences when descriptors refer to a shared open file description.

The kernel also permits source and destination descriptors to refer to the same file, provided the requested ranges do not overlap. The prohibition on overlap is part of the operation’s contract rather than a recommendation about application structure.

A positive return value can be smaller than the request

Success does not imply that the full requested count moved. The return value is the number of bytes copied, and Linux explicitly permits that value to be smaller than the requested size.

A complete-copy routine therefore needs a state machine around the syscall rather than a single success check:

while (remaining > 0) {
    ssize_t n = copy_file_range(src, &in_off, dst, &out_off,
                                remaining, 0);
    if (n > 0) {
        remaining -= (size_t)n;
        continue;
    }
    if (n == 0)
        break;
    /* inspect errno and apply the caller's error policy */
}

A zero return means no bytes were copied. When the source position is at or beyond end of file, zero is the expected result. A caller that has an independently established source length can distinguish an expected end boundary from an unexpectedly incomplete operation using its own range accounting.

This is a general systems boundary: syscall completion and application-level completion are separate facts. The kernel reports progress for one invocation. The caller decides whether that progress satisfies the larger copy transaction.

Copy acceleration can change physical storage without changing logical bytes

The interface gives filesystems an opportunity to accelerate the copy. A filesystem may support mechanisms such as reflink-style copy-on-write sharing, where source and destination initially reference shared physical storage rather than receiving two immediately independent block copies.

From the application’s byte-oriented view, that can still satisfy the requested range copy. Subsequent writes are subject to the filesystem’s copy-on-write behavior. The optimization is therefore below the logical content boundary but can remain visible through storage allocation, quota effects, fragmentation, and filesystem-specific inspection tools.

Code must not infer a particular physical representation from a successful call. The opposite assumption is also unsafe: applications cannot require reflink behavior merely because the interface permits a filesystem to use it. A deployment that needs block sharing as a semantic requirement should use an interface whose contract explicitly requests cloning and should handle unsupported filesystems accordingly.

copy_file_range() is consequently an abstraction over transfer strategy, not an API for selecting one transfer strategy.

Sparse input does not imply sparse output

A sparse file can contain logical regions that read as zeroes without corresponding allocated data blocks. Copying its byte range does not necessarily preserve that allocation topology.

Linux documentation explicitly allows copy_file_range() to expand holes in the source range. The destination can therefore contain the same logical bytes while occupying a different physical layout.

This separates content equivalence from representation equivalence. For many consumers, identical bytes are the required property and sparse layout is incidental. For disk-image tooling, archival systems, or quota-sensitive pipelines, preserving holes may be part of the required result.

A sparse-aware copier can discover data and hole regions with filesystem interfaces such as SEEK_DATA and SEEK_HOLE, where supported, and issue copy operations only for data extents. That design has its own platform and filesystem conditions; those seek modes are not a universal description of every storage backend.

The relevant design question is therefore not whether the destination reads correctly. It is whether the application contract also includes allocation structure.

Cross-filesystem behavior is conditional

The syscall’s historical behavior across filesystem boundaries has changed. On current Linux semantics, cross-filesystem copying is not an unconditional capability. Support depends on the source and destination filesystem types and their implementation of the operation.

Since Linux 5.19 semantics, an unsupported cross-filesystem combination can fail with EXDEV, while a filesystem that does not support the operation can report EOPNOTSUPP. Applications that target varied kernels or storage deployments need an explicit fallback policy if byte copying must still proceed.

That fallback can use ordinary buffered I/O, but it is not semantically invisible. The fallback must reproduce the application’s required offset handling, partial-write handling, metadata policy, sparse-file policy, cancellation behavior, and error reporting. Replacing one syscall with a loop is easy only when the application’s copy contract is narrow.

Older kernel behavior also matters for compatibility claims. Linux 5.3 through 5.18 had a generic cross-filesystem path with documented problems on some virtual filesystems. A program supporting those kernels should not project current behavior backward without accounting for that history.

Data movement does not copy file metadata

The operation copies a range of file data. It is not a complete file replication primitive.

Ownership, mode bits, timestamps, extended attributes, access-control lists, security labels, and other metadata have separate interfaces and separate permission rules. The destination file already exists as an object opened for writing; copy_file_range() modifies data within it.

This separation is useful because many systems intentionally want data movement without identity replication. A cache file should not necessarily inherit source ownership. A staging file may deliberately have tighter permissions. Conversely, backup or migration software that promises metadata fidelity must build that fidelity outside the range-copy operation.

Durability is separate as well. A successful byte copy establishes syscall-level completion under the relevant filesystem semantics; it does not by itself replace explicit persistence operations required by an application’s crash-consistency protocol.

Destination state can be partially changed on failure

A multi-call copy can fail after earlier calls have already modified the destination. Even one invocation can return positive progress smaller than requested, leaving the destination with a valid copied prefix of the attempted range.

Applications that require all-or-nothing publication need a higher-level transaction boundary. A common file-oriented design writes into a separate staging object, verifies the required result, establishes the required durability boundary, then publishes through a namespace operation appropriate to the platform and deployment.

That pattern does not make the range copy itself transactional. It moves atomic visibility to another mechanism. The distinction is important during recovery: an incomplete staging object and a partially overwritten live object create very different failure surfaces.

The destination range also overwrites existing data where the copy succeeds. A caller that reuses a longer destination file must separately decide whether bytes beyond the copied region should remain, be truncated, or be replaced through a new file object.

Explicit offsets isolate range accounting from shared file position

Passing offset pointers can reduce accidental coupling through mutable file position. This is especially relevant when multiple operations act on the same open file description.

With explicit offsets, range ownership lives in caller-managed values. The syscall updates those values according to copied progress but does not change the descriptor’s current file offset. That makes the source and destination ranges visible in application state and avoids using seek position as an implicit coordination channel.

It does not make concurrent writes safe by itself. Two workers can still target overlapping destination ranges if the application assigns them incorrectly. Filesystem-level interactions with concurrent mutation of source data also remain governed by the relevant filesystem and syscall semantics.

Explicit offsets remove one shared state variable; they do not create a transaction or snapshot.

The useful abstraction boundary is logical progress

copy_file_range() is strongest when treated as a logical range-copy primitive whose implementation is intentionally open. The stable facts are the descriptors, offsets, requested count, returned progress, error conditions, and filesystem constraints documented for the running platform.

Physical block sharing, internal transfer paths, and sparse allocation can vary without violating the byte-level operation. Complete-file semantics such as metadata replication, crash durability, atomic publication, and fallback behavior remain application responsibilities unless another interface explicitly supplies them.

That separation lets the kernel and filesystem optimize data movement while keeping the caller’s correctness obligations visible. The syscall can remove a userspace transfer buffer from the common path; it cannot remove the need to define what a completed copy means for the system that requested it.