[PATCH 2/2] btrfs: allow reflinking from checksummed files into nodatasum files
From: Daan De Meyer via B4 Relay
Date: Sun Jul 12 2026 - 10:26:00 EST
From: Daan De Meyer <daan@xxxxxxxxxxxx>
Cloning and deduplication have always been rejected when the source and
destination inodes differ in their NODATASUM flag, making it impossible
to reflink data from a regular, checksummed file into a NODATACOW file.
This is a long-standing annoyance for use cases like copying VM images
into chattr +C directories, where cp --reflink=auto silently degrades
to a full data copy, as previously reported.
The restriction is only needed in one direction. Checksum items live in
the csum tree keyed by disk bytenr and are shared by every inode that
references an extent, and a clone operation copies no csum items at
all. Therefore:
- Reflinking from a NODATASUM file into a checksummed file would make
the destination refer to extents that have no checksums, and reads of
those extents would fail with -EIO. Creating the missing checksums at
clone time is not an option either: besides making the clone read and
checksum the full range, the checksums would be shared with the
source file, whose NOCOW behavior would then silently break, as
writes never go in place for extents that have checksums. This
direction remains rejected.
- Reflinking from a checksummed file into a NODATASUM file is safe. The
destination inode has NODATASUM set, so reads through it never verify
checksums, and the on-disk state it creates, a NODATASUM inode
referencing extents that have checksums, is already handled correctly
everywhere:
* can_nocow_file_extent() forces COW for any range that has
checksums, so in-place writes can never invalidate the checksums
shared with the source, even after the source file is deleted and
the extents are no longer shared. The first write to a cloned range
COWs into a fresh extent without checksums, after which NOCOW
behavior resumes. This is the same "COW once" behavior that NOCOW
files already have after a snapshot.
* fsync of the destination logs no csum items, and log replay only
deletes csums that were found in the log, so replaying the
destination cannot remove the checksums of the source's extents.
* Scrub, balance and device replace operate on the csum tree per
extent and do not consult inode flags. Relocation already tolerates
per-extent mixed checksum state via EXTENT_NODATASUM.
* Swap file activation rejects files with checksummed extents as of
the previous patch.
Relax the check in btrfs_remap_file_range_prep() to only reject the
NODATASUM source -> checksummed destination direction. This applies to
both clone and dedupe, which share the prep code.
Link: https://lore.kernel.org/all/CA+H1V9zNSiJgXj6w8i2syhm_4qeaxkYPZHuxLgjmfP-jjGMYBQ@xxxxxxxxxxxxxx/
Signed-off-by: Daan De Meyer <daan@xxxxxxxxxxxx>
---
fs/btrfs/reflink.c | 18 ++++++++++++++----
1 file changed, 14 insertions(+), 4 deletions(-)
diff --git a/fs/btrfs/reflink.c b/fs/btrfs/reflink.c
index 9a49d2ecb949..9dab689ea388 100644
--- a/fs/btrfs/reflink.c
+++ b/fs/btrfs/reflink.c
@@ -858,11 +858,21 @@ static int btrfs_remap_file_range_prep(struct file *file_in, loff_t pos_in,
if (IS_ENCRYPTED(&inode_in->vfs_inode) != IS_ENCRYPTED(&inode_out->vfs_inode))
return -EINVAL;
- /* Don't make the dst file partly checksummed */
- if ((inode_in->flags & BTRFS_INODE_NODATASUM) !=
- (inode_out->flags & BTRFS_INODE_NODATASUM)) {
+ /*
+ * Reflinking from a NODATASUM inode into a checksummed inode would
+ * make the destination refer to extents that have no csum items in
+ * the csum tree, and reads of those extents would then fail with
+ * -EIO. We can't create the missing csums here, so reject it.
+ *
+ * The other direction is safe: the destination inode has NODATASUM
+ * set, so reads through it never verify csums, and any write over a
+ * range whose extent has csums is forced to COW into a new extent
+ * (see can_nocow_file_extent()), so the csums shared with the source
+ * extents can never be invalidated by in-place writes.
+ */
+ if ((inode_in->flags & BTRFS_INODE_NODATASUM) &&
+ !(inode_out->flags & BTRFS_INODE_NODATASUM))
return -EINVAL;
- }
/*
* Now that the inodes are locked, we need to start writeback ourselves
--
2.54.0