* [PATCH 1/1] xfs: add test for ENOSR/ENOSPC on DIO into delalloc extents
@ 2026-10-16 9:16 Lukas Herbolt
2026-10-22 13:26 ` Brian Foster
2026-10-27 8:33 ` Zorro Lang
0 siblings, 2 replies; 5+ messages in thread
From: Lukas Herbolt @ 2026-10-16 9:16 UTC (permalink / raw)
To: zlang; +Cc: fstests, hch, linux-xfs, Eric Sandeen
Test if a DIO extending write into a speculative delalloc region on a
fragmented XFS filesystem does not trigger a dirty transaction cancel
(-ENOSR) or return a spurious -ENOSPC.
With allocsize=128MB, a buffered write creates a large delalloc extent.
A subsequent DIO write at i_size lands in the speculative prealloc
region. On a fragmented filesystem, xfs_bmapi_write converts the
delalloc starting from offset 0 but the allocation is too short to
reach the DIO offset, causing either -ENOSR (dirty transaction cancel
and filesystem shutdown) or -ENOSPC (wrong error from xfs_bmapi_write).
Assisted by LLM.
Fixed by kernel commits:
2a9b99d45be0 xfs: pass the actual offset and len to xfs_bmapi_allocate
21255afdd729 xfs: do not allocate the entire delalloc extent
6773da870ab8 xfs: fix error returns from xfs_bmapi_write
Co-developed-by: Eric Sandeen <sandeen@redhat.com>
Signed-off-by: Eric Sandeen <sandeen@redhat.com>
Signed-off-by: Lukas Herbolt <lherbolt@redhat.com>
---
.gitignore | 2 +
src/Makefile | 3 +-
src/buffer_direct_io.c | 154 ++++++++++++++++++++++++++++++++
src/fragment_fs.c | 101 +++++++++++++++++++++
src/mix_buffer_direct_io.c | 175 +++++++++++++++++++++++++++++++++++++
tests/xfs/842 | 63 +++++++++++++
tests/xfs/842.out | 2 +
7 files changed, 499 insertions(+), 1 deletion(-)
create mode 100644 src/buffer_direct_io.c
create mode 100644 src/fragment_fs.c
create mode 100644 src/mix_buffer_direct_io.c
create mode 100755 tests/xfs/842
create mode 100644 tests/xfs/842.out
diff --git a/.gitignore b/.gitignore
index d52ba4ad3085..eafc36c7d8ab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -217,6 +217,8 @@ tags
/src/t_btrfs_received_uuid_ioctl
/src/btrfs_ioctl
/src/dio-read-into-mmap
+/src/buffer_direct_io
+/src/fragment_fs
# Symlinked files
/tests/generic/035.out
diff --git a/src/Makefile b/src/Makefile
index 0ff607ae1593..c8964a59a539 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -21,7 +21,8 @@ TARGETS = dirstress fill fill2 getpagesize holes lstat64 \
t_mmap_writev_overlap checkpoint_journal mmap-rw-fault allocstale \
t_mmap_cow_memory_failure fake-dump-rootino dio-buf-fault rewinddir-test \
readdir-while-renames dio-append-buf-fault dio-write-fsync-same-fd \
- dio-writeback-race unlink-fsync truncate dio-read-into-mmap
+ dio-writeback-race unlink-fsync truncate dio-read-into-mmap \
+ mix_buffer_direct_io fragment_fs
LINUX_TARGETS = xfsctl bstat t_mtab getdevicesize preallo_rw_pattern_reader \
preallo_rw_pattern_writer ftrunc trunc fs_perms testx looptest \
diff --git a/src/buffer_direct_io.c b/src/buffer_direct_io.c
new file mode 100644
index 000000000000..01b19994a2e2
--- /dev/null
+++ b/src/buffer_direct_io.c
@@ -0,0 +1,154 @@
+/*
+ * Mixer of buffered and direct IO originally written to test for XFS -ENOSPC
+ * and -ENOSR dirty transaction cancel on fragmented filesystem.
+ *
+ * XFS notes:
+ * Buffered write size. With allocsize=128MB, the buffered write creates
+ * a delalloc extent of [0, BUF_WRITE_SIZE + 128MB). The DIO at i_size
+ * is at file offset BUF_WRITE_SIZE. xfs_bmapi_allocate (RHEL 8) starts
+ * the delalloc conversion from offset 0; the allocation must span
+ * BUF_WRITE_SIZE/blocksize blocks in one contiguous extent to reach the
+ * DIO offset. On a fragmented FS that's impossible — but the FS may
+ * still have large free extents in metadata-sparse regions (20-40 MB is
+ * common). 64 MB puts the DIO offset safely beyond those.
+
+
+ *
+ * Usage:
+ * ./mix_buffer_direct_io $SCRATCH_MNT
+ */
+
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/stat.h>
+#include <sys/statvfs.h>
+
+#define BUF_WRITE_SIZE (64 * 1024 * 1024) /* 64 MB */
+
+/* DIO write size — must be block-aligned */
+#define DIO_SIZE 4096
+
+static int attempt_trigger(const char *dir, int attempt)
+{
+ char filepath[4096];
+ char *wbuf;
+ void *dbuf;
+ int bfd, dfd, ret;
+
+ snprintf(filepath, sizeof(filepath), "%s/target_%d", dir, attempt);
+
+ wbuf = calloc(1, BUF_WRITE_SIZE);
+ if (!wbuf) {
+ perror("calloc");
+ return -1;
+ }
+
+ if (posix_memalign(&dbuf, DIO_SIZE, DIO_SIZE)) {
+ perror("posix_memalign");
+ free(wbuf);
+ return -1;
+ }
+ memset(dbuf, 0xbb, DIO_SIZE);
+
+ /*
+ * Open both fds up front. Keep the buffered fd open across the
+ * DIO write so close() can't trigger writeback that converts the
+ * delalloc before we hit it with DIO.
+ */
+ bfd = open(filepath, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ if (bfd < 0) {
+ if (errno == EIO || errno == EROFS) {
+ fprintf(stderr, "Filesystem shut down!\n");
+ free(wbuf); free(dbuf);
+ return 1;
+ }
+ perror("open for buffered write");
+ free(wbuf); free(dbuf);
+ return -1;
+ }
+
+ dfd = open(filepath, O_WRONLY | O_DIRECT);
+ if (dfd < 0) {
+ if (errno == EIO || errno == EROFS) {
+ fprintf(stderr, "Filesystem shut down!\n");
+ close(bfd);
+ free(wbuf); free(dbuf);
+ return 1;
+ }
+ perror("open for DIO");
+ close(bfd);
+ unlink(filepath);
+ free(wbuf); free(dbuf);
+ return -1;
+ }
+
+ /*
+ * Step 1: One buffered write.
+ * On XFS with allocsize=128MB, this creates a delalloc extent of
+ * [0, BUF_WRITE_SIZE + 128MB) but only [0, BUF_WRITE_SIZE) has pages.
+ * The rest is speculative prealloc.
+ */
+ ret = pwrite(bfd, wbuf, BUF_WRITE_SIZE, 0);
+ if (ret < 0) {
+ if (errno == ENOSPC) {
+ fprintf(stderr, "attempt %d: ENOSPC on buffered write\n",
+ attempt);
+ close(dfd);
+ close(bfd);
+ unlink(filepath);
+ free(wbuf); free(dbuf);
+ return 0;
+ }
+ perror("buffered pwrite");
+ close(dfd);
+ close(bfd);
+ free(wbuf); free(dbuf);
+ return -1;
+ }
+
+ /*
+ * Step 2: DIO extending write at i_size.
+ *
+ * Offset BUF_WRITE_SIZE is within the speculative delalloc but has no
+ * pages, so the DIO flush is a no-op. xfs_bmapi_write finds the
+ * delalloc starting at offset 0 and tries to convert from there. On
+ * our fragmented FS the allocation is 1 block at file offset 0,
+ * nowhere near the DIO offset at block 256. Returns -ENOSPC or -ENOSR.
+ */
+ ret = pwrite(dfd, dbuf, DIO_SIZE, BUF_WRITE_SIZE);
+ if (ret < 0) {
+ fprintf(stderr, "DIO pwrite failed: %s\n", strerror(errno));
+ close(dfd);
+ close(bfd);
+ free(wbuf); free(dbuf);
+ return errno;
+ }
+ close(dfd);
+ close(bfd);
+ unlink(filepath);
+ free(wbuf);
+ free(dbuf);
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ int i, ret;
+
+ if (argc != 2) {
+ fprintf(stderr, " %s <directory>\n", argv[0]);
+ return 1;
+ }
+
+ for (i = 0; i < 1000; i++) {
+ ret = attempt_trigger(argv[1], i);
+ if (ret != 0)
+ return ret;
+ }
+ return 0;
+}
diff --git a/src/fragment_fs.c b/src/fragment_fs.c
new file mode 100644
index 000000000000..cee4edef73ff
--- /dev/null
+++ b/src/fragment_fs.c
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (c) 2026 Eric Sandeen, Lukas Herbolt
+ */
+
+/*
+ * Fragment filesystem by creating fixed-size files and then deleting
+ * every n-th one.
+ *
+ * Usage:
+ * fragment_fs <directory> <prefix> <file size> <delete every n-th file>
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/statvfs.h>
+
+static int fragment_fs(const char *dir, const char *prefix,
+ off_t size, int delete_nth)
+{
+ char path[4096];
+ int i, fd;
+ int created = 0;
+
+ fprintf(stderr, "Fragmenting filesystem (filling to ENOSPC)...\n");
+
+ for (i = 0; ; i++) {
+ snprintf(path, sizeof(path), "%s/%s_%06d", dir, prefix, i);
+ fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ if (fd < 0) {
+ if (errno == ENOSPC)
+ break;
+ perror("open frag file");
+ return -1;
+ }
+ if (fallocate(fd, 0, 0, size) < 0) {
+ close(fd);
+ unlink(path);
+ if (errno == ENOSPC)
+ break;
+ perror("fallocate frag file");
+ return -1;
+ }
+ close(fd);
+ created++;
+ }
+ fprintf(stderr, "Created %d files\n", created);
+
+ fprintf(stderr, "Deleting every %d-th file...\n", delete_nth);
+ for (i = 0; i < created; i += delete_nth) {
+ snprintf(path, sizeof(path), "%s/%s_%06d", dir, prefix, i);
+ unlink(path);
+ }
+ sync();
+
+ {
+ struct statvfs st;
+ if (statvfs(dir, &st) == 0) {
+ unsigned long free_mb = (st.f_bfree * st.f_bsize)
+ / (1024 * 1024);
+ fprintf(stderr,
+ "%d files remain, ~%lu MB free\n",
+ created - created / delete_nth, free_mb);
+ }
+ }
+
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ off_t file_size;
+ int delete_nth;
+
+ if (argc != 5) {
+ fprintf(stderr,
+ "Usage: %s <directory> <prefix> <file_size> <delete_every_nth>\n",
+ argv[0]);
+ return 1;
+ }
+
+ file_size = strtoll(argv[3], NULL, 0);
+ if (file_size <= 0) {
+ fprintf(stderr, "Invalid file size: %s\n", argv[3]);
+ return 1;
+ }
+
+ delete_nth = atoi(argv[4]);
+ if (delete_nth < 2) {
+ fprintf(stderr, "delete_every_nth must be >= 2\n");
+ return 1;
+ }
+
+ if (fragment_fs(argv[1], argv[2], file_size, delete_nth) < 0)
+ return 1;
+
+ return 0;
+}
diff --git a/src/mix_buffer_direct_io.c b/src/mix_buffer_direct_io.c
new file mode 100644
index 000000000000..0af227a7c9f0
--- /dev/null
+++ b/src/mix_buffer_direct_io.c
@@ -0,0 +1,175 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (c) 2026 Eric Sandeen, Lukas Herbolt
+ */
+
+/*
+ * Mix buffered and direct IO originally written to test for XFS -ENOSPC
+ * and -ENOSR dirty transaction cancel on fragmented filesystem.
+ *
+ * XFS notes:
+ * Buffered write size. With allocsize=128MB, the buffered write creates
+ * a delalloc extent of [0, buf_write_size + 128MB). The DIO at i_size
+ * is at file offset buf_write_size. xfs_bmapi_allocate (RHEL 8) starts
+ * the delalloc conversion from offset 0; the allocation must span
+ * buf_write_size/blocksize blocks in one contiguous extent to reach the
+ * DIO offset. On a fragmented FS that's impossible — but the FS may
+ * still have large free extents in metadata-sparse regions (20-40 MB is
+ * common). 64 MB puts the DIO offset safely beyond those.
+ *
+ * Usage:
+ * ./mix_buffer_direct_io <dir> [buf_write_size] [dio_size]
+ *
+ * buf_write_size Buffered write size in bytes (default 67108864 / 64 MB)
+ * dio_size DIO write size in bytes (default 4096), must be
+ * block-aligned
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+
+static int attempt_trigger(const char *dir, int attempt,
+ off_t buf_write_size, size_t dio_size)
+{
+ char filepath[4096];
+ char *wbuf;
+ void *dbuf;
+ int bfd, dfd, ret;
+
+ snprintf(filepath, sizeof(filepath), "%s/target_%d", dir, attempt);
+
+ wbuf = calloc(1, buf_write_size);
+ if (!wbuf) {
+ perror("calloc");
+ return -1;
+ }
+
+ if (posix_memalign(&dbuf, dio_size, dio_size)) {
+ perror("posix_memalign");
+ free(wbuf);
+ return -1;
+ }
+ memset(dbuf, 0xbb, dio_size);
+
+ /*
+ * Open both fds up front. Keep the buffered fd open across the
+ * DIO write so close() can't trigger writeback that converts the
+ * delalloc before we hit it with DIO.
+ */
+ bfd = open(filepath, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ if (bfd < 0) {
+ if (errno == EIO || errno == EROFS) {
+ fprintf(stderr, "Filesystem shut down!\n");
+ free(wbuf); free(dbuf);
+ return 1;
+ }
+ perror("open for buffered write");
+ free(wbuf); free(dbuf);
+ return -1;
+ }
+
+ dfd = open(filepath, O_WRONLY | O_DIRECT);
+ if (dfd < 0) {
+ if (errno == EIO || errno == EROFS) {
+ fprintf(stderr, "Filesystem shut down!\n");
+ close(bfd);
+ free(wbuf); free(dbuf);
+ return 1;
+ }
+ perror("open for DIO");
+ close(bfd);
+ unlink(filepath);
+ free(wbuf); free(dbuf);
+ return -1;
+ }
+
+ /*
+ * Step 1: One buffered write.
+ * On XFS with allocsize=128MB, this creates a delalloc extent of
+ * [0, buf_write_size + 128MB) but only [0, buf_write_size) has pages.
+ * The rest is speculative prealloc.
+ */
+ ret = pwrite(bfd, wbuf, buf_write_size, 0);
+ if (ret < 0) {
+ if (errno == ENOSPC) {
+ fprintf(stderr, "attempt %d: ENOSPC on buffered write\n",
+ attempt);
+ close(dfd);
+ close(bfd);
+ unlink(filepath);
+ free(wbuf); free(dbuf);
+ return 0;
+ }
+ perror("buffered pwrite");
+ close(dfd);
+ close(bfd);
+ free(wbuf); free(dbuf);
+ return -1;
+ }
+
+ /*
+ * Step 2: DIO extending write at i_size.
+ *
+ * Offset buf_write_size is within the speculative delalloc but has no
+ * pages, so the DIO flush is a no-op. xfs_bmapi_write finds the
+ * delalloc starting at offset 0 and tries to convert from there. On
+ * our fragmented FS the allocation is 1 block at file offset 0,
+ * nowhere near the DIO offset at block 256. Returns -ENOSPC or -ENOSR.
+ */
+ ret = pwrite(dfd, dbuf, dio_size, buf_write_size);
+ if (ret < 0) {
+ fprintf(stderr, "DIO pwrite failed: %s\n", strerror(errno));
+ close(dfd);
+ close(bfd);
+ free(wbuf); free(dbuf);
+ return errno;
+ }
+ close(dfd);
+ close(bfd);
+ unlink(filepath);
+ free(wbuf);
+ free(dbuf);
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ off_t buf_write_size = 64 * 1024 * 1024;
+ size_t dio_size = 4096;
+ int i, ret;
+
+ if (argc < 2 || argc > 4) {
+ fprintf(stderr,
+ "Usage: %s <dir> [buf_write_size] [dio_size]\n",
+ argv[0]);
+ return 1;
+ }
+
+ if (argc >= 3) {
+ buf_write_size = strtoll(argv[2], NULL, 0);
+ if (buf_write_size <= 0) {
+ fprintf(stderr, "Invalid buf_write_size: %s\n",
+ argv[2]);
+ return 1;
+ }
+ }
+ if (argc >= 4) {
+ dio_size = strtoul(argv[3], NULL, 0);
+ if (dio_size == 0 || dio_size % 512) {
+ fprintf(stderr, "Invalid dio_size: %s\n", argv[3]);
+ return 1;
+ }
+ }
+
+ for (i = 0; i < 1000; i++) {
+ ret = attempt_trigger(argv[1], i,
+ buf_write_size, dio_size);
+ if (ret != 0)
+ return ret;
+ }
+ return 0;
+}
diff --git a/tests/xfs/842 b/tests/xfs/842
new file mode 100755
index 000000000000..b2b2f7c493e7
--- /dev/null
+++ b/tests/xfs/842
@@ -0,0 +1,63 @@
+#! /bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 Lukas Herbolt. All Rights Reserved.
+#
+# FS QA Test 842
+#
+# Regression test for XFS -ENOSR dirty transaction cancel during DIO
+# write into a speculative delalloc region on a fragmented filesystem.
+# With allocsize=128MB, a buffered write creates a large delalloc extent.
+# A subsequent DIO extending write at i_size triggers xfs_bmapi_write to
+# convert the delalloc from offset 0; on a fragmented FS the allocation
+# is too short to reach the DIO offset, returning -ENOSR and cancelling
+# a dirty transaction, which shuts down the filesystem.
+#
+. ./common/preamble
+_begin_fstest auto enospc
+
+_cleanup()
+{
+ cd /
+ rm -r -f $tmp.*
+}
+
+_require_test
+_require_scratch
+_require_test_program "fragment_fs"
+_require_test_program "buffer_direct_io"
+
+_scratch_mkfs_sized $((1<<30)) > "$seqres.full" 2>&1
+_scratch_mount -o allocsize=131072k >> "$seqres.full" 2>&1
+
+src/fragment_fs $SCRATCH_MNT frag 4096 2 >> "$seqres.full" 2>&1
+src/buffer_direct_io $SCRATCH_MNT >> "$seqres.full" 2>&1
+RET=$?
+
+_dmesg_since_test_start | grep -q "detected at xfs_trans_cancel"
+DMESG_CHECK=$?
+
+if [ $RET -eq 5 -a $DMESG_CHECK -eq 0 ]; then
+ _fixed_by_kernel_commit 2a9b99d45be0 \
+ "xfs: pass the actual offset and len to xfs_bmapi_allocate"
+ _fixed_by_kernel_commit 21255afdd729 \
+ "xfs: do not allocate the entire delalloc extent"
+ _fail "ENOSR dirty transaction cancel detected (ret=$RET)"
+
+elif [ $RET -eq 28 ]; then
+ _fixed_by_kernel_commit 2a9b99d45be0 \
+ "xfs: pass the actual offset and len to xfs_bmapi_allocate"
+ _fixed_by_kernel_commit 21255afdd729 \
+ "xfs: do not allocate the entire delalloc extent"
+ _fixed_by_kernel_commit 6773da870ab8 \
+ "xfs: fix error returns from xfs_bmapi_write"
+ _fail "ENOSPC returned from xfs_bmapi_write (ret=$RET)"
+
+elif [ $RET -eq 0 ]; then
+ echo "Silence is golden"
+else
+ _fail "Unknown error: src/buffer_direct_io error code $RET"
+fi
+
+# success, all done
+status=0
+exit
diff --git a/tests/xfs/842.out b/tests/xfs/842.out
new file mode 100644
index 000000000000..643d05289a33
--- /dev/null
+++ b/tests/xfs/842.out
@@ -0,0 +1,2 @@
+QA output created by 842
+Silence is golden
--
2.56.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* Re: [PATCH 1/1] xfs: add test for ENOSR/ENOSPC on DIO into delalloc extents
2026-10-16 9:16 [PATCH 1/1] xfs: add test for ENOSR/ENOSPC on DIO into delalloc extents Lukas Herbolt
@ 2026-10-22 13:26 ` Brian Foster
2026-10-27 10:39 ` Lukas Herbolt
2026-10-27 8:33 ` Zorro Lang
1 sibling, 1 reply; 5+ messages in thread
From: Brian Foster @ 2026-10-22 13:26 UTC (permalink / raw)
To: Lukas Herbolt; +Cc: zlang, fstests, hch, linux-xfs, Eric Sandeen
On Fri, Oct 16, 2026 at 11:16:25AM +0200, Lukas Herbolt wrote:
> Test if a DIO extending write into a speculative delalloc region on a
> fragmented XFS filesystem does not trigger a dirty transaction cancel
> (-ENOSR) or return a spurious -ENOSPC.
>
> With allocsize=128MB, a buffered write creates a large delalloc extent.
> A subsequent DIO write at i_size lands in the speculative prealloc
> region. On a fragmented filesystem, xfs_bmapi_write converts the
> delalloc starting from offset 0 but the allocation is too short to
> reach the DIO offset, causing either -ENOSR (dirty transaction cancel
> and filesystem shutdown) or -ENOSPC (wrong error from xfs_bmapi_write).
>
> Assisted by LLM.
>
> Fixed by kernel commits:
> 2a9b99d45be0 xfs: pass the actual offset and len to xfs_bmapi_allocate
> 21255afdd729 xfs: do not allocate the entire delalloc extent
> 6773da870ab8 xfs: fix error returns from xfs_bmapi_write
>
> Co-developed-by: Eric Sandeen <sandeen@redhat.com>
> Signed-off-by: Eric Sandeen <sandeen@redhat.com>
> Signed-off-by: Lukas Herbolt <lherbolt@redhat.com>
> ---
> .gitignore | 2 +
> src/Makefile | 3 +-
> src/buffer_direct_io.c | 154 ++++++++++++++++++++++++++++++++
> src/fragment_fs.c | 101 +++++++++++++++++++++
> src/mix_buffer_direct_io.c | 175 +++++++++++++++++++++++++++++++++++++
> tests/xfs/842 | 63 +++++++++++++
> tests/xfs/842.out | 2 +
> 7 files changed, 499 insertions(+), 1 deletion(-)
> create mode 100644 src/buffer_direct_io.c
> create mode 100644 src/fragment_fs.c
> create mode 100644 src/mix_buffer_direct_io.c
> create mode 100755 tests/xfs/842
> create mode 100644 tests/xfs/842.out
>
...
Just a few higher level comments..
> diff --git a/src/buffer_direct_io.c b/src/buffer_direct_io.c
> new file mode 100644
> index 000000000000..01b19994a2e2
> --- /dev/null
> +++ b/src/buffer_direct_io.c
...
> diff --git a/src/fragment_fs.c b/src/fragment_fs.c
> new file mode 100644
> index 000000000000..cee4edef73ff
> --- /dev/null
> +++ b/src/fragment_fs.c
> @@ -0,0 +1,101 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Copyright (c) 2026 Eric Sandeen, Lukas Herbolt
> + */
> +
> +/*
> + * Fragment filesystem by creating fixed-size files and then deleting
> + * every n-th one.
> + *
> + * Usage:
> + * fragment_fs <directory> <prefix> <file size> <delete every n-th file>
> + */
> +
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <unistd.h>
> +#include <fcntl.h>
> +#include <errno.h>
> +#include <sys/statvfs.h>
> +
> +static int fragment_fs(const char *dir, const char *prefix,
> + off_t size, int delete_nth)
> +{
> + char path[4096];
> + int i, fd;
> + int created = 0;
> +
> + fprintf(stderr, "Fragmenting filesystem (filling to ENOSPC)...\n");
> +
> + for (i = 0; ; i++) {
> + snprintf(path, sizeof(path), "%s/%s_%06d", dir, prefix, i);
> + fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
> + if (fd < 0) {
> + if (errno == ENOSPC)
> + break;
> + perror("open frag file");
> + return -1;
> + }
> + if (fallocate(fd, 0, 0, size) < 0) {
> + close(fd);
> + unlink(path);
> + if (errno == ENOSPC)
> + break;
> + perror("fallocate frag file");
> + return -1;
> + }
> + close(fd);
> + created++;
> + }
> + fprintf(stderr, "Created %d files\n", created);
> +
> + fprintf(stderr, "Deleting every %d-th file...\n", delete_nth);
> + for (i = 0; i < created; i += delete_nth) {
> + snprintf(path, sizeof(path), "%s/%s_%06d", dir, prefix, i);
> + unlink(path);
> + }
> + sync();
> +
> + {
> + struct statvfs st;
> + if (statvfs(dir, &st) == 0) {
> + unsigned long free_mb = (st.f_bfree * st.f_bsize)
> + / (1024 * 1024);
> + fprintf(stderr,
> + "%d files remain, ~%lu MB free\n",
> + created - created / delete_nth, free_mb);
> + }
> + }
> +
> + return 0;
> +}
> +
Any chance we can reuse existing code in fstests for this sort of free
space fragmentation? For example, see the punch-alternating tool and/or
something like generic/186 (if useful, maybe that code can be lifted
into a common helper).
...
> diff --git a/src/mix_buffer_direct_io.c b/src/mix_buffer_direct_io.c
> new file mode 100644
> index 000000000000..0af227a7c9f0
> --- /dev/null
> +++ b/src/mix_buffer_direct_io.c
> diff --git a/tests/xfs/842 b/tests/xfs/842
...
It looks like we have two mix I/O helper programs here, where this one
appears unused..?
Also both appear to have some minor whitespace damage in the form of
trailing spaces at the end of some lines, so it might be worth fixing
that up as well.
> new file mode 100755
> index 000000000000..b2b2f7c493e7
> --- /dev/null
> +++ b/tests/xfs/842
> @@ -0,0 +1,63 @@
> +#! /bin/bash
> +# SPDX-License-Identifier: GPL-2.0
> +# Copyright (c) 2026 Lukas Herbolt. All Rights Reserved.
> +#
> +# FS QA Test 842
> +#
> +# Regression test for XFS -ENOSR dirty transaction cancel during DIO
> +# write into a speculative delalloc region on a fragmented filesystem.
> +# With allocsize=128MB, a buffered write creates a large delalloc extent.
> +# A subsequent DIO extending write at i_size triggers xfs_bmapi_write to
> +# convert the delalloc from offset 0; on a fragmented FS the allocation
> +# is too short to reach the DIO offset, returning -ENOSR and cancelling
> +# a dirty transaction, which shuts down the filesystem.
> +#
> +. ./common/preamble
> +_begin_fstest auto enospc
> +
> +_cleanup()
> +{
> + cd /
> + rm -r -f $tmp.*
> +}
> +
> +_require_test
Looks like we don't use the test dev in this test.
> +_require_scratch
> +_require_test_program "fragment_fs"
> +_require_test_program "buffer_direct_io"
> +
> +_scratch_mkfs_sized $((1<<30)) > "$seqres.full" 2>&1
> +_scratch_mount -o allocsize=131072k >> "$seqres.full" 2>&1
> +
> +src/fragment_fs $SCRATCH_MNT frag 4096 2 >> "$seqres.full" 2>&1
> +src/buffer_direct_io $SCRATCH_MNT >> "$seqres.full" 2>&1
> +RET=$?
> +
> +_dmesg_since_test_start | grep -q "detected at xfs_trans_cancel"
> +DMESG_CHECK=$?
> +
> +if [ $RET -eq 5 -a $DMESG_CHECK -eq 0 ]; then
> + _fixed_by_kernel_commit 2a9b99d45be0 \
> + "xfs: pass the actual offset and len to xfs_bmapi_allocate"
> + _fixed_by_kernel_commit 21255afdd729 \
> + "xfs: do not allocate the entire delalloc extent"
> + _fail "ENOSR dirty transaction cancel detected (ret=$RET)"
> +
> +elif [ $RET -eq 28 ]; then
> + _fixed_by_kernel_commit 2a9b99d45be0 \
> + "xfs: pass the actual offset and len to xfs_bmapi_allocate"
> + _fixed_by_kernel_commit 21255afdd729 \
> + "xfs: do not allocate the entire delalloc extent"
> + _fixed_by_kernel_commit 6773da870ab8 \
> + "xfs: fix error returns from xfs_bmapi_write"
> + _fail "ENOSPC returned from xfs_bmapi_write (ret=$RET)"
> +
> +elif [ $RET -eq 0 ]; then
> + echo "Silence is golden"
> +else
> + _fail "Unknown error: src/buffer_direct_io error code $RET"
> +fi
A brief comment above this might be helpful. I'm also curious if it can
be simplified a little. For example, I'm not sure we need to explicitly
_fail when we can rely on golden output and/or the dmesg checking that
happens by default. Also it looks like the commit hints are duplicate
except for the -ENOSPC specific one. So perhaps we could do something
like the following?
echo "Silence is golden"
# words about the error codes, i.e. EIO vs ENOSPC?
if [ $RET -neq 0 ]; then
_fixed_by_kernel_commit <X>
_fixed_by_kernel_commit <Y>
[ $RET -eq 28 ] && _fixed_by_kernel_commit <Z>
echo "... error code $RET"
fi
Brian
> +
> +# success, all done
> +status=0
> +exit
> diff --git a/tests/xfs/842.out b/tests/xfs/842.out
> new file mode 100644
> index 000000000000..643d05289a33
> --- /dev/null
> +++ b/tests/xfs/842.out
> @@ -0,0 +1,2 @@
> +QA output created by 842
> +Silence is golden
> --
> 2.56.0
>
>
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH 1/1] xfs: add test for ENOSR/ENOSPC on DIO into delalloc extents
2026-10-16 9:16 [PATCH 1/1] xfs: add test for ENOSR/ENOSPC on DIO into delalloc extents Lukas Herbolt
2026-10-22 13:26 ` Brian Foster
@ 2026-10-27 8:33 ` Zorro Lang
2026-10-27 10:42 ` Lukas Herbolt
1 sibling, 1 reply; 5+ messages in thread
From: Zorro Lang @ 2026-10-27 8:33 UTC (permalink / raw)
To: Lukas Herbolt; +Cc: fstests, hch, linux-xfs, Eric Sandeen
On Fri, Oct 16, 2026 at 11:16:25AM +0200, Lukas Herbolt wrote:
> Test if a DIO extending write into a speculative delalloc region on a
> fragmented XFS filesystem does not trigger a dirty transaction cancel
> (-ENOSR) or return a spurious -ENOSPC.
>
> With allocsize=128MB, a buffered write creates a large delalloc extent.
> A subsequent DIO write at i_size lands in the speculative prealloc
> region. On a fragmented filesystem, xfs_bmapi_write converts the
> delalloc starting from offset 0 but the allocation is too short to
> reach the DIO offset, causing either -ENOSR (dirty transaction cancel
> and filesystem shutdown) or -ENOSPC (wrong error from xfs_bmapi_write).
>
> Assisted by LLM.
>
> Fixed by kernel commits:
> 2a9b99d45be0 xfs: pass the actual offset and len to xfs_bmapi_allocate
> 21255afdd729 xfs: do not allocate the entire delalloc extent
> 6773da870ab8 xfs: fix error returns from xfs_bmapi_write
>
> Co-developed-by: Eric Sandeen <sandeen@redhat.com>
> Signed-off-by: Eric Sandeen <sandeen@redhat.com>
> Signed-off-by: Lukas Herbolt <lherbolt@redhat.com>
> ---
> .gitignore | 2 +
> src/Makefile | 3 +-
> src/buffer_direct_io.c | 154 ++++++++++++++++++++++++++++++++
> src/fragment_fs.c | 101 +++++++++++++++++++++
> src/mix_buffer_direct_io.c | 175 +++++++++++++++++++++++++++++++++++++
> tests/xfs/842 | 63 +++++++++++++
> tests/xfs/842.out | 2 +
> 7 files changed, 499 insertions(+), 1 deletion(-)
> create mode 100644 src/buffer_direct_io.c
> create mode 100644 src/fragment_fs.c
> create mode 100644 src/mix_buffer_direct_io.c
> create mode 100755 tests/xfs/842
> create mode 100644 tests/xfs/842.out
>
> diff --git a/.gitignore b/.gitignore
> index d52ba4ad3085..eafc36c7d8ab 100644
> --- a/.gitignore
> +++ b/.gitignore
> @@ -217,6 +217,8 @@ tags
> /src/t_btrfs_received_uuid_ioctl
> /src/btrfs_ioctl
> /src/dio-read-into-mmap
> +/src/buffer_direct_io
> +/src/fragment_fs
You're trying to add 3 *.c programs in this patch, there're only 2 lines.
And ...
>
> # Symlinked files
> /tests/generic/035.out
> diff --git a/src/Makefile b/src/Makefile
> index 0ff607ae1593..c8964a59a539 100644
> --- a/src/Makefile
> +++ b/src/Makefile
> @@ -21,7 +21,8 @@ TARGETS = dirstress fill fill2 getpagesize holes lstat64 \
> t_mmap_writev_overlap checkpoint_journal mmap-rw-fault allocstale \
> t_mmap_cow_memory_failure fake-dump-rootino dio-buf-fault rewinddir-test \
> readdir-while-renames dio-append-buf-fault dio-write-fsync-same-fd \
> - dio-writeback-race unlink-fsync truncate dio-read-into-mmap
> + dio-writeback-race unlink-fsync truncate dio-read-into-mmap \
> + mix_buffer_direct_io fragment_fs
... there're only 2 too.
>
> LINUX_TARGETS = xfsctl bstat t_mtab getdevicesize preallo_rw_pattern_reader \
> preallo_rw_pattern_writer ftrunc trunc fs_perms testx looptest \
> diff --git a/src/buffer_direct_io.c b/src/buffer_direct_io.c
> new file mode 100644
> index 000000000000..01b19994a2e2
> --- /dev/null
> +++ b/src/buffer_direct_io.c
> @@ -0,0 +1,154 @@
> +/*
> + * Mixer of buffered and direct IO originally written to test for XFS -ENOSPC
> + * and -ENOSR dirty transaction cancel on fragmented filesystem.
> + *
> + * XFS notes:
> + * Buffered write size. With allocsize=128MB, the buffered write creates
> + * a delalloc extent of [0, BUF_WRITE_SIZE + 128MB). The DIO at i_size
> + * is at file offset BUF_WRITE_SIZE. xfs_bmapi_allocate (RHEL 8) starts
> + * the delalloc conversion from offset 0; the allocation must span
> + * BUF_WRITE_SIZE/blocksize blocks in one contiguous extent to reach the
> + * DIO offset. On a fragmented FS that's impossible — but the FS may
> + * still have large free extents in metadata-sparse regions (20-40 MB is
> + * common). 64 MB puts the DIO offset safely beyond those.
> +
> +
> + *
> + * Usage:
> + * ./mix_buffer_direct_io $SCRATCH_MNT
> + */
> +
> +#define _GNU_SOURCE
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <unistd.h>
> +#include <fcntl.h>
> +#include <errno.h>
> +#include <sys/stat.h>
> +#include <sys/statvfs.h>
> +
> +#define BUF_WRITE_SIZE (64 * 1024 * 1024) /* 64 MB */
> +
> +/* DIO write size — must be block-aligned */
> +#define DIO_SIZE 4096
> +
> +static int attempt_trigger(const char *dir, int attempt)
> +{
> + char filepath[4096];
> + char *wbuf;
> + void *dbuf;
> + int bfd, dfd, ret;
> +
> + snprintf(filepath, sizeof(filepath), "%s/target_%d", dir, attempt);
> +
> + wbuf = calloc(1, BUF_WRITE_SIZE);
> + if (!wbuf) {
> + perror("calloc");
> + return -1;
> + }
> +
> + if (posix_memalign(&dbuf, DIO_SIZE, DIO_SIZE)) {
> + perror("posix_memalign");
> + free(wbuf);
> + return -1;
> + }
> + memset(dbuf, 0xbb, DIO_SIZE);
> +
> + /*
> + * Open both fds up front. Keep the buffered fd open across the
> + * DIO write so close() can't trigger writeback that converts the
> + * delalloc before we hit it with DIO.
> + */
> + bfd = open(filepath, O_WRONLY | O_CREAT | O_TRUNC, 0644);
> + if (bfd < 0) {
> + if (errno == EIO || errno == EROFS) {
> + fprintf(stderr, "Filesystem shut down!\n");
> + free(wbuf); free(dbuf);
> + return 1;
> + }
> + perror("open for buffered write");
> + free(wbuf); free(dbuf);
> + return -1;
> + }
> +
> + dfd = open(filepath, O_WRONLY | O_DIRECT);
> + if (dfd < 0) {
> + if (errno == EIO || errno == EROFS) {
> + fprintf(stderr, "Filesystem shut down!\n");
> + close(bfd);
> + free(wbuf); free(dbuf);
> + return 1;
> + }
> + perror("open for DIO");
> + close(bfd);
> + unlink(filepath);
> + free(wbuf); free(dbuf);
> + return -1;
> + }
> +
> + /*
> + * Step 1: One buffered write.
> + * On XFS with allocsize=128MB, this creates a delalloc extent of
> + * [0, BUF_WRITE_SIZE + 128MB) but only [0, BUF_WRITE_SIZE) has pages.
> + * The rest is speculative prealloc.
> + */
> + ret = pwrite(bfd, wbuf, BUF_WRITE_SIZE, 0);
> + if (ret < 0) {
> + if (errno == ENOSPC) {
> + fprintf(stderr, "attempt %d: ENOSPC on buffered write\n",
> + attempt);
> + close(dfd);
> + close(bfd);
> + unlink(filepath);
> + free(wbuf); free(dbuf);
> + return 0;
> + }
> + perror("buffered pwrite");
> + close(dfd);
> + close(bfd);
> + free(wbuf); free(dbuf);
> + return -1;
> + }
> +
> + /*
> + * Step 2: DIO extending write at i_size.
> + *
> + * Offset BUF_WRITE_SIZE is within the speculative delalloc but has no
> + * pages, so the DIO flush is a no-op. xfs_bmapi_write finds the
> + * delalloc starting at offset 0 and tries to convert from there. On
> + * our fragmented FS the allocation is 1 block at file offset 0,
> + * nowhere near the DIO offset at block 256. Returns -ENOSPC or -ENOSR.
> + */
> + ret = pwrite(dfd, dbuf, DIO_SIZE, BUF_WRITE_SIZE);
> + if (ret < 0) {
> + fprintf(stderr, "DIO pwrite failed: %s\n", strerror(errno));
> + close(dfd);
> + close(bfd);
> + free(wbuf); free(dbuf);
> + return errno;
> + }
> + close(dfd);
> + close(bfd);
> + unlink(filepath);
> + free(wbuf);
> + free(dbuf);
> + return 0;
> +}
> +
> +int main(int argc, char *argv[])
> +{
> + int i, ret;
> +
> + if (argc != 2) {
> + fprintf(stderr, " %s <directory>\n", argv[0]);
> + return 1;
> + }
> +
> + for (i = 0; i < 1000; i++) {
> + ret = attempt_trigger(argv[1], i);
> + if (ret != 0)
> + return ret;
> + }
> + return 0;
> +}
> diff --git a/src/fragment_fs.c b/src/fragment_fs.c
> new file mode 100644
> index 000000000000..cee4edef73ff
> --- /dev/null
> +++ b/src/fragment_fs.c
> @@ -0,0 +1,101 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Copyright (c) 2026 Eric Sandeen, Lukas Herbolt
> + */
> +
> +/*
> + * Fragment filesystem by creating fixed-size files and then deleting
> + * every n-th one.
> + *
> + * Usage:
> + * fragment_fs <directory> <prefix> <file size> <delete every n-th file>
> + */
> +
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <unistd.h>
> +#include <fcntl.h>
> +#include <errno.h>
> +#include <sys/statvfs.h>
> +
> +static int fragment_fs(const char *dir, const char *prefix,
> + off_t size, int delete_nth)
> +{
> + char path[4096];
> + int i, fd;
> + int created = 0;
> +
> + fprintf(stderr, "Fragmenting filesystem (filling to ENOSPC)...\n");
> +
> + for (i = 0; ; i++) {
> + snprintf(path, sizeof(path), "%s/%s_%06d", dir, prefix, i);
> + fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
> + if (fd < 0) {
> + if (errno == ENOSPC)
> + break;
> + perror("open frag file");
> + return -1;
> + }
> + if (fallocate(fd, 0, 0, size) < 0) {
> + close(fd);
> + unlink(path);
> + if (errno == ENOSPC)
> + break;
> + perror("fallocate frag file");
> + return -1;
> + }
> + close(fd);
> + created++;
> + }
> + fprintf(stderr, "Created %d files\n", created);
> +
> + fprintf(stderr, "Deleting every %d-th file...\n", delete_nth);
> + for (i = 0; i < created; i += delete_nth) {
> + snprintf(path, sizeof(path), "%s/%s_%06d", dir, prefix, i);
> + unlink(path);
> + }
> + sync();
> +
> + {
> + struct statvfs st;
> + if (statvfs(dir, &st) == 0) {
> + unsigned long free_mb = (st.f_bfree * st.f_bsize)
> + / (1024 * 1024);
> + fprintf(stderr,
> + "%d files remain, ~%lu MB free\n",
> + created - created / delete_nth, free_mb);
> + }
> + }
> +
> + return 0;
> +}
> +
> +int main(int argc, char *argv[])
> +{
> + off_t file_size;
> + int delete_nth;
> +
> + if (argc != 5) {
> + fprintf(stderr,
> + "Usage: %s <directory> <prefix> <file_size> <delete_every_nth>\n",
> + argv[0]);
> + return 1;
> + }
> +
> + file_size = strtoll(argv[3], NULL, 0);
> + if (file_size <= 0) {
> + fprintf(stderr, "Invalid file size: %s\n", argv[3]);
> + return 1;
> + }
> +
> + delete_nth = atoi(argv[4]);
> + if (delete_nth < 2) {
> + fprintf(stderr, "delete_every_nth must be >= 2\n");
> + return 1;
> + }
> +
> + if (fragment_fs(argv[1], argv[2], file_size, delete_nth) < 0)
> + return 1;
> +
> + return 0;
> +}
> diff --git a/src/mix_buffer_direct_io.c b/src/mix_buffer_direct_io.c
> new file mode 100644
> index 000000000000..0af227a7c9f0
> --- /dev/null
> +++ b/src/mix_buffer_direct_io.c
> @@ -0,0 +1,175 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Copyright (c) 2026 Eric Sandeen, Lukas Herbolt
> + */
> +
> +/*
> + * Mix buffered and direct IO originally written to test for XFS -ENOSPC
> + * and -ENOSR dirty transaction cancel on fragmented filesystem.
> + *
> + * XFS notes:
> + * Buffered write size. With allocsize=128MB, the buffered write creates
> + * a delalloc extent of [0, buf_write_size + 128MB). The DIO at i_size
> + * is at file offset buf_write_size. xfs_bmapi_allocate (RHEL 8) starts
> + * the delalloc conversion from offset 0; the allocation must span
> + * buf_write_size/blocksize blocks in one contiguous extent to reach the
> + * DIO offset. On a fragmented FS that's impossible — but the FS may
> + * still have large free extents in metadata-sparse regions (20-40 MB is
> + * common). 64 MB puts the DIO offset safely beyond those.
> + *
> + * Usage:
> + * ./mix_buffer_direct_io <dir> [buf_write_size] [dio_size]
> + *
> + * buf_write_size Buffered write size in bytes (default 67108864 / 64 MB)
> + * dio_size DIO write size in bytes (default 4096), must be
> + * block-aligned
> + */
> +
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <unistd.h>
> +#include <fcntl.h>
> +#include <errno.h>
> +
> +static int attempt_trigger(const char *dir, int attempt,
> + off_t buf_write_size, size_t dio_size)
> +{
> + char filepath[4096];
> + char *wbuf;
> + void *dbuf;
> + int bfd, dfd, ret;
> +
> + snprintf(filepath, sizeof(filepath), "%s/target_%d", dir, attempt);
> +
> + wbuf = calloc(1, buf_write_size);
> + if (!wbuf) {
> + perror("calloc");
> + return -1;
> + }
> +
> + if (posix_memalign(&dbuf, dio_size, dio_size)) {
> + perror("posix_memalign");
> + free(wbuf);
> + return -1;
> + }
> + memset(dbuf, 0xbb, dio_size);
> +
> + /*
> + * Open both fds up front. Keep the buffered fd open across the
> + * DIO write so close() can't trigger writeback that converts the
> + * delalloc before we hit it with DIO.
> + */
> + bfd = open(filepath, O_WRONLY | O_CREAT | O_TRUNC, 0644);
> + if (bfd < 0) {
> + if (errno == EIO || errno == EROFS) {
> + fprintf(stderr, "Filesystem shut down!\n");
> + free(wbuf); free(dbuf);
> + return 1;
> + }
> + perror("open for buffered write");
> + free(wbuf); free(dbuf);
> + return -1;
> + }
> +
> + dfd = open(filepath, O_WRONLY | O_DIRECT);
> + if (dfd < 0) {
> + if (errno == EIO || errno == EROFS) {
> + fprintf(stderr, "Filesystem shut down!\n");
> + close(bfd);
> + free(wbuf); free(dbuf);
> + return 1;
> + }
> + perror("open for DIO");
> + close(bfd);
> + unlink(filepath);
> + free(wbuf); free(dbuf);
> + return -1;
> + }
> +
> + /*
> + * Step 1: One buffered write.
> + * On XFS with allocsize=128MB, this creates a delalloc extent of
> + * [0, buf_write_size + 128MB) but only [0, buf_write_size) has pages.
> + * The rest is speculative prealloc.
> + */
> + ret = pwrite(bfd, wbuf, buf_write_size, 0);
> + if (ret < 0) {
> + if (errno == ENOSPC) {
> + fprintf(stderr, "attempt %d: ENOSPC on buffered write\n",
> + attempt);
> + close(dfd);
> + close(bfd);
> + unlink(filepath);
> + free(wbuf); free(dbuf);
> + return 0;
> + }
> + perror("buffered pwrite");
> + close(dfd);
> + close(bfd);
> + free(wbuf); free(dbuf);
> + return -1;
> + }
> +
> + /*
> + * Step 2: DIO extending write at i_size.
> + *
> + * Offset buf_write_size is within the speculative delalloc but has no
> + * pages, so the DIO flush is a no-op. xfs_bmapi_write finds the
> + * delalloc starting at offset 0 and tries to convert from there. On
> + * our fragmented FS the allocation is 1 block at file offset 0,
> + * nowhere near the DIO offset at block 256. Returns -ENOSPC or -ENOSR.
> + */
> + ret = pwrite(dfd, dbuf, dio_size, buf_write_size);
> + if (ret < 0) {
> + fprintf(stderr, "DIO pwrite failed: %s\n", strerror(errno));
> + close(dfd);
> + close(bfd);
> + free(wbuf); free(dbuf);
> + return errno;
> + }
> + close(dfd);
> + close(bfd);
> + unlink(filepath);
> + free(wbuf);
> + free(dbuf);
> + return 0;
> +}
> +
> +int main(int argc, char *argv[])
> +{
> + off_t buf_write_size = 64 * 1024 * 1024;
> + size_t dio_size = 4096;
> + int i, ret;
> +
> + if (argc < 2 || argc > 4) {
> + fprintf(stderr,
> + "Usage: %s <dir> [buf_write_size] [dio_size]\n",
> + argv[0]);
> + return 1;
> + }
> +
> + if (argc >= 3) {
> + buf_write_size = strtoll(argv[2], NULL, 0);
> + if (buf_write_size <= 0) {
> + fprintf(stderr, "Invalid buf_write_size: %s\n",
> + argv[2]);
> + return 1;
> + }
> + }
> + if (argc >= 4) {
> + dio_size = strtoul(argv[3], NULL, 0);
> + if (dio_size == 0 || dio_size % 512) {
> + fprintf(stderr, "Invalid dio_size: %s\n", argv[3]);
> + return 1;
> + }
> + }
> +
> + for (i = 0; i < 1000; i++) {
> + ret = attempt_trigger(argv[1], i,
> + buf_write_size, dio_size);
> + if (ret != 0)
> + return ret;
> + }
> + return 0;
> +}
> diff --git a/tests/xfs/842 b/tests/xfs/842
> new file mode 100755
> index 000000000000..b2b2f7c493e7
> --- /dev/null
> +++ b/tests/xfs/842
> @@ -0,0 +1,63 @@
> +#! /bin/bash
> +# SPDX-License-Identifier: GPL-2.0
> +# Copyright (c) 2026 Lukas Herbolt. All Rights Reserved.
> +#
> +# FS QA Test 842
> +#
> +# Regression test for XFS -ENOSR dirty transaction cancel during DIO
> +# write into a speculative delalloc region on a fragmented filesystem.
> +# With allocsize=128MB, a buffered write creates a large delalloc extent.
> +# A subsequent DIO extending write at i_size triggers xfs_bmapi_write to
> +# convert the delalloc from offset 0; on a fragmented FS the allocation
> +# is too short to reach the DIO offset, returning -ENOSR and cancelling
> +# a dirty transaction, which shuts down the filesystem.
> +#
> +. ./common/preamble
> +_begin_fstest auto enospc
> +
> +_cleanup()
> +{
> + cd /
> + rm -r -f $tmp.*
This _cleanup does same thing with default _cleanup, so we can remove it.
> +}
> +
> +_require_test
> +_require_scratch
> +_require_test_program "fragment_fs"
> +_require_test_program "buffer_direct_io"
> +
> +_scratch_mkfs_sized $((1<<30)) > "$seqres.full" 2>&1
> +_scratch_mount -o allocsize=131072k >> "$seqres.full" 2>&1
> +
> +src/fragment_fs $SCRATCH_MNT frag 4096 2 >> "$seqres.full" 2>&1
> +src/buffer_direct_io $SCRATCH_MNT >> "$seqres.full" 2>&1
> +RET=$?
> +
> +_dmesg_since_test_start | grep -q "detected at xfs_trans_cancel"
> +DMESG_CHECK=$?
Can we just do:
_check_dmesg_for "detected at xfs_trans_cancel" || echo "xxxx" ?
> +
> +if [ $RET -eq 5 -a $DMESG_CHECK -eq 0 ]; then
> + _fixed_by_kernel_commit 2a9b99d45be0 \
> + "xfs: pass the actual offset and len to xfs_bmapi_allocate"
> + _fixed_by_kernel_commit 21255afdd729 \
> + "xfs: do not allocate the entire delalloc extent"
> + _fail "ENOSR dirty transaction cancel detected (ret=$RET)"
> +
> +elif [ $RET -eq 28 ]; then
> + _fixed_by_kernel_commit 2a9b99d45be0 \
> + "xfs: pass the actual offset and len to xfs_bmapi_allocate"
> + _fixed_by_kernel_commit 21255afdd729 \
> + "xfs: do not allocate the entire delalloc extent"
> + _fixed_by_kernel_commit 6773da870ab8 \
> + "xfs: fix error returns from xfs_bmapi_write"
We usually do _fixed_by_kernel_commit as hints before the test starts. If you
want to explain which hints correspond to different test results, I think
adding a few comments should be enough.
> + _fail "ENOSPC returned from xfs_bmapi_write (ret=$RET)"
> +
> +elif [ $RET -eq 0 ]; then
> + echo "Silence is golden"
Just print this line at the end of this case.
> +else
> + _fail "Unknown error: src/buffer_direct_io error code $RET"
> +fi
The logic here is a bit messy. It might be better to let the .c test print
different outputs to fail the golden image. Better to clean this up to
keep the code straightforward.
> +
> +# success, all done
> +status=0
> +exit
_exit 0
> diff --git a/tests/xfs/842.out b/tests/xfs/842.out
> new file mode 100644
> index 000000000000..643d05289a33
> --- /dev/null
> +++ b/tests/xfs/842.out
> @@ -0,0 +1,2 @@
> +QA output created by 842
> +Silence is golden
> --
> 2.56.0
>
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH 1/1] xfs: add test for ENOSR/ENOSPC on DIO into delalloc extents
2026-10-22 13:26 ` Brian Foster
@ 2026-10-27 10:39 ` Lukas Herbolt
0 siblings, 0 replies; 5+ messages in thread
From: Lukas Herbolt @ 2026-10-27 10:39 UTC (permalink / raw)
To: Brian Foster; +Cc: zlang, fstests, hch, linux-xfs, Eric Sandeen
On Thu, Oct 22, 2026 at 3:26 PM Brian Foster <bfoster@redhat.com> wrote:
>
> On Fri, Oct 16, 2026 at 11:16:25AM +0200, Lukas Herbolt wrote:
> > Test if a DIO extending write into a speculative delalloc region on a
> > fragmented XFS filesystem does not trigger a dirty transaction cancel
> > (-ENOSR) or return a spurious -ENOSPC.
> >
> > With allocsize=128MB, a buffered write creates a large delalloc extent.
> > A subsequent DIO write at i_size lands in the speculative prealloc
> > region. On a fragmented filesystem, xfs_bmapi_write converts the
> > delalloc starting from offset 0 but the allocation is too short to
> > reach the DIO offset, causing either -ENOSR (dirty transaction cancel
> > and filesystem shutdown) or -ENOSPC (wrong error from xfs_bmapi_write).
> >
> > Assisted by LLM.
> >
> > Fixed by kernel commits:
> > 2a9b99d45be0 xfs: pass the actual offset and len to xfs_bmapi_allocate
> > 21255afdd729 xfs: do not allocate the entire delalloc extent
> > 6773da870ab8 xfs: fix error returns from xfs_bmapi_write
> >
> > Co-developed-by: Eric Sandeen <sandeen@redhat.com>
> > Signed-off-by: Eric Sandeen <sandeen@redhat.com>
> > Signed-off-by: Lukas Herbolt <lherbolt@redhat.com>
> > ---
> > .gitignore | 2 +
> > src/Makefile | 3 +-
> > src/buffer_direct_io.c | 154 ++++++++++++++++++++++++++++++++
> > src/fragment_fs.c | 101 +++++++++++++++++++++
> > src/mix_buffer_direct_io.c | 175 +++++++++++++++++++++++++++++++++++++
> > tests/xfs/842 | 63 +++++++++++++
> > tests/xfs/842.out | 2 +
> > 7 files changed, 499 insertions(+), 1 deletion(-)
> > create mode 100644 src/buffer_direct_io.c
> > create mode 100644 src/fragment_fs.c
> > create mode 100644 src/mix_buffer_direct_io.c
> > create mode 100755 tests/xfs/842
> > create mode 100644 tests/xfs/842.out
> >
> ...
>
> Just a few higher level comments..
>
> > diff --git a/src/buffer_direct_io.c b/src/buffer_direct_io.c
> > new file mode 100644
> > index 000000000000..01b19994a2e2
> > --- /dev/null
> > +++ b/src/buffer_direct_io.c
> ...
> > diff --git a/src/fragment_fs.c b/src/fragment_fs.c
> > new file mode 100644
> > index 000000000000..cee4edef73ff
> > --- /dev/null
> > +++ b/src/fragment_fs.c
> > @@ -0,0 +1,101 @@
> > +// SPDX-License-Identifier: GPL-2.0
> > +/*
> > + * Copyright (c) 2026 Eric Sandeen, Lukas Herbolt
> > + */
> > +
> > +/*
> > + * Fragment filesystem by creating fixed-size files and then deleting
> > + * every n-th one.
> > + *
> > + * Usage:
> > + * fragment_fs <directory> <prefix> <file size> <delete every n-th file>
> > + */
> > +
> > +#include <stdio.h>
> > +#include <stdlib.h>
> > +#include <unistd.h>
> > +#include <fcntl.h>
> > +#include <errno.h>
> > +#include <sys/statvfs.h>
> > +
> > +static int fragment_fs(const char *dir, const char *prefix,
> > + off_t size, int delete_nth)
> > +{
> > + char path[4096];
> > + int i, fd;
> > + int created = 0;
> > +
> > + fprintf(stderr, "Fragmenting filesystem (filling to ENOSPC)...\n");
> > +
> > + for (i = 0; ; i++) {
> > + snprintf(path, sizeof(path), "%s/%s_%06d", dir, prefix, i);
> > + fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
> > + if (fd < 0) {
> > + if (errno == ENOSPC)
> > + break;
> > + perror("open frag file");
> > + return -1;
> > + }
> > + if (fallocate(fd, 0, 0, size) < 0) {
> > + close(fd);
> > + unlink(path);
> > + if (errno == ENOSPC)
> > + break;
> > + perror("fallocate frag file");
> > + return -1;
> > + }
> > + close(fd);
> > + created++;
> > + }
> > + fprintf(stderr, "Created %d files\n", created);
> > +
> > + fprintf(stderr, "Deleting every %d-th file...\n", delete_nth);
> > + for (i = 0; i < created; i += delete_nth) {
> > + snprintf(path, sizeof(path), "%s/%s_%06d", dir, prefix, i);
> > + unlink(path);
> > + }
> > + sync();
> > +
> > + {
> > + struct statvfs st;
> > + if (statvfs(dir, &st) == 0) {
> > + unsigned long free_mb = (st.f_bfree * st.f_bsize)
> > + / (1024 * 1024);
> > + fprintf(stderr,
> > + "%d files remain, ~%lu MB free\n",
> > + created - created / delete_nth, free_mb);
> > + }
> > + }
> > +
> > + return 0;
> > +}
> > +
>
> Any chance we can reuse existing code in fstests for this sort of free
> space fragmentation? For example, see the punch-alternating tool and/or
> something like generic/186 (if useful, maybe that code can be lifted
> into a common helper).
>
I used to xfs_io to fragment the fs but it tool much more time. I will check
how it's done in the g/186 and see if we can use common helper
> ...
> > diff --git a/src/mix_buffer_direct_io.c b/src/mix_buffer_direct_io.c
> > new file mode 100644
> > index 000000000000..0af227a7c9f0
> > --- /dev/null
> > +++ b/src/mix_buffer_direct_io.c
> > diff --git a/tests/xfs/842 b/tests/xfs/842
> ...
>
> It looks like we have two mix I/O helper programs here, where this one
> appears unused..?
Ugh, I was doing some rename and I missed that.
>
> Also both appear to have some minor whitespace damage in the form of
> trailing spaces at the end of some lines, so it might be worth fixing
> that up as well.
>
> > new file mode 100755
> > index 000000000000..b2b2f7c493e7
> > --- /dev/null
> > +++ b/tests/xfs/842
> > @@ -0,0 +1,63 @@
> > +#! /bin/bash
> > +# SPDX-License-Identifier: GPL-2.0
> > +# Copyright (c) 2026 Lukas Herbolt. All Rights Reserved.
> > +#
> > +# FS QA Test 842
> > +#
> > +# Regression test for XFS -ENOSR dirty transaction cancel during DIO
> > +# write into a speculative delalloc region on a fragmented filesystem.
> > +# With allocsize=128MB, a buffered write creates a large delalloc extent.
> > +# A subsequent DIO extending write at i_size triggers xfs_bmapi_write to
> > +# convert the delalloc from offset 0; on a fragmented FS the allocation
> > +# is too short to reach the DIO offset, returning -ENOSR and cancelling
> > +# a dirty transaction, which shuts down the filesystem.
> > +#
> > +. ./common/preamble
> > +_begin_fstest auto enospc
> > +
> > +_cleanup()
> > +{
> > + cd /
> > + rm -r -f $tmp.*
> > +}
> > +
> > +_require_test
>
> Looks like we don't use the test dev in this test.
>
> > +_require_scratch
> > +_require_test_program "fragment_fs"
> > +_require_test_program "buffer_direct_io"
> > +
> > +_scratch_mkfs_sized $((1<<30)) > "$seqres.full" 2>&1
> > +_scratch_mount -o allocsize=131072k >> "$seqres.full" 2>&1
> > +
> > +src/fragment_fs $SCRATCH_MNT frag 4096 2 >> "$seqres.full" 2>&1
> > +src/buffer_direct_io $SCRATCH_MNT >> "$seqres.full" 2>&1
> > +RET=$?
> > +
> > +_dmesg_since_test_start | grep -q "detected at xfs_trans_cancel"
> > +DMESG_CHECK=$?
> > +
> > +if [ $RET -eq 5 -a $DMESG_CHECK -eq 0 ]; then
> > + _fixed_by_kernel_commit 2a9b99d45be0 \
> > + "xfs: pass the actual offset and len to xfs_bmapi_allocate"
> > + _fixed_by_kernel_commit 21255afdd729 \
> > + "xfs: do not allocate the entire delalloc extent"
> > + _fail "ENOSR dirty transaction cancel detected (ret=$RET)"
> > +
> > +elif [ $RET -eq 28 ]; then
> > + _fixed_by_kernel_commit 2a9b99d45be0 \
> > + "xfs: pass the actual offset and len to xfs_bmapi_allocate"
> > + _fixed_by_kernel_commit 21255afdd729 \
> > + "xfs: do not allocate the entire delalloc extent"
> > + _fixed_by_kernel_commit 6773da870ab8 \
> > + "xfs: fix error returns from xfs_bmapi_write"
> > + _fail "ENOSPC returned from xfs_bmapi_write (ret=$RET)"
> > +
> > +elif [ $RET -eq 0 ]; then
> > + echo "Silence is golden"
> > +else
> > + _fail "Unknown error: src/buffer_direct_io error code $RET"
> > +fi
>
> A brief comment above this might be helpful. I'm also curious if it can
> be simplified a little. For example, I'm not sure we need to explicitly
> _fail when we can rely on golden output and/or the dmesg checking that
> happens by default. Also it looks like the commit hints are duplicate
> except for the -ENOSPC specific one. So perhaps we could do something
> like the following?
>
> echo "Silence is golden"
>
> # words about the error codes, i.e. EIO vs ENOSPC?
> if [ $RET -neq 0 ]; then
> _fixed_by_kernel_commit <X>
> _fixed_by_kernel_commit <Y>
> [ $RET -eq 28 ] && _fixed_by_kernel_commit <Z>
> echo "... error code $RET"
> fi
>
> Brian
>
> > +
> > +# success, all done
> > +status=0
> > +exit
> > diff --git a/tests/xfs/842.out b/tests/xfs/842.out
> > new file mode 100644
> > index 000000000000..643d05289a33
> > --- /dev/null
> > +++ b/tests/xfs/842.out
> > @@ -0,0 +1,2 @@
> > +QA output created by 842
> > +Silence is golden
> > --
> > 2.56.0
> >
> >
>
Thanks
--
Lukas Herbolt
SSME
Red Hat
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH 1/1] xfs: add test for ENOSR/ENOSPC on DIO into delalloc extents
2026-10-27 8:33 ` Zorro Lang
@ 2026-10-27 10:42 ` Lukas Herbolt
0 siblings, 0 replies; 5+ messages in thread
From: Lukas Herbolt @ 2026-10-27 10:42 UTC (permalink / raw)
To: Lukas Herbolt, fstests, hch, linux-xfs, Eric Sandeen
On Tue, Oct 27, 2026 at 9:33 AM Zorro Lang <zlang@kernel.org> wrote:
>
> On Fri, Oct 16, 2026 at 11:16:25AM +0200, Lukas Herbolt wrote:
> > Test if a DIO extending write into a speculative delalloc region on a
> > fragmented XFS filesystem does not trigger a dirty transaction cancel
> > (-ENOSR) or return a spurious -ENOSPC.
> >
> > With allocsize=128MB, a buffered write creates a large delalloc extent.
> > A subsequent DIO write at i_size lands in the speculative prealloc
> > region. On a fragmented filesystem, xfs_bmapi_write converts the
> > delalloc starting from offset 0 but the allocation is too short to
> > reach the DIO offset, causing either -ENOSR (dirty transaction cancel
> > and filesystem shutdown) or -ENOSPC (wrong error from xfs_bmapi_write).
> >
> > Assisted by LLM.
> >
> > Fixed by kernel commits:
> > 2a9b99d45be0 xfs: pass the actual offset and len to xfs_bmapi_allocate
> > 21255afdd729 xfs: do not allocate the entire delalloc extent
> > 6773da870ab8 xfs: fix error returns from xfs_bmapi_write
> >
> > Co-developed-by: Eric Sandeen <sandeen@redhat.com>
> > Signed-off-by: Eric Sandeen <sandeen@redhat.com>
> > Signed-off-by: Lukas Herbolt <lherbolt@redhat.com>
> > ---
> > .gitignore | 2 +
> > src/Makefile | 3 +-
> > src/buffer_direct_io.c | 154 ++++++++++++++++++++++++++++++++
> > src/fragment_fs.c | 101 +++++++++++++++++++++
> > src/mix_buffer_direct_io.c | 175 +++++++++++++++++++++++++++++++++++++
> > tests/xfs/842 | 63 +++++++++++++
> > tests/xfs/842.out | 2 +
> > 7 files changed, 499 insertions(+), 1 deletion(-)
> > create mode 100644 src/buffer_direct_io.c
> > create mode 100644 src/fragment_fs.c
> > create mode 100644 src/mix_buffer_direct_io.c
> > create mode 100755 tests/xfs/842
> > create mode 100644 tests/xfs/842.out
> >
> > diff --git a/.gitignore b/.gitignore
> > index d52ba4ad3085..eafc36c7d8ab 100644
> > --- a/.gitignore
> > +++ b/.gitignore
> > @@ -217,6 +217,8 @@ tags
> > /src/t_btrfs_received_uuid_ioctl
> > /src/btrfs_ioctl
> > /src/dio-read-into-mmap
> > +/src/buffer_direct_io
> > +/src/fragment_fs
>
> You're trying to add 3 *.c programs in this patch, there're only 2 lines.
> And ...
>
> >
> > # Symlinked files
> > /tests/generic/035.out
> > diff --git a/src/Makefile b/src/Makefile
> > index 0ff607ae1593..c8964a59a539 100644
> > --- a/src/Makefile
> > +++ b/src/Makefile
> > @@ -21,7 +21,8 @@ TARGETS = dirstress fill fill2 getpagesize holes lstat64 \
> > t_mmap_writev_overlap checkpoint_journal mmap-rw-fault allocstale \
> > t_mmap_cow_memory_failure fake-dump-rootino dio-buf-fault rewinddir-test \
> > readdir-while-renames dio-append-buf-fault dio-write-fsync-same-fd \
> > - dio-writeback-race unlink-fsync truncate dio-read-into-mmap
> > + dio-writeback-race unlink-fsync truncate dio-read-into-mmap \
> > + mix_buffer_direct_io fragment_fs
>
> ... there're only 2 too.
Sorry for that, I changed buffer_direct_io to mix_buffer_direct_io and
forgot to clean it up.
> >
> > LINUX_TARGETS = xfsctl bstat t_mtab getdevicesize preallo_rw_pattern_reader \
> > preallo_rw_pattern_writer ftrunc trunc fs_perms testx looptest \
> > diff --git a/src/buffer_direct_io.c b/src/buffer_direct_io.c
> > new file mode 100644
> > index 000000000000..01b19994a2e2
> > --- /dev/null
> > +++ b/src/buffer_direct_io.c
> > @@ -0,0 +1,154 @@
> > +/*
> > + * Mixer of buffered and direct IO originally written to test for XFS -ENOSPC
> > + * and -ENOSR dirty transaction cancel on fragmented filesystem.
> > + *
> > + * XFS notes:
> > + * Buffered write size. With allocsize=128MB, the buffered write creates
> > + * a delalloc extent of [0, BUF_WRITE_SIZE + 128MB). The DIO at i_size
> > + * is at file offset BUF_WRITE_SIZE. xfs_bmapi_allocate (RHEL 8) starts
> > + * the delalloc conversion from offset 0; the allocation must span
> > + * BUF_WRITE_SIZE/blocksize blocks in one contiguous extent to reach the
> > + * DIO offset. On a fragmented FS that's impossible — but the FS may
> > + * still have large free extents in metadata-sparse regions (20-40 MB is
> > + * common). 64 MB puts the DIO offset safely beyond those.
> > +
> > +
> > + *
> > + * Usage:
> > + * ./mix_buffer_direct_io $SCRATCH_MNT
> > + */
> > +
> > +#define _GNU_SOURCE
> > +#include <stdio.h>
> > +#include <stdlib.h>
> > +#include <string.h>
> > +#include <unistd.h>
> > +#include <fcntl.h>
> > +#include <errno.h>
> > +#include <sys/stat.h>
> > +#include <sys/statvfs.h>
> > +
> > +#define BUF_WRITE_SIZE (64 * 1024 * 1024) /* 64 MB */
> > +
> > +/* DIO write size — must be block-aligned */
> > +#define DIO_SIZE 4096
> > +
> > +static int attempt_trigger(const char *dir, int attempt)
> > +{
> > + char filepath[4096];
> > + char *wbuf;
> > + void *dbuf;
> > + int bfd, dfd, ret;
> > +
> > + snprintf(filepath, sizeof(filepath), "%s/target_%d", dir, attempt);
> > +
> > + wbuf = calloc(1, BUF_WRITE_SIZE);
> > + if (!wbuf) {
> > + perror("calloc");
> > + return -1;
> > + }
> > +
> > + if (posix_memalign(&dbuf, DIO_SIZE, DIO_SIZE)) {
> > + perror("posix_memalign");
> > + free(wbuf);
> > + return -1;
> > + }
> > + memset(dbuf, 0xbb, DIO_SIZE);
> > +
> > + /*
> > + * Open both fds up front. Keep the buffered fd open across the
> > + * DIO write so close() can't trigger writeback that converts the
> > + * delalloc before we hit it with DIO.
> > + */
> > + bfd = open(filepath, O_WRONLY | O_CREAT | O_TRUNC, 0644);
> > + if (bfd < 0) {
> > + if (errno == EIO || errno == EROFS) {
> > + fprintf(stderr, "Filesystem shut down!\n");
> > + free(wbuf); free(dbuf);
> > + return 1;
> > + }
> > + perror("open for buffered write");
> > + free(wbuf); free(dbuf);
> > + return -1;
> > + }
> > +
> > + dfd = open(filepath, O_WRONLY | O_DIRECT);
> > + if (dfd < 0) {
> > + if (errno == EIO || errno == EROFS) {
> > + fprintf(stderr, "Filesystem shut down!\n");
> > + close(bfd);
> > + free(wbuf); free(dbuf);
> > + return 1;
> > + }
> > + perror("open for DIO");
> > + close(bfd);
> > + unlink(filepath);
> > + free(wbuf); free(dbuf);
> > + return -1;
> > + }
> > +
> > + /*
> > + * Step 1: One buffered write.
> > + * On XFS with allocsize=128MB, this creates a delalloc extent of
> > + * [0, BUF_WRITE_SIZE + 128MB) but only [0, BUF_WRITE_SIZE) has pages.
> > + * The rest is speculative prealloc.
> > + */
> > + ret = pwrite(bfd, wbuf, BUF_WRITE_SIZE, 0);
> > + if (ret < 0) {
> > + if (errno == ENOSPC) {
> > + fprintf(stderr, "attempt %d: ENOSPC on buffered write\n",
> > + attempt);
> > + close(dfd);
> > + close(bfd);
> > + unlink(filepath);
> > + free(wbuf); free(dbuf);
> > + return 0;
> > + }
> > + perror("buffered pwrite");
> > + close(dfd);
> > + close(bfd);
> > + free(wbuf); free(dbuf);
> > + return -1;
> > + }
> > +
> > + /*
> > + * Step 2: DIO extending write at i_size.
> > + *
> > + * Offset BUF_WRITE_SIZE is within the speculative delalloc but has no
> > + * pages, so the DIO flush is a no-op. xfs_bmapi_write finds the
> > + * delalloc starting at offset 0 and tries to convert from there. On
> > + * our fragmented FS the allocation is 1 block at file offset 0,
> > + * nowhere near the DIO offset at block 256. Returns -ENOSPC or -ENOSR.
> > + */
> > + ret = pwrite(dfd, dbuf, DIO_SIZE, BUF_WRITE_SIZE);
> > + if (ret < 0) {
> > + fprintf(stderr, "DIO pwrite failed: %s\n", strerror(errno));
> > + close(dfd);
> > + close(bfd);
> > + free(wbuf); free(dbuf);
> > + return errno;
> > + }
> > + close(dfd);
> > + close(bfd);
> > + unlink(filepath);
> > + free(wbuf);
> > + free(dbuf);
> > + return 0;
> > +}
> > +
> > +int main(int argc, char *argv[])
> > +{
> > + int i, ret;
> > +
> > + if (argc != 2) {
> > + fprintf(stderr, " %s <directory>\n", argv[0]);
> > + return 1;
> > + }
> > +
> > + for (i = 0; i < 1000; i++) {
> > + ret = attempt_trigger(argv[1], i);
> > + if (ret != 0)
> > + return ret;
> > + }
> > + return 0;
> > +}
> > diff --git a/src/fragment_fs.c b/src/fragment_fs.c
> > new file mode 100644
> > index 000000000000..cee4edef73ff
> > --- /dev/null
> > +++ b/src/fragment_fs.c
> > @@ -0,0 +1,101 @@
> > +// SPDX-License-Identifier: GPL-2.0
> > +/*
> > + * Copyright (c) 2026 Eric Sandeen, Lukas Herbolt
> > + */
> > +
> > +/*
> > + * Fragment filesystem by creating fixed-size files and then deleting
> > + * every n-th one.
> > + *
> > + * Usage:
> > + * fragment_fs <directory> <prefix> <file size> <delete every n-th file>
> > + */
> > +
> > +#include <stdio.h>
> > +#include <stdlib.h>
> > +#include <unistd.h>
> > +#include <fcntl.h>
> > +#include <errno.h>
> > +#include <sys/statvfs.h>
> > +
> > +static int fragment_fs(const char *dir, const char *prefix,
> > + off_t size, int delete_nth)
> > +{
> > + char path[4096];
> > + int i, fd;
> > + int created = 0;
> > +
> > + fprintf(stderr, "Fragmenting filesystem (filling to ENOSPC)...\n");
> > +
> > + for (i = 0; ; i++) {
> > + snprintf(path, sizeof(path), "%s/%s_%06d", dir, prefix, i);
> > + fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
> > + if (fd < 0) {
> > + if (errno == ENOSPC)
> > + break;
> > + perror("open frag file");
> > + return -1;
> > + }
> > + if (fallocate(fd, 0, 0, size) < 0) {
> > + close(fd);
> > + unlink(path);
> > + if (errno == ENOSPC)
> > + break;
> > + perror("fallocate frag file");
> > + return -1;
> > + }
> > + close(fd);
> > + created++;
> > + }
> > + fprintf(stderr, "Created %d files\n", created);
> > +
> > + fprintf(stderr, "Deleting every %d-th file...\n", delete_nth);
> > + for (i = 0; i < created; i += delete_nth) {
> > + snprintf(path, sizeof(path), "%s/%s_%06d", dir, prefix, i);
> > + unlink(path);
> > + }
> > + sync();
> > +
> > + {
> > + struct statvfs st;
> > + if (statvfs(dir, &st) == 0) {
> > + unsigned long free_mb = (st.f_bfree * st.f_bsize)
> > + / (1024 * 1024);
> > + fprintf(stderr,
> > + "%d files remain, ~%lu MB free\n",
> > + created - created / delete_nth, free_mb);
> > + }
> > + }
> > +
> > + return 0;
> > +}
> > +
> > +int main(int argc, char *argv[])
> > +{
> > + off_t file_size;
> > + int delete_nth;
> > +
> > + if (argc != 5) {
> > + fprintf(stderr,
> > + "Usage: %s <directory> <prefix> <file_size> <delete_every_nth>\n",
> > + argv[0]);
> > + return 1;
> > + }
> > +
> > + file_size = strtoll(argv[3], NULL, 0);
> > + if (file_size <= 0) {
> > + fprintf(stderr, "Invalid file size: %s\n", argv[3]);
> > + return 1;
> > + }
> > +
> > + delete_nth = atoi(argv[4]);
> > + if (delete_nth < 2) {
> > + fprintf(stderr, "delete_every_nth must be >= 2\n");
> > + return 1;
> > + }
> > +
> > + if (fragment_fs(argv[1], argv[2], file_size, delete_nth) < 0)
> > + return 1;
> > +
> > + return 0;
> > +}
> > diff --git a/src/mix_buffer_direct_io.c b/src/mix_buffer_direct_io.c
> > new file mode 100644
> > index 000000000000..0af227a7c9f0
> > --- /dev/null
> > +++ b/src/mix_buffer_direct_io.c
> > @@ -0,0 +1,175 @@
> > +// SPDX-License-Identifier: GPL-2.0
> > +/*
> > + * Copyright (c) 2026 Eric Sandeen, Lukas Herbolt
> > + */
> > +
> > +/*
> > + * Mix buffered and direct IO originally written to test for XFS -ENOSPC
> > + * and -ENOSR dirty transaction cancel on fragmented filesystem.
> > + *
> > + * XFS notes:
> > + * Buffered write size. With allocsize=128MB, the buffered write creates
> > + * a delalloc extent of [0, buf_write_size + 128MB). The DIO at i_size
> > + * is at file offset buf_write_size. xfs_bmapi_allocate (RHEL 8) starts
> > + * the delalloc conversion from offset 0; the allocation must span
> > + * buf_write_size/blocksize blocks in one contiguous extent to reach the
> > + * DIO offset. On a fragmented FS that's impossible — but the FS may
> > + * still have large free extents in metadata-sparse regions (20-40 MB is
> > + * common). 64 MB puts the DIO offset safely beyond those.
> > + *
> > + * Usage:
> > + * ./mix_buffer_direct_io <dir> [buf_write_size] [dio_size]
> > + *
> > + * buf_write_size Buffered write size in bytes (default 67108864 / 64 MB)
> > + * dio_size DIO write size in bytes (default 4096), must be
> > + * block-aligned
> > + */
> > +
> > +#include <stdio.h>
> > +#include <stdlib.h>
> > +#include <string.h>
> > +#include <unistd.h>
> > +#include <fcntl.h>
> > +#include <errno.h>
> > +
> > +static int attempt_trigger(const char *dir, int attempt,
> > + off_t buf_write_size, size_t dio_size)
> > +{
> > + char filepath[4096];
> > + char *wbuf;
> > + void *dbuf;
> > + int bfd, dfd, ret;
> > +
> > + snprintf(filepath, sizeof(filepath), "%s/target_%d", dir, attempt);
> > +
> > + wbuf = calloc(1, buf_write_size);
> > + if (!wbuf) {
> > + perror("calloc");
> > + return -1;
> > + }
> > +
> > + if (posix_memalign(&dbuf, dio_size, dio_size)) {
> > + perror("posix_memalign");
> > + free(wbuf);
> > + return -1;
> > + }
> > + memset(dbuf, 0xbb, dio_size);
> > +
> > + /*
> > + * Open both fds up front. Keep the buffered fd open across the
> > + * DIO write so close() can't trigger writeback that converts the
> > + * delalloc before we hit it with DIO.
> > + */
> > + bfd = open(filepath, O_WRONLY | O_CREAT | O_TRUNC, 0644);
> > + if (bfd < 0) {
> > + if (errno == EIO || errno == EROFS) {
> > + fprintf(stderr, "Filesystem shut down!\n");
> > + free(wbuf); free(dbuf);
> > + return 1;
> > + }
> > + perror("open for buffered write");
> > + free(wbuf); free(dbuf);
> > + return -1;
> > + }
> > +
> > + dfd = open(filepath, O_WRONLY | O_DIRECT);
> > + if (dfd < 0) {
> > + if (errno == EIO || errno == EROFS) {
> > + fprintf(stderr, "Filesystem shut down!\n");
> > + close(bfd);
> > + free(wbuf); free(dbuf);
> > + return 1;
> > + }
> > + perror("open for DIO");
> > + close(bfd);
> > + unlink(filepath);
> > + free(wbuf); free(dbuf);
> > + return -1;
> > + }
> > +
> > + /*
> > + * Step 1: One buffered write.
> > + * On XFS with allocsize=128MB, this creates a delalloc extent of
> > + * [0, buf_write_size + 128MB) but only [0, buf_write_size) has pages.
> > + * The rest is speculative prealloc.
> > + */
> > + ret = pwrite(bfd, wbuf, buf_write_size, 0);
> > + if (ret < 0) {
> > + if (errno == ENOSPC) {
> > + fprintf(stderr, "attempt %d: ENOSPC on buffered write\n",
> > + attempt);
> > + close(dfd);
> > + close(bfd);
> > + unlink(filepath);
> > + free(wbuf); free(dbuf);
> > + return 0;
> > + }
> > + perror("buffered pwrite");
> > + close(dfd);
> > + close(bfd);
> > + free(wbuf); free(dbuf);
> > + return -1;
> > + }
> > +
> > + /*
> > + * Step 2: DIO extending write at i_size.
> > + *
> > + * Offset buf_write_size is within the speculative delalloc but has no
> > + * pages, so the DIO flush is a no-op. xfs_bmapi_write finds the
> > + * delalloc starting at offset 0 and tries to convert from there. On
> > + * our fragmented FS the allocation is 1 block at file offset 0,
> > + * nowhere near the DIO offset at block 256. Returns -ENOSPC or -ENOSR.
> > + */
> > + ret = pwrite(dfd, dbuf, dio_size, buf_write_size);
> > + if (ret < 0) {
> > + fprintf(stderr, "DIO pwrite failed: %s\n", strerror(errno));
> > + close(dfd);
> > + close(bfd);
> > + free(wbuf); free(dbuf);
> > + return errno;
> > + }
> > + close(dfd);
> > + close(bfd);
> > + unlink(filepath);
> > + free(wbuf);
> > + free(dbuf);
> > + return 0;
> > +}
> > +
> > +int main(int argc, char *argv[])
> > +{
> > + off_t buf_write_size = 64 * 1024 * 1024;
> > + size_t dio_size = 4096;
> > + int i, ret;
> > +
> > + if (argc < 2 || argc > 4) {
> > + fprintf(stderr,
> > + "Usage: %s <dir> [buf_write_size] [dio_size]\n",
> > + argv[0]);
> > + return 1;
> > + }
> > +
> > + if (argc >= 3) {
> > + buf_write_size = strtoll(argv[2], NULL, 0);
> > + if (buf_write_size <= 0) {
> > + fprintf(stderr, "Invalid buf_write_size: %s\n",
> > + argv[2]);
> > + return 1;
> > + }
> > + }
> > + if (argc >= 4) {
> > + dio_size = strtoul(argv[3], NULL, 0);
> > + if (dio_size == 0 || dio_size % 512) {
> > + fprintf(stderr, "Invalid dio_size: %s\n", argv[3]);
> > + return 1;
> > + }
> > + }
> > +
> > + for (i = 0; i < 1000; i++) {
> > + ret = attempt_trigger(argv[1], i,
> > + buf_write_size, dio_size);
> > + if (ret != 0)
> > + return ret;
> > + }
> > + return 0;
> > +}
> > diff --git a/tests/xfs/842 b/tests/xfs/842
> > new file mode 100755
> > index 000000000000..b2b2f7c493e7
> > --- /dev/null
> > +++ b/tests/xfs/842
> > @@ -0,0 +1,63 @@
> > +#! /bin/bash
> > +# SPDX-License-Identifier: GPL-2.0
> > +# Copyright (c) 2026 Lukas Herbolt. All Rights Reserved.
> > +#
> > +# FS QA Test 842
> > +#
> > +# Regression test for XFS -ENOSR dirty transaction cancel during DIO
> > +# write into a speculative delalloc region on a fragmented filesystem.
> > +# With allocsize=128MB, a buffered write creates a large delalloc extent.
> > +# A subsequent DIO extending write at i_size triggers xfs_bmapi_write to
> > +# convert the delalloc from offset 0; on a fragmented FS the allocation
> > +# is too short to reach the DIO offset, returning -ENOSR and cancelling
> > +# a dirty transaction, which shuts down the filesystem.
> > +#
> > +. ./common/preamble
> > +_begin_fstest auto enospc
> > +
> > +_cleanup()
> > +{
> > + cd /
> > + rm -r -f $tmp.*
>
> This _cleanup does same thing with default _cleanup, so we can remove it.
>
> > +}
> > +
> > +_require_test
> > +_require_scratch
> > +_require_test_program "fragment_fs"
> > +_require_test_program "buffer_direct_io"
> > +
> > +_scratch_mkfs_sized $((1<<30)) > "$seqres.full" 2>&1
> > +_scratch_mount -o allocsize=131072k >> "$seqres.full" 2>&1
> > +
> > +src/fragment_fs $SCRATCH_MNT frag 4096 2 >> "$seqres.full" 2>&1
> > +src/buffer_direct_io $SCRATCH_MNT >> "$seqres.full" 2>&1
> > +RET=$?
> > +
> > +_dmesg_since_test_start | grep -q "detected at xfs_trans_cancel"
> > +DMESG_CHECK=$?
>
> Can we just do:
> _check_dmesg_for "detected at xfs_trans_cancel" || echo "xxxx" ?
>
> > +
> > +if [ $RET -eq 5 -a $DMESG_CHECK -eq 0 ]; then
> > + _fixed_by_kernel_commit 2a9b99d45be0 \
> > + "xfs: pass the actual offset and len to xfs_bmapi_allocate"
> > + _fixed_by_kernel_commit 21255afdd729 \
> > + "xfs: do not allocate the entire delalloc extent"
> > + _fail "ENOSR dirty transaction cancel detected (ret=$RET)"
> > +
> > +elif [ $RET -eq 28 ]; then
> > + _fixed_by_kernel_commit 2a9b99d45be0 \
> > + "xfs: pass the actual offset and len to xfs_bmapi_allocate"
> > + _fixed_by_kernel_commit 21255afdd729 \
> > + "xfs: do not allocate the entire delalloc extent"
> > + _fixed_by_kernel_commit 6773da870ab8 \
> > + "xfs: fix error returns from xfs_bmapi_write"
>
> We usually do _fixed_by_kernel_commit as hints before the test starts. If you
> want to explain which hints correspond to different test results, I think
> adding a few comments should be enough.
>
> > + _fail "ENOSPC returned from xfs_bmapi_write (ret=$RET)"
> > +
> > +elif [ $RET -eq 0 ]; then
> > + echo "Silence is golden"
>
> Just print this line at the end of this case.
>
> > +else
> > + _fail "Unknown error: src/buffer_direct_io error code $RET"
> > +fi
>
> The logic here is a bit messy. It might be better to let the .c test print
> different outputs to fail the golden image. Better to clean this up to
> keep the code straightforward.
>
Ok noted, I will rework it and clean it up.
> > +
> > +# success, all done
> > +status=0
> > +exit
>
> _exit 0
>
> > diff --git a/tests/xfs/842.out b/tests/xfs/842.out
> > new file mode 100644
> > index 000000000000..643d05289a33
> > --- /dev/null
> > +++ b/tests/xfs/842.out
> > @@ -0,0 +1,2 @@
> > +QA output created by 842
> > +Silence is golden
> > --
> > 2.56.0
> >
>
thanks for the comments
--
Lukas Herbolt
SSME
Red Hat
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-10-27 10:42 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-16 9:16 [PATCH 1/1] xfs: add test for ENOSR/ENOSPC on DIO into delalloc extents Lukas Herbolt
2026-10-22 13:26 ` Brian Foster
2026-10-27 10:39 ` Lukas Herbolt
2026-10-27 8:33 ` Zorro Lang
2026-10-27 10:42 ` Lukas Herbolt
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox