[PATCH] btrfs: test checksum handling for clone, dedupe and swap on NOCOW files

Daan De Meyer via B4 Relay <[email protected]>
Newsgroups org.kernel.vger.linux-btrfs,org.kernel.feeds.b4-sent,org.kernel.vger.fstests
Message-ID <[email protected]>
From: Daan De Meyer <[email protected]>

Add two tests covering how checksummed and NOCOW extents interact.

btrfs/348 verifies that checksummed extents can be cloned and
deduplicated into NOCOW files, and that the reverse direction stays
rejected because NOCOW extents do not have checksums.

btrfs/349 verifies that a NOCOW swap file containing a checksummed
extent is rejected even after the checksummed source is deleted and the
extent is no longer shared.

Signed-off-by: Daan De Meyer <[email protected]>
---
These tests cover the kernel series available at:
https://lore.kernel.org/linux-btrfs/[email protected]/
---
 tests/btrfs/348     | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 tests/btrfs/348.out |   2 +
 tests/btrfs/349     |  73 +++++++++++++++++++++
 tests/btrfs/349.out |   2 +
 4 files changed, 257 insertions(+)

diff --git a/tests/btrfs/348 b/tests/btrfs/348
new file mode 100755
index 00000000..4faf994e
--- /dev/null
+++ b/tests/btrfs/348
@@ -0,0 +1,180 @@
+#! /bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 Daan De Meyer. All Rights Reserved.
+#
+# FS QA Test 348
+#
+# Verify that checksummed extents can be cloned and deduplicated into NOCOW
+# files.  The reverse direction must remain rejected because NOCOW extents do
+# not have checksums.
+#
+. ./common/preamble
+_begin_fstest auto quick clone dedupe scrub
+
+. ./common/reflink
+
+_require_scratch_reflink
+_require_scratch_dedupe
+_require_chattr C
+_require_btrfs_no_nodatacow
+_require_btrfs_no_nodatasum
+_require_btrfs_command inspect-internal dump-tree
+
+_fixed_by_kernel_commit 28ce1d263f4d \
+	"btrfs: allow reflinking from checksummed files into nodatasum files"
+
+filesize=128K
+
+write_pattern()
+{
+	local file=$1
+	local pattern=$2
+
+	$XFS_IO_PROG -f -c "pwrite -q -S $pattern 0 $filesize" \
+		-c fsync "$file" || _fail "failed to write $file"
+}
+
+make_nocow_file()
+{
+	local file=$1
+
+	touch "$file"
+	$CHATTR_PROG +C "$file" || _fail "failed to set NOCOW on $file"
+}
+
+get_extent_bytenr()
+{
+	local file=$1
+	local offset=${2:-0}
+
+	extent_bytenr=$(_btrfs_get_file_extent_item_bytenr "$file" "$offset")
+	[ -n "$extent_bytenr" ] || _fail "failed to find an extent for $file"
+}
+
+check_contents()
+{
+	local file=$1
+	local expected=$2
+
+	cmp -s "$file" "$expected" || _fail "unexpected contents in $file"
+}
+
+# Generate reference files outside the scratch filesystem.
+write_pattern "$tmp.pattern_a" 0x61
+write_pattern "$tmp.pattern_b" 0x62
+write_pattern "$tmp.pattern_c" 0x63
+
+_scratch_mkfs >> $seqres.full 2>&1 || _fail "mkfs failed"
+_scratch_mount
+
+# Clone a checksummed extent into a NOCOW file.
+source=$SCRATCH_MNT/clone_source
+dest=$SCRATCH_MNT/clone_dest
+write_pattern "$source" 0x61
+make_nocow_file "$dest"
+_reflink_range "$source" 0 "$dest" 0 "$filesize" > /dev/null 2>&1
+
+# Remount so that the data checks below cannot be satisfied from page cache.
+_scratch_cycle_mount
+get_extent_bytenr "$source"
+source_bytenr=$extent_bytenr
+get_extent_bytenr "$dest"
+dest_bytenr=$extent_bytenr
+[ "$source_bytenr" = "$dest_bytenr" ] || \
+	_fail "clone did not share the source extent"
+check_contents "$source" "$tmp.pattern_a"
+check_contents "$dest" "$tmp.pattern_a"
+
+# The first write to the NOCOW destination must COW because the shared extent
+# has checksums.  This keeps the source data and its checksums intact.
+write_pattern "$dest" 0x62
+get_extent_bytenr "$dest"
+first_write_bytenr=$extent_bytenr
+[ "$first_write_bytenr" != "$source_bytenr" ] || \
+	_fail "first NOCOW destination write did not COW"
+_scratch_cycle_mount
+check_contents "$source" "$tmp.pattern_a"
+check_contents "$dest" "$tmp.pattern_b"
+
+# Once the destination has a private extent without checksums, NOCOW writes
+# should resume modifying that extent in place.
+write_pattern "$dest" 0x63
+get_extent_bytenr "$dest"
+second_write_bytenr=$extent_bytenr
+[ "$second_write_bytenr" = "$first_write_bytenr" ] || \
+	_fail "subsequent NOCOW destination write was not in place"
+check_contents "$dest" "$tmp.pattern_c"
+
+# The checksums must also force the first write to COW after the checksummed
+# source is deleted and the destination is the extent's only remaining owner.
+source=$SCRATCH_MNT/unshared_source
+dest=$SCRATCH_MNT/unshared_dest
+write_pattern "$source" 0x61
+make_nocow_file "$dest"
+_reflink_range "$source" 0 "$dest" 0 "$filesize" > /dev/null 2>&1
+get_extent_bytenr "$source"
+source_bytenr=$extent_bytenr
+get_extent_bytenr "$dest"
+shared_bytenr=$extent_bytenr
+[ "$source_bytenr" = "$shared_bytenr" ] || \
+	_fail "clone did not share the source extent"
+rm "$source"
+_scratch_cycle_mount
+write_pattern "$dest" 0x62
+get_extent_bytenr "$dest"
+unshared_write_bytenr=$extent_bytenr
+[ "$unshared_write_bytenr" != "$shared_bytenr" ] || \
+	_fail "write to unshared checksummed extent did not COW"
+check_contents "$dest" "$tmp.pattern_b"
+
+# Dedupe uses the same remap preparation path and must allow the same
+# checksummed source to NOCOW destination direction.
+source=$SCRATCH_MNT/dedupe_source
+dest=$SCRATCH_MNT/dedupe_dest
+write_pattern "$source" 0x61
+make_nocow_file "$dest"
+$XFS_IO_PROG -f -c "pwrite -q -S 0x61 0 384K" -c fsync "$dest" || \
+	_fail "failed to write $dest"
+_dedupe_range "$source" 0 "$dest" "$filesize" "$filesize" \
+	> /dev/null 2>&1
+_scratch_cycle_mount
+get_extent_bytenr "$source"
+source_bytenr=$extent_bytenr
+get_extent_bytenr "$dest" "$((128 * 1024))"
+dest_bytenr=$extent_bytenr
+[ "$source_bytenr" = "$dest_bytenr" ] || \
+	_fail "dedupe did not share the source extent"
+_compare_range "$source" 0 "$dest" "$((128 * 1024))" "$((128 * 1024))" || \
+	_fail "deduped range contents differ from the source"
+
+# Scrub must handle the NOCOW destination's mixture of checksummed and
+# non-checksummed extents without reporting an error.
+$BTRFS_UTIL_PROG scrub start -B "$SCRATCH_MNT" >> $seqres.full 2>&1 || \
+	_fail "scrub found errors"
+
+# Cloning or deduplicating a NOCOW source into a checksummed destination must
+# not share the source extent, since it has no checksums for destination reads.
+source=$SCRATCH_MNT/reverse_source
+clone_dest=$SCRATCH_MNT/reverse_clone_dest
+dedupe_dest=$SCRATCH_MNT/reverse_dedupe_dest
+make_nocow_file "$source"
+write_pattern "$source" 0x61
+touch "$clone_dest"
+_reflink_range "$source" 0 "$clone_dest" 0 "$filesize" > /dev/null 2>&1
+get_extent_bytenr "$source"
+source_bytenr=$extent_bytenr
+clone_bytenr=$(_btrfs_get_file_extent_item_bytenr "$clone_dest" 0)
+if [ -n "$clone_bytenr" ] && [ "$clone_bytenr" = "$source_bytenr" ]; then
+	_fail "NOCOW to checksummed clone succeeded"
+fi
+write_pattern "$dedupe_dest" 0x61
+_dedupe_range "$source" 0 "$dedupe_dest" 0 "$filesize" > /dev/null 2>&1
+get_extent_bytenr "$dedupe_dest"
+dedupe_bytenr=$extent_bytenr
+if [ "$dedupe_bytenr" = "$source_bytenr" ]; then
+	_fail "NOCOW to checksummed dedupe succeeded"
+fi
+
+echo "Silence is golden"
+
+_exit 0
diff --git a/tests/btrfs/348.out b/tests/btrfs/348.out
new file mode 100644
index 00000000..ec5e4701
--- /dev/null
+++ b/tests/btrfs/348.out
@@ -0,0 +1,2 @@
+QA output created by 348
+Silence is golden
diff --git a/tests/btrfs/349 b/tests/btrfs/349
new file mode 100755
index 00000000..66e0935a
--- /dev/null
+++ b/tests/btrfs/349
@@ -0,0 +1,73 @@
+#! /bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 Daan De Meyer. All Rights Reserved.
+#
+# FS QA Test 349
+#
+# Verify that a NOCOW swap file containing a checksummed extent is rejected
+# even after the checksummed source is deleted and the extent is no longer
+# shared.
+#
+. ./common/preamble
+_begin_fstest auto quick clone swap
+
+_cleanup()
+{
+	cd /
+	test -n "$swap_file" && swapoff "$swap_file" > /dev/null 2>&1
+	rm -f $tmp.*
+}
+
+. ./common/reflink
+
+_require_scratch_swapfile
+_require_scratch_reflink
+_require_chattr C
+_require_btrfs_no_nodatacow
+_require_btrfs_no_nodatasum
+_require_btrfs_command inspect-internal dump-tree
+
+_fixed_by_kernel_commit 8e4ef5e540bb \
+	"btrfs: reject swapfile activation if any extent has checksums"
+_fixed_by_kernel_commit 28ce1d263f4d \
+	"btrfs: allow reflinking from checksummed files into nodatasum files"
+
+_scratch_mkfs >> $seqres.full 2>&1 || _fail "mkfs failed"
+_scratch_mount
+
+swap_file=$SCRATCH_MNT/swapfile
+source=$SCRATCH_MNT/checksummed_source
+swap_size=$((16 * 1024 * 1024))
+extent_size=$((128 * 1024))
+dest_offset=$((1024 * 1024))
+
+# First verify that this filesystem and configuration can activate a regular
+# NOCOW swap file.
+_format_swapfile "$swap_file" "$swap_size" >> $seqres.full
+_swapon_file "$swap_file" >> $seqres.full 2>&1 || \
+	_fail "failed to activate plain NOCOW swap file"
+swapoff "$swap_file" >> $seqres.full 2>&1 || \
+	_fail "failed to deactivate plain NOCOW swap file"
+
+# Replace a range away from the swap header with a checksummed extent.  Delete
+# the source and commit the deletion so that sharedness cannot be the reason
+# for rejecting swap activation.
+$XFS_IO_PROG -f -c "pwrite -q -S 0x61 0 $extent_size" -c fsync "$source" || \
+	_fail "failed to write checksummed source"
+_reflink_range "$source" 0 "$swap_file" "$dest_offset" "$extent_size" \
+	> /dev/null 2>&1
+source_bytenr=$(_btrfs_get_file_extent_item_bytenr "$source" 0)
+swap_bytenr=$(_btrfs_get_file_extent_item_bytenr "$swap_file" "$dest_offset")
+[ -n "$source_bytenr" ] && [ "$source_bytenr" = "$swap_bytenr" ] || \
+	_fail "checksummed extent was not cloned into the swap file"
+rm "$source"
+_scratch_cycle_mount
+
+if _swapon_file "$swap_file" >> $seqres.full 2>&1; then
+	swapoff "$swap_file" > /dev/null 2>&1
+	_fail "activated swap file containing a checksummed extent"
+fi
+
+echo "Silence is golden"
+
+_exit 0
diff --git a/tests/btrfs/349.out b/tests/btrfs/349.out
new file mode 100644
index 00000000..aac66ede
--- /dev/null
+++ b/tests/btrfs/349.out
@@ -0,0 +1,2 @@
+QA output created by 349
+Silence is golden

---
base-commit: cac9fe2b8dc3e6dbc0c2383b9e3b4d3c1b9e7dd0
change-id: 20260712-push-kwzqkrlxwvnv-3aa56ae4bd7c

Best regards,
--  
Daan De Meyer <[email protected]>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.