diff mbox series

[08/10] generic: test file writers racing with FIDEDUPERANGE

Message ID 161526484769.1214319.11110389021630982078.stgit@magnolia (mailing list archive)
State Accepted, archived
Headers show
Series fstests: test kernel regressions fixed in 5.12 | expand

Commit Message

Darrick J. Wong March 9, 2021, 4:40 a.m. UTC
From: Darrick J. Wong <djwong@kernel.org>

Create a test to make sure that dedupe actually locks the file ranges
correctly before starting the content comparison and keeps them locked
until the operation completes.

Signed-off-by: Darrick J. Wong <djwong@kernel.org>
---
 src/Makefile          |    2 
 src/deduperace.c      |  370 +++++++++++++++++++++++++++++++++++++++++++++++++
 tests/generic/949     |   51 +++++++
 tests/generic/949.out |    2 
 tests/generic/group   |    1 
 5 files changed, 425 insertions(+), 1 deletion(-)
 create mode 100644 src/deduperace.c
 create mode 100755 tests/generic/949
 create mode 100644 tests/generic/949.out

Comments

Chandan Babu R March 12, 2021, 7:13 a.m. UTC | #1
On 09 Mar 2021 at 10:10, Darrick J. Wong wrote:
> From: Darrick J. Wong <djwong@kernel.org>
>
> Create a test to make sure that dedupe actually locks the file ranges
> correctly before starting the content comparison and keeps them locked
> until the operation completes.
>

Looks good to me.

Reviewed-by: Chandan Babu R <chandanrlinux@gmail.com>

> Signed-off-by: Darrick J. Wong <djwong@kernel.org>
> ---
>  src/Makefile          |    2 
>  src/deduperace.c      |  370 +++++++++++++++++++++++++++++++++++++++++++++++++
>  tests/generic/949     |   51 +++++++
>  tests/generic/949.out |    2 
>  tests/generic/group   |    1 
>  5 files changed, 425 insertions(+), 1 deletion(-)
>  create mode 100644 src/deduperace.c
>  create mode 100755 tests/generic/949
>  create mode 100644 tests/generic/949.out
>
>
> diff --git a/src/Makefile b/src/Makefile
> index 811b24e4..38ee6718 100644
> --- a/src/Makefile
> +++ b/src/Makefile
> @@ -21,7 +21,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 \
> +	locktest unwritten_mmap bulkstat_unlink_test deduperace \
>  	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/deduperace.c b/src/deduperace.c
> new file mode 100644
> index 00000000..b252d436
> --- /dev/null
> +++ b/src/deduperace.c
> @@ -0,0 +1,370 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Copyright (c) 2021 Oracle.  All Rights Reserved.
> + * Author: Darrick J. Wong <djwong@kernel.org>
> + *
> + * Race pwrite/mwrite with dedupe to see if we got the locking right.
> + *
> + * File writes and mmap writes should not be able to change the src_fd's
> + * contents after dedupe prep has verified that the file contents are the same.
> + */
> +#include <sys/types.h>
> +#include <sys/stat.h>
> +#include <sys/mman.h>
> +#include <sys/ioctl.h>
> +#include <linux/fs.h>
> +#include <string.h>
> +#include <stdio.h>
> +#include <unistd.h>
> +#include <fcntl.h>
> +#include <pthread.h>
> +#include <stdlib.h>
> +#include <errno.h>
> +
> +#define GOOD_BYTE		0x58
> +#define BAD_BYTE		0x66
> +
> +#ifndef FIDEDUPERANGE
> +/* extent-same (dedupe) ioctls; these MUST match the btrfs ioctl definitions */
> +#define FILE_DEDUPE_RANGE_SAME		0
> +#define FILE_DEDUPE_RANGE_DIFFERS	1
> +
> +/* from struct btrfs_ioctl_file_extent_same_info */
> +struct file_dedupe_range_info {
> +	__s64 dest_fd;		/* in - destination file */
> +	__u64 dest_offset;	/* in - start of extent in destination */
> +	__u64 bytes_deduped;	/* out - total # of bytes we were able
> +				 * to dedupe from this file. */
> +	/* status of this dedupe operation:
> +	 * < 0 for error
> +	 * == FILE_DEDUPE_RANGE_SAME if dedupe succeeds
> +	 * == FILE_DEDUPE_RANGE_DIFFERS if data differs
> +	 */
> +	__s32 status;		/* out - see above description */
> +	__u32 reserved;		/* must be zero */
> +};
> +
> +/* from struct btrfs_ioctl_file_extent_same_args */
> +struct file_dedupe_range {
> +	__u64 src_offset;	/* in - start of extent in source */
> +	__u64 src_length;	/* in - length of extent */
> +	__u16 dest_count;	/* in - total elements in info array */
> +	__u16 reserved1;	/* must be zero */
> +	__u32 reserved2;	/* must be zero */
> +	struct file_dedupe_range_info info[0];
> +};
> +#define FIDEDUPERANGE	_IOWR(0x94, 54, struct file_dedupe_range)
> +#endif /* FIDEDUPERANGE */
> +
> +static int fd1, fd2;
> +static loff_t offset = 37; /* Nice low offset to trick the compare */
> +static loff_t blksz;
> +
> +/* Continuously dirty the pagecache for the region being dupe-tested. */
> +void *
> +mwriter(
> +	void		*data)
> +{
> +	volatile char	*p;
> +
> +	p = mmap(NULL, blksz, PROT_WRITE, MAP_SHARED, fd1, 0);
> +	if (p == MAP_FAILED) {
> +		perror("mmap");
> +		exit(2);
> +	}
> +
> +	while (1) {
> +		*(p + offset) = BAD_BYTE;
> +		*(p + offset) = GOOD_BYTE;
> +	}
> +}
> +
> +/* Continuously write to the region being dupe-tested. */
> +void *
> +pwriter(
> +	void		*data)
> +{
> +	char		v;
> +	ssize_t		sz;
> +
> +	while (1) {
> +		v = BAD_BYTE;
> +		sz = pwrite(fd1, &v, sizeof(v), offset);
> +		if (sz != sizeof(v)) {
> +			perror("pwrite0");
> +			exit(2);
> +		}
> +
> +		v = GOOD_BYTE;
> +		sz = pwrite(fd1, &v, sizeof(v), offset);
> +		if (sz != sizeof(v)) {
> +			perror("pwrite1");
> +			exit(2);
> +		}
> +	}
> +
> +	return NULL;
> +}
> +
> +static inline void
> +complain(
> +	loff_t	offset,
> +	char	bad)
> +{
> +	fprintf(stderr, "ASSERT: offset %llu should be 0x%x, got 0x%x!\n",
> +			(unsigned long long)offset, GOOD_BYTE, bad);
> +	abort();
> +}
> +
> +/* Make sure the destination file pagecache never changes. */
> +void *
> +mreader(
> +	void		*data)
> +{
> +	volatile char	*p;
> +
> +	p = mmap(NULL, blksz, PROT_READ, MAP_SHARED, fd2, 0);
> +	if (p == MAP_FAILED) {
> +		perror("mmap");
> +		exit(2);
> +	}
> +
> +	while (1) {
> +		if (*(p + offset) != GOOD_BYTE)
> +			complain(offset, *(p + offset));
> +	}
> +}
> +
> +/* Make sure the destination file never changes. */
> +void *
> +preader(
> +	void		*data)
> +{
> +	char		v;
> +	ssize_t		sz;
> +
> +	while (1) {
> +		sz = pread(fd2, &v, sizeof(v), offset);
> +		if (sz != sizeof(v)) {
> +			perror("pwrite0");
> +			exit(2);
> +		}
> +
> +		if (v != GOOD_BYTE)
> +			complain(offset, v);
> +	}
> +
> +	return NULL;
> +}
> +
> +void
> +print_help(const char *progname)
> +{
> +	printf("Usage: %s [-b blksz] [-c dir] [-n nr_ops] [-o offset] [-r] [-w] [-v]\n",
> +			progname);
> +	printf("-b sets the block size (default is autoconfigured)\n");
> +	printf("-c chdir to this path before starting\n");
> +	printf("-n controls the number of dedupe ops (default 10000)\n");
> +	printf("-o reads and writes to this offset (default 37)\n");
> +	printf("-r uses pread instead of mmap read.\n");
> +	printf("-v prints status updates.\n");
> +	printf("-w uses pwrite instead of mmap write.\n");
> +}
> +
> +int
> +main(
> +	int		argc,
> +	char		*argv[])
> +{
> +	struct file_dedupe_range *fdr;
> +	char		*Xbuf;
> +	void		*(*reader_fn)(void *) = mreader;
> +	void		*(*writer_fn)(void *) = mwriter;
> +	unsigned long	same = 0;
> +	unsigned long	differs = 0;
> +	unsigned long	i, nr_ops = 10000;
> +	ssize_t		sz;
> +	pthread_t	reader, writer;
> +	int		verbose = 0;
> +	int		c;
> +	int		ret;
> +
> +	while ((c = getopt(argc, argv, "b:c:n:o:rvw")) != -1) {
> +		switch (c) {
> +		case 'b':
> +			errno = 0;
> +			blksz = strtoul(optarg, NULL, 0);
> +			if (errno) {
> +				perror(optarg);
> +				exit(1);
> +			}
> +			break;
> +		case 'c':
> +			ret = chdir(optarg);
> +			if (ret) {
> +				perror("chdir");
> +				exit(1);
> +			}
> +			break;
> +		case 'n':
> +			errno = 0;
> +			nr_ops = strtoul(optarg, NULL, 0);
> +			if (errno) {
> +				perror(optarg);
> +				exit(1);
> +			}
> +			break;
> +		case 'o':
> +			errno = 0;
> +			offset = strtoul(optarg, NULL, 0);
> +			if (errno) {
> +				perror(optarg);
> +				exit(1);
> +			}
> +			break;
> +		case 'r':
> +			reader_fn = preader;
> +			break;
> +		case 'v':
> +			verbose = 1;
> +			break;
> +		case 'w':
> +			writer_fn = pwriter;
> +			break;
> +		default:
> +			print_help(argv[0]);
> +			exit(1);
> +			break;
> +		}
> +	}
> +
> +	fdr = malloc(sizeof(struct file_dedupe_range) +
> +			sizeof(struct file_dedupe_range_info));
> +	if (!fdr) {
> +		perror("malloc");
> +		exit(1);
> +	}
> +
> +	/* Initialize both files. */
> +	fd1 = open("file1", O_RDWR | O_CREAT | O_TRUNC | O_NOATIME, 0600);
> +	if (fd1 < 0) {
> +		perror("file1");
> +		exit(1);
> +	}
> +
> +	fd2 = open("file2", O_RDWR | O_CREAT | O_TRUNC | O_NOATIME, 0600);
> +	if (fd2 < 0) {
> +		perror("file2");
> +		exit(1);
> +	}
> +
> +	if (blksz <= 0) {
> +		struct stat	statbuf;
> +
> +		ret = fstat(fd1, &statbuf);
> +		if (ret) {
> +			perror("file1 stat");
> +			exit(1);
> +		}
> +		blksz = statbuf.st_blksize;
> +	}
> +
> +	if (offset >= blksz) {
> +		fprintf(stderr, "offset (%llu) < blksz (%llu)?\n",
> +				(unsigned long long)offset,
> +				(unsigned long long)blksz);
> +		exit(1);
> +	}
> +
> +	Xbuf = malloc(blksz);
> +	if (!Xbuf) {
> +		perror("malloc buffer");
> +		exit(1);
> +	}
> +	memset(Xbuf, GOOD_BYTE, blksz);
> +
> +	sz = pwrite(fd1, Xbuf, blksz, 0);
> +	if (sz != blksz) {
> +		perror("file1 write");
> +		exit(1);
> +	}
> +
> +	sz = pwrite(fd2, Xbuf, blksz, 0);
> +	if (sz != blksz) {
> +		perror("file2 write");
> +		exit(1);
> +	}
> +
> +	ret = fsync(fd1);
> +	if (ret) {
> +		perror("file1 fsync");
> +		exit(1);
> +	}
> +
> +	ret = fsync(fd2);
> +	if (ret) {
> +		perror("file2 fsync");
> +		exit(1);
> +	}
> +
> +	/* Start our reader and writer threads. */
> +	ret = pthread_create(&reader, NULL, reader_fn, NULL);
> +	if (ret) {
> +		fprintf(stderr, "rthread: %s\n", strerror(ret));
> +		exit(1);
> +	}
> +
> +	ret = pthread_create(&writer, NULL, writer_fn, NULL);
> +	if (ret) {
> +		fprintf(stderr, "wthread: %s\n", strerror(ret));
> +		exit(1);
> +	}
> +
> +	/*
> +	 * Now start deduping.  If the contents match, fd1's blocks will be
> +	 * remapped into fd2, which is why the writer thread targets fd1 and
> +	 * the reader checks fd2 to make sure that none of fd1's writes ever
> +	 * make it into fd2.
> +	 */
> +	for (i = 1; i <= nr_ops; i++) {
> +		fdr->src_offset = 0;
> +		fdr->src_length = blksz;
> +		fdr->dest_count = 1;
> +		fdr->reserved1 = 0;
> +		fdr->reserved2 = 0;
> +		fdr->info[0].dest_fd = fd2;
> +		fdr->info[0].dest_offset = 0;
> +		fdr->info[0].reserved = 0;
> +
> +		ret = ioctl(fd1, FIDEDUPERANGE, fdr);
> +		if (ret) {
> +			perror("dedupe");
> +			exit(2);
> +		}
> +
> +		switch (fdr->info[0].status) {
> +		case FILE_DEDUPE_RANGE_DIFFERS:
> +			differs++;
> +			break;
> +		case FILE_DEDUPE_RANGE_SAME:
> +			same++;
> +			break;
> +		default:
> +			fprintf(stderr, "deduperange: %s\n",
> +					strerror(-fdr->info[0].status));
> +			exit(2);
> +			break;
> +		}
> +
> +		if (verbose && (i % 337) == 0)
> +			printf("nr_ops: %lu; same: %lu; differs: %lu\n",
> +					i, same, differs);
> +	}
> +
> +	if (verbose)
> +		printf("nr_ops: %lu; same: %lu; differs: %lu\n", i - 1, same,
> +				differs);
> +
> +	/* Program termination will kill the threads and close the files. */
> +	return 0;
> +}
> diff --git a/tests/generic/949 b/tests/generic/949
> new file mode 100755
> index 00000000..3951490b
> --- /dev/null
> +++ b/tests/generic/949
> @@ -0,0 +1,51 @@
> +#! /bin/bash
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +# Copyright (c) 2021 Oracle.  All Rights Reserved.
> +#
> +# FS QA Test No. 949
> +#
> +# Make sure that mmap and file writers racing with FIDEDUPERANGE cannot write
> +# to the file after the dedupe prep function has decided that the file contents
> +# are identical and we can therefore go ahead with the remapping.
> +
> +seq=`basename $0`
> +seqres=$RESULT_DIR/$seq
> +echo "QA output created by $seq"
> +
> +here=`pwd`
> +tmp=/tmp/$$
> +status=1    # failure is the default!
> +trap "_cleanup; exit \$status" 0 1 2 3 15
> +
> +_cleanup()
> +{
> +	cd /
> +	rm -f $tmp.*
> +}
> +
> +# get standard environment, filters and checks
> +. ./common/rc
> +. ./common/reflink
> +
> +# real QA test starts here
> +_supported_fs generic
> +_require_scratch_dedupe
> +
> +rm -f $seqres.full
> +
> +nr_ops=$((TIME_FACTOR * 10000))
> +
> +# Format filesystem
> +_scratch_mkfs > $seqres.full
> +_scratch_mount
> +
> +# Test once with mmap writes
> +$here/src/deduperace -c $SCRATCH_MNT -n $nr_ops
> +
> +# Test again with pwrites for the lulz
> +$here/src/deduperace -c $SCRATCH_MNT -n $nr_ops -w
> +
> +echo Silence is golden.
> +# success, all done
> +status=0
> +exit
> diff --git a/tests/generic/949.out b/tests/generic/949.out
> new file mode 100644
> index 00000000..2998b46c
> --- /dev/null
> +++ b/tests/generic/949.out
> @@ -0,0 +1,2 @@
> +QA output created by 949
> +Silence is golden.
> diff --git a/tests/generic/group b/tests/generic/group
> index d5cfdd51..778aa8c4 100644
> --- a/tests/generic/group
> +++ b/tests/generic/group
> @@ -630,3 +630,4 @@
>  625 auto quick verity
>  947 auto quick rw clone
>  948 auto quick rw copy_range
> +949 auto quick rw dedupe clone
diff mbox series

Patch

diff --git a/src/Makefile b/src/Makefile
index 811b24e4..38ee6718 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -21,7 +21,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 \
+	locktest unwritten_mmap bulkstat_unlink_test deduperace \
 	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/deduperace.c b/src/deduperace.c
new file mode 100644
index 00000000..b252d436
--- /dev/null
+++ b/src/deduperace.c
@@ -0,0 +1,370 @@ 
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2021 Oracle.  All Rights Reserved.
+ * Author: Darrick J. Wong <djwong@kernel.org>
+ *
+ * Race pwrite/mwrite with dedupe to see if we got the locking right.
+ *
+ * File writes and mmap writes should not be able to change the src_fd's
+ * contents after dedupe prep has verified that the file contents are the same.
+ */
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <sys/ioctl.h>
+#include <linux/fs.h>
+#include <string.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <stdlib.h>
+#include <errno.h>
+
+#define GOOD_BYTE		0x58
+#define BAD_BYTE		0x66
+
+#ifndef FIDEDUPERANGE
+/* extent-same (dedupe) ioctls; these MUST match the btrfs ioctl definitions */
+#define FILE_DEDUPE_RANGE_SAME		0
+#define FILE_DEDUPE_RANGE_DIFFERS	1
+
+/* from struct btrfs_ioctl_file_extent_same_info */
+struct file_dedupe_range_info {
+	__s64 dest_fd;		/* in - destination file */
+	__u64 dest_offset;	/* in - start of extent in destination */
+	__u64 bytes_deduped;	/* out - total # of bytes we were able
+				 * to dedupe from this file. */
+	/* status of this dedupe operation:
+	 * < 0 for error
+	 * == FILE_DEDUPE_RANGE_SAME if dedupe succeeds
+	 * == FILE_DEDUPE_RANGE_DIFFERS if data differs
+	 */
+	__s32 status;		/* out - see above description */
+	__u32 reserved;		/* must be zero */
+};
+
+/* from struct btrfs_ioctl_file_extent_same_args */
+struct file_dedupe_range {
+	__u64 src_offset;	/* in - start of extent in source */
+	__u64 src_length;	/* in - length of extent */
+	__u16 dest_count;	/* in - total elements in info array */
+	__u16 reserved1;	/* must be zero */
+	__u32 reserved2;	/* must be zero */
+	struct file_dedupe_range_info info[0];
+};
+#define FIDEDUPERANGE	_IOWR(0x94, 54, struct file_dedupe_range)
+#endif /* FIDEDUPERANGE */
+
+static int fd1, fd2;
+static loff_t offset = 37; /* Nice low offset to trick the compare */
+static loff_t blksz;
+
+/* Continuously dirty the pagecache for the region being dupe-tested. */
+void *
+mwriter(
+	void		*data)
+{
+	volatile char	*p;
+
+	p = mmap(NULL, blksz, PROT_WRITE, MAP_SHARED, fd1, 0);
+	if (p == MAP_FAILED) {
+		perror("mmap");
+		exit(2);
+	}
+
+	while (1) {
+		*(p + offset) = BAD_BYTE;
+		*(p + offset) = GOOD_BYTE;
+	}
+}
+
+/* Continuously write to the region being dupe-tested. */
+void *
+pwriter(
+	void		*data)
+{
+	char		v;
+	ssize_t		sz;
+
+	while (1) {
+		v = BAD_BYTE;
+		sz = pwrite(fd1, &v, sizeof(v), offset);
+		if (sz != sizeof(v)) {
+			perror("pwrite0");
+			exit(2);
+		}
+
+		v = GOOD_BYTE;
+		sz = pwrite(fd1, &v, sizeof(v), offset);
+		if (sz != sizeof(v)) {
+			perror("pwrite1");
+			exit(2);
+		}
+	}
+
+	return NULL;
+}
+
+static inline void
+complain(
+	loff_t	offset,
+	char	bad)
+{
+	fprintf(stderr, "ASSERT: offset %llu should be 0x%x, got 0x%x!\n",
+			(unsigned long long)offset, GOOD_BYTE, bad);
+	abort();
+}
+
+/* Make sure the destination file pagecache never changes. */
+void *
+mreader(
+	void		*data)
+{
+	volatile char	*p;
+
+	p = mmap(NULL, blksz, PROT_READ, MAP_SHARED, fd2, 0);
+	if (p == MAP_FAILED) {
+		perror("mmap");
+		exit(2);
+	}
+
+	while (1) {
+		if (*(p + offset) != GOOD_BYTE)
+			complain(offset, *(p + offset));
+	}
+}
+
+/* Make sure the destination file never changes. */
+void *
+preader(
+	void		*data)
+{
+	char		v;
+	ssize_t		sz;
+
+	while (1) {
+		sz = pread(fd2, &v, sizeof(v), offset);
+		if (sz != sizeof(v)) {
+			perror("pwrite0");
+			exit(2);
+		}
+
+		if (v != GOOD_BYTE)
+			complain(offset, v);
+	}
+
+	return NULL;
+}
+
+void
+print_help(const char *progname)
+{
+	printf("Usage: %s [-b blksz] [-c dir] [-n nr_ops] [-o offset] [-r] [-w] [-v]\n",
+			progname);
+	printf("-b sets the block size (default is autoconfigured)\n");
+	printf("-c chdir to this path before starting\n");
+	printf("-n controls the number of dedupe ops (default 10000)\n");
+	printf("-o reads and writes to this offset (default 37)\n");
+	printf("-r uses pread instead of mmap read.\n");
+	printf("-v prints status updates.\n");
+	printf("-w uses pwrite instead of mmap write.\n");
+}
+
+int
+main(
+	int		argc,
+	char		*argv[])
+{
+	struct file_dedupe_range *fdr;
+	char		*Xbuf;
+	void		*(*reader_fn)(void *) = mreader;
+	void		*(*writer_fn)(void *) = mwriter;
+	unsigned long	same = 0;
+	unsigned long	differs = 0;
+	unsigned long	i, nr_ops = 10000;
+	ssize_t		sz;
+	pthread_t	reader, writer;
+	int		verbose = 0;
+	int		c;
+	int		ret;
+
+	while ((c = getopt(argc, argv, "b:c:n:o:rvw")) != -1) {
+		switch (c) {
+		case 'b':
+			errno = 0;
+			blksz = strtoul(optarg, NULL, 0);
+			if (errno) {
+				perror(optarg);
+				exit(1);
+			}
+			break;
+		case 'c':
+			ret = chdir(optarg);
+			if (ret) {
+				perror("chdir");
+				exit(1);
+			}
+			break;
+		case 'n':
+			errno = 0;
+			nr_ops = strtoul(optarg, NULL, 0);
+			if (errno) {
+				perror(optarg);
+				exit(1);
+			}
+			break;
+		case 'o':
+			errno = 0;
+			offset = strtoul(optarg, NULL, 0);
+			if (errno) {
+				perror(optarg);
+				exit(1);
+			}
+			break;
+		case 'r':
+			reader_fn = preader;
+			break;
+		case 'v':
+			verbose = 1;
+			break;
+		case 'w':
+			writer_fn = pwriter;
+			break;
+		default:
+			print_help(argv[0]);
+			exit(1);
+			break;
+		}
+	}
+
+	fdr = malloc(sizeof(struct file_dedupe_range) +
+			sizeof(struct file_dedupe_range_info));
+	if (!fdr) {
+		perror("malloc");
+		exit(1);
+	}
+
+	/* Initialize both files. */
+	fd1 = open("file1", O_RDWR | O_CREAT | O_TRUNC | O_NOATIME, 0600);
+	if (fd1 < 0) {
+		perror("file1");
+		exit(1);
+	}
+
+	fd2 = open("file2", O_RDWR | O_CREAT | O_TRUNC | O_NOATIME, 0600);
+	if (fd2 < 0) {
+		perror("file2");
+		exit(1);
+	}
+
+	if (blksz <= 0) {
+		struct stat	statbuf;
+
+		ret = fstat(fd1, &statbuf);
+		if (ret) {
+			perror("file1 stat");
+			exit(1);
+		}
+		blksz = statbuf.st_blksize;
+	}
+
+	if (offset >= blksz) {
+		fprintf(stderr, "offset (%llu) < blksz (%llu)?\n",
+				(unsigned long long)offset,
+				(unsigned long long)blksz);
+		exit(1);
+	}
+
+	Xbuf = malloc(blksz);
+	if (!Xbuf) {
+		perror("malloc buffer");
+		exit(1);
+	}
+	memset(Xbuf, GOOD_BYTE, blksz);
+
+	sz = pwrite(fd1, Xbuf, blksz, 0);
+	if (sz != blksz) {
+		perror("file1 write");
+		exit(1);
+	}
+
+	sz = pwrite(fd2, Xbuf, blksz, 0);
+	if (sz != blksz) {
+		perror("file2 write");
+		exit(1);
+	}
+
+	ret = fsync(fd1);
+	if (ret) {
+		perror("file1 fsync");
+		exit(1);
+	}
+
+	ret = fsync(fd2);
+	if (ret) {
+		perror("file2 fsync");
+		exit(1);
+	}
+
+	/* Start our reader and writer threads. */
+	ret = pthread_create(&reader, NULL, reader_fn, NULL);
+	if (ret) {
+		fprintf(stderr, "rthread: %s\n", strerror(ret));
+		exit(1);
+	}
+
+	ret = pthread_create(&writer, NULL, writer_fn, NULL);
+	if (ret) {
+		fprintf(stderr, "wthread: %s\n", strerror(ret));
+		exit(1);
+	}
+
+	/*
+	 * Now start deduping.  If the contents match, fd1's blocks will be
+	 * remapped into fd2, which is why the writer thread targets fd1 and
+	 * the reader checks fd2 to make sure that none of fd1's writes ever
+	 * make it into fd2.
+	 */
+	for (i = 1; i <= nr_ops; i++) {
+		fdr->src_offset = 0;
+		fdr->src_length = blksz;
+		fdr->dest_count = 1;
+		fdr->reserved1 = 0;
+		fdr->reserved2 = 0;
+		fdr->info[0].dest_fd = fd2;
+		fdr->info[0].dest_offset = 0;
+		fdr->info[0].reserved = 0;
+
+		ret = ioctl(fd1, FIDEDUPERANGE, fdr);
+		if (ret) {
+			perror("dedupe");
+			exit(2);
+		}
+
+		switch (fdr->info[0].status) {
+		case FILE_DEDUPE_RANGE_DIFFERS:
+			differs++;
+			break;
+		case FILE_DEDUPE_RANGE_SAME:
+			same++;
+			break;
+		default:
+			fprintf(stderr, "deduperange: %s\n",
+					strerror(-fdr->info[0].status));
+			exit(2);
+			break;
+		}
+
+		if (verbose && (i % 337) == 0)
+			printf("nr_ops: %lu; same: %lu; differs: %lu\n",
+					i, same, differs);
+	}
+
+	if (verbose)
+		printf("nr_ops: %lu; same: %lu; differs: %lu\n", i - 1, same,
+				differs);
+
+	/* Program termination will kill the threads and close the files. */
+	return 0;
+}
diff --git a/tests/generic/949 b/tests/generic/949
new file mode 100755
index 00000000..3951490b
--- /dev/null
+++ b/tests/generic/949
@@ -0,0 +1,51 @@ 
+#! /bin/bash
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (c) 2021 Oracle.  All Rights Reserved.
+#
+# FS QA Test No. 949
+#
+# Make sure that mmap and file writers racing with FIDEDUPERANGE cannot write
+# to the file after the dedupe prep function has decided that the file contents
+# are identical and we can therefore go ahead with the remapping.
+
+seq=`basename $0`
+seqres=$RESULT_DIR/$seq
+echo "QA output created by $seq"
+
+here=`pwd`
+tmp=/tmp/$$
+status=1    # failure is the default!
+trap "_cleanup; exit \$status" 0 1 2 3 15
+
+_cleanup()
+{
+	cd /
+	rm -f $tmp.*
+}
+
+# get standard environment, filters and checks
+. ./common/rc
+. ./common/reflink
+
+# real QA test starts here
+_supported_fs generic
+_require_scratch_dedupe
+
+rm -f $seqres.full
+
+nr_ops=$((TIME_FACTOR * 10000))
+
+# Format filesystem
+_scratch_mkfs > $seqres.full
+_scratch_mount
+
+# Test once with mmap writes
+$here/src/deduperace -c $SCRATCH_MNT -n $nr_ops
+
+# Test again with pwrites for the lulz
+$here/src/deduperace -c $SCRATCH_MNT -n $nr_ops -w
+
+echo Silence is golden.
+# success, all done
+status=0
+exit
diff --git a/tests/generic/949.out b/tests/generic/949.out
new file mode 100644
index 00000000..2998b46c
--- /dev/null
+++ b/tests/generic/949.out
@@ -0,0 +1,2 @@ 
+QA output created by 949
+Silence is golden.
diff --git a/tests/generic/group b/tests/generic/group
index d5cfdd51..778aa8c4 100644
--- a/tests/generic/group
+++ b/tests/generic/group
@@ -630,3 +630,4 @@ 
 625 auto quick verity
 947 auto quick rw clone
 948 auto quick rw copy_range
+949 auto quick rw dedupe clone