--- /dev/null
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * dio-writeback-race -- test direct IO writes with the contents of
+ * the buffer changed during writeback.
+ *
+ * Copyright (C) 2025 SUSE Linux Products GmbH.
+ */
+
+/*
+ * dio-writeback-race
+ *
+ * Compile:
+ *
+ * gcc -Wall -pthread -o dio-writeback-race dio-writeback-race.c
+ *
+ * Make sure filesystems with explicit data checksum can handle direct IO
+ * writes correctly, so that even the contents of the direct IO buffer changes
+ * during writeback, the fs should still work fine without data checksum mismatch.
+ * (Normally by falling back to buffer IO to keep the checksum and contents
+ * consistent)
+ *
+ * Usage:
+ *
+ * dio-writeback-race [-b <buffersize>] [-s <filesize>] <filename>
+ *
+ * Where:
+ *
+ * <filename> is the name of the test file to create, if the file exists
+ * it will be overwritten.
+ *
+ * <buffersize> is the buffer size for direct IO. Users are responsible to
+ * probe the correct direct IO size and alignment requirement.
+ * If not specified, the default value will be 4096.
+ *
+ * <filesize> is the total size of the test file. If not aligned to <blocksize>
+ * the result file size will be rounded up to <blocksize>.
+ * If not specified, the default value will be 64MiB.
+ */
+
+#include <fcntl.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <pthread.h>
+#include <unistd.h>
+#include <getopt.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/stat.h>
+
+static int fd = -1;
+static void *buf = NULL;
+static unsigned long long filesize = 64 * 1024 * 1024;
+static int blocksize = 4096;
+
+const int orig_pattern = 0xff;
+const int modify_pattern = 0x00;
+
+static void *do_io(void *arg)
+{
+ ssize_t ret;
+
+ ret = write(fd, buf, blocksize);
+ pthread_exit((void *)ret);
+}
+
+static void *do_modify(void *arg)
+{
+ memset(buf, modify_pattern, blocksize);
+ pthread_exit(NULL);
+}
+
+int main (int argc, char *argv[])
+{
+ pthread_t io_thread;
+ pthread_t modify_thread;
+ unsigned long long filepos;
+ int opt;
+ int ret = -EINVAL;
+
+ while ((opt = getopt(argc, argv, "b:s:")) > 0) {
+ switch (opt) {
+ case 'b':
+ blocksize = atoi(optarg);
+ if (blocksize == 0) {
+ fprintf(stderr, "invalid blocksize '%s'\n", optarg);
+ goto error;
+ }
+ break;
+ case 's':
+ filesize = atoll(optarg);
+ if (filesize == 0) {
+ fprintf(stderr, "invalid filesize '%s'\n", optarg);
+ goto error;
+ }
+ break;
+ default:
+ fprintf(stderr, "unknown option '%c'\n", opt);
+ goto error;
+ }
+ }
+ if (optind >= argc) {
+ fprintf(stderr, "missing argument\n");
+ goto error;
+ }
+ ret = posix_memalign(&buf, blocksize, blocksize);
+ if (!buf) {
+ fprintf(stderr, "failed to allocate aligned memory\n");
+ exit(EXIT_FAILURE);
+ }
+ fd = open(argv[optind], O_DIRECT | O_WRONLY | O_CREAT);
+ if (fd < 0) {
+ fprintf(stderr, "failed to open file '%s': %m\n", argv[optind]);
+ goto error;
+ }
+
+ for (filepos = 0; filepos < filesize; filepos += blocksize) {
+ void *retval = NULL;
+
+ memset(buf, orig_pattern, blocksize);
+ pthread_create(&io_thread, NULL, do_io, NULL);
+ pthread_create(&modify_thread, NULL, do_modify, NULL);
+ ret = pthread_join(io_thread, &retval);
+ if (ret) {
+ errno = ret;
+ ret = -ret;
+ fprintf(stderr, "failed to join io thread: %m\n");
+ goto error;
+ }
+ if ((ssize_t )retval < blocksize) {
+ ret = -EIO;
+ fprintf(stderr, "io thread failed\n");
+ goto error;
+ }
+ ret = pthread_join(modify_thread, NULL);
+ if (ret) {
+ errno = ret;
+ ret = -ret;
+ fprintf(stderr, "failed to join modify thread: %m\n");
+ goto error;
+ }
+ }
+error:
+ close(fd);
+ free(buf);
+ if (ret < 0)
+ return EXIT_FAILURE;
+ return EXIT_SUCCESS;
+}
--- /dev/null
+#! /bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2025 SUSE Linux Products GmbH. All Rights Reserved.
+#
+# FS QA Test 761
+#
+# Making sure direct IO (O_DIRECT) writes won't cause any data checksum mismatch,
+# even if the contents of the buffer changes during writeback.
+#
+# This is mostly for filesystems with data checksum support, which should fallback
+# to buffer IO to avoid inconsistency.
+# For filesystems without data checksum support, nothing needs to be bothered.
+#
+
+. ./common/preamble
+_begin_fstest auto quick
+
+_require_scratch
+_require_odirect
+_require_test_program dio-writeback-race
+_fixed_by_kernel_commit XXXXXXXX \
+ "btrfs: always fallback to buffered write if the inode requires checksum"
+
+_scratch_mkfs > $seqres.full 2>&1
+_scratch_mount
+
+blocksize=$(_get_file_block_size $SCRATCH_MNT)
+filesize=$(( 64 * 1024 * 1024))
+
+echo "blocksize=$blocksize filesize=$filesize" >> $seqres.full
+
+$here/src/dio-writeback-race -b $blocksize -s $filesize $SCRATCH_MNT/foobar
+
+# Read out the file, which should trigger checksum verification
+cat $SCRATCH_MNT/foobar > /dev/null
+
+echo "Silence is golden"
+
+# success, all done
+status=0
+exit