--- /dev/null
+// 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;
+}