Block cloning lets ZFS create a file — or extend an existing one — that references another file's existing data blocks instead of copying them. Nothing is read from or written to the underlying vdevs; only block pointers and a reference count change. Writing to a cloned region later behaves exactly like any other copy-on-write: ZFS allocates a fresh block for the changed data and leaves every other reference to the original block untouched. This is ZFS's version of what XFS, Btrfs, and Windows ReFS call a reflink.
Note
"Clone" already meant something in ZFS. zfs clone creates a new dataset or zvol from a snapshot, sharing blocks with that snapshot until they diverge — it's dataset-level and has existed since early ZFS. Block cloning is unrelated: it operates on individual files inside a dataset, needs no snapshot, and is invoked through ordinary copy syscalls rather than the zfs command. The rest of this page is about the latter.
Block cloning landed in OpenZFS 2.2.0 (October 2023), authored by Pawel Dawidek as PR #13392, after being proposed at the 2020 OpenZFS Developer Summit.
ZFS has four different ways to make one block do the work of many. Only one of them is block cloning.
| Mechanism | Granularity | Triggered by | Data I/O | Shared until |
|---|---|---|---|---|
| Regular copy | Whole file | cp, rsync, any copy | Full read + full write | Never — fully duplicated |
zfs clone | Dataset / zvol | zfs clone from a snapshot | None | Either side writes past the snapshot |
| Deduplication | Block, at write time | Any write, when dedup=on | Full read + write, plus a DDT hash lookup per write | Matching block is freed |
| Block cloning | Block, on request | copy_file_range, FICLONE(RANGE), reflink cp | None | Either side writes to that block |
Every cloned block gets an entry in the Block Reference Table (BRT): a vdev, an offset, and a reference count. It's a much simpler structure than the dedup table (DDT), and the two behave very differently in practice:
dedup is on, whether or not that particular write turns out to match anything already on disk. That lookup, plus keeping a large hash table resident in memory, is most of why dedup has a reputation for wrecking write throughput.One asymmetry runs the other way. Block pointers carry a spare bit (the "D" bit) that flags whether a block is in the DDT, so freeing a non-deduped block skips the DDT lookup entirely. There's no equivalent bit for block cloning, so every block free has to check the BRT regardless of whether that particular block was ever cloned. Cloning is free on the read and write side; it isn't quite free on the free path.
Modifying a cloned block is ordinary copy-on-write: ZFS allocates a new block for the changed data, decrements the BRT reference count on the original, and every other file still pointing at that original block is unaffected.
Block cloning is opt-in per operation — nothing clones automatically just because two files happen to contain the same bytes. A program has to explicitly ask, through one of:
copy_file_range(2) — the primary Linux syscall; modern cp, and a growing number of other copy tools, call this under the hoodFICLONE / FICLONERANGE ioctls — the same ioctls XFS and Btrfs use for their own reflinkscp --reflink=always or --reflink=auto — GNU coreutils has defaulted to --reflink=auto since 9.2, so a stock cp on a current distro tries to clone first and silently falls back to a normal copy if it can't; FreeBSD's /bin/cp gained the same behavior in 13.0.zfs/snapshot/ directory — this goes through the same reflink-capable cp, so restoring a large file from a snapshot doesn't temporarily double its space usageCOPY operation) and SMB (FSCTL_SRV_COPYCHUNK, via Samba) — a client asking the server to copy a file on its own disk can land on block cloning instead of streaming the data twice over the wire$ cp --reflink=always /tank/vm/base.qcow2 /tank/vm/clone.qcow2
$ cp /tank/movies/.zfs/snapshot/nightly/movie.mkv /tank/movies/
ZFS clones through this syscall whenever the source range, destination range, and both files' properties satisfy the requirements below — otherwise the kernel transparently falls back to a read/write copy loop.
ssize_t copy_file_range(int fd_in, off_t *off_in,
int fd_out, off_t *off_out,
size_t len, unsigned int flags);
FICLONERANGE takes an explicit source/destination range via a struct passed to ioctl(2):
struct file_clone_range {
__s64 src_fd;
__u64 src_offset;
__u64 src_length;
__u64 dest_offset;
};
A clone request falls back to a real copy — sometimes silently — unless all of the following hold:
recordsize property.zfs_bclone_wait_dirty below.Tip
If a clone is silently doing a full copy, check recordsize mismatch and cross-pool destinations first — they account for most cases that aren't simply zfs_bclone_enabled=0.
| Name | Type | Default | Effect |
|---|---|---|---|
feature@block_cloning |
Pool feature | — | Read/write-compatible. Becomes active the first time any block on the pool is cloned; drops back to enabled once the last clone reference pool-wide is freed. |
feature@block_cloning_endian |
Pool feature | — | Corrective feature (com.truenas:block_cloning_endian), landed for OpenZFS 2.4. Original BRT ZAP entries were stored as eight separate one-byte fields instead of one eight-byte field, which isn't portable between big- and little-endian systems. New pools get the corrected layout automatically; pools created under the original implementation keep it until rebuilt. |
zfs_bclone_enabled |
Module parameter | 1 on current releases |
Gates whether copy_file_range / FICLONE / FICLONERANGE are allowed to clone at all. Linux: /sys/module/zfs/parameters/zfs_bclone_enabled. FreeBSD: sysctl vfs.zfs.bclone_enabled. |
zfs_bclone_wait_dirty |
Module parameter | 1 |
Behavior when the requested range has dirty data. 1: wait for the next txg sync, then clone. 0: fail the clone immediately instead of waiting. |
# cat /sys/module/zfs/parameters/zfs_bclone_enabled
1
# echo "options zfs zfs_bclone_enabled=1" > /etc/modprobe.d/zfs.conf
Danger
OpenZFS 2.2.0 (October 2023) shipped block cloning enabled by default. Within weeks, Gentoo users reported files coming back with chunks silently replaced by zeroes — real data corruption, tracked as issue #15526. OpenZFS 2.2.1 responded immediately by switching block cloning off by default via the new zfs_bclone_enabled parameter, rather than shipping a root-cause fix. That fix — for an incorrect dirty-dnode check, only exposed in practice by behavioral changes in coreutils 9.x's cp — landed in 2.2.2 (#15571). Even after the fix, zfs_bclone_enabled stayed off by default through the rest of the 2.2.x series as a deliberate stabilization period.
Current OpenZFS defaults zfs_bclone_enabled back to 1 — but confirm the actual value on your system before relying on it; distributions can and do override module defaults. Any host still running a 2.2.x release should be treated as off-by-default and opt-in only, regardless of what upstream ships elsewhere.
Pool-level properties expose clone savings without walking the filesystem for BRT references:
| Property | Meaning |
|---|---|
bcloneused | Total logical size of all blocks currently referenced by a clone. |
bclonesaved | Space avoided by cloning instead of copying — the gap between logical and physical footprint of cloned data. |
bcloneratio | bclonesaved expressed as a ratio; 2.00x means clones currently occupy half the space a full copy would have. |
$ zpool get bcloneused,bclonesaved,bcloneratio tank
NAME PROPERTY VALUE SOURCE
tank bcloneused 100M -
tank bclonesaved 100M -
tank bcloneratio 2.00x -
Warn
The BRT is local to a pool and doesn't travel in a send stream. Every clone gets fully materialized as independent data on the receiving side — there's no "send only the reference." A dataset that looks compact on disk because it's built from clones (ten near-identical VM images sharing most of their blocks, for example) can produce a send stream, and consume space on the receiving pool, far larger than its bcloneratio suggests. Size a replication target off zfs list USED and an observed incremental stream size, not off how little space the source appears to use.
.zfs/snapshot/. A plain cp out of a snapshot clones instead of duplicating, so pulling a large file back into the live filesystem doesn't temporarily double its space usage.Warn
Production issue #17485 reported reflink failures during high-throughput Veeam synthetic-full runs specifically when zfs_bclone_wait_dirty was forced to 0 — clones failed outright under sustained load instead of degrading gracefully. Leave it at its default of 1 unless a specific low-latency requirement says otherwise.