[PATCH] btrfs: test checksum handling for clone, dedupe and swap on NOCOW files
Daan De Meyer via B4 Relay <[email protected]> Sun, 12 Jul 2026 16:37:22 +0200
| Newsgroups | org.kernel.vger.fstests,org.kernel.feeds.b4-sent,org.kernel.vger.linux-btrfs |
|---|---|
| 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]>