* [PATCHv2 1/2] blktests: test direct io offsets
2025-11-19 19:54 [PATCHv2 0/2] blktests: add tests with offsets Keith Busch
@ 2025-11-19 19:54 ` Keith Busch
2025-11-19 23:39 ` Chaitanya Kulkarni
` (2 more replies)
2025-11-19 19:54 ` [PATCHv2 2/2] blktests: test io_uring user metadata offsets Keith Busch
1 sibling, 3 replies; 12+ messages in thread
From: Keith Busch @ 2025-11-19 19:54 UTC (permalink / raw)
To: linux-block, shinichiro.kawasaki; +Cc: chaitanyak, Keith Busch
From: Keith Busch <kbusch@kernel.org>
Tests various direct IO memory and length alignments against the
device's queue limits reported from sysfs.
Signed-off-by: Keith Busch <kbusch@kernel.org>
---
src/.gitignore | 1 +
src/Makefile | 1 +
src/dio-offsets.c | 708 ++++++++++++++++++++++++++++++++++++++++++++
tests/block/042 | 26 ++
tests/block/042.out | 2 +
5 files changed, 738 insertions(+)
create mode 100644 src/dio-offsets.c
create mode 100644 tests/block/042
create mode 100644 tests/block/042.out
diff --git a/src/.gitignore b/src/.gitignore
index 2ece754..865675c 100644
--- a/src/.gitignore
+++ b/src/.gitignore
@@ -1,3 +1,4 @@
+/dio-offsets
/discontiguous-io
/loblksize
/loop_change_fd
diff --git a/src/Makefile b/src/Makefile
index ba0d9b7..179a673 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -9,6 +9,7 @@ HAVE_C_MACRO = $(shell if echo "$(H)include <$(1)>" | \
then echo 1;else echo 0; fi)
C_TARGETS := \
+ dio-offsets \
loblksize \
loop_change_fd \
loop_get_status_null \
diff --git a/src/dio-offsets.c b/src/dio-offsets.c
new file mode 100644
index 0000000..8e46091
--- /dev/null
+++ b/src/dio-offsets.c
@@ -0,0 +1,708 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (c) 2025 Meta Platforms, Inc. All Rights Reserved.
+ *
+ * Description: test direct-io memory alignment offsets
+ */
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <libgen.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdbool.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/uio.h>
+
+#define power_of_2(x) ((x) && !((x) & ((x) - 1)))
+
+static unsigned long logical_block_size;
+static unsigned long dma_alignment;
+static unsigned long virt_boundary;
+static unsigned long max_segments;
+static unsigned long max_bytes;
+static size_t buf_size;
+static long pagesize;
+static void *out_buf;
+static void *in_buf;
+static int test_fd;
+
+static void init_args(char **argv)
+{
+ test_fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC | O_DIRECT);
+ if (test_fd < 0)
+ err(errno, "%s: failed to open %s", __func__, argv[1]);
+
+ max_segments = strtoul(argv[2], NULL, 0);
+ max_bytes = strtoul(argv[3], NULL, 0) * 1024;
+ dma_alignment = strtoul(argv[4], NULL, 0) + 1;
+ virt_boundary = strtoul(argv[5], NULL, 0) + 1;
+ logical_block_size = strtoul(argv[6], NULL, 0);
+
+ if (!power_of_2(virt_boundary) ||
+ !power_of_2(dma_alignment) ||
+ !power_of_2(logical_block_size)) {
+ errno = EINVAL;
+ err(1, "%s: bad parameters", __func__);
+ }
+
+ if (virt_boundary > 1 && virt_boundary < logical_block_size) {
+ errno = EINVAL;
+ err(1, "%s: virt_boundary:%lu logical_block_size:%lu", __func__,
+ virt_boundary, logical_block_size);
+ }
+
+ if (dma_alignment > logical_block_size) {
+ errno = EINVAL;
+ err(1, "%s: dma_alignment:%lu logical_block_size:%lu", __func__,
+ dma_alignment, logical_block_size);
+ }
+
+ if (max_segments > 4096)
+ max_segments = 4096;
+ if (max_bytes > 16384 * 1024)
+ max_bytes = 16384 * 1024;
+ if (max_bytes & (logical_block_size - 1))
+ max_bytes -= max_bytes & (logical_block_size - 1);
+
+ pagesize = sysconf(_SC_PAGE_SIZE);
+}
+
+static void init_buffers()
+{
+ unsigned long lb_mask = logical_block_size - 1;
+ int fd, ret;
+
+ buf_size = max_bytes * max_segments / 2;
+ if (buf_size < logical_block_size * max_segments)
+ err(EINVAL, "%s: logical block size is too big", __func__);
+
+ if (buf_size < logical_block_size * 1024 * 4)
+ buf_size = logical_block_size * 1024 * 4;
+
+ if (buf_size & lb_mask)
+ buf_size = (buf_size + lb_mask) & ~(lb_mask);
+
+ ret = posix_memalign((void **)&in_buf, pagesize, buf_size);
+ if (ret)
+ err(EINVAL, "%s: failed to allocate in-buf", __func__);
+
+ ret = posix_memalign((void **)&out_buf, pagesize, buf_size);
+ if (ret)
+ err(EINVAL, "%s: failed to allocate out-buf", __func__);
+
+ fd = open("/dev/urandom", O_RDONLY);
+ if (fd < 0)
+ err(EINVAL, "%s: failed to open urandom", __func__);
+
+ ret = read(fd, out_buf, buf_size);
+ if (ret < 0)
+ err(EINVAL, "%s: failed to randomize output buffer", __func__);
+
+ close(fd);
+}
+
+static void __compare(void *a, void *b, size_t size, const char *test)
+{
+ if (!memcmp(a, b, size))
+ return;
+ err(EIO, "%s: data corruption", test);
+}
+#define compare(a, b, size) __compare(a, b, size, __func__)
+
+/*
+ * Test using page aligned buffers, single source
+ *
+ * Total size is aligned to a logical block size and exceeds the max transfer
+ * size as well as the max segments. This should test the kernel's split bio
+ * construction and bio splitting for exceeding these limits.
+ */
+static void test_full_size_aligned()
+{
+ int ret;
+
+ memset(in_buf, 0, buf_size);
+ ret = pwrite(test_fd, out_buf, buf_size, 0);
+ if (ret < 0)
+ err(errno, "%s: failed to write buf", __func__);
+
+ ret = pread(test_fd, in_buf, buf_size, 0);
+ if (ret < 0)
+ err(errno, "%s: failed to read buf", __func__);
+
+ compare(out_buf, in_buf, buf_size);
+}
+
+/*
+ * Test using dma aligned buffers, single source
+ *
+ * This tests the kernel's dio memory alignment
+ */
+static void test_dma_aligned()
+{
+ int ret;
+
+ memset(in_buf, 0, buf_size);
+ ret = pwrite(test_fd, out_buf + dma_alignment, max_bytes, 0);
+ if (ret < 0)
+ err(errno, "%s: failed to write buf", __func__);
+
+ ret = pread(test_fd, in_buf + dma_alignment, max_bytes, 0);
+ if (ret < 0)
+ err(errno, "%s: failed to read buf", __func__);
+
+ compare(out_buf + dma_alignment, in_buf + dma_alignment, max_bytes);
+}
+
+/*
+ * Test using page aligned buffers + logicaly block sized vectored source
+ *
+ * This tests discontiguous vectored sources
+ */
+static void test_page_aligned_vectors()
+{
+ const int vecs = 4;
+
+ int i, ret, offset;
+ struct iovec iov[vecs];
+
+ memset(in_buf, 0, buf_size);
+ for (i = 0; i < vecs; i++) {
+ offset = logical_block_size * i * 4;
+ iov[i].iov_base = out_buf + offset;
+ iov[i].iov_len = logical_block_size * 2;
+ }
+
+ ret = pwritev(test_fd, iov, vecs, 0);
+ if (ret < 0)
+ err(errno, "%s: failed to write buf", __func__);
+
+ for (i = 0; i < vecs; i++) {
+ offset = logical_block_size * i * 4;
+ iov[i].iov_base = in_buf + offset;
+ iov[i].iov_len = logical_block_size * 2;
+ }
+
+ ret = preadv(test_fd, iov, vecs, 0);
+ if (ret < 0)
+ err(errno, "%s: failed to read buf", __func__);
+
+ for (i = 0; i < vecs; i++) {
+ offset = logical_block_size * i * 4;
+ compare(in_buf + offset, out_buf + offset, logical_block_size * 2);
+ }
+}
+
+/*
+ * Test using dma aligned buffers, vectored source
+ *
+ * This tests discontiguous vectored sources with incrementing dma aligned
+ * offsets
+ */
+static void test_dma_aligned_vectors()
+{
+ const int vecs = 4;
+
+ int i, ret, offset;
+ struct iovec iov[vecs];
+
+ memset(in_buf, 0, buf_size);
+ for (i = 0; i < vecs; i++) {
+ offset = logical_block_size * i * 8 + dma_alignment * (i + 1);
+ iov[i].iov_base = out_buf + offset;
+ iov[i].iov_len = logical_block_size * 2;
+ }
+
+ ret = pwritev(test_fd, iov, vecs, 0);
+ if (ret < 0)
+ err(errno, "%s: failed to write buf", __func__);
+
+ for (i = 0; i < vecs; i++) {
+ offset = logical_block_size * i * 8 + dma_alignment * (i + 1);
+ iov[i].iov_base = in_buf + offset;
+ iov[i].iov_len = logical_block_size * 2;
+ }
+
+ ret = preadv(test_fd, iov, vecs, 0);
+ if (ret < 0)
+ err(errno, "%s: failed to read buf", __func__);
+
+ for (i = 0; i < vecs; i++) {
+ offset = logical_block_size * i * 8 + dma_alignment * (i + 1);
+ compare(in_buf + offset, out_buf + offset, logical_block_size * 2);
+ }
+}
+
+/*
+ * Test vectored read with a total size aligned to a block, but some individual
+ * vectors will not be aligned to to the block size.
+ *
+ * All the middle vectors start and end on page boundaries which should
+ * satisfy any virt_boundary condition. This test will fail prior to kernel
+ * 6.18.
+ */
+static void test_unaligned_page_vectors()
+{
+ const int vecs = 4;
+
+ int i, ret, offset, mult;
+ struct iovec iov[vecs];
+ bool should_fail = true;
+
+ i = 0;
+ memset(in_buf, 0, buf_size);
+ mult = pagesize / logical_block_size;
+ if (mult < 2)
+ mult = 2;
+
+ offset = pagesize - (logical_block_size / 4);
+ if (offset & (dma_alignment - 1))
+ offset = pagesize - dma_alignment;
+
+ iov[i].iov_base = out_buf + offset;
+ iov[i].iov_len = pagesize - offset;
+
+ for (i = 1; i < vecs - 1; i++) {
+ offset = logical_block_size * i * 8 * mult;
+ iov[i].iov_base = out_buf + offset;
+ iov[i].iov_len = logical_block_size * mult;
+ }
+
+ offset = logical_block_size * i * 8 * mult;
+ iov[i].iov_base = out_buf + offset;
+ iov[i].iov_len = logical_block_size * mult - iov[0].iov_len;
+
+ ret = pwritev(test_fd, iov, vecs, 0);
+ if (ret < 0) {
+ if (should_fail)
+ return;
+ err(errno, "%s: failed to write buf", __func__);
+ }
+
+ i = 0;
+ offset = pagesize - (logical_block_size / 4);
+ if (offset & (dma_alignment - 1))
+ offset = pagesize - dma_alignment;
+
+ iov[i].iov_base = in_buf + offset;
+ iov[i].iov_len = pagesize - offset;
+
+ for (i = 1; i < vecs - 1; i++) {
+ offset = logical_block_size * i * 8 * mult;
+ iov[i].iov_base = in_buf + offset;
+ iov[i].iov_len = logical_block_size * mult;
+ }
+
+ offset = logical_block_size * i * 8 * mult;
+ iov[i].iov_base = in_buf + offset;
+ iov[i].iov_len = logical_block_size * mult - iov[0].iov_len;
+
+ ret = preadv(test_fd, iov, vecs, 0);
+ if (ret < 0)
+ err(errno, "%s: failed to read buf", __func__);
+
+ i = 0;
+ offset = pagesize - (logical_block_size / 4);
+ if (offset & (dma_alignment - 1))
+ offset = pagesize - dma_alignment;
+
+ compare(in_buf + offset, out_buf + offset, iov[i].iov_len);
+ for (i = 1; i < vecs - 1; i++) {
+ offset = logical_block_size * i * 8 * mult;
+ compare(in_buf + offset, out_buf + offset, iov[i].iov_len);
+ }
+ offset = logical_block_size * i * 8 * mult;
+ compare(in_buf + offset, out_buf + offset, iov[i].iov_len);
+}
+
+/*
+ * Total size is a logical block size multiple, but none of the vectors are.
+ *
+ * Total vectors will be less than the max. The vectors will be dma aligned. If
+ * a virtual boundary exists, this should fail, otherwise it should succceed on
+ * kernels 6.18 and newer.
+ */
+static void test_unaligned_vectors()
+{
+ const int vecs = 4;
+
+ struct iovec iov[vecs];
+ int i, ret, offset;
+
+ memset(in_buf, 0, buf_size);
+ for (i = 0; i < vecs; i++) {
+ offset = logical_block_size * i * 8;
+ iov[i].iov_base = out_buf + offset;
+ iov[i].iov_len = logical_block_size / 2;
+ }
+
+ ret = pwritev(test_fd, iov, vecs, 0);
+ if (ret < 0)
+ return;
+
+ for (i = 0; i < vecs; i++) {
+ offset = logical_block_size * i * 8;
+ iov[i].iov_base = in_buf + offset;
+ iov[i].iov_len = logical_block_size / 2;
+ }
+
+ ret = preadv(test_fd, iov, vecs, 0);
+ if (ret < 0)
+ err(errno, "%s: failed to read buf", __func__);
+
+ for (i = 0; i < vecs; i++) {
+ offset = logical_block_size * i * 8;
+ compare(in_buf + offset, out_buf + offset, logical_block_size / 2);
+ }
+}
+
+/*
+ * Provide an invalid iov_base at the beginning to test the kernel catching it
+ * while building a bio.
+ */
+static void test_invalid_starting_addr()
+{
+ const int vecs = 4;
+
+ int i, ret, offset;
+ struct iovec iov[vecs];
+
+ i = 0;
+ iov[i].iov_base = 0;
+ iov[i].iov_len = logical_block_size;
+
+ for (i = 1; i < vecs; i++) {
+ offset = logical_block_size * i * 8;
+ iov[i].iov_base = out_buf + offset;
+ iov[i].iov_len = logical_block_size;
+ }
+
+ ret = pwritev(test_fd, iov, vecs, 0);
+ if (ret < 0)
+ return;
+
+ err(ENOTSUP, "%s: write buf unexpectedly succeeded with NULL address ret:%d",
+ __func__, ret);
+}
+
+/*
+ * Provide an invalid iov_base in the middle to test the kernel catching it
+ * while building split bios. Ensure it is split by sending enough vectors to
+ * exceed bio's MAX_VEC; this should cause part of the io to dispatch.
+ */
+static void test_invalid_middle_addr()
+{
+ const int vecs = 1024;
+
+ int i, ret, offset;
+ struct iovec iov[vecs];
+
+ for (i = 0; i < vecs / 2 + 1; i++) {
+ offset = logical_block_size * i * 2;
+ iov[i].iov_base = out_buf + offset;
+ iov[i].iov_len = logical_block_size;
+ }
+
+ offset = logical_block_size * i * 2;
+ iov[i].iov_base = 0;
+ iov[i].iov_len = logical_block_size;
+
+ for (++i; i < vecs; i++) {
+ offset = logical_block_size * i * 2;
+ iov[i].iov_base = out_buf + offset;
+ iov[i].iov_len = logical_block_size;
+ }
+
+ ret = pwritev(test_fd, iov, vecs, 0);
+ if (ret < 0)
+ return;
+
+ err(ENOTSUP, "%s: write buf unexpectedly succeeded with NULL address ret:%d",
+ __func__, ret);
+}
+
+/*
+ * Test with an invalid DMA address. Should get caught early when splitting. If
+ * the device supports byte aligned memory (which is unusual), then this should
+ * be successful.
+ */
+static void test_invalid_dma_alignment()
+{
+ int ret, offset;
+ size_t size;
+ bool should_fail = dma_alignment > 1;
+
+ memset(in_buf, 0, buf_size);
+ offset = 2 * dma_alignment - 1;
+ size = logical_block_size * 256;
+ ret = pwrite(test_fd, out_buf + offset, size, 0);
+ if (ret < 0) {
+ if (should_fail)
+ return;
+ err(errno, "%s: failed to write buf", __func__);
+ }
+
+ if (should_fail)
+ err(ENOTSUP, "%s: write buf unexpectedly succeeded with invalid DMA offset address, ret:%d",
+ __func__, ret);
+
+ ret = pread(test_fd, in_buf + offset, size, 0);
+ if (ret < 0)
+ err(errno, "%s: failed to read buf", __func__);
+
+ compare(out_buf + offset, in_buf + offset, size);
+}
+
+/*
+ * Test with invalid DMA alignment in the middle. This should get split with
+ * the first part being dispatched, and the 2nd one failing without dispatch.
+ */
+static void test_invalid_dma_vector_alignment()
+{
+ const int vecs = 5;
+
+ bool should_fail = dma_alignment > 1;
+ struct iovec iov[vecs];
+ int ret, offset;
+
+ offset = dma_alignment * 2 - 1;
+ memset(in_buf, 0, buf_size);
+
+ iov[0].iov_base = out_buf;
+ iov[0].iov_len = max_bytes;
+
+ iov[1].iov_base = out_buf + max_bytes * 2;
+ iov[1].iov_len = max_bytes;
+
+ iov[2].iov_base = out_buf + max_bytes * 4 + offset;
+ iov[2].iov_len = max_bytes;
+
+ iov[3].iov_base = out_buf + max_bytes * 6;
+ iov[3].iov_len = max_bytes;
+
+ iov[4].iov_base = out_buf + max_bytes * 8;
+ iov[4].iov_len = max_bytes;
+
+ ret = pwritev(test_fd, iov, vecs, 0);
+ if (ret < 0) {
+ if (should_fail)
+ return;
+ err(errno, "%s: failed to write buf", __func__);
+ }
+ if (should_fail)
+ err(ENOTSUP, "%s: write buf unexpectedly succeeded with invalid DMA offset address ret:%d",
+ __func__, ret);
+
+ iov[0].iov_base = in_buf;
+ iov[0].iov_len = max_bytes;
+
+ iov[1].iov_base = in_buf + max_bytes * 2;
+ iov[1].iov_len = max_bytes;
+
+ iov[2].iov_base = in_buf + max_bytes * 4 + offset;
+ iov[2].iov_len = max_bytes;
+
+ iov[3].iov_base = in_buf + max_bytes * 6;
+ iov[3].iov_len = max_bytes;
+
+ iov[4].iov_base = in_buf + max_bytes * 8;
+ iov[4].iov_len = max_bytes;
+
+ ret = preadv(test_fd, iov, vecs, 0);
+ if (ret < 0)
+ err(errno, "%s: failed to read buf", __func__);
+
+ compare(out_buf, in_buf, max_bytes);
+ compare(out_buf + max_bytes * 2, in_buf + max_bytes * 2, max_bytes);
+ compare(out_buf + max_bytes * 4 + offset, in_buf + max_bytes * 4 + offset, max_bytes);
+ compare(out_buf + max_bytes * 6, in_buf + max_bytes * 6, max_bytes);
+ compare(out_buf + max_bytes * 8, in_buf + max_bytes * 8, max_bytes);
+}
+
+/*
+ * Test a bunch of small vectors if the device dma alignemnt allows it. We'll
+ * try to force a MAX_IOV split that can't form a valid IO so expect a failure.
+ */
+static void test_max_vector_limits()
+{
+ const int vecs = 320;
+
+ int ret, i, offset, iovpb, iov_size;
+ bool should_fail = true;
+ struct iovec iov[vecs];
+
+ memset(in_buf, 0, buf_size);
+ iovpb = logical_block_size / dma_alignment;
+ iov_size = logical_block_size / iovpb;
+
+ if ((pagesize / iov_size) < 256 &&
+ iov_size >= virt_boundary)
+ should_fail = false;
+
+ for (i = 0; i < vecs; i++) {
+ offset = i * iov_size * 2;
+ iov[i].iov_base = out_buf + offset;
+ iov[i].iov_len = iov_size;
+ }
+
+ ret = pwritev(test_fd, iov, vecs, 0);
+ if (ret < 0) {
+ if (should_fail)
+ return;
+ err(errno, "%s: failed to write buf", __func__);
+ }
+
+ if (should_fail)
+ err(ENOTSUP, "%s: write buf unexpectedly succeeded with excess vectors ret:%d",
+ __func__, ret);
+
+ for (i = 0; i < vecs; i++) {
+ offset = i * iov_size * 2;
+ iov[i].iov_base = in_buf + offset;
+ iov[i].iov_len = iov_size;
+ }
+
+ ret = preadv(test_fd, iov, vecs, 0);
+ if (ret < 0)
+ err(errno, "%s: failed to read buf", __func__);
+
+ for (i = 0; i < vecs; i++) {
+ offset = i * iov_size * 2;
+ compare(in_buf + offset, out_buf + offset, logical_block_size / 2);
+ }
+}
+
+/*
+ * Start with a valid vector that can be split into a dispatched IO, but poison
+ * the rest with an invalid DMA offset testing the kernel's late catch.
+ */
+static void test_invalid_dma_vector_alignment_large()
+{
+ const int vecs = 4;
+
+ struct iovec iov[vecs];
+ int i, ret;
+
+ i = 0;
+ iov[i].iov_base = out_buf;
+ iov[i].iov_len = max_bytes - logical_block_size;
+
+ i++;
+ iov[i].iov_base = out_buf + max_bytes + logical_block_size;
+ iov[i].iov_len = logical_block_size;
+
+ i++;
+ iov[i].iov_base = iov[1].iov_base + pagesize * 2 + (dma_alignment - 1);
+ iov[i].iov_len = logical_block_size;
+
+ i++;
+ iov[i].iov_base = out_buf + max_bytes * 8;
+ iov[i].iov_len = logical_block_size;
+
+ ret = pwritev(test_fd, iov, vecs, 0);
+ if (ret < 0)
+ return;
+
+ err(ENOTSUP, "%s: write buf unexpectedly succeeded with NULL address ret:%d",
+ __func__, ret);
+}
+
+/*
+ * Total size is block aligned, addresses are dma aligned, but invidual vector
+ * sizes may not be dma aligned. If device has byte sized dma alignment, this
+ * should succeed. If not, part of this should get dispatched, and the other
+ * part should fail.
+ */
+static void test_invalid_dma_vector_length()
+{
+ const int vecs = 4;
+
+ bool should_fail = dma_alignment > 1;
+ struct iovec iov[vecs];
+ int ret;
+
+ iov[0].iov_base = out_buf;
+ iov[0].iov_len = max_bytes * 2 - max_bytes / 2;
+
+ iov[1].iov_base = out_buf + max_bytes * 4;
+ iov[1].iov_len = logical_block_size * 2 - (dma_alignment + 1);
+
+ iov[2].iov_base = out_buf + max_bytes * 8;
+ iov[2].iov_len = logical_block_size * 2 + (dma_alignment + 1);
+
+ iov[3].iov_base = out_buf + max_bytes * 12;
+ iov[3].iov_len = max_bytes - max_bytes / 2;
+
+ ret = pwritev(test_fd, iov, vecs, 0);
+ if (ret < 0) {
+ if (should_fail)
+ return;
+ err(errno, "%s: failed to write buf", __func__);
+ }
+
+ if (should_fail)
+ err(ENOTSUP, "%s: write buf unexpectedly succeeded with invalid DMA offset address ret:%d",
+ __func__, ret);
+
+ iov[0].iov_base = in_buf;
+ iov[0].iov_len = max_bytes * 2 - max_bytes / 2;
+
+ iov[1].iov_base = in_buf + max_bytes * 4;
+ iov[1].iov_len = logical_block_size * 2 - (dma_alignment + 1);
+
+ iov[2].iov_base = in_buf + max_bytes * 8;
+ iov[2].iov_len = logical_block_size * 2 + (dma_alignment + 1);
+
+ iov[3].iov_base = in_buf + max_bytes * 12;
+ iov[3].iov_len = max_bytes - max_bytes / 2;
+
+ ret = pwritev(test_fd, iov, vecs, 0);
+ if (ret < 0)
+ err(errno, "%s: failed to read buf", __func__);
+
+ compare(out_buf, in_buf, iov[0].iov_len);
+ compare(out_buf + max_bytes * 4, in_buf + max_bytes * 4, iov[1].iov_len);
+ compare(out_buf + max_bytes * 8, in_buf + max_bytes * 8, iov[2].iov_len);
+ compare(out_buf + max_bytes * 12, in_buf + max_bytes * 12, iov[3].iov_len);
+}
+
+static void run_tests()
+{
+ test_full_size_aligned();
+ test_dma_aligned();
+ test_page_aligned_vectors();
+ test_dma_aligned_vectors();
+ test_unaligned_page_vectors();
+ test_unaligned_vectors();
+ test_invalid_starting_addr();
+ test_invalid_middle_addr();
+ test_invalid_dma_alignment();
+ test_invalid_dma_vector_alignment();
+ test_max_vector_limits();
+ test_invalid_dma_vector_alignment_large();
+ test_invalid_dma_vector_length();
+}
+
+/* ./$prog-name file */
+int main(int argc, char **argv)
+{
+ if (argc < 2)
+ errx(EINVAL, "expect argments: file");
+
+ init_args(argv);
+ init_buffers();
+ run_tests();
+ close(test_fd);
+ free(out_buf);
+ free(in_buf);
+
+ return 0;
diff --git a/tests/block/042 b/tests/block/042
new file mode 100644
index 0000000..a911d82
--- /dev/null
+++ b/tests/block/042
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+. tests/block/rc
+
+DESCRIPTION="Test unusual direct-io offsets"
+QUICK=1
+
+device_requires() {
+ _require_test_dev_sysfs
+}
+
+test_device() {
+ echo "Running ${TEST_NAME}"
+
+ sys_max_segments=$(cat "${TEST_DEV_SYSFS}/queue/max_segments")
+ sys_dma_alignment=$(cat "${TEST_DEV_SYSFS}/queue/dma_alignment")
+ sys_virt_boundary_mask=$(cat "${TEST_DEV_SYSFS}/queue/virt_boundary_mask")
+ sys_logical_block_size=$(cat "${TEST_DEV_SYSFS}/queue/logical_block_size")
+ sys_max_sectors_kb=$(cat "${TEST_DEV_SYSFS}/queue/max_sectors_kb")
+
+ if ! src/dio-offsets ${TEST_DEV} $sys_max_segments $sys_max_sectors_kb $sys_dma_alignment $sys_virt_boundary_mask $sys_logical_block_size; then
+ echo "src/dio-offsets failed"
+ fi
+
+ echo "Test complete"
+}
diff --git a/tests/block/042.out b/tests/block/042.out
new file mode 100644
index 0000000..b35c7ce
--- /dev/null
+++ b/tests/block/042.out
@@ -0,0 +1,2 @@
+Running block/042
+Test complete
--
2.47.3
^ permalink raw reply related [flat|nested] 12+ messages in thread* [PATCHv2 2/2] blktests: test io_uring user metadata offsets
2025-11-19 19:54 [PATCHv2 0/2] blktests: add tests with offsets Keith Busch
2025-11-19 19:54 ` [PATCHv2 1/2] blktests: test direct io offsets Keith Busch
@ 2025-11-19 19:54 ` Keith Busch
2025-11-19 23:39 ` Chaitanya Kulkarni
1 sibling, 1 reply; 12+ messages in thread
From: Keith Busch @ 2025-11-19 19:54 UTC (permalink / raw)
To: linux-block, shinichiro.kawasaki; +Cc: chaitanyak, Keith Busch
From: Keith Busch <kbusch@kernel.org>
For devices with metadata, tests various userspace offsets with
io_uring capabilities. If the metadata is formatted with ref tag
protection information, test various seed offsets as well.
Signed-off-by: Keith Busch <kbusch@kernel.org>
---
src/.gitignore | 1 +
src/Makefile | 16 +-
src/metadata.c | 488 ++++++++++++++++++++++++++++++++++++++++++++
tests/block/043 | 33 +++
tests/block/043.out | 2 +
5 files changed, 532 insertions(+), 8 deletions(-)
create mode 100644 src/metadata.c
create mode 100755 tests/block/043
create mode 100644 tests/block/043.out
diff --git a/src/.gitignore b/src/.gitignore
index 865675c..e6c6c38 100644
--- a/src/.gitignore
+++ b/src/.gitignore
@@ -3,6 +3,7 @@
/loblksize
/loop_change_fd
/loop_get_status_null
+/metadata
/mount_clear_sock
/nbdsetsize
/openclose
diff --git a/src/Makefile b/src/Makefile
index 179a673..dac07c7 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -22,7 +22,8 @@ C_TARGETS := \
sg/syzkaller1 \
zbdioctl
-C_MINIUBLK := miniublk
+C_URING_TARGETS := miniublk \
+ metadata
HAVE_LIBURING := $(call HAVE_C_MACRO,liburing.h,IORING_OP_URING_CMD)
HAVE_UBLK_HEADER := $(call HAVE_C_HEADER,linux/ublk_cmd.h,1)
@@ -31,9 +32,9 @@ CXX_TARGETS := \
discontiguous-io
ifeq ($(HAVE_LIBURING)$(HAVE_UBLK_HEADER), 11)
-TARGETS := $(C_TARGETS) $(CXX_TARGETS) $(C_MINIUBLK)
+TARGETS := $(C_TARGETS) $(CXX_TARGETS) $(C_URING_TARGETS)
else
-$(info Skip $(C_MINIUBLK) build due to missing kernel header(v6.0+) or liburing(2.2+))
+$(info Skip $(C_URING_TARGETS) build due to missing kernel header(v6.0+) or liburing(2.2+))
TARGETS := $(C_TARGETS) $(CXX_TARGETS)
endif
@@ -42,8 +43,8 @@ CONFIG_DEFS := $(call HAVE_C_HEADER,linux/blkzoned.h,-DHAVE_LINUX_BLKZONED_H)
override CFLAGS := -O2 -Wall -Wshadow $(CFLAGS) $(CONFIG_DEFS)
override CXXFLAGS := -O2 -std=c++11 -Wall -Wextra -Wshadow -Wno-sign-compare \
-Werror $(CXXFLAGS) $(CONFIG_DEFS)
-MINIUBLK_FLAGS := -D_GNU_SOURCE
-MINIUBLK_LIBS := -lpthread -luring
+URING_FLAGS := -D_GNU_SOURCE
+URING_LIBS := -lpthread -luring
LDFLAGS ?=
all: $(TARGETS)
@@ -61,8 +62,7 @@ $(C_TARGETS): %: %.c
$(CXX_TARGETS): %: %.cpp
$(CXX) $(CPPFLAGS) $(CXXFLAGS) $(LDFLAGS) -o $@ $^
-$(C_MINIUBLK): %: miniublk.c
- $(CC) $(CFLAGS) $(LDFLAGS) $(MINIUBLK_FLAGS) -o $@ miniublk.c \
- $(MINIUBLK_LIBS)
+$(C_URING_TARGETS): %: %.c
+ $(CC) $(CFLAGS) $(LDFLAGS) $(URING_FLAGS) -o $@ $^ $(URING_LIBS)
.PHONY: all clean install
diff --git a/src/metadata.c b/src/metadata.c
new file mode 100644
index 0000000..d935fd6
--- /dev/null
+++ b/src/metadata.c
@@ -0,0 +1,488 @@
+// SPDX-License-Identifier: GPL-3.0+
+/*
+ * Copyright (c) 2025 Meta Platforms, Inc. All Rights Reserved.
+ *
+ * Description: test userspace metadata
+ */
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <linux/fs.h>
+#include <liburing.h>
+
+#ifndef IORING_RW_ATTR_FLAG_PI
+#define PI_URING_COMPAT
+#define IORING_RW_ATTR_FLAG_PI (1U << 0)
+/* PI attribute information */
+struct io_uring_attr_pi {
+ __u16 flags;
+ __u16 app_tag;
+ __u32 len;
+ __u64 addr;
+ __u64 seed;
+ __u64 rsvd;
+};
+#endif
+
+#ifndef FS_IOC_GETLBMD_CAP
+/* Protection info capability flags */
+#define LBMD_PI_CAP_INTEGRITY (1 << 0)
+#define LBMD_PI_CAP_REFTAG (1 << 1)
+
+/* Checksum types for Protection Information */
+#define LBMD_PI_CSUM_NONE 0
+#define LBMD_PI_CSUM_IP 1
+#define LBMD_PI_CSUM_CRC16_T10DIF 2
+#define LBMD_PI_CSUM_CRC64_NVME 4
+
+/*
+ * Logical block metadata capability descriptor
+ * If the device does not support metadata, all the fields will be zero.
+ * Applications must check lbmd_flags to determine whether metadata is
+ * supported or not.
+ */
+struct logical_block_metadata_cap {
+ /* Bitmask of logical block metadata capability flags */
+ __u32 lbmd_flags;
+ /*
+ * The amount of data described by each unit of logical block
+ * metadata
+ */
+ __u16 lbmd_interval;
+ /*
+ * Size in bytes of the logical block metadata associated with each
+ * interval
+ */
+ __u8 lbmd_size;
+ /*
+ * Size in bytes of the opaque block tag associated with each
+ * interval
+ */
+ __u8 lbmd_opaque_size;
+ /*
+ * Offset in bytes of the opaque block tag within the logical block
+ * metadata
+ */
+ __u8 lbmd_opaque_offset;
+ /* Size in bytes of the T10 PI tuple associated with each interval */
+ __u8 lbmd_pi_size;
+ /* Offset in bytes of T10 PI tuple within the logical block metadata */
+ __u8 lbmd_pi_offset;
+ /* T10 PI guard tag type */
+ __u8 lbmd_guard_tag_type;
+ /* Size in bytes of the T10 PI application tag */
+ __u8 lbmd_app_tag_size;
+ /* Size in bytes of the T10 PI reference tag */
+ __u8 lbmd_ref_tag_size;
+ /* Size in bytes of the T10 PI storage tag */
+ __u8 lbmd_storage_tag_size;
+ __u8 pad;
+};
+
+#define FS_IOC_GETLBMD_CAP _IOWR(0x15, 2, struct logical_block_metadata_cap)
+#endif /* FS_IOC_GETLBMD_CAP */
+
+#ifndef IO_INTEGRITY_CHK_GUARD
+/* flags for integrity meta */
+#define IO_INTEGRITY_CHK_GUARD (1U << 0) /* enforce guard check */
+#define IO_INTEGRITY_CHK_REFTAG (1U << 1) /* enforce ref check */
+#define IO_INTEGRITY_CHK_APPTAG (1U << 2) /* enforce app check */
+#endif /* IO_INTEGRITY_CHK_GUARD */
+
+/* This size should guarantee at least one split */
+#define DATA_SIZE (8 * 1024 * 1024)
+
+static unsigned short lba_size;
+static unsigned char metadata_size;
+static unsigned char pi_size;
+static unsigned char pi_offset;
+static bool reftag_enabled;
+
+static long pagesize;
+
+struct t10_pi_tuple {
+ __be16 guard_tag; /* Checksum */
+ __be16 app_tag; /* Opaque storage */
+ __be32 ref_tag; /* Target LBA or indirect LBA */
+};
+
+struct crc64_pi_tuple {
+ __be64 guard_tag;
+ __be16 app_tag;
+ __u8 ref_tag[6];
+};
+
+static int init_capabilities(int fd)
+{
+ struct logical_block_metadata_cap md_cap;
+ int ret;
+
+ ret = ioctl(fd, FS_IOC_GETLBMD_CAP, &md_cap);
+ if (ret < 0) {
+ perror("FS_IOC_GETLBMD_CAP");
+ return ret;
+ }
+
+ lba_size = md_cap.lbmd_interval;
+ metadata_size = md_cap.lbmd_size;
+ pi_size = md_cap.lbmd_pi_size;
+ pi_offset = md_cap.lbmd_pi_offset;
+ reftag_enabled = md_cap.lbmd_flags & LBMD_PI_CAP_REFTAG;
+
+ pagesize = sysconf(_SC_PAGE_SIZE);
+ return 0;
+}
+
+static unsigned int swap(unsigned int value)
+{
+ return ((value >> 24) & 0x000000ff) |
+ ((value >> 8) & 0x0000ff00) |
+ ((value << 8) & 0x00ff0000) |
+ ((value << 24) & 0xff000000);
+}
+
+static inline void __put_unaligned_be48(const __u64 val, __u8 *p)
+{
+ *p++ = (val >> 40) & 0xff;
+ *p++ = (val >> 32) & 0xff;
+ *p++ = (val >> 24) & 0xff;
+ *p++ = (val >> 16) & 0xff;
+ *p++ = (val >> 8) & 0xff;
+ *p++ = val & 0xff;
+}
+
+static inline void put_unaligned_be48(const __u64 val, void *p)
+{
+ __put_unaligned_be48(val, p);
+}
+
+static inline __u64 __get_unaligned_be48(const __u8 *p)
+{
+ return (__u64)p[0] << 40 | (__u64)p[1] << 32 | (__u64)p[2] << 24 |
+ p[3] << 16 | p[4] << 8 | p[5];
+}
+
+static inline __u64 get_unaligned_be48(const void *p)
+{
+ return __get_unaligned_be48(p);
+}
+
+static void init_metadata(void *p, int intervals, int ref)
+{
+ int i, j;
+
+ for (i = 0; i < intervals; i++, ref++) {
+ int remaining = metadata_size - pi_offset;
+ unsigned char *m = p;
+
+ for (j = 0; j < pi_offset; j++)
+ m[j] = (unsigned char)(ref + j + i);
+
+ p += pi_offset;
+ if (reftag_enabled) {
+ if (pi_size == 8) {
+ struct t10_pi_tuple *tuple = p;
+
+ tuple->ref_tag = swap(ref);
+ remaining -= sizeof(*tuple);
+ p += sizeof(*tuple);
+ } else if (pi_size == 16) {
+ struct crc64_pi_tuple *tuple = p;
+
+ __put_unaligned_be48(ref, tuple->ref_tag);
+ remaining -= sizeof(*tuple);
+ p += sizeof(*tuple);
+ }
+ }
+
+ m = p;
+ for (j = 0; j < remaining; j++)
+ m[j] = (unsigned char)~(ref + j + i);
+
+ p += remaining;
+ }
+}
+
+static int check_metadata(void *p, int intervals, int ref)
+{
+ int i, j;
+
+ for (i = 0; i < intervals; i++, ref++) {
+ int remaining = metadata_size - pi_offset;
+ unsigned char *m = p;
+
+ for (j = 0; j < pi_offset; j++) {
+ if (m[j] != (unsigned char)(ref + j + i)) {
+ fprintf(stderr, "(pre)interval:%d byte:%d expected:%x got:%x\n",
+ i, j, (unsigned char)(ref + j + i), m[j]);
+ return -1;
+ }
+ }
+
+ p += pi_offset;
+ if (reftag_enabled) {
+ if (pi_size == 8) {
+ struct t10_pi_tuple *tuple = p;
+
+ if (swap(tuple->ref_tag) != ref) {
+ fprintf(stderr, "reftag interval:%d expected:%x got:%x\n",
+ i, ref, swap(tuple->ref_tag));
+ return -1;
+ }
+
+ remaining -= sizeof(*tuple);
+ p += sizeof(*tuple);
+ } else if (pi_size == 16) {
+ struct crc64_pi_tuple *tuple = p;
+ __u64 v = get_unaligned_be48(tuple->ref_tag);
+
+ if (v != ref) {
+ fprintf(stderr, "reftag interval:%d expected:%x got:%llx\n",
+ i, ref, v);
+ return -1;
+ }
+ remaining -= sizeof(*tuple);
+ p += sizeof(*tuple);
+ }
+ }
+
+ m = p;
+ for (j = 0; j < remaining; j++) {
+ if (m[j] != (unsigned char)~(ref + j + i)) {
+ fprintf(stderr, "(post)interval:%d byte:%d expected:%x got:%x\n",
+ i, j, (unsigned char)~(ref + j + i), m[j]);
+ return -1;
+ }
+ }
+
+ p += remaining;
+ }
+
+ return 0;
+}
+
+static void init_data(void *data, int offset)
+{
+ unsigned char *d = data;
+ int i;
+
+ for (i = 0; i < DATA_SIZE; i++)
+ d[i] = (unsigned char)(0xaa + offset + i);
+}
+
+static int check_data(void *data, int offset)
+{
+ unsigned char *d = data;
+ int i;
+
+ for (i = 0; i < DATA_SIZE; i++)
+ if (d[i] != (unsigned char)(0xaa + offset + i))
+ return -1;
+
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ int fd, ret, i, offset, intervals, metabuffer_size, metabuffer_tx_size;
+ void *orig_data_buf, *orig_pi_buf, *data_buf;
+ struct io_uring_cqe *cqes[2];
+ struct io_uring_cqe *cqe;
+ struct io_uring_sqe *sqe;
+ struct io_uring ring;
+
+ if (argc < 2) {
+ fprintf(stderr, "Usage: %s <dev>\n", argv[0]);
+ return 1;
+ }
+
+ fd = open(argv[1], O_RDWR | O_DIRECT);
+ if (fd < 0) {
+ perror("Failed to open device with O_DIRECT");
+ return 1;
+ }
+
+ ret = init_capabilities(fd);
+ if (ret < 0)
+ return 1;
+ if (lba_size == 0 || metadata_size == 0)
+ return 1;
+
+ intervals = DATA_SIZE / lba_size;
+ metabuffer_tx_size = intervals * metadata_size;
+ metabuffer_size = metabuffer_tx_size * 2;
+
+ if (posix_memalign(&orig_data_buf, pagesize, DATA_SIZE)) {
+ perror("posix_memalign failed for data buffer");
+ ret = 1;
+ goto close;
+ }
+
+ if (posix_memalign(&orig_pi_buf, pagesize, metabuffer_size)) {
+ perror("posix_memalign failed for metadata buffer");
+ ret = 1;
+ goto free;
+ }
+
+ ret = io_uring_queue_init(8, &ring, 0);
+ if (ret < 0) {
+ perror("io_uring_queue_init failed");
+ goto cleanup;
+ }
+
+ data_buf = orig_data_buf;
+ for (offset = 0; offset < 512; offset++) {
+ void *pi_buf = (char *)orig_pi_buf + offset * 4;
+ struct io_uring_attr_pi pi_attr = {
+ .addr = (__u64)pi_buf,
+ .seed = offset,
+ .len = metabuffer_tx_size,
+ };
+
+ if (reftag_enabled)
+ pi_attr.flags = IO_INTEGRITY_CHK_REFTAG;
+
+ init_data(data_buf, offset);
+ init_metadata(pi_buf, intervals, offset);
+
+ sqe = io_uring_get_sqe(&ring);
+ if (!sqe) {
+ fprintf(stderr, "Failed to get SQE\n");
+ ret = 1;
+ goto ring_exit;
+ }
+
+ io_uring_prep_write(sqe, fd, data_buf, DATA_SIZE, offset * lba_size * 8);
+ io_uring_sqe_set_data(sqe, (void *)1L);
+
+#ifdef PI_URING_COMPAT
+ /* old liburing, use fields that overlap in the union */
+ sqe->__pad2[0] = IORING_RW_ATTR_FLAG_PI;
+ sqe->addr3 = (__u64)&pi_attr;
+#else
+ sqe->attr_type_mask = IORING_RW_ATTR_FLAG_PI;
+ sqe->attr_ptr = (__u64)&pi_attr;
+#endif
+ ret = io_uring_submit(&ring);
+ if (ret < 1) {
+ perror("io_uring_submit failed (WRITE)");
+ ret = 1;
+ goto ring_exit;
+ }
+
+ ret = io_uring_wait_cqe(&ring, &cqe);
+ if (ret < 0) {
+ perror("io_uring_wait_cqe failed (WRITE)");
+ ret = 1;
+ goto ring_exit;
+ }
+
+ if (cqe->res < 0) {
+ fprintf(stderr, "write failed at offset %d: %s\n",
+ offset, strerror(-cqe->res));
+ ret = 1;
+ goto ring_exit;
+ }
+
+ io_uring_cqe_seen(&ring, cqe);
+
+ memset(data_buf, 0, DATA_SIZE);
+ memset(pi_buf, 0, metabuffer_tx_size);
+
+ sqe = io_uring_get_sqe(&ring);
+ if (!sqe) {
+ fprintf(stderr, "failed to get SQE\n");
+ ret = 1;
+ goto ring_exit;
+ }
+
+ io_uring_prep_read(sqe, fd, data_buf, DATA_SIZE, offset * lba_size * 8);
+ io_uring_sqe_set_data(sqe, (void *)2L);
+
+#ifdef PI_URING_COMPAT
+ sqe->__pad2[0] = IORING_RW_ATTR_FLAG_PI;
+ sqe->addr3 = (__u64)&pi_attr;
+#else
+ sqe->attr_type_mask = IORING_RW_ATTR_FLAG_PI;
+ sqe->attr_ptr = (__u64)&pi_attr;
+#endif
+
+ ret = io_uring_submit(&ring);
+ if (ret < 1) {
+ perror("io_uring_submit failed (read)");
+ ret = 1;
+ goto ring_exit;
+ }
+
+ ret = io_uring_wait_cqe(&ring, &cqe);
+ if (ret < 0) {
+ fprintf(stderr, "io_uring_wait_cqe failed (read): %s\n", strerror(-ret));
+ ret = 1;
+ goto ring_exit;
+ }
+
+ if (cqe->res < 0) {
+ fprintf(stderr, "read failed at offset %d: %s\n",
+ offset, strerror(-cqe->res));
+ ret = 1;
+ goto ring_exit;
+ }
+
+ ret = check_data(data_buf, offset);
+ if (ret) {
+ fprintf(stderr, "data corruption at offset %d\n",
+ offset);
+ ret = 1;
+ goto ring_exit;
+ }
+
+ ret = check_metadata(pi_buf, intervals, offset);
+ if (ret) {
+ fprintf(stderr, "metadata corruption at offset %d\n",
+ offset);
+ ret = 1;
+ goto ring_exit;
+ }
+
+ io_uring_cqe_seen(&ring, cqe);
+ }
+
+ memset(data_buf, 0, DATA_SIZE);
+ for (i = 0; i < 2; i++) {
+ sqe = io_uring_get_sqe(&ring);
+ if (!sqe) {
+ fprintf(stderr, "failed get sqe\n");
+ ret = 1;
+ goto ring_exit;
+ }
+
+ io_uring_prep_write(sqe, fd, data_buf, DATA_SIZE, DATA_SIZE * i);
+ io_uring_sqe_set_data(sqe, (void *)(uintptr_t)i + 1);
+ }
+
+ ret = io_uring_submit(&ring);
+ if (ret < 1) {
+ fprintf(stderr, "failed to submit sqes\n");
+ goto ring_exit;
+ }
+ ret = io_uring_wait_cqe_nr(&ring, cqes, 2);
+ if (ret)
+ fprintf(stderr, "failed to reap cqes\n");
+ring_exit:
+ io_uring_queue_exit(&ring);
+cleanup:
+ free(orig_pi_buf);
+free:
+ free(orig_data_buf);
+close:
+ close(fd);
+ return ret;
+}
diff --git a/tests/block/043 b/tests/block/043
new file mode 100755
index 0000000..dcd80d4
--- /dev/null
+++ b/tests/block/043
@@ -0,0 +1,33 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright 2025 Keith Busch <kbusch@kernel.org>
+#
+# Tests various user space metadata offsets with io_uring capabilities. If the
+# format uses ref tag protection, test various seed offsets as well.
+
+. tests/block/rc
+. common/nvme
+
+DESCRIPTION="Test userspace metadata offsets"
+QUICK=1
+
+device_requires() {
+ _test_dev_has_metadata
+ _test_dev_disables_extended_lba
+}
+
+requires() {
+ _have_kernel_option IO_URING
+ _have_kernel_option BLK_DEV_INTEGRITY
+}
+
+test_device() {
+ echo "Running ${TEST_NAME}"
+
+ if ! src/metadata ${TEST_DEV}; then
+ echo "src/metadata failed"
+ fi
+
+ echo "Test complete"
+}
+
diff --git a/tests/block/043.out b/tests/block/043.out
new file mode 100644
index 0000000..fda7fca
--- /dev/null
+++ b/tests/block/043.out
@@ -0,0 +1,2 @@
+Running block/043
+Test complete
--
2.47.3
^ permalink raw reply related [flat|nested] 12+ messages in thread