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 v2 4/4] block/052: add a torn atomic write test for block devices
Date: Mon, 12 Oct 2026 17:04:03 +0100	[thread overview]
Message-ID: <7d331842-5d6c-44e9-929d-2ac730a9cda7@linux.dev> (raw)
In-Reply-To: <20261009-blkdev-fixes-tests-v2-4-fcb3599e47f7@columbia.edu>

On 10/10/26 00:53, Tal Zussman wrote:
> An atomic write is 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 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, then read the range back with O_DIRECT and fail if
> it holds a mix of old and new data.
> 
> The write has to span at least two pages so that one can be left
> unpinnable, and the device has to support it atomically. Use the largest
> power of two up to 16K that the device's atomic write unit max allows,
> and skip the test if that is below two pages.
> 
> [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 | 131 ++++++++++++++++++++++++++++++++++++++++++++++++
>   tests/block/052         |  70 ++++++++++++++++++++++++++
>   tests/block/052.out     |   2 +
>   5 files changed, 205 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 0bdfd05..54335d9 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
> new file mode 100644
> index 0000000..e8ebba3
> --- /dev/null
> +++ b/src/atomic-write-torn.c
> @@ -0,0 +1,131 @@
> +// 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 a range of the device with one byte value, then issue a pwritev2()
> + * with RWF_ATOMIC of a 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 outright rather than submit what it pinned.
> + *
> + * Read the range back with O_DIRECT and count the bytes of each value. A
> + * mix of old and new bytes means the write was torn.
> + *
> + * usage: atomic-write-torn <blockdev> <length>
> + *
> + * <length> is the size of the atomic write, a power of two of at least two
> + * pages that the device supports. The first <length> bytes of <blockdev>
> + * are overwritten.
> + *
> + * exit:  0 = the write was all or nothing
> + *        1 = setup error
> + *        2 = the write was torn
> + */
> +#define _GNU_SOURCE
> +#include <errno.h>
> +#include <fcntl.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <sys/mman.h>
> +#include <sys/uio.h>
> +#include <unistd.h>
> +
> +#ifndef RWF_ATOMIC
> +#define RWF_ATOMIC	0x00000040
> +#endif
> +
> +#define EXIT_TORN	2
> +
> +#define OLD	'A'
> +#define NEW	'B'
> +
> +int main(int argc, char **argv)

This code should really test that the block device is atomically 
writable, ideally via statx.

> +{
> +	size_t old_bytes = 0, new_bytes = 0;
> +	size_t len, pgsz, i;
> +	struct iovec iov;
> +	char *buf, *check;
> +	ssize_t ret;
> +	int fd;
> +
> +	if (argc != 3) {
> +		fprintf(stderr, "usage: %s <blockdev> <length>\n", argv[0]);
> +		return 1;
> +	}
> +
> +	pgsz = sysconf(_SC_PAGESIZE);
> +	len = strtoul(argv[2], NULL, 0);
> +	if (len < 2 * pgsz || (len & (len - 1))) {
> +		fprintf(stderr, "length must be a power of two of at least two pages\n");
> +		return 1;
> +	}
> +
> +	fd = open(argv[1], O_RDWR | O_DIRECT);
> +	if (fd < 0) {
> +		perror("open");
> +		return 1;
> +	}
> +
> +	buf = mmap(NULL, len, PROT_READ | PROT_WRITE,
> +		   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> +	if (buf == MAP_FAILED) {

Are you intentionally not handling close(fd)?

> +		perror("mmap");
> +		return 1;
> +	}
> +
> +	/* Write the old contents. */
> +	memset(buf, OLD, len);
> +	if (pwrite(fd, buf, len, 0) != (ssize_t)len) {
> +		perror("pwrite");
 > +		return 1;> +	}

I suppose that it would be total paranoia to re-read and test vs expected.

> +
> +	/* 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");
> +		return 1;
> +	}
> +
> +	iov.iov_base = buf;
> +	iov.iov_len = len;
> +	ret = pwritev2(fd, &iov, 1, 0, RWF_ATOMIC);
> +	if (ret < 0)
> +		printf("pwritev2(RWF_ATOMIC) failed: %s\n", strerror(errno));
> +	else
> +		printf("pwritev2(RWF_ATOMIC) returned %zd of %zu\n", ret, len);

As a sanity check we could test the error code is -EINVAL. That's always 
expected, right?

> +
> +	/* Read the range back past the page cache. */
> +	if (posix_memalign((void **)&check, pgsz, len)) {

Can you just re-use buf?

> +		perror("posix_memalign");
> +		return 1;
> +	}
> +	if (pread(fd, check, len, 0) != (ssize_t)len) {
> +		perror("pread");
> +		return 1;
> +	}
> +
> +	for (i = 0; i < len; i++) {
> +		if (check[i] == OLD)
> +			old_bytes++;
> +		else if (check[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);
> +
> +	free(check);
> +	munmap(buf, len);
> +	close(fd);
> +
> +	if (old_bytes != len) {
> +		printf("atomic write was torn: %zu of %zu bytes written\n",
> +		       new_bytes, len);

So what happens if we did write all the new bytes (even though it should 
not happen)? That's not a torn write. This really goes back to testing 
pwritev2() error code, above.

> +		return EXIT_TORN;
> +	}
> +	printf("atomic write was all or nothing\n");
> +	return 0;
> +}
> diff --git a/tests/block/052 b/tests/block/052
> new file mode 100755
> index 0000000..8ec115a
> --- /dev/null
> +++ b/tests/block/052
> @@ -0,0 +1,70 @@
> +#!/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. An atomic
> +# write is 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.
> +#
> +# Regression test for patch "block: fail a short atomic pin in
> +# bio_iov_iter_get_pages()".

Please run shellcheck on this if not already done so. JFYI, the 
maintainer asks for this.

> +
> +. 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 dev len page_size unit_max ret
> +
> +	if ! _configure_scsi_debug delay=0 atomic_wr=1 dev_size_mb=16; then

I think that delay defaults to zero now.

> +		echo "configuring scsi_debug failed"
> +		return 1
> +	fi
> +	dev="/dev/${SCSI_DEBUG_DEVICES[0]}"
> +
> +	# The largest power of two the device can write atomically, capped at
> +	# 16K. It has to span at least two pages so that one can be left
> +	# unpinnable.
> +	page_size=$(_get_page_size)
> +	unit_max=$(< "/sys/block/${SCSI_DEBUG_DEVICES[0]}/queue/atomic_write_unit_max_bytes")
> +	len=16384
> +	while (( len > unit_max )); do
> +		len=$(( len / 2 ))
> +	done
> +	if (( len < 2 * page_size )); then
> +		SKIP_REASONS+=("atomic write unit max ${unit_max} is below two pages")

Can you set params for scsi_debug such that this does not happen?

> +		_exit_scsi_debug
> +		return
> +	fi
> +
> +	src/atomic-write-torn "${dev}" "${len}" >>"${FULL}" 2>&1
> +	ret=$?
> +
> +	_exit_scsi_debug
> +
> +	case $ret in
> +	0)
> +		;;
> +	2)
> +		echo "atomic write was torn"
> +		;;
> +	*)
> +		echo "atomic-write-torn helper failed"
> +		;;
> +	esac
> +
> +	echo "Test complete"
> +}
> diff --git a/tests/block/052.out b/tests/block/052.out
> new file mode 100644
> index 0000000..71c1f0a
> --- /dev/null
> +++ b/tests/block/052.out
> @@ -0,0 +1,2 @@
> +Running block/052
> +Test complete
> 


      parent reply	other threads:[~2026-10-12 16:04 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-09 23:53 [PATCH blktests v2 0/4] blktests: tests for the direct I/O fallback, splice, bio trimming, and atomic write fixes Tal Zussman
2026-10-09 23:53 ` [PATCH blktests v2 1/4] block/049: add a direct I/O write fallback race test for block devices Tal Zussman
2026-10-09 23:53 ` [PATCH blktests v2 2/4] block/050: add a splice read " Tal Zussman
2026-10-09 23:53 ` [PATCH blktests v2 3/4] block/051: add a pinned page leak test for trimmed direct writes Tal Zussman
2026-10-09 23:53 ` [PATCH blktests v2 4/4] block/052: add a torn atomic write test for block devices Tal Zussman
2026-10-12  8:12   ` John Garry
2026-10-12 15:15     ` Tal Zussman
2026-10-12 16:04   ` 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=7d331842-5d6c-44e9-929d-2ac730a9cda7@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