* [PATCH 0/2] fstests: expect and test truthful FIDEDUPERANGE progress
@ 2026-08-05 7:18 Matthias Goergens
2026-08-05 7:18 ` [PATCH 1/2] generic/517: expect truthful dedupe progress Matthias Goergens
` (2 more replies)
0 siblings, 3 replies; 6+ messages in thread
From: Matthias Goergens @ 2026-08-05 7:18 UTC (permalink / raw)
To: fstests; +Cc: Matthias Goergens, Zorro Lang
This series pairs with the kernel series "vfs: report truthful
FIDEDUPERANGE progress safely", posted separately to linux-fsdevel:
https://lore.kernel.org/linux-fsdevel/20260805071414.3414870-1-matthias.goergens@gmail.com/
Patch 1 corrects generic/517's expected output: an unaligned 131172-byte
request is shortened to 131072 bytes, and the 100-byte remainder that
xfs_io retries now fails per destination with -EINVAL instead of
reporting success. These expectations encode the paired kernel change;
on an unpatched kernel the test fails on exactly these corrected lines,
as it did when the original kernel fix was briefly merged in 2022.
Patch 2 adds generic/806, a raw FIDEDUPERANGE ioctl test covering the
cases the retrying xfs_io interface cannot express precisely: an
explicit zero-length request succeeds with bytes_deduped=0, and mixed
multi-destination requests report the shortened count for an interior
destination and the full count for a destination ending at EOF. The new
number follows for-next's generic/798 and the publicly claimed numbers
through 805.
generic/517 and generic/806 both pass on Btrfs and XFS against the
paired kernel.
Matthias Goergens (2):
generic/517: expect truthful dedupe progress
generic: test FIDEDUPERANGE result reporting
src/Makefile | 2 +-
src/fideduperange.c | 138 ++++++++++++++++++++++++++++++++++++++++++
tests/generic/517 | 6 ++
tests/generic/517.out | 6 +-
tests/generic/806 | 83 +++++++++++++++++++++++++
tests/generic/806.out | 4 ++
6 files changed, 235 insertions(+), 4 deletions(-)
create mode 100644 src/fideduperange.c
create mode 100755 tests/generic/806
create mode 100644 tests/generic/806.out
--
2.55.0
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH 1/2] generic/517: expect truthful dedupe progress
2026-08-05 7:18 [PATCH 0/2] fstests: expect and test truthful FIDEDUPERANGE progress Matthias Goergens
@ 2026-08-05 7:18 ` Matthias Goergens
2026-08-11 8:36 ` Zorro Lang
2026-08-05 7:18 ` [PATCH 2/2] generic: test FIDEDUPERANGE result reporting Matthias Goergens
2026-08-17 9:50 ` [PATCH v2] generic: test FIDEDUPERANGE result reporting with REPORT_PROGRESS Matthias Goergens
2 siblings, 1 reply; 6+ messages in thread
From: Matthias Goergens @ 2026-08-05 7:18 UTC (permalink / raw)
To: fstests; +Cc: Matthias Goergens, Zorro Lang
The VFS currently reports the requested FIDEDUPERANGE length even when it
shortens the operation to avoid sharing a partial EOF block into the middle
of the destination. Once the kernel reports its actual progress, the first
dedupe in this test reports 131072 of 131172 bytes. xfs_io then retries the
100-byte remainder, which cannot make progress and returns -EINVAL. The
separate 100-byte request returns the same error.
Update the golden output for those truthful results. This is the output
change that caused commit 4a57a8400075 ("vf/remap: return the amount of
bytes actually deduplicated") to be reverted. The paired kernel series that
determines the new output is linked from the cover letter.
The updated test passes on btrfs and XFS with the paired kernel changes.
Signed-off-by: Matthias Goergens <matthias.goergens@gmail.com>
---
tests/generic/517 | 6 ++++++
tests/generic/517.out | 6 +++---
2 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/tests/generic/517 b/tests/generic/517
index 3f7027a8..ab5d17e7 100755
--- a/tests/generic/517
+++ b/tests/generic/517
@@ -11,6 +11,12 @@
. ./common/preamble
_begin_fstest auto quick dedupe clone
+# The dedupe amounts this test records are only correct once the kernel
+# reports the number of bytes it actually deduplicated rather than the number
+# requested. Both dedupe calls below target a range that does not reach the
+# destination's EOF, so generic_remap_check_len() shortens them under
+# REMAP_FILE_CAN_SHORTEN.
+
# Import common functions.
. ./common/filter
. ./common/reflink
diff --git a/tests/generic/517.out b/tests/generic/517.out
index b9b63207..6ba753d8 100644
--- a/tests/generic/517.out
+++ b/tests/generic/517.out
@@ -13,7 +13,8 @@ File content before first deduplication:
*
0786528 ae ae ae ae
0786532
-deduped 131172/131172 bytes at offset 65536
+XFS_IOC_FILE_EXTENT_SAME: Invalid argument
+deduped 131072/131172 bytes at offset 65536
XXX Bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
File content after first deduplication and before unmounting:
0000000 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b
@@ -33,8 +34,7 @@ File content after first unmount:
0786532
wrote 100/100 bytes at offset 0
XXX Bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
-deduped 100/100 bytes at offset 655360
-XXX Bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+XFS_IOC_FILE_EXTENT_SAME: Invalid argument
File content after second deduplication:
0000000 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b
*
--
2.55.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH 2/2] generic: test FIDEDUPERANGE result reporting
2026-08-05 7:18 [PATCH 0/2] fstests: expect and test truthful FIDEDUPERANGE progress Matthias Goergens
2026-08-05 7:18 ` [PATCH 1/2] generic/517: expect truthful dedupe progress Matthias Goergens
@ 2026-08-05 7:18 ` Matthias Goergens
2026-08-12 19:39 ` Zorro Lang
2026-08-17 9:50 ` [PATCH v2] generic: test FIDEDUPERANGE result reporting with REPORT_PROGRESS Matthias Goergens
2 siblings, 1 reply; 6+ messages in thread
From: Matthias Goergens @ 2026-08-05 7:18 UTC (permalink / raw)
To: fstests; +Cc: Matthias Goergens, Zorro Lang
xfs_io retries shortened dedupe operations, so its output cannot verify one
raw multi-destination ioctl or distinguish an explicit zero-length request
from a nonzero request shortened to zero.
Add a small FIDEDUPERANGE helper that reports each destination's raw
bytes_deduped and status fields. Use it in generic/806 to check an explicit
zero-length success, a request that is shortened for an interior destination
but remains full-length at EOF, and a sub-block request that returns -EINVAL
for the interior destination while succeeding at EOF.
Compute the request lengths from the filesystem block size so the expected
semantics do not assume 4K blocks. The paired kernel series is linked from
the cover letter.
The test passes on btrfs and XFS with the paired kernel changes.
Signed-off-by: Matthias Goergens <matthias.goergens@gmail.com>
---
src/Makefile | 2 +-
src/fideduperange.c | 138 ++++++++++++++++++++++++++++++++++++++++++
tests/generic/806 | 83 +++++++++++++++++++++++++
tests/generic/806.out | 4 ++
4 files changed, 226 insertions(+), 1 deletion(-)
create mode 100644 src/fideduperange.c
create mode 100755 tests/generic/806
create mode 100644 tests/generic/806.out
diff --git a/src/Makefile b/src/Makefile
index 76cf50c3..e686fe27 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -25,7 +25,7 @@ TARGETS = dirstress fill fill2 getpagesize holes lstat64 \
LINUX_TARGETS = xfsctl bstat t_mtab getdevicesize preallo_rw_pattern_reader \
preallo_rw_pattern_writer ftrunc trunc fs_perms testx looptest \
- locktest unwritten_mmap bulkstat_unlink_test deduperace \
+ locktest unwritten_mmap bulkstat_unlink_test deduperace fideduperange \
bulkstat_unlink_test_modified t_dir_offset t_futimens t_immutable \
stale_handle pwrite_mmap_blocked t_dir_offset2 seek_sanity_test \
seek_copy_test t_readdir_1 t_readdir_2 fsync-tester nsexec cloner \
diff --git a/src/fideduperange.c b/src/fideduperange.c
new file mode 100644
index 00000000..24813de8
--- /dev/null
+++ b/src/fideduperange.c
@@ -0,0 +1,138 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Issue a raw FIDEDUPERANGE ioctl and report each destination's result.
+ */
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <linux/fs.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+
+#ifndef FIDEDUPERANGE
+/* These definitions must match the kernel UAPI. */
+#define FILE_DEDUPE_RANGE_SAME 0
+#define FILE_DEDUPE_RANGE_DIFFERS 1
+
+struct file_dedupe_range_info {
+ __s64 dest_fd;
+ __u64 dest_offset;
+ __u64 bytes_deduped;
+ __s32 status;
+ __u32 reserved;
+};
+
+struct file_dedupe_range {
+ __u64 src_offset;
+ __u64 src_length;
+ __u16 dest_count;
+ __u16 reserved1;
+ __u32 reserved2;
+ struct file_dedupe_range_info info[0];
+};
+
+#define FIDEDUPERANGE _IOWR(0x94, 54, struct file_dedupe_range)
+#endif
+
+static void
+usage(const char *progname)
+{
+ fprintf(stderr,
+ "Usage: %s src src_offset length dest dest_offset "
+ "[dest dest_offset ...]\n",
+ progname);
+ exit(EXIT_FAILURE);
+}
+
+static uint64_t
+parse_u64(const char *arg, const char *name)
+{
+ char *end;
+ unsigned long long value;
+
+ errno = 0;
+ value = strtoull(arg, &end, 0);
+ if (errno || end == arg || *end != '\0') {
+ fprintf(stderr, "Invalid %s: %s\n", name, arg);
+ exit(EXIT_FAILURE);
+ }
+
+ return value;
+}
+
+int
+main(int argc, char **argv)
+{
+ struct file_dedupe_range *range;
+ size_t range_size;
+ unsigned int dest_count;
+ unsigned int i;
+ int src_fd;
+ int ret;
+
+ if (argc < 6 || (argc - 4) % 2 != 0)
+ usage(argv[0]);
+
+ dest_count = (argc - 4) / 2;
+ if (dest_count > UINT16_MAX) {
+ fprintf(stderr, "Too many destinations: %u\n", dest_count);
+ return EXIT_FAILURE;
+ }
+
+ range_size = sizeof(*range) +
+ dest_count * sizeof(struct file_dedupe_range_info);
+ range = calloc(1, range_size);
+ if (!range) {
+ perror("calloc");
+ return EXIT_FAILURE;
+ }
+
+ src_fd = open(argv[1], O_RDONLY);
+ if (src_fd < 0) {
+ perror(argv[1]);
+ free(range);
+ return EXIT_FAILURE;
+ }
+
+ range->src_offset = parse_u64(argv[2], "source offset");
+ range->src_length = parse_u64(argv[3], "length");
+ range->dest_count = dest_count;
+
+ for (i = 0; i < dest_count; i++) {
+ const char *dest_path = argv[4 + i * 2];
+
+ range->info[i].dest_fd = open(dest_path, O_RDWR);
+ if (range->info[i].dest_fd < 0) {
+ perror(dest_path);
+ ret = EXIT_FAILURE;
+ goto close_destinations;
+ }
+ range->info[i].dest_offset =
+ parse_u64(argv[5 + i * 2], "destination offset");
+ }
+
+ if (ioctl(src_fd, FIDEDUPERANGE, range) < 0) {
+ perror("FIDEDUPERANGE");
+ ret = EXIT_FAILURE;
+ goto close_destinations;
+ }
+
+ for (i = 0; i < dest_count; i++)
+ printf("destination %u: bytes=%" PRIu64 " status=%d\n", i,
+ (uint64_t)range->info[i].bytes_deduped,
+ range->info[i].status);
+ ret = EXIT_SUCCESS;
+
+close_destinations:
+ while (i > 0) {
+ i--;
+ close(range->info[i].dest_fd);
+ }
+ close(src_fd);
+ free(range);
+ return ret;
+}
diff --git a/tests/generic/806 b/tests/generic/806
new file mode 100755
index 00000000..98254b79
--- /dev/null
+++ b/tests/generic/806
@@ -0,0 +1,83 @@
+#! /bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 Matthias Goergens. All Rights Reserved.
+#
+# FS QA Test No. 806
+#
+# Check FIDEDUPERANGE result reporting for zero-length requests and for a
+# multi-destination request where the VFS shortens only some destinations.
+#
+. ./common/preamble
+_begin_fstest auto quick dedupe clone
+
+. ./common/reflink
+
+_require_scratch_dedupe
+_require_test_program "fideduperange"
+
+_scratch_mkfs >>$seqres.full 2>&1
+_scratch_mount
+
+block_size=$(_get_block_size "$SCRATCH_MNT")
+short_length=$((2 * block_size))
+full_length=$((short_length + 100))
+
+src=$SCRATCH_MNT/src
+src_short=$SCRATCH_MNT/src-short
+dest_interior=$SCRATCH_MNT/dest-interior
+dest_eof=$SCRATCH_MNT/dest-eof
+dest_short_eof=$SCRATCH_MNT/dest-short-eof
+
+$XFS_IO_PROG -f -c "pwrite -S 0x6b 0 $full_length" "$src" \
+ >>$seqres.full 2>&1
+$XFS_IO_PROG -f -c "pwrite -S 0x6b 0 100" "$src_short" \
+ >>$seqres.full 2>&1
+$XFS_IO_PROG -f -c "pwrite -S 0x6b 0 $((full_length + block_size))" \
+ "$dest_interior" >>$seqres.full 2>&1
+$XFS_IO_PROG -f -c "pwrite -S 0x6b 0 $full_length" "$dest_eof" \
+ >>$seqres.full 2>&1
+$XFS_IO_PROG -f -c "pwrite -S 0x6b 0 100" "$dest_short_eof" \
+ >>$seqres.full 2>&1
+
+check_results()
+{
+ local description="$1"
+ local expected="$2"
+ shift 2
+ local result
+ local ret
+
+ result=$($here/src/fideduperange "$@" 2>&1)
+ ret=$?
+ {
+ echo "$description"
+ echo "$result"
+ } >>$seqres.full
+
+ if [ $ret -ne 0 ]; then
+ _fail "$description: ioctl helper failed"
+ fi
+ if [ "$result" != "$expected" ]; then
+ _fail "$description: unexpected ioctl result"
+ fi
+
+ echo "$description"
+}
+
+expected="destination 0: bytes=0 status=0"
+check_results "zero length: 0 bytes, SAME" "$expected" \
+ "$src" 0 0 "$dest_interior" 0
+
+expected="destination 0: bytes=$short_length status=0
+destination 1: bytes=$full_length status=0"
+check_results "mixed partial request: shortened, full" "$expected" \
+ "$src" 0 "$full_length" "$dest_interior" 0 "$dest_eof" 0
+
+expected="destination 0: bytes=0 status=-22
+destination 1: bytes=100 status=0"
+check_results "mixed sub-block request: EINVAL, full" "$expected" \
+ "$src_short" 0 100 "$dest_interior" 0 "$dest_short_eof" 0
+
+# success, all done
+status=0
+exit
diff --git a/tests/generic/806.out b/tests/generic/806.out
new file mode 100644
index 00000000..d7226e34
--- /dev/null
+++ b/tests/generic/806.out
@@ -0,0 +1,4 @@
+QA output created by 806
+zero length: 0 bytes, SAME
+mixed partial request: shortened, full
+mixed sub-block request: EINVAL, full
--
2.55.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* Re: [PATCH 1/2] generic/517: expect truthful dedupe progress
2026-08-05 7:18 ` [PATCH 1/2] generic/517: expect truthful dedupe progress Matthias Goergens
@ 2026-08-11 8:36 ` Zorro Lang
0 siblings, 0 replies; 6+ messages in thread
From: Zorro Lang @ 2026-08-11 8:36 UTC (permalink / raw)
To: Matthias Goergens; +Cc: fstests, xfs-list, btrfs-list, Filipe Manana
On Wed, Aug 05, 2026 at 03:18:38PM +0800, Matthias Goergens wrote:
> The VFS currently reports the requested FIDEDUPERANGE length even when it
> shortens the operation to avoid sharing a partial EOF block into the middle
> of the destination. Once the kernel reports its actual progress, the first
> dedupe in this test reports 131072 of 131172 bytes. xfs_io then retries the
> 100-byte remainder, which cannot make progress and returns -EINVAL. The
> separate 100-byte request returns the same error.
>
> Update the golden output for those truthful results. This is the output
> change that caused commit 4a57a8400075 ("vf/remap: return the amount of
> bytes actually deduplicated") to be reverted. The paired kernel series that
> determines the new output is linked from the cover letter.
>
> The updated test passes on btrfs and XFS with the paired kernel changes.
To avoid panic in downstream testing caused by sudden breaking changes in
expected behavior, we'd better to reference the commit or patch that
introduced the change.
Rather than using _fixed_by_kernel_commit helper, _wants_kernel_commit
might be more appropriate here.
CC'ing Filipe, the original author of this test case, for further review.
Thanks,
Zorro
>
> Signed-off-by: Matthias Goergens <matthias.goergens@gmail.com>
> ---
> tests/generic/517 | 6 ++++++
> tests/generic/517.out | 6 +++---
> 2 files changed, 9 insertions(+), 3 deletions(-)
>
> diff --git a/tests/generic/517 b/tests/generic/517
> index 3f7027a8..ab5d17e7 100755
> --- a/tests/generic/517
> +++ b/tests/generic/517
> @@ -11,6 +11,12 @@
> . ./common/preamble
> _begin_fstest auto quick dedupe clone
>
> +# The dedupe amounts this test records are only correct once the kernel
> +# reports the number of bytes it actually deduplicated rather than the number
> +# requested. Both dedupe calls below target a range that does not reach the
> +# destination's EOF, so generic_remap_check_len() shortens them under
> +# REMAP_FILE_CAN_SHORTEN.
> +
> # Import common functions.
> . ./common/filter
> . ./common/reflink
> diff --git a/tests/generic/517.out b/tests/generic/517.out
> index b9b63207..6ba753d8 100644
> --- a/tests/generic/517.out
> +++ b/tests/generic/517.out
> @@ -13,7 +13,8 @@ File content before first deduplication:
> *
> 0786528 ae ae ae ae
> 0786532
> -deduped 131172/131172 bytes at offset 65536
> +XFS_IOC_FILE_EXTENT_SAME: Invalid argument
> +deduped 131072/131172 bytes at offset 65536
> XXX Bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
> File content after first deduplication and before unmounting:
> 0000000 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b
> @@ -33,8 +34,7 @@ File content after first unmount:
> 0786532
> wrote 100/100 bytes at offset 0
> XXX Bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
> -deduped 100/100 bytes at offset 655360
> -XXX Bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
> +XFS_IOC_FILE_EXTENT_SAME: Invalid argument
> File content after second deduplication:
> 0000000 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b 6b
> *
> --
> 2.55.0
>
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH 2/2] generic: test FIDEDUPERANGE result reporting
2026-08-05 7:18 ` [PATCH 2/2] generic: test FIDEDUPERANGE result reporting Matthias Goergens
@ 2026-08-12 19:39 ` Zorro Lang
0 siblings, 0 replies; 6+ messages in thread
From: Zorro Lang @ 2026-08-12 19:39 UTC (permalink / raw)
To: Matthias Goergens; +Cc: fstests
On Wed, Aug 05, 2026 at 03:18:39PM +0800, Matthias Goergens wrote:
> xfs_io retries shortened dedupe operations, so its output cannot verify one
> raw multi-destination ioctl or distinguish an explicit zero-length request
> from a nonzero request shortened to zero.
>
> Add a small FIDEDUPERANGE helper that reports each destination's raw
> bytes_deduped and status fields. Use it in generic/806 to check an explicit
> zero-length success, a request that is shortened for an interior destination
> but remains full-length at EOF, and a sub-block request that returns -EINVAL
> for the interior destination while succeeding at EOF.
>
> Compute the request lengths from the filesystem block size so the expected
> semantics do not assume 4K blocks. The paired kernel series is linked from
> the cover letter.
>
> The test passes on btrfs and XFS with the paired kernel changes.
>
> Signed-off-by: Matthias Goergens <matthias.goergens@gmail.com>
> ---
> src/Makefile | 2 +-
> src/fideduperange.c | 138 ++++++++++++++++++++++++++++++++++++++++++
Need to update .gitignore file too.
> tests/generic/806 | 83 +++++++++++++++++++++++++
> tests/generic/806.out | 4 ++
> 4 files changed, 226 insertions(+), 1 deletion(-)
> create mode 100644 src/fideduperange.c
> create mode 100755 tests/generic/806
> create mode 100644 tests/generic/806.out
>
> diff --git a/src/Makefile b/src/Makefile
> index 76cf50c3..e686fe27 100644
> --- a/src/Makefile
> +++ b/src/Makefile
> @@ -25,7 +25,7 @@ TARGETS = dirstress fill fill2 getpagesize holes lstat64 \
>
> LINUX_TARGETS = xfsctl bstat t_mtab getdevicesize preallo_rw_pattern_reader \
> preallo_rw_pattern_writer ftrunc trunc fs_perms testx looptest \
> - locktest unwritten_mmap bulkstat_unlink_test deduperace \
> + locktest unwritten_mmap bulkstat_unlink_test deduperace fideduperange \
> bulkstat_unlink_test_modified t_dir_offset t_futimens t_immutable \
> stale_handle pwrite_mmap_blocked t_dir_offset2 seek_sanity_test \
> seek_copy_test t_readdir_1 t_readdir_2 fsync-tester nsexec cloner \
> diff --git a/src/fideduperange.c b/src/fideduperange.c
> new file mode 100644
> index 00000000..24813de8
> --- /dev/null
> +++ b/src/fideduperange.c
> @@ -0,0 +1,138 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Issue a raw FIDEDUPERANGE ioctl and report each destination's result.
> + */
> +#include <errno.h>
> +#include <fcntl.h>
> +#include <inttypes.h>
> +#include <linux/fs.h>
> +#include <stdint.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <sys/ioctl.h>
> +#include <unistd.h>
> +
> +#ifndef FIDEDUPERANGE
> +/* These definitions must match the kernel UAPI. */
> +#define FILE_DEDUPE_RANGE_SAME 0
> +#define FILE_DEDUPE_RANGE_DIFFERS 1
> +
> +struct file_dedupe_range_info {
> + __s64 dest_fd;
> + __u64 dest_offset;
> + __u64 bytes_deduped;
> + __s32 status;
> + __u32 reserved;
> +};
> +
> +struct file_dedupe_range {
> + __u64 src_offset;
> + __u64 src_length;
> + __u16 dest_count;
> + __u16 reserved1;
> + __u32 reserved2;
> + struct file_dedupe_range_info info[0];
> +};
> +
> +#define FIDEDUPERANGE _IOWR(0x94, 54, struct file_dedupe_range)
> +#endif
> +
> +static void
> +usage(const char *progname)
> +{
> + fprintf(stderr,
> + "Usage: %s src src_offset length dest dest_offset "
> + "[dest dest_offset ...]\n",
> + progname);
> + exit(EXIT_FAILURE);
> +}
> +
> +static uint64_t
> +parse_u64(const char *arg, const char *name)
> +{
> + char *end;
> + unsigned long long value;
> +
> + errno = 0;
> + value = strtoull(arg, &end, 0);
> + if (errno || end == arg || *end != '\0') {
> + fprintf(stderr, "Invalid %s: %s\n", name, arg);
> + exit(EXIT_FAILURE);
> + }
> +
> + return value;
> +}
> +
> +int
> +main(int argc, char **argv)
> +{
> + struct file_dedupe_range *range;
> + size_t range_size;
> + unsigned int dest_count;
> + unsigned int i;
> + int src_fd;
> + int ret;
> +
> + if (argc < 6 || (argc - 4) % 2 != 0)
> + usage(argv[0]);
> +
> + dest_count = (argc - 4) / 2;
> + if (dest_count > UINT16_MAX) {
> + fprintf(stderr, "Too many destinations: %u\n", dest_count);
> + return EXIT_FAILURE;
> + }
> +
> + range_size = sizeof(*range) +
> + dest_count * sizeof(struct file_dedupe_range_info);
> + range = calloc(1, range_size);
> + if (!range) {
> + perror("calloc");
> + return EXIT_FAILURE;
> + }
> +
> + src_fd = open(argv[1], O_RDONLY);
> + if (src_fd < 0) {
> + perror(argv[1]);
> + free(range);
> + return EXIT_FAILURE;
> + }
> +
> + range->src_offset = parse_u64(argv[2], "source offset");
> + range->src_length = parse_u64(argv[3], "length");
> + range->dest_count = dest_count;
> +
> + for (i = 0; i < dest_count; i++) {
> + const char *dest_path = argv[4 + i * 2];
> +
> + range->info[i].dest_fd = open(dest_path, O_RDWR);
> + if (range->info[i].dest_fd < 0) {
> + perror(dest_path);
> + ret = EXIT_FAILURE;
> + goto close_destinations;
> + }
> + range->info[i].dest_offset =
> + parse_u64(argv[5 + i * 2], "destination offset");
> + }
> +
> + if (ioctl(src_fd, FIDEDUPERANGE, range) < 0) {
> + perror("FIDEDUPERANGE");
> + ret = EXIT_FAILURE;
> + goto close_destinations;
> + }
> +
> + for (i = 0; i < dest_count; i++)
> + printf("destination %u: bytes=%" PRIu64 " status=%d\n", i,
> + (uint64_t)range->info[i].bytes_deduped,
> + range->info[i].status);
> + ret = EXIT_SUCCESS;
> +
> +close_destinations:
> + while (i > 0) {
> + i--;
> + close(range->info[i].dest_fd);
> + }
> + close(src_fd);
> + free(range);
> + return ret;
> +}
> diff --git a/tests/generic/806 b/tests/generic/806
> new file mode 100755
> index 00000000..98254b79
> --- /dev/null
> +++ b/tests/generic/806
> @@ -0,0 +1,83 @@
> +#! /bin/bash
> +# SPDX-License-Identifier: GPL-2.0
> +# Copyright (c) 2026 Matthias Goergens. All Rights Reserved.
> +#
> +# FS QA Test No. 806
> +#
> +# Check FIDEDUPERANGE result reporting for zero-length requests and for a
> +# multi-destination request where the VFS shortens only some destinations.
> +#
> +. ./common/preamble
> +_begin_fstest auto quick dedupe clone
> +
> +. ./common/reflink
> +
> +_require_scratch_dedupe
> +_require_test_program "fideduperange"
> +
> +_scratch_mkfs >>$seqres.full 2>&1
> +_scratch_mount
> +
> +block_size=$(_get_block_size "$SCRATCH_MNT")
Better to use "_get_file_block_size".
This new test looks good, but let's wait until kernel patches
get acked first.
> +short_length=$((2 * block_size))
> +full_length=$((short_length + 100))
> +
> +src=$SCRATCH_MNT/src
> +src_short=$SCRATCH_MNT/src-short
> +dest_interior=$SCRATCH_MNT/dest-interior
> +dest_eof=$SCRATCH_MNT/dest-eof
> +dest_short_eof=$SCRATCH_MNT/dest-short-eof
> +
> +$XFS_IO_PROG -f -c "pwrite -S 0x6b 0 $full_length" "$src" \
> + >>$seqres.full 2>&1
> +$XFS_IO_PROG -f -c "pwrite -S 0x6b 0 100" "$src_short" \
> + >>$seqres.full 2>&1
> +$XFS_IO_PROG -f -c "pwrite -S 0x6b 0 $((full_length + block_size))" \
> + "$dest_interior" >>$seqres.full 2>&1
> +$XFS_IO_PROG -f -c "pwrite -S 0x6b 0 $full_length" "$dest_eof" \
> + >>$seqres.full 2>&1
> +$XFS_IO_PROG -f -c "pwrite -S 0x6b 0 100" "$dest_short_eof" \
> + >>$seqres.full 2>&1
> +
> +check_results()
> +{
> + local description="$1"
> + local expected="$2"
> + shift 2
> + local result
> + local ret
> +
> + result=$($here/src/fideduperange "$@" 2>&1)
> + ret=$?
> + {
> + echo "$description"
> + echo "$result"
> + } >>$seqres.full
> +
> + if [ $ret -ne 0 ]; then
> + _fail "$description: ioctl helper failed"
> + fi
> + if [ "$result" != "$expected" ]; then
> + _fail "$description: unexpected ioctl result"
> + fi
> +
> + echo "$description"
> +}
> +
> +expected="destination 0: bytes=0 status=0"
> +check_results "zero length: 0 bytes, SAME" "$expected" \
> + "$src" 0 0 "$dest_interior" 0
> +
> +expected="destination 0: bytes=$short_length status=0
> +destination 1: bytes=$full_length status=0"
> +check_results "mixed partial request: shortened, full" "$expected" \
> + "$src" 0 "$full_length" "$dest_interior" 0 "$dest_eof" 0
> +
> +expected="destination 0: bytes=0 status=-22
> +destination 1: bytes=100 status=0"
> +check_results "mixed sub-block request: EINVAL, full" "$expected" \
> + "$src_short" 0 100 "$dest_interior" 0 "$dest_short_eof" 0
> +
> +# success, all done
> +status=0
> +exit
> diff --git a/tests/generic/806.out b/tests/generic/806.out
> new file mode 100644
> index 00000000..d7226e34
> --- /dev/null
> +++ b/tests/generic/806.out
> @@ -0,0 +1,4 @@
> +QA output created by 806
> +zero length: 0 bytes, SAME
> +mixed partial request: shortened, full
> +mixed sub-block request: EINVAL, full
> --
> 2.55.0
>
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH v2] generic: test FIDEDUPERANGE result reporting with REPORT_PROGRESS
2026-08-05 7:18 [PATCH 0/2] fstests: expect and test truthful FIDEDUPERANGE progress Matthias Goergens
2026-08-05 7:18 ` [PATCH 1/2] generic/517: expect truthful dedupe progress Matthias Goergens
2026-08-05 7:18 ` [PATCH 2/2] generic: test FIDEDUPERANGE result reporting Matthias Goergens
@ 2026-08-17 9:50 ` Matthias Goergens
2 siblings, 0 replies; 6+ messages in thread
From: Matthias Goergens @ 2026-08-17 9:50 UTC (permalink / raw)
To: fstests; +Cc: zlang, fdmanana, linux-xfs, linux-btrfs
Add a raw FIDEDUPERANGE helper that reports each destination's
bytes_deduped and status, and a test for the proposed
FILE_DEDUPE_RANGE_REPORT_PROGRESS flag: explicit zero-length success,
a request the VFS shortens for an interior destination, a sub-block
request that makes no progress (zero, no error), and differing
contents (the examined span). One unflagged case pins the legacy
reporting so the default ABI cannot change silently.
The kernel side is under review:
https://lore.kernel.org/linux-fsdevel/20260805071414.3414870-1-matthias.goergens@gmail.com/
The commit id in _wants_kernel_commit is a placeholder until it lands.
Signed-off-by: Matthias Goergens <matthias.goergens@gmail.com>
---
v2 (per Zorro's review and the kernel thread):
- drop the generic/517 golden-output change: the kernel side is now an
opt-in flag, so default reporting is unchanged
- test the flag semantics proposed by Darrick in the kernel thread,
including bytes_deduped as an advance hint on DIFFERS and zero on a
per-destination error
- sentinel-fill the helper's output fields so the test proves the
kernel wrote them
- add the helper binary to .gitignore
- use _wants_kernel_commit (placeholder id until the kernel side lands)
Every expected line was produced by a kernel with the flag patch
applied (btrfs scratch under QEMU) rather than written from the spec.
.gitignore | 1 +
src/Makefile | 2 +-
src/fideduperange.c | 166 ++++++++++++++++++++++++++++++++++++++++++
tests/generic/806 | 118 ++++++++++++++++++++++++++++++
tests/generic/806.out | 7 ++
5 files changed, 293 insertions(+), 1 deletion(-)
create mode 100644 src/fideduperange.c
create mode 100755 tests/generic/806
create mode 100644 tests/generic/806.out
diff --git a/.gitignore b/.gitignore
index 0b6b9452..3d6ce89d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -71,6 +71,7 @@ tags
/src/cloner
/src/dbtest
/src/deduperace
+/src/fideduperange
/src/detached_mounts_propagation
/src/devzero
/src/dio-append-buf-fault
diff --git a/src/Makefile b/src/Makefile
index 76cf50c3..e686fe27 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -25,7 +25,7 @@ TARGETS = dirstress fill fill2 getpagesize holes lstat64 \
LINUX_TARGETS = xfsctl bstat t_mtab getdevicesize preallo_rw_pattern_reader \
preallo_rw_pattern_writer ftrunc trunc fs_perms testx looptest \
- locktest unwritten_mmap bulkstat_unlink_test deduperace \
+ locktest unwritten_mmap bulkstat_unlink_test deduperace fideduperange \
bulkstat_unlink_test_modified t_dir_offset t_futimens t_immutable \
stale_handle pwrite_mmap_blocked t_dir_offset2 seek_sanity_test \
seek_copy_test t_readdir_1 t_readdir_2 fsync-tester nsexec cloner \
diff --git a/src/fideduperange.c b/src/fideduperange.c
new file mode 100644
index 00000000..b36365f6
--- /dev/null
+++ b/src/fideduperange.c
@@ -0,0 +1,166 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Issue a raw FIDEDUPERANGE ioctl and report each destination's result.
+ */
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <inttypes.h>
+#include <linux/fs.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+
+/* Local copies of the UAPI types: the system linux/fs.h may predate the
+ * FILE_DEDUPE_RANGE_REPORT_PROGRESS flag. The layout matches the kernel
+ * UAPI. */
+#ifndef FILE_DEDUPE_RANGE_REPORT_PROGRESS
+#define FILE_DEDUPE_RANGE_REPORT_PROGRESS (1U << 0)
+#endif
+#define FILE_DEDUPE_RANGE_SAME 0
+#define FILE_DEDUPE_RANGE_DIFFERS 1
+
+struct file_dedupe_range_info_local {
+ __s64 dest_fd;
+ __u64 dest_offset;
+ __u64 bytes_deduped;
+ __s32 status;
+ __u32 reserved;
+};
+
+struct file_dedupe_range_local {
+ __u64 src_offset;
+ __u64 src_length;
+ __u16 dest_count;
+ __u16 reserved1;
+ __u32 flags;
+ struct file_dedupe_range_info_local info[0];
+};
+
+#ifndef FIDEDUPERANGE
+#define FIDEDUPERANGE _IOWR(0x94, 54, struct file_dedupe_range_local)
+#endif
+
+static __u32 range_flags;
+
+static void
+usage(const char *progname)
+{
+ fprintf(stderr,
+ "Usage: %s [-f] src src_offset length dest dest_offset "
+ "[dest dest_offset ...]\n"
+ " -f: set FILE_DEDUPE_RANGE_REPORT_PROGRESS\n",
+ progname);
+ exit(EXIT_FAILURE);
+}
+
+static uint64_t
+parse_u64(const char *arg, const char *name)
+{
+ char *end;
+ unsigned long long value;
+
+ errno = 0;
+ value = strtoull(arg, &end, 0);
+ if (errno || end == arg || *end != '\0') {
+ fprintf(stderr, "Invalid %s: %s\n", name, arg);
+ exit(EXIT_FAILURE);
+ }
+
+ return value;
+}
+
+int
+main(int argc, char **argv)
+{
+ struct file_dedupe_range_local *range;
+ size_t range_size;
+ unsigned int dest_count;
+ unsigned int i;
+ int src_fd;
+ int ret;
+ int c;
+
+ const char *progname = argv[0];
+
+ while ((c = getopt(argc, argv, "f")) != -1) {
+ switch (c) {
+ case 'f':
+ range_flags |= FILE_DEDUPE_RANGE_REPORT_PROGRESS;
+ break;
+ default:
+ usage(progname);
+ }
+ }
+ argc -= optind;
+ argv += optind;
+
+ if (argc < 5 || (argc - 3) % 2 != 0)
+ usage(progname);
+
+ dest_count = (argc - 3) / 2;
+ if (dest_count > UINT16_MAX) {
+ fprintf(stderr, "Too many destinations: %u\n", dest_count);
+ return EXIT_FAILURE;
+ }
+
+ range_size = sizeof(*range) +
+ dest_count * sizeof(struct file_dedupe_range_info_local);
+ range = calloc(1, range_size);
+ if (!range) {
+ perror("calloc");
+ return EXIT_FAILURE;
+ }
+
+ src_fd = open(argv[0], O_RDONLY);
+ if (src_fd < 0) {
+ perror(argv[0]);
+ free(range);
+ return EXIT_FAILURE;
+ }
+
+ range->src_offset = parse_u64(argv[1], "source offset");
+ range->src_length = parse_u64(argv[2], "length");
+ range->dest_count = dest_count;
+ range->flags = range_flags;
+
+ for (i = 0; i < dest_count; i++) {
+ const char *dest_path = argv[3 + i * 2];
+
+ range->info[i].dest_fd = open(dest_path, O_RDWR);
+ if (range->info[i].dest_fd < 0) {
+ perror(dest_path);
+ ret = EXIT_FAILURE;
+ goto close_destinations;
+ }
+ range->info[i].dest_offset =
+ parse_u64(argv[4 + i * 2], "destination offset");
+ /* Sentinels: the kernel must overwrite both output fields. */
+ range->info[i].bytes_deduped = ~0ULL;
+ range->info[i].status = INT32_MIN;
+ }
+
+ if (ioctl(src_fd, FIDEDUPERANGE, range) < 0) {
+ perror("FIDEDUPERANGE");
+ ret = EXIT_FAILURE;
+ goto close_destinations;
+ }
+
+ for (i = 0; i < dest_count; i++)
+ printf("destination %u: bytes=%" PRIu64 " status=%d\n", i,
+ (uint64_t)range->info[i].bytes_deduped,
+ range->info[i].status);
+ ret = EXIT_SUCCESS;
+
+close_destinations:
+ while (i > 0) {
+ i--;
+ close(range->info[i].dest_fd);
+ }
+ close(src_fd);
+ free(range);
+ return ret;
+}
diff --git a/tests/generic/806 b/tests/generic/806
new file mode 100755
index 00000000..70a5fe36
--- /dev/null
+++ b/tests/generic/806
@@ -0,0 +1,118 @@
+#! /bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 Matthias Goergens. All Rights Reserved.
+#
+# FS QA Test No. 806
+#
+# Check FIDEDUPERANGE result reporting with FILE_DEDUPE_RANGE_REPORT_PROGRESS:
+# bytes_deduped as an advance hint for zero-length, shortened, zero-progress
+# and differing requests, plus one unflagged case pinning the legacy
+# reporting.
+#
+. ./common/preamble
+_begin_fstest auto quick dedupe clone
+
+. ./common/reflink
+
+_require_scratch_dedupe
+_require_test_program "fideduperange"
+
+# Placeholder commit id: the kernel patch is still under review; to be
+# replaced with the real id when it lands (the helper only prints a hint).
+_wants_kernel_commit XXXXXXXXXXXX \
+ "vfs: add FILE_DEDUPE_RANGE_REPORT_PROGRESS flag to FIDEDUPERANGE"
+
+_scratch_mkfs >>$seqres.full 2>&1
+_scratch_mount
+
+block_size=$(_get_block_size "$SCRATCH_MNT")
+short_length=$((2 * block_size))
+full_length=$((short_length + 100))
+
+src=$SCRATCH_MNT/src
+src_short=$SCRATCH_MNT/src-short
+src_diff=$SCRATCH_MNT/src-diff
+dest_interior=$SCRATCH_MNT/dest-interior
+dest_eof=$SCRATCH_MNT/dest-eof
+dest_short_eof=$SCRATCH_MNT/dest-short-eof
+dest_diff=$SCRATCH_MNT/dest-diff
+
+$XFS_IO_PROG -f -c "pwrite -S 0x6b 0 $full_length" "$src" \
+ >>$seqres.full 2>&1
+$XFS_IO_PROG -f -c "pwrite -S 0x6b 0 100" "$src_short" \
+ >>$seqres.full 2>&1
+$XFS_IO_PROG -f -c "pwrite -S 0x6b 0 $((full_length + block_size))" \
+ "$dest_interior" >>$seqres.full 2>&1
+$XFS_IO_PROG -f -c "pwrite -S 0x6b 0 $full_length" "$dest_eof" \
+ >>$seqres.full 2>&1
+$XFS_IO_PROG -f -c "pwrite -S 0x6b 0 100" "$dest_short_eof" \
+ >>$seqres.full 2>&1
+$XFS_IO_PROG -f -c "pwrite -S 0x61 0 $full_length" "$src_diff" \
+ >>$seqres.full 2>&1
+$XFS_IO_PROG -f -c "pwrite -S 0x62 0 $full_length" "$dest_diff" \
+ >>$seqres.full 2>&1
+
+check_results()
+{
+ local description="$1"
+ local expected="$2"
+ shift 2
+ local result
+ local ret
+
+ result=$($here/src/fideduperange "$@" 2>&1)
+ ret=$?
+ {
+ echo "$description"
+ echo "$result"
+ } >>$seqres.full
+
+ if [ $ret -ne 0 ]; then
+ _fail "$description: ioctl helper failed"
+ fi
+ if [ "$result" != "$expected" ]; then
+ _fail "$description: unexpected ioctl result"
+ fi
+
+ echo "$description"
+}
+
+# With the flag, bytes_deduped is an advance hint: actual bytes on SAME,
+# the examined span on DIFFERS, zero when no further work is possible.
+
+expected="destination 0: bytes=0 status=0"
+check_results "zero length: 0 bytes, SAME" "$expected" \
+ -f "$src" 0 0 "$dest_interior" 0
+
+expected="destination 0: bytes=$short_length status=0
+destination 1: bytes=$full_length status=0"
+check_results "mixed partial request: shortened, full" "$expected" \
+ -f "$src" 0 "$full_length" "$dest_interior" 0 "$dest_eof" 0
+
+expected="destination 0: bytes=0 status=0
+destination 1: bytes=100 status=0"
+check_results "sub-block request: zero progress, full" "$expected" \
+ -f "$src_short" 0 100 "$dest_interior" 0 "$dest_short_eof" 0
+
+expected="destination 0: bytes=$block_size status=1"
+check_results "differing contents: DIFFERS with advance hint" "$expected" \
+ -f "$src_diff" 0 "$full_length" "$dest_diff" 0
+
+# On a per-destination error, status carries the errno and bytes_deduped
+# is zero (the huge offset fails VFS validation with EINVAL).
+
+expected="destination 0: bytes=0 status=-22"
+check_results "error: bytes zero on EINVAL" "$expected" \
+ -f "$src" 0 "$full_length" "$dest_interior" 9223372036854775807
+
+# Without the flag, the legacy reporting is unchanged: the requested
+# length is reported even when the VFS shortens the range.
+
+expected="destination 0: bytes=$full_length status=0
+destination 1: bytes=$full_length status=0"
+check_results "unflagged legacy reporting: full length" "$expected" \
+ "$src" 0 "$full_length" "$dest_interior" 0 "$dest_eof" 0
+
+# success, all done
+status=0
+exit
diff --git a/tests/generic/806.out b/tests/generic/806.out
new file mode 100644
index 00000000..5c436cb3
--- /dev/null
+++ b/tests/generic/806.out
@@ -0,0 +1,7 @@
+QA output created by 806
+zero length: 0 bytes, SAME
+mixed partial request: shortened, full
+sub-block request: zero progress, full
+differing contents: DIFFERS with advance hint
+error: bytes zero on EINVAL
+unflagged legacy reporting: full length
--
2.55.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-08-17 9:50 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-05 7:18 [PATCH 0/2] fstests: expect and test truthful FIDEDUPERANGE progress Matthias Goergens
2026-08-05 7:18 ` [PATCH 1/2] generic/517: expect truthful dedupe progress Matthias Goergens
2026-08-11 8:36 ` Zorro Lang
2026-08-05 7:18 ` [PATCH 2/2] generic: test FIDEDUPERANGE result reporting Matthias Goergens
2026-08-12 19:39 ` Zorro Lang
2026-08-17 9:50 ` [PATCH v2] generic: test FIDEDUPERANGE result reporting with REPORT_PROGRESS Matthias Goergens
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox