Linux block layer
 help / color / mirror / Atom feed
* [PATCH 11/12] dm-linear: Enable copy offloading
From: Bart Van Assche @ 2026-04-24 22:42 UTC (permalink / raw)
  To: Jens Axboe
  Cc: linux-block, linux-scsi, linux-nvme, Christoph Hellwig,
	Nitesh Shetty, Bart Van Assche
In-Reply-To: <20260424224201.1949243-1-bvanassche@acm.org>

Set BLK_FEAT_STACKING_COPY_OFFL and max_copy_hw_sectors to enable copy
offloading.

Signed-off-by: Bart Van Assche <bvanassche@acm.org>
---
 drivers/md/dm-linear.c | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/drivers/md/dm-linear.c b/drivers/md/dm-linear.c
index 38c17846deb0..3de8bf5f11fb 100644
--- a/drivers/md/dm-linear.c
+++ b/drivers/md/dm-linear.c
@@ -119,6 +119,11 @@ static void linear_status(struct dm_target *ti, status_type_t type,
 	}
 }
 
+static void linear_io_hints(struct dm_target *ti, struct queue_limits *limits)
+{
+	limits->features |= BLK_FEAT_STACKING_COPY_OFFL;
+}
+
 static int linear_prepare_ioctl(struct dm_target *ti, struct block_device **bdev,
 				unsigned int cmd, unsigned long arg,
 				bool *forward)
@@ -211,6 +216,7 @@ static struct target_type linear_target = {
 	.dtr    = linear_dtr,
 	.map    = linear_map,
 	.status = linear_status,
+	.io_hints = linear_io_hints,
 	.prepare_ioctl = linear_prepare_ioctl,
 	.iterate_devices = linear_iterate_devices,
 	.direct_access = linear_dax_direct_access,

^ permalink raw reply related

* [PATCH 08/12] nvme: Add copy offloading support
From: Bart Van Assche @ 2026-04-24 22:41 UTC (permalink / raw)
  To: Jens Axboe
  Cc: linux-block, linux-scsi, linux-nvme, Christoph Hellwig,
	Nitesh Shetty, Bart Van Assche, Kanchan Joshi,
	Javier González, Anuj Gupta
In-Reply-To: <20260424224201.1949243-1-bvanassche@acm.org>

From: Nitesh Shetty <nj.shetty@samsung.com>

Add support for the NVMe Copy command. This command supports a single
destination range and up to 256 source ranges.

Add trace event support for nvme_copy_cmd.

Signed-off-by: Kanchan Joshi <joshi.k@samsung.com>
Signed-off-by: Nitesh Shetty <nj.shetty@samsung.com>
Signed-off-by: Javier González <javier.gonz@samsung.com>
Signed-off-by: Anuj Gupta <anuj20.g@samsung.com>
[ bvanassche: generalized Copy support from one to 256 source ranges; fixed
  an endianness issue in nvme_config_copy(); renamed rsvd91 into rsvd81 and
  verified the offset with pahole ]
Signed-off-by: Bart Van Assche <bvanassche@acm.org>
---
 drivers/nvme/host/constants.c |   1 +
 drivers/nvme/host/core.c      | 106 ++++++++++++++++++++++++++++++++++
 drivers/nvme/host/trace.c     |  19 ++++++
 include/linux/nvme.h          |  46 ++++++++++++++-
 4 files changed, 169 insertions(+), 3 deletions(-)

diff --git a/drivers/nvme/host/constants.c b/drivers/nvme/host/constants.c
index dc90df9e13a2..b80c7c7fb629 100644
--- a/drivers/nvme/host/constants.c
+++ b/drivers/nvme/host/constants.c
@@ -19,6 +19,7 @@ static const char * const nvme_ops[] = {
 	[nvme_cmd_resv_report] = "Reservation Report",
 	[nvme_cmd_resv_acquire] = "Reservation Acquire",
 	[nvme_cmd_resv_release] = "Reservation Release",
+	[nvme_cmd_copy] = "Copy Offload",
 	[nvme_cmd_zone_mgmt_send] = "Zone Management Send",
 	[nvme_cmd_zone_mgmt_recv] = "Zone Management Receive",
 	[nvme_cmd_zone_append] = "Zone Append",
diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c
index 1e33af94c24b..6f3c1fde112f 100644
--- a/drivers/nvme/host/core.c
+++ b/drivers/nvme/host/core.c
@@ -6,6 +6,7 @@
 
 #include <linux/async.h>
 #include <linux/blkdev.h>
+#include <linux/blk-copy.h>
 #include <linux/blk-mq.h>
 #include <linux/blk-integrity.h>
 #include <linux/compat.h>
@@ -821,6 +822,87 @@ static inline void nvme_setup_flush(struct nvme_ns *ns,
 	cmnd->common.nsid = cpu_to_le32(ns->head->ns_id);
 }
 
+/*
+ * Translate REQ_OP_COPY_SRC and REQ_OP_COPY_DST bios into an NVMe Copy command.
+ * The NVMe copy command supports multiple source LBA ranges, a single
+ * destination LBA range, and also supports copying across NVMe namespaces. This
+ * implementation supports all these features except copying across NVMe
+ * namespaces.
+ */
+static inline blk_status_t nvme_setup_copy_offload(struct nvme_ns *ns,
+						   struct request *req,
+						   struct nvme_command *cmnd)
+{
+	const u32 nr_range = blk_copy_bio_count(req, REQ_OP_COPY_SRC);
+	struct nvme_ns *src_ns, *dst_ns;
+	struct bio *src_bio = NULL, *dst_bio;
+	struct nvme_copy_range *range;
+	u16 control = 0;
+	u64 dlba;
+
+	dst_bio = blk_first_copy_bio(req, REQ_OP_COPY_DST);
+
+	if (WARN_ON_ONCE(!dst_bio))
+		return BLK_STS_IOERR;
+
+	/* TO DO: derive dst_ns from dst_bio. */
+	dst_ns = ns;
+	dlba = nvme_sect_to_lba(dst_ns->head, dst_bio->bi_iter.bi_sector);
+
+	if (req->cmd_flags & REQ_FUA)
+		control |= NVME_RW_FUA;
+
+	if (req->cmd_flags & REQ_FAILFAST_DEV)
+		control |= NVME_RW_LR;
+
+	*cmnd = (typeof(*cmnd)){
+		.copy = {
+			.opcode = nvme_cmd_copy,
+			.nsid = cpu_to_le32(dst_ns->head->ns_id),
+			.control = cpu_to_le16(control),
+			.sdlba = cpu_to_le64(dlba),
+			.desfmt_prinfor = 2, /* DESFMT=2 */
+			.nr_range = nr_range - 1, /* 0's based */
+		}
+	};
+
+	range = kmalloc_array(nr_range, sizeof(*range),
+			      GFP_ATOMIC | __GFP_ZERO | __GFP_NOWARN);
+	if (!range)
+		return BLK_STS_RESOURCE;
+
+	for (unsigned int i = 0; i < nr_range; i++) {
+		u64 slba;
+		u32 nslb;
+
+		if (!src_bio)
+			src_bio = blk_first_copy_bio(req, REQ_OP_COPY_SRC);
+		else
+			src_bio = blk_next_copy_bio(src_bio);
+		if (WARN_ON_ONCE(!src_bio))
+			goto free_range;
+		/* TO DO: derive src_ns from src_bio. */
+		src_ns = ns;
+		slba = nvme_sect_to_lba(src_ns->head,
+					src_bio->bi_iter.bi_sector);
+		nslb = src_bio->bi_iter.bi_size >> src_ns->head->lba_shift;
+		range[i].nsid = cpu_to_le32(src_ns->head->ns_id); /* requires DESFMT=2 */
+		range[i].slba = cpu_to_le64(slba);
+		range[i].nlb = cpu_to_le16(nslb - 1);
+	}
+
+	req->special_vec.bv_page = virt_to_page(range);
+	req->special_vec.bv_offset = offset_in_page(range);
+	req->special_vec.bv_len = sizeof(*range) * nr_range;
+	req->rq_flags |= RQF_SPECIAL_PAYLOAD;
+
+	return BLK_STS_OK;
+
+free_range:
+	kfree(range);
+	return BLK_STS_IOERR;
+}
+
 static blk_status_t nvme_setup_discard(struct nvme_ns *ns, struct request *req,
 		struct nvme_command *cmnd)
 {
@@ -1122,6 +1204,10 @@ blk_status_t nvme_setup_cmd(struct nvme_ns *ns, struct request *req)
 	case REQ_OP_ZONE_APPEND:
 		ret = nvme_setup_rw(ns, req, cmd, nvme_cmd_zone_append);
 		break;
+	case REQ_OP_COPY_DST:
+	case REQ_OP_COPY_SRC:
+		ret = nvme_setup_copy_offload(ns, req, cmd);
+		break;
 	default:
 		WARN_ON_ONCE(1);
 		return BLK_STS_IOERR;
@@ -1884,6 +1970,21 @@ static bool nvme_init_integrity(struct nvme_ns_head *head,
 	return true;
 }
 
+static void nvme_config_copy(struct nvme_ns *ns, struct nvme_id_ns *id,
+			     struct queue_limits *lim)
+{
+	struct nvme_ctrl *ctrl = ns->ctrl;
+
+	if (!(ctrl->oncs & NVME_CTRL_ONCS_COPY)) {
+		lim->max_copy_hw_sectors = 0;
+		return;
+	}
+	lim->max_copy_hw_sectors = nvme_lba_to_sect(ns->head,
+						    le16_to_cpu(id->mssrl));
+	lim->max_copy_src_segments = 256;
+	lim->max_copy_dst_segments = 1;
+}
+
 static bool nvme_ns_ids_equal(struct nvme_ns_ids *a, struct nvme_ns_ids *b)
 {
 	return uuid_equal(&a->uuid, &b->uuid) &&
@@ -2416,6 +2517,7 @@ static int nvme_update_ns_info_block(struct nvme_ns *ns,
 	if (!nvme_update_disk_info(ns, id, nvm, &lim))
 		capacity = 0;
 
+	nvme_config_copy(ns, id, &lim);
 	if (IS_ENABLED(CONFIG_BLK_DEV_ZONED) &&
 	    ns->head->ids.csi == NVME_CSI_ZNS)
 		nvme_update_zone_info(ns, &lim, &zi);
@@ -2542,6 +2644,9 @@ static int nvme_update_ns_info(struct nvme_ns *ns, struct nvme_ns_info *info)
 		lim.physical_block_size = ns_lim->physical_block_size;
 		lim.io_min = ns_lim->io_min;
 		lim.io_opt = ns_lim->io_opt;
+		lim.max_copy_hw_sectors = UINT_MAX;
+		lim.max_copy_src_segments = U16_MAX;
+		lim.max_copy_dst_segments = U16_MAX;
 		queue_limits_stack_bdev(&lim, ns->disk->part0, 0,
 					ns->head->disk->disk_name);
 		if (unsupported)
@@ -5368,6 +5473,7 @@ static inline void _nvme_check_size(void)
 	BUILD_BUG_ON(sizeof(struct nvme_download_firmware) != 64);
 	BUILD_BUG_ON(sizeof(struct nvme_format_cmd) != 64);
 	BUILD_BUG_ON(sizeof(struct nvme_dsm_cmd) != 64);
+	BUILD_BUG_ON(sizeof(struct nvme_copy_command) != 64);
 	BUILD_BUG_ON(sizeof(struct nvme_write_zeroes_cmd) != 64);
 	BUILD_BUG_ON(sizeof(struct nvme_abort_cmd) != 64);
 	BUILD_BUG_ON(sizeof(struct nvme_get_log_page_command) != 64);
diff --git a/drivers/nvme/host/trace.c b/drivers/nvme/host/trace.c
index ad25ad1e4041..7096ade7740c 100644
--- a/drivers/nvme/host/trace.c
+++ b/drivers/nvme/host/trace.c
@@ -153,6 +153,23 @@ static const char *nvme_trace_read_write(struct trace_seq *p, u8 *cdw10)
 	return ret;
 }
 
+static const char *nvme_trace_copy(struct trace_seq *p, u8 *cdw10)
+{
+	const char *ret = trace_seq_buffer_ptr(p);
+	u64 sdlba = get_unaligned_le64(cdw10);
+	u8 nr_range = get_unaligned_le16(cdw10 + 8);
+	u16 control = get_unaligned_le16(cdw10 + 10);
+	u32 dsmgmt = get_unaligned_le32(cdw10 + 12);
+	u32 reftag = get_unaligned_le32(cdw10 + 16);
+
+	trace_seq_printf(p,
+		"sdlba=%llu, nr_range=%u, ctrl=0x%x, dsmgmt=%u, reftag=%u",
+		sdlba, nr_range, control, dsmgmt, reftag);
+	trace_seq_putc(p, 0);
+
+	return ret;
+}
+
 static const char *nvme_trace_dsm(struct trace_seq *p, u8 *cdw10)
 {
 	const char *ret = trace_seq_buffer_ptr(p);
@@ -386,6 +403,8 @@ const char *nvme_trace_parse_nvm_cmd(struct trace_seq *p,
 		return nvme_trace_resv_rel(p, cdw10);
 	case nvme_cmd_resv_report:
 		return nvme_trace_resv_report(p, cdw10);
+	case nvme_cmd_copy:
+		return nvme_trace_copy(p, cdw10);
 	default:
 		return nvme_trace_common(p, cdw10);
 	}
diff --git a/include/linux/nvme.h b/include/linux/nvme.h
index 041f30931a90..ead8e5128e3b 100644
--- a/include/linux/nvme.h
+++ b/include/linux/nvme.h
@@ -376,7 +376,7 @@ struct nvme_id_ctrl {
 	__u8			nvscc;
 	__u8			nwpc;
 	__le16			acwu;
-	__u8			rsvd534[2];
+	__le16			ocfs;
 	__le32			sgls;
 	__le32			mnan;
 	__u8			rsvd544[224];
@@ -404,6 +404,7 @@ enum {
 	NVME_CTRL_ONCS_WRITE_ZEROES		= 1 << 3,
 	NVME_CTRL_ONCS_RESERVATIONS		= 1 << 5,
 	NVME_CTRL_ONCS_TIMESTAMP		= 1 << 6,
+	NVME_CTRL_ONCS_COPY			= 1 << 8,
 	NVME_CTRL_VWC_PRESENT			= 1 << 0,
 	NVME_CTRL_OACS_SEC_SUPP                 = 1 << 0,
 	NVME_CTRL_OACS_NS_MNGT_SUPP		= 1 << 3,
@@ -458,7 +459,10 @@ struct nvme_id_ns {
 	__le16			npdg;
 	__le16			npda;
 	__le16			nows;
-	__u8			rsvd74[18];
+	__le16			mssrl;
+	__le32			mcl;
+	__u8			msrc;
+	__u8			rsvd81[11];
 	__le32			anagrpid;
 	__u8			rsvd96[3];
 	__u8			nsattr;
@@ -967,6 +971,7 @@ enum nvme_opcode {
 	nvme_cmd_resv_acquire	= 0x11,
 	nvme_cmd_io_mgmt_recv	= 0x12,
 	nvme_cmd_resv_release	= 0x15,
+	nvme_cmd_copy		= 0x19,
 	nvme_cmd_zone_mgmt_send	= 0x79,
 	nvme_cmd_zone_mgmt_recv	= 0x7a,
 	nvme_cmd_zone_append	= 0x7d,
@@ -991,7 +996,8 @@ enum nvme_opcode {
 		nvme_opcode_name(nvme_cmd_resv_release),	\
 		nvme_opcode_name(nvme_cmd_zone_mgmt_send),	\
 		nvme_opcode_name(nvme_cmd_zone_mgmt_recv),	\
-		nvme_opcode_name(nvme_cmd_zone_append))
+		nvme_opcode_name(nvme_cmd_zone_append),		\
+		nvme_opcode_name(nvme_cmd_copy))
 
 
 
@@ -1169,6 +1175,39 @@ struct nvme_dsm_range {
 	__le64			slba;
 };
 
+struct nvme_copy_command {
+	__u8			opcode;
+	__u8			flags;
+	__u16			command_id;
+	__le32			nsid;
+	__u64			rsvd2;
+	__le64			metadata;
+	union nvme_data_ptr	dptr;
+	__le64			sdlba;
+	__u8			nr_range;
+	__u8			desfmt_prinfor;
+	__le16			control;
+	__le16			rsvd13;
+	__le16			dspec;
+	__le32			ilbrt;
+	__le16			lbat;
+	__le16			lbatm;
+};
+
+struct nvme_copy_range {
+	__le32			nsid;	/* DESFMT=2 only */
+	__le32			rsvd1;
+	__le64			slba;
+	__le16			nlb;
+	__le16			rsvd18;
+	__le32			rsvd20;
+	__le32			eilbrt;
+	__le16			elbat;
+	__le16			elbatm;
+};
+
+static_assert(sizeof(struct nvme_copy_range) == 32);
+
 struct nvme_write_zeroes_cmd {
 	__u8			opcode;
 	__u8			flags;
@@ -2001,6 +2040,7 @@ struct nvme_command {
 		struct nvme_download_firmware dlfw;
 		struct nvme_format_cmd format;
 		struct nvme_dsm_cmd dsm;
+		struct nvme_copy_command copy;
 		struct nvme_write_zeroes_cmd write_zeroes;
 		struct nvme_zone_mgmt_send_cmd zms;
 		struct nvme_zone_mgmt_recv_cmd zmr;

^ permalink raw reply related

* [PATCH 09/12] nvmet: Support the Copy command
From: Bart Van Assche @ 2026-04-24 22:41 UTC (permalink / raw)
  To: Jens Axboe
  Cc: linux-block, linux-scsi, linux-nvme, Christoph Hellwig,
	Nitesh Shetty, Bart Van Assche, Anuj Gupta
In-Reply-To: <20260424224201.1949243-1-bvanassche@acm.org>

From: Nitesh Shetty <nj.shetty@samsung.com>

Support the Copy command for namespaces backed by a block device or by a
file. For namespaces backed by a block device, we call
blkdev_copy_offload() and fall back to blkdev_copy_onload() if necessary.
For namespaces backed by a file we call vfs_copy_file_range().

nvmet always reports that the Copy command is supported.

Tracing support is added for the Copy command.

Signed-off-by: Nitesh Shetty <nj.shetty@samsung.com>
Signed-off-by: Anuj Gupta <anuj20.g@samsung.com>
[ bvanassche: Increased namespace limits. ]
Signed-off-by: Bart Van Assche <bvanassche@acm.org>
---
 drivers/nvme/host/trace.c         |  2 +-
 drivers/nvme/target/admin-cmd.c   | 26 +++++++++-
 drivers/nvme/target/io-cmd-bdev.c | 80 +++++++++++++++++++++++++++++++
 drivers/nvme/target/io-cmd-file.c | 59 +++++++++++++++++++++--
 drivers/nvme/target/trace.c       | 19 ++++++++
 include/linux/nvme.h              |  1 +
 6 files changed, 179 insertions(+), 8 deletions(-)

diff --git a/drivers/nvme/host/trace.c b/drivers/nvme/host/trace.c
index 7096ade7740c..fd49363f8516 100644
--- a/drivers/nvme/host/trace.c
+++ b/drivers/nvme/host/trace.c
@@ -143,7 +143,7 @@ static const char *nvme_trace_read_write(struct trace_seq *p, u8 *cdw10)
 	u16 length = get_unaligned_le16(cdw10 + 8);
 	u16 control = get_unaligned_le16(cdw10 + 10);
 	u32 dsmgmt = get_unaligned_le32(cdw10 + 12);
-	u32 reftag = get_unaligned_le32(cdw10 +  16);
+	u32 reftag = get_unaligned_le32(cdw10 + 16);
 
 	trace_seq_printf(p,
 			 "slba=%llu, len=%u, ctrl=0x%x, dsmgmt=%u, reftag=%u",
diff --git a/drivers/nvme/target/admin-cmd.c b/drivers/nvme/target/admin-cmd.c
index e4fd1caadfb0..1e404df6ad84 100644
--- a/drivers/nvme/target/admin-cmd.c
+++ b/drivers/nvme/target/admin-cmd.c
@@ -733,8 +733,7 @@ static void nvmet_execute_identify_ctrl(struct nvmet_req *req)
 	id->mnan = cpu_to_le32(NVMET_MAX_NAMESPACES);
 	id->oncs = cpu_to_le16(NVME_CTRL_ONCS_DSM |
 			NVME_CTRL_ONCS_WRITE_ZEROES |
-			NVME_CTRL_ONCS_RESERVATIONS);
-
+			NVME_CTRL_ONCS_RESERVATIONS | NVME_CTRL_ONCS_COPY);
 	/* XXX: don't report vwc if the underlying device is write through */
 	id->vwc = NVME_CTRL_VWC_PRESENT;
 
@@ -797,6 +796,27 @@ static void nvmet_execute_identify_ctrl(struct nvmet_req *req)
 	nvmet_req_complete(req, status);
 }
 
+static void nvmet_set_copy_limits(struct nvme_id_ns *id)
+{
+	/*
+	 * MSRC = Maximum Source Range Count - the maximum number of
+	 * source ranges that may be used to specify source data in a
+	 * Copy command. 0's based.
+	 */
+	id->msrc = 256 - 1;
+	/*
+	 * MSSRL = Maximum Single Source Range Length - the maximum number
+	 * of logical blocks that may be specified in the Number of Logical
+	 * Blocks field in each valid Source Range Entries Descriptor.
+	 */
+	id->mssrl = cpu_to_le16(U16_MAX);
+	/*
+	 * MCL = Maximum Copy Length - the maximum number of logical
+	 * blocks that may be specified in a Copy command.
+	 */
+	id->mcl = cpu_to_le32(U32_MAX);
+}
+
 static void nvmet_execute_identify_ns(struct nvmet_req *req)
 {
 	struct nvme_id_ns *id;
@@ -845,6 +865,8 @@ static void nvmet_execute_identify_ns(struct nvmet_req *req)
 	if (req->ns->bdev)
 		nvmet_bdev_set_limits(req->ns->bdev, id);
 
+	nvmet_set_copy_limits(id);
+
 	/*
 	 * We just provide a single LBA format that matches what the
 	 * underlying device reports.
diff --git a/drivers/nvme/target/io-cmd-bdev.c b/drivers/nvme/target/io-cmd-bdev.c
index f2d9e8901df4..4196f10b02ab 100644
--- a/drivers/nvme/target/io-cmd-bdev.c
+++ b/drivers/nvme/target/io-cmd-bdev.c
@@ -451,6 +451,83 @@ static void nvmet_bdev_execute_write_zeroes(struct nvmet_req *req)
 	}
 }
 
+static void nvmet_bdev_copy_endio(const struct blk_copy_params *params)
+{
+	struct nvmet_req *rq = params->private;
+	blk_status_t status = params->status;
+
+	/*
+	 * From the NVM Command Set Specification section about the Copy
+	 * Command: "If the command completes with failure (i.e., completes with
+	 * a status code other than Successful Completion), then: [ ... ] Dword
+	 * 0 of the completion queue entry contains the number of the lowest
+	 * numbered Source Range entry that was not successfully copied". Since
+	 * that information is not available, clear Dword 0.
+	 */
+	rq->cqe->result.u32 = cpu_to_le32(0);
+
+	nvmet_req_complete(rq, blk_to_nvme_status(rq, status));
+}
+
+static void nvmet_bdev_execute_copy(struct nvmet_req *rq)
+{
+	u32 i, nr_range = (u32)rq->cmd->copy.nr_range + 1;
+	struct blk_copy_seg *in_segs __free(kfree) = NULL;
+	struct nvme_command *cmd = rq->cmd;
+	struct nvme_copy_range range;
+	u64 src_len, copy_len = 0;
+	loff_t dst_pos, src_pos;
+	u16 status;
+	int ret;
+
+	status = NVME_SC_INTERNAL;
+	in_segs = kmalloc_array(nr_range, sizeof(*in_segs), GFP_KERNEL);
+	if (!in_segs)
+		goto err_rq_complete;
+
+	for (i = 0; i < nr_range; i++) {
+		status = nvmet_copy_from_sgl(rq, i * sizeof(range), &range,
+					     sizeof(range));
+		if (WARN_ON_ONCE(status))
+			goto err_rq_complete;
+		/*
+		 * TO DO: implement support for different source and destination namespace
+		 * IDs.
+		 */
+		status = errno_to_nvme_status(rq, -EIO);
+		if (le32_to_cpu(range.nsid) != rq->ns->nsid)
+			goto err_rq_complete;
+		src_pos = le64_to_cpu(range.slba) << rq->ns->blksize_shift;
+		src_len = (le16_to_cpu(range.nlb) + 1) << rq->ns->blksize_shift;
+		in_segs[i] =
+			(struct blk_copy_seg){ .pos = src_pos, .len = src_len };
+		copy_len += src_len;
+	}
+
+	dst_pos = le64_to_cpu(cmd->copy.sdlba) << rq->ns->blksize_shift;
+	struct blk_copy_seg out_seg = { .pos = dst_pos, .len = copy_len };
+	struct blk_copy_params params = {
+		.in_bdev = rq->ns->bdev,
+		.in_segs = in_segs,
+		.in_nseg = nr_range,
+		.out_bdev = rq->ns->bdev,
+		.out_segs = &out_seg,
+		.out_nseg = 1,
+		.end_io = nvmet_bdev_copy_endio,
+		.private = rq,
+	};
+	ret = blkdev_copy_offload(&params);
+	if (ret == -EIOCBQUEUED)
+		return;
+	if (ret)
+		ret = blkdev_copy_onload(&params);
+
+	rq->cqe->result.u32 = cpu_to_le32(ret == 0);
+	status = errno_to_nvme_status(rq, ret);
+err_rq_complete:
+	nvmet_req_complete(rq, status);
+}
+
 u16 nvmet_bdev_parse_io_cmd(struct nvmet_req *req)
 {
 	switch (req->cmd->common.opcode) {
@@ -469,6 +546,9 @@ u16 nvmet_bdev_parse_io_cmd(struct nvmet_req *req)
 	case nvme_cmd_write_zeroes:
 		req->execute = nvmet_bdev_execute_write_zeroes;
 		return 0;
+	case nvme_cmd_copy:
+		req->execute = nvmet_bdev_execute_copy;
+		return 0;
 	default:
 		return nvmet_report_invalid_opcode(req);
 	}
diff --git a/drivers/nvme/target/io-cmd-file.c b/drivers/nvme/target/io-cmd-file.c
index 0b22d183f927..5e8738b45d52 100644
--- a/drivers/nvme/target/io-cmd-file.c
+++ b/drivers/nvme/target/io-cmd-file.c
@@ -131,11 +131,7 @@ static bool nvmet_file_execute_io(struct nvmet_req *req, int ki_flags)
 	if (req->f.mpool_alloc && nr_bvec > NVMET_MAX_MPOOL_BVEC)
 		is_sync = true;
 
-	pos = le64_to_cpu(req->cmd->rw.slba) << req->ns->blksize_shift;
-	if (unlikely(pos + req->transfer_len > req->ns->size)) {
-		nvmet_req_complete(req, errno_to_nvme_status(req, -ENOSPC));
-		return true;
-	}
+	pos = le64_to_cpu(req->cmd->copy.sdlba) << req->ns->blksize_shift;
 
 	memset(&req->f.iocb, 0, sizeof(struct kiocb));
 	for_each_sg(req->sg, sg, req->sg_cnt, i) {
@@ -321,6 +317,50 @@ static void nvmet_file_dsm_work(struct work_struct *w)
 	}
 }
 
+static void nvmet_file_copy_work(struct work_struct *w)
+{
+	struct nvmet_req *req = container_of(w, struct nvmet_req, f.work);
+	u32 id, nr_range = req->cmd->copy.nr_range + 1;
+	loff_t dst_pos;
+	ssize_t ret;
+	u16 status;
+
+	status = errno_to_nvme_status(req, -ENOSPC);
+	dst_pos = le64_to_cpu(req->cmd->copy.sdlba) << req->ns->blksize_shift;
+
+	for (id = 0; id < nr_range; id++) {
+		struct nvme_copy_range range;
+		loff_t src_pos, src_len;
+
+		status = nvmet_copy_from_sgl(req, id * sizeof(range), &range,
+					     sizeof(range));
+		if (status)
+			goto out;
+		/*
+		 * TO DO: implement support for different source and destination namespace
+		 * IDs.
+		 */
+		status = errno_to_nvme_status(req, -EIO);
+		if (le32_to_cpu(range.nsid) != req->ns->nsid)
+			goto out;
+		src_pos = le64_to_cpu(range.slba) << (req->ns->blksize_shift);
+		src_len = (le16_to_cpu(range.nlb) + 1) << req->ns->blksize_shift;
+		ret = vfs_copy_file_range(req->ns->file, src_pos, req->ns->file,
+					  dst_pos, src_len, COPY_FILE_SPLICE);
+		if (ret != src_len) {
+			req->cqe->result.u32 = cpu_to_le32(id);
+			status = errno_to_nvme_status(req, ret < 0 ? ret : -EIO);
+			goto out;
+		}
+		dst_pos += ret;
+	}
+
+	status = 0;
+
+out:
+	nvmet_req_complete(req, status);
+}
+
 static void nvmet_file_execute_dsm(struct nvmet_req *req)
 {
 	if (!nvmet_check_data_len_lte(req, nvmet_dsm_len(req)))
@@ -329,6 +369,12 @@ static void nvmet_file_execute_dsm(struct nvmet_req *req)
 	queue_work(nvmet_wq, &req->f.work);
 }
 
+static void nvmet_file_execute_copy(struct nvmet_req *req)
+{
+	INIT_WORK(&req->f.work, nvmet_file_copy_work);
+	queue_work(nvmet_wq, &req->f.work);
+}
+
 static void nvmet_file_write_zeroes_work(struct work_struct *w)
 {
 	struct nvmet_req *req = container_of(w, struct nvmet_req, f.work);
@@ -375,6 +421,9 @@ u16 nvmet_file_parse_io_cmd(struct nvmet_req *req)
 	case nvme_cmd_write_zeroes:
 		req->execute = nvmet_file_execute_write_zeroes;
 		return 0;
+	case nvme_cmd_copy:
+		req->execute = nvmet_file_execute_copy;
+		return 0;
 	default:
 		return nvmet_report_invalid_opcode(req);
 	}
diff --git a/drivers/nvme/target/trace.c b/drivers/nvme/target/trace.c
index 6dbc7036f2e4..2baef7294491 100644
--- a/drivers/nvme/target/trace.c
+++ b/drivers/nvme/target/trace.c
@@ -92,6 +92,23 @@ static const char *nvmet_trace_dsm(struct trace_seq *p, u8 *cdw10)
 	return ret;
 }
 
+static const char *nvmet_trace_copy(struct trace_seq *p, u8 *cdw10)
+{
+	const char *ret = trace_seq_buffer_ptr(p);
+	u64 sdlba = get_unaligned_le64(cdw10);
+	u8 nr_range = get_unaligned_le16(cdw10 + 8);
+	u16 control = get_unaligned_le16(cdw10 + 10);
+	u32 dsmgmt = get_unaligned_le32(cdw10 + 12);
+	u32 reftag = get_unaligned_le32(cdw10 +  16);
+
+	trace_seq_printf(p,
+		"sdlba=%llu, nr_range=%u, ctrl=1x%x, dsmgmt=%u, reftag=%u",
+		sdlba, nr_range, control, dsmgmt, reftag);
+	trace_seq_putc(p, 0);
+
+	return ret;
+}
+
 static const char *nvmet_trace_common(struct trace_seq *p, u8 *cdw10)
 {
 	const char *ret = trace_seq_buffer_ptr(p);
@@ -303,6 +320,8 @@ const char *nvmet_trace_parse_nvm_cmd(struct trace_seq *p,
 		return nvmet_trace_resv_rel(p, cdw10);
 	case nvme_cmd_resv_report:
 		return nvmet_trace_resv_report(p, cdw10);
+	case nvme_cmd_copy:
+		return nvmet_trace_copy(p, cdw10);
 	default:
 		return nvmet_trace_common(p, cdw10);
 	}
diff --git a/include/linux/nvme.h b/include/linux/nvme.h
index ead8e5128e3b..c6325aeb13a0 100644
--- a/include/linux/nvme.h
+++ b/include/linux/nvme.h
@@ -2220,6 +2220,7 @@ enum {
 	NVME_SC_PMR_SAN_PROHIBITED	= 0x123,
 	NVME_SC_ANA_GROUP_ID_INVALID	= 0x124,
 	NVME_SC_ANA_ATTACH_FAILED	= 0x125,
+	NVME_SC_COMMAND_SIZE_LIMIT_EXC	= 0x183,
 
 	/*
 	 * I/O Command Set Specific - NVM commands:

^ permalink raw reply related

* [PATCH 12/12] null_blk: Add support for REQ_OP_COPY_*
From: Bart Van Assche @ 2026-04-24 22:42 UTC (permalink / raw)
  To: Jens Axboe
  Cc: linux-block, linux-scsi, linux-nvme, Christoph Hellwig,
	Nitesh Shetty, Bart Van Assche, Damien Le Moal, Anuj Gupta,
	Vincent Fu
In-Reply-To: <20260424224201.1949243-1-bvanassche@acm.org>

From: Nitesh Shetty <nj.shetty@samsung.com>

Implementation is based on existing read and write infrastructure.
copy_max_bytes: A new configfs and module parameter is introduced, which
can be used to set hardware/driver supported maximum copy limit.
Only request based queue mode will support for copy offload.
Added tracefs support to copy IO tracing.

Suggested-by: Damien Le Moal <damien.lemoal@opensource.wdc.com>
Signed-off-by: Anuj Gupta <anuj20.g@samsung.com>
Signed-off-by: Nitesh Shetty <nj.shetty@samsung.com>
Signed-off-by: Vincent Fu <vincent.fu@samsung.com>
[ bvanassche: Split nullb_do_copy() into two functions. Added a
  cond_resched() call inside nullb_do_copy(). ]
Signed-off-by: Bart Van Assche <bvanassche@acm.org>
---
 Documentation/block/null_blk.rst  |   4 ++
 drivers/block/null_blk/main.c     | 113 ++++++++++++++++++++++++++++++
 drivers/block/null_blk/null_blk.h |   1 +
 3 files changed, 118 insertions(+)

diff --git a/Documentation/block/null_blk.rst b/Documentation/block/null_blk.rst
index 4dd78f24d10a..ea0616dbf7f3 100644
--- a/Documentation/block/null_blk.rst
+++ b/Documentation/block/null_blk.rst
@@ -149,3 +149,7 @@ zone_size=[MB]: Default: 256
 zone_nr_conv=[nr_conv]: Default: 0
   The number of conventional zones to create when block device is zoned.  If
   zone_nr_conv >= nr_zones, it will be reduced to nr_zones - 1.
+
+max_copy_bytes=[size in bytes]: Default: UINT_MAX
+  A module and configfs parameter which can be used to set hardware/driver
+  supported maximum copy offload limit.
diff --git a/drivers/block/null_blk/main.c b/drivers/block/null_blk/main.c
index f8c0fd57e041..87a2f3536b50 100644
--- a/drivers/block/null_blk/main.c
+++ b/drivers/block/null_blk/main.c
@@ -9,6 +9,7 @@
 #include <linux/sched.h>
 #include <linux/fs.h>
 #include <linux/init.h>
+#include <linux/blk-copy.h>
 #include "null_blk.h"
 
 #undef pr_fmt
@@ -169,6 +170,10 @@ static int g_max_sectors;
 module_param_named(max_sectors, g_max_sectors, int, 0444);
 MODULE_PARM_DESC(max_sectors, "Maximum size of a command (in 512B sectors)");
 
+static unsigned long g_max_copy_bytes = UINT_MAX;
+module_param_named(max_copy_bytes, g_max_copy_bytes, ulong, 0444);
+MODULE_PARM_DESC(max_copy_bytes, "Maximum size of a copy command (in bytes)");
+
 static unsigned int nr_devices = 1;
 module_param(nr_devices, uint, 0444);
 MODULE_PARM_DESC(nr_devices, "Number of devices to register");
@@ -450,6 +455,7 @@ NULLB_DEVICE_ATTR(home_node, uint, NULL);
 NULLB_DEVICE_ATTR(queue_mode, uint, NULL);
 NULLB_DEVICE_ATTR(blocksize, uint, NULL);
 NULLB_DEVICE_ATTR(max_sectors, uint, NULL);
+NULLB_DEVICE_ATTR(max_copy_bytes, uint, NULL);
 NULLB_DEVICE_ATTR(irqmode, uint, NULL);
 NULLB_DEVICE_ATTR(hw_queue_depth, uint, NULL);
 NULLB_DEVICE_ATTR(index, uint, NULL);
@@ -601,6 +607,7 @@ static struct configfs_attribute *nullb_device_attrs[] = {
 	&nullb_device_attr_blocksize,
 	&nullb_device_attr_cache_size,
 	&nullb_device_attr_completion_nsec,
+	&nullb_device_attr_max_copy_bytes,
 	&nullb_device_attr_discard,
 	&nullb_device_attr_fua,
 	&nullb_device_attr_home_node,
@@ -805,6 +812,7 @@ static struct nullb_device *null_alloc_dev(void)
 	dev->queue_mode = g_queue_mode;
 	dev->blocksize = g_bs;
 	dev->max_sectors = g_max_sectors;
+	dev->max_copy_bytes = g_max_copy_bytes;
 	dev->irqmode = g_irqmode;
 	dev->hw_queue_depth = g_hw_queue_depth;
 	dev->blocking = g_blocking;
@@ -1275,6 +1283,96 @@ static blk_status_t null_transfer(struct nullb *nullb, struct page *page,
 	return err;
 }
 
+static ssize_t nullb_copy_sector(struct nullb *nullb, sector_t sector_in,
+				 sector_t sector_out, ssize_t rem, bool is_fua)
+{
+	struct nullb_page *t_page_in, *t_page_out;
+	loff_t offset_in, offset_out;
+	void *in, *out;
+	ssize_t chunk;
+
+	chunk = min_t(size_t, nullb->dev->blocksize, rem);
+	offset_in = (sector_in & SECTOR_MASK) << SECTOR_SHIFT;
+	offset_out = (sector_out & SECTOR_MASK) << SECTOR_SHIFT;
+
+	guard(spinlock_irq)(&nullb->lock);
+
+	if (null_cache_active(nullb) && !is_fua)
+		null_make_cache_space(nullb, PAGE_SIZE);
+
+	t_page_in = null_insert_page(nullb, sector_in,
+				     !null_cache_active(nullb));
+	if (!t_page_in)
+		return -1;
+	t_page_out = null_insert_page(nullb, sector_out,
+				      !null_cache_active(nullb) || is_fua);
+	if (!t_page_out)
+		return -1;
+
+	in = kmap_local_page(t_page_in->page);
+	out = kmap_local_page(t_page_out->page);
+	memcpy(out + offset_out, in + offset_in, chunk);
+	kunmap_local(out);
+	kunmap_local(in);
+
+	__set_bit(sector_out & SECTOR_MASK, t_page_out->bitmap);
+
+	if (is_fua)
+		null_free_sector(nullb, sector_out, true);
+
+	return chunk;
+}
+
+static blk_status_t nullb_do_copy(struct nullb *nullb, struct request *rq)
+{
+	sector_t sector_in, sector_in_end, sector_out, sector_out_end;
+	struct bio_copy_offload_ctx *copy_ctx = rq->bio->bi_copy_ctx;
+	ssize_t chunk, rem = copy_ctx->len;
+	struct bio *src_bio, *dst_bio;
+
+	src_bio = blk_first_copy_bio(rq, REQ_OP_COPY_SRC);
+	dst_bio = blk_first_copy_bio(rq, REQ_OP_COPY_DST);
+
+	if (WARN_ON_ONCE(!src_bio || !dst_bio))
+		return BLK_STS_IOERR;
+
+	sector_in = src_bio->bi_iter.bi_sector;
+	sector_in_end = sector_in + (src_bio->bi_iter.bi_size >> SECTOR_SHIFT);
+	sector_out = dst_bio->bi_iter.bi_sector;
+	sector_out_end = sector_out + (dst_bio->bi_iter.bi_size >> SECTOR_SHIFT);
+
+	while (rem > 0) {
+		chunk = nullb_copy_sector(nullb, sector_in, sector_out, rem,
+					  rq->cmd_flags & REQ_FUA);
+		if (chunk < 0)
+			return BLK_STS_IOERR;
+		rem -= chunk;
+		if (!rem)
+			break;
+		sector_in += chunk >> SECTOR_SHIFT;
+		if (sector_in >= sector_in_end) {
+			src_bio = blk_next_copy_bio(src_bio);
+			if (WARN_ON_ONCE(!src_bio))
+				return BLK_STS_IOERR;
+			sector_in = src_bio->bi_iter.bi_sector;
+			sector_in_end = sector_in +
+				(src_bio->bi_iter.bi_size >> SECTOR_SHIFT);
+		}
+		sector_out += chunk >> SECTOR_SHIFT;
+		if (sector_out >= sector_out_end) {
+			dst_bio = blk_next_copy_bio(dst_bio);
+			if (WARN_ON_ONCE(!dst_bio))
+				return BLK_STS_IOERR;
+			sector_out = dst_bio->bi_iter.bi_sector;
+			sector_out_end = sector_out +
+				(dst_bio->bi_iter.bi_size >> SECTOR_SHIFT);
+		}
+		cond_resched();
+	}
+
+	return BLK_STS_OK;
+}
+
 /*
  * Transfer data for the given request. The transfer size is capped with the
  * nr_sectors argument.
@@ -1292,6 +1390,9 @@ static blk_status_t null_handle_data_transfer(struct nullb_cmd *cmd,
 	struct req_iterator iter;
 	struct bio_vec bvec;
 
+	if (op_is_copy(req_op(rq)))
+		return nullb_do_copy(nullb, rq);
+
 	spin_lock_irq(&nullb->lock);
 	rq_for_each_segment(bvec, rq, iter) {
 		len = bvec.bv_len;
@@ -1806,6 +1907,13 @@ static void null_config_discard(struct nullb *nullb, struct queue_limits *lim)
 	lim->max_hw_discard_sectors = UINT_MAX >> 9;
 }
 
+static void null_config_copy(struct nullb *nullb, struct queue_limits *lim)
+{
+	lim->max_copy_hw_sectors = nullb->dev->max_copy_bytes >> SECTOR_SHIFT;
+	lim->max_copy_src_segments = nullb->dev->max_copy_bytes ? U16_MAX : 0;
+	lim->max_copy_dst_segments = lim->max_copy_src_segments;
+}
+
 static const struct block_device_operations null_ops = {
 	.owner		= THIS_MODULE,
 	.report_zones	= null_report_zones,
@@ -1922,6 +2030,9 @@ static int null_validate_conf(struct nullb_device *dev)
 		return -EINVAL;
 	}
 
+	if (dev->queue_mode == NULL_Q_BIO)
+		dev->max_copy_bytes = 0;
+
 	return 0;
 }
 
@@ -1989,6 +2100,8 @@ static int null_add_dev(struct nullb_device *dev)
 	if (dev->virt_boundary)
 		lim.virt_boundary_mask = PAGE_SIZE - 1;
 	null_config_discard(nullb, &lim);
+	null_config_copy(nullb, &lim);
+
 	if (dev->zoned) {
 		rv = null_init_zoned_dev(dev, &lim);
 		if (rv)
diff --git a/drivers/block/null_blk/null_blk.h b/drivers/block/null_blk/null_blk.h
index 6c4c4bbe7dad..c15c319ed91b 100644
--- a/drivers/block/null_blk/null_blk.h
+++ b/drivers/block/null_blk/null_blk.h
@@ -93,6 +93,7 @@ struct nullb_device {
 	unsigned int queue_mode; /* block interface */
 	unsigned int blocksize; /* block size */
 	unsigned int max_sectors; /* Max sectors per command */
+	unsigned long max_copy_bytes; /* Max copy offload length in bytes */
 	unsigned int irqmode; /* IRQ completion handler */
 	unsigned int hw_queue_depth; /* queue depth */
 	unsigned int index; /* index of the disk, only valid with a disk */

^ permalink raw reply related

* [syzbot] [block?] possible deadlock in bioset_exit
From: syzbot @ 2026-04-25  2:12 UTC (permalink / raw)
  To: axboe, linux-block, linux-kernel, syzkaller-bugs

Hello,

syzbot found the following issue on:

HEAD commit:    dd6c438c3e64 Merge tag 'vfs-7.1-rc1.fixes' of git://git.ke..
git tree:       upstream
console output: https://syzkaller.appspot.com/x/log.txt?x=14efc2ce580000
kernel config:  https://syzkaller.appspot.com/x/.config?x=6d74203cd0463a9f
dashboard link: https://syzkaller.appspot.com/bug?extid=02b4e6eb8b1460d955a5
compiler:       gcc (Debian 14.2.0-19) 14.2.0, GNU ld (GNU Binutils for Debian) 2.44

Unfortunately, I don't have any reproducer for this issue yet.

Downloadable assets:
disk image: https://storage.googleapis.com/syzbot-assets/8bebdd77187b/disk-dd6c438c.raw.xz
vmlinux: https://storage.googleapis.com/syzbot-assets/2527d5942bbb/vmlinux-dd6c438c.xz
kernel image: https://storage.googleapis.com/syzbot-assets/d446b541e2e0/bzImage-dd6c438c.xz

IMPORTANT: if you fix the issue, please add the following tag to the commit:
Reported-by: syzbot+02b4e6eb8b1460d955a5@syzkaller.appspotmail.com

ubi31: attaching mtd0
======================================================
WARNING: possible circular locking dependency detected
syzkaller #0 Tainted: G             L     
------------------------------------------------------
syz.7.5359/28440 is trying to acquire lock:
ffffffff8e9b1780 (fs_reclaim){+.+.}-{0:0}, at: might_alloc include/linux/sched/mm.h:317 [inline]
ffffffff8e9b1780 (fs_reclaim){+.+.}-{0:0}, at: slab_pre_alloc_hook mm/slub.c:4520 [inline]
ffffffff8e9b1780 (fs_reclaim){+.+.}-{0:0}, at: slab_alloc_node mm/slub.c:4875 [inline]
ffffffff8e9b1780 (fs_reclaim){+.+.}-{0:0}, at: kmem_cache_alloc_noprof+0x4c/0x6e0 mm/slub.c:4905

but task is already holding lock:
ffffffff8e97f100 (slab_mutex){+.+.}-{4:4}, at: __kmem_cache_create_args+0x44/0x420 mm/slab_common.c:350

which lock already depends on the new lock.


the existing dependency chain (in reverse order) is:

-> #7 (slab_mutex){+.+.}-{4:4}:
       __mutex_lock_common kernel/locking/mutex.c:632 [inline]
       __mutex_lock+0x1a4/0x1b10 kernel/locking/mutex.c:806
       kmem_cache_destroy mm/slab_common.c:557 [inline]
       kmem_cache_destroy+0x59/0x180 mm/slab_common.c:527
       bio_put_slab block/bio.c:163 [inline]
       bioset_exit+0x3b1/0x520 block/bio.c:1907
       mddev_destroy drivers/md/md.c:819 [inline]
       mddev_free drivers/md/md.c:911 [inline]
       md_free_disk+0x18e/0x210 drivers/md/md.c:8578
       disk_release+0x2b2/0x430 block/genhd.c:1311
       device_release+0xd2/0x270 drivers/base/core.c:2566
       kobject_cleanup lib/kobject.c:689 [inline]
       kobject_release lib/kobject.c:720 [inline]
       kref_put include/linux/kref.h:65 [inline]
       kobject_put+0x1f7/0x640 lib/kobject.c:737
       process_one_work+0xa0e/0x1980 kernel/workqueue.c:3302
       process_scheduled_works kernel/workqueue.c:3385 [inline]
       worker_thread+0x5ef/0xe50 kernel/workqueue.c:3466
       kthread+0x370/0x450 kernel/kthread.c:436
       ret_from_fork+0x72b/0xd50 arch/x86/kernel/process.c:158
       ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245

-> #6 (cpu_hotplug_lock){++++}-{0:0}:
       percpu_down_read_internal include/linux/percpu-rwsem.h:53 [inline]
       percpu_down_read include/linux/percpu-rwsem.h:77 [inline]
       cpus_read_lock+0x42/0x170 kernel/cpu.c:490
       static_key_slow_inc+0x12/0x30 kernel/jump_label.c:190
       nbd_reconnect_socket drivers/block/nbd.c:1342 [inline]
       nbd_genl_reconfigure+0xd82/0x1a00 drivers/block/nbd.c:2431
       genl_family_rcv_msg_doit+0x214/0x300 net/netlink/genetlink.c:1114
       genl_family_rcv_msg net/netlink/genetlink.c:1194 [inline]
       genl_rcv_msg+0x560/0x800 net/netlink/genetlink.c:1209
       netlink_rcv_skb+0x159/0x420 net/netlink/af_netlink.c:2550
       genl_rcv+0x28/0x40 net/netlink/genetlink.c:1218
       netlink_unicast_kernel net/netlink/af_netlink.c:1318 [inline]
       netlink_unicast+0x585/0x850 net/netlink/af_netlink.c:1344
       netlink_sendmsg+0x8b0/0xda0 net/netlink/af_netlink.c:1894
       sock_sendmsg_nosec net/socket.c:787 [inline]
       __sock_sendmsg net/socket.c:802 [inline]
       ____sys_sendmsg+0x9e1/0xb70 net/socket.c:2698
       ___sys_sendmsg+0x190/0x1e0 net/socket.c:2752
       __sys_sendmsg+0x170/0x220 net/socket.c:2784
       do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
       do_syscall_64+0x10b/0xf80 arch/x86/entry/syscall_64.c:94
       entry_SYSCALL_64_after_hwframe+0x77/0x7f

-> #5 (&nsock->tx_lock){+.+.}-{4:4}:
       __mutex_lock_common kernel/locking/mutex.c:632 [inline]
       __mutex_lock+0x1a4/0x1b10 kernel/locking/mutex.c:806
       nbd_handle_cmd drivers/block/nbd.c:1143 [inline]
       nbd_queue_rq+0x428/0x1080 drivers/block/nbd.c:1207
       blk_mq_dispatch_rq_list+0x422/0x1e70 block/blk-mq.c:2148
       __blk_mq_do_dispatch_sched block/blk-mq-sched.c:168 [inline]
       blk_mq_do_dispatch_sched block/blk-mq-sched.c:182 [inline]
       __blk_mq_sched_dispatch_requests+0xcea/0x1620 block/blk-mq-sched.c:307
       blk_mq_sched_dispatch_requests+0xd7/0x1c0 block/blk-mq-sched.c:329
       blk_mq_run_hw_queue+0x23c/0x670 block/blk-mq.c:2386
       blk_mq_dispatch_list+0x51d/0x1360 block/blk-mq.c:2949
       blk_mq_flush_plug_list block/blk-mq.c:2997 [inline]
       blk_mq_flush_plug_list+0x130/0x600 block/blk-mq.c:2969
       __blk_flush_plug+0x2c4/0x4b0 block/blk-core.c:1230
       blk_finish_plug block/blk-core.c:1257 [inline]
       __submit_bio+0x584/0x6c0 block/blk-core.c:649
       __submit_bio_noacct_mq block/blk-core.c:722 [inline]
       submit_bio_noacct_nocheck+0x543/0xbf0 block/blk-core.c:753
       submit_bio_noacct+0xd18/0x2000 block/blk-core.c:884
       blk_crypto_submit_bio include/linux/blk-crypto.h:203 [inline]
       submit_bh_wbc+0x681/0x890 fs/buffer.c:2737
       submit_bh fs/buffer.c:2742 [inline]
       block_read_full_folio+0x4c8/0x8e0 fs/buffer.c:2358
       filemap_read_folio+0xfc/0x3b0 mm/filemap.c:2502
       do_read_cache_folio+0x2d7/0x6b0 mm/filemap.c:4107
       read_mapping_folio include/linux/pagemap.h:1017 [inline]
       read_part_sector+0xd1/0x370 block/partitions/core.c:724
       adfspart_check_ICS+0x91/0x7d0 block/partitions/acorn.c:356
       check_partition block/partitions/core.c:143 [inline]
       blk_add_partitions block/partitions/core.c:591 [inline]
       bdev_disk_changed+0x7a3/0x1250 block/partitions/core.c:695
       blkdev_get_whole+0x187/0x290 block/bdev.c:756
       bdev_open+0x2c7/0xe40 block/bdev.c:965
       blkdev_open+0x34e/0x4f0 block/fops.c:697
       do_dentry_open+0x6d8/0x1660 fs/open.c:947
       vfs_open+0x82/0x3f0 fs/open.c:1079
       do_open fs/namei.c:4699 [inline]
       path_openat+0x208c/0x31a0 fs/namei.c:4858
       do_file_open+0x20e/0x430 fs/namei.c:4887
       do_sys_openat2+0x10d/0x1e0 fs/open.c:1364
       do_sys_open fs/open.c:1370 [inline]
       __do_sys_openat fs/open.c:1386 [inline]
       __se_sys_openat fs/open.c:1381 [inline]
       __x64_sys_openat+0x12d/0x210 fs/open.c:1381
       do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
       do_syscall_64+0x10b/0xf80 arch/x86/entry/syscall_64.c:94
       entry_SYSCALL_64_after_hwframe+0x77/0x7f

-> #4 (&cmd->lock){+.+.}-{4:4}:
       __mutex_lock_common kernel/locking/mutex.c:632 [inline]
       __mutex_lock+0x1a4/0x1b10 kernel/locking/mutex.c:806
       nbd_queue_rq+0xba/0x1080 drivers/block/nbd.c:1199
       blk_mq_dispatch_rq_list+0x422/0x1e70 block/blk-mq.c:2148
       __blk_mq_do_dispatch_sched block/blk-mq-sched.c:168 [inline]
       blk_mq_do_dispatch_sched block/blk-mq-sched.c:182 [inline]
       __blk_mq_sched_dispatch_requests+0xcea/0x1620 block/blk-mq-sched.c:307
       blk_mq_sched_dispatch_requests+0xd7/0x1c0 block/blk-mq-sched.c:329
       blk_mq_run_hw_queue+0x23c/0x670 block/blk-mq.c:2386
       blk_mq_dispatch_list+0x51d/0x1360 block/blk-mq.c:2949
       blk_mq_flush_plug_list block/blk-mq.c:2997 [inline]
       blk_mq_flush_plug_list+0x130/0x600 block/blk-mq.c:2969
       __blk_flush_plug+0x2c4/0x4b0 block/blk-core.c:1230
       blk_finish_plug block/blk-core.c:1257 [inline]
       __submit_bio+0x584/0x6c0 block/blk-core.c:649
       __submit_bio_noacct_mq block/blk-core.c:722 [inline]
       submit_bio_noacct_nocheck+0x543/0xbf0 block/blk-core.c:753
       submit_bio_noacct+0xd18/0x2000 block/blk-core.c:884
       blk_crypto_submit_bio include/linux/blk-crypto.h:203 [inline]
       submit_bh_wbc+0x681/0x890 fs/buffer.c:2737
       submit_bh fs/buffer.c:2742 [inline]
       block_read_full_folio+0x4c8/0x8e0 fs/buffer.c:2358
       filemap_read_folio+0xfc/0x3b0 mm/filemap.c:2502
       do_read_cache_folio+0x2d7/0x6b0 mm/filemap.c:4107
       read_mapping_folio include/linux/pagemap.h:1017 [inline]
       read_part_sector+0xd1/0x370 block/partitions/core.c:724
       adfspart_check_ICS+0x91/0x7d0 block/partitions/acorn.c:356
       check_partition block/partitions/core.c:143 [inline]
       blk_add_partitions block/partitions/core.c:591 [inline]
       bdev_disk_changed+0x7a3/0x1250 block/partitions/core.c:695
       blkdev_get_whole+0x187/0x290 block/bdev.c:756
       bdev_open+0x2c7/0xe40 block/bdev.c:965
       blkdev_open+0x34e/0x4f0 block/fops.c:697
       do_dentry_open+0x6d8/0x1660 fs/open.c:947
       vfs_open+0x82/0x3f0 fs/open.c:1079
       do_open fs/namei.c:4699 [inline]
       path_openat+0x208c/0x31a0 fs/namei.c:4858
       do_file_open+0x20e/0x430 fs/namei.c:4887
       do_sys_openat2+0x10d/0x1e0 fs/open.c:1364
       do_sys_open fs/open.c:1370 [inline]
       __do_sys_openat fs/open.c:1386 [inline]
       __se_sys_openat fs/open.c:1381 [inline]
       __x64_sys_openat+0x12d/0x210 fs/open.c:1381
       do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
       do_syscall_64+0x10b/0xf80 arch/x86/entry/syscall_64.c:94
       entry_SYSCALL_64_after_hwframe+0x77/0x7f

-> #3 (set->srcu){.+.+}-{0:0}:
       srcu_lock_sync include/linux/srcu.h:199 [inline]
       __synchronize_srcu+0xa2/0x300 kernel/rcu/srcutree.c:1481
       blk_mq_wait_quiesce_done block/blk-mq.c:284 [inline]
       blk_mq_wait_quiesce_done block/blk-mq.c:281 [inline]
       blk_mq_quiesce_queue block/blk-mq.c:304 [inline]
       blk_mq_quiesce_queue+0x149/0x1c0 block/blk-mq.c:299
       elevator_switch+0x17b/0x7e0 block/elevator.c:576
       elevator_change+0x352/0x530 block/elevator.c:681
       elevator_set_default+0x29e/0x360 block/elevator.c:754
       blk_register_queue+0x48e/0x630 block/blk-sysfs.c:987
       __add_disk+0x73f/0xe40 block/genhd.c:528
       add_disk_fwnode+0x118/0x5c0 block/genhd.c:597
       add_disk include/linux/blkdev.h:794 [inline]
       nbd_dev_add+0x77a/0xb10 drivers/block/nbd.c:1984
       nbd_init+0x291/0x2b0 drivers/block/nbd.c:2692
       do_one_initcall+0x121/0x750 init/main.c:1392
       do_initcall_level init/main.c:1454 [inline]
       do_initcalls init/main.c:1470 [inline]
       do_basic_setup init/main.c:1490 [inline]
       kernel_init_freeable+0x6ea/0x7b0 init/main.c:1703
       kernel_init+0x1f/0x1e0 init/main.c:1593
       ret_from_fork+0x72b/0xd50 arch/x86/kernel/process.c:158
       ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245

-> #2 (&q->elevator_lock){+.+.}-{4:4}:
       __mutex_lock_common kernel/locking/mutex.c:632 [inline]
       __mutex_lock+0x1a4/0x1b10 kernel/locking/mutex.c:806
       elevator_change+0x1bc/0x530 block/elevator.c:679
       elevator_set_none+0x92/0xf0 block/elevator.c:769
       blk_mq_elv_switch_none block/blk-mq.c:5131 [inline]
       __blk_mq_update_nr_hw_queues block/blk-mq.c:5176 [inline]
       blk_mq_update_nr_hw_queues+0x4c1/0x15f0 block/blk-mq.c:5241
       nbd_start_device+0x1a6/0xbd0 drivers/block/nbd.c:1489
       nbd_genl_connect+0xff2/0x1a40 drivers/block/nbd.c:2239
       genl_family_rcv_msg_doit+0x214/0x300 net/netlink/genetlink.c:1114
       genl_family_rcv_msg net/netlink/genetlink.c:1194 [inline]
       genl_rcv_msg+0x560/0x800 net/netlink/genetlink.c:1209
       netlink_rcv_skb+0x159/0x420 net/netlink/af_netlink.c:2550
       genl_rcv+0x28/0x40 net/netlink/genetlink.c:1218
       netlink_unicast_kernel net/netlink/af_netlink.c:1318 [inline]
       netlink_unicast+0x585/0x850 net/netlink/af_netlink.c:1344
       netlink_sendmsg+0x8b0/0xda0 net/netlink/af_netlink.c:1894
       sock_sendmsg_nosec net/socket.c:787 [inline]
       __sock_sendmsg net/socket.c:802 [inline]
       ____sys_sendmsg+0x9e1/0xb70 net/socket.c:2698
       ___sys_sendmsg+0x190/0x1e0 net/socket.c:2752
       __sys_sendmsg+0x170/0x220 net/socket.c:2784
       do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
       do_syscall_64+0x10b/0xf80 arch/x86/entry/syscall_64.c:94
       entry_SYSCALL_64_after_hwframe+0x77/0x7f

-> #1 (&q->q_usage_counter(io)#50){++++}-{0:0}:
       blk_alloc_queue+0x610/0x790 block/blk-core.c:461
       blk_mq_alloc_queue+0x174/0x290 block/blk-mq.c:4450
       __blk_mq_alloc_disk+0x29/0x120 block/blk-mq.c:4497
       nbd_dev_add+0x492/0xb10 drivers/block/nbd.c:1954
       nbd_init+0x291/0x2b0 drivers/block/nbd.c:2692
       do_one_initcall+0x121/0x750 init/main.c:1392
       do_initcall_level init/main.c:1454 [inline]
       do_initcalls init/main.c:1470 [inline]
       do_basic_setup init/main.c:1490 [inline]
       kernel_init_freeable+0x6ea/0x7b0 init/main.c:1703
       kernel_init+0x1f/0x1e0 init/main.c:1593
       ret_from_fork+0x72b/0xd50 arch/x86/kernel/process.c:158
       ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245

-> #0 (fs_reclaim){+.+.}-{0:0}:
       check_prev_add kernel/locking/lockdep.c:3165 [inline]
       check_prevs_add kernel/locking/lockdep.c:3284 [inline]
       validate_chain kernel/locking/lockdep.c:3908 [inline]
       __lock_acquire+0x14b8/0x2630 kernel/locking/lockdep.c:5237
       lock_acquire kernel/locking/lockdep.c:5868 [inline]
       lock_acquire+0x1b1/0x370 kernel/locking/lockdep.c:5825
       __fs_reclaim_acquire mm/page_alloc.c:4325 [inline]
       fs_reclaim_acquire+0xc4/0x100 mm/page_alloc.c:4339
       might_alloc include/linux/sched/mm.h:317 [inline]
       slab_pre_alloc_hook mm/slub.c:4520 [inline]
       slab_alloc_node mm/slub.c:4875 [inline]
       kmem_cache_alloc_noprof+0x4c/0x6e0 mm/slub.c:4905
       create_cache mm/slab_common.c:249 [inline]
       __kmem_cache_create_args+0x366/0x420 mm/slab_common.c:380
       __kmem_cache_create include/linux/slab.h:390 [inline]
       alloc_ai drivers/mtd/ubi/attach.c:1464 [inline]
       ubi_attach+0x32a/0x4d30 drivers/mtd/ubi/attach.c:1560
       ubi_attach_mtd_dev+0x139f/0x32a0 drivers/mtd/ubi/build.c:1004
       ctrl_cdev_ioctl+0x36a/0x400 drivers/mtd/ubi/cdev.c:1113
       vfs_ioctl fs/ioctl.c:51 [inline]
       __do_sys_ioctl fs/ioctl.c:597 [inline]
       __se_sys_ioctl fs/ioctl.c:583 [inline]
       __x64_sys_ioctl+0x18e/0x210 fs/ioctl.c:583
       do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
       do_syscall_64+0x10b/0xf80 arch/x86/entry/syscall_64.c:94
       entry_SYSCALL_64_after_hwframe+0x77/0x7f

other info that might help us debug this:

Chain exists of:
  fs_reclaim --> cpu_hotplug_lock --> slab_mutex

 Possible unsafe locking scenario:

       CPU0                    CPU1
       ----                    ----
  lock(slab_mutex);
                               lock(cpu_hotplug_lock);
                               lock(slab_mutex);
  lock(fs_reclaim);

 *** DEADLOCK ***

2 locks held by syz.7.5359/28440:
 #0: ffffffff8f930b20 (ubi_devices_mutex){+.+.}-{4:4}, at: ctrl_cdev_ioctl+0x323/0x400 drivers/mtd/ubi/cdev.c:1112
 #1: ffffffff8e97f100 (slab_mutex){+.+.}-{4:4}, at: __kmem_cache_create_args+0x44/0x420 mm/slab_common.c:350

stack backtrace:
CPU: 1 UID: 0 PID: 28440 Comm: syz.7.5359 Tainted: G             L      syzkaller #0 PREEMPT(full) 
Tainted: [L]=SOFTLOCKUP
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 04/18/2026
Call Trace:
 <TASK>
 __dump_stack lib/dump_stack.c:94 [inline]
 dump_stack_lvl+0x100/0x190 lib/dump_stack.c:120
 print_circular_bug.cold+0x178/0x1c7 kernel/locking/lockdep.c:2043
 check_noncircular+0x146/0x160 kernel/locking/lockdep.c:2175
 check_prev_add kernel/locking/lockdep.c:3165 [inline]
 check_prevs_add kernel/locking/lockdep.c:3284 [inline]
 validate_chain kernel/locking/lockdep.c:3908 [inline]
 __lock_acquire+0x14b8/0x2630 kernel/locking/lockdep.c:5237
 lock_acquire kernel/locking/lockdep.c:5868 [inline]
 lock_acquire+0x1b1/0x370 kernel/locking/lockdep.c:5825
 __fs_reclaim_acquire mm/page_alloc.c:4325 [inline]
 fs_reclaim_acquire+0xc4/0x100 mm/page_alloc.c:4339
 might_alloc include/linux/sched/mm.h:317 [inline]
 slab_pre_alloc_hook mm/slub.c:4520 [inline]
 slab_alloc_node mm/slub.c:4875 [inline]
 kmem_cache_alloc_noprof+0x4c/0x6e0 mm/slub.c:4905
 create_cache mm/slab_common.c:249 [inline]
 __kmem_cache_create_args+0x366/0x420 mm/slab_common.c:380
 __kmem_cache_create include/linux/slab.h:390 [inline]
 alloc_ai drivers/mtd/ubi/attach.c:1464 [inline]
 ubi_attach+0x32a/0x4d30 drivers/mtd/ubi/attach.c:1560
 ubi_attach_mtd_dev+0x139f/0x32a0 drivers/mtd/ubi/build.c:1004
 ctrl_cdev_ioctl+0x36a/0x400 drivers/mtd/ubi/cdev.c:1113
 vfs_ioctl fs/ioctl.c:51 [inline]
 __do_sys_ioctl fs/ioctl.c:597 [inline]
 __se_sys_ioctl fs/ioctl.c:583 [inline]
 __x64_sys_ioctl+0x18e/0x210 fs/ioctl.c:583
 do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
 do_syscall_64+0x10b/0xf80 arch/x86/entry/syscall_64.c:94
 entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f5ceb79cdd9
Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 44 00 00 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 e8 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007f5cec650028 EFLAGS: 00000246 ORIG_RAX: 0000000000000010
RAX: ffffffffffffffda RBX: 00007f5ceba16270 RCX: 00007f5ceb79cdd9
RDX: 0000200000000440 RSI: 0000000040186f40 RDI: 0000000000000008
RBP: 00007f5ceb832d69 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
R13: 00007f5ceba16308 R14: 00007f5ceba16270 R15: 00007ffca1349ce8
 </TASK>
ubi31: scanning is finished
ubi31: empty MTD device detected
ubi31 error: ubi_attach_mtd_dev: cannot spawn "ubi_bgt31d", error -4


---
This report is generated by a bot. It may contain errors.
See https://goo.gl/tpsmEJ for more information about syzbot.
syzbot engineers can be reached at syzkaller@googlegroups.com.

syzbot will keep track of this issue. See:
https://goo.gl/tpsmEJ#status for how to communicate with syzbot.

If the report is already addressed, let syzbot know by replying with:
#syz fix: exact-commit-title

If you want to overwrite report's subsystems, reply with:
#syz set subsystems: new-subsystem
(See the list of subsystem names on the web dashboard)

If the report is a duplicate of another one, reply with:
#syz dup: exact-subject-of-another-report

If you want to undo deduplication, reply with:
#syz undup

^ permalink raw reply

* Re: [PATCHv3] ublk: persistent device links
From: Ming Lei @ 2026-04-25  4:40 UTC (permalink / raw)
  To: Hannes Reinecke; +Cc: Jens Axboe, linux-block, Ming Lei
In-Reply-To: <20260423121739.7694-1-hare@kernel.org>

On Thu, Apr 23, 2026 at 8:17 PM Hannes Reinecke <hare@kernel.org> wrote:
>
> Implement a 'uuid' parameter to export a device UUID in a sysfs
> attribute 'uuid'. The ublk server is required to set the uuid
> parameter when creating the device.
> This allows udev to create persistent device links for ublk devices
> eg with this rule:
>
> KERNEL=="ublk*", ATTRS{uuid}=="?*", SYMLINK+="disk/by-id/ublk-$attr{uuid}"
>
> Signed-off-by: Hannes Reinecke <hare@kernel.org>
> Reviewed-by: Ming Lei <ming.lei@redhat.com>

Reviewed-by: Ming Lei <tom.leiming@gmail.com>

Thanks
Ming Lei

^ permalink raw reply

* [LSF/MM/BPF] Running BOF
From: Matthew Wilcox @ 2026-04-26  0:46 UTC (permalink / raw)
  To: linux-fsdevel, linux-mm, linux-block, linux-ide, linux-scsi,
	linux-nvme, bpf, lsf-pc

I'm going to go for a run each morning.  If you want to come
with me, meet in the Esplanade Zagreb hotel lobby around 6:45am
(this should get us back in plenty of time to get to breakfast).
I've plotted three routes, all based on routes described in
https://greatruns.com/location/zagreb-croatia/

Monday: https://connect.garmin.com/modern/course/453744315
Tuesday: https://connect.garmin.com/modern/course/453746189
Wednesday: https://connect.garmin.com/modern/course/453747068


^ permalink raw reply

* Re: [RFC PATCH v2 0/4] mm/zsmalloc: reduce zs_free() latency on swap release path
From: Wenchao Hao @ 2026-04-26  4:13 UTC (permalink / raw)
  To: Andrew Morton, Chengming Zhou, Jens Axboe, Johannes Weiner,
	Minchan Kim, Nhat Pham, Sergey Senozhatsky, Yosry Ahmed,
	linux-block, linux-kernel, linux-mm
  Cc: Barry Song, Xueyuan Chen, Wenchao Hao
In-Reply-To: <20260421121616.3298845-1-haowenchao@xiaomi.com>

On Tue, Apr 21, 2026 at 8:16 PM Wenchao Hao <haowenchao22@gmail.com> wrote:
>
> Swap freeing can be expensive when unmapping a VMA containing
> many swap entries. This has been reported to significantly
> delay memory reclamation during Android's low-memory killing,
> especially when multiple processes are terminated to free
> memory, with slot_free() accounting for more than 80% of
> the total cost of freeing swap entries.
>
> Two earlier attempts by Lei and Zhiguo added a new thread in the mm core
> to asynchronously collect and free swap entries [1][2], but the
> design itself is fairly complex.
>
Hi Nhat, Kairui, Barry, Xueyuan,

Thanks for the review. I agree with the direction and have some ideas for
an alternative approach.

My approach: first eliminate pool->lock from zs_free() itself, then defer
free to per-cpu buffers with a lockless handoff, and finally reduce
class->lock overhead during drain by exploiting natural class locality.
Achieving both per-cpu and per-class is difficult, so the class->lock
optimization is a compromise — but one that works well in practice.

1. Encode class_idx in obj to eliminate pool->lock

OBJ_INDEX_BITS is over-provisioned on 64-bit. For example on arm64
(chain_size=8): OBJ_INDEX_BITS=24 but only 10 bits are actually needed
for obj_idx, leaving 14 spare bits.
We can split OBJ_INDEX into class_idx + obj_idx:

    obj: [PFN | class_idx (OBJ_CLASS_BITS) | obj_idx (OBJ_IDX_BITS)]

OBJ_CLASS_BITS is computed dynamically as `ilog2(ZS_SIZE_CLASSES - 1) + 1`
(8 bits for 4K pages, 9 for 64K).
Since class_idx is invariant across migration (only PFN changes), zs_free()
can extract class_idx locklessly, then acquire class->lock and re-read obj for a
stable PFN. No pool->lock needed.

2. Per-cpu deferred free with lockless buffer swap

Defer zs_free() to per-cpu dynamically-allocated buffers (~2048 entries).
Enqueue: one array write + WRITE_ONCE under preempt_disable — no lock,
no atomic. When buffers full, schedule a drain worker; overflow falls back
to sync zs_free().

Drain: allocate a fresh buffer, swap it in, reset count. Since
the producer stops writing at count==SIZE, the handoff is
race-free without any lock.

Pseudo-code:

    /* enqueue - hot path */
    def = get_cpu_ptr(pool->deferred);
    if (def->count < SIZE) {
        def->handles[def->count] = handle;
        WRITE_ONCE(def->count, def->count + 1);
        if (def->count == SIZE)
            schedule_work(&pool->drain_work);
    } else {
        zs_free(pool, handle);  /* fallback */
    }
    put_cpu_ptr(pool->deferred);

    /* drain - worker */
    for_each_possible_cpu(cpu) {
        def = per_cpu_ptr(pool->deferred, cpu);
        if (def->count < SIZE)
            continue;
        new_buf = kvmalloc_array(SIZE, sizeof(long));
        old_buf = def->handles;
        old_count = def->count;
        def->handles = new_buf;
        WRITE_ONCE(def->count, 0);
        /* now drain old_buf[0..old_count-1] */
        ...
        kvfree(old_buf);
    }

3. Consecutive-class batching during drain

The drain worker extracts class_idx from each handle locklessly, and holds
class->lock across consecutive same-class handles.
On the exit path, compressed sizes tend to cluster, so consecutive handles
naturally share the same class — giving batch-like lock
amortization without sorting.

Pseudo-code:

    cur_cls = -1;
    for (i = 0; i < count; i++) {
        obj = handle_to_obj(handles[i]);
        cls = obj_to_class_idx(obj);
        if (cls != cur_cls) {
            if (cur_cls >= 0)
                spin_unlock(&pool->size_class[cur_cls]->lock);
            spin_lock(&pool->size_class[cls]->lock);
            cur_cls = cls;
        }
        __zs_free(pool, handles[i]);  /* free under lock */
    }
    if (cur_cls >= 0)
        spin_unlock(&pool->size_class[cur_cls]->lock);

---

Benefits over current mainline:
- Removes pool->lock from zs_free() entirely
- Deferred free path is nearly zero-cost
- class->lock is amortized across batches instead of acquired per-handle
- Producer-consumer handoff is fully lockless

I've prototyped this on 64-bit and it works. Still need to sort out
32-bit compatibility and Kconfig gating. Does this direction look reasonable?

Thanks,
Wenchao

^ permalink raw reply

* Re: [RFC PATCH v2 0/4] mm/zsmalloc: reduce zs_free() latency on swap release path
From: Xueyuan Chen @ 2026-04-26  8:50 UTC (permalink / raw)
  To: haowenchao22
  Cc: akpm, chengming.zhou, axboe, hannes, minchan, nphamcs,
	senozhatsky, yosry, linux-block, linux-kernel, linux-mm, baohua,
	xueyuan.chen21, haowenchao
In-Reply-To: <CAOptpSPs-1UrEa8AHg19e590=SiV6bpnex7gCbif8=aY7BtpuA@mail.gmail.com>


On Sun, Apr 26, 2026 at 12:13:02PM +0800, Wenchao Hao wrote:

[...]

>2. Per-cpu deferred free with lockless buffer swap
>
>Defer zs_free() to per-cpu dynamically-allocated buffers (~2048 entries).
>Enqueue: one array write + WRITE_ONCE under preempt_disable — no lock,
>no atomic. When buffers full, schedule a drain worker; overflow falls back
>to sync zs_free().
>
>Drain: allocate a fresh buffer, swap it in, reset count. Since
>the producer stops writing at count==SIZE, the handoff is
>race-free without any lock.
>
>Pseudo-code:
>
>    /* enqueue - hot path */
>    def = get_cpu_ptr(pool->deferred);
>    if (def->count < SIZE) {
>        def->handles[def->count] = handle;
>        WRITE_ONCE(def->count, def->count + 1);
>        if (def->count == SIZE)
>            schedule_work(&pool->drain_work);
>    } else {
>        zs_free(pool, handle);  /* fallback */
>    }
>    put_cpu_ptr(pool->deferred);
>
>    /* drain - worker */
>    for_each_possible_cpu(cpu) {
>        def = per_cpu_ptr(pool->deferred, cpu);
>        if (def->count < SIZE)
>            continue;
>        new_buf = kvmalloc_array(SIZE, sizeof(long));
>        old_buf = def->handles;
>        old_count = def->count;
>        def->handles = new_buf;
>        WRITE_ONCE(def->count, 0);
>        /* now drain old_buf[0..old_count-1] */
>        ...
>        kvfree(old_buf);
>    }
>

Hi Wenchao,

I suspect there is a memory ordering issue here:

def->handles = new_buf;
WRITE_ONCE(def->count, 0);

Since there are no explicit memory barriers, we cannot guarantee the 
order of these stores. If def->count is cleared to 0 first, an enqueue 
might end up operating on the old_buf.

This race condition is more likely to be triggered when the size is
smaller. Perhaps we should consider using smp_store_release() to enforce
the ordering?

Thanks
Xueyuan

^ permalink raw reply

* [PATCH] n64cart: use strscpy() in n64cart_probe
From: Thorsten Blum @ 2026-04-26 11:59 UTC (permalink / raw)
  To: Jens Axboe; +Cc: Thorsten Blum, linux-block, linux-kernel

strcpy() has been deprecated¹ because it performs no bounds checking on
the destination buffer, which can lead to buffer overflows. While the
current code works correctly, replace strcpy() with the safer strscpy()
to follow secure coding best practices.

¹ https://www.kernel.org/doc/html/latest/process/deprecated.html#strcpy

Signed-off-by: Thorsten Blum <thorsten.blum@linux.dev>
---
 drivers/block/n64cart.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/drivers/block/n64cart.c b/drivers/block/n64cart.c
index b9fdeff31caf..328da73b6f2c 100644
--- a/drivers/block/n64cart.c
+++ b/drivers/block/n64cart.c
@@ -12,6 +12,7 @@
 #include <linux/init.h>
 #include <linux/module.h>
 #include <linux/platform_device.h>
+#include <linux/string.h>
 
 enum {
 	PI_DRAM_REG = 0,
@@ -145,7 +146,7 @@ static int __init n64cart_probe(struct platform_device *pdev)
 	disk->flags = GENHD_FL_NO_PART;
 	disk->fops = &n64cart_fops;
 	disk->private_data = &pdev->dev;
-	strcpy(disk->disk_name, "n64cart");
+	strscpy(disk->disk_name, "n64cart");
 
 	set_capacity(disk, size >> SECTOR_SHIFT);
 	set_disk_ro(disk, 1);

^ permalink raw reply related

* [PATCH] block: fix infinite loop in badblocks_clear()
From: Ramesh Adhikari @ 2026-04-26 14:55 UTC (permalink / raw)
  To: gregkh; +Cc: axboe, linux-block, security, Ramesh Adhikari

An infinite loop can occur in _badblocks_clear() when BB_OFFSET(p[prev + 1])
equals bad.start, resulting in len = 0. This causes the update_sectors loop
to spin forever without making progress:

    s += 0;         // no advancement
    sectors -= 0;   // stays positive
    goto re_clear;  // infinite loop

After approximately 21 seconds, the RCU stall detector triggers and the
system becomes completely unresponsive, requiring a hard reboot.

Add a check to ensure len is non-zero before entering the loop.

Signed-off-by: Ramesh Adhikari <adhikari.resume@gmail.com>
---
 block/badblocks.c | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/block/badblocks.c b/block/badblocks.c
index ece64e76fe8..9ea4a067710 100644
--- a/block/badblocks.c
+++ b/block/badblocks.c
@@ -1151,6 +1151,10 @@ static bool _badblocks_clear(struct badblocks *bb, sector_t s, sector_t sectors)
 	/* Not front overlap, but behind overlap */
 	if ((prev + 1) < bb->count && overlap_behind(bb, &bad, prev + 1)) {
 		len = BB_OFFSET(p[prev + 1]) - bad.start;
+		if (len == 0) {
+			pr_warn_once("badblocks_clear: zero-length segment detected\n");
+			len = 1;
+		}
 		hint = prev + 1;
 		/* Clear non-bad range should be treated as successful */
 		cleared++;
-- 
2.43.0


^ permalink raw reply related

* [PATCH v2] block: fix infinite loop in badblocks_clear()
From: Ramesh Adhikari @ 2026-04-26 17:11 UTC (permalink / raw)
  To: gregkh; +Cc: axboe, linux-block, security, Ramesh Adhikari
In-Reply-To: <20260426145527.589804-1-adhikari.resume@gmail.com>

An infinite loop can occur in _badblocks_clear() when BB_OFFSET(p[prev + 1])
equals bad.start, resulting in len = 0. This causes the update_sectors loop
to spin forever without making progress:

    s += 0;         // no advancement
    sectors -= 0;   // stays positive
    goto re_clear;  // infinite loop

The bug exists in two code paths:
1. _badblocks_clear() at line 1153 (behind overlap check)
2. _badblocks_check() at line 1240 (behind overlap check)

Add checks in both functions to ensure len is non-zero before entering
the loop.

Signed-off-by: Ramesh Adhikari <adhikari.resume@gmail.com>
---
 block/badblocks.c | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/block/badblocks.c b/block/badblocks.c
index ece64e76fe8..1c6728866f9 100644
--- a/block/badblocks.c
+++ b/block/badblocks.c
@@ -1151,6 +1151,10 @@ static bool _badblocks_clear(struct badblocks *bb, sector_t s, sector_t sectors)
 	/* Not front overlap, but behind overlap */
 	if ((prev + 1) < bb->count && overlap_behind(bb, &bad, prev + 1)) {
 		len = BB_OFFSET(p[prev + 1]) - bad.start;
+		if (len == 0) {
+			pr_warn_once("badblocks_clear: zero-length segment detected\n");
+			len = 1;
+		}
 		hint = prev + 1;
 		/* Clear non-bad range should be treated as successful */
 		cleared++;
@@ -1234,6 +1238,10 @@ static int _badblocks_check(struct badblocks *bb, sector_t s, sector_t sectors,
 	/* Not front overlap, but behind overlap */
 	if ((prev + 1) < bb->count && overlap_behind(bb, &bad, prev + 1)) {
 		len = BB_OFFSET(p[prev + 1]) - bad.start;
+		if (len == 0) {
+			pr_warn_once("badblocks_check: zero-length segment detected\n");
+			len = 1;
+		}
 		hint = prev + 1;
 		goto update_sectors;
 	}
-- 
2.43.0


^ permalink raw reply related

* [PATCH v3] block: fix infinite loop in badblocks_clear() and badblocks_check()
From: Ramesh Adhikari @ 2026-04-26 17:26 UTC (permalink / raw)
  To: gregkh; +Cc: axboe, linux-block, security, Ramesh Adhikari
In-Reply-To: <20260426145527.589804-1-adhikari.resume@gmail.com>

An infinite loop can occur in both _badblocks_clear() and _badblocks_check()
when BB_OFFSET() equals the start position, resulting in len = 0. This causes
the update_sectors loop to spin forever without making progress:

    s += 0;         // no advancement
    sectors -= 0;   // stays positive
    goto re_clear;  // infinite loop

The bug exists in three code paths:
1. _badblocks_clear() at line 1100 (start before all badblocks)
2. _badblocks_clear() at line 1153 (behind overlap check)
3. _badblocks_check() at line 1240 (behind overlap check)

Add checks in all three locations to ensure len is non-zero before
entering the loop.

Signed-off-by: Ramesh Adhikari <adhikari.resume@gmail.com>
---
 block/badblocks.c | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/block/badblocks.c b/block/badblocks.c
index ece64e76fe8..9dd2d921e1b 100644
--- a/block/badblocks.c
+++ b/block/badblocks.c
@@ -1098,6 +1098,10 @@ static bool _badblocks_clear(struct badblocks *bb, sector_t s, sector_t sectors)
 	if (prev < 0) {
 		if (overlap_behind(bb, &bad, 0)) {
 			len = BB_OFFSET(p[0]) - s;
+			if (len == 0) {
+				pr_warn_once("badblocks_clear: zero-length at start\n");
+				len = 1;
+			}
 			hint = 0;
 		} else {
 			len = sectors;
@@ -1151,6 +1155,10 @@ static bool _badblocks_clear(struct badblocks *bb, sector_t s, sector_t sectors)
 	/* Not front overlap, but behind overlap */
 	if ((prev + 1) < bb->count && overlap_behind(bb, &bad, prev + 1)) {
 		len = BB_OFFSET(p[prev + 1]) - bad.start;
+		if (len == 0) {
+			pr_warn_once("badblocks_clear: zero-length segment detected\n");
+			len = 1;
+		}
 		hint = prev + 1;
 		/* Clear non-bad range should be treated as successful */
 		cleared++;
@@ -1234,6 +1242,10 @@ static int _badblocks_check(struct badblocks *bb, sector_t s, sector_t sectors,
 	/* Not front overlap, but behind overlap */
 	if ((prev + 1) < bb->count && overlap_behind(bb, &bad, prev + 1)) {
 		len = BB_OFFSET(p[prev + 1]) - bad.start;
+		if (len == 0) {
+			pr_warn_once("badblocks_check: zero-length segment detected\n");
+			len = 1;
+		}
 		hint = prev + 1;
 		goto update_sectors;
 	}
-- 
2.43.0


^ permalink raw reply related

* Re: [PATCH v3] block: fix infinite loop in badblocks_clear() and badblocks_check()
From: Jens Axboe @ 2026-04-26 17:30 UTC (permalink / raw)
  To: Ramesh Adhikari, gregkh; +Cc: linux-block, security
In-Reply-To: <20260426172641.629158-1-adhikari.resume@gmail.com>

On 4/26/26 11:26 AM, Ramesh Adhikari wrote:
> An infinite loop can occur in both _badblocks_clear() and _badblocks_check()
> when BB_OFFSET() equals the start position, resulting in len = 0. This causes
> the update_sectors loop to spin forever without making progress:
> 
>     s += 0;         // no advancement
>     sectors -= 0;   // stays positive
>     goto re_clear;  // infinite loop
> 
> The bug exists in three code paths:
> 1. _badblocks_clear() at line 1100 (start before all badblocks)
> 2. _badblocks_clear() at line 1153 (behind overlap check)
> 3. _badblocks_check() at line 1240 (behind overlap check)

1) please calm down with the repostings
2) sit back, actually take your time, and find all the spots. this
drive-by-development is not useful.

-- 
Jens Axboe


^ permalink raw reply

* Re: [PATCH v2] cdrom, scsi: sr: propagate read-only status to block layer via set_disk_ro()
From: Phillip Potter @ 2026-04-26 21:03 UTC (permalink / raw)
  To: Daan De Meyer
  Cc: phil, martin.petersen, James.Bottomley, axboe, linux-scsi,
	linux-block, linux-kernel, Daan De Meyer
In-Reply-To: <20260422113206.246267-1-daan@amutable.com>

On Wed, Apr 22, 2026 at 11:32:06AM +0000, Daan De Meyer wrote:
> 
>  drivers/cdrom/cdrom.c | 73 ++++++++++++++++++++++++++++---------------
>  drivers/scsi/sr.c     | 11 ++-----
>  drivers/scsi/sr.h     |  1 -
>  include/linux/cdrom.h |  1 +
>  4 files changed, 51 insertions(+), 35 deletions(-)
> 
> diff --git a/drivers/cdrom/cdrom.c b/drivers/cdrom/cdrom.c
> index fc049612d6dc..62934cf4b10d 100644
> --- a/drivers/cdrom/cdrom.c
> +++ b/drivers/cdrom/cdrom.c
> @@ -631,6 +631,16 @@ int register_cdrom(struct gendisk *disk, struct cdrom_device_info *cdi)
>  
>  	WARN_ON(!cdo->generic_packet);
>  
> +	/*
> +	 * Propagate the drive's write support to the block layer so BLKROGET
> +	 * reflects actual write capability. Drivers that use GET CONFIGURATION
> +	 * features (CDC_MRW_W, CDC_RAM) must have called
> +	 * cdrom_probe_write_features() before register_cdrom() so the mask is
> +	 * complete here.
> +	 */
> +	set_disk_ro(disk, !CDROM_CAN(CDC_DVD_RAM | CDC_MRW_W | CDC_RAM |
> +				     CDC_CD_RW));
> +
>  	cd_dbg(CD_REG_UNREG, "drive \"/dev/%s\" registered\n", cdi->name);
>  	mutex_lock(&cdrom_mutex);
>  	list_add(&cdi->list, &cdrom_list);
> @@ -742,6 +752,44 @@ static int cdrom_is_random_writable(struct cdrom_device_info *cdi, int *write)
>  	return 0;
>  }
>  
> +/*
> + * Probe write-related MMC features via GET CONFIGURATION and update
> + * cdi->mask accordingly. Drivers that populate cdi->mask from the MODE SENSE
> + * capabilities page (e.g. sr) should call this after those MODE SENSE bits
> + * have been set but before register_cdrom(), so that the full set of
> + * write-capability bits is known by the time register_cdrom() decides on the
> + * initial read-only state of the disk.
> + */
> +void cdrom_probe_write_features(struct cdrom_device_info *cdi)
> +{
> +	int mrw, mrw_write, ram_write;
> +
> +	mrw = 0;
> +	if (!cdrom_is_mrw(cdi, &mrw_write))
> +		mrw = 1;
> +
> +	if (CDROM_CAN(CDC_MO_DRIVE))
> +		ram_write = 1;
> +	else
> +		(void) cdrom_is_random_writable(cdi, &ram_write);
> +
> +	if (mrw)
> +		cdi->mask &= ~CDC_MRW;
> +	else
> +		cdi->mask |= CDC_MRW;
> +
> +	if (mrw_write)
> +		cdi->mask &= ~CDC_MRW_W;
> +	else
> +		cdi->mask |= CDC_MRW_W;
> +
> +	if (ram_write)
> +		cdi->mask &= ~CDC_RAM;
> +	else
> +		cdi->mask |= CDC_RAM;
> +}
> +EXPORT_SYMBOL(cdrom_probe_write_features);
> +
>  static int cdrom_media_erasable(struct cdrom_device_info *cdi)
>  {
>  	disc_information di;
> @@ -894,33 +942,8 @@ static int cdrom_is_dvd_rw(struct cdrom_device_info *cdi)
>   */
>  static int cdrom_open_write(struct cdrom_device_info *cdi)
>  {
> -	int mrw, mrw_write, ram_write;
>  	int ret = 1;
>  
> -	mrw = 0;
> -	if (!cdrom_is_mrw(cdi, &mrw_write))
> -		mrw = 1;
> -
> -	if (CDROM_CAN(CDC_MO_DRIVE))
> -		ram_write = 1;
> -	else
> -		(void) cdrom_is_random_writable(cdi, &ram_write);
> -	
> -	if (mrw)
> -		cdi->mask &= ~CDC_MRW;
> -	else
> -		cdi->mask |= CDC_MRW;
> -
> -	if (mrw_write)
> -		cdi->mask &= ~CDC_MRW_W;
> -	else
> -		cdi->mask |= CDC_MRW_W;
> -
> -	if (ram_write)
> -		cdi->mask &= ~CDC_RAM;
> -	else
> -		cdi->mask |= CDC_RAM;
> -
>  	if (CDROM_CAN(CDC_MRW_W))
>  		ret = cdrom_mrw_open_write(cdi);
>  	else if (CDROM_CAN(CDC_DVD_RAM))
> diff --git a/drivers/scsi/sr.c b/drivers/scsi/sr.c
> index 7adb2573f50d..c36c54ecd354 100644
> --- a/drivers/scsi/sr.c
> +++ b/drivers/scsi/sr.c
> @@ -395,7 +395,7 @@ static blk_status_t sr_init_command(struct scsi_cmnd *SCpnt)
>  
>  	switch (req_op(rq)) {
>  	case REQ_OP_WRITE:
> -		if (!cd->writeable)
> +		if (get_disk_ro(cd->disk))
>  			goto out;
>  		SCpnt->cmnd[0] = WRITE_10;
>  		cd->cdi.media_written = 1;
> @@ -681,6 +681,7 @@ static int sr_probe(struct scsi_device *sdev)
>  	error = -ENOMEM;
>  	if (get_capabilities(cd))
>  		goto fail_minor;
> +	cdrom_probe_write_features(&cd->cdi);
>  	sr_vendor_init(cd);
>  
>  	set_capacity(disk, cd->capacity);
> @@ -899,14 +900,6 @@ static int get_capabilities(struct scsi_cd *cd)
>  	/*else    I don't think it can close its tray
>  		cd->cdi.mask |= CDC_CLOSE_TRAY; */
>  
> -	/*
> -	 * if DVD-RAM, MRW-W or CD-RW, we are randomly writable
> -	 */
> -	if ((cd->cdi.mask & (CDC_DVD_RAM | CDC_MRW_W | CDC_RAM | CDC_CD_RW)) !=
> -			(CDC_DVD_RAM | CDC_MRW_W | CDC_RAM | CDC_CD_RW)) {
> -		cd->writeable = 1;
> -	}
> -
>  	kfree(buffer);
>  	return 0;
>  }
> diff --git a/drivers/scsi/sr.h b/drivers/scsi/sr.h
> index dc899277b3a4..2d92f9cb6fec 100644
> --- a/drivers/scsi/sr.h
> +++ b/drivers/scsi/sr.h
> @@ -35,7 +35,6 @@ typedef struct scsi_cd {
>  	struct scsi_device *device;
>  	unsigned int vendor;	/* vendor code, see sr_vendor.c         */
>  	unsigned long ms_offset;	/* for reading multisession-CD's        */
> -	unsigned writeable : 1;
>  	unsigned use:1;		/* is this device still supportable     */
>  	unsigned xa_flag:1;	/* CD has XA sectors ? */
>  	unsigned readcd_known:1;	/* drive supports READ_CD (0xbe) */
> diff --git a/include/linux/cdrom.h b/include/linux/cdrom.h
> index b907e6c2307d..260d7968cf72 100644
> --- a/include/linux/cdrom.h
> +++ b/include/linux/cdrom.h
> @@ -108,6 +108,7 @@ int cdrom_ioctl(struct cdrom_device_info *cdi, struct block_device *bdev,
>  extern unsigned int cdrom_check_events(struct cdrom_device_info *cdi,
>  				       unsigned int clearing);
>  
> +extern void cdrom_probe_write_features(struct cdrom_device_info *cdi);
>  extern int register_cdrom(struct gendisk *disk, struct cdrom_device_info *cdi);
>  extern void unregister_cdrom(struct cdrom_device_info *cdi);
>  
> -- 
> 2.53.0
>

Hi Daan,

I've looked through the patch and it looks good to me. Looks like a
decent change. I think in this case too, it's unlikely this historically
broken behaviour is being relied upon (i.e. it seems unlikely to me that
fixing it would break anything).

In addition, I've build tested and booted/run some read/write tests with
your patch which worked fine for me too.

Reviewed-by: Phillip Potter <phil@philpotter.co.uk>

One final question from me though, and a purely procedural one:
The patch was submitted from your gmail address, but signed off by your
corporate address. Are you happy for me to adjust the submission so that
the author appears as your corporate address when sending on for
inclusion? Let me know, thanks.

Regards,
Phil

^ permalink raw reply

* Re: [PATCH v2] cdrom, scsi: sr: propagate read-only status to block layer via set_disk_ro()
From: Daan De Meyer @ 2026-04-26 21:06 UTC (permalink / raw)
  To: Phillip Potter
  Cc: martin.petersen, James.Bottomley, axboe, linux-scsi, linux-block,
	linux-kernel, Daan De Meyer
In-Reply-To: <ae59luYMh6npxD09@equinox>

Hi Phil,

Thanks for the review! You can adjust the submission. Yeah I should
probably fix my smtp stuff to use my corporate address. Feel free to
adjust the submission as you suggested.

Thanks!

Daan

On Sun, 26 Apr 2026 at 23:03, Phillip Potter <phil@philpotter.co.uk> wrote:
>
> On Wed, Apr 22, 2026 at 11:32:06AM +0000, Daan De Meyer wrote:
> >
> >  drivers/cdrom/cdrom.c | 73 ++++++++++++++++++++++++++++---------------
> >  drivers/scsi/sr.c     | 11 ++-----
> >  drivers/scsi/sr.h     |  1 -
> >  include/linux/cdrom.h |  1 +
> >  4 files changed, 51 insertions(+), 35 deletions(-)
> >
> > diff --git a/drivers/cdrom/cdrom.c b/drivers/cdrom/cdrom.c
> > index fc049612d6dc..62934cf4b10d 100644
> > --- a/drivers/cdrom/cdrom.c
> > +++ b/drivers/cdrom/cdrom.c
> > @@ -631,6 +631,16 @@ int register_cdrom(struct gendisk *disk, struct cdrom_device_info *cdi)
> >
> >       WARN_ON(!cdo->generic_packet);
> >
> > +     /*
> > +      * Propagate the drive's write support to the block layer so BLKROGET
> > +      * reflects actual write capability. Drivers that use GET CONFIGURATION
> > +      * features (CDC_MRW_W, CDC_RAM) must have called
> > +      * cdrom_probe_write_features() before register_cdrom() so the mask is
> > +      * complete here.
> > +      */
> > +     set_disk_ro(disk, !CDROM_CAN(CDC_DVD_RAM | CDC_MRW_W | CDC_RAM |
> > +                                  CDC_CD_RW));
> > +
> >       cd_dbg(CD_REG_UNREG, "drive \"/dev/%s\" registered\n", cdi->name);
> >       mutex_lock(&cdrom_mutex);
> >       list_add(&cdi->list, &cdrom_list);
> > @@ -742,6 +752,44 @@ static int cdrom_is_random_writable(struct cdrom_device_info *cdi, int *write)
> >       return 0;
> >  }
> >
> > +/*
> > + * Probe write-related MMC features via GET CONFIGURATION and update
> > + * cdi->mask accordingly. Drivers that populate cdi->mask from the MODE SENSE
> > + * capabilities page (e.g. sr) should call this after those MODE SENSE bits
> > + * have been set but before register_cdrom(), so that the full set of
> > + * write-capability bits is known by the time register_cdrom() decides on the
> > + * initial read-only state of the disk.
> > + */
> > +void cdrom_probe_write_features(struct cdrom_device_info *cdi)
> > +{
> > +     int mrw, mrw_write, ram_write;
> > +
> > +     mrw = 0;
> > +     if (!cdrom_is_mrw(cdi, &mrw_write))
> > +             mrw = 1;
> > +
> > +     if (CDROM_CAN(CDC_MO_DRIVE))
> > +             ram_write = 1;
> > +     else
> > +             (void) cdrom_is_random_writable(cdi, &ram_write);
> > +
> > +     if (mrw)
> > +             cdi->mask &= ~CDC_MRW;
> > +     else
> > +             cdi->mask |= CDC_MRW;
> > +
> > +     if (mrw_write)
> > +             cdi->mask &= ~CDC_MRW_W;
> > +     else
> > +             cdi->mask |= CDC_MRW_W;
> > +
> > +     if (ram_write)
> > +             cdi->mask &= ~CDC_RAM;
> > +     else
> > +             cdi->mask |= CDC_RAM;
> > +}
> > +EXPORT_SYMBOL(cdrom_probe_write_features);
> > +
> >  static int cdrom_media_erasable(struct cdrom_device_info *cdi)
> >  {
> >       disc_information di;
> > @@ -894,33 +942,8 @@ static int cdrom_is_dvd_rw(struct cdrom_device_info *cdi)
> >   */
> >  static int cdrom_open_write(struct cdrom_device_info *cdi)
> >  {
> > -     int mrw, mrw_write, ram_write;
> >       int ret = 1;
> >
> > -     mrw = 0;
> > -     if (!cdrom_is_mrw(cdi, &mrw_write))
> > -             mrw = 1;
> > -
> > -     if (CDROM_CAN(CDC_MO_DRIVE))
> > -             ram_write = 1;
> > -     else
> > -             (void) cdrom_is_random_writable(cdi, &ram_write);
> > -
> > -     if (mrw)
> > -             cdi->mask &= ~CDC_MRW;
> > -     else
> > -             cdi->mask |= CDC_MRW;
> > -
> > -     if (mrw_write)
> > -             cdi->mask &= ~CDC_MRW_W;
> > -     else
> > -             cdi->mask |= CDC_MRW_W;
> > -
> > -     if (ram_write)
> > -             cdi->mask &= ~CDC_RAM;
> > -     else
> > -             cdi->mask |= CDC_RAM;
> > -
> >       if (CDROM_CAN(CDC_MRW_W))
> >               ret = cdrom_mrw_open_write(cdi);
> >       else if (CDROM_CAN(CDC_DVD_RAM))
> > diff --git a/drivers/scsi/sr.c b/drivers/scsi/sr.c
> > index 7adb2573f50d..c36c54ecd354 100644
> > --- a/drivers/scsi/sr.c
> > +++ b/drivers/scsi/sr.c
> > @@ -395,7 +395,7 @@ static blk_status_t sr_init_command(struct scsi_cmnd *SCpnt)
> >
> >       switch (req_op(rq)) {
> >       case REQ_OP_WRITE:
> > -             if (!cd->writeable)
> > +             if (get_disk_ro(cd->disk))
> >                       goto out;
> >               SCpnt->cmnd[0] = WRITE_10;
> >               cd->cdi.media_written = 1;
> > @@ -681,6 +681,7 @@ static int sr_probe(struct scsi_device *sdev)
> >       error = -ENOMEM;
> >       if (get_capabilities(cd))
> >               goto fail_minor;
> > +     cdrom_probe_write_features(&cd->cdi);
> >       sr_vendor_init(cd);
> >
> >       set_capacity(disk, cd->capacity);
> > @@ -899,14 +900,6 @@ static int get_capabilities(struct scsi_cd *cd)
> >       /*else    I don't think it can close its tray
> >               cd->cdi.mask |= CDC_CLOSE_TRAY; */
> >
> > -     /*
> > -      * if DVD-RAM, MRW-W or CD-RW, we are randomly writable
> > -      */
> > -     if ((cd->cdi.mask & (CDC_DVD_RAM | CDC_MRW_W | CDC_RAM | CDC_CD_RW)) !=
> > -                     (CDC_DVD_RAM | CDC_MRW_W | CDC_RAM | CDC_CD_RW)) {
> > -             cd->writeable = 1;
> > -     }
> > -
> >       kfree(buffer);
> >       return 0;
> >  }
> > diff --git a/drivers/scsi/sr.h b/drivers/scsi/sr.h
> > index dc899277b3a4..2d92f9cb6fec 100644
> > --- a/drivers/scsi/sr.h
> > +++ b/drivers/scsi/sr.h
> > @@ -35,7 +35,6 @@ typedef struct scsi_cd {
> >       struct scsi_device *device;
> >       unsigned int vendor;    /* vendor code, see sr_vendor.c         */
> >       unsigned long ms_offset;        /* for reading multisession-CD's        */
> > -     unsigned writeable : 1;
> >       unsigned use:1;         /* is this device still supportable     */
> >       unsigned xa_flag:1;     /* CD has XA sectors ? */
> >       unsigned readcd_known:1;        /* drive supports READ_CD (0xbe) */
> > diff --git a/include/linux/cdrom.h b/include/linux/cdrom.h
> > index b907e6c2307d..260d7968cf72 100644
> > --- a/include/linux/cdrom.h
> > +++ b/include/linux/cdrom.h
> > @@ -108,6 +108,7 @@ int cdrom_ioctl(struct cdrom_device_info *cdi, struct block_device *bdev,
> >  extern unsigned int cdrom_check_events(struct cdrom_device_info *cdi,
> >                                      unsigned int clearing);
> >
> > +extern void cdrom_probe_write_features(struct cdrom_device_info *cdi);
> >  extern int register_cdrom(struct gendisk *disk, struct cdrom_device_info *cdi);
> >  extern void unregister_cdrom(struct cdrom_device_info *cdi);
> >
> > --
> > 2.53.0
> >
>
> Hi Daan,
>
> I've looked through the patch and it looks good to me. Looks like a
> decent change. I think in this case too, it's unlikely this historically
> broken behaviour is being relied upon (i.e. it seems unlikely to me that
> fixing it would break anything).
>
> In addition, I've build tested and booted/run some read/write tests with
> your patch which worked fine for me too.
>
> Reviewed-by: Phillip Potter <phil@philpotter.co.uk>
>
> One final question from me though, and a purely procedural one:
> The patch was submitted from your gmail address, but signed off by your
> corporate address. Are you happy for me to adjust the submission so that
> the author appears as your corporate address when sending on for
> inclusion? Let me know, thanks.
>
> Regards,
> Phil

^ permalink raw reply

* [PATCH RFC 1/2] nvme: downgrade WARN in nvme_setup_rw to pr_debug
From: Chao Shi @ 2026-04-27  0:34 UTC (permalink / raw)
  To: linux-nvme
  Cc: linux-block, hch, kbusch, sagi, axboe, Chao Shi, Sungwoo Kim,
	Dave Tian, Weidong Zhu

When an NVMe namespace is configured with embedded metadata (flbas bit 4
set, NVME_NS_FLBAS_META_EXT) but no Protection Information (dps=0) and
no NVME_NS_METADATA_SUPPORTED, nvme_setup_rw() fires WARN_ON_ONCE on
any request that reaches it with REQ_INTEGRITY unset.  The WARN was
observed repeatedly during NVMe fuzz testing with a FEMU-based fuzzer
that performs semantic mutation of Identify Namespace responses.

The trigger requires three conditions to align: (a) a namespace
transitions through the EXT_LBAS non-PI state (head->ms != 0,
features & NVME_NS_EXT_LBAS, !(features & NVME_NS_METADATA_SUPPORTED)),
(b) nvme_init_integrity() returns false through the early-exit branch
at core.c:1834 without populating bi->metadata_size, leaving the disk
without an integrity profile (blk_get_integrity() returns NULL), and
(c) a request that was admitted to the block layer before the namespace
update reaches nvme_setup_rw() after it.

The admission gap arises in two places.  First, the plug-list flush
path: a process with dirty pages queued in a plug before the namespace
update flushes them on file close (blk_finish_plug -> blk_mq_dispatch
-> nvme_setup_rw), bypassing any capacity-zero gate.  Second, the
cached-rq path: blk_mq_submit_bio() at blk-mq.c:3155 may find a cached
request; if so, the bio_queue_enter() freeze-serialization guard at
blk-mq.c:3174-3176 is skipped and the bio is dispatched immediately.

In both cases the bio was submitted without REQ_INTEGRITY (because
blk_get_integrity() returned NULL at dispatch time, so
bio_integrity_action() returned 0 and bio_integrity_prep() was not
called), and it reaches nvme_setup_rw() for a namespace where
head->ms != 0.  The existing BLK_STS_NOTSUPP return correctly handles
this dispatch; the WARN_ON_ONCE is a false positive.

The WARN was reproduced six times over four days of fuzzing (April
2026).  A representative crash shows the plug-flush path:

  nvme0n1: detected capacity change from 2097152 to 0
  WARNING: drivers/nvme/host/core.c:1042 at nvme_setup_rw+0x768/0xfd0
  PID: 785 (systemd-udevd)
  Call Trace:
   nvme_setup_cmd / nvme_queue_rq / blk_mq_dispatch_rq_list
   blk_mq_flush_plug_list / blk_finish_plug / blkdev_writepages
   sync_blockdev / bdev_release / __fput / sys_close

Replace WARN_ON_ONCE with pr_debug_ratelimited so the condition is
logged at debug level without splat.  The BLK_STS_NOTSUPP return is
preserved; I/O to the transitioning namespace is still rejected.

An alternative approach that addresses the root cause at the
integrity-profile level is proposed in patch 2/2: populate
bi->metadata_size for EXT_LBAS non-PI namespaces in nvme_init_integrity()
so that bio_integrity_action() returns non-zero, bio_integrity_prep()
sets REQ_INTEGRITY, and nvme_setup_rw() never reaches this branch.
Both patches are sent as RFC for maintainer guidance on the preferred
direction.

Tested: Compiled on linux-kcov-debug (6.19.0+, KASAN/DEBUG_LIST).
Boot-tested under FEMU with NVME_MALICIOUS_RESPONDER=1
NVME_SEMANTIC_DATA_MUTATOR=1; ran 4 concurrent dd processes plus 500
rescan_controller cycles.  No WARN, BUG, or Oops observed.

Found by FuzzNvme(Syzkaller with FEMU fuzzing framework).

Acked-by: Sungwoo Kim <iam@sung-woo.kim>
Acked-by: Dave Tian <daveti@purdue.edu>
Acked-by: Weidong Zhu <weizhu@fiu.edu>
Signed-off-by: Chao Shi <coshi036@gmail.com>
---
 drivers/nvme/host/core.c | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c
index d1711ef59fb..4e20c8f08e4 100644
--- a/drivers/nvme/host/core.c
+++ b/drivers/nvme/host/core.c
@@ -1039,8 +1039,12 @@ static inline blk_status_t nvme_setup_rw(struct nvme_ns *ns,
 		 * namespace capacity to zero to prevent any I/O.
 		 */
 		if (!blk_integrity_rq(req)) {
-			if (WARN_ON_ONCE(!nvme_ns_has_pi(ns->head)))
+			if (!nvme_ns_has_pi(ns->head)) {
+				pr_debug_ratelimited("nvme: %s: metadata (ms=%u) without PI or integrity request, returning NOTSUPP\n",
+						     ns->disk->disk_name,
+						     ns->head->ms);
 				return BLK_STS_NOTSUPP;
+			}
 			control |= NVME_RW_PRINFO_PRACT;
 			nvme_set_ref_tag(ns, cmnd, req);
 		}
-- 
2.43.0


^ permalink raw reply related

* [PATCH RFC 2/2] nvme: set integrity metadata size for EXT_LBAS non-PI namespace
From: Chao Shi @ 2026-04-27  0:34 UTC (permalink / raw)
  To: linux-nvme
  Cc: linux-block, hch, kbusch, sagi, axboe, Chao Shi, Sungwoo Kim,
	Dave Tian, Weidong Zhu
In-Reply-To: <20260427003457.1264511-1-coshi036@gmail.com>

This patch is an alternative to patch 1/2: instead of downgrading the
assertion in nvme_setup_rw(), it addresses the root cause at the
integrity-profile level so that the assertion is never reached.

For PCIe namespaces with extended LBAs (NVME_NS_EXT_LBAS set, flbas
bit 4) but without PI and without NVME_NS_METADATA_SUPPORTED, the early-
exit branch of nvme_init_integrity() at core.c:1834 returns false
without populating bi->metadata_size.  As a result blk_get_integrity()
returns NULL (it checks q->limits.integrity.metadata_size via
blk_integrity_queue_supports_integrity()), bio_integrity_action() returns
0, bio_integrity_prep() is never called, and REQ_INTEGRITY is never set
on bios dispatched to the namespace.  Any such bio that reaches
nvme_setup_rw() triggers WARN_ON_ONCE because head->ms != 0 but
blk_integrity_rq() returns false.

Populate bi->metadata_size = head->ms in the early-exit path for the
EXT_LBAS non-PI case.  This is sufficient to make blk_get_integrity()
return non-NULL, which causes bio_integrity_action() to return non-zero,
which causes bio_integrity_prep() to run and set REQ_INTEGRITY on any
bio submitted to the namespace.  Requests that reach nvme_setup_rw()
then satisfy blk_integrity_rq() and the assertion is not reached.

blk_validate_integrity_limits() accepts this configuration: with
csum_type=BLK_INTEGRITY_CSUM_NONE, pi_tuple_size=0, and pi_offset=0,
all checks pass (pi_offset + pi_tuple_size <= metadata_size, pi_tuple_size
must be 0 for CSUM_NONE), and interval_exp is auto-filled to
ilog2(logical_block_size).  No generate/verify callbacks are configured,
so no actual integrity computation occurs; only the blk_integrity_rq()
predicate is satisfied.  Capacity is still forced to 0 by
set_capacity_and_notify(), so new bios are rejected by bio_check_eod()
before queue entry.

Tested: Compiled on linux-kcov-debug (6.19.0+, KASAN/DEBUG_LIST).
Boot-tested under FEMU with NVME_SEMANTIC_DATA_MUTATOR=1; ran 4
concurrent dd processes plus 500 rescan_controller cycles with no WARN,
BUG, or Oops.  The EXT_LBAS + ms!=0 + !PI combination was not triggered
during testing (FEMU's mutator varies flbas and lbaf[0].ms independently;
flbas=0x10 with lbaf_idx=0 was not produced in this run).  The
bi->metadata_size assignment path was not exercised in testing;
correctness of blk_validate_integrity_limits() for this configuration
was verified by code inspection.  Provided as RFC.

Found by FuzzNvme(Syzkaller with FEMU fuzzing framework).

Acked-by: Sungwoo Kim <iam@sung-woo.kim>
Acked-by: Dave Tian <daveti@purdue.edu>
Acked-by: Weidong Zhu <weizhu@fiu.edu>
Signed-off-by: Chao Shi <coshi036@gmail.com>
---
 drivers/nvme/host/core.c | 25 +++++++++++++++++++++++--
 1 file changed, 23 insertions(+), 2 deletions(-)

diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c
index 4e20c8f08e4..76fb788024f 100644
--- a/drivers/nvme/host/core.c
+++ b/drivers/nvme/host/core.c
@@ -1836,8 +1836,29 @@ static bool nvme_init_integrity(struct nvme_ns_head *head,
 	 * insert/strip it, which is not possible for other kinds of metadata.
 	 */
 	if (!IS_ENABLED(CONFIG_BLK_DEV_INTEGRITY) ||
-	    !(head->features & NVME_NS_METADATA_SUPPORTED))
-		return nvme_ns_has_pi(head);
+	    !(head->features & NVME_NS_METADATA_SUPPORTED)) {
+		bool has_pi = nvme_ns_has_pi(head);
+
+		/*
+		 * For PCIe EXT_LBAS non-PI namespaces the block layer sets
+		 * capacity to 0 (we return false) to prevent block I/O, but a
+		 * cached-rq bio may bypass bio_queue_enter freeze serialisation
+		 * and reach nvme_setup_rw() with head->ms != 0 and no
+		 * REQ_INTEGRITY set.  Populate bi->metadata_size so that
+		 * bio_integrity_action() returns non-zero and bio_integrity_prep()
+		 * sets REQ_INTEGRITY on any such bio, preventing the WARN_ON_ONCE
+		 * at nvme_setup_rw() (addressed by patch 1/2).
+		 *
+		 * NOTE: only metadata_size is populated; no csum or PI profile is
+		 * configured.  Actual data integrity for EXT_LBAS non-PI workloads
+		 * is untested; this patch is RFC for direction discussion.
+		 */
+		if (IS_ENABLED(CONFIG_BLK_DEV_INTEGRITY) &&
+		    (head->features & NVME_NS_EXT_LBAS) &&
+		    head->ms && !has_pi)
+			bi->metadata_size = head->ms;
+		return has_pi;
+	}
 
 	switch (head->pi_type) {
 	case NVME_NS_DPS_PI_TYPE3:
-- 
2.43.0


^ permalink raw reply related

* Re: [PATCH v2 2/3] dm-inlinecrypt: add target for inline block device encryption
From: Benjamin Marzinski @ 2026-04-27  1:19 UTC (permalink / raw)
  To: Linlin Zhang
  Cc: linux-block, ebiggers, mpatocka, gmazyland, linux-kernel,
	adrianvovk, dm-devel, quic_mdalam, israelr, hch, axboe
In-Reply-To: <20260410134031.2880675-3-linlin.zhang@oss.qualcomm.com>

On Fri, Apr 10, 2026 at 06:40:30AM -0700, Linlin Zhang wrote:
> From: Eric Biggers <ebiggers@google.com>
> 
> Add a new device-mapper target "dm-inlinecrypt" that is similar to
> dm-crypt but uses the blk-crypto API instead of the regular crypto API.
> This allows it to take advantage of inline encryption hardware such as
> that commonly built into UFS host controllers.
> 
> The table syntax matches dm-crypt's, but for now only a stripped-down
> set of parameters is supported.  For example, for now AES-256-XTS is the
> only supported cipher.
> 
> dm-inlinecrypt is based on Android's dm-default-key with the
> controversial passthrough support removed.  Note that due to the removal
> of passthrough support, use of dm-inlinecrypt in combination with
> fscrypt causes double encryption of file contents (similar to dm-crypt +
> fscrypt), with the fscrypt layer not being able to use the inline
> encryption hardware.  This makes dm-inlinecrypt unusable on systems such
> as Android that use fscrypt and where a more optimized approach is
> needed.  It is however suitable as a replacement for dm-crypt.
> 
> dm-inlinecrypt supports both keyring key and hex key, the former avoids
> the key to be exposed in dm-table message. Similar to dm-default-key in
> Android, it will fallabck to the software block crypto once the inline
> crypto hardware cannot support the expected cipher.
> 
> Test:
> dmsetup create inlinecrypt_logon --table "0 `blockdev --getsz $1` \
> inlinecrypt aes-xts-plain64 :64:logon:fde:dminlinecrypt_test_key 0 $1 0"
> 
> Signed-off-by: Eric Biggers <ebiggers@google.com>
> Signed-off-by: Linlin Zhang <linlin.zhang@oss.qualcomm.com>
> ---
>  drivers/md/Kconfig          |  10 +
>  drivers/md/Makefile         |   1 +
>  drivers/md/dm-inlinecrypt.c | 559 ++++++++++++++++++++++++++++++++++++
>  3 files changed, 570 insertions(+)
>  create mode 100644 drivers/md/dm-inlinecrypt.c
> 
> diff --git a/drivers/md/Kconfig b/drivers/md/Kconfig
> index c58a9a8ea54e..aa541cc22ecc 100644
> --- a/drivers/md/Kconfig
> +++ b/drivers/md/Kconfig
> @@ -313,6 +313,16 @@ config DM_CRYPT
>  
>  	  If unsure, say N.
>  
> +config DM_INLINECRYPT
> +	tristate "Inline encryption target support"
> +	depends on BLK_DEV_DM
> +	depends on BLK_INLINE_ENCRYPTION
> +	help
> +	  This device-mapper target is similar to dm-crypt, but it uses the
> +	  blk-crypto API instead of the regular crypto API. This allows it to
> +	  take advantage of inline encryption hardware such as that commonly
> +	  built into UFS host controllers.
> +
>  config DM_SNAPSHOT
>         tristate "Snapshot target"
>         depends on BLK_DEV_DM
> diff --git a/drivers/md/Makefile b/drivers/md/Makefile
> index c338cc6fbe2e..517d1f7d8288 100644
> --- a/drivers/md/Makefile
> +++ b/drivers/md/Makefile
> @@ -55,6 +55,7 @@ obj-$(CONFIG_DM_UNSTRIPED)	+= dm-unstripe.o
>  obj-$(CONFIG_DM_BUFIO)		+= dm-bufio.o
>  obj-$(CONFIG_DM_BIO_PRISON)	+= dm-bio-prison.o
>  obj-$(CONFIG_DM_CRYPT)		+= dm-crypt.o
> +obj-$(CONFIG_DM_INLINECRYPT)	+= dm-inlinecrypt.o
>  obj-$(CONFIG_DM_DELAY)		+= dm-delay.o
>  obj-$(CONFIG_DM_DUST)		+= dm-dust.o
>  obj-$(CONFIG_DM_FLAKEY)		+= dm-flakey.o
> diff --git a/drivers/md/dm-inlinecrypt.c b/drivers/md/dm-inlinecrypt.c
> new file mode 100644
> index 000000000000..b6e98fdf8af1
> --- /dev/null
> +++ b/drivers/md/dm-inlinecrypt.c
> @@ -0,0 +1,559 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Copyright 2024 Google LLC
> + */
> +
> +#include <linux/blk-crypto.h>
> +#include <linux/ctype.h>
> +#include <linux/device-mapper.h>
> +#include <linux/hex.h>
> +#include <linux/module.h>
> +#include <keys/user-type.h>
> +
> +#define DM_MSG_PREFIX	"inlinecrypt"
> +
> +static const struct dm_inlinecrypt_cipher {
> +	const char *name;
> +	enum blk_crypto_mode_num mode_num;
> +} dm_inlinecrypt_ciphers[] = {
> +	{
> +		.name = "aes-xts-plain64",
> +		.mode_num = BLK_ENCRYPTION_MODE_AES_256_XTS,
> +	},
> +};
> +
> +/**
> + * struct inlinecrypt_ctx - private data of an inlinecrypt target
> + * @dev: the underlying device
> + * @start: starting sector of the range of @dev which this target actually maps.
> + *	   For this purpose a "sector" is 512 bytes.
> + * @cipher_string: the name of the encryption algorithm being used
> + * @iv_offset: starting offset for IVs.  IVs are generated as if the target were
> + *	       preceded by @iv_offset 512-byte sectors.
> + * @sector_size: crypto sector size in bytes (usually 4096)
> + * @sector_bits: log2(sector_size)
> + * @key: the encryption key to use
> + * @max_dun: the maximum DUN that may be used (computed from other params)
> + */
> +struct inlinecrypt_ctx {
> +	struct dm_dev *dev;
> +	sector_t start;
> +	const char *cipher_string;
> +	unsigned int key_size;
> +	u64 iv_offset;
> +	unsigned int sector_size;
> +	unsigned int sector_bits;
> +	struct blk_crypto_key key;
> +	u64 max_dun;
> +};
> +
> +static const struct dm_inlinecrypt_cipher *
> +lookup_cipher(const char *cipher_string)
> +{
> +	int i;
> +
> +	for (i = 0; i < ARRAY_SIZE(dm_inlinecrypt_ciphers); i++) {
> +		if (strcmp(cipher_string, dm_inlinecrypt_ciphers[i].name) == 0)
> +			return &dm_inlinecrypt_ciphers[i];
> +	}
> +	return NULL;
> +}
> +
> +static void inlinecrypt_dtr(struct dm_target *ti)
> +{
> +	struct inlinecrypt_ctx *ctx = ti->private;
> +
> +	if (ctx->dev) {
> +		if (ctx->key.size)
> +			blk_crypto_evict_key(ctx->dev->bdev, &ctx->key);
> +		dm_put_device(ti, ctx->dev);
> +	}
> +	kfree_sensitive(ctx->cipher_string);
> +	kfree_sensitive(ctx);
> +}
> +
> +static bool contains_whitespace(const char *str)
> +{
> +	while (*str)
> +		if (isspace(*str++))
> +			return true;
> +	return false;
> +}
> +
> +static int set_key_user(struct key *key, char *bin_key,
> +			const unsigned int bin_key_size)
> +{
> +	const struct user_key_payload *ukp;
> +
> +	ukp = user_key_payload_locked(key);
> +	if (!ukp)
> +		return -EKEYREVOKED;
> +
> +	if (bin_key_size != ukp->datalen)
> +		return -EINVAL;
> +
> +	memcpy(bin_key, ukp->data, bin_key_size);
> +
> +	return 0;
> +}
> +
> +static int inlinecrypt_get_keyring_key(const char *key_string, u8 *bin_key,
> +					const unsigned int bin_key_size)
> +{
> +	char *key_desc;
> +	int ret;
> +	struct key_type *type;
> +	struct key *key;

There's nothing forcing CONFIG_KEYS to be set when CONFIG_DM_INLINECRYPT
is, and without it, struct key won't be defined and this won't compile.

> +	int (*set_key)(struct key *key, char *bin_key,
> +				   const unsigned int bin_key_size);
> +
> +	/*
> +	 * Reject key_string with whitespace. dm core currently lacks code for
> +	 * proper whitespace escaping in arguments on DM_TABLE_STATUS path.
> +	 */
> +	if (contains_whitespace(key_string)) {
> +		DMERR("whitespace chars not allowed in key string");
> +		return -EINVAL;
> +	}
> +
> +	/* look for next ':' separating key_type from key_description */
> +	key_desc = strchr(key_string, ':');
> +	if (!key_desc || key_desc == key_string || !strlen(key_desc + 1))
> +		return -EINVAL;
> +
> +	if (!strncmp(key_string, "logon:", key_desc - key_string + 1)) {
> +		type = &key_type_logon;
> +		set_key = set_key_user;
> +	} else {
> +		return -EINVAL;
> +	}
> +
> +	key = request_key(type, key_desc + 1, NULL);
> +	if (IS_ERR(key))
> +		return PTR_ERR(key);
> +
> +	down_read(&key->sem);
> +
> +	ret = set_key(key, (char *)bin_key, bin_key_size);
> +	if (ret < 0) {

This does the same commands regardless of whether it takes this branch.

> +		up_read(&key->sem);
> +		key_put(key);
> +		return ret;
> +	}
> +
> +	up_read(&key->sem);
> +	key_put(key);
> +
> +	return ret;
> +}
> +
> +static int inlinecrypt_get_key(const char *key_string,
> +				u8 key[BLK_CRYPTO_MAX_ANY_KEY_SIZE],
> +				const unsigned int key_size)
> +{
> +	int ret = 0;
> +
> +	/* ':' means the key is in kernel keyring, short-circuit normal key processing */
> +	if (key_string[0] == ':') {
> +		if (key_size > BLK_CRYPTO_MAX_ANY_KEY_SIZE) {
> +			DMERR("Invalid keysize");
> +			return -EINVAL;
> +		}
> +		/* key string should be :<logon|user>:<key_desc> */
> +		ret = inlinecrypt_get_keyring_key(key_string + 1, key, key_size);
> +		goto out;
> +	}
> +
> +	if (key_size > 2 * BLK_CRYPTO_MAX_ANY_KEY_SIZE

get_key_size() returns the size of the binary key in this case, so
shouldn't this check for "key_size > BLK_CRYPTO_MAX_ANY_KEY_SIZE", and
it seems like the check if the key_size is odd would make more sense in
get_key_size().

> +		|| key_size  % 2
> +		|| !key_size) {
> +		DMERR("Invalid keysize");
> +		return -EINVAL;
> +	}
> +	if (hex2bin(key, key_string, key_size) != 0)
> +		ret = -EINVAL;
> +
> +out:
> +	return ret;
> +}
> +
> +static int get_key_size(char **key_string)
> +{
> +	char *colon, dummy;
> +	int ret;
> +
> +	if (*key_string[0] != ':')
> +		return strlen(*key_string) >> 1;
> +
> +	/* look for next ':' in key string */
> +	colon = strpbrk(*key_string + 1, ":");
> +	if (!colon)
> +		return -EINVAL;
> +
> +	if (sscanf(*key_string + 1, "%u%c", &ret, &dummy) != 2 || dummy != ':')
> +		return -EINVAL;
> +
> +	/* remaining key string should be :<logon|user>:<key_desc> */
> +	*key_string = colon;
> +
> +	return ret;
> +}
> +
> +static int inlinecrypt_ctr_optional(struct dm_target *ti,
> +				    unsigned int argc, char **argv)
> +{
> +	struct inlinecrypt_ctx *ctx = ti->private;
> +	struct dm_arg_set as;
> +	static const struct dm_arg _args[] = {
> +		{0, 3, "Invalid number of feature args"},
> +	};
> +	unsigned int opt_params;
> +	const char *opt_string;
> +	bool iv_large_sectors = false;
> +	char dummy;
> +	int err;
> +
> +	as.argc = argc;
> +	as.argv = argv;
> +
> +	err = dm_read_arg_group(_args, &as, &opt_params, &ti->error);
> +	if (err)
> +		return err;
> +
> +	while (opt_params--) {
> +		opt_string = dm_shift_arg(&as);
> +		if (!opt_string) {
> +			ti->error = "Not enough feature arguments";
> +			return -EINVAL;
> +		}
> +		if (!strcmp(opt_string, "allow_discards")) {
> +			ti->num_discard_bios = 1;
> +		} else if (sscanf(opt_string, "sector_size:%u%c",
> +				  &ctx->sector_size, &dummy) == 1) {
> +			if (ctx->sector_size < SECTOR_SIZE ||
> +			    ctx->sector_size > 4096 ||
> +			    !is_power_of_2(ctx->sector_size)) {
> +				ti->error = "Invalid sector_size";
> +				return -EINVAL;
> +			}
> +		} else if (!strcmp(opt_string, "iv_large_sectors")) {
> +			iv_large_sectors = true;
> +		} else {
> +			ti->error = "Invalid feature arguments";
> +			return -EINVAL;
> +		}
> +	}
> +
> +	/* dm-inlinecrypt doesn't implement iv_large_sectors=false. */
> +	if (ctx->sector_size != SECTOR_SIZE && !iv_large_sectors) {
> +		ti->error = "iv_large_sectors must be specified";

Since setting sector_size forces setting iv_large_sectors, does it
really need to be a separate parameter? Can't it just be implied by
setting a non-512 sector_size. Is this here to futureproof the table
line?

> +		return -EINVAL;
> +	}
> +
> +	return 0;
> +}
> +
> +/*
> + * Construct an inlinecrypt mapping:
> + * <cipher> [<key>|:<key_size>:<logon>:<key_description>] <iv_offset> <dev_path> <start>
> + *
> + * This syntax matches dm-crypt's, but the set of supported functionality has
> + * been stripped down.
> + */
> +static int inlinecrypt_ctr(struct dm_target *ti, unsigned int argc, char **argv)
> +{
> +	struct inlinecrypt_ctx *ctx;
> +	const struct dm_inlinecrypt_cipher *cipher;
> +	u8 raw_key[BLK_CRYPTO_MAX_ANY_KEY_SIZE];
> +	unsigned int dun_bytes;
> +	unsigned long long tmpll;
> +	char dummy;
> +	int err;
> +
> +	if (argc < 5) {
> +		ti->error = "Not enough arguments";
> +		return -EINVAL;
> +	}
> +
> +	ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
> +	if (!ctx) {
> +		ti->error = "Out of memory";
> +		return -ENOMEM;
> +	}
> +	ti->private = ctx;
> +
> +	/* <cipher> */
> +	ctx->cipher_string = kstrdup(argv[0], GFP_KERNEL);
> +	if (!ctx->cipher_string) {
> +		ti->error = "Out of memory";
> +		err = -ENOMEM;
> +		goto bad;
> +	}
> +	cipher = lookup_cipher(ctx->cipher_string);
> +	if (!cipher) {
> +		ti->error = "Unsupported cipher";
> +		err = -EINVAL;
> +		goto bad;
> +	}
> +
> +	/* <key> */
> +	ctx->key_size = get_key_size(&argv[1]);
> +	if (ctx->key_size < 0) {
> +		ti->error = "Cannot parse key size";
> +		return -EINVAL;
> +	}
> +	err = inlinecrypt_get_key(argv[1], raw_key, ctx->key_size);
> +	if (err) {
> +		ti->error = "Malformed key string";
> +		goto bad;
> +	}
> +
> +	/* <iv_offset> */
> +	if (sscanf(argv[2], "%llu%c", &ctx->iv_offset, &dummy) != 1) {
> +		ti->error = "Invalid iv_offset sector";
> +		err = -EINVAL;
> +		goto bad;
> +	}
> +
> +	/* <dev_path> */
> +	err = dm_get_device(ti, argv[3], dm_table_get_mode(ti->table),
> +			    &ctx->dev);
> +	if (err) {
> +		ti->error = "Device lookup failed";
> +		goto bad;
> +	}
> +
> +	/* <start> */
> +	if (sscanf(argv[4], "%llu%c", &tmpll, &dummy) != 1 ||
> +	    tmpll != (sector_t)tmpll) {
> +		ti->error = "Invalid start sector";
> +		err = -EINVAL;
> +		goto bad;
> +	}
> +	ctx->start = tmpll;
> +
> +	/* optional arguments */
> +	ctx->sector_size = SECTOR_SIZE;
> +	if (argc > 5) {
> +		err = inlinecrypt_ctr_optional(ti, argc - 5, &argv[5]);
> +		if (err)
> +			goto bad;
> +	}
> +	ctx->sector_bits = ilog2(ctx->sector_size);
> +	if (ti->len & ((ctx->sector_size >> SECTOR_SHIFT) - 1)) {
> +		ti->error = "Device size is not a multiple of sector_size";
> +		err = -EINVAL;
> +		goto bad;
> +	}
> +
> +	ctx->max_dun = (ctx->iv_offset + ti->len - 1) >>
> +		       (ctx->sector_bits - SECTOR_SHIFT);
> +	dun_bytes = DIV_ROUND_UP(fls64(ctx->max_dun), 8);
> +
> +	err = blk_crypto_init_key(&ctx->key, raw_key, ctx->key_size,
> +				  BLK_CRYPTO_KEY_TYPE_RAW,
> +				  cipher->mode_num, dun_bytes,
> +				  ctx->sector_size);
> +	if (err) {
> +		ti->error = "Error initializing blk-crypto key";
> +		goto bad;
> +	}
> +
> +	err = blk_crypto_start_using_key(ctx->dev->bdev, &ctx->key);
> +	if (err) {
> +		ti->error = "Error starting to use blk-crypto";
> +		goto bad;
> +	}
> +
> +	ti->num_flush_bios = 1;
> +
> +	err = 0;
> +	goto out;
> +
> +bad:
> +	inlinecrypt_dtr(ti);
> +out:
> +	memzero_explicit(raw_key, sizeof(raw_key));
> +	return err;
> +}
> +
> +static int inlinecrypt_map(struct dm_target *ti, struct bio *bio)
> +{
> +	const struct inlinecrypt_ctx *ctx = ti->private;
> +	sector_t sector_in_target;
> +	u64 dun[BLK_CRYPTO_DUN_ARRAY_SIZE] = {};
> +
> +	bio_set_dev(bio, ctx->dev->bdev);
> +
> +	/*
> +	 * If the bio is a device-level request which doesn't target a specific
> +	 * sector, there's nothing more to do.
> +	 */
> +	if (bio_sectors(bio) == 0)
> +		return DM_MAPIO_REMAPPED;
> +
> +	/*
> +	 * The bio should never have an encryption context already, since
> +	 * dm-inlinecrypt doesn't pass through any inline encryption
> +	 * capabilities to the layer above it.
> +	 */
> +	if (WARN_ON_ONCE(bio_has_crypt_ctx(bio)))
> +		return DM_MAPIO_KILL;
> +
> +	/* Map the bio's sector to the underlying device. (512-byte sectors) */
> +	sector_in_target = dm_target_offset(ti, bio->bi_iter.bi_sector);
> +	bio->bi_iter.bi_sector = ctx->start + sector_in_target;
> +	/*
> +	 * If the bio doesn't have any data (e.g. if it's a DISCARD request),
> +	 * there's nothing more to do.
> +	 */
> +	if (!bio_has_data(bio))
> +		return DM_MAPIO_REMAPPED;
> +
> +	/* Calculate the DUN and enforce data-unit (crypto sector) alignment. */
> +	dun[0] = ctx->iv_offset + sector_in_target; /* 512-byte sectors */
> +	if (dun[0] & ((ctx->sector_size >> SECTOR_SHIFT) - 1))
> +		return DM_MAPIO_KILL;

If ctx->iv_offset is not a multiple of ctx->sector_size, this will
always fail. ctx->iv_offset should probably get validated in
inlinecrypt_ctr()

-Ben

> +	dun[0] >>= ctx->sector_bits - SECTOR_SHIFT; /* crypto sectors */
> +
> +	/*
> +	 * This check isn't necessary as we should have calculated max_dun
> +	 * correctly, but be safe.
> +	 */
> +	if (WARN_ON_ONCE(dun[0] > ctx->max_dun))
> +		return DM_MAPIO_KILL;
> +
> +	bio_crypt_set_ctx(bio, &ctx->key, dun, GFP_NOIO);
> +
> +	/*
> +	 * Since we've added an encryption context to the bio and
> +	 * blk-crypto-fallback may be needed to process it, it's necessary to
> +	 * use the fallback-aware bio submission code rather than
> +	 * unconditionally returning DM_MAPIO_REMAPPED.
> +	 *
> +	 * To get the correct accounting for a dm target in the case where
> +	 * __blk_crypto_submit_bio() doesn't take ownership of the bio (returns
> +	 * true), call __blk_crypto_submit_bio() directly and return
> +	 * DM_MAPIO_REMAPPED in that case, rather than relying on
> +	 * blk_crypto_submit_bio() which calls submit_bio() in that case.
> +	 */
> +	if (__blk_crypto_submit_bio(bio))
> +		return DM_MAPIO_REMAPPED;
> +	return DM_MAPIO_SUBMITTED;
> +}
> +
> +static void inlinecrypt_status(struct dm_target *ti, status_type_t type,
> +			       unsigned int status_flags, char *result,
> +			       unsigned int maxlen)
> +{
> +	const struct inlinecrypt_ctx *ctx = ti->private;
> +	unsigned int sz = 0;
> +	int num_feature_args = 0;
> +
> +	switch (type) {
> +	case STATUSTYPE_INFO:
> +	case STATUSTYPE_IMA:
> +		result[0] = '\0';
> +		break;
> +
> +	case STATUSTYPE_TABLE:
> +		/*
> +		 * Warning: like dm-crypt, dm-inlinecrypt includes the key in
> +		 * the returned table.  Userspace is responsible for redacting
> +		 * the key when needed.
> +		 */
> +		DMEMIT("%s %*phN %llu %s %llu", ctx->cipher_string,
> +		       ctx->key.size, ctx->key.bytes, ctx->iv_offset,
> +		       ctx->dev->name, ctx->start);
> +		num_feature_args += !!ti->num_discard_bios;
> +		if (ctx->sector_size != SECTOR_SIZE)
> +			num_feature_args += 2;
> +		if (num_feature_args != 0) {
> +			DMEMIT(" %d", num_feature_args);
> +			if (ti->num_discard_bios)
> +				DMEMIT(" allow_discards");
> +			if (ctx->sector_size != SECTOR_SIZE) {
> +				DMEMIT(" sector_size:%u", ctx->sector_size);
> +				DMEMIT(" iv_large_sectors");
> +			}
> +		}
> +		break;
> +	}
> +}
> +
> +static int inlinecrypt_prepare_ioctl(struct dm_target *ti,
> +				     struct block_device **bdev, unsigned int cmd,
> +				     unsigned long arg, bool *forward)
> +{
> +	const struct inlinecrypt_ctx *ctx = ti->private;
> +	const struct dm_dev *dev = ctx->dev;
> +
> +	*bdev = dev->bdev;
> +
> +	/* Only pass ioctls through if the device sizes match exactly. */
> +	return ctx->start != 0 || ti->len != bdev_nr_sectors(dev->bdev);
> +}
> +
> +static int inlinecrypt_iterate_devices(struct dm_target *ti,
> +				       iterate_devices_callout_fn fn,
> +				       void *data)
> +{
> +	const struct inlinecrypt_ctx *ctx = ti->private;
> +
> +	return fn(ti, ctx->dev, ctx->start, ti->len, data);
> +}
> +
> +#ifdef CONFIG_BLK_DEV_ZONED
> +static int inlinecrypt_report_zones(struct dm_target *ti,
> +				    struct dm_report_zones_args *args,
> +				    unsigned int nr_zones)
> +{
> +	const struct inlinecrypt_ctx *ctx = ti->private;
> +
> +	return dm_report_zones(ctx->dev->bdev, ctx->start,
> +			ctx->start + dm_target_offset(ti, args->next_sector),
> +			args, nr_zones);
> +}
> +#else
> +#define inlinecrypt_report_zones NULL
> +#endif
> +
> +static void inlinecrypt_io_hints(struct dm_target *ti,
> +				 struct queue_limits *limits)
> +{
> +	const struct inlinecrypt_ctx *ctx = ti->private;
> +	const unsigned int sector_size = ctx->sector_size;
> +
> +	limits->logical_block_size =
> +		max_t(unsigned int, limits->logical_block_size, sector_size);
> +	limits->physical_block_size =
> +		max_t(unsigned int, limits->physical_block_size, sector_size);
> +	limits->io_min = max_t(unsigned int, limits->io_min, sector_size);
> +	limits->dma_alignment = limits->logical_block_size - 1;
> +}
> +
> +static struct target_type inlinecrypt_target = {
> +	.name			= "inlinecrypt",
> +	.version		= {1, 0, 0},
> +	/*
> +	 * Do not set DM_TARGET_PASSES_CRYPTO, since dm-inlinecrypt consumes the
> +	 * crypto capability itself.
> +	 */
> +	.features		= DM_TARGET_ZONED_HM,
> +	.module			= THIS_MODULE,
> +	.ctr			= inlinecrypt_ctr,
> +	.dtr			= inlinecrypt_dtr,
> +	.map			= inlinecrypt_map,
> +	.status			= inlinecrypt_status,
> +	.prepare_ioctl		= inlinecrypt_prepare_ioctl,
> +	.iterate_devices	= inlinecrypt_iterate_devices,
> +	.report_zones		= inlinecrypt_report_zones,
> +	.io_hints		= inlinecrypt_io_hints,
> +};
> +
> +module_dm(inlinecrypt);
> +
> +MODULE_AUTHOR("Eric Biggers <ebiggers@google.com>");
> +MODULE_AUTHOR("Linlin Zhang <linlin.zhang@oss.qualcomm.com>");
> +MODULE_DESCRIPTION(DM_NAME " target for inline encryption");
> +MODULE_LICENSE("GPL");
> -- 
> 2.34.1
> 


^ permalink raw reply

* Re: [PATCH v2] cdrom, scsi: sr: propagate read-only status to block layer via set_disk_ro()
From: Martin K. Petersen @ 2026-04-27  1:29 UTC (permalink / raw)
  To: Daan De Meyer
  Cc: phil, martin.petersen, James.Bottomley, axboe, linux-scsi,
	linux-block, linux-kernel, Daan De Meyer
In-Reply-To: <20260422113206.246267-1-daan@amutable.com>


Daan,

> Fix this by factoring the GET CONFIGURATION probing out of
> cdrom_open_write() into a new exported helper,
> cdrom_probe_write_features(), and having sr call it from sr_probe()
> right after get_capabilities() has populated the MODE SENSE bits.
> register_cdrom() then calls set_disk_ro() based on the full
> write-capability mask (CDC_DVD_RAM | CDC_MRW_W | CDC_RAM | CDC_CD_RW)
> so the block layer reflects the drive's actual write support.

LGTM.

Reviewed-by: Martin K. Petersen <martin.petersen@oracle.com>

-- 
Martin K. Petersen

^ permalink raw reply

* [PATCH v5 0/2] blk-mq: introduce tag starvation observability
From: Aaron Tomlin @ 2026-04-27  2:01 UTC (permalink / raw)
  To: axboe, rostedt, mhiramat, mathieu.desnoyers
  Cc: bvanassche, johannes.thumshirn, kch, dlemoal, ritesh.list,
	loberman, neelx, sean, mproche, chjohnst, linux-block,
	linux-kernel, linux-trace-kernel

Hi Jens, Steve, Masami,

In high-performance storage environments, particularly when utilising RAID
controllers with shared tag sets (BLK_MQ_F_TAG_HCTX_SHARED), severe latency
spikes can occur when fast devices are starved of available tags.
Currently, diagnosing this specific queue contention requires deploying
dynamic kprobes or inferring sleep states, which lacks a simple,
out-of-the-box diagnostic path.

This short series introduces dedicated, low-overhead observability for tag
exhaustion events in the block layer:

  - Patch 1 introduces the "block_rq_tag_wait" tracepoint in the tag
    allocation slow-path to capture precise, event-based starvation.

  - Patch 2 complements this by exposing "wait_on_hw_tag" and
    "wait_on_sched_tag" per-CPU counters via debugfs for quick,
    point-in-time cumulative polling.

Together, these provide storage engineers with zero-configuration
mechanisms to definitively identify shared-tag bottlenecks.

Please let me know your thoughts.


Changes since v4 [1]:
 - Prevented a NULL pointer dereference in the tracepoint fast-assign for
   disk-less request queues by safely checking q->disk before resolving the
   dev_t

 - Fixed a Use-After-Free (UAF) and permanent memory leak by decoupling
   the per-CPU counter allocation from the volatile debugfs lifecycle and
   tying it directly to the core hctx lifecycle (i.e., blk_mq_init_hctx()
   and blk_mq_exit_hctx())

 - Fixed a potential compiler double-fetch bug by wrapping the per-CPU
   pointer evaluations with READ_ONCE() in blk_mq_debugfs_inc_wait_tags()

 - Passed the appropriate gfp_t flags down to the allocation routines to
   maintain the strict GFP_NOIO context

 - Updated kernel-doc descriptions to clarify that the NULL pointer 
   checks guard against memory allocation failures under pressure, rather 
   than initialisation race conditions

Changes since v3 [2]:
 - Transitioned tracking architecture from shared atomic_t variables to
   dynamically allocated per-CPU counters to resolve cache line bouncing
   (Bart Van Assche)

Changes since v2 [3]:
 - Added "Reviewed-by:" and "Tested-by:" tags for patch 1

 - Evaluate is_sched_tag directly within TP_fast_assign (Steven Rostedt)

 - Introduced atomic counters via debugfs 

Changes since v1 [4]:
 - Improved the description of the trace point (Damien Le Moal)

 - Removed the redundant "active requests" (Laurence Oberman)

 - Introduced pool-specific starvation tracking

[1]: https://lore.kernel.org/lkml/20260419023036.1419514-1-atomlin@atomlin.com/
[2]: https://lore.kernel.org/lkml/20260319221956.332770-1-atomlin@atomlin.com/
[3]: https://lore.kernel.org/lkml/20260319015300.287653-1-atomlin@atomlin.com/
[4]: https://lore.kernel.org/lkml/20260317182835.258183-1-atomlin@atomlin.com/


Aaron Tomlin (2):
  blk-mq: add tracepoint block_rq_tag_wait
  blk-mq: expose tag starvation counts via debugfs

 block/blk-mq-debugfs.c       | 109 +++++++++++++++++++++++++++++++++++
 block/blk-mq-debugfs.h       |  19 ++++++
 block/blk-mq-tag.c           |   8 +++
 block/blk-mq.c               |   5 ++
 include/linux/blk-mq.h       |  12 ++++
 include/trace/events/block.h |  43 ++++++++++++++
 6 files changed, 196 insertions(+)

-- 
2.51.0


^ permalink raw reply

* [PATCH v5 1/2] blk-mq: add tracepoint block_rq_tag_wait
From: Aaron Tomlin @ 2026-04-27  2:01 UTC (permalink / raw)
  To: axboe, rostedt, mhiramat, mathieu.desnoyers
  Cc: bvanassche, johannes.thumshirn, kch, dlemoal, ritesh.list,
	loberman, neelx, sean, mproche, chjohnst, linux-block,
	linux-kernel, linux-trace-kernel
In-Reply-To: <20260427020142.358912-1-atomlin@atomlin.com>

In high-performance storage environments, particularly when utilising
RAID controllers with shared tag sets (BLK_MQ_F_TAG_HCTX_SHARED), severe
latency spikes can occur when fast devices (SSDs) are starved of hardware
tags when sharing the same blk_mq_tag_set.

Currently, diagnosing this specific hardware queue contention is
difficult. When a CPU thread exhausts the tag pool, blk_mq_get_tag()
forces the current thread to block uninterruptible via io_schedule().
While this can be inferred via sched:sched_switch or dynamically
traced by attaching a kprobe to blk_mq_mark_tag_wait(), there is no
dedicated, out-of-the-box observability for this event.

This patch introduces the block_rq_tag_wait trace point in the tag
allocation slow-path. It triggers immediately before the thread yields
the CPU, exposing the exact hardware context (hctx) that is starved, the
specific pool experiencing starvation (hardware or software scheduler),
and the total pool depth.

This provides storage engineers and performance monitoring agents
with a zero-configuration, low-overhead mechanism to definitively
identify shared-tag bottlenecks and tune I/O schedulers or cgroup
throttling accordingly.

Reviewed-by: Johannes Thumshirn <johannes.thumshirn@wdc.com>
Reviewed-by: Damien Le Moal <dlemoal@kernel.org>
Reviewed-by: Chaitanya Kulkarni <kch@nvidia.com>
Reviewed-by: Laurence Oberman <loberman@redhat.com>
Tested-by: Laurence Oberman <loberman@redhat.com>
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
 block/blk-mq-tag.c           |  4 ++++
 include/trace/events/block.h | 43 ++++++++++++++++++++++++++++++++++++
 2 files changed, 47 insertions(+)

diff --git a/block/blk-mq-tag.c b/block/blk-mq-tag.c
index 33946cdb5716..66138dd043d4 100644
--- a/block/blk-mq-tag.c
+++ b/block/blk-mq-tag.c
@@ -13,6 +13,7 @@
 #include <linux/kmemleak.h>
 
 #include <linux/delay.h>
+#include <trace/events/block.h>
 #include "blk.h"
 #include "blk-mq.h"
 #include "blk-mq-sched.h"
@@ -187,6 +188,9 @@ unsigned int blk_mq_get_tag(struct blk_mq_alloc_data *data)
 		if (tag != BLK_MQ_NO_TAG)
 			break;
 
+		trace_block_rq_tag_wait(data->q, data->hctx,
+					data->rq_flags & RQF_SCHED_TAGS);
+
 		bt_prev = bt;
 		io_schedule();
 
diff --git a/include/trace/events/block.h b/include/trace/events/block.h
index 6aa79e2d799c..7c1026d1cb35 100644
--- a/include/trace/events/block.h
+++ b/include/trace/events/block.h
@@ -226,6 +226,49 @@ DECLARE_EVENT_CLASS(block_rq,
 		  IOPRIO_PRIO_LEVEL(__entry->ioprio), __entry->comm)
 );
 
+/**
+ * block_rq_tag_wait - triggered when a request is starved of a tag
+ * @q: request queue of the target device
+ * @hctx: hardware context of the request experiencing starvation
+ * @is_sched_tag: indicates whether the starved pool is the software scheduler
+ *
+ * Called immediately before the submitting context is forced to block due
+ * to the exhaustion of available tags (i.e., physical hardware driver tags
+ * or software scheduler tags). This trace point indicates that the context
+ * will be placed into an uninterruptible state via io_schedule() until an
+ * active request completes and relinquishes its assigned tag.
+ */
+TRACE_EVENT(block_rq_tag_wait,
+
+	TP_PROTO(struct request_queue *q, struct blk_mq_hw_ctx *hctx, bool is_sched_tag),
+
+	TP_ARGS(q, hctx, is_sched_tag),
+
+	TP_STRUCT__entry(
+		__field( dev_t,		dev			)
+		__field( u32,		hctx_id			)
+		__field( u32,		nr_tags			)
+		__field( bool,		is_sched_tag		)
+	),
+
+	TP_fast_assign(
+		__entry->dev		= q->disk ? disk_devt(q->disk);
+		__entry->hctx_id	= hctx->queue_num;
+		__entry->is_sched_tag	= is_sched_tag;
+
+		if (is_sched_tag)
+			__entry->nr_tags = hctx->sched_tags->nr_tags;
+		else
+			__entry->nr_tags = hctx->tags->nr_tags;
+	),
+
+	TP_printk("%d,%d hctx=%u starved on %s tags (depth=%u)",
+		  MAJOR(__entry->dev), MINOR(__entry->dev),
+		  __entry->hctx_id,
+		  __entry->is_sched_tag ? "scheduler" : "hardware",
+		  __entry->nr_tags)
+);
+
 /**
  * block_rq_insert - insert block operation request into queue
  * @rq: block IO operation request
-- 
2.51.0


^ permalink raw reply related

* [PATCH v5 2/2] blk-mq: expose tag starvation counts via debugfs
From: Aaron Tomlin @ 2026-04-27  2:01 UTC (permalink / raw)
  To: axboe, rostedt, mhiramat, mathieu.desnoyers
  Cc: bvanassche, johannes.thumshirn, kch, dlemoal, ritesh.list,
	loberman, neelx, sean, mproche, chjohnst, linux-block,
	linux-kernel, linux-trace-kernel
In-Reply-To: <20260427020142.358912-1-atomlin@atomlin.com>

In high-performance storage environments, particularly when utilising
RAID controllers with shared tag sets (BLK_MQ_F_TAG_HCTX_SHARED), severe
latency spikes can occur when fast devices are starved of available
tags.

This patch introduces two new debugfs attributes for each block
hardware queue:
  - /sys/kernel/debug/block/[device]/hctxN/wait_on_hw_tag
  - /sys/kernel/debug/block/[device]/hctxN/wait_on_sched_tag

These files expose atomic counters that increment each time a submitting
context is forced into an uninterruptible sleep via io_schedule() due to
the complete exhaustion of physical driver tags or software scheduler
tags, respectively.

To ensure negligible performance overhead even in production
environments where CONFIG_BLK_DEBUG_FS is actively enabled, this
tracking logic utilises dynamically allocated per-CPU counters. When
this configuration is disabled, the tracking logic compiles down to a
safe no-op.

Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
 block/blk-mq-debugfs.c | 109 +++++++++++++++++++++++++++++++++++++++++
 block/blk-mq-debugfs.h |  19 +++++++
 block/blk-mq-tag.c     |   4 ++
 block/blk-mq.c         |   5 ++
 include/linux/blk-mq.h |  12 +++++
 5 files changed, 149 insertions(+)

diff --git a/block/blk-mq-debugfs.c b/block/blk-mq-debugfs.c
index 047ec887456b..1a993bcea5c9 100644
--- a/block/blk-mq-debugfs.c
+++ b/block/blk-mq-debugfs.c
@@ -7,6 +7,7 @@
 #include <linux/blkdev.h>
 #include <linux/build_bug.h>
 #include <linux/debugfs.h>
+#include <linux/percpu.h>
 
 #include "blk.h"
 #include "blk-mq.h"
@@ -484,6 +485,54 @@ static int hctx_dispatch_busy_show(void *data, struct seq_file *m)
 	return 0;
 }
 
+/**
+ * hctx_wait_on_hw_tag_show - display hardware tag starvation count
+ * @data: generic pointer to the associated hardware context (hctx)
+ * @m: seq_file pointer for debugfs output formatting
+ *
+ * Prints the cumulative number of times a submitting context was forced
+ * to block due to the exhaustion of physical hardware driver tags.
+ *
+ * Return: 0 on success.
+ */
+static int hctx_wait_on_hw_tag_show(void *data, struct seq_file *m)
+{
+	struct blk_mq_hw_ctx *hctx = data;
+	unsigned long count = 0;
+	int cpu;
+
+	if (hctx->wait_on_hw_tag) {
+		for_each_possible_cpu(cpu)
+			count += *per_cpu_ptr(hctx->wait_on_hw_tag, cpu);
+	}
+	seq_printf(m, "%lu\n", count);
+	return 0;
+}
+
+/**
+ * hctx_wait_on_sched_tag_show - display scheduler tag starvation count
+ * @data: generic pointer to the associated hardware context (hctx)
+ * @m: seq_file pointer for debugfs output formatting
+ *
+ * Prints the cumulative number of times a submitting context was forced
+ * to block due to the exhaustion of software scheduler tags.
+ *
+ * Return: 0 on success.
+ */
+static int hctx_wait_on_sched_tag_show(void *data, struct seq_file *m)
+{
+	struct blk_mq_hw_ctx *hctx = data;
+	unsigned long count = 0;
+	int cpu;
+
+	if (hctx->wait_on_sched_tag) {
+		for_each_possible_cpu(cpu)
+			count += *per_cpu_ptr(hctx->wait_on_sched_tag, cpu);
+	}
+	seq_printf(m, "%lu\n", count);
+	return 0;
+}
+
 #define CTX_RQ_SEQ_OPS(name, type)					\
 static void *ctx_##name##_rq_list_start(struct seq_file *m, loff_t *pos) \
 	__acquires(&ctx->lock)						\
@@ -599,6 +648,8 @@ static const struct blk_mq_debugfs_attr blk_mq_debugfs_hctx_attrs[] = {
 	{"active", 0400, hctx_active_show},
 	{"dispatch_busy", 0400, hctx_dispatch_busy_show},
 	{"type", 0400, hctx_type_show},
+	{"wait_on_hw_tag", 0400, hctx_wait_on_hw_tag_show},
+	{"wait_on_sched_tag", 0400, hctx_wait_on_sched_tag_show},
 	{},
 };
 
@@ -815,3 +866,61 @@ void blk_mq_debugfs_unregister_sched_hctx(struct blk_mq_hw_ctx *hctx)
 	debugfs_remove_recursive(hctx->sched_debugfs_dir);
 	hctx->sched_debugfs_dir = NULL;
 }
+
+/**
+ * blk_mq_debugfs_alloc_hctx_stats - Allocate per-cpu starvation statistics
+ * @hctx: hardware context associated with the tag allocation
+ * @gfp: memory allocation flags
+ *
+ * Allocates the per-cpu memory for tracking hardware and scheduler tag
+ * starvation.
+ */
+void blk_mq_debugfs_alloc_hctx_stats(struct blk_mq_hw_ctx *hctx, gfp_t gfp)
+{
+	if (!hctx->wait_on_hw_tag)
+		hctx->wait_on_hw_tag = alloc_percpu_gfp(unsigned long,
+							gfp);
+	if (!hctx->wait_on_sched_tag)
+		hctx->wait_on_sched_tag = alloc_percpu_gfp(unsigned long,
+							   gfp);
+}
+
+/**
+ * blk_mq_debugfs_free_hctx_stats - Free per-cpu starvation statistics
+ * @hctx: hardware context associated with the tag allocation
+ *
+ * Frees the per-cpu memory used for tracking hardware and scheduler tag
+ * starvation. This must only be called during hardware queue teardown when
+ * the queue is safely frozen and no active I/O submissions can race to
+ * increment the statistics.
+ */
+void blk_mq_debugfs_free_hctx_stats(struct blk_mq_hw_ctx *hctx)
+{
+	free_percpu(hctx->wait_on_hw_tag);
+	hctx->wait_on_hw_tag = NULL;
+	free_percpu(hctx->wait_on_sched_tag);
+	hctx->wait_on_sched_tag = NULL;
+}
+
+/**
+ * blk_mq_debugfs_inc_wait_tags - increment the tag starvation counters
+ * @hctx: hardware context associated with the tag allocation
+ * @is_sched: true if the starved pool is the software scheduler
+ *
+ * Evaluates the exhausted tag pool and safely increments the appropriate
+ * per-cpu debugfs starvation counter.
+ *
+ * Note: The per-cpu pointers are explicitly checked to prevent a NULL
+ * pointer dereference in the event that the system was under heavy memory
+ * pressure and the initial per-cpu allocation failed.
+ */
+void blk_mq_debugfs_inc_wait_tags(struct blk_mq_hw_ctx *hctx,
+				  bool is_sched)
+{
+	unsigned long __percpu *tags = is_sched ?
+			READ_ONCE(hctx->wait_on_sched_tag) :
+			READ_ONCE(hctx->wait_on_hw_tag);
+
+	if (likely(tags))
+		this_cpu_inc(*tags);
+}
diff --git a/block/blk-mq-debugfs.h b/block/blk-mq-debugfs.h
index 49bb1aaa83dc..7a7c0f376a2b 100644
--- a/block/blk-mq-debugfs.h
+++ b/block/blk-mq-debugfs.h
@@ -17,6 +17,8 @@ struct blk_mq_debugfs_attr {
 	const struct seq_operations *seq_ops;
 };
 
+void blk_mq_debugfs_inc_wait_tags(struct blk_mq_hw_ctx *hctx,
+				  bool is_sched);
 int __blk_mq_debugfs_rq_show(struct seq_file *m, struct request *rq);
 int blk_mq_debugfs_rq_show(struct seq_file *m, void *v);
 
@@ -26,6 +28,9 @@ void blk_mq_debugfs_register_hctx(struct request_queue *q,
 void blk_mq_debugfs_unregister_hctx(struct blk_mq_hw_ctx *hctx);
 void blk_mq_debugfs_register_hctxs(struct request_queue *q);
 void blk_mq_debugfs_unregister_hctxs(struct request_queue *q);
+void blk_mq_debugfs_alloc_hctx_stats(struct blk_mq_hw_ctx *hctx,
+				     gfp_t gfp);
+void blk_mq_debugfs_free_hctx_stats(struct blk_mq_hw_ctx *hctx);
 
 void blk_mq_debugfs_register_sched(struct request_queue *q);
 void blk_mq_debugfs_unregister_sched(struct request_queue *q);
@@ -35,6 +40,11 @@ void blk_mq_debugfs_unregister_sched_hctx(struct blk_mq_hw_ctx *hctx);
 
 void blk_mq_debugfs_register_rq_qos(struct request_queue *q);
 #else
+static inline void blk_mq_debugfs_inc_wait_tags(struct blk_mq_hw_ctx *hctx,
+						bool is_sched)
+{
+}
+
 static inline void blk_mq_debugfs_register(struct request_queue *q)
 {
 }
@@ -56,6 +66,15 @@ static inline void blk_mq_debugfs_unregister_hctxs(struct request_queue *q)
 {
 }
 
+static inline void blk_mq_debugfs_alloc_hctx_stats(struct blk_mq_hw_ctx *hctx,
+						   gfp_t gfp)
+{
+}
+
+static inline void blk_mq_debugfs_free_hctx_stats(struct blk_mq_hw_ctx *hctx)
+{
+}
+
 static inline void blk_mq_debugfs_register_sched(struct request_queue *q)
 {
 }
diff --git a/block/blk-mq-tag.c b/block/blk-mq-tag.c
index 66138dd043d4..3cc6a97a87a0 100644
--- a/block/blk-mq-tag.c
+++ b/block/blk-mq-tag.c
@@ -17,6 +17,7 @@
 #include "blk.h"
 #include "blk-mq.h"
 #include "blk-mq-sched.h"
+#include "blk-mq-debugfs.h"
 
 /*
  * Recalculate wakeup batch when tag is shared by hctx.
@@ -191,6 +192,9 @@ unsigned int blk_mq_get_tag(struct blk_mq_alloc_data *data)
 		trace_block_rq_tag_wait(data->q, data->hctx,
 					data->rq_flags & RQF_SCHED_TAGS);
 
+		blk_mq_debugfs_inc_wait_tags(data->hctx,
+					     data->rq_flags & RQF_SCHED_TAGS);
+
 		bt_prev = bt;
 		io_schedule();
 
diff --git a/block/blk-mq.c b/block/blk-mq.c
index 4c5c16cce4f8..cd52bf6f82ce 100644
--- a/block/blk-mq.c
+++ b/block/blk-mq.c
@@ -3991,6 +3991,8 @@ static void blk_mq_exit_hctx(struct request_queue *q,
 			blk_free_flush_queue_callback);
 	hctx->fq = NULL;
 
+	blk_mq_debugfs_free_hctx_stats(hctx);
+
 	spin_lock(&q->unused_hctx_lock);
 	list_add(&hctx->hctx_list, &q->unused_hctx_list);
 	spin_unlock(&q->unused_hctx_lock);
@@ -4016,6 +4018,8 @@ static int blk_mq_init_hctx(struct request_queue *q,
 {
 	gfp_t gfp = GFP_NOIO | __GFP_NOWARN | __GFP_NORETRY;
 
+	blk_mq_debugfs_alloc_hctx_stats(hctx, gfp);
+
 	hctx->fq = blk_alloc_flush_queue(hctx->numa_node, set->cmd_size, gfp);
 	if (!hctx->fq)
 		goto fail;
@@ -4041,6 +4045,7 @@ static int blk_mq_init_hctx(struct request_queue *q,
 	blk_free_flush_queue(hctx->fq);
 	hctx->fq = NULL;
  fail:
+	blk_mq_debugfs_free_hctx_stats(hctx);
 	return -1;
 }
 
diff --git a/include/linux/blk-mq.h b/include/linux/blk-mq.h
index 18a2388ba581..41d61488d683 100644
--- a/include/linux/blk-mq.h
+++ b/include/linux/blk-mq.h
@@ -453,6 +453,18 @@ struct blk_mq_hw_ctx {
 	struct dentry		*debugfs_dir;
 	/** @sched_debugfs_dir:	debugfs directory for the scheduler. */
 	struct dentry		*sched_debugfs_dir;
+	/**
+	 * @wait_on_hw_tag: Cumulative per-cpu counter incremented each
+	 * time a submitting context is forced to block due to physical
+	 * hardware tag exhaustion.
+	 */
+	unsigned long __percpu	*wait_on_hw_tag;
+	/**
+	 * @wait_on_sched_tag: Cumulative per-cpu counter incremented each
+	 * time a submitting context is forced to block due to software
+	 * scheduler tag exhaustion.
+	 */
+	unsigned long __percpu	*wait_on_sched_tag;
 #endif
 
 	/**
-- 
2.51.0


^ permalink raw reply related

* Re: [RFC PATCH v2 0/4] mm/zsmalloc: reduce zs_free() latency on swap release path
From: Wenchao Hao @ 2026-04-27  3:10 UTC (permalink / raw)
  To: Xueyuan Chen
  Cc: akpm, chengming.zhou, axboe, hannes, minchan, nphamcs,
	senozhatsky, yosry, linux-block, linux-kernel, linux-mm, baohua,
	haowenchao
In-Reply-To: <20260426085017.166935-1-xueyuan.chen21@gmail.com>

On Sun, Apr 26, 2026 at 4:50 PM Xueyuan Chen <xueyuan.chen21@gmail.com> wrote:
>
>
> On Sun, Apr 26, 2026 at 12:13:02PM +0800, Wenchao Hao wrote:
>
> [...]
>
> >2. Per-cpu deferred free with lockless buffer swap
> >
> >Defer zs_free() to per-cpu dynamically-allocated buffers (~2048 entries).
> >Enqueue: one array write + WRITE_ONCE under preempt_disable — no lock,
> >no atomic. When buffers full, schedule a drain worker; overflow falls back
> >to sync zs_free().
> >
> >Drain: allocate a fresh buffer, swap it in, reset count. Since
> >the producer stops writing at count==SIZE, the handoff is
> >race-free without any lock.
> >
> >Pseudo-code:
> >
> >    /* enqueue - hot path */
> >    def = get_cpu_ptr(pool->deferred);
> >    if (def->count < SIZE) {
> >        def->handles[def->count] = handle;
> >        WRITE_ONCE(def->count, def->count + 1);
> >        if (def->count == SIZE)
> >            schedule_work(&pool->drain_work);
> >    } else {
> >        zs_free(pool, handle);  /* fallback */
> >    }
> >    put_cpu_ptr(pool->deferred);
> >
> >    /* drain - worker */
> >    for_each_possible_cpu(cpu) {
> >        def = per_cpu_ptr(pool->deferred, cpu);
> >        if (def->count < SIZE)
> >            continue;
> >        new_buf = kvmalloc_array(SIZE, sizeof(long));
> >        old_buf = def->handles;
> >        old_count = def->count;
> >        def->handles = new_buf;
> >        WRITE_ONCE(def->count, 0);
> >        /* now drain old_buf[0..old_count-1] */
> >        ...
> >        kvfree(old_buf);
> >    }
> >
>
> Hi Wenchao,
>
> I suspect there is a memory ordering issue here:
>
> def->handles = new_buf;
> WRITE_ONCE(def->count, 0);
>
> Since there are no explicit memory barriers, we cannot guarantee the
> order of these stores. If def->count is cleared to 0 first, an enqueue
> might end up operating on the old_buf.
>
> This race condition is more likely to be triggered when the size is
> smaller. Perhaps we should consider using smp_store_release() to enforce
> the ordering?
>

Hi Xueyuan,

Good catch! You are right — there is a memory ordering issue between
the handles pointer swap and the count reset.

I'll fix this in the next version by using smp_store_release() /
smp_load_acquire() pairs:

    /* drain - worker */
    def->handles = new_buf;
    smp_store_release(&def->count, 0);

    /* enqueue - producer */
    count = smp_load_acquire(&def->count);
    if (count < SIZE) {
        def->handles[count] = handle;
        smp_store_release(&def->count, count + 1);
    }

This ensures the producer always observes the new handles pointer
before it sees count reset to 0. Will include this fix when posting
the formal patch series.

Thanks,
Wenchao

> Thanks
> Xueyuan

^ permalink raw reply

* [PATCH v4] block: bio-integrity: Fix null-ptr-deref in bio_integrity_map_user()
From: Sungwoo Kim @ 2026-04-27  4:09 UTC (permalink / raw)
  To: Jens Axboe, Keith Busch
  Cc: Sungwoo Kim, Chao Shi, Weidong Zhu, Dave Tian, linux-block,
	linux-kernel

pin_user_pages_fast() can partially succeed and return the number of
pages that were actually pinned. However, the bio_integrity_map_user()
does not handle this partial pinning. This leads to a general protection
fault since bvec_from_pages() dereferences an unpinned page address,
which is 0.

To fix this, add a check to verify that all requested memory is pinned.
If partial pinning occurs, unpin the memory and return -EFAULT.

Reproducer in blktest: https://github.com/linux-blktests/blktests/pull/244

Kernel Oops:

Oops: general protection fault, probably for non-canonical address 0xdffffc0000000001: 0000 [#1] SMP KASAN NOPTI
KASAN: null-ptr-deref in range [0x0000000000000008-0x000000000000000f]
CPU: 0 UID: 0 PID: 1061 Comm: nvme-passthroug Not tainted 7.0.0-11783-g90957f9314e8-dirty #16 PREEMPT(lazy)
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.17.0-0-gb52ca86e094d-prebuilt.qemu.org 04/01/2014
RIP: 0010:bio_integrity_map_user.cold+0x1b0/0x9d6

Fixes: 492c5d455969 ("block: bio-integrity: directly map user buffers")
Acked-by: Chao Shi <cshi008@fiu.edu>
Acked-by: Weidong Zhu <weizhu@fiu.edu>
Acked-by: Dave Tian <daveti@purdue.edu>
Signed-off-by: Sungwoo Kim <iam@sung-woo.kim>
---
V3: https://lore.kernel.org/linux-block/20260420020327.1667156-3-iam@sung-woo.kim/

V3->V4
- Addressed a sashiko's comments [1], if it makes sense.
- V3 wrongly assumed that iov_iter_extract_pages() always pins user
  memory.
- V3 insufficiently handled the return value range.

[1] https://sashiko.dev/#/patchset/20260420020327.1667156-3-iam%40sung-woo.kim

 block/bio-integrity.c | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/block/bio-integrity.c b/block/bio-integrity.c
index e54c6e06e1cb..766cc2d1d51c 100644
--- a/block/bio-integrity.c
+++ b/block/bio-integrity.c
@@ -403,6 +403,23 @@ int bio_integrity_map_user(struct bio *bio, struct iov_iter *iter)
 	if (unlikely(ret < 0))
 		goto free_bvec;
 
+	/* Handle partial pinning. This can happen when pin_user_pages_fast()
+	 * returns fewer pages than requested
+	 */
+	if (user_backed_iter(iter) && unlikely(ret != bytes)) {
+		if (0 < ret) {
+			int npinned = DIV_ROUND_UP(offset + ret, PAGE_SIZE);
+			int i;
+
+			for (i = 0; i < npinned; i++)
+				unpin_user_page(pages[i]);
+		}
+		if (pages != stack_pages)
+				kvfree(pages);
+		ret = -EFAULT;
+		goto free_bvec;
+	}
+
 	nr_bvecs = bvec_from_pages(bvec, pages, nr_vecs, bytes, offset,
 				   &is_p2p);
 	if (pages != stack_pages)
-- 
2.47.3


^ permalink raw reply related


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox