From cf23a7bc34e46f4a58c077607bdb9c3c9a00baad Mon Sep 17 00:00:00 2001 From: Peng Fan Date: Sat, 19 Sep 2026 21:08:19 +0800 Subject: [PATCH 01/28] nvme: use assign_bit() where applicable Convert open-coded if/else with set_bit/clear_bit to the assign_bit API. Reviewed-by: Christoph Hellwig Signed-off-by: Peng Fan Signed-off-by: Keith Busch --- drivers/nvme/host/multipath.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/drivers/nvme/host/multipath.c b/drivers/nvme/host/multipath.c index 75dbb58286a3..98b167df61f4 100644 --- a/drivers/nvme/host/multipath.c +++ b/drivers/nvme/host/multipath.c @@ -1167,10 +1167,7 @@ static ssize_t delayed_removal_secs_store(struct device *dev, mutex_lock(&head->subsys->lock); head->delayed_removal_secs = sec; - if (sec) - set_bit(NVME_NSHEAD_QUEUE_IF_NO_PATH, &head->flags); - else - clear_bit(NVME_NSHEAD_QUEUE_IF_NO_PATH, &head->flags); + assign_bit(NVME_NSHEAD_QUEUE_IF_NO_PATH, &head->flags, sec); mutex_unlock(&head->subsys->lock); /* * Ensure that update to NVME_NSHEAD_QUEUE_IF_NO_PATH is seen From 25f82e8212fb0b121879bea8023a166de40f5f41 Mon Sep 17 00:00:00 2001 From: Guixin Liu Date: Sun, 20 Sep 2026 10:52:14 +0800 Subject: [PATCH 02/28] nvme-multipath: expose path_state via sysfs Add a read-only "path_state" sysfs attribute to each NVMe path namespace device (/sys/class/nvme/nvmeX/nvmeXcYnZ/path_state) that exposes the current path state, including whether the path is enabled or disabled with a specific reason. Factor the path disable checks from nvme_path_is_disabled() into a new nvme_path_get_state() helper that returns an enum nvme_path_state. This keeps the path selection logic and sysfs reporting in sync, so any future updates to the path disable criteria are automatically reflected in the sysfs output. Possible values: - "enabled" : path is usable for I/O - "ctrl-down" : controller is not live - "ana-pending" : ANA state change pending - "ns-not-ready" : namespace is not ready This gives userspace visibility into the multipath path selection state without requiring users to piece together controller state and namespace flags manually. Reviewed-by: Hannes Reinecke Reviewed-by: Christoph Hellwig Reviewed-by: Nilay Shroff Signed-off-by: Guixin Liu Signed-off-by: Keith Busch --- Documentation/ABI/stable/sysfs-nvme | 21 +++++++++++++ drivers/nvme/host/multipath.c | 47 +++++++++++++++++++++++++---- drivers/nvme/host/nvme.h | 1 + drivers/nvme/host/sysfs.c | 4 ++- 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/Documentation/ABI/stable/sysfs-nvme b/Documentation/ABI/stable/sysfs-nvme index a2f5d0710db4..a0bb88ca1694 100644 --- a/Documentation/ABI/stable/sysfs-nvme +++ b/Documentation/ABI/stable/sysfs-nvme @@ -326,6 +326,27 @@ Description: currently selected path. Returns empty if iopolicy is not "numa". Requires CONFIG_NVME_MULTIPATH. +What: /sys/block/nvmeXcYnZ/path_state +Date: September 2026 +KernelVersion: 7.4 +Contact: Guixin Liu +Description: + Shows the current multipath path state for this namespace + path device. Possible values: + + "enabled": the path is usable for I/O. + + "ctrl-down": the path is disabled because the controller + is not live. + + "ana-pending": the path is disabled because an ANA state + change is pending. + + "ns-not-ready": the path is disabled because the namespace + is not ready. + + Requires CONFIG_NVME_MULTIPATH. + What: /sys/block/nvmeXnY/delayed_removal_secs Date: May 2025 KernelVersion: 6.16 diff --git a/drivers/nvme/host/multipath.c b/drivers/nvme/host/multipath.c index 98b167df61f4..3967911a6891 100644 --- a/drivers/nvme/host/multipath.c +++ b/drivers/nvme/host/multipath.c @@ -307,7 +307,14 @@ int nvme_mpath_revalidate_zones(struct nvme_ns_head *head) } #endif /* CONFIG_BLK_DEV_ZONED */ -static bool nvme_path_is_disabled(struct nvme_ns *ns) +enum nvme_path_state { + NVME_PATH_ENABLED, + NVME_PATH_DISABLED_CTRL_DOWN, + NVME_PATH_DISABLED_ANA_PENDING, + NVME_PATH_DISABLED_NS_NOT_READY, +}; + +static enum nvme_path_state nvme_path_get_state(struct nvme_ns *ns) { enum nvme_ctrl_state state = nvme_ctrl_state(ns->ctrl); @@ -317,11 +324,17 @@ static bool nvme_path_is_disabled(struct nvme_ns *ns) * Otherwise it will fail immediately and return to the requeue list. */ if (state != NVME_CTRL_LIVE && state != NVME_CTRL_DELETING) - return true; - if (test_bit(NVME_NS_ANA_PENDING, &ns->flags) || - !test_bit(NVME_NS_READY, &ns->flags)) - return true; - return false; + return NVME_PATH_DISABLED_CTRL_DOWN; + if (test_bit(NVME_NS_ANA_PENDING, &ns->flags)) + return NVME_PATH_DISABLED_ANA_PENDING; + if (!test_bit(NVME_NS_READY, &ns->flags)) + return NVME_PATH_DISABLED_NS_NOT_READY; + return NVME_PATH_ENABLED; +} + +static bool nvme_path_is_disabled(struct nvme_ns *ns) +{ + return nvme_path_get_state(ns) != NVME_PATH_ENABLED; } static struct nvme_ns *__nvme_find_path(struct nvme_ns_head *head, int node) @@ -1113,6 +1126,28 @@ static ssize_t queue_depth_show(struct device *dev, } DEVICE_ATTR_RO(queue_depth); +static const char * const nvme_path_state_names[] = { + [NVME_PATH_ENABLED] = "enabled", + [NVME_PATH_DISABLED_CTRL_DOWN] = "ctrl-down", + [NVME_PATH_DISABLED_ANA_PENDING] = "ana-pending", + [NVME_PATH_DISABLED_NS_NOT_READY] = "ns-not-ready", +}; + +static ssize_t path_state_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct nvme_ns *ns = nvme_get_ns_from_dev(dev); + enum nvme_path_state state = nvme_path_get_state(ns); + const char *name = "unknown"; + + if (state < ARRAY_SIZE(nvme_path_state_names) && + nvme_path_state_names[state]) + name = nvme_path_state_names[state]; + + return sysfs_emit(buf, "%s\n", name); +} +DEVICE_ATTR_RO(path_state); + static ssize_t numa_nodes_show(struct device *dev, struct device_attribute *attr, char *buf) { diff --git a/drivers/nvme/host/nvme.h b/drivers/nvme/host/nvme.h index 2cff9fcbf740..2606d9ce16b1 100644 --- a/drivers/nvme/host/nvme.h +++ b/drivers/nvme/host/nvme.h @@ -1095,6 +1095,7 @@ extern struct device_attribute dev_attr_ana_grpid; extern struct device_attribute dev_attr_ana_state; extern struct device_attribute dev_attr_queue_depth; extern struct device_attribute dev_attr_numa_nodes; +extern struct device_attribute dev_attr_path_state; extern struct device_attribute dev_attr_delayed_removal_secs; extern struct device_attribute dev_attr_multipath_failover_count; extern struct device_attribute dev_attr_io_requeue_no_usable_path_count; diff --git a/drivers/nvme/host/sysfs.c b/drivers/nvme/host/sysfs.c index 02a2490a9ed7..8c6adf4fc168 100644 --- a/drivers/nvme/host/sysfs.c +++ b/drivers/nvme/host/sysfs.c @@ -263,6 +263,7 @@ static struct attribute *nvme_ns_attrs[] = { &dev_attr_ana_state.attr, &dev_attr_queue_depth.attr, &dev_attr_numa_nodes.attr, + &dev_attr_path_state.attr, &dev_attr_delayed_removal_secs.attr, #endif &dev_attr_io_passthru_err_log_enabled.attr, @@ -296,7 +297,8 @@ static umode_t nvme_ns_attrs_are_visible(struct kobject *kobj, if (!nvme_ctrl_use_ana(nvme_get_ns_from_dev(dev)->ctrl)) return 0; } - if (a == &dev_attr_queue_depth.attr || a == &dev_attr_numa_nodes.attr) { + if (a == &dev_attr_queue_depth.attr || a == &dev_attr_numa_nodes.attr || + a == &dev_attr_path_state.attr) { if (nvme_disk_is_ns_head(dev_to_disk(dev))) return 0; } From d9cc476535d29a44df3f2aa3b14af9a833fddf90 Mon Sep 17 00:00:00 2001 From: Laurence Oberman Date: Mon, 21 Sep 2026 10:43:47 -0400 Subject: [PATCH 03/28] nvme-multipath: add iopolicies sysfs attribute The subsystem iopolicy attribute only shows the active policy, so there is no way to see which policies are available. Changing the output of iopolicy would break existing userspace, since nvme-cli compares the value against a bare policy name. Leave iopolicy unchanged and add a read-only iopolicies attribute that lists the available policies: # cat iopolicies numa round-robin queue-depth The current policy is still read from iopolicy. Reviewed-by: John Garry Reviewed-by: Christoph Hellwig Signed-off-by: Laurence Oberman Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Keith Busch --- drivers/nvme/host/multipath.c | 15 +++++++++++++++ drivers/nvme/host/nvme.h | 1 + drivers/nvme/host/sysfs.c | 1 + 3 files changed, 17 insertions(+) diff --git a/drivers/nvme/host/multipath.c b/drivers/nvme/host/multipath.c index 3967911a6891..11871f5f18c2 100644 --- a/drivers/nvme/host/multipath.c +++ b/drivers/nvme/host/multipath.c @@ -1098,6 +1098,21 @@ static ssize_t nvme_subsys_iopolicy_store(struct device *dev, SUBSYS_ATTR_RW(iopolicy, S_IRUGO | S_IWUSR, nvme_subsys_iopolicy_show, nvme_subsys_iopolicy_store); +static ssize_t iopolicies_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int i, len = 0; + + for (i = 0; i < ARRAY_SIZE(nvme_iopolicy_names); i++) + len += sysfs_emit_at(buf, len, "%s%s", i ? " " : "", + nvme_iopolicy_names[i]); + + len += sysfs_emit_at(buf, len, "\n"); + return len; +} + +struct device_attribute subsys_attr_iopolicies = __ATTR_RO(iopolicies); + static ssize_t ana_grpid_show(struct device *dev, struct device_attribute *attr, char *buf) { diff --git a/drivers/nvme/host/nvme.h b/drivers/nvme/host/nvme.h index 2606d9ce16b1..bac25b287d25 100644 --- a/drivers/nvme/host/nvme.h +++ b/drivers/nvme/host/nvme.h @@ -1101,6 +1101,7 @@ extern struct device_attribute dev_attr_multipath_failover_count; extern struct device_attribute dev_attr_io_requeue_no_usable_path_count; extern struct device_attribute dev_attr_io_fail_no_available_path_count; extern struct device_attribute subsys_attr_iopolicy; +extern struct device_attribute subsys_attr_iopolicies; static inline bool nvme_disk_is_ns_head(struct gendisk *disk) { diff --git a/drivers/nvme/host/sysfs.c b/drivers/nvme/host/sysfs.c index 8c6adf4fc168..4ac3ea3a86fd 100644 --- a/drivers/nvme/host/sysfs.c +++ b/drivers/nvme/host/sysfs.c @@ -1300,6 +1300,7 @@ static struct attribute *nvme_subsys_attrs[] = { &subsys_attr_subsystype.attr, #ifdef CONFIG_NVME_MULTIPATH &subsys_attr_iopolicy.attr, + &subsys_attr_iopolicies.attr, #endif NULL, }; From 1826a8e0b6d69a346b5193637cf45bb8a84e0223 Mon Sep 17 00:00:00 2001 From: Yureka Lilian Date: Sun, 6 Sep 2026 22:15:29 +0200 Subject: [PATCH 04/28] dt-bindings: nvme: Add apple,t8132-nvme-ans2 compatible Add a new base compatible for the ANS2 NVMe on the Apple t8132 (M4) SoC, which requires the IO queues to be registered using additional MMIO writes. Signed-off-by: Yureka Lilian Acked-by: Conor Dooley Signed-off-by: Keith Busch --- Documentation/devicetree/bindings/nvme/apple,nvme-ans.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/devicetree/bindings/nvme/apple,nvme-ans.yaml b/Documentation/devicetree/bindings/nvme/apple,nvme-ans.yaml index 4c0b1f90aff8..a1e5b1831468 100644 --- a/Documentation/devicetree/bindings/nvme/apple,nvme-ans.yaml +++ b/Documentation/devicetree/bindings/nvme/apple,nvme-ans.yaml @@ -16,6 +16,7 @@ properties: - items: - const: apple,t6020-nvme-ans2 - const: apple,t8103-nvme-ans2 + - const: apple,t8132-nvme-ans2 - items: - enum: # Do not add additional SoC to this list. From 304bc392dbfb7fc173b46b3bebf17a4b023c2e07 Mon Sep 17 00:00:00 2001 From: Yureka Lilian Date: Sun, 6 Sep 2026 22:15:30 +0200 Subject: [PATCH 05/28] nvme: apple: add t8132 (M4) ANS2 NVMe support Add support for ANS2 NVMe on the Apple t8132 (M4) SoC. This variant requires the IO queues to be registered using additional MMIO writes. The qsize of the create_{cq,sq} command is now ignored, and instead a write to mmio_nvme+0x1210 (which was previously known as APPLE_ANS_MAX_PEND_CMDS_CTRL) sets the IO queue sizes. Signed-off-by: Yureka Lilian Reviewed-by: Sven Peter Signed-off-by: Keith Busch --- drivers/nvme/host/apple.c | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/drivers/nvme/host/apple.c b/drivers/nvme/host/apple.c index c63e28c75766..4621584b1b6f 100644 --- a/drivers/nvme/host/apple.c +++ b/drivers/nvme/host/apple.c @@ -42,7 +42,11 @@ #define APPLE_ANS_ACQ_DB 0x1004 #define APPLE_ANS_IOCQ_DB 0x100c -#define APPLE_ANS_MAX_PEND_CMDS_CTRL 0x1210 +#define APPLE_ANS_T8132_IOQ_CMDS 0x1200 +#define APPLE_ANS_T8132_IOQ_CQES 0x1208 +#define APPLE_ANS_T8132_IOQ_SIZE 0x1210 + +#define APPLE_ANS_T8103_MAX_PEND_CMDS_CTRL 0x1210 #define APPLE_ANS_BOOT_STATUS 0x1300 #define APPLE_ANS_BOOT_STATUS_OK 0xde71ce55 @@ -184,6 +188,7 @@ struct apple_nvme_iod { struct apple_nvme_hw { bool has_lsq_nvmmu; + bool needs_ioq_register; u32 max_queue_depth; }; @@ -1133,9 +1138,16 @@ static void apple_nvme_reset_work(struct work_struct *work) anv->mmio_nvme + APPLE_ANS_LINEAR_SQ_CTRL); /* Allow as many pending command as possible for both queues */ - writel(anv->hw->max_queue_depth - | (anv->hw->max_queue_depth << 16), anv->mmio_nvme - + APPLE_ANS_MAX_PEND_CMDS_CTRL); + if (!anv->hw->needs_ioq_register) { + /* + * Since T8132, this is replaced by IOQ_SIZE which is + * located at the same offset and is handled with the + * IO queue setup further below. + */ + writel(anv->hw->max_queue_depth + | (anv->hw->max_queue_depth << 16), anv->mmio_nvme + + APPLE_ANS_T8103_MAX_PEND_CMDS_CTRL); + } /* Setup the NVMMU for the maximum admin and IO queue depth */ writel(anv->hw->max_queue_depth - 1, @@ -1190,6 +1202,16 @@ static void apple_nvme_reset_work(struct work_struct *work) if (ret) goto out_remove_cq; + if (anv->hw->needs_ioq_register) { + writeq(anv->ioq.cq_dma_addr, + anv->mmio_nvme + APPLE_ANS_T8132_IOQ_CQES); + writeq(anv->ioq.sq_dma_addr, + anv->mmio_nvme + APPLE_ANS_T8132_IOQ_CMDS); + writel((anv->hw->max_queue_depth - 1) + | ((anv->hw->max_queue_depth - 1) << 16), anv->mmio_nvme + + APPLE_ANS_T8132_IOQ_SIZE); + } + apple_nvme_init_queue(&anv->ioq); nr_io_queues = 1; ret = nvme_set_queue_count(&anv->ctrl, &nr_io_queues); @@ -1699,17 +1721,26 @@ static DEFINE_SIMPLE_DEV_PM_OPS(apple_nvme_pm_ops, apple_nvme_suspend, static const struct apple_nvme_hw apple_nvme_t8015_hw = { .has_lsq_nvmmu = false, + .needs_ioq_register = false, .max_queue_depth = 16, }; static const struct apple_nvme_hw apple_nvme_t8103_hw = { .has_lsq_nvmmu = true, + .needs_ioq_register = false, + .max_queue_depth = 64, +}; + +static const struct apple_nvme_hw apple_nvme_t8132_hw = { + .has_lsq_nvmmu = true, + .needs_ioq_register = true, .max_queue_depth = 64, }; static const struct of_device_id apple_nvme_of_match[] = { { .compatible = "apple,t8015-nvme-ans2", .data = &apple_nvme_t8015_hw }, { .compatible = "apple,t8103-nvme-ans2", .data = &apple_nvme_t8103_hw }, + { .compatible = "apple,t8132-nvme-ans2", .data = &apple_nvme_t8132_hw }, { .compatible = "apple,nvme-ans2", .data = &apple_nvme_t8103_hw }, {}, }; From 87a1ac965502858f9207153a92c3aef8ecf6d4ae Mon Sep 17 00:00:00 2001 From: Surabhi Gogte Date: Tue, 22 Sep 2026 14:07:16 -0600 Subject: [PATCH 06/28] nvme-tcp: store and use the caller's network namespace nvme_tcp_alloc_queue() creates each socket using current->nsproxy->net_ns, which is correct for the initial connect that runs in userspace context. However, the reconnect and error recovery paths run from a workqueue where current netns is a kernel thread in init_net, so sockets created during reconnect end up in a different namespace than the one the controller was originally connected from. Fix this by capturing the caller's network namespace at controller allocation time. Use ctrl->net for all socket creation so that every queue, on initial connect, reset, or reconnect, is created in the same namespace. Fixes: 3f2304f8c6d6 ("nvme-tcp: add NVMe over TCP host driver") Signed-off-by: Surabhi Gogte Reviewed-by: Hannes Reinecke Reviewed-by: Sagi Grimberg Signed-off-by: Keith Busch --- drivers/nvme/host/tcp.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/drivers/nvme/host/tcp.c b/drivers/nvme/host/tcp.c index 921934028e0b..f916bcca304f 100644 --- a/drivers/nvme/host/tcp.c +++ b/drivers/nvme/host/tcp.c @@ -171,6 +171,7 @@ struct nvme_tcp_ctrl { struct delayed_work connect_work; struct nvme_tcp_request async_req; u32 io_queues[HCTX_MAX_TYPES]; + struct net *net; }; static struct workqueue_struct *nvme_tcp_wq; @@ -1858,7 +1859,7 @@ static int nvme_tcp_alloc_queue(struct nvme_ctrl *nctrl, int qid, queue->cmnd_capsule_len = sizeof(struct nvme_command) + NVME_TCP_ADMIN_CCSZ; - ret = sock_create_kern(current->nsproxy->net_ns, + ret = sock_create_kern(ctrl->net, ctrl->addr.ss_family, SOCK_STREAM, IPPROTO_TCP, &queue->sock); if (ret) { @@ -2650,6 +2651,7 @@ static void nvme_tcp_free_ctrl(struct nvme_ctrl *nctrl) nvmf_free_options(nctrl->opts); free_ctrl: + put_net(ctrl->net); kfree(ctrl->queues); kfree(ctrl); } @@ -3048,12 +3050,15 @@ static struct nvme_tcp_ctrl *nvme_tcp_alloc_ctrl(struct device *dev, goto out_free_ctrl; } + ctrl->net = get_net(current->nsproxy->net_ns); + ret = nvme_init_ctrl(&ctrl->ctrl, dev, &nvme_tcp_ctrl_ops, 0); if (ret) goto out_kfree_queues; return ctrl; out_kfree_queues: + put_net(ctrl->net); kfree(ctrl->queues); out_free_ctrl: kfree(ctrl); From c58465c1e3d6f6fa720c922781cad4330a573cfb Mon Sep 17 00:00:00 2001 From: Surabhi Gogte Date: Tue, 22 Sep 2026 14:07:17 -0600 Subject: [PATCH 07/28] nvme-tcp: refactor I/O queue setup path Split the I/O queue setup helpers apart so that the individual steps can be called directly from nvme_tcp_configure_io_queues(): - Queue count negotiation moves out of nvme_tcp_alloc_io_queues() into a new nvme_tcp_set_io_queue_count(), leaving the allocator with just the per-queue allocation loop. - TLS PSK validation moves out of __nvme_tcp_alloc_io_queues() into a new nvme_tcp_tls_check_psk(). - nvme_tcp_configure_io_queues() now calls the three steps directly in the same order as before. Signed-off-by: Surabhi Gogte Reviewed-by: Hannes Reinecke Reviewed-by: Sagi Grimberg Signed-off-by: Keith Busch --- drivers/nvme/host/tcp.c | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/drivers/nvme/host/tcp.c b/drivers/nvme/host/tcp.c index f916bcca304f..9308feba8a74 100644 --- a/drivers/nvme/host/tcp.c +++ b/drivers/nvme/host/tcp.c @@ -2184,10 +2184,8 @@ static int nvme_tcp_alloc_admin_queue(struct nvme_ctrl *ctrl) return ret; } -static int __nvme_tcp_alloc_io_queues(struct nvme_ctrl *ctrl) +static int nvme_tcp_tls_check_psk(struct nvme_ctrl *ctrl) { - int i, ret; - if (nvme_tcp_tls_configured(ctrl)) { if (ctrl->opts->concat) { /* @@ -2209,6 +2207,13 @@ static int __nvme_tcp_alloc_io_queues(struct nvme_ctrl *ctrl) } } + return 0; +} + +static int __nvme_tcp_alloc_io_queues(struct nvme_ctrl *ctrl) +{ + int i, ret; + for (i = 1; i < ctrl->queue_count; i++) { ret = nvme_tcp_alloc_queue(ctrl, i, ctrl->tls_pskid); @@ -2225,7 +2230,7 @@ static int __nvme_tcp_alloc_io_queues(struct nvme_ctrl *ctrl) return ret; } -static int nvme_tcp_alloc_io_queues(struct nvme_ctrl *ctrl) +static int nvme_tcp_set_io_queue_count(struct nvme_ctrl *ctrl) { unsigned int nr_io_queues; int ret; @@ -2247,14 +2252,22 @@ static int nvme_tcp_alloc_io_queues(struct nvme_ctrl *ctrl) nvmf_set_io_queues(ctrl->opts, nr_io_queues, to_tcp_ctrl(ctrl)->io_queues); - return __nvme_tcp_alloc_io_queues(ctrl); + return 0; } static int nvme_tcp_configure_io_queues(struct nvme_ctrl *ctrl, bool new) { int ret, nr_queues; - ret = nvme_tcp_alloc_io_queues(ctrl); + ret = nvme_tcp_set_io_queue_count(ctrl); + if (ret) + return ret; + + ret = nvme_tcp_tls_check_psk(ctrl); + if (ret) + return ret; + + ret = __nvme_tcp_alloc_io_queues(ctrl); if (ret) return ret; From 61b0ed3be81b612ed4e33841a840aab27610499e Mon Sep 17 00:00:00 2001 From: Surabhi Gogte Date: Tue, 22 Sep 2026 14:07:18 -0600 Subject: [PATCH 08/28] nvme-tcp: parallelize I/O queue allocation and startup Similar to commit 2a8513091d2f ("nvme-rdma: parallelize I/O queue allocation and startup"), refactor nvme tcp I/O queue setup to use async API, combining allocation and startup into a single parallel operation per queue. This reduces connection and reconnection setup time when there are delays in establishing connections, which is especially important for high-core-count hosts. Key changes: - Use async API to facilitate parallel calls for io queue setup. - Add nvme_tcp_queue_setup_ctx for propagating errors from async workers. - Remove nvme_tcp_start_io_queues() and __nvme_tcp_alloc_io_queues(); their logic is folded into nvme_tcp_setup_io_queues() and nvme_tcp_configure_io_queues(). - Allocate the io tag set before the queues so that the queue range is known, and only set up the reconnect grow case if the queue count actually increased. - Serialize the cpu scan and claim in nvme_tcp_set_queue_io_cpu() with a spinlock, as concurrent callers would otherwise select the same cpu. The per-cpu counters no longer need to be atomics. Testing on a 64-core host with 64 IO-queues shows nvme-tcp connection time reduced from 61ms to 11ms. Signed-off-by: Surabhi Gogte Reviewed-by: Hannes Reinecke Signed-off-by: Keith Busch --- drivers/nvme/host/tcp.c | 119 ++++++++++++++++++++++++++-------------- 1 file changed, 77 insertions(+), 42 deletions(-) diff --git a/drivers/nvme/host/tcp.c b/drivers/nvme/host/tcp.c index 9308feba8a74..ec678ed07af8 100644 --- a/drivers/nvme/host/tcp.c +++ b/drivers/nvme/host/tcp.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -54,7 +55,8 @@ MODULE_PARM_DESC(tls_handshake_timeout, "nvme TLS handshake timeout in seconds (default 10)"); #endif -static atomic_t nvme_tcp_cpu_queues[NR_CPUS]; +static int nvme_tcp_cpu_queues[NR_CPUS]; +static DEFINE_SPINLOCK(nvme_tcp_cpu_queues_lock); enum nvme_tcp_send_state { NVME_TCP_SEND_CMD_PDU = 0, @@ -154,6 +156,12 @@ struct nvme_tcp_queue { static DEFINE_MUTEX(nvme_tcp_ctrl_mutex); static LIST_HEAD_GUARDED(nvme_tcp_ctrl_list, nvme_tcp_ctrl_mutex); +struct nvme_tcp_queue_setup_ctx { + struct nvme_ctrl *ctrl; + int qid; + int *err; +}; + struct nvme_tcp_ctrl { /* read only in the hot path */ struct nvme_tcp_queue *queues; @@ -1731,9 +1739,10 @@ static void nvme_tcp_set_queue_io_cpu(struct nvme_tcp_queue *queue) goto out; /* Search for the least used cpu from the mq_map */ + spin_lock(&nvme_tcp_cpu_queues_lock); io_cpu = WORK_CPU_UNBOUND; for_each_online_cpu(cpu) { - int num_queues = atomic_read(&nvme_tcp_cpu_queues[cpu]); + int num_queues = nvme_tcp_cpu_queues[cpu]; if (mq_map[cpu] != qid) continue; @@ -1744,9 +1753,10 @@ static void nvme_tcp_set_queue_io_cpu(struct nvme_tcp_queue *queue) } if (io_cpu != WORK_CPU_UNBOUND) { queue->io_cpu = io_cpu; - atomic_inc(&nvme_tcp_cpu_queues[io_cpu]); + nvme_tcp_cpu_queues[io_cpu]++; set_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags); } + spin_unlock(&nvme_tcp_cpu_queues_lock); out: dev_dbg(ctrl->ctrl.device, "queue %d: using cpu %d\n", qid, queue->io_cpu); @@ -2023,8 +2033,11 @@ static void nvme_tcp_stop_queue_nowait(struct nvme_ctrl *nctrl, int qid) if (!test_bit(NVME_TCP_Q_ALLOCATED, &queue->flags)) return; - if (test_and_clear_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags)) - atomic_dec(&nvme_tcp_cpu_queues[queue->io_cpu]); + if (test_and_clear_bit(NVME_TCP_Q_IO_CPU_SET, &queue->flags)) { + spin_lock(&nvme_tcp_cpu_queues_lock); + nvme_tcp_cpu_queues[queue->io_cpu]--; + spin_unlock(&nvme_tcp_cpu_queues_lock); + } mutex_lock(&queue->queue_lock); if (test_and_clear_bit(NVME_TCP_Q_LIVE, &queue->flags)) @@ -2131,25 +2144,6 @@ static void nvme_tcp_stop_io_queues(struct nvme_ctrl *ctrl) nvme_tcp_wait_queue(ctrl, i); } -static int nvme_tcp_start_io_queues(struct nvme_ctrl *ctrl, - int first, int last) -{ - int i, ret; - - for (i = first; i < last; i++) { - ret = nvme_tcp_start_queue(ctrl, i); - if (ret) - goto out_stop_queues; - } - - return 0; - -out_stop_queues: - for (i--; i >= first; i--) - nvme_tcp_stop_queue(ctrl, i); - return ret; -} - static int nvme_tcp_alloc_admin_queue(struct nvme_ctrl *ctrl) { int ret; @@ -2210,22 +2204,67 @@ static int nvme_tcp_tls_check_psk(struct nvme_ctrl *ctrl) return 0; } -static int __nvme_tcp_alloc_io_queues(struct nvme_ctrl *ctrl) +static void nvme_tcp_setup_queue_async(void *data, async_cookie_t cookie) { - int i, ret; + struct nvme_tcp_queue_setup_ctx *ctx = data; + struct nvme_ctrl *ctrl = ctx->ctrl; + int ret; - for (i = 1; i < ctrl->queue_count; i++) { - ret = nvme_tcp_alloc_queue(ctrl, i, - ctrl->tls_pskid); - if (ret) - goto out_free_queues; + ret = nvme_tcp_alloc_queue(ctrl, ctx->qid, ctrl->tls_pskid); + if (ret) + goto out_err; + + ret = nvme_tcp_start_queue(ctrl, ctx->qid); + if (ret) + goto out_err; + + return; + +out_err: + WRITE_ONCE(*ctx->err, ret); +} + +static int nvme_tcp_setup_io_queues(struct nvme_ctrl *ctrl, unsigned int first, + unsigned int last) +{ + ASYNC_DOMAIN_EXCLUSIVE(queue_domain); + struct nvme_tcp_queue_setup_ctx *ctxs; + int nr_queues = last - first; + int err = 0, i, ret; + + if (first >= last) + return 0; + + ctxs = kmalloc_objs(*ctxs, nr_queues); + if (!ctxs) + return -ENOMEM; + + for (i = 0; i < nr_queues; i++) { + ctxs[i].ctrl = ctrl; + ctxs[i].qid = first + i; + ctxs[i].err = &err; + async_schedule_domain(nvme_tcp_setup_queue_async, &ctxs[i], + &queue_domain); } + async_synchronize_full_domain(&queue_domain); + kfree(ctxs); + + ret = READ_ONCE(err); + if (ret) + goto out_free_queues; + return 0; out_free_queues: - for (i--; i >= 1; i--) - nvme_tcp_free_queue(ctrl, i); + for (i = first; i < last; i++) { + struct nvme_tcp_queue *queue = &to_tcp_ctrl(ctrl)->queues[i]; + + if (test_bit(NVME_TCP_Q_LIVE, &queue->flags)) + nvme_tcp_stop_queue(ctrl, i); + if (test_bit(NVME_TCP_Q_ALLOCATED, &queue->flags)) + nvme_tcp_free_queue(ctrl, i); + } return ret; } @@ -2267,10 +2306,6 @@ static int nvme_tcp_configure_io_queues(struct nvme_ctrl *ctrl, bool new) if (ret) return ret; - ret = __nvme_tcp_alloc_io_queues(ctrl); - if (ret) - return ret; - if (new) { ret = nvme_alloc_io_tag_set(ctrl, &to_tcp_ctrl(ctrl)->tag_set, &nvme_tcp_mq_ops, @@ -2281,12 +2316,12 @@ static int nvme_tcp_configure_io_queues(struct nvme_ctrl *ctrl, bool new) } /* - * Only start IO queues for which we have allocated the tagset + * Only setup IO queues for which we have allocated the tagset * and limited it to the available queues. On reconnects, the * queue number might have changed. */ nr_queues = min(ctrl->tagset->nr_hw_queues + 1, ctrl->queue_count); - ret = nvme_tcp_start_io_queues(ctrl, 1, nr_queues); + ret = nvme_tcp_setup_io_queues(ctrl, 1, nr_queues); if (ret) goto out_cleanup_connect_q; @@ -2310,9 +2345,9 @@ static int nvme_tcp_configure_io_queues(struct nvme_ctrl *ctrl, bool new) /* * If the number of queues has increased (reconnect case) - * start all new queues now. + * setup all new queues now. */ - ret = nvme_tcp_start_io_queues(ctrl, nr_queues, + ret = nvme_tcp_setup_io_queues(ctrl, nr_queues, ctrl->tagset->nr_hw_queues + 1); if (ret) goto out_wait_freeze_timed_out; @@ -3157,7 +3192,7 @@ static int __init nvme_tcp_init_module(void) return -ENOMEM; for_each_possible_cpu(cpu) - atomic_set(&nvme_tcp_cpu_queues[cpu], 0); + nvme_tcp_cpu_queues[cpu] = 0; nvmf_register_transport(&nvme_tcp_transport); return 0; From e98e45e9465bfd0b1635d70001ca8b294a29b98f Mon Sep 17 00:00:00 2001 From: Jesse Taube Date: Wed, 23 Sep 2026 16:20:03 -0400 Subject: [PATCH 09/28] nvmet: Add traffic based keep alive support to configfs Add the ability to turn on and off tbkas (traffic based keep alive support) via configfs. This is useful for testing and debugging. Reviewed-by: Mohamed Khalfella Reviewed-by: Christoph Hellwig Signed-off-by: Jesse Taube Signed-off-by: Keith Busch --- drivers/nvme/target/admin-cmd.c | 4 +++- drivers/nvme/target/configfs.c | 29 +++++++++++++++++++++++++++++ drivers/nvme/target/core.c | 4 +++- drivers/nvme/target/nvmet.h | 2 ++ 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/drivers/nvme/target/admin-cmd.c b/drivers/nvme/target/admin-cmd.c index 7764a3c0195c..26028be8fe4d 100644 --- a/drivers/nvme/target/admin-cmd.c +++ b/drivers/nvme/target/admin-cmd.c @@ -696,7 +696,9 @@ static void nvmet_execute_identify_ctrl(struct nvmet_req *req) /* XXX: figure out what to do about RTD3R/RTD3 */ id->oaes = cpu_to_le32(NVMET_AEN_CFG_OPTIONAL); - ctratt = NVME_CTRL_ATTR_HID_128_BIT | NVME_CTRL_ATTR_TBKAS; + ctratt = NVME_CTRL_ATTR_HID_128_BIT; + if (ctrl->tbkas) + ctratt |= NVME_CTRL_ATTR_TBKAS; if (nvmet_is_pci_ctrl(ctrl)) ctratt |= NVME_CTRL_ATTR_RHII; id->ctratt = cpu_to_le32(ctratt); diff --git a/drivers/nvme/target/configfs.c b/drivers/nvme/target/configfs.c index 6286e38436dd..2b879b1d4c3c 100644 --- a/drivers/nvme/target/configfs.c +++ b/drivers/nvme/target/configfs.c @@ -1369,6 +1369,34 @@ static ssize_t nvmet_subsys_attr_serial_store(struct config_item *item, } CONFIGFS_ATTR(nvmet_subsys_, attr_serial); +static ssize_t nvmet_subsys_attr_tbkas_show(struct config_item *item, + char *page) +{ + return snprintf(page, PAGE_SIZE, "%d\n", to_subsys(item)->tbkas); +} + +static ssize_t nvmet_subsys_attr_tbkas_store(struct config_item *item, + const char *page, size_t count) +{ + struct nvmet_subsys *subsys = to_subsys(item); + struct nvmet_ctrl *ctrl; + bool tbkas; + + if (kstrtobool(page, &tbkas)) + return -EINVAL; + + down_write(&nvmet_config_sem); + subsys->tbkas = tbkas; + + /* Force reconnect */ + list_for_each_entry(ctrl, &subsys->ctrls, subsys_entry) + ctrl->ops->delete_ctrl(ctrl); + up_write(&nvmet_config_sem); + + return count; +} +CONFIGFS_ATTR(nvmet_subsys_, attr_tbkas); + static ssize_t nvmet_subsys_attr_cntlid_min_show(struct config_item *item, char *page) { @@ -1706,6 +1734,7 @@ static struct configfs_attribute *nvmet_subsys_attrs[] = { &nvmet_subsys_attr_attr_qid_max, &nvmet_subsys_attr_attr_ieee_oui, &nvmet_subsys_attr_attr_firmware, + &nvmet_subsys_attr_attr_tbkas, #ifdef CONFIG_BLK_DEV_INTEGRITY &nvmet_subsys_attr_attr_pi_enable, #endif diff --git a/drivers/nvme/target/core.c b/drivers/nvme/target/core.c index 43871a8f56ca..6b4bda231bd9 100644 --- a/drivers/nvme/target/core.c +++ b/drivers/nvme/target/core.c @@ -1227,7 +1227,7 @@ bool nvmet_req_init(struct nvmet_req *req, struct nvmet_sq *sq, goto fail; } - if (sq->ctrl) + if (sq->ctrl && sq->ctrl->tbkas) sq->ctrl->reset_tbkas = true; return true; @@ -1677,6 +1677,7 @@ struct nvmet_ctrl *nvmet_alloc_ctrl(struct nvmet_alloc_ctrl_args *args) mutex_lock(&subsys->lock); ctrl->max_qid = subsys->max_qid; + ctrl->tbkas = subsys->tbkas; ctrl->sqs = kzalloc_objs(struct nvmet_sq *, ctrl->max_qid + 1); if (!ctrl->sqs) @@ -1865,6 +1866,7 @@ struct nvmet_subsys *nvmet_subsys_alloc(const char *subsysnqn, } subsys->ieee_oui = 0; + subsys->tbkas = true; subsys->firmware_rev = kstrndup(UTS_RELEASE, NVMET_FR_MAX_SIZE, GFP_KERNEL); if (!subsys->firmware_rev) { diff --git a/drivers/nvme/target/nvmet.h b/drivers/nvme/target/nvmet.h index dbda55895f4f..2bc0e87ea981 100644 --- a/drivers/nvme/target/nvmet.h +++ b/drivers/nvme/target/nvmet.h @@ -270,6 +270,7 @@ struct nvmet_ctrl { u16 cntlid; u16 max_qid; u32 kato; + bool tbkas; struct nvmet_port *port; @@ -343,6 +344,7 @@ struct nvmet_subsys { bool subsys_discovered; char *subsysnqn; bool pi_support; + bool tbkas; struct config_group group; From 9e7fe5ade9fd1058be20ff8cc43a944ef435662c Mon Sep 17 00:00:00 2001 From: Nilay Shroff Date: Tue, 29 Sep 2026 16:56:47 +0530 Subject: [PATCH 10/28] nvmet: introduce struct nvmet_passthru Currently, passthru parameters are grouped under struct nvmet_subsys. Since passthru can be configured through configfs and all passthru parameters are exposed under a separate configfs subdirectory, group these parameters under a new struct nvmet_passthru. Grouping passthru-specific parameters in a separate structure makes the code easier to maintain and reason about. This also allows the passthru structure to be allocated on demand, avoiding the memory overhead for subsystems that do not use passthru. The structure is allocated when passthru configuration is needed and its configfs attributes are accessed under subsys->lock. The passthru configfs group is still created when the subsystem is allocated. There are no functional changes intended. Reviewed-by: Christoph Hellwig Signed-off-by: Nilay Shroff Signed-off-by: Keith Busch --- drivers/nvme/target/configfs.c | 90 +++++++++++++++++++++++++++++----- drivers/nvme/target/core.c | 12 ++++- drivers/nvme/target/nvmet.h | 34 ++++++++++--- drivers/nvme/target/passthru.c | 52 ++++++++++++-------- 4 files changed, 148 insertions(+), 40 deletions(-) diff --git a/drivers/nvme/target/configfs.c b/drivers/nvme/target/configfs.c index 2b879b1d4c3c..0b12b75c6e07 100644 --- a/drivers/nvme/target/configfs.c +++ b/drivers/nvme/target/configfs.c @@ -902,21 +902,34 @@ static ssize_t nvmet_passthru_device_path_show(struct config_item *item, char *page) { struct nvmet_subsys *subsys = to_subsys(item->ci_parent); + ssize_t ret; - return snprintf(page, PAGE_SIZE, "%s\n", subsys->passthru_ctrl_path); + mutex_lock(&subsys->lock); + ret = snprintf(page, PAGE_SIZE, "%s\n", + subsys->passthru && subsys->passthru->ctrl_path ? + subsys->passthru->ctrl_path : ""); + mutex_unlock(&subsys->lock); + return ret; } static ssize_t nvmet_passthru_device_path_store(struct config_item *item, const char *page, size_t count) { struct nvmet_subsys *subsys = to_subsys(item->ci_parent); + struct nvmet_passthru *passthru; size_t len; int ret; mutex_lock(&subsys->lock); + passthru = nvmet_subsys_passthru(subsys); + if (!passthru) { + ret = -ENOMEM; + goto out_unlock; + } + ret = -EBUSY; - if (subsys->passthru_ctrl) + if (passthru->ctrl) goto out_unlock; ret = -EINVAL; @@ -924,10 +937,10 @@ static ssize_t nvmet_passthru_device_path_store(struct config_item *item, if (!len) goto out_unlock; - kfree(subsys->passthru_ctrl_path); + kfree(passthru->ctrl_path); ret = -ENOMEM; - subsys->passthru_ctrl_path = kstrndup(page, len, GFP_KERNEL); - if (!subsys->passthru_ctrl_path) + passthru->ctrl_path = kstrndup(page, len, GFP_KERNEL); + if (!passthru->ctrl_path) goto out_unlock; mutex_unlock(&subsys->lock); @@ -943,8 +956,13 @@ static ssize_t nvmet_passthru_enable_show(struct config_item *item, char *page) { struct nvmet_subsys *subsys = to_subsys(item->ci_parent); + ssize_t ret; - return sprintf(page, "%d\n", subsys->passthru_ctrl ? 1 : 0); + mutex_lock(&subsys->lock); + ret = sprintf(page, "%d\n", + subsys->passthru && subsys->passthru->ctrl ? 1 : 0); + mutex_unlock(&subsys->lock); + return ret; } static ssize_t nvmet_passthru_enable_store(struct config_item *item, @@ -969,18 +987,34 @@ CONFIGFS_ATTR(nvmet_passthru_, enable); static ssize_t nvmet_passthru_admin_timeout_show(struct config_item *item, char *page) { - return sprintf(page, "%u\n", to_subsys(item->ci_parent)->admin_timeout); + struct nvmet_subsys *subsys = to_subsys(item->ci_parent); + ssize_t ret; + + mutex_lock(&subsys->lock); + ret = sprintf(page, "%u\n", subsys->passthru ? + subsys->passthru->admin_timeout : 0); + mutex_unlock(&subsys->lock); + return ret; } static ssize_t nvmet_passthru_admin_timeout_store(struct config_item *item, const char *page, size_t count) { struct nvmet_subsys *subsys = to_subsys(item->ci_parent); + struct nvmet_passthru *passthru; unsigned int timeout; if (kstrtouint(page, 0, &timeout)) return -EINVAL; - subsys->admin_timeout = timeout; + + mutex_lock(&subsys->lock); + passthru = nvmet_subsys_passthru(subsys); + if (!passthru) { + mutex_unlock(&subsys->lock); + return -ENOMEM; + } + passthru->admin_timeout = timeout; + mutex_unlock(&subsys->lock); return count; } CONFIGFS_ATTR(nvmet_passthru_, admin_timeout); @@ -988,18 +1022,34 @@ CONFIGFS_ATTR(nvmet_passthru_, admin_timeout); static ssize_t nvmet_passthru_io_timeout_show(struct config_item *item, char *page) { - return sprintf(page, "%u\n", to_subsys(item->ci_parent)->io_timeout); + struct nvmet_subsys *subsys = to_subsys(item->ci_parent); + ssize_t ret; + + mutex_lock(&subsys->lock); + ret = sprintf(page, "%u\n", + subsys->passthru ? subsys->passthru->io_timeout : 0); + mutex_unlock(&subsys->lock); + return ret; } static ssize_t nvmet_passthru_io_timeout_store(struct config_item *item, const char *page, size_t count) { struct nvmet_subsys *subsys = to_subsys(item->ci_parent); + struct nvmet_passthru *passthru; unsigned int timeout; if (kstrtouint(page, 0, &timeout)) return -EINVAL; - subsys->io_timeout = timeout; + + mutex_lock(&subsys->lock); + passthru = nvmet_subsys_passthru(subsys); + if (!passthru) { + mutex_unlock(&subsys->lock); + return -ENOMEM; + } + passthru->io_timeout = timeout; + mutex_unlock(&subsys->lock); return count; } CONFIGFS_ATTR(nvmet_passthru_, io_timeout); @@ -1007,18 +1057,34 @@ CONFIGFS_ATTR(nvmet_passthru_, io_timeout); static ssize_t nvmet_passthru_clear_ids_show(struct config_item *item, char *page) { - return sprintf(page, "%u\n", to_subsys(item->ci_parent)->clear_ids); + struct nvmet_subsys *subsys = to_subsys(item->ci_parent); + ssize_t ret; + + mutex_lock(&subsys->lock); + ret = sprintf(page, "%u\n", + subsys->passthru ? subsys->passthru->clear_ids : 0); + mutex_unlock(&subsys->lock); + return ret; } static ssize_t nvmet_passthru_clear_ids_store(struct config_item *item, const char *page, size_t count) { struct nvmet_subsys *subsys = to_subsys(item->ci_parent); + struct nvmet_passthru *passthru; unsigned int clear_ids; if (kstrtouint(page, 0, &clear_ids)) return -EINVAL; - subsys->clear_ids = clear_ids; + + mutex_lock(&subsys->lock); + passthru = nvmet_subsys_passthru(subsys); + if (!passthru) { + mutex_unlock(&subsys->lock); + return -ENOMEM; + } + passthru->clear_ids = clear_ids; + mutex_unlock(&subsys->lock); return count; } CONFIGFS_ATTR(nvmet_passthru_, clear_ids); diff --git a/drivers/nvme/target/core.c b/drivers/nvme/target/core.c index 6b4bda231bd9..a9c9d3ef76cb 100644 --- a/drivers/nvme/target/core.c +++ b/drivers/nvme/target/core.c @@ -1637,8 +1637,16 @@ struct nvmet_ctrl *nvmet_alloc_ctrl(struct nvmet_alloc_ctrl_args *args) #ifdef CONFIG_NVME_TARGET_PASSTHRU /* By default, set loop targets to clear IDS by default */ - if (ctrl->port->disc_addr.trtype == NVMF_TRTYPE_LOOP) - subsys->clear_ids = 1; + if (ctrl->port->disc_addr.trtype == NVMF_TRTYPE_LOOP) { + mutex_lock(&subsys->lock); + subsys->passthru = nvmet_subsys_passthru(subsys); + if (!subsys->passthru) { + mutex_unlock(&subsys->lock); + goto out_free_ctrl; + } + subsys->passthru->clear_ids = 1; + mutex_unlock(&subsys->lock); + } #endif INIT_WORK(&ctrl->async_event_work, nvmet_async_event_work); diff --git a/drivers/nvme/target/nvmet.h b/drivers/nvme/target/nvmet.h index 2bc0e87ea981..25b419ff6437 100644 --- a/drivers/nvme/target/nvmet.h +++ b/drivers/nvme/target/nvmet.h @@ -318,6 +318,14 @@ struct nvmet_ctrl { struct nvmet_pr_log_mgr pr_log_mgr; }; +struct nvmet_passthru { + struct nvme_ctrl *ctrl; + char *ctrl_path; + unsigned int admin_timeout; + unsigned int io_timeout; + unsigned int clear_ids; +}; + struct nvmet_subsys { enum nvme_subsys_type type; @@ -358,12 +366,8 @@ struct nvmet_subsys { char *firmware_rev; #ifdef CONFIG_NVME_TARGET_PASSTHRU - struct nvme_ctrl *passthru_ctrl; - char *passthru_ctrl_path; + struct nvmet_passthru *passthru; struct config_group passthru_group; - unsigned int admin_timeout; - unsigned int io_timeout; - unsigned int clear_ids; #endif /* CONFIG_NVME_TARGET_PASSTHRU */ #ifdef CONFIG_BLK_DEV_ZONED @@ -793,7 +797,20 @@ u16 nvmet_parse_passthru_admin_cmd(struct nvmet_req *req); u16 nvmet_parse_passthru_io_cmd(struct nvmet_req *req); static inline bool nvmet_is_passthru_subsys(struct nvmet_subsys *subsys) { - return subsys->passthru_ctrl; + return subsys->passthru && subsys->passthru->ctrl; +} + +static inline struct nvmet_passthru *nvmet_subsys_passthru( + struct nvmet_subsys *subsys) +{ + lockdep_assert_held(&subsys->lock); + + if (!subsys->passthru) { + subsys->passthru = kzalloc_obj(*subsys->passthru); + if (!subsys->passthru) + return NULL; + } + return subsys->passthru; } #else /* CONFIG_NVME_TARGET_PASSTHRU */ static inline void nvmet_passthru_subsys_free(struct nvmet_subsys *subsys) @@ -814,6 +831,11 @@ static inline bool nvmet_is_passthru_subsys(struct nvmet_subsys *subsys) { return NULL; } +static inline struct nvmet_passthru *nvmet_subsys_passthru( + struct nvmet_subsys *subsys) +{ + return NULL; +} #endif /* CONFIG_NVME_TARGET_PASSTHRU */ static inline bool nvmet_is_passthru_req(struct nvmet_req *req) diff --git a/drivers/nvme/target/passthru.c b/drivers/nvme/target/passthru.c index fa6527c537e2..16cd3fdf98ec 100644 --- a/drivers/nvme/target/passthru.c +++ b/drivers/nvme/target/passthru.c @@ -26,7 +26,7 @@ void nvmet_passthrough_override_cap(struct nvmet_ctrl *ctrl) * Multiple command set support can only be declared if the underlying * controller actually supports it. */ - if (!nvme_multi_css(ctrl->subsys->passthru_ctrl)) + if (!nvme_multi_css(ctrl->subsys->passthru->ctrl)) ctrl->cap &= ~(1ULL << 43); } @@ -39,7 +39,7 @@ static u16 nvmet_passthru_override_id_descs(struct nvmet_req *req) void *data; u8 csi; - if (!ctrl->subsys->clear_ids) + if (!ctrl->subsys->passthru->clear_ids) return status; data = kzalloc(NVME_IDENTIFY_DATA_SIZE, GFP_KERNEL); @@ -89,7 +89,7 @@ static u16 nvmet_passthru_override_id_descs(struct nvmet_req *req) static u16 nvmet_passthru_override_id_ctrl(struct nvmet_req *req) { struct nvmet_ctrl *ctrl = req->sq->ctrl; - struct nvme_ctrl *pctrl = ctrl->subsys->passthru_ctrl; + struct nvme_ctrl *pctrl = ctrl->subsys->passthru->ctrl; u16 status = NVME_SC_SUCCESS; struct nvme_id_ctrl *id; unsigned int max_hw_sectors; @@ -208,7 +208,7 @@ static u16 nvmet_passthru_override_id_ns(struct nvmet_req *req) */ id->mc = 0; - if (req->sq->ctrl->subsys->clear_ids) { + if (req->sq->ctrl->subsys->passthru->clear_ids) { memset(id->nguid, 0, NVME_NIDT_NGUID_LEN); memset(id->eui64, 0, NVME_NIDT_EUI64_LEN); } @@ -305,7 +305,8 @@ static int nvmet_passthru_map_sg(struct nvmet_req *req, struct request *rq) static void nvmet_passthru_execute_cmd(struct nvmet_req *req) { - struct nvme_ctrl *ctrl = nvmet_req_subsys(req)->passthru_ctrl; + struct nvmet_passthru *passthru = nvmet_req_subsys(req)->passthru; + struct nvme_ctrl *ctrl = passthru->ctrl; struct request_queue *q = ctrl->admin_q; struct nvme_ns *ns = NULL; struct request *rq = NULL; @@ -325,9 +326,9 @@ static void nvmet_passthru_execute_cmd(struct nvmet_req *req) } q = ns->queue; - timeout = nvmet_req_subsys(req)->io_timeout; + timeout = passthru->io_timeout; } else { - timeout = nvmet_req_subsys(req)->admin_timeout; + timeout = passthru->admin_timeout; } rq = blk_mq_alloc_request(q, nvme_req_op(req->cmd), 0); @@ -386,7 +387,7 @@ static void nvmet_passthru_execute_cmd(struct nvmet_req *req) */ static void nvmet_passthru_set_host_behaviour(struct nvmet_req *req) { - struct nvme_ctrl *ctrl = nvmet_req_subsys(req)->passthru_ctrl; + struct nvme_ctrl *ctrl = nvmet_req_subsys(req)->passthru->ctrl; struct nvme_feat_host_behavior *host; u16 status = NVME_SC_INTERNAL; int ret; @@ -586,15 +587,17 @@ u16 nvmet_parse_passthru_admin_cmd(struct nvmet_req *req) int nvmet_passthru_ctrl_enable(struct nvmet_subsys *subsys) { + struct nvmet_passthru *passthru; struct nvme_ctrl *ctrl; struct file *file; int ret = -EINVAL; void *old; mutex_lock(&subsys->lock); - if (!subsys->passthru_ctrl_path) + passthru = subsys->passthru; + if (!passthru || !passthru->ctrl_path) goto out_unlock; - if (subsys->passthru_ctrl) + if (passthru->ctrl) goto out_unlock; if (subsys->nr_namespaces) { @@ -602,7 +605,7 @@ int nvmet_passthru_ctrl_enable(struct nvmet_subsys *subsys) goto out_unlock; } - file = filp_open(subsys->passthru_ctrl_path, O_RDWR, 0); + file = filp_open(passthru->ctrl_path, O_RDWR, 0); if (IS_ERR(file)) { ret = PTR_ERR(file); goto out_unlock; @@ -611,7 +614,7 @@ int nvmet_passthru_ctrl_enable(struct nvmet_subsys *subsys) ctrl = nvme_ctrl_from_file(file); if (!ctrl) { pr_err("failed to open nvme controller %s\n", - subsys->passthru_ctrl_path); + passthru->ctrl_path); goto out_put_file; } @@ -626,7 +629,7 @@ int nvmet_passthru_ctrl_enable(struct nvmet_subsys *subsys) if (old) goto out_put_file; - subsys->passthru_ctrl = ctrl; + passthru->ctrl = ctrl; subsys->ver = ctrl->vs; if (subsys->ver < NVME_VS(1, 2, 1)) { @@ -636,7 +639,7 @@ int nvmet_passthru_ctrl_enable(struct nvmet_subsys *subsys) subsys->ver = NVME_VS(1, 2, 1); } nvme_get_ctrl(ctrl); - __module_get(subsys->passthru_ctrl->ops->module); + __module_get(passthru->ctrl->ops->module); ret = 0; out_put_file: @@ -648,12 +651,17 @@ int nvmet_passthru_ctrl_enable(struct nvmet_subsys *subsys) static void __nvmet_passthru_ctrl_disable(struct nvmet_subsys *subsys) { - if (subsys->passthru_ctrl) { - xa_erase(&passthru_subsystems, subsys->passthru_ctrl->instance); - module_put(subsys->passthru_ctrl->ops->module); - nvme_put_ctrl(subsys->passthru_ctrl); + struct nvmet_passthru *passthru = subsys->passthru; + + if (!passthru) + return; + + if (passthru->ctrl) { + xa_erase(&passthru_subsystems, passthru->ctrl->instance); + module_put(passthru->ctrl->ops->module); + nvme_put_ctrl(passthru->ctrl); } - subsys->passthru_ctrl = NULL; + passthru->ctrl = NULL; subsys->ver = NVMET_DEFAULT_VS; } @@ -668,6 +676,10 @@ void nvmet_passthru_subsys_free(struct nvmet_subsys *subsys) { mutex_lock(&subsys->lock); __nvmet_passthru_ctrl_disable(subsys); + if (subsys->passthru) { + kfree(subsys->passthru->ctrl_path); + kfree(subsys->passthru); + subsys->passthru = NULL; + } mutex_unlock(&subsys->lock); - kfree(subsys->passthru_ctrl_path); } From 5e12d81874a135258217252cf9980b2294ef8958 Mon Sep 17 00:00:00 2001 From: Nilay Shroff Date: Tue, 29 Sep 2026 16:56:48 +0530 Subject: [PATCH 11/28] nvmet: fix use-after-free in passthru I/O hotpath Concurrently disabling a passthru controller while passthru I/Os are in flight can potentially result in a use-after-free. Introduce a percpu refcount, an atomic flag, and an nvmet request flag to protect the passthru controller lifetime. When enabling the passthru controller, initialize the percpu refcount and set the enabled flag. Each passthru I/O acquires a reference before entering the passthru hotpath and sets an nvmet request flag to record that the reference is held. The reference is released when the I/O completes. When disabling the passthru controller, clear the enabled flag, kill the percpu refcount, and wait for all in-flight I/Os to release their references before releasing the passthru controller. Disable operations are serialized by subsys->lock. The enabled flag ensures that only the first disable operation proceeds and subsequent attempts observe the flag as cleared and return. Once disabling starts, new I/Os either fail to be routed to the passthru path or fail to acquire a live reference, and therefore cannot dereference the passthru controller and thus avoid use-after-free. Reviewed-by: Christoph Hellwig Signed-off-by: Nilay Shroff Signed-off-by: Keith Busch --- drivers/nvme/target/core.c | 3 ++ drivers/nvme/target/nvmet.h | 34 +++++++++++++++++- drivers/nvme/target/passthru.c | 65 +++++++++++++++++++++++++++++----- 3 files changed, 92 insertions(+), 10 deletions(-) diff --git a/drivers/nvme/target/core.c b/drivers/nvme/target/core.c index a9c9d3ef76cb..291d782ce544 100644 --- a/drivers/nvme/target/core.c +++ b/drivers/nvme/target/core.c @@ -811,6 +811,8 @@ static void __nvmet_req_complete(struct nvmet_req *req, u16 status) nvmet_pr_put_ns_pc_ref(pc_ref); if (ns) nvmet_put_namespace(ns); + + nvmet_put_passthru_ref(req); } void nvmet_req_complete(struct nvmet_req *req, u16 status) @@ -1187,6 +1189,7 @@ bool nvmet_req_init(struct nvmet_req *req, struct nvmet_sq *sq, req->error_loc = NVMET_NO_ERROR_LOC; req->error_slba = 0; req->pc_ref = NULL; + req->p.ref_held = false; /* no support for fused commands yet */ if (unlikely(flags & (NVME_CMD_FUSE_FIRST | NVME_CMD_FUSE_SECOND))) { diff --git a/drivers/nvme/target/nvmet.h b/drivers/nvme/target/nvmet.h index 25b419ff6437..a0bd05603271 100644 --- a/drivers/nvme/target/nvmet.h +++ b/drivers/nvme/target/nvmet.h @@ -319,6 +319,11 @@ struct nvmet_ctrl { }; struct nvmet_passthru { + struct percpu_ref ref; + struct completion disable_done; +#define NVMET_PASSTHRU_ENABLED 0 + unsigned long flags; + struct nvme_ctrl *ctrl; char *ctrl_path; unsigned int admin_timeout; @@ -478,6 +483,7 @@ struct nvmet_req { struct request *rq; struct work_struct work; bool use_workqueue; + bool ref_held; } p; #ifdef CONFIG_BLK_DEV_ZONED struct { @@ -797,7 +803,8 @@ u16 nvmet_parse_passthru_admin_cmd(struct nvmet_req *req); u16 nvmet_parse_passthru_io_cmd(struct nvmet_req *req); static inline bool nvmet_is_passthru_subsys(struct nvmet_subsys *subsys) { - return subsys->passthru && subsys->passthru->ctrl; + return subsys->passthru && + test_bit(NVMET_PASSTHRU_ENABLED, &subsys->passthru->flags); } static inline struct nvmet_passthru *nvmet_subsys_passthru( @@ -812,6 +819,24 @@ static inline struct nvmet_passthru *nvmet_subsys_passthru( } return subsys->passthru; } + +static inline bool nvmet_get_passthru_ref(struct nvmet_req *req) +{ + if (!percpu_ref_tryget_live(&nvmet_req_subsys(req)->passthru->ref)) + return false; + + req->p.ref_held = true; + return true; +} + +static inline void nvmet_put_passthru_ref(struct nvmet_req *req) +{ + if (!req->p.ref_held) + return; + + req->p.ref_held = false; + percpu_ref_put(&nvmet_req_subsys(req)->passthru->ref); +} #else /* CONFIG_NVME_TARGET_PASSTHRU */ static inline void nvmet_passthru_subsys_free(struct nvmet_subsys *subsys) { @@ -836,6 +861,13 @@ static inline struct nvmet_passthru *nvmet_subsys_passthru( { return NULL; } +static inline bool nvmet_get_passthru_ref(struct nvmet_req *req) +{ + return false; +} +static inline void nvmet_put_passthru_ref(struct nvmet_req *req) +{ +} #endif /* CONFIG_NVME_TARGET_PASSTHRU */ static inline bool nvmet_is_passthru_req(struct nvmet_req *req) diff --git a/drivers/nvme/target/passthru.c b/drivers/nvme/target/passthru.c index 16cd3fdf98ec..6b67880c7bb2 100644 --- a/drivers/nvme/target/passthru.c +++ b/drivers/nvme/target/passthru.c @@ -305,9 +305,9 @@ static int nvmet_passthru_map_sg(struct nvmet_req *req, struct request *rq) static void nvmet_passthru_execute_cmd(struct nvmet_req *req) { - struct nvmet_passthru *passthru = nvmet_req_subsys(req)->passthru; - struct nvme_ctrl *ctrl = passthru->ctrl; - struct request_queue *q = ctrl->admin_q; + struct nvmet_passthru *passthru; + struct nvme_ctrl *ctrl; + struct request_queue *q; struct nvme_ns *ns = NULL; struct request *rq = NULL; unsigned int timeout; @@ -315,6 +315,15 @@ static void nvmet_passthru_execute_cmd(struct nvmet_req *req) u16 status; int ret; + if (!nvmet_get_passthru_ref(req)) { + status = NVME_SC_INTERNAL | NVME_STATUS_DNR; + goto out; + } + + passthru = nvmet_req_subsys(req)->passthru; + ctrl = passthru->ctrl; + q = ctrl->admin_q; + if (likely(req->sq->qid != 0)) { u32 nsid = le32_to_cpu(req->cmd->common.nsid); @@ -387,11 +396,17 @@ static void nvmet_passthru_execute_cmd(struct nvmet_req *req) */ static void nvmet_passthru_set_host_behaviour(struct nvmet_req *req) { - struct nvme_ctrl *ctrl = nvmet_req_subsys(req)->passthru->ctrl; + struct nvme_ctrl *ctrl; struct nvme_feat_host_behavior *host; u16 status = NVME_SC_INTERNAL; int ret; + if (!nvmet_get_passthru_ref(req)) { + status |= NVME_STATUS_DNR; + goto out_complete_req; + } + ctrl = nvmet_req_subsys(req)->passthru->ctrl; + host = kzalloc(sizeof(*host) * 2, GFP_KERNEL); if (!host) goto out_complete_req; @@ -585,6 +600,14 @@ u16 nvmet_parse_passthru_admin_cmd(struct nvmet_req *req) } } +static void nvmet_release_passthru_ctrl(struct percpu_ref *ref) +{ + struct nvmet_passthru *passthru = container_of(ref, + struct nvmet_passthru, ref); + + complete(&passthru->disable_done); +} + int nvmet_passthru_ctrl_enable(struct nvmet_subsys *subsys) { struct nvmet_passthru *passthru; @@ -629,6 +652,13 @@ int nvmet_passthru_ctrl_enable(struct nvmet_subsys *subsys) if (old) goto out_put_file; + ret = percpu_ref_init(&passthru->ref, nvmet_release_passthru_ctrl, + 0, GFP_KERNEL); + if (ret) { + xa_erase(&passthru_subsystems, ctrl->instance); + goto out_put_file; + } + init_completion(&passthru->disable_done); passthru->ctrl = ctrl; subsys->ver = ctrl->vs; @@ -640,6 +670,7 @@ int nvmet_passthru_ctrl_enable(struct nvmet_subsys *subsys) } nvme_get_ctrl(ctrl); __module_get(passthru->ctrl->ops->module); + set_bit(NVMET_PASSTHRU_ENABLED, &passthru->flags); ret = 0; out_put_file: @@ -653,14 +684,30 @@ static void __nvmet_passthru_ctrl_disable(struct nvmet_subsys *subsys) { struct nvmet_passthru *passthru = subsys->passthru; + lockdep_assert_held(&subsys->lock); + if (!passthru) return; - if (passthru->ctrl) { - xa_erase(&passthru_subsystems, passthru->ctrl->instance); - module_put(passthru->ctrl->ops->module); - nvme_put_ctrl(passthru->ctrl); - } + if (!test_and_clear_bit(NVMET_PASSTHRU_ENABLED, &passthru->flags)) + return; + + mutex_unlock(&subsys->lock); + /* + * Now new I/Os should not enter passthru hotpath as we cleared the + * enabled flag. Kill percpu reference and wait for in-flight I/Os + * to drain. + */ + percpu_ref_kill(&passthru->ref); + wait_for_completion(&passthru->disable_done); + percpu_ref_exit(&passthru->ref); + + mutex_lock(&subsys->lock); + + xa_erase(&passthru_subsystems, passthru->ctrl->instance); + module_put(passthru->ctrl->ops->module); + nvme_put_ctrl(passthru->ctrl); + passthru->ctrl = NULL; subsys->ver = NVMET_DEFAULT_VS; } From 909307cea08aff4e486cdb7491de5c76a2c31341 Mon Sep 17 00:00:00 2001 From: Yehyeong Lee Date: Tue, 29 Sep 2026 00:38:11 +0900 Subject: [PATCH 12/28] nvme-rdma: don't complete a request after a bogus remote invalidation When a remote invalidation names an rkey that is not the one we handed out for this request, nvme_rdma_process_nvme_rsp() logs and schedules error recovery, then falls through to nvme_rdma_end_request(). The request is completed with req->mr never invalidated, and nvme_rdma_unmap_data() hands it back with ib_mr_pool_put(), which only puts it on a list. The rkey stays valid and still maps the pages the completed read filled, so a controller that sent the bogus invalidation can write into those pages after pread() has returned. Return instead, the way the local invalidation path below already does, and leave the request for error recovery to cancel. Fixes: 3ef0279bb0031 ("nvme-rdma: Check remotely invalidated rkey matches our expected rkey") Cc: stable@vger.kernel.org Reviewed-by: Christoph Hellwig Signed-off-by: Yehyeong Lee Signed-off-by: Keith Busch --- drivers/nvme/host/rdma.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/nvme/host/rdma.c b/drivers/nvme/host/rdma.c index 9cb811a2ce1f..5b9b540693b5 100644 --- a/drivers/nvme/host/rdma.c +++ b/drivers/nvme/host/rdma.c @@ -1748,6 +1748,7 @@ static void nvme_rdma_process_nvme_rsp(struct nvme_rdma_queue *queue, "Bogus remote invalidation for rkey %#x\n", req->mr ? req->mr->rkey : 0); nvme_rdma_error_recovery(queue->ctrl); + return; } } else if (req->mr) { int ret; From 53283a76180388c2bfc46eca2ef3ff2c131f5fc7 Mon Sep 17 00:00:00 2001 From: Martin George Date: Wed, 30 Sep 2026 21:06:58 +0530 Subject: [PATCH 13/28] nvme-multipath: report "none" iopolicy for discovery subsystems A discovery subsystem never exposes namespaces, so it has no multipath head and no I/O paths to select between. Nevertheless the per-subsystem iopolicy attribute is registered unconditionally and reads back the driver-wide default which is 'numa' for the discovery subsystems too. This can be misleading as it suggests a path selection policy is in effect when nothing is ever selected, and writing the attribute silently mutates state that can never be used. Report "none" instead for a discovery subsystem, and reject writes with -EOPNOTSUPP. Report "none" from iopolicies as well, so that the value read from iopolicy always remains a member of the set advertised by iopolicies; otherwise userspace validating the current policy against the available ones would reject its own subsystem. And while at it, document iopolicies too, which was not covered when it was added previously. Reviewed-by: Laurence Oberman Reviewed-by: Christoph Hellwig Signed-off-by: Martin George Signed-off-by: Keith Busch --- Documentation/ABI/stable/sysfs-nvme | 15 +++++++++++++++ drivers/nvme/host/multipath.c | 12 ++++++++++++ 2 files changed, 27 insertions(+) diff --git a/Documentation/ABI/stable/sysfs-nvme b/Documentation/ABI/stable/sysfs-nvme index a0bb88ca1694..67795a2dd905 100644 --- a/Documentation/ABI/stable/sysfs-nvme +++ b/Documentation/ABI/stable/sysfs-nvme @@ -465,6 +465,21 @@ Description: selections. Only available when CONFIG_NVME_MULTIPATH is enabled. + A discovery subsystem has no namespaces and hence no I/O + paths to select from: reading returns "none" and writing + fails with EOPNOTSUPP. + +What: /sys/class/nvme-subsystem/nvme-subsysX/iopolicies +Date: September 2026 +KernelVersion: 7.4 +Contact: Laurence Oberman +Description: + Shows the I/O path selection policies that may be written + to the iopolicy attribute of this subsystem, separated by + spaces: "numa round-robin queue-depth". A discovery + subsystem accepts no policy at all and reports "none". + Only available when CONFIG_NVME_MULTIPATH is enabled. + What: /sys/class/nvme-subsystem/nvme-subsysX/subsystype Date: September 2021 KernelVersion: 5.16 diff --git a/drivers/nvme/host/multipath.c b/drivers/nvme/host/multipath.c index 11871f5f18c2..56ee25b448ad 100644 --- a/drivers/nvme/host/multipath.c +++ b/drivers/nvme/host/multipath.c @@ -1054,6 +1054,10 @@ static ssize_t nvme_subsys_iopolicy_show(struct device *dev, struct nvme_subsystem *subsys = container_of(dev, struct nvme_subsystem, dev); + /* A discovery subsystem has no namespaces and hence no I/O paths */ + if (subsys->subtype == NVME_NQN_DISC) + return sysfs_emit(buf, "none\n"); + return sysfs_emit(buf, "%s\n", nvme_iopolicy_names[READ_ONCE(subsys->iopolicy)]); } @@ -1088,6 +1092,9 @@ static ssize_t nvme_subsys_iopolicy_store(struct device *dev, container_of(dev, struct nvme_subsystem, dev); int policy; + if (subsys->subtype == NVME_NQN_DISC) + return -EOPNOTSUPP; + policy = nvme_iopolicy_parse(buf); if (policy < 0) return policy; @@ -1101,8 +1108,13 @@ SUBSYS_ATTR_RW(iopolicy, S_IRUGO | S_IWUSR, static ssize_t iopolicies_show(struct device *dev, struct device_attribute *attr, char *buf) { + struct nvme_subsystem *subsys = + container_of(dev, struct nvme_subsystem, dev); int i, len = 0; + if (subsys->subtype == NVME_NQN_DISC) + return sysfs_emit(buf, "none\n"); + for (i = 0; i < ARRAY_SIZE(nvme_iopolicy_names); i++) len += sysfs_emit_at(buf, len, "%s%s", i ? " " : "", nvme_iopolicy_names[i]); From 0374b39b72feb9139fd74b979d37200a1f22ca76 Mon Sep 17 00:00:00 2001 From: Krishna Iyer Date: Thu, 8 Oct 2026 07:14:06 -0700 Subject: [PATCH 14/28] nvme-multipath: fix path state evaluation for failfast and ANA nvme_available_path() has two problems that keep I/O queued when it should be failed: 1. When fast_io_fail_tmo expires, NVME_CTRL_FAILFAST_EXPIRED is set and the path is skipped, but the function still falls through to nvme_mpath_queue_if_no_path(). If delayed_removal_secs is configured the I/O is requeued indefinitely, defeating fast_io_fail_tmo. Only fall through to queue_if_no_path when there really are no paths; if a path exists but its failfast timer has expired, fail instead. 2. A LIVE controller counts as a usable path regardless of the namespace ANA state. A path whose ANA state is inaccessible or persistent-loss cannot serve I/O, so it should not count as available. ANA change is transient and bounded by ANATT, so keep queueing while it resolves. Also kick the requeue work from nvme_update_ns_ana_state() when a path does not transition to live, so parked I/O is re-evaluated when an ANA transition leaves the namespace inaccessible. Move nvme_state_is_live() above nvme_available_path() so it can be used there. Assisted-by: Claude:claude-fable-5 Reviewed-by: Christoph Hellwig Signed-off-by: Krishna Iyer Reviewed-by: Hannes Reinecke Reviewed-by: Nilay Shroff Signed-off-by: Keith Busch --- drivers/nvme/host/multipath.c | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/drivers/nvme/host/multipath.c b/drivers/nvme/host/multipath.c index 56ee25b448ad..a280a4b4a508 100644 --- a/drivers/nvme/host/multipath.c +++ b/drivers/nvme/host/multipath.c @@ -509,9 +509,15 @@ inline struct nvme_ns *nvme_find_path(struct nvme_ns_head *head) } } +static inline bool nvme_state_is_live(enum nvme_ana_state state) +{ + return state == NVME_ANA_OPTIMIZED || state == NVME_ANA_NONOPTIMIZED; +} + static bool nvme_available_path(struct nvme_ns_head *head) __must_hold_shared(&head->srcu) { + bool failfast = false; struct nvme_ns *ns; if (!test_bit(NVME_NSHEAD_DISK_LIVE, &head->flags)) @@ -519,10 +525,16 @@ static bool nvme_available_path(struct nvme_ns_head *head) list_for_each_entry_srcu(ns, &head->list, siblings, srcu_read_lock_held(&head->srcu)) { - if (test_bit(NVME_CTRL_FAILFAST_EXPIRED, &ns->ctrl->flags)) + if (test_bit(NVME_CTRL_FAILFAST_EXPIRED, &ns->ctrl->flags)) { + failfast = true; continue; + } switch (nvme_ctrl_state(ns->ctrl)) { case NVME_CTRL_LIVE: + if (!nvme_state_is_live(ns->ana_state) && + ns->ana_state != NVME_ANA_CHANGE) + continue; + return true; case NVME_CTRL_RESETTING: case NVME_CTRL_CONNECTING: return true; @@ -531,6 +543,9 @@ static bool nvme_available_path(struct nvme_ns_head *head) } } + if (failfast) + return false; + /* * If "head->delayed_removal_secs" is configured (i.e., non-zero), do * not immediately fail I/O. Instead, requeue the I/O for the configured @@ -887,11 +902,6 @@ static int nvme_parse_ana_log(struct nvme_ctrl *ctrl, void *data, return 0; } -static inline bool nvme_state_is_live(enum nvme_ana_state state) -{ - return state == NVME_ANA_OPTIMIZED || state == NVME_ANA_NONOPTIMIZED; -} - static void nvme_update_ns_ana_state(struct nvme_ana_group_desc *desc, struct nvme_ns *ns) { @@ -926,8 +936,10 @@ static void nvme_update_ns_ana_state(struct nvme_ana_group_desc *desc, * is not live but still create the sysfs link to this path from * head node if head node of the path has already come alive. */ - if (test_bit(NVME_NSHEAD_DISK_LIVE, &ns->head->flags)) + if (test_bit(NVME_NSHEAD_DISK_LIVE, &ns->head->flags)) { nvme_mpath_add_sysfs_link(ns->head); + kblockd_schedule_work(&ns->head->requeue_work); + } } } From 970ce28526c879678439770b8262830822515971 Mon Sep 17 00:00:00 2001 From: Krishna Iyer Date: Thu, 8 Oct 2026 07:14:07 -0700 Subject: [PATCH 15/28] nvme-multipath: add fail_on_ctrl_loss sysfs attribute When no usable path exists, I/O on a multipath namespace is queued until a path returns. With ctrl_loss_tmo=-1 that can be forever: during a long fabric outage any process waiting on the I/O is stuck in D state. We hit this on virtualization hosts, where a SIGKILLed VM process cannot exit while draining I/O to an unreachable NVMe/TCP target. Nothing can fail this I/O without tearing something down: controller deletion takes every namespace on the controller with it. Add a fail_on_ctrl_loss attribute on the ns-head disk: a persistent per-namespace policy to fail parked and newly arriving I/O instead of queueing it when no usable path exists. It is the namespace-scoped counterpart to the controller-scoped fast_io_fail_tmo: the trigger is an event userspace observes (a consumer known to be gone, for us a SIGKILLed VM the host must reap), not a duration picked up front, and sibling namespaces behind the same controllers keep queueing and ride out the outage. A CONNECTING controller stops counting as an available path, and with no controllers left the policy overrides the delayed_removal_secs queue-if-no-path window; the two are opposites, so fail_on_ctrl_loss takes precedence when both are set. ANA change and controller resetting still queue, since both are transient. Controller state is untouched and reconnects continue. Like dm-multipath, the policy is transport agnostic. Assisted-by: Claude:claude-fable-5 Reviewed-by: Nilay Shroff Reviewed-by: Christoph Hellwig Signed-off-by: Krishna Iyer Signed-off-by: Keith Busch --- Documentation/ABI/stable/sysfs-nvme | 16 +++++++++ drivers/nvme/host/multipath.c | 50 +++++++++++++++++++++++++++-- drivers/nvme/host/nvme.h | 2 ++ drivers/nvme/host/sysfs.c | 4 ++- 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/Documentation/ABI/stable/sysfs-nvme b/Documentation/ABI/stable/sysfs-nvme index 67795a2dd905..9291595f8102 100644 --- a/Documentation/ABI/stable/sysfs-nvme +++ b/Documentation/ABI/stable/sysfs-nvme @@ -358,6 +358,22 @@ Description: is deferred. Only visible on multipath head devices. Requires CONFIG_NVME_MULTIPATH. +What: /sys/block/nvmeXnY/fail_on_ctrl_loss +Date: October 2026 +KernelVersion: 7.4 +Contact: Krishna Iyer +Description: + Shows or sets the fail-on-ctrl-loss policy of the multipath + head device ("on" or "off", default "off"). When on, I/O + queued or arriving while no usable path exists is failed + immediately instead of being queued. This is the opposite of + the queue-if-no-path behavior enabled by a nonzero + delayed_removal_secs: when both are set fail_on_ctrl_loss takes + precedence and the parked I/O is failed rather than held for + the delayed_removal_secs window. Reconnect attempts are not + affected. Only visible on multipath head devices. + Requires CONFIG_NVME_MULTIPATH. + What: /sys/block/nvmeXnY/csi What: /sys/block/nvmeXnY/metadata_bytes What: /sys/block/nvmeXnY/nuse diff --git a/drivers/nvme/host/multipath.c b/drivers/nvme/host/multipath.c index a280a4b4a508..87b934b46b85 100644 --- a/drivers/nvme/host/multipath.c +++ b/drivers/nvme/host/multipath.c @@ -517,6 +517,8 @@ static inline bool nvme_state_is_live(enum nvme_ana_state state) static bool nvme_available_path(struct nvme_ns_head *head) __must_hold_shared(&head->srcu) { + bool fail_on_ctrl_loss = test_bit(NVME_NSHEAD_FAIL_ON_CTRL_LOSS, + &head->flags); bool failfast = false; struct nvme_ns *ns; @@ -536,14 +538,17 @@ static bool nvme_available_path(struct nvme_ns_head *head) continue; return true; case NVME_CTRL_RESETTING: - case NVME_CTRL_CONNECTING: return true; + case NVME_CTRL_CONNECTING: + if (!fail_on_ctrl_loss) + return true; + continue; default: break; } } - if (failfast) + if (failfast || fail_on_ctrl_loss) return false; /* @@ -1254,6 +1259,47 @@ static ssize_t delayed_removal_secs_store(struct device *dev, DEVICE_ATTR_RW(delayed_removal_secs); +static ssize_t fail_on_ctrl_loss_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct gendisk *disk = dev_to_disk(dev); + struct nvme_ns_head *head = disk->private_data; + + return sysfs_emit(buf, test_bit(NVME_NSHEAD_FAIL_ON_CTRL_LOSS, + &head->flags) ? "on\n" : "off\n"); +} + +static ssize_t fail_on_ctrl_loss_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + struct gendisk *disk = dev_to_disk(dev); + struct nvme_ns_head *head = disk->private_data; + bool enable; + int ret; + + ret = kstrtobool(buf, &enable); + if (ret < 0) + return ret; + + if (enable) { + if (test_and_set_bit(NVME_NSHEAD_FAIL_ON_CTRL_LOSS, &head->flags)) + return count; + } else { + if (!test_and_clear_bit(NVME_NSHEAD_FAIL_ON_CTRL_LOSS, + &head->flags)) + return count; + } + + synchronize_srcu(&head->srcu); + + if (enable) + kblockd_schedule_work(&head->requeue_work); + + return count; +} + +DEVICE_ATTR_RW(fail_on_ctrl_loss); + static ssize_t multipath_failover_count_show(struct device *dev, struct device_attribute *attr, char *buf) { diff --git a/drivers/nvme/host/nvme.h b/drivers/nvme/host/nvme.h index bac25b287d25..a8ef62b8e34b 100644 --- a/drivers/nvme/host/nvme.h +++ b/drivers/nvme/host/nvme.h @@ -589,6 +589,7 @@ struct nvme_ns_head { #define NVME_NSHEAD_DISK_LIVE 0 #define NVME_NSHEAD_QUEUE_IF_NO_PATH 1 #define NVME_NSHEAD_CDEV_LIVE 2 +#define NVME_NSHEAD_FAIL_ON_CTRL_LOSS 3 struct nvme_ns __rcu_guarded *current_path[]; #endif }; @@ -1097,6 +1098,7 @@ extern struct device_attribute dev_attr_queue_depth; extern struct device_attribute dev_attr_numa_nodes; extern struct device_attribute dev_attr_path_state; extern struct device_attribute dev_attr_delayed_removal_secs; +extern struct device_attribute dev_attr_fail_on_ctrl_loss; extern struct device_attribute dev_attr_multipath_failover_count; extern struct device_attribute dev_attr_io_requeue_no_usable_path_count; extern struct device_attribute dev_attr_io_fail_no_available_path_count; diff --git a/drivers/nvme/host/sysfs.c b/drivers/nvme/host/sysfs.c index 4ac3ea3a86fd..b3abb852a3ec 100644 --- a/drivers/nvme/host/sysfs.c +++ b/drivers/nvme/host/sysfs.c @@ -265,6 +265,7 @@ static struct attribute *nvme_ns_attrs[] = { &dev_attr_numa_nodes.attr, &dev_attr_path_state.attr, &dev_attr_delayed_removal_secs.attr, + &dev_attr_fail_on_ctrl_loss.attr, #endif &dev_attr_io_passthru_err_log_enabled.attr, NULL, @@ -302,7 +303,8 @@ static umode_t nvme_ns_attrs_are_visible(struct kobject *kobj, if (nvme_disk_is_ns_head(dev_to_disk(dev))) return 0; } - if (a == &dev_attr_delayed_removal_secs.attr) { + if (a == &dev_attr_delayed_removal_secs.attr || + a == &dev_attr_fail_on_ctrl_loss.attr) { struct gendisk *disk = dev_to_disk(dev); if (!nvme_disk_is_ns_head(disk)) From f316aa7f018840523939183425394931b373e4d7 Mon Sep 17 00:00:00 2001 From: Daejun Park Date: Fri, 2 Oct 2026 16:02:00 +0900 Subject: [PATCH 16/28] nvmet: fix use-after-free of the old registrant in nvmet_pr_preempt() nvmet_pr_preempt() handles a preempt sent by the reservation holder by changing the reservation type with nvmet_pr_update_reg_attr(), unless the reservation is of an All Registrants type. For the holder that helper does not modify the registrant in place: it puts a copy on the registrant list, points pr->holder to the copy and passes the old registrant to kfree_rcu(). If the reservation type changed, nvmet_pr_preempt() then hands ®->hostid of that old registrant to nvmet_pr_resv_released(). Unless the reservation released notification is masked, that function takes subsys->lock and compares the host id against every controller of the subsystem. The caller only holds pr->pr_sem and is not in an RCU read-side critical section, so nothing keeps the old registrant alive while it is read. The result is a read of 16 freed bytes that decides which controllers get the notification. Found by code inspection. kfree_rcu() defers the free, so it usually happens after the read; when the pending free is forced to run before the read, KASAN reports the host id as freed memory. Pass the host id of the controller that sent the command instead. The registrant was looked up by that host id, and the controller outlives the request. Fixes: 5a47c2080a73 ("nvmet: support reservation feature") Cc: stable@vger.kernel.org Reviewed-by: Christoph Hellwig Signed-off-by: Daejun Park Signed-off-by: Keith Busch --- drivers/nvme/target/pr.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/nvme/target/pr.c b/drivers/nvme/target/pr.c index 09d8c63f5680..bfd5447f6f41 100644 --- a/drivers/nvme/target/pr.c +++ b/drivers/nvme/target/pr.c @@ -589,7 +589,7 @@ static u16 nvmet_pr_preempt(struct nvmet_req *req, status = nvmet_pr_update_reg_attr(pr, holder, nvmet_pr_update_holder_rtype, &rtype); if (!status && original_rtype != rtype) - nvmet_pr_resv_released(pr, ®->hostid); + nvmet_pr_resv_released(pr, &ctrl->hostid); return status; } From d9e84d4e19994818b80846bd2abb7b396e4116a9 Mon Sep 17 00:00:00 2001 From: Daejun Park Date: Fri, 2 Oct 2026 16:03:41 +0900 Subject: [PATCH 17/28] nvmet: fix preempt of another registrant by the reservation holder Unless the reservation is of an All Registrants type, nvmet_pr_preempt() treats every Reservation Acquire command with the Preempt or Preempt and Abort action that is sent by the reservation holder as the holder preempting itself. It updates the reservation type to RTYPE and completes the command successfully without looking at PRKEY. NVMe Base Specification 2.1, section 8.1.22.7 makes the action depend on PRKEY for these reservation types, no matter which registrant sends the command: - PRKEY matches the key of the reservation holder: the reservation is preempted. This is how a holder preempts itself. - PRKEY does not match the key of the holder and is not 0h: the registrants whose key matches PRKEY are unregistered. - PRKEY does not match the key of the holder and is 0h: the command is aborted with Invalid Field in Command. With the current code, a holder that preempts with the key of another registrant leaves that registrant registered, changes the reservation type to RTYPE and still gets a successful completion. With hosts A, B and C registered with the keys 0xa, 0xb and 0xc, and A holding an Exclusive Access - Registrants Only reservation, A sends: # nvme resv-acquire /dev/nvme1 -n 1 --crkey=0xa --prkey=0xb \ --rtype=4 --racqa=1 NVME Reservation Acquire success # nvme resv-report /dev/nvme1 -n 1 --eds | grep '^regctl ' regctl : 3 and B can still write to the namespace. Preempt and Abort (--racqa=2) gives the same result. When C sends the preempt instead (--crkey=0xc), B is unregistered and its writes fail with Reservation Conflict. This defeats fencing in the pNFS SCSI layout server. nfsd holds the reservation and fences a client that does not return its layout by calling pr_preempt() with the key of that client and the abort flag, which is the command RFC 9561 section 2.2.3 asks for. On an nvmet namespace the preempt succeeds, nfsd logs the client as fenced, and the client keeps writing to the device. In a test with an nfsd server and two clients on an nvmet-tcp namespace, the namespace still had three registrants after the fence and the fenced client went on writing to it. With this patch two registrants are left and the writes of the fenced client fail with a reservation conflict. Take the self preempt path only when PRKEY is the key of the holder. Any other PRKEY sent by the holder is then handled by the code that already serves a non-holder registrant, for Preempt and for Preempt and Abort. For a host that holds the reservation this changes the following: - A preempt with the key of another registrant unregisters the registrants with that key, sends them the registration preempted notification and leaves the reservation type alone. - A preempt with PRKEY 0h fails with Invalid Field in Command. A holder that changed its reservation type this way has to pass its own key in PRKEY, which works with and without this patch. - A preempt with a PRKEY that matches no registrant fails with Reservation Conflict. The specification defines that status only for the All Registrants types. It is what nvmet already returns to a non-holder registrant in this case and what the SCSI target returns for an unregistered key. nfsd treats it as a successful fence. Not changed: when the holder preempts itself, registrants that share its key stay registered. Fixes: 5a47c2080a73 ("nvmet: support reservation feature") Cc: stable@vger.kernel.org Reviewed-by: Christoph Hellwig Signed-off-by: Daejun Park Signed-off-by: Keith Busch --- drivers/nvme/target/pr.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/nvme/target/pr.c b/drivers/nvme/target/pr.c index bfd5447f6f41..665cee8fc276 100644 --- a/drivers/nvme/target/pr.c +++ b/drivers/nvme/target/pr.c @@ -585,7 +585,7 @@ static u16 nvmet_pr_preempt(struct nvmet_req *req, &ctrl->hostid, abort); } - if (holder == reg) { + if (holder == reg && prkey == holder->rkey) { status = nvmet_pr_update_reg_attr(pr, holder, nvmet_pr_update_holder_rtype, &rtype); if (!status && original_rtype != rtype) From 53f6efe00cea0c0cf7bd99debe3cdac8567170b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Jean?= Date: Sat, 3 Oct 2026 21:01:55 +0000 Subject: [PATCH 18/28] nvmet-auth: serialize access to DH-HMAC-CHAP state to fix UAF MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A REPLY AUTH_SEND command can derive a dhchap_skey key while a concurrent AUTH_RECEIVE command observing this REPLY state executes the failure path and frees that key. The REPLY command then continues and writes into freed memory, causing a use-after-free. KASAN reports it as: BUG: KASAN: slab-use-after-free in __sha256_final Write of size 4 at addr ffff88800359c540 ... nvmet_auth_sq_free (drivers/nvme/target/auth.c:237) nvmet_execute_auth_receive (drivers/nvme/target/fabrics-cmd-auth.c:621) Add a mutex to each submission queue to serialize authentication commands and timeout handling. Unlock the mutex before completing requests and ignore outdated timeout callbacks. Limit timeout delays to prevent immediate expiry on 32-bit kernels. Fixes: 7a277c37d352 ("nvmet-auth: Diffie-Hellman key exchange support") Cc: stable@vger.kernel.org Assisted-by: LLM Reviewed-by: Christoph Hellwig Signed-off-by: Jérémy Jean Signed-off-by: Keith Busch --- drivers/nvme/target/auth.c | 1 + drivers/nvme/target/fabrics-cmd-auth.c | 32 ++++++++++++++++++++++---- drivers/nvme/target/nvmet.h | 2 ++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/drivers/nvme/target/auth.c b/drivers/nvme/target/auth.c index a55319bcdbd1..1680e5e6839c 100644 --- a/drivers/nvme/target/auth.c +++ b/drivers/nvme/target/auth.c @@ -229,6 +229,7 @@ u8 nvmet_setup_auth(struct nvmet_ctrl *ctrl, struct nvmet_sq *sq, bool reset) void nvmet_auth_sq_free(struct nvmet_sq *sq) { + sq->auth_expiry_active = false; cancel_delayed_work(&sq->auth_expired_work); kfree(sq->dhchap_c1); sq->dhchap_c1 = NULL; diff --git a/drivers/nvme/target/fabrics-cmd-auth.c b/drivers/nvme/target/fabrics-cmd-auth.c index 92f8a76f10ff..e94d8bb8a934 100644 --- a/drivers/nvme/target/fabrics-cmd-auth.c +++ b/drivers/nvme/target/fabrics-cmd-auth.c @@ -6,6 +6,7 @@ */ #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt #include +#include #include #include #include @@ -17,16 +18,27 @@ static void nvmet_auth_expired_work(struct work_struct *work) struct nvmet_sq *sq = container_of(to_delayed_work(work), struct nvmet_sq, auth_expired_work); + mutex_lock(&sq->auth_lock); + /* A command may have cancelled or rearmed this timeout. */ + if (!sq->auth_expiry_active || + delayed_work_pending(&sq->auth_expired_work)) + goto out_unlock; + sq->auth_expiry_active = false; + pr_debug("%s: ctrl %d qid %d transaction %u expired, resetting\n", __func__, sq->ctrl->cntlid, sq->qid, sq->dhchap_tid); sq->dhchap_step = NVME_AUTH_DHCHAP_MESSAGE_NEGOTIATE; sq->dhchap_tid = -1; +out_unlock: + mutex_unlock(&sq->auth_lock); } void nvmet_auth_sq_init(struct nvmet_sq *sq) { /* Initialize in-band authentication */ + mutex_init(&sq->auth_lock); INIT_DELAYED_WORK(&sq->auth_expired_work, nvmet_auth_expired_work); + sq->auth_expiry_active = false; sq->authenticated = false; sq->dhchap_step = NVME_AUTH_DHCHAP_MESSAGE_NEGOTIATE; } @@ -256,6 +268,8 @@ void nvmet_execute_auth_send(struct nvmet_req *req) u16 status = 0; u8 dhchap_status; + mutex_lock(&req->sq->auth_lock); + if (req->cmd->auth_send.secp != NVME_AUTH_DHCHAP_PROTOCOL_IDENTIFIER) { status = NVME_SC_INVALID_FIELD | NVME_STATUS_DNR; req->error_loc = @@ -281,8 +295,10 @@ void nvmet_execute_auth_send(struct nvmet_req *req) offsetof(struct nvmf_auth_send_command, tl); goto done; } - if (!nvmet_check_transfer_len(req, tl)) { + if (unlikely(tl != req->transfer_len)) { pr_debug("%s: transfer length mismatch (%u)\n", __func__, tl); + mutex_unlock(&req->sq->auth_lock); + nvmet_check_transfer_len(req, tl); return; } @@ -406,10 +422,12 @@ void nvmet_execute_auth_send(struct nvmet_req *req) status, req->error_loc); if (req->sq->dhchap_step != NVME_AUTH_DHCHAP_MESSAGE_SUCCESS2 && req->sq->dhchap_step != NVME_AUTH_DHCHAP_MESSAGE_FAILURE2) { - unsigned long auth_expire_secs = ctrl->kato ? ctrl->kato : 120; + unsigned long auth_expire = min_t(u64, MAX_JIFFY_OFFSET, + (u64)(ctrl->kato ? ctrl->kato : 120) * HZ); + req->sq->auth_expiry_active = true; mod_delayed_work(system_percpu_wq, &req->sq->auth_expired_work, - auth_expire_secs * HZ); + auth_expire); goto complete; } /* Final states, clear up variables */ @@ -418,6 +436,7 @@ void nvmet_execute_auth_send(struct nvmet_req *req) nvmet_ctrl_fatal_error(ctrl); complete: + mutex_unlock(&req->sq->auth_lock); nvmet_req_complete(req, status); } @@ -536,6 +555,8 @@ void nvmet_execute_auth_receive(struct nvmet_req *req) u32 al; u16 status = 0; + mutex_lock(&req->sq->auth_lock); + if (req->cmd->auth_receive.secp != NVME_AUTH_DHCHAP_PROTOCOL_IDENTIFIER) { status = NVME_SC_INVALID_FIELD | NVME_STATUS_DNR; req->error_loc = @@ -561,8 +582,10 @@ void nvmet_execute_auth_receive(struct nvmet_req *req) offsetof(struct nvmf_auth_receive_command, al); goto done; } - if (!nvmet_check_transfer_len(req, al)) { + if (unlikely(al != req->transfer_len)) { pr_debug("%s: transfer length mismatch (%u)\n", __func__, al); + mutex_unlock(&req->sq->auth_lock); + nvmet_check_transfer_len(req, al); return; } @@ -621,5 +644,6 @@ void nvmet_execute_auth_receive(struct nvmet_req *req) nvmet_auth_sq_free(req->sq); nvmet_ctrl_fatal_error(ctrl); } + mutex_unlock(&req->sq->auth_lock); nvmet_req_complete(req, status); } diff --git a/drivers/nvme/target/nvmet.h b/drivers/nvme/target/nvmet.h index a0bd05603271..e315f1c24e73 100644 --- a/drivers/nvme/target/nvmet.h +++ b/drivers/nvme/target/nvmet.h @@ -159,8 +159,10 @@ struct nvmet_sq { u32 sqhd; bool sqhd_disabled; #ifdef CONFIG_NVME_TARGET_AUTH + struct mutex auth_lock; bool authenticated; struct delayed_work auth_expired_work; + bool auth_expiry_active; u16 dhchap_tid; u8 sc_c; u8 dhchap_status; From f9e43d924bc74feeb56197de82b2845de2a4b85a Mon Sep 17 00:00:00 2001 From: Yehyeong Lee Date: Tue, 6 Oct 2026 22:48:05 +0900 Subject: [PATCH 19/28] nvme-auth: fail when the controller does not authenticate itself nvme_auth_process_dhchap_success1() returns success as soon as the controller clears the rvalid flag in its DH-HMAC-CHAP success1 message, without looking at whether the host had asked the controller to authenticate. A host configured with a controller secret sends a challenge in its reply, computes the expected controller response, and sets chap->bi_directional; if the controller then answers with rvalid = 0 the response is never compared, the host still sends success2, and the queue is marked as authenticated. A malicious or man-in-the-middle controller therefore only has to clear one flag to turn the configured bidirectional authentication into a one-way one, without holding any secret of its own. The target side already treats the mirror image of this as fatal: when the host supplied a challenge but no controller key is configured, nvmet_auth_success1() fails the authentication rather than answering with rvalid = 0. Refuse the connection if the controller returns no response although bidirectional authentication was requested. chap->bi_directional is the right condition rather than ctrl->ctrl_key, because secure concatenation sends a challenge only to derive the PSK and deliberately clears bi_directional; a one-way setup without a controller secret keeps returning success as before. Fixes: f50fff73d620 ("nvme: implement In-Band authentication") Cc: stable@vger.kernel.org Reviewed-by: Hannes Reinecke Reviewed-by: Christoph Hellwig Signed-off-by: Yehyeong Lee Signed-off-by: Keith Busch --- drivers/nvme/host/auth.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/drivers/nvme/host/auth.c b/drivers/nvme/host/auth.c index e55920642f2c..cbb38935fa22 100644 --- a/drivers/nvme/host/auth.c +++ b/drivers/nvme/host/auth.c @@ -358,8 +358,21 @@ static int nvme_auth_process_dhchap_success1(struct nvme_ctrl *ctrl, nvme_auth_hmac_name(chap->hash_id), nvme_auth_dhgroup_name(chap->dhgroup_id)); - if (!data->rvalid) + if (!data->rvalid) { + /* + * The controller did not return a response. If we asked it to + * authenticate itself the session must not be used, otherwise + * mutual authentication would silently degrade to one-way. + */ + if (chap->bi_directional) { + dev_warn(ctrl->device, + "qid %d: controller did not authenticate\n", + chap->qid); + chap->status = NVME_AUTH_DHCHAP_FAILURE_FAILED; + return -ECONNREFUSED; + } return 0; + } /* Validate controller response */ if (crypto_memneq(chap->response, data->rval, data->hl)) { From 9d820d55b3cb4859c7cf42a74dc78f6154f97398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Jean?= Date: Wed, 7 Oct 2026 19:27:46 +0000 Subject: [PATCH 20/28] nvmet: free the bvec on file namespace range errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nvmet_file_execute_rw() allocates the bvec before nvmet_file_execute_io() checks whether the request fits in the backing file. If the request extends beyond the file, nvmet_file_execute_io() calls nvmet_req_complete() to return an error. This skips nvmet_file_io_done(), so the allocated memory is never freed. Repeated requests can exhaust memory. Use nvmet_file_io_done() to free the bvec before returning the range error to the client. Fixes: 50a909db36f2 ("nvmet: use IOCB_NOWAIT for file-ns buffered I/O") Cc: stable@vger.kernel.org Assisted-by: LLM Reviewed-by: Christoph Hellwig Signed-off-by: Jérémy Jean Signed-off-by: Keith Busch --- drivers/nvme/target/io-cmd-file.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/nvme/target/io-cmd-file.c b/drivers/nvme/target/io-cmd-file.c index 0b22d183f927..695963dd932e 100644 --- a/drivers/nvme/target/io-cmd-file.c +++ b/drivers/nvme/target/io-cmd-file.c @@ -133,8 +133,8 @@ static bool nvmet_file_execute_io(struct nvmet_req *req, int ki_flags) pos = le64_to_cpu(req->cmd->rw.slba) << req->ns->blksize_shift; if (unlikely(pos + req->transfer_len > req->ns->size)) { - nvmet_req_complete(req, errno_to_nvme_status(req, -ENOSPC)); - return true; + ret = -ENOSPC; + goto complete; } memset(&req->f.iocb, 0, sizeof(struct kiocb)); From 44512543ff0413031516a02cde67323a55e0f769 Mon Sep 17 00:00:00 2001 From: Sung Byeongchan Date: Wed, 7 Oct 2026 15:30:41 +0900 Subject: [PATCH 21/28] nvmet: require authentication before property set I/O queues do not perform independent DH-HMAC-CHAP authentication. They instead rely on authentication of the controller's admin queue. Fabrics property commands, however, are dispatched before the general admin command authentication check. An unauthenticated peer can consequently set CC.EN, connect an I/O queue, and access a namespace after admin CONNECT reported that authentication was required. Reject PROPERTY_SET until the admin queue has authenticated. Targets without a configured authentication key are unchanged. This was found by source review with AI assistance. A localhost NVMe/TCP wire reproducer retrieved a known 4 KiB namespace block in two independent boots without completing AUTH_SEND or AUTH_RECEIVE. With this change, PROPERTY_SET returned NVME_SC_AUTH_REQUIRED and the namespace data was not returned; the no-authentication control continued to work. Fixes: c91a20129185 ("nvmet-auth: authenticate on admin queue only") Cc: stable@vger.kernel.org Assisted-by: LLM Reviewed-by: Christoph Hellwig Signed-off-by: Sung Byeongchan Signed-off-by: Keith Busch --- drivers/nvme/target/fabrics-cmd.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/drivers/nvme/target/fabrics-cmd.c b/drivers/nvme/target/fabrics-cmd.c index 42d1d1811671..b451029ab8f4 100644 --- a/drivers/nvme/target/fabrics-cmd.c +++ b/drivers/nvme/target/fabrics-cmd.c @@ -14,6 +14,10 @@ static void nvmet_execute_prop_set(struct nvmet_req *req) if (!nvmet_check_transfer_len(req, 0)) return; + if (!nvmet_check_auth_status(req)) { + status = NVME_SC_AUTH_REQUIRED | NVME_STATUS_DNR; + goto out; + } if (req->cmd->prop_set.attrib & 1) { req->error_loc = From 165301b5e2fd765fa05ea1044bbca8ba79820197 Mon Sep 17 00:00:00 2001 From: Palla Raghunath Date: Thu, 8 Oct 2026 20:18:48 +0100 Subject: [PATCH 22/28] nvmet-fc: don't drop the last assoc reference in nvmet_fc_delete_ctrl() syzbot hit a recursive locking report on subsys->lock when a subsystem is unlinked from an FC port. nvmet_port_del_ctrls() holds subsys->lock while it calls ->delete_ctrl() for each controller. nvmet_fc_delete_ctrl() grabs a reference on the association, queues del_work and then drops the reference. If del_work gets to run to completion before that put, ours is the last reference. Freeing the assoc tears down the queues, the last nvmet_cq_put() drops the last ctrl reference, and nvmet_ctrl_free() goes for subsys->lock again: WARNING: possible recursive locking detected ... nvmet_ctrl_free+0xa6/0x8e0 drivers/nvme/target/core.c:1766 nvmet_cq_put+0x158/0x200 drivers/nvme/target/core.c:848 nvmet_fc_target_assoc_free+0x418/0x2220 drivers/nvme/target/fc.c:1173 nvmet_fc_delete_ctrl+0x717/0x7e0 drivers/nvme/target/fc.c:1581 nvmet_port_del_ctrls+0xfb/0x150 drivers/nvme/target/core.c:311 nvmet_port_subsys_drop_link+0x1e8/0x310 drivers/nvme/target/configfs.c:1119 It's a real hang, not only a lockdep warning. I reproduced it with fcloop by putting an msleep() before the put: rm on the configfs link gets stuck in D state for good. I don't think we need that reference at all. del_work does list_del_rcu() and synchronize_rcu() before it drops the initial reference, so an assoc we find on assoc_list can't go away before rcu_read_unlock(). nvmet_fc_schedule_delete_assoc() doesn't sleep, so just call it inside the RCU section and get rid of the get/put. If the assoc is already being deleted, terminating is set and the call is a no-op, which matches what happened before when the get failed. With the same msleep() in place the patched kernel unlinks cleanly and the assoc is freed from the workqueue. Fixes: 4049dc96b8de ("nvmet-fc: defer cleanup using RCU properly") Reported-by: syzbot+da94a51633758d13bd63@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=da94a51633758d13bd63 Reviewed-by: Daniel Wagner Reviewed-by: Christoph Hellwig Signed-off-by: Palla Raghunath Signed-off-by: Keith Busch --- drivers/nvme/target/fc.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/drivers/nvme/target/fc.c b/drivers/nvme/target/fc.c index 1b557775e033..eecdffefa24f 100644 --- a/drivers/nvme/target/fc.c +++ b/drivers/nvme/target/fc.c @@ -1567,8 +1567,14 @@ nvmet_fc_delete_ctrl(struct nvmet_ctrl *ctrl) list_for_each_entry_rcu(assoc, &tgtport->assoc_list, a_list) { queue = assoc->queues[0]; if (queue && queue->nvme_sq.ctrl == ctrl) { - if (nvmet_fc_tgt_a_get(assoc)) - found_ctrl = true; + /* + * No assoc reference here: we're called with + * subsys->lock held, and if del_work beat us, + * our put would free the ctrl under that lock. + * RCU keeps the assoc alive until unlock. + */ + nvmet_fc_schedule_delete_assoc(assoc); + found_ctrl = true; break; } } @@ -1576,11 +1582,8 @@ nvmet_fc_delete_ctrl(struct nvmet_ctrl *ctrl) nvmet_fc_tgtport_put(tgtport); - if (found_ctrl) { - nvmet_fc_schedule_delete_assoc(assoc); - nvmet_fc_tgt_a_put(assoc); + if (found_ctrl) return; - } spin_lock_irqsave(&nvmet_fc_tgtlock, flags); } From c9359401036e8294c007a10e8c8d8da7030fcc76 Mon Sep 17 00:00:00 2001 From: Sung Byeongchan Date: Fri, 9 Oct 2026 03:38:10 +0900 Subject: [PATCH 23/28] nvmet-auth: reject undersized AUTH_SEND payloads nvmet_execute_auth_send() allocates and copies the initiator-supplied transfer length, then reads auth_type and auth_id through a common DHCHAP message pointer. The existing zero and transfer-length checks do not ensure that both common-header bytes are present. An unauthenticated NVMe/TCP initiator can complete ICReq and admin CONNECT, then send AUTH_SEND with tl equal to one. KASAN reports a one-byte slab out-of-bounds read when auth_id is loaded. Reject payloads that do not cover the common auth_type/auth_id header before allocating or parsing them. The one-byte trigger and header-minus-one boundary were cleanly rejected in three fixed fresh-boot runs. Exact-header, valid DHCHAP, wrong-type, mismatch, and zero-length controls remained clean, and a following valid AUTH_SEND on the same connection received a response. The touched source is unchanged at 0c2669a9f4a1d607e7591ae50ccf3c432a0aff08, where the patch applies and builds cleanly. The demonstrated impact is unauthenticated pre-authentication target-kernel denial of service. No disclosure, controlled write, RCE, or LPE was demonstrated. The source reproducer and complete logs are available on request and are not included in this public report. OpenAI Codex assisted source inspection, test design, patch drafting, and evidence organization. Fixes: db1312dd9548 ("nvmet: implement basic In-Band Authentication") Assisted-by: LLM Reviewed-by: Christoph Hellwig Signed-off-by: Sung Byeongchan Signed-off-by: Keith Busch --- drivers/nvme/target/fabrics-cmd-auth.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/nvme/target/fabrics-cmd-auth.c b/drivers/nvme/target/fabrics-cmd-auth.c index e94d8bb8a934..ff6b73cfe0f9 100644 --- a/drivers/nvme/target/fabrics-cmd-auth.c +++ b/drivers/nvme/target/fabrics-cmd-auth.c @@ -301,6 +301,12 @@ void nvmet_execute_auth_send(struct nvmet_req *req) nvmet_check_transfer_len(req, tl); return; } + if (tl < offsetof(struct nvmf_auth_dhchap_success2_data, rsvd1)) { + status = NVME_SC_INVALID_FIELD | NVME_STATUS_DNR; + req->error_loc = + offsetof(struct nvmf_auth_send_command, tl); + goto done; + } d = kmalloc(tl, GFP_KERNEL); if (!d) { From 6908a702955929b0876baac9fdc57070780a76f0 Mon Sep 17 00:00:00 2001 From: Yao Sang Date: Tue, 13 Oct 2026 15:53:24 +0800 Subject: [PATCH 24/28] nvme: pass partition flags to passthrough checks nvme_user_cmd() handles NVME_IOCTL_IO_CMD on namespace block devices, including partitions. It receives flags identifying a partition, but passes 0 to nvme_cmd_allowed(), bypassing the partition guard and allowing unprivileged passthrough commands to access sectors outside the partition. Pass flags to the permission check, rejecting unprivileged commands on partitions. Fixes: 313c08c72ee7 ("nvme: don't allow unprivileged passthrough on partitions") Reviewed-by: Christoph Hellwig Signed-off-by: Yao Sang Reviewed-by: Kanchan Joshi Signed-off-by: Keith Busch --- drivers/nvme/host/ioctl.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/nvme/host/ioctl.c b/drivers/nvme/host/ioctl.c index 6539d4750098..f2904c7fe0ab 100644 --- a/drivers/nvme/host/ioctl.c +++ b/drivers/nvme/host/ioctl.c @@ -343,7 +343,7 @@ static int nvme_user_cmd(struct nvme_ctrl *ctrl, struct nvme_ns *ns, c.common.cdw14 = cpu_to_le32(cmd.cdw14); c.common.cdw15 = cpu_to_le32(cmd.cdw15); - if (!nvme_cmd_allowed(ctrl, ns, &c, 0, open_for_write)) + if (!nvme_cmd_allowed(ctrl, ns, &c, flags, open_for_write)) return -EACCES; if (cmd.timeout_ms) From 3cf6de080d97d0d9641fbdb613d546c0734905dd Mon Sep 17 00:00:00 2001 From: Yehyeong Lee Date: Tue, 13 Oct 2026 19:28:12 +0900 Subject: [PATCH 25/28] nvme: bound FDP config log reads to the received buffer nvme_query_fdp_granularity() parses the FDP Configurations log using sizes the controller returns, without checking that they fit the buffer it was given. The log size is only bounded from above before kvmalloc(), so a size below the log header makes le16_to_cpu(h->numfdpc) read past the allocation, and a size of zero yields ZERO_SIZE_PTR and faults. Each configuration descriptor is then dereferenced (dsze while walking, and nrg and runs on the selected one) without checking that the whole descriptor lies within the buffer, so a descriptor at or past the end is read out of bounds. The out-of-bounds runs value is then exposed to userspace through /sys/block/nvmeXnY/queue/write_stream_granularity. A device-supplied dsze that is not a multiple of eight also leaves the following descriptors unaligned, and the structure is not packed, so reading nrg and runs from one can fault on architectures without efficient unaligned access. A malicious or buggy controller triggers this during namespace scan with a short FDP Configurations log: [ 1.104828] BUG: KASAN: slab-out-of-bounds in nvme_alloc_ns+0x39a6/0x4480 [ 1.104848] Read of size 8 at addr ffff888009fd8fe0 by task kworker/u8:3/52 [ 1.104852] [ 1.104857] CPU: 1 UID: 0 PID: 52 Comm: kworker/u8:3 Not tainted 7.3.0-rc6-00063-g0c2669a9f4a1 #1 PREEMPT(lazy) [ 1.104862] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS rel-1.17.0-0-gb52ca86e094d-prebuilt.qemu.org 04/01/2014 [ 1.104866] Workqueue: async async_run_entry_fn [ 1.104873] Call Trace: [ 1.104876] [ 1.104878] dump_stack_lvl+0x66/0xa0 [ 1.104886] print_report+0xd0/0x630 [ 1.104893] ? nvme_alloc_ns+0x39a6/0x4480 [ 1.104897] ? __virt_addr_valid+0x209/0x3f0 [ 1.104903] ? nvme_alloc_ns+0x39a6/0x4480 [ 1.104907] kasan_report+0xe4/0x120 [ 1.104913] ? nvme_alloc_ns+0x39a6/0x4480 [ 1.104919] nvme_alloc_ns+0x39a6/0x4480 [ 1.104926] ? __pfx_nvme_alloc_ns+0x10/0x10 [ 1.104931] ? check_prev_add+0xfd/0xe80 [ 1.104942] ? lock_acquire+0x18d/0x300 [ 1.104948] ? nvme_find_get_ns+0xbb/0x2e0 [ 1.104952] ? find_held_lock+0x2b/0x80 [ 1.104956] ? nvme_find_get_ns+0x221/0x2e0 [ 1.104962] ? nvme_find_get_ns+0x231/0x2e0 [ 1.104967] ? nvme_scan_ns+0x2e1/0x8d0 [ 1.104971] ? kfree+0x2e5/0x510 [ 1.104976] ? __lock_acquire+0x570/0x1b00 [ 1.104982] nvme_scan_ns+0x56a/0x8d0 [ 1.104987] ? __pfx_nvme_scan_ns+0x10/0x10 [ 1.104994] ? lockdep_hardirqs_on_prepare+0xdc/0x190 [ 1.104999] ? trace_hardirqs_on+0x18/0x160 [ 1.105005] ? kvm_clock_get_cycles+0x19/0x40 [ 1.105011] ? __pfx_nvme_scan_ns_async+0x10/0x10 [ 1.105015] async_run_entry_fn+0x8d/0x290 [ 1.105020] process_one_work+0x8ac/0x1b00 [ 1.105029] ? __pfx_process_one_work+0x10/0x10 [ 1.105034] ? lock_acquire+0x18d/0x300 [ 1.105040] ? lock_is_held_type+0x8f/0x100 [ 1.105046] ? __pfx_async_run_entry_fn+0x10/0x10 [ 1.105050] worker_thread+0x4ee/0xe70 [ 1.105056] ? lockdep_hardirqs_on_prepare+0xdc/0x190 [ 1.105061] ? trace_hardirqs_on+0x18/0x160 [ 1.105066] ? __pfx_worker_thread+0x10/0x10 [ 1.105071] ? __pfx_worker_thread+0x10/0x10 [ 1.105076] kthread+0x2ce/0x3b0 [ 1.105080] ? __pfx_kthread+0x10/0x10 [ 1.105085] ret_from_fork+0x52e/0x780 [ 1.105090] ? __pfx_ret_from_fork+0x10/0x10 [ 1.105094] ? __switch_to+0x572/0xde0 [ 1.105101] ? __pfx_kthread+0x10/0x10 [ 1.105105] ret_from_fork_asm+0x1a/0x30 [ 1.105114] [ 1.105116] [ 1.105117] Allocated by task 52: [ 1.105119] kasan_save_stack+0x33/0x60 [ 1.105123] kasan_save_track+0x14/0x30 [ 1.105126] __kasan_kmalloc+0x8f/0xa0 [ 1.105129] __kvmalloc_node_noprof+0x2e1/0x820 [ 1.105134] nvme_alloc_ns+0x1097/0x4480 [ 1.105138] nvme_scan_ns+0x56a/0x8d0 [ 1.105142] async_run_entry_fn+0x8d/0x290 [ 1.105145] process_one_work+0x8ac/0x1b00 [ 1.105148] worker_thread+0x4ee/0xe70 [ 1.105152] kthread+0x2ce/0x3b0 [ 1.105155] ret_from_fork+0x52e/0x780 [ 1.105159] ret_from_fork_asm+0x1a/0x30 [ 1.105163] [ 1.105164] The buggy address belongs to the object at ffff888009fd8fc0 [ 1.105164] which belongs to the cache kmalloc-32 of size 32 [ 1.105167] The buggy address is located 8 bytes to the right of [ 1.105167] allocated 24-byte region [ffff888009fd8fc0, ffff888009fd8fd8) [ 1.105171] [ 1.105172] The buggy address belongs to the physical page: [ 1.105175] page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x9fd8 [ 1.105179] flags: 0x100000000000000(node=0|zone=1) [ 1.105183] page_type: f5(slab) [ 1.105188] raw: 0100000000000000 ffff888008c41780 dead000000000100 dead000000000122 [ 1.105192] raw: 0000000000000000 0000000000400040 00000000f5000000 0000000000000000 [ 1.105194] page dumped because: kasan: bad access detected [ 1.105195] [ 1.105196] Memory state around the buggy address: [ 1.105198] ffff888009fd8e80: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc [ 1.105201] ffff888009fd8f00: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc [ 1.105203] >ffff888009fd8f80: fc fc fc fc fc fc fc fc 00 00 00 fc fc fc fc fc [ 1.105205] ^ [ 1.105207] ffff888009fd9000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 fc [ 1.105210] ffff888009fd9080: fc fc fc fc fc fc fc fc 00 00 00 00 00 00 00 00 [ 1.105212] ================================================================== Reject a log smaller than its header plus one configuration descriptor, zero the buffer so a short transfer leaves no uninitialized tail to parse, require each descriptor's declared size to be at least a descriptor and to leave room for the next one before walking it, and read the descriptor fields with the unaligned accessors. Fixes: 30b5f20bb2dda ("nvme: register fdp parameters with the block layer") Cc: stable@vger.kernel.org Suggested-by: Kanchan Joshi Reviewed-by: Christoph Hellwig Reviewed-by: Kanchan Joshi Signed-off-by: Yehyeong Lee Signed-off-by: Keith Busch --- drivers/nvme/host/core.c | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c index beea23d04a70..531f9bb14bab 100644 --- a/drivers/nvme/host/core.c +++ b/drivers/nvme/host/core.c @@ -2273,13 +2273,18 @@ static int nvme_query_fdp_granularity(struct nvme_ctrl *ctrl, } size = le32_to_cpu(hdr.sze); + if (size < sizeof(*h) + sizeof(*desc)) { + dev_warn(ctrl->device, "FDP config size too small:%zu\n", + size); + return 0; + } if (size > PAGE_SIZE * MAX_ORDER_NR_PAGES) { dev_warn(ctrl->device, "FDP config size too large:%zu\n", size); return 0; } - h = kvmalloc(size, GFP_KERNEL); + h = kvzalloc(size, GFP_KERNEL); if (!h) return -ENOMEM; @@ -2305,9 +2310,9 @@ static int nvme_query_fdp_granularity(struct nvme_ctrl *ctrl, desc = log; end = log + size - sizeof(*h); for (i = 0; i < fdp_idx; i++) { - u16 dsze = le16_to_cpu(desc->dsze); + u16 dsze = get_unaligned_le16(&desc->dsze); - if (!dsze || log + dsze > end) { + if (dsze < sizeof(*desc) || log + dsze + sizeof(*desc) > end) { dev_warn(ctrl->device, "FDP invalid config descriptor at index %d\n", i); ret = 0; @@ -2317,13 +2322,13 @@ static int nvme_query_fdp_granularity(struct nvme_ctrl *ctrl, desc = log; } - if (le32_to_cpu(desc->nrg) > 1) { + if (get_unaligned_le32(&desc->nrg) > 1) { dev_warn(ctrl->device, "FDP NRG > 1 not supported\n"); ret = 0; goto out; } - info->runs = le64_to_cpu(desc->runs); + info->runs = get_unaligned_le64(&desc->runs); out: kvfree(h); return ret; From 31b8a6af19b73e6167c4b655cd5e86681bac1c41 Mon Sep 17 00:00:00 2001 From: Andrea Parri Date: Mon, 12 Oct 2026 16:12:29 +0200 Subject: [PATCH 26/28] nvmet: zns: don't leak stale memory in zone management receive A Zone Management Receive report returns the full buffer the host asked for, but nvmet_bdev_zone_zmgmt_recv_work() writes only the 8-byte Number of Zones field and one 64-byte descriptor for each matching zone that fits. The 56 reserved header bytes and every descriptor slot past the last match keep whatever the buffer held before. A 4096-byte report covering a single zone leaves 4024 bytes unwritten. The tcp, rdma and fc transports allocate that buffer with sgl_alloc() and pci-epf with kmalloc(), none with __GFP_ZERO, and on success each sends the whole buffer to the host (nvmet_setup_c2h_data_pdu() for tcp), so the host receives old kernel memory from the target. nvmet_bdev_execute_zone_mgmt_recv() also never checks the transport data length against NUMD, so on transports where the host sets that length, the buffer sent back can be much larger than the report NUMD describes. pci-epf derives its data length from NUMD, so the length check below does not apply there, but the zeroing still covers the report. Reject a data length that does not match NUMD with nvmet_check_transfer_len(), as nvmet_bdev_execute_zone_append() does, and zero the report with nvmet_zero_sgl() before filling it. Zeroing NUMD bytes is only enough because of the length check, so the two go together. Allocating with __GFP_ZERO in the transports would also close the leak, but it adds a memset to every read command to fix one handler, so the transports are left alone. Tested over the loop, tcp, rdma and fc transports with a zoned null_blk namespace: before this patch, zone reports carried stale bytes and a data length larger than NUMD was accepted; with it, reports are fully initialized and such a length fails with Data SGL Length Invalid. Fixes: aaf2e048af27 ("nvmet: add ZBD over ZNS backend support") Cc: stable@vger.kernel.org Assisted-by: LLM Reviewed-by: Christoph Hellwig Signed-off-by: Andrea Parri Signed-off-by: Keith Busch --- drivers/nvme/target/zns.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/drivers/nvme/target/zns.c b/drivers/nvme/target/zns.c index 23a17c02abee..78d82b6ff121 100644 --- a/drivers/nvme/target/zns.c +++ b/drivers/nvme/target/zns.c @@ -282,6 +282,11 @@ static void nvmet_bdev_zone_zmgmt_recv_work(struct work_struct *w) if (status) goto out; + /* Only the zone count and matching descriptors are written below. */ + status = nvmet_zero_sgl(req, 0, out_bufsize); + if (status) + goto out; + if (!req_slba_nr_zones) { status = NVME_SC_SUCCESS; goto out; @@ -317,6 +322,11 @@ static void nvmet_bdev_zone_zmgmt_recv_work(struct work_struct *w) void nvmet_bdev_execute_zone_mgmt_recv(struct nvmet_req *req) { + u32 out_bufsize = (le32_to_cpu(req->cmd->zmr.numd) + 1) << 2; + + if (!nvmet_check_transfer_len(req, out_bufsize)) + return; + INIT_WORK(&req->z.zmgmt_work, nvmet_bdev_zone_zmgmt_recv_work); queue_work(zbd_wq, &req->z.zmgmt_work); } From bbca7c312ab25efd0024eb87545c51a76ab2ea39 Mon Sep 17 00:00:00 2001 From: Martin Belanger Date: Wed, 14 Oct 2026 08:02:57 -0400 Subject: [PATCH 27/28] nvme: make the DH-HMAC-CHAP secret attributes readable by root only The dhchap_secret and dhchap_ctrl_secret sysfs attributes have mode 0644. Their show functions return the secret, so any local user can read the DH-HMAC-CHAP secrets of a connected controller. Define them with DEVICE_ATTR_ADMIN_RW(), which sets mode 0600. The macro takes the show and store functions from the attribute name, so rename them. Writing the attributes was already limited to root and is not changed. Fixes: f50fff73d620 ("nvme: implement In-Band authentication") Cc: stable@vger.kernel.org Reviewed-by: Daniel Wagner Assisted-by: LLM Signed-off-by: Martin Belanger Signed-off-by: Keith Busch --- drivers/nvme/host/sysfs.c | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/drivers/nvme/host/sysfs.c b/drivers/nvme/host/sysfs.c index b3abb852a3ec..5224597a32f1 100644 --- a/drivers/nvme/host/sysfs.c +++ b/drivers/nvme/host/sysfs.c @@ -813,7 +813,7 @@ 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, +static ssize_t dhchap_secret_show(struct device *dev, struct device_attribute *attr, char *buf) { struct nvme_ctrl *ctrl = dev_get_drvdata(dev); @@ -824,7 +824,7 @@ static ssize_t nvme_ctrl_dhchap_secret_show(struct device *dev, return sysfs_emit(buf, "%s\n", opts->dhchap_secret); } -static ssize_t nvme_ctrl_dhchap_secret_store(struct device *dev, +static ssize_t dhchap_secret_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { struct nvme_ctrl *ctrl = dev_get_drvdata(dev); @@ -868,10 +868,9 @@ static ssize_t nvme_ctrl_dhchap_secret_store(struct device *dev, return count; } -static DEVICE_ATTR(dhchap_secret, S_IRUGO | S_IWUSR, - nvme_ctrl_dhchap_secret_show, nvme_ctrl_dhchap_secret_store); +static DEVICE_ATTR_ADMIN_RW(dhchap_secret); -static ssize_t nvme_ctrl_dhchap_ctrl_secret_show(struct device *dev, +static ssize_t dhchap_ctrl_secret_show(struct device *dev, struct device_attribute *attr, char *buf) { struct nvme_ctrl *ctrl = dev_get_drvdata(dev); @@ -882,7 +881,7 @@ static ssize_t nvme_ctrl_dhchap_ctrl_secret_show(struct device *dev, return sysfs_emit(buf, "%s\n", opts->dhchap_ctrl_secret); } -static ssize_t nvme_ctrl_dhchap_ctrl_secret_store(struct device *dev, +static ssize_t dhchap_ctrl_secret_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { struct nvme_ctrl *ctrl = dev_get_drvdata(dev); @@ -926,8 +925,7 @@ static ssize_t nvme_ctrl_dhchap_ctrl_secret_store(struct device *dev, return count; } -static DEVICE_ATTR(dhchap_ctrl_secret, S_IRUGO | S_IWUSR, - nvme_ctrl_dhchap_ctrl_secret_show, nvme_ctrl_dhchap_ctrl_secret_store); +static DEVICE_ATTR_ADMIN_RW(dhchap_ctrl_secret); #endif static struct attribute *nvme_dev_attrs[] = { From 2e8e30b3cf9f165e1a1d079331543cb7f2ad0e0c Mon Sep 17 00:00:00 2001 From: Christoph Hellwig Date: Tue, 6 Oct 2026 14:02:48 +0200 Subject: [PATCH 28/28] Revert "nvme-pci: clamp max_hw_sectors based on DMA optimized limitation" This turns down to cause up to 15% performance regressions on standard large transfer workloads. Revert it and hope the IOVA allocator can eventually be fixed to remove the need for the original workaround. This reverts commit 3710e2b056cb92ad816e4d79fa54a6a5b6ad8cbd. Signed-off-by: Christoph Hellwig Signed-off-by: Keith Busch --- 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 5440cf18b55b..82e308fe4f4c 100644 --- a/drivers/nvme/host/pci.c +++ b/drivers/nvme/host/pci.c @@ -3747,7 +3747,7 @@ static struct nvme_dev *nvme_pci_alloc_dev(struct pci_dev *pdev, */ dev->ctrl.max_hw_sectors = min_t(u32, NVME_MAX_BYTES >> SECTOR_SHIFT, - dma_opt_mapping_size(&pdev->dev) >> 9); + dma_max_mapping_size(&pdev->dev) >> 9); dev->ctrl.max_segments = NVME_MAX_SEGS; dev->ctrl.max_integrity_segments = 1; return dev;