From 4b92d10e679763197f7c9d7c8b6343316000bff4 Mon Sep 17 00:00:00 2001 From: Maurizio Lombardi Date: Mon, 26 Jan 2026 15:12:51 +0100 Subject: [PATCH 01/36] nvme: add definitions for cancel command Add new definitions needed to support TP4097a. Signed-off-by: John Meneghini Signed-off-by: Maurizio Lombardi --- drivers/nvme/host/constants.c | 1 + drivers/nvme/host/core.c | 1 + include/linux/nvme.h | 22 ++++++++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/drivers/nvme/host/constants.c b/drivers/nvme/host/constants.c index dc90df9e13a213..583d12f334d221 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_cancel] = "Cancel", [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 0d623476e36f87..2593321584200b 100644 --- a/drivers/nvme/host/core.c +++ b/drivers/nvme/host/core.c @@ -5371,6 +5371,7 @@ static inline void _nvme_check_size(void) BUILD_BUG_ON(sizeof(struct nvme_dsm_cmd) != 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_cancel_cmd) != 64); BUILD_BUG_ON(sizeof(struct nvme_get_log_page_command) != 64); BUILD_BUG_ON(sizeof(struct nvme_command) != 64); BUILD_BUG_ON(sizeof(struct nvme_id_ctrl) != NVME_IDENTIFY_DATA_SIZE); diff --git a/include/linux/nvme.h b/include/linux/nvme.h index 041f30931a908a..524d864c8ac81e 100644 --- a/include/linux/nvme.h +++ b/include/linux/nvme.h @@ -28,6 +28,9 @@ /* Special NSSR value, 'NVMe' */ #define NVME_SUBSYS_RESET 0x4E564D65 +/* Maximum number of reserved commands for Cancel */ +#define NVME_RSV_CANCEL_MAX 2 + enum nvme_subsys_type { /* Referral to another discovery type target subsystem */ NVME_NQN_DISC = 1, @@ -967,6 +970,7 @@ enum nvme_opcode { nvme_cmd_resv_acquire = 0x11, nvme_cmd_io_mgmt_recv = 0x12, nvme_cmd_resv_release = 0x15, + nvme_cmd_cancel = 0x18, nvme_cmd_zone_mgmt_send = 0x79, nvme_cmd_zone_mgmt_recv = 0x7a, nvme_cmd_zone_append = 0x7d, @@ -1538,6 +1542,22 @@ struct nvme_abort_cmd { __u32 rsvd11[5]; }; +struct nvme_cancel_cmd { + __u8 opcode; + __u8 flags; + __u16 command_id; + __le32 nsid; + __u32 rsvd1[8]; + __le16 sqid; + __u16 cid; + __u8 action; + __u8 rsvd11[3]; + __u32 rsvd12[4]; +}; + +#define NVME_CANCEL_ACTION_MUL_CMD 1 +#define NVME_CANCEL_ACTION_SINGLE_CMD 0 + struct nvme_download_firmware { __u8 opcode; __u8 flags; @@ -2005,6 +2025,7 @@ struct nvme_command { struct nvme_zone_mgmt_send_cmd zms; struct nvme_zone_mgmt_recv_cmd zmr; struct nvme_abort_cmd abort; + struct nvme_cancel_cmd cancel; struct nvme_get_log_page_command get_log_page; struct nvmf_common_command fabrics; struct nvmf_connect_command connect; @@ -2188,6 +2209,7 @@ enum { NVME_SC_INVALID_PI = 0x181, NVME_SC_READ_ONLY = 0x182, NVME_SC_CMD_SIZE_LIM_EXCEEDED = 0x183, + NVME_SC_INVALID_CID = 0x184, /* * I/O Command Set Specific - Fabrics commands: From 71b9b1b7633d67fc75a44d6561781bf15197cd8e Mon Sep 17 00:00:00 2001 From: Maurizio Lombardi Date: Fri, 7 Feb 2025 15:37:30 +0100 Subject: [PATCH 02/36] nvme-core: reserve tags for cancel commands If the controller supports cancel commands, reserve NVME_RSV_CANCEL_MAX tags for them. Signed-off-by: Maurizio Lombardi --- drivers/nvme/host/core.c | 15 ++++++++++++++- drivers/nvme/host/nvme.h | 1 + 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c index 2593321584200b..1900d5b2dea778 100644 --- a/drivers/nvme/host/core.c +++ b/drivers/nvme/host/core.c @@ -1232,6 +1232,14 @@ u32 nvme_command_effects(struct nvme_ctrl *ctrl, struct nvme_ns *ns, u8 opcode) } EXPORT_SYMBOL_NS_GPL(nvme_command_effects, "NVME_TARGET_PASSTHRU"); +bool nvme_io_command_supported(struct nvme_ctrl *ctrl, u8 opcode) +{ + u32 effects = le32_to_cpu(ctrl->effects->iocs[opcode]); + + return effects & NVME_CMD_EFFECTS_CSUPP; +} +EXPORT_SYMBOL_GPL(nvme_io_command_supported); + u32 nvme_passthru_start(struct nvme_ctrl *ctrl, struct nvme_ns *ns, u8 opcode) { u32 effects = nvme_command_effects(ctrl, ns, opcode); @@ -4951,9 +4959,14 @@ int nvme_alloc_io_tag_set(struct nvme_ctrl *ctrl, struct blk_mq_tag_set *set, */ if (ctrl->quirks & NVME_QUIRK_SHARED_TAGS) set->reserved_tags = NVME_AQ_DEPTH; - else if (ctrl->ops->flags & NVME_F_FABRICS) + else if (ctrl->ops->flags & NVME_F_FABRICS) { /* Reserved for fabric connect */ set->reserved_tags = 1; + if (nvme_io_command_supported(ctrl, nvme_cmd_cancel)) { + /* Reserved for cancel commands */ + set->reserved_tags += NVME_RSV_CANCEL_MAX; + } + } set->numa_node = ctrl->numa_node; if (ctrl->ops->flags & NVME_F_BLOCKING) set->flags |= BLK_MQ_F_BLOCKING; diff --git a/drivers/nvme/host/nvme.h b/drivers/nvme/host/nvme.h index ccd5e05dac98f0..f25a658053a142 100644 --- a/drivers/nvme/host/nvme.h +++ b/drivers/nvme/host/nvme.h @@ -1272,6 +1272,7 @@ static inline void nvme_auth_revoke_tls_key(struct nvme_ctrl *ctrl) {}; u32 nvme_command_effects(struct nvme_ctrl *ctrl, struct nvme_ns *ns, u8 opcode); +bool nvme_io_command_supported(struct nvme_ctrl *ctrl, u8 opcode); u32 nvme_passthru_start(struct nvme_ctrl *ctrl, struct nvme_ns *ns, u8 opcode); int nvme_execute_rq(struct request *rq, bool at_head); void nvme_passthru_end(struct nvme_ctrl *ctrl, struct nvme_ns *ns, u32 effects, From 64e508c6c26396e7edf161cdcaa9d8785ce26475 Mon Sep 17 00:00:00 2001 From: Maurizio Lombardi Date: Mon, 26 Jan 2026 15:13:48 +0100 Subject: [PATCH 03/36] nvme-core: add a function to submit a cancel command Add a function to send a cancel command to abort the specified request. When the cancel command completes, the host driver will print the number of deferred and immediate aborts performed by the target. Signed-off-by: Maurizio Lombardi --- drivers/nvme/host/core.c | 63 ++++++++++++++++++++++++++++++++++++++++ drivers/nvme/host/nvme.h | 2 ++ 2 files changed, 65 insertions(+) diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c index 1900d5b2dea778..5b64bb04fc8a5f 100644 --- a/drivers/nvme/host/core.c +++ b/drivers/nvme/host/core.c @@ -3351,6 +3351,69 @@ int nvme_get_log(struct nvme_ctrl *ctrl, u32 nsid, u8 log_page, u8 lsp, u8 csi, offset, 0); } +static enum rq_end_io_ret nvme_cancel_endio(struct request *req, + blk_status_t error, + const struct io_comp_batch *iob) +{ + struct nvme_ctrl *ctrl = req->end_io_data; + u32 result; + u16 imm_abrts, def_abrts; + + if (!error) { + result = le32_to_cpu(nvme_req(req)->result.u32); + def_abrts = upper_16_bits(result); + imm_abrts = lower_16_bits(result); + + dev_warn(ctrl->device, + "Cancel status: 0x0 imm abrts = %u def abrts = %u", + imm_abrts, def_abrts); + } else { + dev_warn(ctrl->device, "Cancel status: 0x%x", + nvme_req(req)->status); + } + + blk_mq_free_request(req); + return RQ_END_IO_NONE; +} + +int nvme_submit_cancel_req(struct nvme_ctrl *ctrl, struct request *rq, + unsigned int sqid, int action) +{ + struct nvme_command c = { }; + struct request *cancel_req; + + if (sqid == 0) + return -EINVAL; + + c.cancel.opcode = nvme_cmd_cancel; + c.cancel.sqid = cpu_to_le32(sqid); + c.cancel.nsid = NVME_NSID_ALL; + c.cancel.action = action; + if (action == NVME_CANCEL_ACTION_SINGLE_CMD) + c.cancel.cid = nvme_cid(rq); + else + c.cancel.cid = 0xFFFF; + + cancel_req = blk_mq_alloc_request_hctx(rq->q, nvme_req_op(&c), + BLK_MQ_REQ_NOWAIT | + BLK_MQ_REQ_RESERVED, + sqid - 1); + if (IS_ERR(cancel_req)) { + dev_warn(ctrl->device, "%s: Could not allocate the Cancel " + "command", __func__); + return PTR_ERR(cancel_req); + } + + nvme_init_request(cancel_req, &c); + cancel_req->end_io = nvme_cancel_endio; + cancel_req->end_io_data = ctrl; + + blk_execute_rq_nowait(cancel_req, false); + + return 0; +} +EXPORT_SYMBOL_GPL(nvme_submit_cancel_req); + static int nvme_get_effects_log(struct nvme_ctrl *ctrl, u8 csi, struct nvme_effects_log **log) { diff --git a/drivers/nvme/host/nvme.h b/drivers/nvme/host/nvme.h index f25a658053a142..d79dce27367c9e 100644 --- a/drivers/nvme/host/nvme.h +++ b/drivers/nvme/host/nvme.h @@ -982,6 +982,8 @@ int nvme_delete_ctrl(struct nvme_ctrl *ctrl); void nvme_queue_scan(struct nvme_ctrl *ctrl); int nvme_get_log(struct nvme_ctrl *ctrl, u32 nsid, u8 log_page, u8 lsp, u8 csi, void *log, size_t size, u64 offset); +int nvme_submit_cancel_req(struct nvme_ctrl *ctrl, struct request *rq, + unsigned int sqid, int action); bool nvme_tryget_ns_head(struct nvme_ns_head *head); void nvme_put_ns_head(struct nvme_ns_head *head); int nvme_cdev_add(struct cdev *cdev, struct device *cdev_device, From 6ad66ac0ca66ec829b6e64f9d4d6c380f8830687 Mon Sep 17 00:00:00 2001 From: Maurizio Lombardi Date: Tue, 14 Apr 2026 15:06:24 +0200 Subject: [PATCH 04/36] nvme: move abort command submission to core Currently, the logic for allocating, initializing, and submitting an NVMe abort command is embedded directly within the PCI-specific timeout handler. This patch extracts the abort request generation into a new generic helper function, `nvme_submit_abort_req()`, located in the NVMe core, and exports it. The PCI driver's timeout handler is then updated to use this new helper. This will make it easier for other NVMe transports (RDMA and TCP) to submit abort requests in a transport-agnostic way without duplicating the allocation and completion logic. Signed-off-by: Maurizio Lombardi --- drivers/nvme/host/core.c | 43 ++++++++++++++++++++++++++++++++++++++++ drivers/nvme/host/nvme.h | 2 ++ drivers/nvme/host/pci.c | 32 ++++-------------------------- 3 files changed, 49 insertions(+), 28 deletions(-) diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c index 5b64bb04fc8a5f..7338d998646d79 100644 --- a/drivers/nvme/host/core.c +++ b/drivers/nvme/host/core.c @@ -3414,6 +3414,49 @@ int nvme_submit_cancel_req(struct nvme_ctrl *ctrl, struct request *rq, } EXPORT_SYMBOL_GPL(nvme_submit_cancel_req); +static enum rq_end_io_ret abort_endio(struct request *req, blk_status_t error, + const struct io_comp_batch *iob) +{ + struct nvme_ctrl *ctrl = req->end_io_data; + + dev_warn(ctrl->device, "Abort status: 0x%x", nvme_req(req)->status); + atomic_inc(&ctrl->abort_limit); + blk_mq_free_request(req); + return RQ_END_IO_NONE; +} + +int nvme_submit_abort_req(struct nvme_ctrl *ctrl, struct request *rq, + unsigned int sqid) +{ + struct nvme_command c = { }; + struct request *abort_req; + + c.abort.opcode = nvme_admin_abort_cmd; + c.abort.cid = nvme_cid(rq); + c.abort.sqid = sqid; + + if (atomic_dec_return(&ctrl->abort_limit) < 0) { + atomic_inc(&ctrl->abort_limit); + return -EBUSY; + } + + abort_req = blk_mq_alloc_request(ctrl->admin_q, nvme_req_op(&c), + BLK_MQ_REQ_NOWAIT); + if (IS_ERR(abort_req)) { + atomic_inc(&ctrl->abort_limit); + return PTR_ERR(abort_req); + } + + nvme_init_request(abort_req, &c); + + abort_req->end_io = abort_endio; + abort_req->end_io_data = ctrl; + + blk_execute_rq_nowait(abort_req, false); + return 0; +} +EXPORT_SYMBOL_GPL(nvme_submit_abort_req); + static int nvme_get_effects_log(struct nvme_ctrl *ctrl, u8 csi, struct nvme_effects_log **log) { diff --git a/drivers/nvme/host/nvme.h b/drivers/nvme/host/nvme.h index d79dce27367c9e..1a2fb0533d9b46 100644 --- a/drivers/nvme/host/nvme.h +++ b/drivers/nvme/host/nvme.h @@ -984,6 +984,8 @@ int nvme_get_log(struct nvme_ctrl *ctrl, u32 nsid, u8 log_page, u8 lsp, u8 csi, void *log, size_t size, u64 offset); int nvme_submit_cancel_req(struct nvme_ctrl *ctrl, struct request *rq, unsigned int sqid, int action); +int nvme_submit_abort_req(struct nvme_ctrl *ctrl, struct request *rq, + unsigned int sqid); bool nvme_tryget_ns_head(struct nvme_ns_head *head); void nvme_put_ns_head(struct nvme_ns_head *head); int nvme_cdev_add(struct cdev *cdev, struct device *cdev_device, diff --git a/drivers/nvme/host/pci.c b/drivers/nvme/host/pci.c index c693308c6dd66e..95d61d514e258a 100644 --- a/drivers/nvme/host/pci.c +++ b/drivers/nvme/host/pci.c @@ -1776,18 +1776,6 @@ static int adapter_delete_sq(struct nvme_dev *dev, u16 sqid) return adapter_delete_queue(dev, nvme_admin_delete_sq, sqid); } -static enum rq_end_io_ret abort_endio(struct request *req, blk_status_t error, - const struct io_comp_batch *iob) -{ - struct nvme_queue *nvmeq = req->mq_hctx->driver_data; - - dev_warn(nvmeq->dev->ctrl.device, - "Abort status: 0x%x", nvme_req(req)->status); - atomic_inc(&nvmeq->dev->ctrl.abort_limit); - blk_mq_free_request(req); - return RQ_END_IO_NONE; -} - static bool nvme_should_reset(struct nvme_dev *dev, u32 csts) { /* If true, indicates loss of adapter communication, possibly by a @@ -1844,11 +1832,10 @@ static enum blk_eh_timer_return nvme_timeout(struct request *req) struct nvme_iod *iod = blk_mq_rq_to_pdu(req); struct nvme_queue *nvmeq = req->mq_hctx->driver_data; struct nvme_dev *dev = nvmeq->dev; - struct request *abort_req; - struct nvme_command cmd = { }; struct pci_dev *pdev = to_pci_dev(dev->dev); u32 csts = readl(dev->bar + NVME_REG_CSTS); u8 opcode; + int r; /* * Shutdown the device immediately if we see it is disconnected. This @@ -1935,11 +1922,6 @@ static enum blk_eh_timer_return nvme_timeout(struct request *req) atomic_inc(&dev->ctrl.abort_limit); return BLK_EH_RESET_TIMER; } - iod->flags |= IOD_ABORTED; - - cmd.abort.opcode = nvme_admin_abort_cmd; - cmd.abort.cid = nvme_cid(req); - cmd.abort.sqid = cpu_to_le16(nvmeq->qid); dev_warn(nvmeq->dev->ctrl.device, "I/O tag %d (%04x) opcode %#x (%s) QID %d timeout, aborting req_op:%s(%u) size:%u\n", @@ -1947,17 +1929,11 @@ static enum blk_eh_timer_return nvme_timeout(struct request *req) nvmeq->qid, blk_op_str(req_op(req)), req_op(req), blk_rq_bytes(req)); - abort_req = blk_mq_alloc_request(dev->ctrl.admin_q, nvme_req_op(&cmd), - BLK_MQ_REQ_NOWAIT); - if (IS_ERR(abort_req)) { - atomic_inc(&dev->ctrl.abort_limit); + r = nvme_submit_abort_req(&dev->ctrl, req, cpu_to_le16(nvmeq->qid)); + if (r == -EBUSY) return BLK_EH_RESET_TIMER; - } - nvme_init_request(abort_req, &cmd); - abort_req->end_io = abort_endio; - abort_req->end_io_data = NULL; - blk_execute_rq_nowait(abort_req, false); + iod->flags |= IOD_ABORTED; /* * The aborted req will be completed on receiving the abort req. From 06ee6b89e8e6e69b918bc4ddea4c97f86ad44d1a Mon Sep 17 00:00:00 2001 From: Maurizio Lombardi Date: Thu, 20 Mar 2025 15:56:51 +0100 Subject: [PATCH 05/36] nvme-tcp: use the cancel command to perform an abort if target supports it If available, use Cancel command to abort the command that timed out instead of resetting the controller. Limit the number of outstanding cancel commands to a maximum of 2 per queue; If more than one command time out at the same time, the driver will use a Cancel command with action flag set to "Multiple commands" to abort all the commands on the specified queue as a last resort to avoid a controller reset. If the cancel command is not supported the driver will try to use the normal abort command. In case of errors, the driver will fall back to performing a controller reset. Signed-off-by: Maurizio Lombardi --- drivers/nvme/host/core.c | 6 ++++ drivers/nvme/host/nvme.h | 1 + drivers/nvme/host/tcp.c | 73 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 75 insertions(+), 5 deletions(-) diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c index 7338d998646d79..1e46d5b413f8b5 100644 --- a/drivers/nvme/host/core.c +++ b/drivers/nvme/host/core.c @@ -1240,6 +1240,12 @@ bool nvme_io_command_supported(struct nvme_ctrl *ctrl, u8 opcode) } EXPORT_SYMBOL_GPL(nvme_io_command_supported); +bool nvme_is_cancel(struct nvme_command *cmd) +{ + return cmd->common.opcode == nvme_cmd_cancel; +} +EXPORT_SYMBOL_GPL(nvme_is_cancel); + u32 nvme_passthru_start(struct nvme_ctrl *ctrl, struct nvme_ns *ns, u8 opcode) { u32 effects = nvme_command_effects(ctrl, ns, opcode); diff --git a/drivers/nvme/host/nvme.h b/drivers/nvme/host/nvme.h index 1a2fb0533d9b46..71242870fa35df 100644 --- a/drivers/nvme/host/nvme.h +++ b/drivers/nvme/host/nvme.h @@ -1277,6 +1277,7 @@ static inline void nvme_auth_revoke_tls_key(struct nvme_ctrl *ctrl) {}; u32 nvme_command_effects(struct nvme_ctrl *ctrl, struct nvme_ns *ns, u8 opcode); bool nvme_io_command_supported(struct nvme_ctrl *ctrl, u8 opcode); +bool nvme_is_cancel(struct nvme_command *cmd); u32 nvme_passthru_start(struct nvme_ctrl *ctrl, struct nvme_ns *ns, u8 opcode); int nvme_execute_rq(struct request *rq, bool at_head); void nvme_passthru_end(struct nvme_ctrl *ctrl, struct nvme_ns *ns, u32 effects, diff --git a/drivers/nvme/host/tcp.c b/drivers/nvme/host/tcp.c index 02c95c32b07e39..da74df41a036e6 100644 --- a/drivers/nvme/host/tcp.c +++ b/drivers/nvme/host/tcp.c @@ -123,6 +123,7 @@ struct nvme_tcp_request { size_t offset; size_t data_sent; enum nvme_tcp_send_state state; + bool aborted; }; enum nvme_tcp_queue_flags { @@ -130,6 +131,8 @@ enum nvme_tcp_queue_flags { NVME_TCP_Q_LIVE = 1, NVME_TCP_Q_POLLING = 2, NVME_TCP_Q_IO_CPU_SET = 3, + NVME_TCP_Q_CANCEL_ONE = 4, + NVME_TCP_Q_CANCEL_ALL = 5, }; enum nvme_tcp_recv_state { @@ -206,6 +209,7 @@ static struct workqueue_struct *nvme_tcp_wq; static const struct blk_mq_ops nvme_tcp_mq_ops; static const struct blk_mq_ops nvme_tcp_admin_mq_ops; static int nvme_tcp_try_send(struct nvme_tcp_queue *queue); +static void nvme_tcp_complete_rq(struct request *rq); static inline struct nvme_tcp_ctrl *to_tcp_ctrl(struct nvme_ctrl *ctrl) { @@ -639,7 +643,7 @@ static int nvme_tcp_process_nvme_cqe(struct nvme_tcp_queue *queue, req->status = cqe->status; if (!nvme_try_complete_req(rq, req->status, cqe->result)) - nvme_complete_rq(rq); + nvme_tcp_complete_rq(rq); queue->nr_cqe++; return 0; @@ -905,7 +909,7 @@ static inline void nvme_tcp_end_request(struct request *rq, u16 status) union nvme_result res = {}; if (!nvme_try_complete_req(rq, cpu_to_le16(status << 1), res)) - nvme_complete_rq(rq); + nvme_tcp_complete_rq(rq); } static int nvme_tcp_recv_data(struct nvme_tcp_queue *queue, struct sk_buff *skb, @@ -1893,6 +1897,8 @@ static int nvme_tcp_alloc_queue(struct nvme_ctrl *nctrl, int qid, goto err_init_connect; set_bit(NVME_TCP_Q_ALLOCATED, &queue->flags); + clear_bit(NVME_TCP_Q_CANCEL_ONE, &queue->flags); + clear_bit(NVME_TCP_Q_CANCEL_ALL, &queue->flags); return 0; @@ -2640,10 +2646,12 @@ static void nvme_tcp_complete_timed_out(struct request *rq) static enum blk_eh_timer_return nvme_tcp_timeout(struct request *rq) { struct nvme_tcp_request *req = blk_mq_rq_to_pdu(rq); - struct nvme_ctrl *ctrl = &req->queue->ctrl->ctrl; struct nvme_tcp_cmd_pdu *pdu = nvme_tcp_req_cmd_pdu(req); struct nvme_command *cmd = &pdu->cmd; - int qid = nvme_tcp_queue_id(req->queue); + struct nvme_tcp_queue *queue = req->queue; + struct nvme_ctrl *ctrl = &queue->ctrl->ctrl; + int qid = nvme_tcp_queue_id(queue); + int error, action; dev_warn(ctrl->device, "I/O tag %d (%04x) type %d opcode %#x (%s) QID %d timeout\n", @@ -2668,10 +2676,50 @@ static enum blk_eh_timer_return nvme_tcp_timeout(struct request *rq) return BLK_EH_DONE; } + if (!req->aborted) { + if (!qid) + goto err_recovery; + + if (!nvme_io_command_supported(ctrl, nvme_cmd_cancel)) { + error = nvme_submit_abort_req(ctrl, rq, qid); + if (error) { + if (error == -EBUSY) + return BLK_EH_RESET_TIMER; + goto err_recovery; + } + + goto abort; + } + + if (!test_and_set_bit(NVME_TCP_Q_CANCEL_ONE, &queue->flags)) { + action = NVME_CANCEL_ACTION_SINGLE_CMD; + } else if (!test_and_set_bit(NVME_TCP_Q_CANCEL_ALL, + &queue->flags)) { + action = NVME_CANCEL_ACTION_MUL_CMD; + } else { + /* No free reserved commands. + * this means a "multiple commands" cancel + * is currently under execution and this request + * is likely to be canceled. Mark this + * request as aborted and reset the timer. + */ + goto abort; + } + + error = nvme_submit_cancel_req(ctrl, rq, qid, action); + if (error) + goto err_recovery; + +abort: + req->aborted = true; + return BLK_EH_RESET_TIMER; + } + /* * LIVE state should trigger the normal error recovery which will * handle completing this request. */ +err_recovery: nvme_tcp_error_recovery(ctrl); return BLK_EH_RESET_TIMER; } @@ -2716,6 +2764,7 @@ static blk_status_t nvme_tcp_setup_cmd_pdu(struct nvme_ns *ns, req->pdu_len = 0; req->pdu_sent = 0; req->h2cdata_left = 0; + req->aborted = false; req->data_len = blk_rq_nr_phys_segments(rq) ? blk_rq_payload_bytes(rq) : 0; req->curr_bio = rq->bio; @@ -2832,10 +2881,24 @@ static int nvme_tcp_get_address(struct nvme_ctrl *ctrl, char *buf, int size) return len; } +static void nvme_tcp_complete_rq(struct request *rq) +{ + struct nvme_tcp_request *req = blk_mq_rq_to_pdu(rq); + struct nvme_tcp_queue *queue = req->queue; + bool is_cancel = nvme_is_cancel(req->req.cmd); + + nvme_complete_rq(rq); + + if (is_cancel) { + if (!test_and_clear_bit(NVME_TCP_Q_CANCEL_ALL, &queue->flags)) + clear_bit(NVME_TCP_Q_CANCEL_ONE, &queue->flags); + } +} + static const struct blk_mq_ops nvme_tcp_mq_ops = { .queue_rq = nvme_tcp_queue_rq, .commit_rqs = nvme_tcp_commit_rqs, - .complete = nvme_complete_rq, + .complete = nvme_tcp_complete_rq, .init_request = nvme_tcp_init_request, .exit_request = nvme_tcp_exit_request, .init_hctx = nvme_tcp_init_hctx, From 2f0670a619c0b6804d345be3220bcf8504b5f05c Mon Sep 17 00:00:00 2001 From: Maurizio Lombardi Date: Fri, 7 Feb 2025 15:40:27 +0100 Subject: [PATCH 06/36] nvme-rdma: use the cancel command to perform an abort if target supports it If available, use Cancel command to abort the command that timed out instead of resetting the controller. Limit the number of outstanding cancel commands to a maximum of 2 per queue; If more than one command time out at the same time, the driver will use a Cancel command with action flag set to "Multiple commands" to abort all the commands on the specified queue as a last resort to avoid a controller reset. If the cancel command is not supported the driver will try to use the normal abort command. In case of errors, the driver will fall back to performing a controller reset. Signed-off-by: Maurizio Lombardi --- drivers/nvme/host/rdma.c | 60 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/drivers/nvme/host/rdma.c b/drivers/nvme/host/rdma.c index f77c960f7632d1..ed2df4a9aad6ed 100644 --- a/drivers/nvme/host/rdma.c +++ b/drivers/nvme/host/rdma.c @@ -74,12 +74,15 @@ struct nvme_rdma_request { struct nvme_rdma_sgl data_sgl; struct nvme_rdma_sgl *metadata_sgl; bool use_sig_mr; + bool aborted; }; enum nvme_rdma_queue_flags { NVME_RDMA_Q_ALLOCATED = 0, NVME_RDMA_Q_LIVE = 1, NVME_RDMA_Q_TR_READY = 2, + NVME_RDMA_Q_CANCEL_ONE = 3, + NVME_RDMA_Q_CANCEL_ALL = 4, }; struct nvme_rdma_queue { @@ -619,6 +622,8 @@ static int nvme_rdma_alloc_queue(struct nvme_rdma_ctrl *ctrl, } set_bit(NVME_RDMA_Q_ALLOCATED, &queue->flags); + clear_bit(NVME_RDMA_Q_CANCEL_ONE, &queue->flags); + clear_bit(NVME_RDMA_Q_CANCEL_ALL, &queue->flags); return 0; @@ -1954,16 +1959,18 @@ static enum blk_eh_timer_return nvme_rdma_timeout(struct request *rq) { struct nvme_rdma_request *req = blk_mq_rq_to_pdu(rq); struct nvme_rdma_queue *queue = req->queue; - struct nvme_rdma_ctrl *ctrl = queue->ctrl; + struct nvme_rdma_ctrl *rdma_ctrl = queue->ctrl; + struct nvme_ctrl *ctrl = &rdma_ctrl->ctrl; struct nvme_command *cmd = req->req.cmd; int qid = nvme_rdma_queue_idx(queue); + int error, action; - dev_warn(ctrl->ctrl.device, + dev_warn(ctrl->device, "I/O tag %d (%04x) opcode %#x (%s) QID %d timeout\n", rq->tag, nvme_cid(rq), cmd->common.opcode, nvme_fabrics_opcode_str(qid, cmd), qid); - if (nvme_ctrl_state(&ctrl->ctrl) != NVME_CTRL_LIVE) { + if (nvme_ctrl_state(ctrl) != NVME_CTRL_LIVE) { /* * If we are resetting, connecting or deleting we should * complete immediately because we may block controller @@ -1981,11 +1988,50 @@ static enum blk_eh_timer_return nvme_rdma_timeout(struct request *rq) return BLK_EH_DONE; } + if (!req->aborted) { + if (!qid) + goto err_recovery; + + if (!nvme_io_command_supported(ctrl, nvme_cmd_cancel)) { + error = nvme_submit_abort_req(ctrl, rq, qid); + if (error) { + if (error == -EBUSY) + return BLK_EH_RESET_TIMER; + goto err_recovery; + } + goto abort; + } + + if (!test_and_set_bit(NVME_RDMA_Q_CANCEL_ONE, &queue->flags)) { + action = NVME_CANCEL_ACTION_SINGLE_CMD; + } else if (!test_and_set_bit(NVME_RDMA_Q_CANCEL_ALL, + &queue->flags)) { + action = NVME_CANCEL_ACTION_MUL_CMD; + } else { + /* No free reserved commands. + * this means a "multiple commands" cancel + * is currently under execution and this request + * is likely to be canceled. Mark this + * request as aborted and reset the timer. + */ + goto abort; + } + + error = nvme_submit_cancel_req(ctrl, rq, qid, action); + if (error) + goto err_recovery; + +abort: + req->aborted = true; + return BLK_EH_RESET_TIMER; + } + /* * LIVE state should trigger the normal error recovery which will * handle completing this request. */ - nvme_rdma_error_recovery(ctrl); +err_recovery: + nvme_rdma_error_recovery(rdma_ctrl); return BLK_EH_RESET_TIMER; } @@ -2009,6 +2055,7 @@ static blk_status_t nvme_rdma_queue_rq(struct blk_mq_hw_ctx *hctx, return nvme_fail_nonready_command(&queue->ctrl->ctrl, rq); dev = queue->device->dev; + req->aborted = false; req->sqe.dma = ib_dma_map_single(dev, req->sqe.data, sizeof(struct nvme_command), @@ -2113,6 +2160,7 @@ static void nvme_rdma_complete_rq(struct request *rq) struct nvme_rdma_request *req = blk_mq_rq_to_pdu(rq); struct nvme_rdma_queue *queue = req->queue; struct ib_device *ibdev = queue->device->dev; + bool is_cancel = nvme_is_cancel(req->req.cmd); if (req->use_sig_mr) nvme_rdma_check_pi_status(req); @@ -2121,6 +2169,10 @@ static void nvme_rdma_complete_rq(struct request *rq) ib_dma_unmap_single(ibdev, req->sqe.dma, sizeof(struct nvme_command), DMA_TO_DEVICE); nvme_complete_rq(rq); + if (is_cancel) { + if (!test_and_clear_bit(NVME_RDMA_Q_CANCEL_ALL, &queue->flags)) + clear_bit(NVME_RDMA_Q_CANCEL_ONE, &queue->flags); + } } static void nvme_rdma_map_queues(struct blk_mq_tag_set *set) From 7154526f1dbb4ce04835f66dc97c99504a2a3b7c Mon Sep 17 00:00:00 2001 From: "Heyne, Maximilian" Date: Thu, 4 Dec 2025 14:11:50 +0000 Subject: [PATCH 07/36] nvme: Let the blocklayer set timeouts for requests When initializing an nvme request which is about to be send to the block layer, we do not need to initialize its timeout. If it's left uninitialized at 0 the block layer will use the request queue's timeout in blk_add_timer (via nvme_start_request which is called from nvme_*_queue_rq). These timeouts are setup to either NVME_IO_TIMEOUT or NVME_ADMIN_TIMEOUT when the request queues were created. Because the io_timeout of the IO queues can be modified via sysfs, the following situation can occur: 1) NVME_IO_TIMEOUT = 30 (default module parameter) 2) nvme1n1 is probed. IO queues default timeout is 30 s 3) manually change the IO timeout to 90 s echo 90000 > /sys/class/nvme/nvme1/nvme1n1/queue/io_timeout 4) Any call of __submit_sync_cmd on nvme1n1 to an IO queue will issue commands with the 30 s timeout instead of the wanted 90 s which might be more suitable for this device. Commit 470e900c8036 ("nvme: refactor nvme_alloc_request") silently changed the behavior for ioctl's already because it unconditionally overrides the request's timeout that was set in nvme_init_request. If it was unset by the user of the ioctl if will be overridden with 0 meaning the block layer will pick the request queue's IO timeout. Following up on that, this patch further improves the consistency of IO timeout usage. However, there are still uses of NVME_IO_TIMEOUT which could be inconsistent with what is set in the device's request_queue by the user. Reviewed-by: Mohamed Khalfella Signed-off-by: Maximilian Heyne --- drivers/nvme/host/core.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c index 1e46d5b413f8b5..679e5804490ed6 100644 --- a/drivers/nvme/host/core.c +++ b/drivers/nvme/host/core.c @@ -724,10 +724,8 @@ void nvme_init_request(struct request *req, struct nvme_command *cmd) struct nvme_ns *ns = req->q->disk->private_data; logging_enabled = ns->head->passthru_err_log_enabled; - req->timeout = NVME_IO_TIMEOUT; } else { /* no queuedata implies admin queue */ logging_enabled = nr->ctrl->passthru_err_log_enabled; - req->timeout = NVME_ADMIN_TIMEOUT; } if (!logging_enabled) From 22ce839e46530e4ae6fb135e3e173cf0c3c85820 Mon Sep 17 00:00:00 2001 From: Maurizio Lombardi Date: Fri, 27 Mar 2026 08:23:32 +0100 Subject: [PATCH 08/36] nvme: add sysfs attribute to change admin timeout per nvme controller Currently, there is no method to adjust the timeout values on a per controller basis with nvme admin queues. Add an admin_timeout attribute to nvme so that different nvme controllers which may have different timeout requirements can have custom admin timeouts set. Signed-off-by: Maurizio Lombardi --- drivers/nvme/host/core.c | 1 + drivers/nvme/host/nvme.h | 1 + drivers/nvme/host/sysfs.c | 42 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c index 679e5804490ed6..b192e661a34445 100644 --- a/drivers/nvme/host/core.c +++ b/drivers/nvme/host/core.c @@ -5260,6 +5260,7 @@ int nvme_init_ctrl(struct nvme_ctrl *ctrl, struct device *dev, memset(&ctrl->ka_cmd, 0, sizeof(ctrl->ka_cmd)); ctrl->ka_cmd.common.opcode = nvme_admin_keep_alive; ctrl->ka_last_check_time = jiffies; + ctrl->admin_timeout = NVME_ADMIN_TIMEOUT; BUILD_BUG_ON(NVME_DSM_MAX_RANGES * sizeof(struct nvme_dsm_range) > PAGE_SIZE); diff --git a/drivers/nvme/host/nvme.h b/drivers/nvme/host/nvme.h index 71242870fa35df..1ecb2dca6aaa2b 100644 --- a/drivers/nvme/host/nvme.h +++ b/drivers/nvme/host/nvme.h @@ -370,6 +370,7 @@ struct nvme_ctrl { u16 mtfa; u32 ctrl_config; u32 queue_count; + u32 admin_timeout; u64 cap; u32 max_hw_sectors; diff --git a/drivers/nvme/host/sysfs.c b/drivers/nvme/host/sysfs.c index e59758616f277e..9456af955affdf 100644 --- a/drivers/nvme/host/sysfs.c +++ b/drivers/nvme/host/sysfs.c @@ -623,6 +623,47 @@ static ssize_t quirks_show(struct device *dev, struct device_attribute *attr, } static DEVICE_ATTR_RO(quirks); +static ssize_t nvme_admin_timeout_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct nvme_ctrl *ctrl = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%u\n", + jiffies_to_msecs(ctrl->admin_timeout)); +} + +static ssize_t nvme_admin_timeout_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct nvme_ctrl *ctrl = dev_get_drvdata(dev); + u32 timeout; + int err; + + /* + * Wait until the controller reaches the LIVE state + * to be sure that admin_q and fabrics_q are + * properly initialized. + */ + if (!test_bit(NVME_CTRL_STARTED_ONCE, &ctrl->flags)) + return -EBUSY; + + err = kstrtou32(buf, 10, &timeout); + if (err || !timeout) + return -EINVAL; + + ctrl->admin_timeout = msecs_to_jiffies(timeout); + + blk_queue_rq_timeout(ctrl->admin_q, ctrl->admin_timeout); + if (ctrl->fabrics_q) + blk_queue_rq_timeout(ctrl->fabrics_q, ctrl->admin_timeout); + + return count; +} + +static DEVICE_ATTR(admin_timeout, S_IRUGO | S_IWUSR, + nvme_admin_timeout_show, nvme_admin_timeout_store); + #ifdef CONFIG_NVME_HOST_AUTH static ssize_t nvme_ctrl_dhchap_secret_show(struct device *dev, struct device_attribute *attr, char *buf) @@ -765,6 +806,7 @@ static struct attribute *nvme_dev_attrs[] = { &dev_attr_cntrltype.attr, &dev_attr_dctype.attr, &dev_attr_quirks.attr, + &dev_attr_admin_timeout.attr, #ifdef CONFIG_NVME_HOST_AUTH &dev_attr_dhchap_secret.attr, &dev_attr_dhchap_ctrl_secret.attr, From d6abbd921bd3129d44d943c0ae31d990c34e5a26 Mon Sep 17 00:00:00 2001 From: Maurizio Lombardi Date: Fri, 13 Sep 2024 09:44:29 -0400 Subject: [PATCH 09/36] nvme: pci: use admin queue timeout over NVME_ADMIN_TIMEOUT While tearing down its queues, nvme-pci uses NVME_ADMIN_TIMEOUT as its timeout target. Instead, use the configured admin queue's timeout value to match the device's existing timeout setting. Signed-off-by: Maurizio Lombardi --- drivers/nvme/host/pci.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/nvme/host/pci.c b/drivers/nvme/host/pci.c index 95d61d514e258a..090f22017187aa 100644 --- a/drivers/nvme/host/pci.c +++ b/drivers/nvme/host/pci.c @@ -3067,7 +3067,7 @@ static bool __nvme_delete_io_queues(struct nvme_dev *dev, u8 opcode) unsigned long timeout; retry: - timeout = NVME_ADMIN_TIMEOUT; + timeout = dev->ctrl.admin_timeout; while (nr_queues > 0) { if (nvme_delete_queue(&dev->queues[nr_queues], opcode)) break; From 9b98736afcec8d096967fd58e0bb09f0e3856249 Mon Sep 17 00:00:00 2001 From: Maurizio Lombardi Date: Mon, 30 Mar 2026 15:00:22 +0200 Subject: [PATCH 10/36] nvme: add sysfs attribute to change IO timeout per nvme controller Currently, there is no method to adjust the timeout values on a per controller basis with nvme I/O queues. Add an io_timeout attribute to nvme so that different nvme controllers which may have different timeout requirements can have custom I/O timeouts set. Signed-off-by: Maurizio Lombardi --- drivers/nvme/host/core.c | 2 ++ drivers/nvme/host/nvme.h | 1 + drivers/nvme/host/sysfs.c | 47 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c index b192e661a34445..79c00cf6988a80 100644 --- a/drivers/nvme/host/core.c +++ b/drivers/nvme/host/core.c @@ -4317,6 +4317,7 @@ static void nvme_alloc_ns(struct nvme_ctrl *ctrl, struct nvme_ns_info *info) mutex_unlock(&ctrl->namespaces_lock); goto out_unlink_ns; } + blk_queue_rq_timeout(ns->queue, ctrl->io_timeout); nvme_ns_add_to_ctrl_list(ns); mutex_unlock(&ctrl->namespaces_lock); synchronize_srcu(&ctrl->srcu); @@ -5261,6 +5262,7 @@ int nvme_init_ctrl(struct nvme_ctrl *ctrl, struct device *dev, ctrl->ka_cmd.common.opcode = nvme_admin_keep_alive; ctrl->ka_last_check_time = jiffies; ctrl->admin_timeout = NVME_ADMIN_TIMEOUT; + ctrl->io_timeout = NVME_IO_TIMEOUT; BUILD_BUG_ON(NVME_DSM_MAX_RANGES * sizeof(struct nvme_dsm_range) > PAGE_SIZE); diff --git a/drivers/nvme/host/nvme.h b/drivers/nvme/host/nvme.h index 1ecb2dca6aaa2b..168e67856492c5 100644 --- a/drivers/nvme/host/nvme.h +++ b/drivers/nvme/host/nvme.h @@ -371,6 +371,7 @@ struct nvme_ctrl { u32 ctrl_config; u32 queue_count; u32 admin_timeout; + u32 io_timeout; u64 cap; u32 max_hw_sectors; diff --git a/drivers/nvme/host/sysfs.c b/drivers/nvme/host/sysfs.c index 9456af955affdf..d368e779b6cce0 100644 --- a/drivers/nvme/host/sysfs.c +++ b/drivers/nvme/host/sysfs.c @@ -664,6 +664,52 @@ static ssize_t nvme_admin_timeout_store(struct device *dev, static DEVICE_ATTR(admin_timeout, S_IRUGO | S_IWUSR, nvme_admin_timeout_show, nvme_admin_timeout_store); +static ssize_t nvme_io_timeout_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct nvme_ctrl *ctrl = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%u\n", jiffies_to_msecs(ctrl->io_timeout)); +} + +static ssize_t nvme_io_timeout_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct nvme_ctrl *ctrl = dev_get_drvdata(dev); + struct nvme_ns *ns; + u32 timeout; + int err; + + /* + * Wait until the controller reaches the LIVE state + * to be sure that connect_q is properly initialized. + */ + if (!test_bit(NVME_CTRL_STARTED_ONCE, &ctrl->flags)) + return -EBUSY; + + err = kstrtou32(buf, 10, &timeout); + if (err || !timeout) + return -EINVAL; + + /* Take the namespaces_lock to avoid racing against nvme_alloc_ns() */ + mutex_lock(&ctrl->namespaces_lock); + + ctrl->io_timeout = msecs_to_jiffies(timeout); + list_for_each_entry(ns, &ctrl->namespaces, list) + blk_queue_rq_timeout(ns->queue, ctrl->io_timeout); + + if (ctrl->connect_q) + blk_queue_rq_timeout(ctrl->connect_q, ctrl->io_timeout); + + mutex_unlock(&ctrl->namespaces_lock); + + return count; +} + +static DEVICE_ATTR(io_timeout, S_IRUGO | S_IWUSR, + nvme_io_timeout_show, nvme_io_timeout_store); + #ifdef CONFIG_NVME_HOST_AUTH static ssize_t nvme_ctrl_dhchap_secret_show(struct device *dev, struct device_attribute *attr, char *buf) @@ -807,6 +853,7 @@ static struct attribute *nvme_dev_attrs[] = { &dev_attr_dctype.attr, &dev_attr_quirks.attr, &dev_attr_admin_timeout.attr, + &dev_attr_io_timeout.attr, #ifdef CONFIG_NVME_HOST_AUTH &dev_attr_dhchap_secret.attr, &dev_attr_dhchap_ctrl_secret.attr, From 43cef148f592e58158e577961d67883311e2ec4b Mon Sep 17 00:00:00 2001 From: Maurizio Lombardi Date: Wed, 12 Nov 2025 16:35:04 +0100 Subject: [PATCH 11/36] nvme: use per controller timeout waits over depending on global default Instead of passing NVME_IO_TIMEOUT as a parameter with every call to nvme_wait_freeze_timeout, use the controller's preferred timeout. Reviewed-by: Mohamed Khalfella Signed-off-by: Maurizio Lombardi --- drivers/nvme/host/apple.c | 2 +- drivers/nvme/host/core.c | 5 +++-- drivers/nvme/host/nvme.h | 2 +- drivers/nvme/host/pci.c | 2 +- drivers/nvme/host/rdma.c | 2 +- drivers/nvme/host/tcp.c | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/drivers/nvme/host/apple.c b/drivers/nvme/host/apple.c index 423c9c628e7bfa..e77c4740810218 100644 --- a/drivers/nvme/host/apple.c +++ b/drivers/nvme/host/apple.c @@ -858,7 +858,7 @@ static void apple_nvme_disable(struct apple_nvme *anv, bool shutdown) * doing a safe shutdown. */ if (!dead && shutdown && freeze) - nvme_wait_freeze_timeout(&anv->ctrl, NVME_IO_TIMEOUT); + nvme_wait_freeze_timeout(&anv->ctrl); nvme_quiesce_io_queues(&anv->ctrl); diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c index 79c00cf6988a80..7ed29cc8b80245 100644 --- a/drivers/nvme/host/core.c +++ b/drivers/nvme/host/core.c @@ -5369,8 +5369,9 @@ void nvme_unfreeze(struct nvme_ctrl *ctrl) } EXPORT_SYMBOL_GPL(nvme_unfreeze); -int nvme_wait_freeze_timeout(struct nvme_ctrl *ctrl, long timeout) +int nvme_wait_freeze_timeout(struct nvme_ctrl *ctrl) { + unsigned long timeout = ctrl->io_timeout; struct nvme_ns *ns; int srcu_idx; @@ -5378,7 +5379,7 @@ int nvme_wait_freeze_timeout(struct nvme_ctrl *ctrl, long timeout) list_for_each_entry_srcu(ns, &ctrl->namespaces, list, srcu_read_lock_held(&ctrl->srcu)) { timeout = blk_mq_freeze_queue_wait_timeout(ns->queue, timeout); - if (timeout <= 0) + if (!timeout) break; } srcu_read_unlock(&ctrl->srcu, srcu_idx); diff --git a/drivers/nvme/host/nvme.h b/drivers/nvme/host/nvme.h index 168e67856492c5..6b8c159a62e061 100644 --- a/drivers/nvme/host/nvme.h +++ b/drivers/nvme/host/nvme.h @@ -902,7 +902,7 @@ void nvme_sync_queues(struct nvme_ctrl *ctrl); void nvme_sync_io_queues(struct nvme_ctrl *ctrl); void nvme_unfreeze(struct nvme_ctrl *ctrl); void nvme_wait_freeze(struct nvme_ctrl *ctrl); -int nvme_wait_freeze_timeout(struct nvme_ctrl *ctrl, long timeout); +int nvme_wait_freeze_timeout(struct nvme_ctrl *ctrl); void nvme_start_freeze(struct nvme_ctrl *ctrl); static inline enum req_op nvme_req_op(struct nvme_command *cmd) diff --git a/drivers/nvme/host/pci.c b/drivers/nvme/host/pci.c index 090f22017187aa..3ed164b47a973e 100644 --- a/drivers/nvme/host/pci.c +++ b/drivers/nvme/host/pci.c @@ -3249,7 +3249,7 @@ static void nvme_dev_disable(struct nvme_dev *dev, bool shutdown) * if doing a safe shutdown. */ if (!dead && shutdown) - nvme_wait_freeze_timeout(&dev->ctrl, NVME_IO_TIMEOUT); + nvme_wait_freeze_timeout(&dev->ctrl); } nvme_quiesce_io_queues(&dev->ctrl); diff --git a/drivers/nvme/host/rdma.c b/drivers/nvme/host/rdma.c index ed2df4a9aad6ed..dbdd1f62005af5 100644 --- a/drivers/nvme/host/rdma.c +++ b/drivers/nvme/host/rdma.c @@ -893,7 +893,7 @@ static int nvme_rdma_configure_io_queues(struct nvme_rdma_ctrl *ctrl, bool new) if (!new) { nvme_start_freeze(&ctrl->ctrl); nvme_unquiesce_io_queues(&ctrl->ctrl); - if (!nvme_wait_freeze_timeout(&ctrl->ctrl, NVME_IO_TIMEOUT)) { + if (!nvme_wait_freeze_timeout(&ctrl->ctrl)) { /* * If we timed out waiting for freeze we are likely to * be stuck. Fail the controller initialization just diff --git a/drivers/nvme/host/tcp.c b/drivers/nvme/host/tcp.c index da74df41a036e6..0a193b3c443841 100644 --- a/drivers/nvme/host/tcp.c +++ b/drivers/nvme/host/tcp.c @@ -2200,7 +2200,7 @@ static int nvme_tcp_configure_io_queues(struct nvme_ctrl *ctrl, bool new) if (!new) { nvme_start_freeze(ctrl); nvme_unquiesce_io_queues(ctrl); - if (!nvme_wait_freeze_timeout(ctrl, NVME_IO_TIMEOUT)) { + if (!nvme_wait_freeze_timeout(ctrl)) { /* * If we timed out waiting for freeze we are likely to * be stuck. Fail the controller initialization just From fea7516f5fbe7eafd0db8cc2017d8e9f91c25306 Mon Sep 17 00:00:00 2001 From: Maurizio Lombardi Date: Tue, 31 Mar 2026 11:36:26 +0200 Subject: [PATCH 12/36] nvme-core: align fabrics_q teardown with admin_q in nvme_free_ctrl Currently, the final reference for the fabrics admin queue (fabrics_q) is dropped inside nvme_remove_admin_tag_set(). However, the primary admin queue (admin_q) defers dropping its final reference until nvme_free_ctrl(). Move the blk_put_queue() call for fabrics_q from nvme_remove_admin_tag_set() to nvme_free_ctrl(). This aligns the lifecycle management of both admin queues, ensuring they are freed symmetrically when the controller is finally torn down. Signed-off-by: Maurizio Lombardi --- drivers/nvme/host/core.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c index 7ed29cc8b80245..c527434794ea11 100644 --- a/drivers/nvme/host/core.c +++ b/drivers/nvme/host/core.c @@ -5047,10 +5047,8 @@ void nvme_remove_admin_tag_set(struct nvme_ctrl *ctrl) */ nvme_stop_keep_alive(ctrl); blk_mq_destroy_queue(ctrl->admin_q); - if (ctrl->ops->flags & NVME_F_FABRICS) { + if (ctrl->ops->flags & NVME_F_FABRICS) blk_mq_destroy_queue(ctrl->fabrics_q); - blk_put_queue(ctrl->fabrics_q); - } blk_mq_free_tag_set(ctrl->admin_tagset); } EXPORT_SYMBOL_GPL(nvme_remove_admin_tag_set); @@ -5197,6 +5195,8 @@ static void nvme_free_ctrl(struct device *dev) if (ctrl->admin_q) blk_put_queue(ctrl->admin_q); + if (ctrl->fabrics_q) + blk_put_queue(ctrl->fabrics_q); if (!subsys || ctrl->instance != subsys->instance) ida_free(&nvme_instance_ida, ctrl->instance); nvme_free_cels(ctrl); From 07ccae3dbb364bbbf13c86a3b7bec13c952596f9 Mon Sep 17 00:00:00 2001 From: Maurizio Lombardi Date: Tue, 31 Mar 2026 18:09:06 +0200 Subject: [PATCH 13/36] nvmet-loop: do not alloc admin tag set during reset Currently, resetting a loopback controller unconditionally invokes nvme_alloc_admin_tag_set() inside nvme_loop_configure_admin_queue(). Doing so drops the old queue and allocates a new one. Consequently, this reverts the admin queue's timeout (q->rq_timeout) back to the module default (NVME_ADMIN_TIMEOUT), completely wiping out any custom timeout values the user may have configured via sysfs and potentially racing against the sysfs nvme_admin_timeout_store() function that may dereference the admin_q pointer during the RESETTING state. Add a 'new' boolean parameter to nvme_loop_configure_admin_queue() to distinguish between an initial probe and a reset. Only allocate the admin tag set during the initial controller creation and do not remove it when resetting the controller. This ensures the existing queues and their sysfs-configured timeouts remain intact across controller resets. Signed-off-by: Maurizio Lombardi --- drivers/nvme/target/loop.c | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/drivers/nvme/target/loop.c b/drivers/nvme/target/loop.c index d98d0cdc5d6fad..c9cf0ddc1b8563 100644 --- a/drivers/nvme/target/loop.c +++ b/drivers/nvme/target/loop.c @@ -261,7 +261,7 @@ static const struct blk_mq_ops nvme_loop_admin_mq_ops = { .init_hctx = nvme_loop_init_admin_hctx, }; -static void nvme_loop_destroy_admin_queue(struct nvme_loop_ctrl *ctrl) +static void nvme_loop_destroy_admin_queue(struct nvme_loop_ctrl *ctrl, bool remove) { if (!test_and_clear_bit(NVME_LOOP_Q_LIVE, &ctrl->queues[0].flags)) return; @@ -274,7 +274,8 @@ static void nvme_loop_destroy_admin_queue(struct nvme_loop_ctrl *ctrl) nvmet_sq_destroy(&ctrl->queues[0].nvme_sq); nvmet_cq_put(&ctrl->queues[0].nvme_cq); - nvme_remove_admin_tag_set(&ctrl->ctrl); + if (remove) + nvme_remove_admin_tag_set(&ctrl->ctrl); } static void nvme_loop_free_ctrl(struct nvme_ctrl *nctrl) @@ -361,7 +362,7 @@ static int nvme_loop_connect_io_queues(struct nvme_loop_ctrl *ctrl) return 0; } -static int nvme_loop_configure_admin_queue(struct nvme_loop_ctrl *ctrl) +static int nvme_loop_configure_admin_queue(struct nvme_loop_ctrl *ctrl, bool new) { int error; @@ -375,12 +376,15 @@ static int nvme_loop_configure_admin_queue(struct nvme_loop_ctrl *ctrl) } ctrl->ctrl.queue_count = 1; - error = nvme_alloc_admin_tag_set(&ctrl->ctrl, &ctrl->admin_tag_set, - &nvme_loop_admin_mq_ops, - sizeof(struct nvme_loop_iod) + - NVME_INLINE_SG_CNT * sizeof(struct scatterlist)); - if (error) - goto out_free_sq; + if (new) { + error = nvme_alloc_admin_tag_set(&ctrl->ctrl, + &ctrl->admin_tag_set, + &nvme_loop_admin_mq_ops, + sizeof(struct nvme_loop_iod) + + NVME_INLINE_SG_CNT * sizeof(struct scatterlist)); + if (error) + goto out_free_sq; + } /* reset stopped state for the fresh admin queue */ clear_bit(NVME_CTRL_ADMIN_Q_STOPPED, &ctrl->ctrl.flags); @@ -415,7 +419,7 @@ static int nvme_loop_configure_admin_queue(struct nvme_loop_ctrl *ctrl) return error; } -static void nvme_loop_shutdown_ctrl(struct nvme_loop_ctrl *ctrl) +static void nvme_loop_shutdown_ctrl(struct nvme_loop_ctrl *ctrl, bool remove) { if (ctrl->ctrl.queue_count > 1) { nvme_quiesce_io_queues(&ctrl->ctrl); @@ -426,12 +430,12 @@ static void nvme_loop_shutdown_ctrl(struct nvme_loop_ctrl *ctrl) if (nvme_ctrl_state(&ctrl->ctrl) == NVME_CTRL_LIVE) nvme_disable_ctrl(&ctrl->ctrl, true); - nvme_loop_destroy_admin_queue(ctrl); + nvme_loop_destroy_admin_queue(ctrl, remove); } static void nvme_loop_delete_ctrl_host(struct nvme_ctrl *ctrl) { - nvme_loop_shutdown_ctrl(to_loop_ctrl(ctrl)); + nvme_loop_shutdown_ctrl(to_loop_ctrl(ctrl), true); } static void nvme_loop_delete_ctrl(struct nvmet_ctrl *nctrl) @@ -453,7 +457,7 @@ static void nvme_loop_reset_ctrl_work(struct work_struct *work) int ret; nvme_stop_ctrl(&ctrl->ctrl); - nvme_loop_shutdown_ctrl(ctrl); + nvme_loop_shutdown_ctrl(ctrl, false); if (!nvme_change_ctrl_state(&ctrl->ctrl, NVME_CTRL_CONNECTING)) { enum nvme_ctrl_state state = nvme_ctrl_state(&ctrl->ctrl); @@ -465,7 +469,7 @@ static void nvme_loop_reset_ctrl_work(struct work_struct *work) return; } - ret = nvme_loop_configure_admin_queue(ctrl); + ret = nvme_loop_configure_admin_queue(ctrl, false); if (ret) goto out_disable; @@ -492,7 +496,7 @@ static void nvme_loop_reset_ctrl_work(struct work_struct *work) out_destroy_admin: nvme_quiesce_admin_queue(&ctrl->ctrl); nvme_cancel_admin_tagset(&ctrl->ctrl); - nvme_loop_destroy_admin_queue(ctrl); + nvme_loop_destroy_admin_queue(ctrl, true); out_disable: dev_warn(ctrl->ctrl.device, "Removing after reset failure\n"); nvme_uninit_ctrl(&ctrl->ctrl); @@ -594,7 +598,7 @@ static struct nvme_ctrl *nvme_loop_create_ctrl(struct device *dev, if (!ctrl->queues) goto out_uninit_ctrl; - ret = nvme_loop_configure_admin_queue(ctrl); + ret = nvme_loop_configure_admin_queue(ctrl, true); if (ret) goto out_free_queues; @@ -632,7 +636,7 @@ static struct nvme_ctrl *nvme_loop_create_ctrl(struct device *dev, out_remove_admin_queue: nvme_quiesce_admin_queue(&ctrl->ctrl); nvme_cancel_admin_tagset(&ctrl->ctrl); - nvme_loop_destroy_admin_queue(ctrl); + nvme_loop_destroy_admin_queue(ctrl, true); out_free_queues: kfree(ctrl->queues); out_uninit_ctrl: From 138c5e5e164e01b265700e9032ff266787aa5dc7 Mon Sep 17 00:00:00 2001 From: Maurizio Lombardi Date: Tue, 31 Mar 2026 10:14:07 +0200 Subject: [PATCH 14/36] nvme-core: warn on allocating admin tag set with existing queue Currently, nvme_alloc_admin_tag_set() silently drops and releases the existing admin_q if it called on a controller that already had one (e.g., during a controller reset). However, transport drivers should not be reallocating the admin tag set and queue during a reset. Dropping the old queue and allocating a new one destroys user-configured timeouts and may race against nvme_admin_timeout_store() Since all transport drivers are now expected to preserve the admin queue across resets, calling nvme_alloc_admin_tag_set() when ctrl->admin_q is already populated is a bug. Remove the silent cleanup and replace it with a WARN_ON_ONCE() to explicitly catch any transport drivers that violate this lifecycle rule Signed-off-by: Maurizio Lombardi --- drivers/nvme/host/core.c | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c index c527434794ea11..824317c6873af0 100644 --- a/drivers/nvme/host/core.c +++ b/drivers/nvme/host/core.c @@ -5004,12 +5004,7 @@ int nvme_alloc_admin_tag_set(struct nvme_ctrl *ctrl, struct blk_mq_tag_set *set, if (ret) return ret; - /* - * If a previous admin queue exists (e.g., from before a reset), - * put it now before allocating a new one to avoid orphaning it. - */ - if (ctrl->admin_q) - blk_put_queue(ctrl->admin_q); + WARN_ON_ONCE(ctrl->admin_q); ctrl->admin_q = blk_mq_alloc_queue(set, &lim, NULL); if (IS_ERR(ctrl->admin_q)) { From aed6bad370fcf0583753182ad7847d9eb1dac554 Mon Sep 17 00:00:00 2001 From: Maurizio Lombardi Date: Fri, 7 Feb 2025 15:40:41 +0100 Subject: [PATCH 15/36] nvmet: emulate basic support for cancel commands Add a module parameter that allows the user to enable a basic cancel command emulation, this could be useful for testing the host driver's cancel command implementation. It just reports that no abort has been executed, it will however do some basic sanity check on the command sent by the host and return an error if the fields are invalid. Signed-off-by: Maurizio Lombardi --- drivers/nvme/target/Makefile | 2 +- drivers/nvme/target/admin-cmd.c | 4 +++ drivers/nvme/target/core.c | 11 ++++++ drivers/nvme/target/io-cmd-cancel.c | 52 +++++++++++++++++++++++++++++ drivers/nvme/target/nvmet.h | 4 +++ 5 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 drivers/nvme/target/io-cmd-cancel.c diff --git a/drivers/nvme/target/Makefile b/drivers/nvme/target/Makefile index ed8522911d1f73..a533357e9be4a7 100644 --- a/drivers/nvme/target/Makefile +++ b/drivers/nvme/target/Makefile @@ -11,7 +11,7 @@ obj-$(CONFIG_NVME_TARGET_TCP) += nvmet-tcp.o obj-$(CONFIG_NVME_TARGET_PCI_EPF) += nvmet-pci-epf.o nvmet-y += core.o configfs.o admin-cmd.o fabrics-cmd.o \ - discovery.o io-cmd-file.o io-cmd-bdev.o pr.o + discovery.o io-cmd-file.o io-cmd-bdev.o pr.o io-cmd-cancel.o nvmet-$(CONFIG_NVME_TARGET_DEBUGFS) += debugfs.o nvmet-$(CONFIG_NVME_TARGET_PASSTHRU) += passthru.o nvmet-$(CONFIG_BLK_DEV_ZONED) += zns.o diff --git a/drivers/nvme/target/admin-cmd.c b/drivers/nvme/target/admin-cmd.c index 3842087209da55..480e3764d5eac9 100644 --- a/drivers/nvme/target/admin-cmd.c +++ b/drivers/nvme/target/admin-cmd.c @@ -389,6 +389,10 @@ static void nvmet_get_cmd_effects_nvm(struct nvme_effects_log *log) log->iocs[nvme_cmd_resv_release] = log->iocs[nvme_cmd_resv_report] = cpu_to_le32(NVME_CMD_EFFECTS_CSUPP); + if (emulate_cancel_support) { + log->iocs[nvme_cmd_cancel] = + cpu_to_le32(NVME_CMD_EFFECTS_CSUPP); + } log->iocs[nvme_cmd_write] = log->iocs[nvme_cmd_write_zeroes] = cpu_to_le32(NVME_CMD_EFFECTS_CSUPP | NVME_CMD_EFFECTS_LBCC); diff --git a/drivers/nvme/target/core.c b/drivers/nvme/target/core.c index a87567f40c9155..5b772e45baa6f2 100644 --- a/drivers/nvme/target/core.c +++ b/drivers/nvme/target/core.c @@ -28,6 +28,10 @@ static DEFINE_IDA(cntlid_ida); struct workqueue_struct *nvmet_wq; EXPORT_SYMBOL_GPL(nvmet_wq); +bool emulate_cancel_support; +module_param(emulate_cancel_support, bool, 0644); +MODULE_PARM_DESC(emulate_cancel_support, "Emulate the cancel command support. Default = false"); + /* * This read/write semaphore is used to synchronize access to configuration * information on a target system that will result in discovery log page @@ -1105,6 +1109,13 @@ static u16 nvmet_parse_io_cmd(struct nvmet_req *req) if (nvmet_is_passthru_req(req)) return nvmet_parse_passthru_io_cmd(req); + if (emulate_cancel_support && + req->cmd->common.opcode == nvme_cmd_cancel) { + req->execute = nvmet_execute_cancel; + if (req->cmd->cancel.nsid == NVME_NSID_ALL) + return 0; + } + ret = nvmet_req_find_ns(req); if (unlikely(ret)) return ret; diff --git a/drivers/nvme/target/io-cmd-cancel.c b/drivers/nvme/target/io-cmd-cancel.c new file mode 100644 index 00000000000000..8f55458e646a25 --- /dev/null +++ b/drivers/nvme/target/io-cmd-cancel.c @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * NVMe I/O cancel command implementation. + * Copyright (c) 2023 Red Hat + */ + +#include "nvmet.h" + +void nvmet_execute_cancel(struct nvmet_req *req) +{ + u16 cid; + __le16 sqid; + bool mult_cmds; + int ret = 0; + struct nvmet_ctrl *ctrl = req->sq->ctrl; + struct nvmet_sq *sq = req->sq; + + if (!nvmet_check_transfer_len(req, 0)) + return; + + sqid = le16_to_cpu(req->cmd->cancel.sqid); + if (sqid > ctrl->subsys->max_qid) { + ret = NVME_SC_INVALID_FIELD | NVME_STATUS_DNR; + goto exit; + } + + mult_cmds = req->cmd->cancel.action & NVME_CANCEL_ACTION_MUL_CMD; + cid = req->cmd->cancel.cid; + + if (cid == req->cmd->cancel.command_id && !mult_cmds) { + /* If action is set to "single command" and cid is + * set to the cid of this cancel command, then + * the controller shall abort the command with + * an "invalid cid" status code. + */ + ret = NVME_SC_INVALID_CID | NVME_STATUS_DNR; + } else if ((cid != 0xFFFF && mult_cmds) || sqid != sq->qid) { + /* if action is set to "multiple commands" and + * cid isn't set to 0xFFFF, then abort the command + * with an "invalid field" status. + * if the sqid field doesn't match the sqid of + * the queue to which the cancel command is submitted, + * then abort the command with an "invalid field" status. + */ + ret = NVME_SC_INVALID_FIELD | NVME_STATUS_DNR; + } + +exit: + nvmet_set_result(req, 0); + nvmet_req_complete(req, ret); +} + diff --git a/drivers/nvme/target/nvmet.h b/drivers/nvme/target/nvmet.h index 64af6bf569bf45..2f19728881fee7 100644 --- a/drivers/nvme/target/nvmet.h +++ b/drivers/nvme/target/nvmet.h @@ -40,6 +40,8 @@ #define nvmet_for_each_enabled_ns(xa, index, entry) \ xa_for_each_marked(xa, index, entry, NVMET_NS_ENABLED) +extern bool emulate_cancel_support; + /* * Supported optional AENs: */ @@ -727,6 +729,8 @@ void nvmet_bdev_execute_zone_mgmt_recv(struct nvmet_req *req); void nvmet_bdev_execute_zone_mgmt_send(struct nvmet_req *req); void nvmet_bdev_execute_zone_append(struct nvmet_req *req); +void nvmet_execute_cancel(struct nvmet_req *req); + static inline u32 nvmet_rw_data_len(struct nvmet_req *req) { return ((u32)le16_to_cpu(req->cmd->rw.length) + 1) << From 3a9b4dd41647bcfdeb6fce75c6522feb7724b9db Mon Sep 17 00:00:00 2001 From: Chris Leech Date: Tue, 14 Jan 2025 13:08:21 -0800 Subject: [PATCH 16/36] nvmet: put all nvmet_req.execute calls behind a function name This is setup for being able to manipluate the outstanding requests. Signed-off-by: Chris Leech --- drivers/nvme/target/fc.c | 4 ++-- drivers/nvme/target/loop.c | 2 +- drivers/nvme/target/nvmet.h | 1 + drivers/nvme/target/rdma.c | 4 ++-- drivers/nvme/target/tcp.c | 4 ++-- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/drivers/nvme/target/fc.c b/drivers/nvme/target/fc.c index d161707559ce69..36cc57005969d8 100644 --- a/drivers/nvme/target/fc.c +++ b/drivers/nvme/target/fc.c @@ -2390,7 +2390,7 @@ nvmet_fc_fod_op_done(struct nvmet_fc_fcp_iod *fod) } /* data transfer complete, resume with nvmet layer */ - fod->req.execute(&fod->req); + nvmet_execute_request(&fod->req); break; case NVMET_FCOP_READDATA: @@ -2599,7 +2599,7 @@ nvmet_fc_handle_fcp_rqst(struct nvmet_fc_tgtport *tgtport, * can invoke the nvmet_layer now. If read data, cmd completion will * push the data */ - fod->req.execute(&fod->req); + nvmet_execute_request(&fod->req); return; transport_error: diff --git a/drivers/nvme/target/loop.c b/drivers/nvme/target/loop.c index c9cf0ddc1b8563..e521d5810150d0 100644 --- a/drivers/nvme/target/loop.c +++ b/drivers/nvme/target/loop.c @@ -127,7 +127,7 @@ static void nvme_loop_execute_work(struct work_struct *work) struct nvme_loop_iod *iod = container_of(work, struct nvme_loop_iod, work); - iod->req.execute(&iod->req); + nvmet_execute_request(&iod->req); } static blk_status_t nvme_loop_queue_rq(struct blk_mq_hw_ctx *hctx, diff --git a/drivers/nvme/target/nvmet.h b/drivers/nvme/target/nvmet.h index 2f19728881fee7..55a1a0310c164e 100644 --- a/drivers/nvme/target/nvmet.h +++ b/drivers/nvme/target/nvmet.h @@ -993,4 +993,5 @@ struct nvmet_feat_arbitration { u8 ab; }; +static inline void nvmet_execute_request(struct nvmet_req *req) { req->execute(req); } #endif /* _NVMET_H */ diff --git a/drivers/nvme/target/rdma.c b/drivers/nvme/target/rdma.c index 2d6eb89f98af2d..c9cb70e30788b2 100644 --- a/drivers/nvme/target/rdma.c +++ b/drivers/nvme/target/rdma.c @@ -772,7 +772,7 @@ static void nvmet_rdma_read_data_done(struct ib_cq *cq, struct ib_wc *wc) if (unlikely(status)) nvmet_req_complete(&rsp->req, status); else - rsp->req.execute(&rsp->req); + nvmet_execute_request(&rsp->req); } static void nvmet_rdma_write_data_done(struct ib_cq *cq, struct ib_wc *wc) @@ -957,7 +957,7 @@ static bool nvmet_rdma_execute_command(struct nvmet_rdma_rsp *rsp) queue->cm_id->port_num, &rsp->read_cqe, NULL)) nvmet_req_complete(&rsp->req, NVME_SC_DATA_XFER_ERROR); } else { - rsp->req.execute(&rsp->req); + nvmet_execute_request(&rsp->req); } return true; diff --git a/drivers/nvme/target/tcp.c b/drivers/nvme/target/tcp.c index 164a564ba3b4e9..ef7cdc96ad57e0 100644 --- a/drivers/nvme/target/tcp.c +++ b/drivers/nvme/target/tcp.c @@ -622,7 +622,7 @@ static void nvmet_tcp_execute_request(struct nvmet_tcp_cmd *cmd) if (unlikely(cmd->flags & NVMET_TCP_F_INIT_FAILED)) nvmet_tcp_queue_response(&cmd->req); else - cmd->req.execute(&cmd->req); + nvmet_execute_request(&cmd->req); } static int nvmet_try_send_data_pdu(struct nvmet_tcp_cmd *cmd) @@ -1116,7 +1116,7 @@ static int nvmet_tcp_done_recv_pdu(struct nvmet_tcp_queue *queue) goto out; } - queue->cmd->req.execute(&queue->cmd->req); + nvmet_execute_request(&queue->cmd->req); out: nvmet_prepare_receive_pdu(queue); return ret; From 24173479bbe157a3e7a6a608d4b61f77f1e2c978 Mon Sep 17 00:00:00 2001 From: Chris Leech Date: Tue, 18 Mar 2025 15:38:05 -0700 Subject: [PATCH 17/36] nvmet: add delay debugfs file to nvmet_ctrl Creates an delay attribute file on the controler in debugfs /sys/kernel/debug/nvmet//ctrlN/delay Reading this file returns two numbers, a reqeust delay count and a delay time in ms. Each delayed request will decrement the delay count until it reaches 0. Writing to this file can set both the delay and count at once, or just the count to trigger more delays. # delay the next 5 request by 5 seconds each echo 5 5000 > delay # set the delay time to 3 seconds without starting a count echo 0 3000 > delay # delay to the next 5 requests by the current delay time echo 5 > delay Signed-off-by: Chris Leech --- drivers/nvme/target/Kconfig | 9 ++++++++ drivers/nvme/target/debugfs.c | 40 +++++++++++++++++++++++++++++++++++ drivers/nvme/target/nvmet.h | 4 ++++ 3 files changed, 53 insertions(+) diff --git a/drivers/nvme/target/Kconfig b/drivers/nvme/target/Kconfig index 4904097dfd490f..49bd56edcadce3 100644 --- a/drivers/nvme/target/Kconfig +++ b/drivers/nvme/target/Kconfig @@ -127,3 +127,12 @@ config NVME_TARGET_PCI_EPF capable PCI controller. If unsure, say N. + +config NVME_TARGET_DELAY_REQUESTS + bool "NVMe over Fabrics target request delay" + depends on NVME_TARGET && NVME_TARGET_DEBUGFS + help + This is a testing feature to allow delaying request completion in an + NVMe over Fabrics target, which allows for support of the cancel command. + + If unsure, say N. diff --git a/drivers/nvme/target/debugfs.c b/drivers/nvme/target/debugfs.c index 5dcbd5aa86e194..0571f86e07e5fe 100644 --- a/drivers/nvme/target/debugfs.c +++ b/drivers/nvme/target/debugfs.c @@ -153,6 +153,42 @@ static int nvmet_ctrl_tls_concat_show(struct seq_file *m, void *p) NVMET_DEBUGFS_ATTR(nvmet_ctrl_tls_concat); #endif +#if IS_ENABLED(CONFIG_NVME_TARGET_DELAY_REQUESTS) +static int nvmet_ctrl_delay_show(struct seq_file *m, void *p) +{ + struct nvmet_ctrl *ctrl = m->private; + int delay_count = atomic_read(&ctrl->delay_count); + + seq_printf(m, "%u %u\n", delay_count, ctrl->delay_msec); + return 0; +} + +static ssize_t nvmet_ctrl_delay_write(struct file *file, const char __user *buf, + size_t count, loff_t *ppos) +{ + struct seq_file *m = file->private_data; + struct nvmet_ctrl *ctrl = m->private; + char delay_buf[22] = {}; + int delay_count; + int delay_msec; + int n; + + if (count >= sizeof(delay_buf)) + return -EINVAL; + if (copy_from_user(delay_buf, buf, count)) + return -EFAULT; + + n = sscanf(delay_buf, "%u %u", &delay_count, &delay_msec); + if (n < 1 || n > 2) + return -EINVAL; + if (n == 2) + ctrl->delay_msec = delay_msec; + atomic_set(&ctrl->delay_count, delay_count); + return count; +} +NVMET_DEBUGFS_RW_ATTR(nvmet_ctrl_delay); +#endif /* CONFIG_NVME_TARGET_DELAY_REQUESTS */ + int nvmet_debugfs_ctrl_setup(struct nvmet_ctrl *ctrl) { char name[32]; @@ -183,6 +219,10 @@ int nvmet_debugfs_ctrl_setup(struct nvmet_ctrl *ctrl) &nvmet_ctrl_tls_concat_fops); debugfs_create_file("tls_key", S_IRUSR, ctrl->debugfs_dir, ctrl, &nvmet_ctrl_tls_key_fops); +#endif +#if IS_ENABLED(CONFIG_NVME_TARGET_DELAY_REQUESTS) + debugfs_create_file("delay", S_IWUSR, ctrl->debugfs_dir, ctrl, + &nvmet_ctrl_delay_fops); #endif return 0; } diff --git a/drivers/nvme/target/nvmet.h b/drivers/nvme/target/nvmet.h index 55a1a0310c164e..5499a7a862e914 100644 --- a/drivers/nvme/target/nvmet.h +++ b/drivers/nvme/target/nvmet.h @@ -311,6 +311,10 @@ struct nvmet_ctrl { #endif #ifdef CONFIG_NVME_TARGET_TCP_TLS struct key *tls_key; +#endif +#ifdef CONFIG_NVME_TARGET_DELAY_REQUESTS + atomic_t delay_count; + u32 delay_msec; #endif struct nvmet_pr_log_mgr pr_log_mgr; }; From 9b5222a164db3945abf2c98a85d736b4f2b5fe16 Mon Sep 17 00:00:00 2001 From: Chris Leech Date: Wed, 15 Jan 2025 20:58:07 -0800 Subject: [PATCH 18/36] nvmet: delay requests Allow requests to be delayed for testing. Signed-off-by: Chris Leech --- drivers/nvme/target/core.c | 30 ++++++++++++++++++++++++++++++ drivers/nvme/target/nvmet.h | 8 ++++++++ 2 files changed, 38 insertions(+) diff --git a/drivers/nvme/target/core.c b/drivers/nvme/target/core.c index 5b772e45baa6f2..d7c4b1bd1885a0 100644 --- a/drivers/nvme/target/core.c +++ b/drivers/nvme/target/core.c @@ -7,7 +7,9 @@ #include #include #include +#include #include +#include #include #include @@ -1803,6 +1805,34 @@ ssize_t nvmet_ctrl_host_traddr(struct nvmet_ctrl *ctrl, return ctrl->ops->host_traddr(ctrl, traddr, traddr_len); } +#if IS_ENABLED(CONFIG_NVME_TARGET_DELAY_REQUESTS) +static void nvmet_delayed_execute_req(struct work_struct *work) { + struct nvmet_req *req = + container_of(to_delayed_work(work), struct nvmet_req, req_work); + req->execute(req); +} + +void nvmet_execute_request(struct nvmet_req *req) { + struct nvmet_ctrl *ctrl = req->sq->ctrl; + int delay_count; + u32 delay_msec; + + if (unlikely(req->sq->qid == 0)) + return req->execute(req); + + if (ctrl) { + delay_count = atomic_dec_if_positive(&ctrl->delay_count) + 1; + delay_msec = ctrl->delay_msec; + } + if (!(ctrl && delay_count && delay_msec)) + return req->execute(req); + + INIT_DELAYED_WORK(&req->req_work, nvmet_delayed_execute_req); + queue_delayed_work(nvmet_wq, &req->req_work, msecs_to_jiffies(delay_msec)); +} +EXPORT_SYMBOL_GPL(nvmet_execute_request); +#endif + static struct nvmet_subsys *nvmet_find_get_subsys(struct nvmet_port *port, const char *subsysnqn) { diff --git a/drivers/nvme/target/nvmet.h b/drivers/nvme/target/nvmet.h index 5499a7a862e914..abd6f0eb2fd911 100644 --- a/drivers/nvme/target/nvmet.h +++ b/drivers/nvme/target/nvmet.h @@ -501,6 +501,9 @@ struct nvmet_req { u16 error_loc; u64 error_slba; struct nvmet_pr_per_ctrl_ref *pc_ref; +#if IS_ENABLED(CONFIG_NVME_TARGET_DELAY_REQUESTS) + struct delayed_work req_work; +#endif }; #define NVMET_MAX_MPOOL_BVEC 16 @@ -997,5 +1000,10 @@ struct nvmet_feat_arbitration { u8 ab; }; +#if IS_ENABLED(CONFIG_NVME_TARGET_DELAY_REQUESTS) +void nvmet_execute_request(struct nvmet_req *req); +#else static inline void nvmet_execute_request(struct nvmet_req *req) { req->execute(req); } +#endif + #endif /* _NVMET_H */ From a3fa2add031b34e586d2c070b58cb96ce74999a4 Mon Sep 17 00:00:00 2001 From: Chris Leech Date: Wed, 15 Jan 2025 16:17:44 -0800 Subject: [PATCH 19/36] nvmet: command tracking Enable tracking of all outstanding requests in an XArray Signed-off-by: Chris Leech --- drivers/nvme/target/core.c | 28 ++++++++++++++++++++++++++++ drivers/nvme/target/nvmet.h | 4 ++++ 2 files changed, 32 insertions(+) diff --git a/drivers/nvme/target/core.c b/drivers/nvme/target/core.c index d7c4b1bd1885a0..d10a6e75d4e2e1 100644 --- a/drivers/nvme/target/core.c +++ b/drivers/nvme/target/core.c @@ -809,10 +809,23 @@ static void __nvmet_req_complete(struct nvmet_req *req, u16 status) nvmet_put_namespace(ns); } +#if IS_ENABLED(CONFIG_NVME_TARGET_DELAY_REQUESTS) +static void nvmet_delayed_execute_req(struct work_struct *work); +#endif + void nvmet_req_complete(struct nvmet_req *req, u16 status) { struct nvmet_sq *sq = req->sq; +#if IS_ENABLED(CONFIG_NVME_TARGET_DELAY_REQUESTS) + unsigned long flags; + /* only need to update the xarray if this was a delayed request */ + if (req->req_work.work.func == nvmet_delayed_execute_req) { + xa_lock_irqsave(&sq->outstanding_requests, flags); + __xa_erase(&sq->outstanding_requests, req->cmd->common.command_id); + xa_unlock_irqrestore(&sq->outstanding_requests, flags); + } +#endif __nvmet_req_complete(req, status); percpu_ref_put(&sq->ref); } @@ -1033,6 +1046,9 @@ int nvmet_sq_init(struct nvmet_sq *sq, struct nvmet_cq *cq) nvmet_auth_sq_init(sq); sq->cq = cq; +#if IS_ENABLED(CONFIG_NVME_TARGET_DELAY_REQUESTS) + xa_init_flags(&sq->outstanding_requests, XA_FLAGS_LOCK_IRQ); +#endif return 0; } EXPORT_SYMBOL_GPL(nvmet_sq_init); @@ -1816,6 +1832,7 @@ void nvmet_execute_request(struct nvmet_req *req) { struct nvmet_ctrl *ctrl = req->sq->ctrl; int delay_count; u32 delay_msec; + unsigned long flags; if (unlikely(req->sq->qid == 0)) return req->execute(req); @@ -1827,6 +1844,17 @@ void nvmet_execute_request(struct nvmet_req *req) { if (!(ctrl && delay_count && delay_msec)) return req->execute(req); + xa_lock_irqsave(&req->sq->outstanding_requests, flags); + int ret = __xa_insert(&req->sq->outstanding_requests, + req->cmd->common.command_id, req, GFP_KERNEL); + xa_unlock_irqrestore(&req->sq->outstanding_requests, flags); + + if (ret) { + pr_err("nvmet: failure to delay command %d", + req->cmd->common.command_id); + return req->execute(req); + } + INIT_DELAYED_WORK(&req->req_work, nvmet_delayed_execute_req); queue_delayed_work(nvmet_wq, &req->req_work, msecs_to_jiffies(delay_msec)); } diff --git a/drivers/nvme/target/nvmet.h b/drivers/nvme/target/nvmet.h index abd6f0eb2fd911..d4b54bd7c8470d 100644 --- a/drivers/nvme/target/nvmet.h +++ b/drivers/nvme/target/nvmet.h @@ -176,6 +176,9 @@ struct nvmet_sq { #endif struct completion free_done; struct completion confirm_done; +#if IS_ENABLED(CONFIG_NVME_TARGET_DELAY_REQUESTS) + struct xarray outstanding_requests; +#endif }; struct nvmet_ana_group { @@ -580,6 +583,7 @@ size_t nvmet_req_transfer_len(struct nvmet_req *req); bool nvmet_check_transfer_len(struct nvmet_req *req, size_t len); bool nvmet_check_data_len_lte(struct nvmet_req *req, size_t data_len); void nvmet_req_complete(struct nvmet_req *req, u16 status); +void nvmet_req_complete_delayed(struct nvmet_req *req, u16 status); int nvmet_req_alloc_sgls(struct nvmet_req *req); void nvmet_req_free_sgls(struct nvmet_req *req); From eac20da87fbeaa848efeeafc5dc432da74f233a8 Mon Sep 17 00:00:00 2001 From: Chris Leech Date: Tue, 4 Feb 2025 10:17:31 -0800 Subject: [PATCH 20/36] nvmet: cancel implementation Requests are canceled if the have been delayed and cancel_delayed_work is successfull. replace emulate_cancel_commands modparam with the delay kconfig Signed-off-by: Chris Leech --- drivers/nvme/target/Makefile | 3 ++- drivers/nvme/target/admin-cmd.c | 7 +++---- drivers/nvme/target/core.c | 9 +++------ drivers/nvme/target/io-cmd-cancel.c | 27 ++++++++++++++++++++++++++- drivers/nvme/target/nvmet.h | 2 -- 5 files changed, 34 insertions(+), 14 deletions(-) diff --git a/drivers/nvme/target/Makefile b/drivers/nvme/target/Makefile index a533357e9be4a7..5d9848605285bc 100644 --- a/drivers/nvme/target/Makefile +++ b/drivers/nvme/target/Makefile @@ -11,7 +11,8 @@ obj-$(CONFIG_NVME_TARGET_TCP) += nvmet-tcp.o obj-$(CONFIG_NVME_TARGET_PCI_EPF) += nvmet-pci-epf.o nvmet-y += core.o configfs.o admin-cmd.o fabrics-cmd.o \ - discovery.o io-cmd-file.o io-cmd-bdev.o pr.o io-cmd-cancel.o + discovery.o io-cmd-file.o io-cmd-bdev.o pr.o +nvmet-$(CONFIG_NVME_TARGET_DELAY_REQUESTS) += io-cmd-cancel.o nvmet-$(CONFIG_NVME_TARGET_DEBUGFS) += debugfs.o nvmet-$(CONFIG_NVME_TARGET_PASSTHRU) += passthru.o nvmet-$(CONFIG_BLK_DEV_ZONED) += zns.o diff --git a/drivers/nvme/target/admin-cmd.c b/drivers/nvme/target/admin-cmd.c index 480e3764d5eac9..3cca4f4221922d 100644 --- a/drivers/nvme/target/admin-cmd.c +++ b/drivers/nvme/target/admin-cmd.c @@ -389,10 +389,9 @@ static void nvmet_get_cmd_effects_nvm(struct nvme_effects_log *log) log->iocs[nvme_cmd_resv_release] = log->iocs[nvme_cmd_resv_report] = cpu_to_le32(NVME_CMD_EFFECTS_CSUPP); - if (emulate_cancel_support) { - log->iocs[nvme_cmd_cancel] = - cpu_to_le32(NVME_CMD_EFFECTS_CSUPP); - } +#if IS_ENABLED(CONFIG_NVME_TARGET_DELAY_REQUESTS) + log->iocs[nvme_cmd_cancel] = cpu_to_le32(NVME_CMD_EFFECTS_CSUPP); +#endif log->iocs[nvme_cmd_write] = log->iocs[nvme_cmd_write_zeroes] = cpu_to_le32(NVME_CMD_EFFECTS_CSUPP | NVME_CMD_EFFECTS_LBCC); diff --git a/drivers/nvme/target/core.c b/drivers/nvme/target/core.c index d10a6e75d4e2e1..c2150a3d9cf3a3 100644 --- a/drivers/nvme/target/core.c +++ b/drivers/nvme/target/core.c @@ -30,10 +30,6 @@ static DEFINE_IDA(cntlid_ida); struct workqueue_struct *nvmet_wq; EXPORT_SYMBOL_GPL(nvmet_wq); -bool emulate_cancel_support; -module_param(emulate_cancel_support, bool, 0644); -MODULE_PARM_DESC(emulate_cancel_support, "Emulate the cancel command support. Default = false"); - /* * This read/write semaphore is used to synchronize access to configuration * information on a target system that will result in discovery log page @@ -1127,12 +1123,13 @@ static u16 nvmet_parse_io_cmd(struct nvmet_req *req) if (nvmet_is_passthru_req(req)) return nvmet_parse_passthru_io_cmd(req); - if (emulate_cancel_support && - req->cmd->common.opcode == nvme_cmd_cancel) { +#if IS_ENABLED(CONFIG_NVME_TARGET_DELAY_REQUESTS) + if (req->cmd->common.opcode == nvme_cmd_cancel) { req->execute = nvmet_execute_cancel; if (req->cmd->cancel.nsid == NVME_NSID_ALL) return 0; } +#endif ret = nvmet_req_find_ns(req); if (unlikely(ret)) diff --git a/drivers/nvme/target/io-cmd-cancel.c b/drivers/nvme/target/io-cmd-cancel.c index 8f55458e646a25..8d7f363c916376 100644 --- a/drivers/nvme/target/io-cmd-cancel.c +++ b/drivers/nvme/target/io-cmd-cancel.c @@ -14,6 +14,8 @@ void nvmet_execute_cancel(struct nvmet_req *req) int ret = 0; struct nvmet_ctrl *ctrl = req->sq->ctrl; struct nvmet_sq *sq = req->sq; + struct nvmet_req *treq; + u32 canceled = 0; if (!nvmet_check_transfer_len(req, 0)) return; @@ -45,8 +47,31 @@ void nvmet_execute_cancel(struct nvmet_req *req) ret = NVME_SC_INVALID_FIELD | NVME_STATUS_DNR; } + if (!mult_cmds) { + treq = xa_load(&sq->outstanding_requests, cid); + if (treq) { + if (cancel_delayed_work(&treq->req_work)) { + pr_info("nvmet: CANCEL success: %d", cid); + nvmet_req_complete(treq, NVME_SC_ABORT_REQ); + canceled += 1; + } else { + pr_info("nvmet: CANCEL failed: %d", cid); + } + } else { + pr_info("nvmet: CANCEL request not found: %d", cid); + } + } else { + unsigned long ucid; + xa_for_each(&sq->outstanding_requests, ucid, treq) { + if (cancel_delayed_work(&treq->req_work)) { + nvmet_req_complete(treq, NVME_SC_ABORT_REQ); + canceled += 1; + } + } + pr_info("nvmet: CANCEL removed %d requests", canceled); + } exit: - nvmet_set_result(req, 0); + nvmet_set_result(req, canceled); nvmet_req_complete(req, ret); } diff --git a/drivers/nvme/target/nvmet.h b/drivers/nvme/target/nvmet.h index d4b54bd7c8470d..ef2354fa4a3c38 100644 --- a/drivers/nvme/target/nvmet.h +++ b/drivers/nvme/target/nvmet.h @@ -40,8 +40,6 @@ #define nvmet_for_each_enabled_ns(xa, index, entry) \ xa_for_each_marked(xa, index, entry, NVMET_NS_ENABLED) -extern bool emulate_cancel_support; - /* * Supported optional AENs: */ From 37b202f7334a323d29de24cfc4ad2ba962a2bc57 Mon Sep 17 00:00:00 2001 From: Mohamed Khalfella Date: Fri, 27 Mar 2026 17:43:32 -0700 Subject: [PATCH 21/36] nvmet: Rapid Path Failure Recovery set controller identify fields TP8028 Rapid Path Failure Recovery defined new fields in controller identify response. The newly defined fields are: - CIU (Controller Instance UNIQUIFIER): is an 8bit non-zero value that is assigned a random value when controller is first created. The value is expected to be incremented when RDY bit in CSTS register is asserted - CIRN (Controller Instance Random Number): is 64bit random value that gets generated when controller is created. CIRN is regenerated everytime RDY bit is CSTS register is asserted. - CCRL (Cross-Controller Reset Limit) is an 8bit value that defines the maximum number of in-progress controller reset operations. CCRL is hardcoded to 4 as recommended by TP8028. Signed-off-by: Mohamed Khalfella Reviewed-by: Hannes Reinecke --- drivers/nvme/target/admin-cmd.c | 5 +++++ drivers/nvme/target/core.c | 9 +++++++++ drivers/nvme/target/nvmet.h | 2 ++ include/linux/nvme.h | 10 ++++++++-- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/drivers/nvme/target/admin-cmd.c b/drivers/nvme/target/admin-cmd.c index 3cca4f4221922d..da096cf6646438 100644 --- a/drivers/nvme/target/admin-cmd.c +++ b/drivers/nvme/target/admin-cmd.c @@ -694,6 +694,11 @@ static void nvmet_execute_identify_ctrl(struct nvmet_req *req) id->mdts = nvmet_ctrl_mdts(req); id->cntlid = cpu_to_le16(ctrl->cntlid); id->ver = cpu_to_le32(ctrl->subsys->ver); + if (!nvmet_is_disc_subsys(ctrl->subsys)) { + id->ciu = ctrl->ciu; + id->cirn = cpu_to_le64(ctrl->cirn); + id->ccrl = NVMF_CCR_LIMIT; + } /* XXX: figure out what to do about RTD3R/RTD3 */ id->oaes = cpu_to_le32(NVMET_AEN_CFG_OPTIONAL); diff --git a/drivers/nvme/target/core.c b/drivers/nvme/target/core.c index c2150a3d9cf3a3..ec38ab8d385703 100644 --- a/drivers/nvme/target/core.c +++ b/drivers/nvme/target/core.c @@ -1428,6 +1428,10 @@ static void nvmet_start_ctrl(struct nvmet_ctrl *ctrl) return; } + if (!nvmet_is_disc_subsys(ctrl->subsys)) { + ctrl->ciu = ((u8)(ctrl->ciu + 1)) ? : 1; + ctrl->cirn = get_random_u64(); + } ctrl->csts = NVME_CSTS_RDY; /* @@ -1693,6 +1697,11 @@ struct nvmet_ctrl *nvmet_alloc_ctrl(struct nvmet_alloc_ctrl_args *args) } ctrl->cntlid = ret; + if (!nvmet_is_disc_subsys(ctrl->subsys)) { + ctrl->ciu = get_random_u8() ? : 1; + ctrl->cirn = get_random_u64(); + } + /* * Discovery controllers may use some arbitrary high value * in order to cleanup stale discovery sessions diff --git a/drivers/nvme/target/nvmet.h b/drivers/nvme/target/nvmet.h index ef2354fa4a3c38..ba776b556c5e63 100644 --- a/drivers/nvme/target/nvmet.h +++ b/drivers/nvme/target/nvmet.h @@ -268,7 +268,9 @@ struct nvmet_ctrl { uuid_t hostid; u16 cntlid; + u8 ciu; u32 kato; + u64 cirn; struct nvmet_port *port; diff --git a/include/linux/nvme.h b/include/linux/nvme.h index 524d864c8ac81e..6b0e8e239a5a11 100644 --- a/include/linux/nvme.h +++ b/include/linux/nvme.h @@ -21,6 +21,8 @@ #define NVMF_TRADDR_SIZE 256 #define NVMF_TSAS_SIZE 256 +#define NVMF_CCR_LIMIT 4 + #define NVME_DISC_SUBSYS_NAME "nqn.2014-08.org.nvmexpress.discovery" #define NVME_NSID_ALL 0xffffffff @@ -331,7 +333,10 @@ struct nvme_id_ctrl { __le16 crdt1; __le16 crdt2; __le16 crdt3; - __u8 rsvd134[122]; + __u8 rsvd134[1]; + __u8 ciu; + __le64 cirn; + __u8 rsvd144[112]; __le16 oacs; __u8 acl; __u8 aerl; @@ -392,7 +397,8 @@ struct nvme_id_ctrl { __u8 msdbd; __u8 rsvd1804[2]; __u8 dctype; - __u8 rsvd1807[241]; + __u8 ccrl; + __u8 rsvd1808[240]; struct nvme_id_power_state psd[32]; __u8 vs[1024]; }; From 0b12dd8146e8c4e783a9522e5077fa94074427af Mon Sep 17 00:00:00 2001 From: Mohamed Khalfella Date: Fri, 27 Mar 2026 17:43:33 -0700 Subject: [PATCH 22/36] nvmet/debugfs: Export controller CIU and CIRN via debugfs Export ctrl->ciu and ctrl->cirn as debugfs files under controller debugfs directory. Signed-off-by: Mohamed Khalfella Reviewed-by: Hannes Reinecke Reviewed-by: Sagi Grimberg --- drivers/nvme/target/debugfs.c | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/drivers/nvme/target/debugfs.c b/drivers/nvme/target/debugfs.c index 0571f86e07e5fe..8315b215a5827b 100644 --- a/drivers/nvme/target/debugfs.c +++ b/drivers/nvme/target/debugfs.c @@ -152,6 +152,23 @@ static int nvmet_ctrl_tls_concat_show(struct seq_file *m, void *p) } NVMET_DEBUGFS_ATTR(nvmet_ctrl_tls_concat); #endif +static int nvmet_ctrl_instance_ciu_show(struct seq_file *m, void *p) +{ + struct nvmet_ctrl *ctrl = m->private; + + seq_printf(m, "%02x\n", ctrl->ciu); + return 0; +} +NVMET_DEBUGFS_ATTR(nvmet_ctrl_instance_ciu); + +static int nvmet_ctrl_instance_cirn_show(struct seq_file *m, void *p) +{ + struct nvmet_ctrl *ctrl = m->private; + + seq_printf(m, "%016llx\n", ctrl->cirn); + return 0; +} +NVMET_DEBUGFS_ATTR(nvmet_ctrl_instance_cirn); #if IS_ENABLED(CONFIG_NVME_TARGET_DELAY_REQUESTS) static int nvmet_ctrl_delay_show(struct seq_file *m, void *p) @@ -224,6 +241,10 @@ int nvmet_debugfs_ctrl_setup(struct nvmet_ctrl *ctrl) debugfs_create_file("delay", S_IWUSR, ctrl->debugfs_dir, ctrl, &nvmet_ctrl_delay_fops); #endif + debugfs_create_file("ciu", S_IRUSR, ctrl->debugfs_dir, ctrl, + &nvmet_ctrl_instance_ciu_fops); + debugfs_create_file("cirn", S_IRUSR, ctrl->debugfs_dir, ctrl, + &nvmet_ctrl_instance_cirn_fops); return 0; } From 902f0a98a198fc1bd396cb5a151e380c27c77b8b Mon Sep 17 00:00:00 2001 From: Mohamed Khalfella Date: Fri, 27 Mar 2026 17:43:34 -0700 Subject: [PATCH 23/36] nvmet: Implement CCR nvme command Defined by TP8028 Rapid Path Failure Recovery, CCR (Cross-Controller Reset) command is an nvme command issued to source controller by initiator to reset impacted controller. Implement CCR command for linux nvme target. Signed-off-by: Mohamed Khalfella Reviewed-by: Hannes Reinecke --- drivers/nvme/target/admin-cmd.c | 74 ++++++++++++++++++++++++++++++++ drivers/nvme/target/core.c | 76 +++++++++++++++++++++++++++++++++ drivers/nvme/target/nvmet.h | 13 ++++++ include/linux/nvme.h | 23 ++++++++++ 4 files changed, 186 insertions(+) diff --git a/drivers/nvme/target/admin-cmd.c b/drivers/nvme/target/admin-cmd.c index da096cf6646438..62f43e8c7c6918 100644 --- a/drivers/nvme/target/admin-cmd.c +++ b/drivers/nvme/target/admin-cmd.c @@ -376,7 +376,9 @@ static void nvmet_get_cmd_effects_admin(struct nvmet_ctrl *ctrl, log->acs[nvme_admin_get_features] = log->acs[nvme_admin_async_event] = log->acs[nvme_admin_keep_alive] = + log->acs[nvme_admin_cross_ctrl_reset] = cpu_to_le32(NVME_CMD_EFFECTS_CSUPP); + } static void nvmet_get_cmd_effects_nvm(struct nvme_effects_log *log) @@ -1614,6 +1616,75 @@ void nvmet_execute_keep_alive(struct nvmet_req *req) nvmet_req_complete(req, status); } +void nvmet_execute_cross_ctrl_reset(struct nvmet_req *req) +{ + struct nvmet_ctrl *ictrl, *sctrl = req->sq->ctrl; + struct nvme_command *cmd = req->cmd; + struct nvmet_ccr *ccr, *new_ccr; + int ccr_active, ccr_total; + u16 cntlid, status = NVME_SC_SUCCESS; + + cntlid = le16_to_cpu(cmd->ccr.icid); + if (sctrl->cntlid == cntlid) { + req->error_loc = + offsetof(struct nvme_cross_ctrl_reset_cmd, icid); + status = NVME_SC_INVALID_FIELD | NVME_STATUS_DNR; + goto out; + } + + /* Find and get impacted controller */ + ictrl = nvmet_ctrl_find_get_ccr(sctrl->subsys, sctrl->hostnqn, + cmd->ccr.ciu, cntlid, + le64_to_cpu(cmd->ccr.cirn)); + if (!ictrl) { + /* Immediate Reset Successful */ + nvmet_set_result(req, 1); + status = NVME_SC_SUCCESS; + goto out; + } + + ccr_total = ccr_active = 0; + mutex_lock(&sctrl->lock); + list_for_each_entry(ccr, &sctrl->ccr_list, entry) { + if (ccr->ctrl == ictrl) { + status = NVME_SC_CCR_IN_PROGRESS | NVME_STATUS_DNR; + goto out_unlock; + } + + ccr_total++; + if (ccr->ctrl) + ccr_active++; + } + + if (ccr_active >= NVMF_CCR_LIMIT) { + status = NVME_SC_CCR_LIMIT_EXCEEDED; + goto out_unlock; + } + if (ccr_total >= NVMF_CCR_PER_PAGE) { + status = NVME_SC_CCR_LOGPAGE_FULL; + goto out_unlock; + } + + new_ccr = kmalloc_obj(*new_ccr, GFP_KERNEL); + if (!new_ccr) { + status = NVME_SC_INTERNAL; + goto out_unlock; + } + + new_ccr->ciu = cmd->ccr.ciu; + new_ccr->icid = cntlid; + new_ccr->ctrl = ictrl; + list_add_tail(&new_ccr->entry, &sctrl->ccr_list); + +out_unlock: + mutex_unlock(&sctrl->lock); + if (status == NVME_SC_SUCCESS) + nvmet_ctrl_fatal_error(ictrl); + nvmet_ctrl_put(ictrl); +out: + nvmet_req_complete(req, status); +} + u32 nvmet_admin_cmd_data_len(struct nvmet_req *req) { struct nvme_command *cmd = req->cmd; @@ -1691,6 +1762,9 @@ u16 nvmet_parse_admin_cmd(struct nvmet_req *req) case nvme_admin_keep_alive: req->execute = nvmet_execute_keep_alive; return 0; + case nvme_admin_cross_ctrl_reset: + req->execute = nvmet_execute_cross_ctrl_reset; + return 0; default: return nvmet_report_invalid_opcode(req); } diff --git a/drivers/nvme/target/core.c b/drivers/nvme/target/core.c index ec38ab8d385703..d59fe2ff0ee9d6 100644 --- a/drivers/nvme/target/core.c +++ b/drivers/nvme/target/core.c @@ -117,6 +117,20 @@ u16 nvmet_zero_sgl(struct nvmet_req *req, off_t off, size_t len) return 0; } +void nvmet_ctrl_cleanup_ccrs(struct nvmet_ctrl *ctrl, bool all) +{ + struct nvmet_ccr *ccr, *tmp; + + lockdep_assert_held(&ctrl->lock); + + list_for_each_entry_safe(ccr, tmp, &ctrl->ccr_list, entry) { + if (all || ccr->ctrl == NULL) { + list_del(&ccr->entry); + kfree(ccr); + } + } +} + static u32 nvmet_max_nsid(struct nvmet_subsys *subsys) { struct nvmet_ns *cur; @@ -1431,6 +1445,7 @@ static void nvmet_start_ctrl(struct nvmet_ctrl *ctrl) if (!nvmet_is_disc_subsys(ctrl->subsys)) { ctrl->ciu = ((u8)(ctrl->ciu + 1)) ? : 1; ctrl->cirn = get_random_u64(); + nvmet_ctrl_cleanup_ccrs(ctrl, false); } ctrl->csts = NVME_CSTS_RDY; @@ -1536,6 +1551,35 @@ struct nvmet_ctrl *nvmet_ctrl_find_get(const char *subsysnqn, return ctrl; } +struct nvmet_ctrl *nvmet_ctrl_find_get_ccr(struct nvmet_subsys *subsys, + const char *hostnqn, u8 ciu, + u16 cntlid, u64 cirn) +{ + struct nvmet_ctrl *ctrl, *ictrl = NULL; + bool found = false; + + mutex_lock(&subsys->lock); + list_for_each_entry(ctrl, &subsys->ctrls, subsys_entry) { + if (ctrl->cntlid != cntlid) + continue; + + /* Avoid racing with a controller that is becoming ready */ + mutex_lock(&ctrl->lock); + if (ctrl->ciu == ciu && ctrl->cirn == cirn) + found = true; + mutex_unlock(&ctrl->lock); + + if (found) { + if (kref_get_unless_zero(&ctrl->ref)) + ictrl = ctrl; + break; + } + }; + mutex_unlock(&subsys->lock); + + return ictrl; +} + u16 nvmet_check_ctrl_status(struct nvmet_req *req) { if (unlikely(!(req->sq->ctrl->cc & NVME_CC_ENABLE))) { @@ -1661,6 +1705,7 @@ struct nvmet_ctrl *nvmet_alloc_ctrl(struct nvmet_alloc_ctrl_args *args) subsys->clear_ids = 1; #endif + INIT_LIST_HEAD(&ctrl->ccr_list); INIT_WORK(&ctrl->async_event_work, nvmet_async_event_work); INIT_LIST_HEAD(&ctrl->async_events); INIT_RADIX_TREE(&ctrl->p2p_ns_map, GFP_KERNEL); @@ -1771,12 +1816,43 @@ struct nvmet_ctrl *nvmet_alloc_ctrl(struct nvmet_alloc_ctrl_args *args) } EXPORT_SYMBOL_GPL(nvmet_alloc_ctrl); +static void nvmet_ctrl_complete_pending_ccr(struct nvmet_ctrl *ctrl) +{ + struct nvmet_subsys *subsys = ctrl->subsys; + struct nvmet_ctrl *sctrl; + struct nvmet_ccr *ccr; + + lockdep_assert_held(&subsys->lock); + + /* Cleanup all CCRs issued by ctrl as source controller */ + mutex_lock(&ctrl->lock); + nvmet_ctrl_cleanup_ccrs(ctrl, true); + mutex_unlock(&ctrl->lock); + + /* + * Find all CCRs targeting ctrl as impacted controller and + * set ccr->ctrl to NULL. This tells the source controller + * that CCR completed successfully. + */ + list_for_each_entry(sctrl, &subsys->ctrls, subsys_entry) { + mutex_lock(&sctrl->lock); + list_for_each_entry(ccr, &sctrl->ccr_list, entry) { + if (ccr->ctrl == ctrl) { + ccr->ctrl = NULL; + break; + } + } + mutex_unlock(&sctrl->lock); + } +} + static void nvmet_ctrl_free(struct kref *ref) { struct nvmet_ctrl *ctrl = container_of(ref, struct nvmet_ctrl, ref); struct nvmet_subsys *subsys = ctrl->subsys; mutex_lock(&subsys->lock); + nvmet_ctrl_complete_pending_ccr(ctrl); nvmet_ctrl_destroy_pr(ctrl); nvmet_release_p2p_ns_map(ctrl); list_del(&ctrl->subsys_entry); diff --git a/drivers/nvme/target/nvmet.h b/drivers/nvme/target/nvmet.h index ba776b556c5e63..d47d2fb624d6ff 100644 --- a/drivers/nvme/target/nvmet.h +++ b/drivers/nvme/target/nvmet.h @@ -272,6 +272,7 @@ struct nvmet_ctrl { u32 kato; u64 cirn; + struct list_head ccr_list; struct nvmet_port *port; u32 aen_enabled; @@ -322,6 +323,13 @@ struct nvmet_ctrl { struct nvmet_pr_log_mgr pr_log_mgr; }; +struct nvmet_ccr { + struct nvmet_ctrl *ctrl; + struct list_head entry; + u16 icid; + u8 ciu; +}; + struct nvmet_subsys { enum nvme_subsys_type type; @@ -590,6 +598,7 @@ void nvmet_req_free_sgls(struct nvmet_req *req); void nvmet_execute_set_features(struct nvmet_req *req); void nvmet_execute_get_features(struct nvmet_req *req); void nvmet_execute_keep_alive(struct nvmet_req *req); +void nvmet_execute_cross_ctrl_reset(struct nvmet_req *req); u16 nvmet_check_cqid(struct nvmet_ctrl *ctrl, u16 cqid, bool create); u16 nvmet_check_io_cqid(struct nvmet_ctrl *ctrl, u16 cqid, bool create); @@ -632,6 +641,10 @@ struct nvmet_ctrl *nvmet_alloc_ctrl(struct nvmet_alloc_ctrl_args *args); struct nvmet_ctrl *nvmet_ctrl_find_get(const char *subsysnqn, const char *hostnqn, u16 cntlid, struct nvmet_req *req); +struct nvmet_ctrl *nvmet_ctrl_find_get_ccr(struct nvmet_subsys *subsys, + const char *hostnqn, u8 ciu, + u16 cntlid, u64 cirn); +void nvmet_ctrl_cleanup_ccrs(struct nvmet_ctrl *ctrl, bool all); void nvmet_ctrl_put(struct nvmet_ctrl *ctrl); u16 nvmet_check_ctrl_status(struct nvmet_req *req); ssize_t nvmet_ctrl_host_traddr(struct nvmet_ctrl *ctrl, diff --git a/include/linux/nvme.h b/include/linux/nvme.h index 6b0e8e239a5a11..10b2b2e6f44ca9 100644 --- a/include/linux/nvme.h +++ b/include/linux/nvme.h @@ -22,6 +22,7 @@ #define NVMF_TSAS_SIZE 256 #define NVMF_CCR_LIMIT 4 +#define NVMF_CCR_PER_PAGE 511 #define NVME_DISC_SUBSYS_NAME "nqn.2014-08.org.nvmexpress.discovery" @@ -1237,6 +1238,22 @@ struct nvme_zone_mgmt_recv_cmd { __le32 cdw14[2]; }; +struct nvme_cross_ctrl_reset_cmd { + __u8 opcode; + __u8 flags; + __u16 command_id; + __le32 nsid; + __le64 rsvd2[2]; + union nvme_data_ptr dptr; + __le16 icid; + __u8 ciu; + __u8 rsvd10; + __le32 cdw11; + __le64 cirn; + __le32 cdw14; + __le32 cdw15; +}; + struct nvme_io_mgmt_recv_cmd { __u8 opcode; __u8 flags; @@ -1335,6 +1352,7 @@ enum nvme_admin_opcode { nvme_admin_virtual_mgmt = 0x1c, nvme_admin_nvme_mi_send = 0x1d, nvme_admin_nvme_mi_recv = 0x1e, + nvme_admin_cross_ctrl_reset = 0x38, nvme_admin_dbbuf = 0x7C, nvme_admin_format_nvm = 0x80, nvme_admin_security_send = 0x81, @@ -1368,6 +1386,7 @@ enum nvme_admin_opcode { nvme_admin_opcode_name(nvme_admin_virtual_mgmt), \ nvme_admin_opcode_name(nvme_admin_nvme_mi_send), \ nvme_admin_opcode_name(nvme_admin_nvme_mi_recv), \ + nvme_admin_opcode_name(nvme_admin_cross_ctrl_reset), \ nvme_admin_opcode_name(nvme_admin_dbbuf), \ nvme_admin_opcode_name(nvme_admin_format_nvm), \ nvme_admin_opcode_name(nvme_admin_security_send), \ @@ -2043,6 +2062,7 @@ struct nvme_command { struct nvme_dbbuf dbbuf; struct nvme_directive_cmd directive; struct nvme_io_mgmt_recv_cmd imr; + struct nvme_cross_ctrl_reset_cmd ccr; }; }; @@ -2207,6 +2227,9 @@ enum { NVME_SC_PMR_SAN_PROHIBITED = 0x123, NVME_SC_ANA_GROUP_ID_INVALID = 0x124, NVME_SC_ANA_ATTACH_FAILED = 0x125, + NVME_SC_CCR_IN_PROGRESS = 0x13f, + NVME_SC_CCR_LOGPAGE_FULL = 0x140, + NVME_SC_CCR_LIMIT_EXCEEDED = 0x141, /* * I/O Command Set Specific - NVM commands: From d9d5696c5f2581c49e41a1ed0a500384df7cac21 Mon Sep 17 00:00:00 2001 From: Mohamed Khalfella Date: Fri, 27 Mar 2026 17:43:35 -0700 Subject: [PATCH 24/36] nvmet: Implement CCR logpage Defined by TP8028 Rapid Path Failure Recovery, CCR (Cross-Controller Reset) log page contains an entry for each CCR request submitted to source controller. Implement CCR logpage for nvme linux target. Signed-off-by: Mohamed Khalfella Reviewed-by: Hannes Reinecke Reviewed-by: Sagi Grimberg Reviewed-by: Randy Jennings --- drivers/nvme/target/admin-cmd.c | 44 +++++++++++++++++++++++++++++++++ include/linux/nvme.h | 29 ++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/drivers/nvme/target/admin-cmd.c b/drivers/nvme/target/admin-cmd.c index 62f43e8c7c6918..49e4c68472e7a7 100644 --- a/drivers/nvme/target/admin-cmd.c +++ b/drivers/nvme/target/admin-cmd.c @@ -220,6 +220,7 @@ static void nvmet_execute_get_supported_log_pages(struct nvmet_req *req) logs->lids[NVME_LOG_FEATURES] = cpu_to_le32(NVME_LIDS_LSUPP); logs->lids[NVME_LOG_RMI] = cpu_to_le32(NVME_LIDS_LSUPP); logs->lids[NVME_LOG_RESERVATION] = cpu_to_le32(NVME_LIDS_LSUPP); + logs->lids[NVME_LOG_CCR] = cpu_to_le32(NVME_LIDS_LSUPP); status = nvmet_copy_to_sgl(req, 0, logs, sizeof(*logs)); kfree(logs); @@ -610,6 +611,47 @@ static void nvmet_execute_get_log_page_features(struct nvmet_req *req) nvmet_req_complete(req, status); } +static void nvmet_execute_get_log_page_ccr(struct nvmet_req *req) +{ + struct nvmet_ctrl *ctrl = req->sq->ctrl; + struct nvmet_ccr *ccr; + struct nvme_ccr_log *log; + int index = 0; + u16 status; + + log = kzalloc(sizeof(*log), GFP_KERNEL); + if (!log) { + status = NVME_SC_INTERNAL; + goto out; + } + + mutex_lock(&ctrl->lock); + list_for_each_entry(ccr, &ctrl->ccr_list, entry) { + u8 flags = NVME_CCR_FLAGS_VALIDATED | NVME_CCR_FLAGS_INITIATED; + u8 status = ccr->ctrl ? NVME_CCR_STATUS_IN_PROGRESS : + NVME_CCR_STATUS_SUCCESS; + + log->entries[index].icid = cpu_to_le16(ccr->icid); + log->entries[index].ciu = ccr->ciu; + log->entries[index].acid = cpu_to_le16(0xffff); + log->entries[index].ccrs = status; + log->entries[index].ccrf = flags; + index++; + } + + /* Cleanup completed CCRs if requested */ + if (req->cmd->get_log_page.lsp & 0x1) + nvmet_ctrl_cleanup_ccrs(ctrl, false); + mutex_unlock(&ctrl->lock); + + log->ne = cpu_to_le16(index); + nvmet_clear_aen_bit(req, NVME_AEN_BIT_CCR_COMPLETE); + status = nvmet_copy_to_sgl(req, 0, log, sizeof(*log)); + kfree(log); +out: + nvmet_req_complete(req, status); +} + static void nvmet_execute_get_log_page(struct nvmet_req *req) { if (!nvmet_check_transfer_len(req, nvmet_get_log_page_len(req->cmd))) @@ -643,6 +685,8 @@ static void nvmet_execute_get_log_page(struct nvmet_req *req) return nvmet_execute_get_log_page_rmi(req); case NVME_LOG_RESERVATION: return nvmet_execute_get_log_page_resv(req); + case NVME_LOG_CCR: + return nvmet_execute_get_log_page_ccr(req); } pr_debug("unhandled lid %d on qid %d\n", req->cmd->get_log_page.lid, req->sq->qid); diff --git a/include/linux/nvme.h b/include/linux/nvme.h index 10b2b2e6f44ca9..60a9bb8ad46bfc 100644 --- a/include/linux/nvme.h +++ b/include/linux/nvme.h @@ -1447,6 +1447,7 @@ enum { NVME_LOG_FDP_CONFIGS = 0x20, NVME_LOG_DISC = 0x70, NVME_LOG_RESERVATION = 0x80, + NVME_LOG_CCR = 0x1E, NVME_FWACT_REPL = (0 << 3), NVME_FWACT_REPL_ACTV = (1 << 3), NVME_FWACT_ACTV = (2 << 3), @@ -1470,6 +1471,34 @@ enum { NVME_FIS_CSCPE = 1 << 21, }; +/* NVMe Cross-Controller Reset Status */ +enum { + NVME_CCR_STATUS_IN_PROGRESS, + NVME_CCR_STATUS_SUCCESS, + NVME_CCR_STATUS_FAILED, +}; + +/* NVMe Cross-Controller Reset Flags */ +enum { + NVME_CCR_FLAGS_VALIDATED = 0x01, + NVME_CCR_FLAGS_INITIATED = 0x02, +}; + +struct nvme_ccr_log_entry { + __le16 icid; + __u8 ciu; + __u8 rsvd3; + __le16 acid; + __u8 ccrs; + __u8 ccrf; +}; + +struct nvme_ccr_log { + __le16 ne; + __u8 rsvd2[6]; + struct nvme_ccr_log_entry entries[NVMF_CCR_PER_PAGE]; +}; + /* NVMe Namespace Write Protect State */ enum { NVME_NS_NO_WRITE_PROTECT = 0, From 118e37be29fc111f7d4e736e8609f50d901dca8e Mon Sep 17 00:00:00 2001 From: Mohamed Khalfella Date: Fri, 27 Mar 2026 17:43:36 -0700 Subject: [PATCH 25/36] nvmet: Send an AEN on CCR completion Send an AEN to initiator when impacted controller exists. The notification points to CCR log page that initiator can read to check which CCR operation completed. Signed-off-by: Mohamed Khalfella Reviewed-by: Hannes Reinecke Reviewed-by: Sagi Grimberg --- drivers/nvme/target/core.c | 25 ++++++++++++++++++++++--- drivers/nvme/target/nvmet.h | 3 ++- include/linux/nvme.h | 3 +++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/drivers/nvme/target/core.c b/drivers/nvme/target/core.c index d59fe2ff0ee9d6..3facd83cdce1cf 100644 --- a/drivers/nvme/target/core.c +++ b/drivers/nvme/target/core.c @@ -205,7 +205,7 @@ static void nvmet_async_event_work(struct work_struct *work) nvmet_async_events_process(ctrl); } -void nvmet_add_async_event(struct nvmet_ctrl *ctrl, u8 event_type, +static void nvmet_add_async_event_locked(struct nvmet_ctrl *ctrl, u8 event_type, u8 event_info, u8 log_page) { struct nvmet_async_event *aen; @@ -218,13 +218,19 @@ void nvmet_add_async_event(struct nvmet_ctrl *ctrl, u8 event_type, aen->event_info = event_info; aen->log_page = log_page; - mutex_lock(&ctrl->lock); list_add_tail(&aen->entry, &ctrl->async_events); - mutex_unlock(&ctrl->lock); queue_work(nvmet_wq, &ctrl->async_event_work); } +void nvmet_add_async_event(struct nvmet_ctrl *ctrl, u8 event_type, + u8 event_info, u8 log_page) +{ + mutex_lock(&ctrl->lock); + nvmet_add_async_event_locked(ctrl, event_type, event_info, log_page); + mutex_unlock(&ctrl->lock); +} + static void nvmet_add_to_changed_ns_log(struct nvmet_ctrl *ctrl, __le32 nsid) { u32 i; @@ -1816,6 +1822,18 @@ struct nvmet_ctrl *nvmet_alloc_ctrl(struct nvmet_alloc_ctrl_args *args) } EXPORT_SYMBOL_GPL(nvmet_alloc_ctrl); +static void nvmet_ctrl_notify_ccr(struct nvmet_ctrl *ctrl) +{ + lockdep_assert_held(&ctrl->lock); + + if (nvmet_aen_bit_disabled(ctrl, NVME_AEN_BIT_CCR_COMPLETE)) + return; + + nvmet_add_async_event_locked(ctrl, NVME_AER_NOTICE, + NVME_AER_NOTICE_CCR_COMPLETED, + NVME_LOG_CCR); +} + static void nvmet_ctrl_complete_pending_ccr(struct nvmet_ctrl *ctrl) { struct nvmet_subsys *subsys = ctrl->subsys; @@ -1839,6 +1857,7 @@ static void nvmet_ctrl_complete_pending_ccr(struct nvmet_ctrl *ctrl) list_for_each_entry(ccr, &sctrl->ccr_list, entry) { if (ccr->ctrl == ctrl) { ccr->ctrl = NULL; + nvmet_ctrl_notify_ccr(sctrl); break; } } diff --git a/drivers/nvme/target/nvmet.h b/drivers/nvme/target/nvmet.h index d47d2fb624d6ff..2b01cef1fb8984 100644 --- a/drivers/nvme/target/nvmet.h +++ b/drivers/nvme/target/nvmet.h @@ -44,7 +44,8 @@ * Supported optional AENs: */ #define NVMET_AEN_CFG_OPTIONAL \ - (NVME_AEN_CFG_NS_ATTR | NVME_AEN_CFG_ANA_CHANGE) + (NVME_AEN_CFG_NS_ATTR | NVME_AEN_CFG_ANA_CHANGE | \ + NVME_AEN_CFG_CCR_COMPLETE) #define NVMET_DISC_AEN_CFG_OPTIONAL \ (NVME_AEN_CFG_DISC_CHANGE) diff --git a/include/linux/nvme.h b/include/linux/nvme.h index 60a9bb8ad46bfc..049e8bf538f485 100644 --- a/include/linux/nvme.h +++ b/include/linux/nvme.h @@ -874,12 +874,14 @@ enum { NVME_AER_NOTICE_FW_ACT_STARTING = 0x01, NVME_AER_NOTICE_ANA = 0x03, NVME_AER_NOTICE_DISC_CHANGED = 0xf0, + NVME_AER_NOTICE_CCR_COMPLETED = 0xf4, }; enum { NVME_AEN_BIT_NS_ATTR = 8, NVME_AEN_BIT_FW_ACT = 9, NVME_AEN_BIT_ANA_CHANGE = 11, + NVME_AEN_BIT_CCR_COMPLETE = 20, NVME_AEN_BIT_DISC_CHANGE = 31, }; @@ -887,6 +889,7 @@ enum { NVME_AEN_CFG_NS_ATTR = 1 << NVME_AEN_BIT_NS_ATTR, NVME_AEN_CFG_FW_ACT = 1 << NVME_AEN_BIT_FW_ACT, NVME_AEN_CFG_ANA_CHANGE = 1 << NVME_AEN_BIT_ANA_CHANGE, + NVME_AEN_CFG_CCR_COMPLETE = 1 << NVME_AEN_BIT_CCR_COMPLETE, NVME_AEN_CFG_DISC_CHANGE = 1 << NVME_AEN_BIT_DISC_CHANGE, }; From aeabd9a91868a953e293499a793402de0aef3050 Mon Sep 17 00:00:00 2001 From: Mohamed Khalfella Date: Fri, 27 Mar 2026 17:43:37 -0700 Subject: [PATCH 26/36] nvme: Rapid Path Failure Recovery read controller identify fields TP8028 Rapid path failure added new fileds to controller identify response. Read CIU (Controller Instance Uniquifier), CIRN (Controller Instance Random Number), and CCRL (Cross-Controller Reset Limit) from controller identify response. Expose CIU and CIRN as sysfs attributes so the values can be used directrly by user if needed. Signed-off-by: Mohamed Khalfella Reviewed-by: Hannes Reinecke Reviewed-by: Sagi Grimberg --- drivers/nvme/host/core.c | 4 ++++ drivers/nvme/host/nvme.h | 10 ++++++++++ drivers/nvme/host/sysfs.c | 23 +++++++++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c index 824317c6873af0..c1db1ba23d8342 100644 --- a/drivers/nvme/host/core.c +++ b/drivers/nvme/host/core.c @@ -3716,6 +3716,10 @@ static int nvme_init_identify(struct nvme_ctrl *ctrl) ctrl->crdt[1] = le16_to_cpu(id->crdt2); ctrl->crdt[2] = le16_to_cpu(id->crdt3); + ctrl->ciu = id->ciu; + ctrl->cirn = le64_to_cpu(id->cirn); + atomic_set(&ctrl->ccr_limit, id->ccrl); + ctrl->oacs = le16_to_cpu(id->oacs); ctrl->oncs = le16_to_cpu(id->oncs); ctrl->mtfa = le16_to_cpu(id->mtfa); diff --git a/drivers/nvme/host/nvme.h b/drivers/nvme/host/nvme.h index 6b8c159a62e061..0b86e5829c25ca 100644 --- a/drivers/nvme/host/nvme.h +++ b/drivers/nvme/host/nvme.h @@ -384,11 +384,14 @@ struct nvme_ctrl { u16 crdt[3]; u16 oncs; u8 dmrl; + u8 ciu; u32 dmrsl; + u64 cirn; u16 oacs; u16 sqsize; u32 max_namespaces; atomic_t abort_limit; + atomic_t ccr_limit; u8 vwc; u32 vs; u32 sgls; @@ -1294,4 +1297,11 @@ static inline bool nvme_multi_css(struct nvme_ctrl *ctrl) return (ctrl->ctrl_config & NVME_CC_CSS_MASK) == NVME_CC_CSS_CSI; } +static inline unsigned long nvme_fence_timeout_ms(struct nvme_ctrl *ctrl) +{ + if (ctrl->ctratt & NVME_CTRL_ATTR_TBKAS) + return 3 * ctrl->kato * 1000; + return 2 * ctrl->kato * 1000; +} + #endif /* _NVME_H */ diff --git a/drivers/nvme/host/sysfs.c b/drivers/nvme/host/sysfs.c index d368e779b6cce0..81b4beccf7b023 100644 --- a/drivers/nvme/host/sysfs.c +++ b/drivers/nvme/host/sysfs.c @@ -388,6 +388,27 @@ nvme_show_int_function(queue_count); nvme_show_int_function(sqsize); nvme_show_int_function(kato); +static ssize_t nvme_sysfs_ciu_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct nvme_ctrl *ctrl = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%02x\n", ctrl->ciu); +} +static DEVICE_ATTR(ciu, S_IRUSR, nvme_sysfs_ciu_show, NULL); + +static ssize_t nvme_sysfs_cirn_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct nvme_ctrl *ctrl = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%016llx\n", ctrl->cirn); +} +static DEVICE_ATTR(cirn, S_IRUSR, nvme_sysfs_cirn_show, NULL); + + static ssize_t nvme_sysfs_delete(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) @@ -843,6 +864,8 @@ static struct attribute *nvme_dev_attrs[] = { &dev_attr_numa_node.attr, &dev_attr_queue_count.attr, &dev_attr_sqsize.attr, + &dev_attr_ciu.attr, + &dev_attr_cirn.attr, &dev_attr_hostnqn.attr, &dev_attr_hostid.attr, &dev_attr_ctrl_loss_tmo.attr, From 64fbdc048163981539473238557e33412dad34b3 Mon Sep 17 00:00:00 2001 From: Mohamed Khalfella Date: Fri, 27 Mar 2026 17:43:38 -0700 Subject: [PATCH 27/36] nvme: Introduce FENCING and FENCED controller states FENCING is a new controller state that a LIVE controller enters when an error is encountered. While in FENCING state, inflight IOs that timeout are not canceled because they should be held until either CCR succeeds or time-based recovery completes. While the queues remain alive, requests are not allowed to be sent in this state, and the controller cannot be reset or deleted. This is intentional because resetting or deleting the controller results in canceling inflight IOs. FENCED is a short-term state the controller enters before it is reset. It exists only to prevent manual resets from happening while controller is in FENCING state. Signed-off-by: Mohamed Khalfella Reviewed-by: Hannes Reinecke --- drivers/nvme/host/core.c | 27 +++++++++++++++++++++++++-- drivers/nvme/host/nvme.h | 4 ++++ drivers/nvme/host/sysfs.c | 2 ++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c index c1db1ba23d8342..01224df1a1efbd 100644 --- a/drivers/nvme/host/core.c +++ b/drivers/nvme/host/core.c @@ -574,10 +574,29 @@ bool nvme_change_ctrl_state(struct nvme_ctrl *ctrl, break; } break; + case NVME_CTRL_FENCING: + switch (old_state) { + case NVME_CTRL_LIVE: + changed = true; + fallthrough; + default: + break; + } + break; + case NVME_CTRL_FENCED: + switch (old_state) { + case NVME_CTRL_FENCING: + changed = true; + fallthrough; + default: + break; + } + break; case NVME_CTRL_RESETTING: switch (old_state) { case NVME_CTRL_NEW: case NVME_CTRL_LIVE: + case NVME_CTRL_FENCED: changed = true; fallthrough; default: @@ -758,6 +777,8 @@ blk_status_t nvme_fail_nonready_command(struct nvme_ctrl *ctrl, if (state != NVME_CTRL_DELETING_NOIO && state != NVME_CTRL_DELETING && + state != NVME_CTRL_FENCING && + state != NVME_CTRL_FENCED && state != NVME_CTRL_DEAD && !test_bit(NVME_CTRL_FAILFAST_EXPIRED, &ctrl->flags) && !blk_noretry_request(rq) && !(rq->cmd_flags & REQ_NVME_MPATH)) @@ -800,10 +821,12 @@ bool __nvme_check_ready(struct nvme_ctrl *ctrl, struct request *rq, req->cmd->fabrics.fctype == nvme_fabrics_type_auth_receive)) return true; break; - default: - break; + case NVME_CTRL_FENCING: + case NVME_CTRL_FENCED: case NVME_CTRL_DEAD: return false; + default: + break; } } diff --git a/drivers/nvme/host/nvme.h b/drivers/nvme/host/nvme.h index 0b86e5829c25ca..06cf42866f5f34 100644 --- a/drivers/nvme/host/nvme.h +++ b/drivers/nvme/host/nvme.h @@ -305,6 +305,8 @@ static inline u16 nvme_req_qid(struct request *req) enum nvme_ctrl_state { NVME_CTRL_NEW, NVME_CTRL_LIVE, + NVME_CTRL_FENCING, + NVME_CTRL_FENCED, NVME_CTRL_RESETTING, NVME_CTRL_CONNECTING, NVME_CTRL_DELETING, @@ -839,6 +841,8 @@ static inline bool nvme_state_terminal(struct nvme_ctrl *ctrl) switch (nvme_ctrl_state(ctrl)) { case NVME_CTRL_NEW: case NVME_CTRL_LIVE: + case NVME_CTRL_FENCING: + case NVME_CTRL_FENCED: case NVME_CTRL_RESETTING: case NVME_CTRL_CONNECTING: return false; diff --git a/drivers/nvme/host/sysfs.c b/drivers/nvme/host/sysfs.c index 81b4beccf7b023..bd9014340cf4ae 100644 --- a/drivers/nvme/host/sysfs.c +++ b/drivers/nvme/host/sysfs.c @@ -443,6 +443,8 @@ static ssize_t nvme_sysfs_show_state(struct device *dev, static const char *const state_name[] = { [NVME_CTRL_NEW] = "new", [NVME_CTRL_LIVE] = "live", + [NVME_CTRL_FENCING] = "fencing", + [NVME_CTRL_FENCED] = "fenced", [NVME_CTRL_RESETTING] = "resetting", [NVME_CTRL_CONNECTING] = "connecting", [NVME_CTRL_DELETING] = "deleting", From 828b9e40182c01af0fd684e7b24f459ad1f7b330 Mon Sep 17 00:00:00 2001 From: Mohamed Khalfella Date: Fri, 27 Mar 2026 17:43:39 -0700 Subject: [PATCH 28/36] nvme: Implement cross-controller reset recovery A host that has more than one path connecting to an nvme subsystem typically has an nvme controller associated with every path. This is mostly applicable to nvmeof. If one path goes down, inflight IOs on that path should not be retried immediately on another path because this could lead to data corruption as described in TP4129. TP8028 defines cross-controller reset mechanism that can be used by host to terminate IOs on the failed path using one of the remaining healthy paths. Only after IOs are terminated, or long enough time passes as defined by TP4129, inflight IOs should be retried on another path. Implement core cross-controller reset shared logic to be used by the transports. Signed-off-by: Mohamed Khalfella --- drivers/nvme/host/constants.c | 1 + drivers/nvme/host/core.c | 145 ++++++++++++++++++++++++++++++++++ drivers/nvme/host/nvme.h | 9 +++ 3 files changed, 155 insertions(+) diff --git a/drivers/nvme/host/constants.c b/drivers/nvme/host/constants.c index 583d12f334d221..0fb6975a01df0e 100644 --- a/drivers/nvme/host/constants.c +++ b/drivers/nvme/host/constants.c @@ -47,6 +47,7 @@ static const char * const nvme_admin_ops[] = { [nvme_admin_virtual_mgmt] = "Virtual Management", [nvme_admin_nvme_mi_send] = "NVMe Send MI", [nvme_admin_nvme_mi_recv] = "NVMe Receive MI", + [nvme_admin_cross_ctrl_reset] = "Cross Controller Reset", [nvme_admin_dbbuf] = "Doorbell Buffer Config", [nvme_admin_format_nvm] = "Format NVM", [nvme_admin_security_send] = "Security Send", diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c index 01224df1a1efbd..e345f7f7ed7cef 100644 --- a/drivers/nvme/host/core.c +++ b/drivers/nvme/host/core.c @@ -554,6 +554,150 @@ void nvme_cancel_admin_tagset(struct nvme_ctrl *ctrl) } EXPORT_SYMBOL_GPL(nvme_cancel_admin_tagset); +static struct nvme_ctrl *nvme_find_ctrl_ccr(struct nvme_ctrl *ictrl, + u32 min_cntlid) +{ + struct nvme_subsystem *subsys = ictrl->subsys; + struct nvme_ctrl *ctrl, *sctrl = NULL; + unsigned long flags; + + mutex_lock(&nvme_subsystems_lock); + list_for_each_entry(ctrl, &subsys->ctrls, subsys_entry) { + if (ctrl->cntlid < min_cntlid) + continue; + + if (atomic_dec_if_positive(&ctrl->ccr_limit) < 0) + continue; + + spin_lock_irqsave(&ctrl->lock, flags); + if (ctrl->state != NVME_CTRL_LIVE) { + spin_unlock_irqrestore(&ctrl->lock, flags); + atomic_inc(&ctrl->ccr_limit); + continue; + } + + /* + * We got a good candidate source controller that is locked and + * LIVE. However, no guarantee ctrl will not be deleted after + * ctrl->lock is released. Get a ref of both ctrl and admin_q + * so they do not disappear until we are done with them. + */ + WARN_ON_ONCE(!blk_get_queue(ctrl->admin_q)); + nvme_get_ctrl(ctrl); + spin_unlock_irqrestore(&ctrl->lock, flags); + sctrl = ctrl; + break; + } + mutex_unlock(&nvme_subsystems_lock); + return sctrl; +} + +static void nvme_put_ctrl_ccr(struct nvme_ctrl *sctrl) +{ + atomic_inc(&sctrl->ccr_limit); + blk_put_queue(sctrl->admin_q); + nvme_put_ctrl(sctrl); +} + +static int nvme_issue_wait_ccr(struct nvme_ctrl *sctrl, struct nvme_ctrl *ictrl, + unsigned long deadline) +{ + struct nvme_ccr_entry ccr = { }; + union nvme_result res = { 0 }; + struct nvme_command c = { }; + unsigned long flags, now, tmo = 0; + bool completed = false; + int ret = 0; + u32 result; + + init_completion(&ccr.complete); + ccr.ictrl = ictrl; + + spin_lock_irqsave(&sctrl->lock, flags); + list_add_tail(&ccr.list, &sctrl->ccr_list); + spin_unlock_irqrestore(&sctrl->lock, flags); + + c.ccr.opcode = nvme_admin_cross_ctrl_reset; + c.ccr.ciu = ictrl->ciu; + c.ccr.icid = cpu_to_le16(ictrl->cntlid); + c.ccr.cirn = cpu_to_le64(ictrl->cirn); + ret = __nvme_submit_sync_cmd(sctrl->admin_q, &c, &res, + NULL, 0, NVME_QID_ANY, 0); + if (ret) { + ret = -EIO; + goto out; + } + + result = le32_to_cpu(res.u32); + if (result & 0x01) /* Immediate Reset Successful */ + goto out; + + now = jiffies; + if (time_before(now, deadline)) + tmo = min_t(unsigned long, + secs_to_jiffies(ictrl->kato), deadline - now); + + if (!wait_for_completion_timeout(&ccr.complete, tmo)) { + ret = -ETIMEDOUT; + goto out; + } + + completed = true; + +out: + spin_lock_irqsave(&sctrl->lock, flags); + list_del(&ccr.list); + spin_unlock_irqrestore(&sctrl->lock, flags); + if (completed) { + if (ccr.ccrs == NVME_CCR_STATUS_SUCCESS) + return 0; + return -EREMOTEIO; + } + return ret; +} + +int nvme_fence_ctrl(struct nvme_ctrl *ictrl) +{ + unsigned long deadline, timeout; + struct nvme_ctrl *sctrl; + u32 min_cntlid = 0; + int ret; + + timeout = nvme_fence_timeout_ms(ictrl); + dev_info(ictrl->device, "attempting CCR, timeout %lums\n", timeout); + + deadline = jiffies + msecs_to_jiffies(timeout); + while (time_is_after_jiffies(deadline)) { + sctrl = nvme_find_ctrl_ccr(ictrl, min_cntlid); + if (!sctrl) { + dev_dbg(ictrl->device, + "failed to find source controller\n"); + return -EIO; + } + + ret = nvme_issue_wait_ccr(sctrl, ictrl, deadline); + if (!ret) { + dev_info(ictrl->device, "CCR succeeded using %s\n", + dev_name(sctrl->device)); + nvme_put_ctrl_ccr(sctrl); + return 0; + } + + min_cntlid = sctrl->cntlid + 1; + nvme_put_ctrl_ccr(sctrl); + + if (ret == -EIO) /* CCR command failed */ + continue; + + /* CCR operation failed or timed out */ + return ret; + } + + dev_info(ictrl->device, "CCR operation timeout\n"); + return -ETIMEDOUT; +} +EXPORT_SYMBOL_GPL(nvme_fence_ctrl); + bool nvme_change_ctrl_state(struct nvme_ctrl *ctrl, enum nvme_ctrl_state new_state) { @@ -5267,6 +5411,7 @@ int nvme_init_ctrl(struct nvme_ctrl *ctrl, struct device *dev, mutex_init(&ctrl->scan_lock); INIT_LIST_HEAD(&ctrl->namespaces); + INIT_LIST_HEAD(&ctrl->ccr_list); xa_init(&ctrl->cels); ctrl->dev = dev; ctrl->ops = ops; diff --git a/drivers/nvme/host/nvme.h b/drivers/nvme/host/nvme.h index 06cf42866f5f34..d4439a858fd2b9 100644 --- a/drivers/nvme/host/nvme.h +++ b/drivers/nvme/host/nvme.h @@ -333,6 +333,13 @@ enum nvme_ctrl_flags { NVME_CTRL_FROZEN = 6, }; +struct nvme_ccr_entry { + struct list_head list; + struct completion complete; + struct nvme_ctrl *ictrl; + u8 ccrs; +}; + struct nvme_ctrl { bool comp_seen; bool identified; @@ -350,6 +357,7 @@ struct nvme_ctrl { struct blk_mq_tag_set *tagset; struct blk_mq_tag_set *admin_tagset; struct list_head namespaces; + struct list_head ccr_list; struct mutex namespaces_lock; struct srcu_struct srcu; struct device ctrl_device; @@ -876,6 +884,7 @@ blk_status_t nvme_host_path_error(struct request *req); bool nvme_cancel_request(struct request *req, void *data); void nvme_cancel_tagset(struct nvme_ctrl *ctrl); void nvme_cancel_admin_tagset(struct nvme_ctrl *ctrl); +int nvme_fence_ctrl(struct nvme_ctrl *ctrl); bool nvme_change_ctrl_state(struct nvme_ctrl *ctrl, enum nvme_ctrl_state new_state); int nvme_disable_ctrl(struct nvme_ctrl *ctrl, bool shutdown); From 3c543e2eabb36aa5bc1ffe0a9ccabfe9e9b4fe5d Mon Sep 17 00:00:00 2001 From: Mohamed Khalfella Date: Fri, 27 Mar 2026 17:43:40 -0700 Subject: [PATCH 29/36] nvme: Implement cross-controller reset completion An nvme source controller that issues CCR command expects to receive an NVME_AER_NOTICE_CCR_COMPLETED when pending CCR succeeds or fails. Add sctrl->ccr_work to read NVME_LOG_CCR logpage and wakeup any thread waiting on CCR completion. Signed-off-by: Mohamed Khalfella --- drivers/nvme/host/core.c | 49 +++++++++++++++++++++++++++++++++++++++- drivers/nvme/host/nvme.h | 1 + 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c index e345f7f7ed7cef..d9731f19b22086 100644 --- a/drivers/nvme/host/core.c +++ b/drivers/nvme/host/core.c @@ -1932,7 +1932,8 @@ EXPORT_SYMBOL_GPL(nvme_set_queue_count); #define NVME_AEN_SUPPORTED \ (NVME_AEN_CFG_NS_ATTR | NVME_AEN_CFG_FW_ACT | \ - NVME_AEN_CFG_ANA_CHANGE | NVME_AEN_CFG_DISC_CHANGE) + NVME_AEN_CFG_ANA_CHANGE | NVME_AEN_CFG_CCR_COMPLETE | \ + NVME_AEN_CFG_DISC_CHANGE) static void nvme_enable_aen(struct nvme_ctrl *ctrl) { @@ -5023,6 +5024,47 @@ static void nvme_get_fw_slot_info(struct nvme_ctrl *ctrl) kfree(log); } +static void nvme_ccr_work(struct work_struct *work) +{ + struct nvme_ctrl *ctrl = container_of(work, struct nvme_ctrl, ccr_work); + struct nvme_ccr_entry *ccr; + struct nvme_ccr_log_entry *entry; + struct nvme_ccr_log *log; + unsigned long flags; + int ret, i; + + log = kmalloc(sizeof(*log), GFP_KERNEL); + if (!log) + return; + + ret = nvme_get_log(ctrl, 0, NVME_LOG_CCR, 0x01, + 0x00, log, sizeof(*log), 0); + if (ret) + goto out; + + spin_lock_irqsave(&ctrl->lock, flags); + for (i = 0; i < le16_to_cpu(log->ne); i++) { + entry = &log->entries[i]; + if (entry->ccrs == NVME_CCR_STATUS_IN_PROGRESS) + continue; + + list_for_each_entry(ccr, &ctrl->ccr_list, list) { + struct nvme_ctrl *ictrl = ccr->ictrl; + + if (ictrl->cntlid != le16_to_cpu(entry->icid) || + ictrl->ciu != entry->ciu) + continue; + + /* Complete matching entry */ + ccr->ccrs = entry->ccrs; + complete(&ccr->complete); + } + } + spin_unlock_irqrestore(&ctrl->lock, flags); +out: + kfree(log); +} + static void nvme_fw_act_work(struct work_struct *work) { struct nvme_ctrl *ctrl = container_of(work, @@ -5099,6 +5141,9 @@ static bool nvme_handle_aen_notice(struct nvme_ctrl *ctrl, u32 result) case NVME_AER_NOTICE_DISC_CHANGED: ctrl->aen_result = result; break; + case NVME_AER_NOTICE_CCR_COMPLETED: + queue_work(nvme_wq, &ctrl->ccr_work); + break; default: dev_warn(ctrl->device, "async event result %08x\n", result); } @@ -5293,6 +5338,7 @@ void nvme_stop_ctrl(struct nvme_ctrl *ctrl) nvme_stop_failfast_work(ctrl); flush_work(&ctrl->async_event_work); cancel_work_sync(&ctrl->fw_act_work); + cancel_work_sync(&ctrl->ccr_work); if (ctrl->ops->stop_ctrl) ctrl->ops->stop_ctrl(ctrl); } @@ -5418,6 +5464,7 @@ int nvme_init_ctrl(struct nvme_ctrl *ctrl, struct device *dev, ctrl->quirks = quirks; ctrl->numa_node = NUMA_NO_NODE; INIT_WORK(&ctrl->scan_work, nvme_scan_work); + INIT_WORK(&ctrl->ccr_work, nvme_ccr_work); INIT_WORK(&ctrl->async_event_work, nvme_async_event_work); INIT_WORK(&ctrl->fw_act_work, nvme_fw_act_work); INIT_WORK(&ctrl->delete_work, nvme_delete_ctrl_work); diff --git a/drivers/nvme/host/nvme.h b/drivers/nvme/host/nvme.h index d4439a858fd2b9..163d562a449509 100644 --- a/drivers/nvme/host/nvme.h +++ b/drivers/nvme/host/nvme.h @@ -421,6 +421,7 @@ struct nvme_ctrl { struct nvme_effects_log *effects; struct xarray cels; struct work_struct scan_work; + struct work_struct ccr_work; struct work_struct async_event_work; struct delayed_work ka_work; struct delayed_work failfast_work; From f2d8e06bc07dcc96733a7679fc32260bfa932802 Mon Sep 17 00:00:00 2001 From: Mohamed Khalfella Date: Fri, 27 Mar 2026 17:43:41 -0700 Subject: [PATCH 30/36] nvme-tcp: Use CCR to recover controller that hits an error An alive nvme controller that hits an error now will move to FENCING state instead of RESETTING state. ctrl->fencing_work attempts CCR to terminate inflight IOs. Regardless of the success or failure of CCR operation the controller is transitioned to RESETTING state to continue error recovery process. Signed-off-by: Mohamed Khalfella --- drivers/nvme/host/tcp.c | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/drivers/nvme/host/tcp.c b/drivers/nvme/host/tcp.c index 0a193b3c443841..d31fdd09673348 100644 --- a/drivers/nvme/host/tcp.c +++ b/drivers/nvme/host/tcp.c @@ -197,6 +197,7 @@ struct nvme_tcp_ctrl { struct sockaddr_storage src_addr; struct nvme_ctrl ctrl; + struct work_struct fencing_work; struct work_struct err_work; struct delayed_work connect_work; struct nvme_tcp_request async_req; @@ -616,6 +617,12 @@ static void nvme_tcp_init_recv_ctx(struct nvme_tcp_queue *queue) static void nvme_tcp_error_recovery(struct nvme_ctrl *ctrl) { + if (nvme_change_ctrl_state(ctrl, NVME_CTRL_FENCING)) { + dev_warn(ctrl->device, "starting controller fencing\n"); + queue_work(nvme_wq, &to_tcp_ctrl(ctrl)->fencing_work); + return; + } + if (!nvme_change_ctrl_state(ctrl, NVME_CTRL_RESETTING)) return; @@ -2477,12 +2484,29 @@ static void nvme_tcp_reconnect_ctrl_work(struct work_struct *work) nvme_tcp_reconnect_or_remove(ctrl, ret); } +static void nvme_tcp_fencing_work(struct work_struct *work) +{ + struct nvme_tcp_ctrl *tcp_ctrl = container_of(work, + struct nvme_tcp_ctrl, fencing_work); + struct nvme_ctrl *ctrl = &tcp_ctrl->ctrl; + int ret; + + ret = nvme_fence_ctrl(ctrl); + if (ret) + dev_info(ctrl->device, "CCR failed with error %d\n", ret); + + nvme_change_ctrl_state(ctrl, NVME_CTRL_FENCED); + if (nvme_change_ctrl_state(ctrl, NVME_CTRL_RESETTING)) + queue_work(nvme_reset_wq, &tcp_ctrl->err_work); +} + static void nvme_tcp_error_recovery_work(struct work_struct *work) { struct nvme_tcp_ctrl *tcp_ctrl = container_of(work, struct nvme_tcp_ctrl, err_work); struct nvme_ctrl *ctrl = &tcp_ctrl->ctrl; + flush_work(&to_tcp_ctrl(ctrl)->fencing_work); if (nvme_tcp_key_revoke_needed(ctrl)) nvme_auth_revoke_tls_key(ctrl); nvme_stop_keep_alive(ctrl); @@ -2525,6 +2549,7 @@ static void nvme_reset_ctrl_work(struct work_struct *work) container_of(work, struct nvme_ctrl, reset_work); int ret; + flush_work(&to_tcp_ctrl(ctrl)->fencing_work); if (nvme_tcp_key_revoke_needed(ctrl)) nvme_auth_revoke_tls_key(ctrl); nvme_stop_ctrl(ctrl); @@ -2652,13 +2677,15 @@ static enum blk_eh_timer_return nvme_tcp_timeout(struct request *rq) struct nvme_ctrl *ctrl = &queue->ctrl->ctrl; int qid = nvme_tcp_queue_id(queue); int error, action; + enum nvme_ctrl_state state; dev_warn(ctrl->device, "I/O tag %d (%04x) type %d opcode %#x (%s) QID %d timeout\n", rq->tag, nvme_cid(rq), pdu->hdr.type, cmd->common.opcode, nvme_fabrics_opcode_str(qid, cmd), qid); - if (nvme_ctrl_state(ctrl) != NVME_CTRL_LIVE) { + state = nvme_ctrl_state(ctrl); + if (state != NVME_CTRL_LIVE && state != NVME_CTRL_FENCING) { /* * If we are resetting, connecting or deleting we should * complete immediately because we may block controller @@ -2968,6 +2995,7 @@ static struct nvme_tcp_ctrl *nvme_tcp_alloc_ctrl(struct device *dev, INIT_DELAYED_WORK(&ctrl->connect_work, nvme_tcp_reconnect_ctrl_work); + INIT_WORK(&ctrl->fencing_work, nvme_tcp_fencing_work); INIT_WORK(&ctrl->err_work, nvme_tcp_error_recovery_work); INIT_WORK(&ctrl->ctrl.reset_work, nvme_reset_ctrl_work); From b988eaa840e30bec447135ed013c5e2a7d1038b4 Mon Sep 17 00:00:00 2001 From: Mohamed Khalfella Date: Fri, 27 Mar 2026 17:43:42 -0700 Subject: [PATCH 31/36] nvme-rdma: Use CCR to recover controller that hits an error An alive nvme controller that hits an error now will move to FENCING state instead of RESETTING state. ctrl->fencing_work attempts CCR to terminate inflight IOs. Regardless of the success or failure of CCR operation the controller is transitioned to RESETTING state to continue error recovery process. Signed-off-by: Mohamed Khalfella --- drivers/nvme/host/rdma.c | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/drivers/nvme/host/rdma.c b/drivers/nvme/host/rdma.c index dbdd1f62005af5..51191756ee17ff 100644 --- a/drivers/nvme/host/rdma.c +++ b/drivers/nvme/host/rdma.c @@ -109,6 +109,7 @@ struct nvme_rdma_ctrl { /* other member variables */ struct blk_mq_tag_set tag_set; + struct work_struct fencing_work; struct work_struct err_work; struct nvme_rdma_qe async_event_sqe; @@ -1125,11 +1126,28 @@ static void nvme_rdma_reconnect_ctrl_work(struct work_struct *work) nvme_rdma_reconnect_or_remove(ctrl, ret); } +static void nvme_rdma_fencing_work(struct work_struct *work) +{ + struct nvme_rdma_ctrl *rdma_ctrl = container_of(work, + struct nvme_rdma_ctrl, fencing_work); + struct nvme_ctrl *ctrl = &rdma_ctrl->ctrl; + int ret; + + ret = nvme_fence_ctrl(ctrl); + if (ret) + dev_info(ctrl->device, "CCR failed with error %d\n", ret); + + nvme_change_ctrl_state(ctrl, NVME_CTRL_FENCED); + if (nvme_change_ctrl_state(ctrl, NVME_CTRL_RESETTING)) + queue_work(nvme_reset_wq, &rdma_ctrl->err_work); +} + static void nvme_rdma_error_recovery_work(struct work_struct *work) { struct nvme_rdma_ctrl *ctrl = container_of(work, struct nvme_rdma_ctrl, err_work); + flush_work(&ctrl->fencing_work); nvme_stop_keep_alive(&ctrl->ctrl); flush_work(&ctrl->ctrl.async_event_work); nvme_rdma_teardown_io_queues(ctrl, false); @@ -1152,6 +1170,12 @@ static void nvme_rdma_error_recovery_work(struct work_struct *work) static void nvme_rdma_error_recovery(struct nvme_rdma_ctrl *ctrl) { + if (nvme_change_ctrl_state(&ctrl->ctrl, NVME_CTRL_FENCING)) { + dev_warn(ctrl->ctrl.device, "starting controller fencing\n"); + queue_work(nvme_wq, &ctrl->fencing_work); + return; + } + if (!nvme_change_ctrl_state(&ctrl->ctrl, NVME_CTRL_RESETTING)) return; @@ -1964,13 +1988,15 @@ static enum blk_eh_timer_return nvme_rdma_timeout(struct request *rq) struct nvme_command *cmd = req->req.cmd; int qid = nvme_rdma_queue_idx(queue); int error, action; + enum nvme_ctrl_state state; dev_warn(ctrl->device, "I/O tag %d (%04x) opcode %#x (%s) QID %d timeout\n", rq->tag, nvme_cid(rq), cmd->common.opcode, nvme_fabrics_opcode_str(qid, cmd), qid); - if (nvme_ctrl_state(ctrl) != NVME_CTRL_LIVE) { + state = nvme_ctrl_state(&ctrl->ctrl); + if (state != NVME_CTRL_LIVE && state != NVME_CTRL_FENCING) { /* * If we are resetting, connecting or deleting we should * complete immediately because we may block controller @@ -2221,6 +2247,7 @@ static void nvme_rdma_reset_ctrl_work(struct work_struct *work) container_of(work, struct nvme_rdma_ctrl, ctrl.reset_work); int ret; + flush_work(&ctrl->fencing_work); nvme_stop_ctrl(&ctrl->ctrl); nvme_rdma_shutdown_ctrl(ctrl, false); @@ -2341,6 +2368,7 @@ static struct nvme_rdma_ctrl *nvme_rdma_alloc_ctrl(struct device *dev, INIT_DELAYED_WORK(&ctrl->reconnect_work, nvme_rdma_reconnect_ctrl_work); + INIT_WORK(&ctrl->fencing_work, nvme_rdma_fencing_work); INIT_WORK(&ctrl->err_work, nvme_rdma_error_recovery_work); INIT_WORK(&ctrl->ctrl.reset_work, nvme_rdma_reset_ctrl_work); From 98deba18d38f3b72897276e36f53a1d4ca5fd6b8 Mon Sep 17 00:00:00 2001 From: Mohamed Khalfella Date: Fri, 27 Mar 2026 17:43:43 -0700 Subject: [PATCH 32/36] nvme-fc: Refactor IO error recovery Added new nvme_fc_start_ioerr_recovery() to trigger error recovery instead of directly queueing ctrl->ioerr_work. nvme_fc_error_recovery() now called only from ctrl->ioerr_work has been updated to not depend on nvme_reset_ctrl() to handle error recovery. nvme_fc_error_recovery() effectively resets the controller and attempts reconnection if needed. This makes nvme-fc ioerr handling similar to other fabric transports. Update nvme_fc_timeout() to not abort timed out IOs. IOs aborted from nvme_fc_timeout() are not accounted for in ctrl->iocnt and this causes nvme_fc_delete_association() not to wait for them. Instead of aborting IOs nvme_fc_timeout() calls nvme_fc_start_ioerr_recovery() to start IO error recovery. Since error recovery runs in ctrl->ioerr_work this change fixes the issue reported in the link below. Link: https://lore.kernel.org/all/20250529214928.2112990-1-mkhalfella@purestorage.com/ Signed-off-by: Mohamed Khalfella --- drivers/nvme/host/fc.c | 119 +++++++++++++++++++++++------------------ 1 file changed, 66 insertions(+), 53 deletions(-) diff --git a/drivers/nvme/host/fc.c b/drivers/nvme/host/fc.c index e4f4528fe2a2d6..c70667e5e26984 100644 --- a/drivers/nvme/host/fc.c +++ b/drivers/nvme/host/fc.c @@ -227,6 +227,10 @@ static DEFINE_IDA(nvme_fc_ctrl_cnt); static struct device *fc_udev_device; static void nvme_fc_complete_rq(struct request *rq); +static void nvme_fc_start_ioerr_recovery(struct nvme_fc_ctrl *ctrl, + char *errmsg); +static void __nvme_fc_abort_outstanding_ios(struct nvme_fc_ctrl *ctrl, + bool start_queues); /* *********************** FC-NVME Port Management ************************ */ @@ -788,7 +792,7 @@ nvme_fc_ctrl_connectivity_loss(struct nvme_fc_ctrl *ctrl) "Reconnect", ctrl->cnum); set_bit(ASSOC_FAILED, &ctrl->flags); - nvme_reset_ctrl(&ctrl->ctrl); + nvme_fc_start_ioerr_recovery(ctrl, "Connectivity Loss"); } /** @@ -985,7 +989,7 @@ fc_dma_unmap_sg(struct device *dev, struct scatterlist *sg, int nents, static void nvme_fc_ctrl_put(struct nvme_fc_ctrl *); static int nvme_fc_ctrl_get(struct nvme_fc_ctrl *); -static void nvme_fc_error_recovery(struct nvme_fc_ctrl *ctrl, char *errmsg); +static void nvme_fc_error_recovery(struct nvme_fc_ctrl *ctrl); static void __nvme_fc_finish_ls_req(struct nvmefc_ls_req_op *lsop) @@ -1567,9 +1571,8 @@ nvme_fc_ls_disconnect_assoc(struct nvmefc_ls_rcv_op *lsop) * for the association have been ABTS'd by * nvme_fc_delete_association(). */ - - /* fail the association */ - nvme_fc_error_recovery(ctrl, "Disconnect Association LS received"); + nvme_fc_start_ioerr_recovery(ctrl, + "Disconnect Association LS received"); /* release the reference taken by nvme_fc_match_disconn_ls() */ nvme_fc_ctrl_put(ctrl); @@ -1871,7 +1874,22 @@ nvme_fc_ctrl_ioerr_work(struct work_struct *work) struct nvme_fc_ctrl *ctrl = container_of(work, struct nvme_fc_ctrl, ioerr_work); - nvme_fc_error_recovery(ctrl, "transport detected io error"); + /* + * if an error (io timeout, etc) while (re)connecting, the remote + * port requested terminating of the association (disconnect_ls) + * or an error (timeout or abort) occurred on an io while creating + * the controller. Abort any ios on the association and let the + * create_association error path resolve things. + */ + if (nvme_ctrl_state(&ctrl->ctrl) == NVME_CTRL_CONNECTING) { + __nvme_fc_abort_outstanding_ios(ctrl, true); + dev_warn(ctrl->ctrl.device, + "NVME-FC{%d}: transport error during (re)connect\n", + ctrl->cnum); + return; + } + + nvme_fc_error_recovery(ctrl); } /* @@ -1892,6 +1910,24 @@ char *nvme_fc_io_getuuid(struct nvmefc_fcp_req *req) } EXPORT_SYMBOL_GPL(nvme_fc_io_getuuid); +static void nvme_fc_start_ioerr_recovery(struct nvme_fc_ctrl *ctrl, + char *errmsg) +{ + enum nvme_ctrl_state state = nvme_ctrl_state(&ctrl->ctrl); + + if (state == NVME_CTRL_CONNECTING || state == NVME_CTRL_DELETING || + state == NVME_CTRL_DELETING_NOIO) { + queue_work(nvme_reset_wq, &ctrl->ioerr_work); + return; + } + + if (nvme_change_ctrl_state(&ctrl->ctrl, NVME_CTRL_RESETTING)) { + dev_warn(ctrl->ctrl.device, "NVME-FC{%d}: starting error recovery %s\n", + ctrl->cnum, errmsg); + queue_work(nvme_reset_wq, &ctrl->ioerr_work); + } +} + static void nvme_fc_fcpio_done(struct nvmefc_fcp_req *req) { @@ -2049,9 +2085,8 @@ nvme_fc_fcpio_done(struct nvmefc_fcp_req *req) nvme_fc_complete_rq(rq); check_error: - if (terminate_assoc && - nvme_ctrl_state(&ctrl->ctrl) != NVME_CTRL_RESETTING) - queue_work(nvme_reset_wq, &ctrl->ioerr_work); + if (terminate_assoc) + nvme_fc_start_ioerr_recovery(ctrl, "io error"); } static int @@ -2495,39 +2530,6 @@ __nvme_fc_abort_outstanding_ios(struct nvme_fc_ctrl *ctrl, bool start_queues) nvme_unquiesce_admin_queue(&ctrl->ctrl); } -static void -nvme_fc_error_recovery(struct nvme_fc_ctrl *ctrl, char *errmsg) -{ - enum nvme_ctrl_state state = nvme_ctrl_state(&ctrl->ctrl); - - /* - * if an error (io timeout, etc) while (re)connecting, the remote - * port requested terminating of the association (disconnect_ls) - * or an error (timeout or abort) occurred on an io while creating - * the controller. Abort any ios on the association and let the - * create_association error path resolve things. - */ - if (state == NVME_CTRL_CONNECTING) { - __nvme_fc_abort_outstanding_ios(ctrl, true); - dev_warn(ctrl->ctrl.device, - "NVME-FC{%d}: transport error during (re)connect\n", - ctrl->cnum); - return; - } - - /* Otherwise, only proceed if in LIVE state - e.g. on first error */ - if (state != NVME_CTRL_LIVE) - return; - - dev_warn(ctrl->ctrl.device, - "NVME-FC{%d}: transport association event: %s\n", - ctrl->cnum, errmsg); - dev_warn(ctrl->ctrl.device, - "NVME-FC{%d}: resetting controller\n", ctrl->cnum); - - nvme_reset_ctrl(&ctrl->ctrl); -} - static enum blk_eh_timer_return nvme_fc_timeout(struct request *rq) { struct nvme_fc_fcp_op *op = blk_mq_rq_to_pdu(rq); @@ -2536,24 +2538,14 @@ static enum blk_eh_timer_return nvme_fc_timeout(struct request *rq) struct nvme_fc_cmd_iu *cmdiu = &op->cmd_iu; struct nvme_command *sqe = &cmdiu->sqe; - /* - * Attempt to abort the offending command. Command completion - * will detect the aborted io and will fail the connection. - */ dev_info(ctrl->ctrl.device, "NVME-FC{%d.%d}: io timeout: opcode %d fctype %d (%s) w10/11: " "x%08x/x%08x\n", ctrl->cnum, qnum, sqe->common.opcode, sqe->fabrics.fctype, nvme_fabrics_opcode_str(qnum, sqe), sqe->common.cdw10, sqe->common.cdw11); - if (__nvme_fc_abort_op(ctrl, op)) - nvme_fc_error_recovery(ctrl, "io timeout abort failed"); - /* - * the io abort has been initiated. Have the reset timer - * restarted and the abort completion will complete the io - * shortly. Avoids a synchronous wait while the abort finishes. - */ + nvme_fc_start_ioerr_recovery(ctrl, "io timeout"); return BLK_EH_RESET_TIMER; } @@ -3352,6 +3344,27 @@ nvme_fc_reset_ctrl_work(struct work_struct *work) } } +static void +nvme_fc_error_recovery(struct nvme_fc_ctrl *ctrl) +{ + nvme_stop_keep_alive(&ctrl->ctrl); + nvme_stop_ctrl(&ctrl->ctrl); + flush_work(&ctrl->ctrl.async_event_work); + + /* will block while waiting for io to terminate */ + nvme_fc_delete_association(ctrl); + + /* Do not reconnect if controller is being deleted */ + if (!nvme_change_ctrl_state(&ctrl->ctrl, NVME_CTRL_CONNECTING)) + return; + + if (ctrl->rport->remoteport.port_state == FC_OBJSTATE_ONLINE) { + queue_delayed_work(nvme_wq, &ctrl->connect_work, 0); + return; + } + + nvme_fc_reconnect_or_delete(ctrl, -ENOTCONN); +} static const struct nvme_ctrl_ops nvme_fc_ctrl_ops = { .name = "fc", From 8d57f30ce15400d356139380854df3cac757e39e Mon Sep 17 00:00:00 2001 From: Mohamed Khalfella Date: Fri, 27 Mar 2026 17:43:44 -0700 Subject: [PATCH 33/36] nvme-fc: Use CCR to recover controller that hits an error An alive nvme controller that hits an error now will move to FENCING state instead of RESETTING state. ctrl->fencing_work attempts CCR to terminate inflight IOs. Regardless of the success or failure of CCR operation the controller is transitioned to RESETTING state to continue error recovery process. Signed-off-by: Mohamed Khalfella --- drivers/nvme/host/fc.c | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/drivers/nvme/host/fc.c b/drivers/nvme/host/fc.c index c70667e5e26984..11d6106d0de02d 100644 --- a/drivers/nvme/host/fc.c +++ b/drivers/nvme/host/fc.c @@ -166,6 +166,7 @@ struct nvme_fc_ctrl { struct blk_mq_tag_set admin_tag_set; struct blk_mq_tag_set tag_set; + struct work_struct fencing_work; struct work_struct ioerr_work; struct delayed_work connect_work; @@ -1868,6 +1869,22 @@ __nvme_fc_fcpop_chk_teardowns(struct nvme_fc_ctrl *ctrl, } } +static void nvme_fc_fencing_work(struct work_struct *work) +{ + struct nvme_fc_ctrl *fc_ctrl = + container_of(work, struct nvme_fc_ctrl, fencing_work); + struct nvme_ctrl *ctrl = &fc_ctrl->ctrl; + int ret; + + ret = nvme_fence_ctrl(ctrl); + if (ret) + dev_info(ctrl->device, "CCR failed with error %d\n", ret); + + nvme_change_ctrl_state(ctrl, NVME_CTRL_FENCED); + if (nvme_change_ctrl_state(ctrl, NVME_CTRL_RESETTING)) + queue_work(nvme_reset_wq, &fc_ctrl->ioerr_work); +} + static void nvme_fc_ctrl_ioerr_work(struct work_struct *work) { @@ -1889,6 +1906,7 @@ nvme_fc_ctrl_ioerr_work(struct work_struct *work) return; } + flush_work(&ctrl->fencing_work); nvme_fc_error_recovery(ctrl); } @@ -1921,6 +1939,14 @@ static void nvme_fc_start_ioerr_recovery(struct nvme_fc_ctrl *ctrl, return; } + if (nvme_change_ctrl_state(&ctrl->ctrl, NVME_CTRL_FENCING)) { + dev_warn(ctrl->ctrl.device, + "NVME-FC{%d}: starting controller fencing %s\n", + ctrl->cnum, errmsg); + queue_work(nvme_wq, &ctrl->fencing_work); + return; + } + if (nvme_change_ctrl_state(&ctrl->ctrl, NVME_CTRL_RESETTING)) { dev_warn(ctrl->ctrl.device, "NVME-FC{%d}: starting error recovery %s\n", ctrl->cnum, errmsg); @@ -3321,6 +3347,7 @@ nvme_fc_reset_ctrl_work(struct work_struct *work) struct nvme_fc_ctrl *ctrl = container_of(work, struct nvme_fc_ctrl, ctrl.reset_work); + flush_work(&ctrl->fencing_work); nvme_stop_ctrl(&ctrl->ctrl); /* will block will waiting for io to terminate */ @@ -3496,6 +3523,7 @@ nvme_fc_alloc_ctrl(struct device *dev, struct nvmf_ctrl_options *opts, INIT_WORK(&ctrl->ctrl.reset_work, nvme_fc_reset_ctrl_work); INIT_DELAYED_WORK(&ctrl->connect_work, nvme_fc_connect_ctrl_work); + INIT_WORK(&ctrl->fencing_work, nvme_fc_fencing_work); INIT_WORK(&ctrl->ioerr_work, nvme_fc_ctrl_ioerr_work); spin_lock_init(&ctrl->lock); From c56d668490ceee5bf6f54558543364a2664931fd Mon Sep 17 00:00:00 2001 From: Mohamed Khalfella Date: Fri, 27 Mar 2026 17:43:45 -0700 Subject: [PATCH 34/36] nvme-fc: Hold inflight requests while in FENCING state While in FENCING state, aborted inflight IOs should be held until fencing is done. Update nvme_fc_fcpio_done() to not complete aborted requests or requests with transport errors. These held requests will be canceled in nvme_fc_delete_association() after fencing is done. nvme_fc_fcpio_done() avoids racing with canceling aborted requests by making sure we complete successful requests before waking up the waiting thread. Signed-off-by: Mohamed Khalfella Signed-off-by: James Smart --- drivers/nvme/host/fc.c | 61 +++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/drivers/nvme/host/fc.c b/drivers/nvme/host/fc.c index 11d6106d0de02d..7424d299a5ddd8 100644 --- a/drivers/nvme/host/fc.c +++ b/drivers/nvme/host/fc.c @@ -172,7 +172,7 @@ struct nvme_fc_ctrl { struct kref ref; unsigned long flags; - u32 iocnt; + atomic_t iocnt; wait_queue_head_t ioabort_wait; struct nvme_fc_fcp_op aen_ops[NVME_NR_AEN_COMMANDS]; @@ -1823,7 +1823,7 @@ __nvme_fc_abort_op(struct nvme_fc_ctrl *ctrl, struct nvme_fc_fcp_op *op) atomic_set(&op->state, opstate); else if (test_bit(FCCTRL_TERMIO, &ctrl->flags)) { op->flags |= FCOP_FLAGS_TERMIO; - ctrl->iocnt++; + atomic_inc(&ctrl->iocnt); } spin_unlock_irqrestore(&ctrl->lock, flags); @@ -1853,20 +1853,29 @@ nvme_fc_abort_aen_ops(struct nvme_fc_ctrl *ctrl) } static inline void +__nvme_fc_fcpop_count_one_down(struct nvme_fc_ctrl *ctrl) +{ + if (atomic_dec_return(&ctrl->iocnt) == 0) + wake_up(&ctrl->ioabort_wait); +} + +static inline bool __nvme_fc_fcpop_chk_teardowns(struct nvme_fc_ctrl *ctrl, struct nvme_fc_fcp_op *op, int opstate) { unsigned long flags; + bool ret = false; if (opstate == FCPOP_STATE_ABORTED) { spin_lock_irqsave(&ctrl->lock, flags); if (test_bit(FCCTRL_TERMIO, &ctrl->flags) && op->flags & FCOP_FLAGS_TERMIO) { - if (!--ctrl->iocnt) - wake_up(&ctrl->ioabort_wait); + ret = true; } spin_unlock_irqrestore(&ctrl->lock, flags); } + + return ret; } static void nvme_fc_fencing_work(struct work_struct *work) @@ -1966,7 +1975,8 @@ nvme_fc_fcpio_done(struct nvmefc_fcp_req *req) struct nvme_command *sqe = &op->cmd_iu.sqe; __le16 status = cpu_to_le16(NVME_SC_SUCCESS << 1); union nvme_result result; - bool terminate_assoc = true; + bool op_term, terminate_assoc = true; + enum nvme_ctrl_state state; int opstate; /* @@ -2099,16 +2109,38 @@ nvme_fc_fcpio_done(struct nvmefc_fcp_req *req) done: if (op->flags & FCOP_FLAGS_AEN) { nvme_complete_async_event(&queue->ctrl->ctrl, status, &result); - __nvme_fc_fcpop_chk_teardowns(ctrl, op, opstate); + if (__nvme_fc_fcpop_chk_teardowns(ctrl, op, opstate)) + __nvme_fc_fcpop_count_one_down(ctrl); atomic_set(&op->state, FCPOP_STATE_IDLE); op->flags = FCOP_FLAGS_AEN; /* clear other flags */ nvme_fc_ctrl_put(ctrl); goto check_error; } - __nvme_fc_fcpop_chk_teardowns(ctrl, op, opstate); + /* + * We can not access op after the request is completed because it can + * be reused immediately. At the same time we want to wakeup the thread + * waiting for ongoing IOs _after_ requests are completed. This is + * necessary because that thread will start canceling inflight IOs + * and we want to avoid request completion racing with cancellation. + */ + op_term = __nvme_fc_fcpop_chk_teardowns(ctrl, op, opstate); + + /* + * If we are going to terminate associations and the controller is + * LIVE or FENCING, then do not complete this request now. Let error + * recovery cancel this request when it is safe to do so. + */ + state = nvme_ctrl_state(&ctrl->ctrl); + if (terminate_assoc && + (state == NVME_CTRL_LIVE || state == NVME_CTRL_FENCING)) + goto check_op_term; + if (!nvme_try_complete_req(rq, status, result)) nvme_fc_complete_rq(rq); +check_op_term: + if (op_term) + __nvme_fc_fcpop_count_one_down(ctrl); check_error: if (terminate_assoc) @@ -2747,7 +2779,8 @@ nvme_fc_start_fcp_op(struct nvme_fc_ctrl *ctrl, struct nvme_fc_queue *queue, * cmd with the csn was supposed to arrive. */ opstate = atomic_xchg(&op->state, FCPOP_STATE_COMPLETE); - __nvme_fc_fcpop_chk_teardowns(ctrl, op, opstate); + if (__nvme_fc_fcpop_chk_teardowns(ctrl, op, opstate)) + __nvme_fc_fcpop_count_one_down(ctrl); if (!(op->flags & FCOP_FLAGS_AEN)) { nvme_fc_unmap_data(ctrl, op->rq, op); @@ -3216,7 +3249,7 @@ nvme_fc_delete_association(struct nvme_fc_ctrl *ctrl) spin_lock_irqsave(&ctrl->lock, flags); set_bit(FCCTRL_TERMIO, &ctrl->flags); - ctrl->iocnt = 0; + atomic_set(&ctrl->iocnt, 0); spin_unlock_irqrestore(&ctrl->lock, flags); __nvme_fc_abort_outstanding_ios(ctrl, false); @@ -3225,11 +3258,19 @@ nvme_fc_delete_association(struct nvme_fc_ctrl *ctrl) nvme_fc_abort_aen_ops(ctrl); /* wait for all io that had to be aborted */ + wait_event(ctrl->ioabort_wait, atomic_read(&ctrl->iocnt) == 0); spin_lock_irq(&ctrl->lock); - wait_event_lock_irq(ctrl->ioabort_wait, ctrl->iocnt == 0, ctrl->lock); clear_bit(FCCTRL_TERMIO, &ctrl->flags); spin_unlock_irq(&ctrl->lock); + /* + * At this point all inflight requests have been successfully + * aborted. Now it is safe to cancel all requests we decided + * not to complete in nvme_fc_fcpio_done(). + */ + nvme_cancel_tagset(&ctrl->ctrl); + nvme_cancel_admin_tagset(&ctrl->ctrl); + nvme_fc_term_aen_ops(ctrl); /* From e7df54f94fd60b77b1be236fcda580ccbe4af6e3 Mon Sep 17 00:00:00 2001 From: Mohamed Khalfella Date: Fri, 27 Mar 2026 17:43:46 -0700 Subject: [PATCH 35/36] nvme-fc: Do not cancel requests in io taget before it is initialized A new nvme-fc controller in CONNECTING state sees admin request timeout schedules ctrl->ioerr_work to abort inflight requests. This ends up calling __nvme_fc_abort_outstanding_ios() which aborts requests in both admin and io tagsets. In case fc_ctrl->tag_set was not initialized we see the warning below. This is because ctrl.queue_count is initialized early in nvme_fc_alloc_ctrl(). nvme nvme0: NVME-FC{0}: starting error recovery Connectivity Loss INFO: trying to register non-static key. The code is fine but needs lockdep annotation, or maybe lpfc 0000:ab:00.0: queue 0 connect admin queue failed (-6). you didn't initialize this object before use? turning off the locking correctness validator. Workqueue: nvme-reset-wq nvme_fc_ctrl_ioerr_work [nvme_fc] Call Trace: dump_stack_lvl+0x57/0x80 register_lock_class+0x567/0x580 __lock_acquire+0x330/0xb90 lock_acquire.part.0+0xad/0x210 blk_mq_tagset_busy_iter+0xf9/0xc00 __nvme_fc_abort_outstanding_ios+0x23f/0x320 [nvme_fc] nvme_fc_ctrl_ioerr_work+0x172/0x210 [nvme_fc] process_one_work+0x82c/0x1450 worker_thread+0x5ee/0xfd0 kthread+0x3a0/0x750 ret_from_fork+0x439/0x670 ret_from_fork_asm+0x1a/0x30 Update the check in __nvme_fc_abort_outstanding_ios() confirm that io tagset was created before iterating over busy requests. Also make sure to cancel ctrl->ioerr_work before removing io tagset. Signed-off-by: Mohamed Khalfella Signed-off-by: James Smart --- drivers/nvme/host/fc.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/drivers/nvme/host/fc.c b/drivers/nvme/host/fc.c index 7424d299a5ddd8..1114a7d246b01f 100644 --- a/drivers/nvme/host/fc.c +++ b/drivers/nvme/host/fc.c @@ -2554,7 +2554,7 @@ __nvme_fc_abort_outstanding_ios(struct nvme_fc_ctrl *ctrl, bool start_queues) * io requests back to the block layer as part of normal completions * (but with error status). */ - if (ctrl->ctrl.queue_count > 1) { + if (ctrl->ctrl.queue_count > 1 && ctrl->ctrl.tagset) { nvme_quiesce_io_queues(&ctrl->ctrl); nvme_sync_io_queues(&ctrl->ctrl); blk_mq_tagset_busy_iter(&ctrl->tag_set, @@ -2951,6 +2951,11 @@ nvme_fc_create_io_queues(struct nvme_fc_ctrl *ctrl) out_delete_hw_queues: nvme_fc_delete_hw_io_queues(ctrl); out_cleanup_tagset: + /* + * In CONNECTING state ctrl->ioerr_work will abort both admin + * and io tagsets. Cancel it first before removing io tagset. + */ + cancel_work_sync(&ctrl->ioerr_work); nvme_remove_io_tag_set(&ctrl->ctrl); nvme_fc_free_io_queues(ctrl); From 4893bc4803f2aafdcb8c512f807752be5c1e7f40 Mon Sep 17 00:00:00 2001 From: John Meneghini Date: Wed, 15 Apr 2026 12:14:55 -0400 Subject: [PATCH 36/36] nvme-rdma: fix use CCR recovery patch This upstream patch doesn't even compile. Fixes: b988eaa840e3 ("nvme-rdma: Use CCR to recover controller that hits an error") Signed-off-by: John Meneghini --- drivers/nvme/host/rdma.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/nvme/host/rdma.c b/drivers/nvme/host/rdma.c index 51191756ee17ff..c0f4020ba5c4a0 100644 --- a/drivers/nvme/host/rdma.c +++ b/drivers/nvme/host/rdma.c @@ -1995,7 +1995,7 @@ static enum blk_eh_timer_return nvme_rdma_timeout(struct request *rq) rq->tag, nvme_cid(rq), cmd->common.opcode, nvme_fabrics_opcode_str(qid, cmd), qid); - state = nvme_ctrl_state(&ctrl->ctrl); + state = nvme_ctrl_state(ctrl); if (state != NVME_CTRL_LIVE && state != NVME_CTRL_FENCING) { /* * If we are resetting, connecting or deleting we should