From: Zorro Lang <zlang@kernel.org>
To: Lukas Herbolt <lherbolt@redhat.com>
Cc: fstests@vger.kernel.org, hch@lst.de, linux-xfs@vger.kernel.org,
Eric Sandeen <sandeen@redhat.com>
Subject: Re: [PATCH 1/1] xfs: add test for ENOSR/ENOSPC on DIO into delalloc extents
Date: Tue, 27 Oct 2026 16:33:21 +0800 [thread overview]
Message-ID: <auBcfVX2v_JOvr80@zlang-mailbox> (raw)
In-Reply-To: <20261016091628.295119-1-lherbolt@redhat.com>
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
>
next prev parent reply other threads:[~2026-10-27 8:33 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
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 [this message]
2026-10-27 10:42 ` Lukas Herbolt
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=auBcfVX2v_JOvr80@zlang-mailbox \
--to=zlang@kernel.org \
--cc=fstests@vger.kernel.org \
--cc=hch@lst.de \
--cc=lherbolt@redhat.com \
--cc=linux-xfs@vger.kernel.org \
--cc=sandeen@redhat.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox