Linux block layer
 help / color / mirror / Atom feed
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>


      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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox