Linux Btrfs filesystem development
 help / color / mirror / Atom feed
From: Matthias Goergens <matthias.goergens@gmail.com>
To: fstests@vger.kernel.org
Cc: zlang@kernel.org, fdmanana@kernel.org, linux-xfs@vger.kernel.org,
	linux-btrfs@vger.kernel.org
Subject: [PATCH v2] generic: test FIDEDUPERANGE result reporting with REPORT_PROGRESS
Date: Mon, 17 Aug 2026 17:50:17 +0800	[thread overview]
Message-ID: <20260817095017.948979-1-matthias.goergens@gmail.com> (raw)
In-Reply-To: <20260805071839.3423779-1-matthias.goergens@gmail.com>

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


      parent reply	other threads:[~2026-08-17  9:50 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
     [not found] <20260805071839.3423779-1-matthias.goergens@gmail.com>
     [not found] ` <20260805071839.3423779-2-matthias.goergens@gmail.com>
2026-08-11  8:36   ` [PATCH 1/2] generic/517: expect truthful dedupe progress Zorro Lang
2026-08-17  9:50 ` Matthias Goergens [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260817095017.948979-1-matthias.goergens@gmail.com \
    --to=matthias.goergens@gmail.com \
    --cc=fdmanana@kernel.org \
    --cc=fstests@vger.kernel.org \
    --cc=linux-btrfs@vger.kernel.org \
    --cc=linux-xfs@vger.kernel.org \
    --cc=zlang@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox