[PATCH 1/2] engines/io_uring: support r/w with metadata

Vincent Fu
2025-07-23T17:05:53Z

Use the new IOCTL to query block devices for metadata support. Then use
the new io_uring capability to send read and write commands with
metadata.

Signed-off-by: Anuj Gupta <anuj20.g@samsung.com>
Signed-off-by: Vincent Fu <vincent.fu@samsung.com>
---
engines/io_uring.c | 241 +++++++++++++++++++++++++++++++++++++++++---
engines/nvme.c | 35 +++----
engines/nvme.h | 18 ++++
io_u.h | 1 +
os/linux/io_uring.h | 15 +++
5 files changed, 273 insertions(+), 37 deletions(-)

diff --git a/engines/io_uring.c b/engines/io_uring.c
index 87018f84..180db5b5 100644
--- a/engines/io_uring.c
+++ b/engines/io_uring.c
@@ -30,6 +30,71 @@

#include <sys/stat.h>

+#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 */
+
+#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 */
+
enum uring_cmd_type {
FIO_URING_CMD_NVME = 1,
};
@@ -73,6 +138,7 @@ struct ioring_data {

struct io_u **io_u_index;
char *md_buf;
+ char *pi_attr;

int *fds;

@@ -436,6 +502,27 @@ static int fio_ioring_prep(struct thread_data *td, struct io_u *io_u)
sqe->len = 1;
}
}
+ if (o->md_per_io_size) {
+ struct io_uring_attr_pi *pi_attr = io_u->pi_attr;
+ struct nvme_data *data = FILE_ENG_DATA(io_u->file);
+
+ sqe->attr_type_mask = IORING_RW_ATTR_FLAG_PI;
+ sqe->attr_ptr = (__u64)(uintptr_t)pi_attr;
+ pi_attr->addr = (__u64)(uintptr_t)io_u->mmap_data;
+ pi_attr->len = o->md_per_io_size;
+ pi_attr->app_tag = o->apptag;
+ pi_attr->flags = 0;
+ if (strstr(o->pi_chk, "GUARD") != NULL)
+ pi_attr->flags |= IO_INTEGRITY_CHK_GUARD;
+ if (strstr(o->pi_chk, "REFTAG") != NULL) {
+ __u64 slba = get_slba(data, io_u->offset);
+
+ pi_attr->flags |= IO_INTEGRITY_CHK_REFTAG;
+ pi_attr->seed = (__u32)slba;
+ }
+ if (strstr(o->pi_chk, "APPTAG") != NULL)
+ pi_attr->flags |= IO_INTEGRITY_CHK_APPTAG;
+ }
sqe->rw_flags = 0;
if (!td->o.odirect && o->uncached)
sqe->rw_flags |= RWF_DONTCACHE;
@@ -565,6 +652,7 @@ static int fio_ioring_cmd_prep(struct thread_data *td, struct io_u *io_u)
static struct io_u *fio_ioring_event(struct thread_data *td, int event)
{
struct ioring_data *ld = td->io_ops_data;
+ struct ioring_options *o = td->eo;
struct io_uring_cqe *cqe;
struct io_u *io_u;
unsigned index;
@@ -590,6 +678,19 @@ static struct io_u *fio_ioring_event(struct thread_data *td, int event)
io_u->error = -cqe->res;
else
io_u->resid = io_u->xfer_buflen - cqe->res;
+ return io_u;
+ }
+
+ if (o->md_per_io_size) {
+ struct nvme_data *data;
+ int ret;
+
+ data = FILE_ENG_DATA(io_u->file);
+ if (data->pi_type && (io_u->ddir == DDIR_READ) && !o->pi_act) {
+ ret = fio_nvme_pi_verify(data, io_u);
+ if (ret)
+ io_u->error = ret;
+ }
}

return io_u;
@@ -802,6 +903,21 @@ static enum fio_q_status fio_ioring_queue(struct thread_data *td,
o->cmd_type == FIO_URING_CMD_NVME)
fio_ioring_cmd_nvme_pi(td, io_u);

+ if (!strcmp(td->io_ops->name, "io_uring") && o->md_per_io_size) {
+ struct nvme_data *data = FILE_ENG_DATA(io_u->file);
+ struct nvme_cmd_ext_io_opts ext_opts = {0};
+
+ if (data->pi_type) {
+ if (o->pi_act)
+ ext_opts.io_flags |= NVME_IO_PRINFO_PRACT;
+
+ ext_opts.io_flags |= o->prchk;
+ ext_opts.apptag = o->apptag;
+ ext_opts.apptag_mask = o->apptag_mask;
+ }
+ fio_nvme_generate_guard(io_u, &ext_opts);
+ }
+
tail = *ring->tail;
ring->array[tail & ld->sq_ring_mask] = io_u->index;
atomic_store_release(ring->tail, tail + 1);
@@ -920,6 +1036,7 @@ static void fio_ioring_cleanup(struct thread_data *td)
fio_cmdprio_cleanup(&ld->cmdprio);
free(ld->io_u_index);
free(ld->md_buf);
+ free(ld->pi_attr);
free(ld->iovecs);
free(ld->fds);
free(ld->dsm);
@@ -1347,12 +1464,20 @@ static int fio_ioring_init(struct thread_data *td)
/* io_u index */
ld->io_u_index = calloc(td->o.iodepth, sizeof(struct io_u *));

+ if (!strcmp(td->io_ops->name, "io_uring") && o->md_per_io_size) {
+ if (o->apptag_mask != 0xffff) {
+ log_err("fio: io_uring with metadata requires an apptag_mask of 0xffff\n");
+ return 1;
+ }
+ }
+
/*
* metadata buffer for nvme command.
* We are only supporting iomem=malloc / mem=malloc as of now.
*/
- if (!strcmp(td->io_ops->name, "io_uring_cmd") &&
- (o->cmd_type == FIO_URING_CMD_NVME) && o->md_per_io_size) {
+ if ((!strcmp(td->io_ops->name, "io_uring_cmd") &&
+ (o->cmd_type == FIO_URING_CMD_NVME) && o->md_per_io_size) ||
+ (!strcmp(td->io_ops->name, "io_uring") && o->md_per_io_size)) {
md_size = (unsigned long long) o->md_per_io_size
* (unsigned long long) td->o.iodepth;
md_size += page_mask + td->o.mem_align;
@@ -1363,6 +1488,12 @@ static int fio_ioring_init(struct thread_data *td)
free(ld);
return 1;
}
+ ld->pi_attr = calloc(ld->iodepth, sizeof(struct io_uring_attr_pi));
+ if (!ld->pi_attr) {
+ free(ld->md_buf);
+ free(ld);
+ return 1;
+ }
}
parse_prchk_flags(o);

@@ -1429,22 +1560,23 @@ static int fio_ioring_io_u_init(struct thread_data *td, struct io_u *io_u)
struct ioring_data *ld = td->io_ops_data;
struct ioring_options *o = td->eo;
struct nvme_pi_data *pi_data;
- char *p;
+ char *p, *q;

ld->io_u_index[io_u->index] = io_u;

- if (!strcmp(td->io_ops->name, "io_uring_cmd")) {
- p = PTR_ALIGN(ld->md_buf, page_mask) + td->o.mem_align;
- p += o->md_per_io_size * io_u->index;
- io_u->mmap_data = p;
-
- if (!o->pi_act) {
- pi_data = calloc(1, sizeof(*pi_data));
- pi_data->io_flags |= o->prchk;
- pi_data->apptag_mask = o->apptag_mask;
- pi_data->apptag = o->apptag;
- io_u->engine_data = pi_data;
- }
+ p = PTR_ALIGN(ld->md_buf, page_mask) + td->o.mem_align;
+ p += o->md_per_io_size * io_u->index;
+ io_u->mmap_data = p;
+ q = ld->pi_attr;
+ q += (sizeof(struct io_uring_attr_pi) * io_u->index);
+ io_u->pi_attr = q;
+
+ if (!o->pi_act) {
+ pi_data = calloc(1, sizeof(*pi_data));
+ pi_data->io_flags |= o->prchk;
+ pi_data->apptag_mask = o->apptag_mask;
+ pi_data->apptag = o->apptag;
+ io_u->engine_data = pi_data;
}

return 0;
@@ -1463,11 +1595,90 @@ static void fio_ioring_io_u_free(struct thread_data *td, struct io_u *io_u)
}
}

+static int fio_get_pi_info(struct fio_file *f, __u64 *nlba, __u32 pi_act,
+ struct nvme_data *data)
+{
+ struct logical_block_metadata_cap md_cap;
+ int ret;
+ int fd, err = 0;
+
+ fd = open(f->file_name, O_RDONLY);
+ if (fd < 0)
+ return -errno;
+
+ ret = ioctl(fd, FS_IOC_GETLBMD_CAP, &md_cap);
+ if (ret < 0) {
+ err = -errno;
+ log_err("%s: failed to query protection information capabilities; error %d\n", f->file_name, errno);
+ goto out;
+ }
+
+ if (!(md_cap.lbmd_flags & LBMD_PI_CAP_INTEGRITY)) {
+ log_err("%s: Protection information not supported\n", f->file_name);
+ err = -ENOTSUP;
+ goto out;
+ }
+
+ /* Currently we don't support storage tags */
+ if (md_cap.lbmd_storage_tag_size) {
+ log_err("%s: Storage tag not supported\n", f->file_name);
+ err = -ENOTSUP;
+ goto out;
+ }
+
+ data->lba_size = md_cap.lbmd_interval;
+ data->lba_shift = ilog2(data->lba_size);
+ data->ms = md_cap.lbmd_size;
+ data->pi_size = md_cap.lbmd_pi_size;
+ data->pi_loc = !(md_cap.lbmd_pi_offset);
+
+ /* Assume Type 1 PI if reference tags supported */
+ if (md_cap.lbmd_flags & LBMD_PI_CAP_REFTAG)
+ data->pi_type = NVME_NS_DPS_PI_TYPE1;
+ else
+ data->pi_type = NVME_NS_DPS_PI_TYPE3;
+
+ switch (md_cap.lbmd_guard_tag_type) {
+ case LBMD_PI_CSUM_CRC16_T10DIF:
+ data->guard_type = NVME_NVM_NS_16B_GUARD;
+ break;
+ case LBMD_PI_CSUM_CRC64_NVME:
+ data->guard_type = NVME_NVM_NS_64B_GUARD;
+ break;
+ default:
+ log_err("%s: unsupported checksum type %d\n", f->file_name, md_cap.lbmd_guard_tag_type);
+ err = -ENOTSUP;
+ goto out;
+ }
+
+out:
+ close(fd);
+ return err;
+}
+
static int fio_ioring_open_file(struct thread_data *td, struct fio_file *f)
{
struct ioring_data *ld = td->io_ops_data;
struct ioring_options *o = td->eo;

+ if (o->md_per_io_size) {
+ struct nvme_data *data = NULL;
+ __u64 nlba = 0;
+ int ret;
+
+ data = FILE_ENG_DATA(f);
+ if (data == NULL) {
+ data = calloc(1, sizeof(struct nvme_data));
+ ret = fio_get_pi_info(f, &nlba, o->pi_act, data);
+ if (ret) {
+ free(data);
+ return ret;
+ }
+
+ FILE_SET_ENG_DATA(f, data);
+ }
+ }
+
if (!ld || !o->registerfiles)
return generic_open_file(td, f);

diff --git a/engines/nvme.c b/engines/nvme.c
index 37a31e2f..4b3d3860 100644
--- a/engines/nvme.c
+++ b/engines/nvme.c
@@ -8,22 +8,6 @@
#include "../crc/crc-t10dif.h"
#include "../crc/crc64.h"

-static inline __u64 get_slba(struct nvme_data *data, __u64 offset)
-{
- if (data->lba_ext)
- return offset / data->lba_ext;
-
- return offset >> data->lba_shift;
-}
-
-static inline __u32 get_nlb(struct nvme_data *data, __u64 len)
-{
- if (data->lba_ext)
- return len / data->lba_ext - 1;
-
- return (len >> data->lba_shift) - 1;
-}
-
static void fio_nvme_generate_pi_16b_guard(struct nvme_data *data,
struct io_u *io_u,
struct nvme_cmd_ext_io_opts *opts)
@@ -421,14 +405,9 @@ int fio_nvme_uring_cmd_prep(struct nvme_uring_cmd *cmd, struct io_u *io_u,
return 0;
}

-void fio_nvme_pi_fill(struct nvme_uring_cmd *cmd, struct io_u *io_u,
- struct nvme_cmd_ext_io_opts *opts)
+void fio_nvme_generate_guard(struct io_u *io_u, struct nvme_cmd_ext_io_opts *opts)
{
struct nvme_data *data = FILE_ENG_DATA(io_u->file);
- __u64 slba;
-
- slba = get_slba(data, io_u->offset);
- cmd->cdw12 |= opts->io_flags;

if (data->pi_type && !(opts->io_flags & NVME_IO_PRINFO_PRACT)) {
if (data->guard_type == NVME_NVM_NS_16B_GUARD)
@@ -436,6 +415,18 @@ void fio_nvme_pi_fill(struct nvme_uring_cmd *cmd, struct io_u *io_u,
else if (data->guard_type == NVME_NVM_NS_64B_GUARD)
fio_nvme_generate_pi_64b_guard(data, io_u, opts);
}
+}
+
+void fio_nvme_pi_fill(struct nvme_uring_cmd *cmd, struct io_u *io_u,
+ struct nvme_cmd_ext_io_opts *opts)
+{
+ struct nvme_data *data = FILE_ENG_DATA(io_u->file);
+ __u64 slba;
+
+ slba = get_slba(data, io_u->offset);
+ cmd->cdw12 |= opts->io_flags;
+
+ fio_nvme_generate_guard(io_u, opts);

switch (data->pi_type) {
case NVME_NS_DPS_PI_TYPE1:
diff --git a/engines/nvme.h b/engines/nvme.h
index 60b38d7f..4371eb5b 100644
--- a/engines/nvme.h
+++ b/engines/nvme.h
@@ -438,6 +438,8 @@ int fio_nvme_uring_cmd_prep(struct nvme_uring_cmd *cmd, struct io_u *io_u,
void fio_nvme_pi_fill(struct nvme_uring_cmd *cmd, struct io_u *io_u,
struct nvme_cmd_ext_io_opts *opts);

+void fio_nvme_generate_guard(struct io_u *io_u, struct nvme_cmd_ext_io_opts *opts);
+
int fio_nvme_pi_verify(struct nvme_data *data, struct io_u *io_u);

int fio_nvme_get_zoned_model(struct thread_data *td, struct fio_file *f,
@@ -476,4 +478,20 @@ static inline bool fio_nvme_pi_ref_escape(__u8 *reftag)
return memcmp(reftag, ref_esc, sizeof(ref_esc)) == 0;
}

+static inline __u64 get_slba(struct nvme_data *data, __u64 offset)
+{
+ if (data->lba_ext)
+ return offset / data->lba_ext;
+
+ return offset >> data->lba_shift;
+}
+
+static inline __u32 get_nlb(struct nvme_data *data, __u64 len)
+{
+ if (data->lba_ext)
+ return len / data->lba_ext - 1;
+
+ return (len >> data->lba_shift) - 1;
+}
+
#endif
diff --git a/io_u.h b/io_u.h
index 178c1229..2d20a2b2 100644
--- a/io_u.h
+++ b/io_u.h
@@ -145,6 +145,7 @@ struct io_u {
#endif
void *mmap_data;
};
+ void *pi_attr;
};

/*
diff --git a/os/linux/io_uring.h b/os/linux/io_uring.h
index b3876381..7b099902 100644
--- a/os/linux/io_uring.h
+++ b/os/linux/io_uring.h
@@ -70,6 +70,10 @@ struct io_uring_sqe {
__u64 addr3;
__u64 __pad2[1];
};
+ struct {
+ __u64 attr_ptr; /* pointer to attribute information */
+ __u64 attr_type_mask; /* bit mask of attributes */
+ };
/*
* If the ring is initialized with IORING_SETUP_SQE128, then
* this field is used for 80 bytes of arbitrary command data
@@ -78,6 +82,17 @@ struct io_uring_sqe {
};
};

+/* sqe->attr_type_mask flags */
+#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;
+};
enum {
IOSQE_FIXED_FILE_BIT,
IOSQE_IO_DRAIN_BIT,
--
2.47.2