[PATCH v2 2/2] nvmet: fix use-after-free in passthru I/O hotpath
Nilay Shroff
nilay at linux.ibm.com
Tue Sep 29 04:26:48 PDT 2026
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.
Signed-off-by: Nilay Shroff <nilay at linux.ibm.com>
---
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 b09681cb4a1f..fa1420065e30 100644
--- a/drivers/nvme/target/core.c
+++ b/drivers/nvme/target/core.c
@@ -819,6 +819,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)
@@ -1195,6 +1197,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 eb0f965b1a7e..04dc33004cb7 100644
--- a/drivers/nvme/target/nvmet.h
+++ b/drivers/nvme/target/nvmet.h
@@ -320,6 +320,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;
}
--
2.53.0
More information about the Linux-nvme
mailing list