From: John Garry <john.garry@linux.dev>
To: Tal Zussman <tz2294@columbia.edu>,
Shin'ichiro Kawasaki <shinichiro.kawasaki@wdc.com>,
linux-block@vger.kernel.org
Cc: Christoph Hellwig <hch@lst.de>
Subject: Re: [PATCH blktests v3 4/4] block/053: add a torn atomic write test for block devices
Date: Fri, 16 Oct 2026 09:53:48 +0100 [thread overview]
Message-ID: <8fff4ba1-1b19-4974-bbd1-16686c8b5bd2@linux.dev> (raw)
In-Reply-To: <20261015-blkdev-fixes-tests-v3-4-b6ffa6410514@columbia.edu>
On 10/15/26 17:29, Tal Zussman wrote:
thanks, just some more comments
> A block device atomic write is a hardware atomic write, submitted as a
> single bio, so when its buffer can't be fully pinned, the direct I/O
> path has to fail the write rather than submit the pinned part.
> Otherwise, it submits what was pinned with REQ_ATOMIC set and leaves
> the rest to the buffered fallback, tearing the write at the pin
> boundary.
>
> This is a regression test for that issue, fixed in the kernel patch
> "block: fail a short atomic pin in bio_iov_iter_get_pages()" [1].
>
> Issue a two-page pwritev2() with RWF_ATOMIC to a scsi_debug device with
> atomic_wr=1 from a buffer whose last page is PROT_NONE, so that pinning
> the buffer stops one page short. Check with statx() that the device
> supports an atomic write of that size, then check that the write fails
> with EINVAL and that the range still reads back as the old data.
>
> Two pages is the smallest write that leaves one page unpinnable, and
> atomic_wr_max_length=256 raises the atomic write unit max to 128K so
> that two pages fit for any page size up to 64K.
>
> [1]: https://lore.kernel.org/linux-block/20261009-blkdev-fixes-v6-5-307939c387df@columbia.edu/
>
> Signed-off-by: Tal Zussman <tz2294@columbia.edu>
> ---
> src/.gitignore | 1 +
> src/Makefile | 1 +
> src/atomic-write-torn.c | 233 ++++++++++++++++++++++++++++++++++++++++++++++++
> tests/block/053 | 60 +++++++++++++
> tests/block/053.out | 2 +
> 5 files changed, 297 insertions(+)
>
> diff --git a/src/.gitignore b/src/.gitignore
> index 92c08d1..76a1c5e 100644
> --- a/src/.gitignore
> +++ b/src/.gitignore
> @@ -1,3 +1,4 @@
> +/atomic-write-torn
> /bio-bounce-read
> /bio-full-trim
> /bio-trim-pin-leak
> diff --git a/src/Makefile b/src/Makefile
> index f3f338b..b244405 100644
> --- a/src/Makefile
> +++ b/src/Makefile
> @@ -14,6 +14,7 @@ HAVE_C_DEF = $(shell if printf '$(H)include <%s>\n$(H)ifdef %s\nHAVE_%s\n$(H)end
> then echo 1;else echo 0; fi)
>
> C_TARGETS := \
> + atomic-write-torn \
> bio-bounce-read \
> bio-full-trim \
> bio-trim-pin-leak \
> diff --git a/src/atomic-write-torn.c b/src/atomic-write-torn.c
We are testing that the atomic write is untorn really (which it should
be), so please rename to atomic-write-untorn.c
> new file mode 100644
> index 0000000..f6f3ccf
> --- /dev/null
> +++ b/src/atomic-write-torn.c
> @@ -0,0 +1,233 @@
> +// SPDX-License-Identifier: GPL-3.0+
> +/*
> + * Copyright (C) 2026 Tal Zussman
> + *
> + * Check that a RWF_ATOMIC direct write to a block device is all or nothing
> + * when only part of its buffer can be pinned.
> + *
> + * Fill two pages of the device with one byte value, then issue a pwritev2()
> + * with RWF_ATOMIC of a two-page buffer holding another value whose last page
> + * is PROT_NONE, so that pinning the buffer stops one page short. The direct
> + * I/O path must fail the write with EINVAL rather than submit what it pinned.
> + *
> + * Read the range back with O_DIRECT and count the bytes of each value, once
> + * before the atomic write to check the baseline and once after it. The write
> + * has to fail with EINVAL and leave only old bytes on the device.
> + *
> + * The device has to support atomic writes of two pages, which is checked
> + * with statx(). The first two pages of <blockdev> are overwritten.
I think that you can condense this description, as the functionality is
not too complex
> + *
> + * usage: atomic-write-torn <blockdev>
> + *
> + * exit: 0 = the write failed with EINVAL and the device is unchanged
> + * 1 = setup error
> + * 2 = the write was torn
> + * 3 = the write did not fail with EINVAL, or went through
> + */
> +#define _GNU_SOURCE
> +#include <errno.h>
> +#include <fcntl.h>
> +#include <stdbool.h>
> +#include <stdint.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <sys/mman.h>
> +#include <sys/syscall.h>
> +#include <sys/uio.h>
> +#include <unistd.h>
> +
> +#ifndef RWF_ATOMIC
> +#define RWF_ATOMIC 0x00000040
> +#endif
> +
> +#ifndef STATX_WRITE_ATOMIC
> +#define STATX_WRITE_ATOMIC 0x00010000U
> +#endif
> +#ifndef STATX_ATTR_WRITE_ATOMIC
> +#define STATX_ATTR_WRITE_ATOMIC 0x00400000
> +#endif
> +
Do we still really need these? Surely a test machine will be installing
the latest headers along with the new kernel, no?
> +/*
> + * The atomic write fields of struct statx are only in glibc 2.43 and later,
> + * so use a local copy of the kernel's layout and call the syscall directly.
> + */
> +struct atomic_statx {
> + uint32_t stx_mask;
> + uint32_t stx_blksize;
> + uint64_t stx_attributes;
> + uint32_t stx_nlink;
> + uint32_t stx_uid;
> + uint32_t stx_gid;
> + uint16_t stx_mode;
> + uint16_t __spare0[1];
> + uint64_t stx_ino;
> + uint64_t stx_size;
> + uint64_t stx_blocks;
> + uint64_t stx_attributes_mask;
> + uint64_t stx_times[8]; /* stx_atime, stx_btime, stx_ctime, stx_mtime */
> + uint32_t stx_rdev_major;
> + uint32_t stx_rdev_minor;
> + uint32_t stx_dev_major;
> + uint32_t stx_dev_minor;
> + uint64_t stx_mnt_id;
> + uint32_t stx_dio_mem_align;
> + uint32_t stx_dio_offset_align;
> + uint64_t stx_subvol;
> + uint32_t stx_atomic_write_unit_min;
> + uint32_t stx_atomic_write_unit_max;
> + uint32_t stx_atomic_write_segments_max;
> + uint32_t stx_dio_read_offset_align;
> + uint32_t stx_atomic_write_unit_max_opt;
> + uint32_t __spare2[1];
> + uint64_t __spare3[8];
> +};
or this.
> +
> +_Static_assert(sizeof(struct atomic_statx) == 256, "struct statx is 256 bytes");
> +
> +#define EXIT_TORN 2
> +#define EXIT_UNEXPECTED 3
EXIT_FAILURE and EXIT_SUCCESS could be used here, no?
> +
> +#define OLD 'A'
> +#define NEW 'B'
> +
> +/* Does the device support an atomic write of len bytes? */
> +static bool can_write_atomically(int fd, size_t len)
bdev_can_atomic_write_for_length() would be a better name and functionality
> +{
> + struct atomic_statx stx = { 0 };
> +
> + if (syscall(SYS_statx, fd, "", AT_EMPTY_PATH, STATX_WRITE_ATOMIC,
> + &stx)) {
> + perror("statx");
> + return false;
> + }
> + if (!(stx.stx_mask & STATX_WRITE_ATOMIC) ||
> + !(stx.stx_attributes & STATX_ATTR_WRITE_ATOMIC)) {
> + fprintf(stderr, "device does not support atomic writes\n");
> + return false;
> + }
> + printf("atomic write unit min %u, max %u\n",
> + stx.stx_atomic_write_unit_min, stx.stx_atomic_write_unit_max);
> + if (len < stx.stx_atomic_write_unit_min ||
> + len > stx.stx_atomic_write_unit_max) {
> + fprintf(stderr, "device cannot write %zu bytes atomically\n",
> + len);
> + return false;
> + }
> + return true;
> +}
> +
> +/* Read the range back past the page cache and count the bytes of each value. */
> +static int count_bytes(int fd, char *buf, size_t len, size_t *old_bytes,
> + size_t *new_bytes)
compare_bytes() may be a better name
> +{
> + size_t i;
> +
> + if (pread(fd, buf, len, 0) != (ssize_t)len) {
> + perror("pread");
> + return -1;
> + }
> +
> + *old_bytes = *new_bytes = 0;
> + for (i = 0; i < len; i++) {
> + if (buf[i] == OLD)
> + (*old_bytes)++;
> + else if (buf[i] == NEW)
> + (*new_bytes)++;
> + }
> + printf("on disk: %zu old bytes, %zu new bytes, %zu other\n",
> + *old_bytes, *new_bytes, len - *old_bytes - *new_bytes);
Do we really need all these non-error messages? I just wonder if they
can be obvious and useful to someone examining the log.
> + return 0;
> +}
> +
> +int main(int argc, char **argv)
> +{
> + size_t old_bytes, new_bytes;
> + size_t len, pgsz;
> + struct iovec iov;
> + char *buf;
> + ssize_t ret;
> + int fd, err, status = 1;
status = 1 seems a bit magic to me
> +
> + if (argc != 2) {
> + fprintf(stderr, "usage: %s <blockdev>\n", argv[0]);
> + return 1;
> + }
> +
> + pgsz = sysconf(_SC_PAGESIZE);
> + len = 2 * pgsz;
> +
> + fd = open(argv[1], O_RDWR | O_DIRECT);
> + if (fd < 0) {
> + perror("open");
> + return 1;
> + }
> +
> + if (!can_write_atomically(fd, len))
> + goto out_close;
> +
> + buf = mmap(NULL, len, PROT_READ | PROT_WRITE,
> + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> + if (buf == MAP_FAILED) {
> + perror("mmap");
> + goto out_close;
> + }
> +
> + /* Write the old contents and check that they read back. */
> + memset(buf, OLD, len);
> + if (pwrite(fd, buf, len, 0) != (ssize_t)len) {
> + perror("pwrite");
> + goto out_unmap;
> + }
> + if (count_bytes(fd, buf, len, &old_bytes, &new_bytes))
> + goto out_unmap;
> + if (old_bytes != len) {
> + fprintf(stderr, "old contents did not read back\n");
> + goto out_unmap;
> + }
Can all this functionality (count_bytes() and length compare) be
combined into a single function?
> +
> + /* Fill in the new contents and make the last page unreadable. */
> + memset(buf, NEW, len);
> + if (mprotect(buf + len - pgsz, pgsz, PROT_NONE)) {
> + perror("mprotect");
> + goto out_unmap;
> + }
> +
> + iov.iov_base = buf;
> + iov.iov_len = len;
> + ret = pwritev2(fd, &iov, 1, 0, RWF_ATOMIC);
> + err = errno;
> + if (ret < 0)
> + printf("pwritev2(RWF_ATOMIC) failed: %s\n", strerror(err));
> + else
> + printf("pwritev2(RWF_ATOMIC) returned %zd of %zu\n", ret, len);
The message does not really make sense, specifically "returned %zd of %zu"
> +
> + /* Make the last page readable again and reuse the buffer. */
Comments like "reuse the buffer" are a bit verbose
> + if (mprotect(buf + len - pgsz, pgsz, PROT_READ | PROT_WRITE)) {
> + perror("mprotect");
> + goto out_unmap;
> + }
> + if (count_bytes(fd, buf, len, &old_bytes, &new_bytes))
> + goto out_unmap;
> +
> + if (old_bytes != len && new_bytes != len) {
> + printf("atomic write was torn: %zu of %zu bytes written\n",
> + new_bytes, len);
> + status = EXIT_TORN;
> + } else if (ret >= 0 || err != EINVAL) {
> + printf("atomic write did not fail with EINVAL\n");
> + status = EXIT_UNEXPECTED;
> + } else if (new_bytes) {
> + printf("atomic write went through despite the unreadable page\n");
> + status = EXIT_UNEXPECTED;
> + } else {
> + printf("atomic write failed with EINVAL and left the device unchanged\n");
maybe, "atomic write failed with EINVAL and left the device unchanged,
as expected"
> + status = 0;
> + }
> +
> +out_unmap:
> + munmap(buf, len);
> +out_close:
> + close(fd);
> + return status;
> +}
> diff --git a/tests/block/053 b/tests/block/053
> new file mode 100755
> index 0000000..c7a24b4
> --- /dev/null
> +++ b/tests/block/053
> @@ -0,0 +1,60 @@
> +#!/bin/bash
> +# SPDX-License-Identifier: GPL-3.0+
> +# Copyright (C) 2026 Tal Zussman
> +#
> +# Issue a RWF_ATOMIC direct write to a block device from a buffer whose last
> +# page cannot be pinned and check that the write is all or nothing. A block
> +# device atomic write is a hardware atomic write, submitted as a single bio,
> +# so the direct I/O path has to fail the write outright when pinning its
> +# buffer comes up short. Without that, it submits what was pinned with
> +# REQ_ATOMIC set and leaves the rest to the buffered fallback, tearing the
> +# write at the pin boundary.
This seems to repeat the comments from the C code
> +#
> +# Regression test for patch "block: fail a short atomic pin in
> +# bio_iov_iter_get_pages()".
> +
> +. tests/block/rc
> +. common/scsi_debug
> +
> +DESCRIPTION="check that a partially pinned atomic write is not torn"
> +QUICK=1
> +
> +requires() {
> + _have_scsi_debug && _have_module_param scsi_debug atomic_wr
> + _have_src_program atomic-write-torn
> +}
> +
> +test() {
> + echo "Running ${TEST_NAME}"
> +
> + local ret
> +
> + # The helper writes two pages atomically. An atomic write unit max of
> + # 256 blocks, 128K, covers page sizes up to 64K.
> + if ! _configure_scsi_debug delay=0 atomic_wr=1 atomic_wr_max_length=256 \
> + dev_size_mb=16; then
> + echo "configuring scsi_debug failed"
> + return 1
> + fi
> +
> + src/atomic-write-torn "/dev/${SCSI_DEBUG_DEVICES[0]}" >>"${FULL}" 2>&1
> + ret=$?
> +
> + _exit_scsi_debug
> +
> + case $ret in
> + 0)
> + ;;
> + 2)
> + echo "atomic write was torn"
> + ;;> + 3)
> + echo "atomic write did not fail cleanly"
> + ;;
> + *)
> + echo "atomic-write-torn helper failed"
> + ;;
"atomic write did not fail cleanly" won't" help a tester much. If the
errors from the C code are logged, then that should help the tester know
know the issue.
> + esac
> +
> + echo "Test complete"
> +}
> diff --git a/tests/block/053.out b/tests/block/053.out
> new file mode 100644
> index 0000000..43d1b4b
> --- /dev/null
> +++ b/tests/block/053.out
> @@ -0,0 +1,2 @@
> +Running block/053
> +Test complete>
prev parent reply other threads:[~2026-10-16 8:53 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-15 16:29 [PATCH blktests v3 0/4] blktests: tests for the direct I/O fallback, splice, bio trimming, and atomic write fixes Tal Zussman
2026-10-15 16:29 ` [PATCH blktests v3 1/4] block/050: add a direct I/O write fallback race test for block devices Tal Zussman
2026-10-15 16:29 ` [PATCH blktests v3 2/4] block/051: add a splice read " Tal Zussman
2026-10-15 16:29 ` [PATCH blktests v3 3/4] block/052: add a pinned page leak test for trimmed direct writes Tal Zussman
2026-10-15 16:29 ` [PATCH blktests v3 4/4] block/053: add a torn atomic write test for block devices Tal Zussman
2026-10-16 8:53 ` John Garry [this message]
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=8fff4ba1-1b19-4974-bbd1-16686c8b5bd2@linux.dev \
--to=john.garry@linux.dev \
--cc=hch@lst.de \
--cc=linux-block@vger.kernel.org \
--cc=shinichiro.kawasaki@wdc.com \
--cc=tz2294@columbia.edu \
/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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.