From: Christoph Hellwig <hch@lst.de>
To: Keith Busch <kbusch@meta.com>
Cc: linux-nvme@lists.infradead.org, axboe@kernel.dk,
Chris Mason <mason@kernel.org>, Sagi Grimberg <sagi@grimberg.me>,
Chaitanya Kulkarni <kch@nvidia.com>,
Keith Busch <kbusch@kernel.org>,
Guixin Liu <kanie@linux.alibaba.com>
Subject: Re: [PATCH 09/19] nvmet: fix pc_ref use-after-free in preempt and abort
Date: Tue, 3 Nov 2026 07:04:14 +0100 [thread overview]
Message-ID: <20261103060414.GE1862@lst.de> (raw)
In-Reply-To: <20261029155252.2332266-10-kbusch@meta.com>
Adding Guixin for the PR code.
On Thu, Oct 29, 2026 at 08:52:42AM -0700, Keith Busch wrote:
> From: Chris Mason <mason@kernel.org>
>
> With reservations enabled on a namespace, the target keeps a pc_ref per
> controller, a percpu_ref counting that controller's I/O on the
> namespace. "Preempt and abort" kills the pc_refs of the preempted
> controllers and waits for the kill to be confirmed in
> nvmet_pr_set_ctrl_to_abort(), then waits for the count to drop to zero
> and resurrects them in nvmet_pr_do_abort(). Neither holds a reference
> to the pc_ref, only pr_sem. nvmet_ctrl_destroy_pr() frees the pc_refs
> under subsys->lock when the controller goes away, so a host that
> registers a key, disconnects, and sends preempt and abort with that key
> over a second connection frees a pc_ref that the abort is sleeping on:
>
> preempt and abort on C1 last put of C2
> nvmet_pr_set_ctrl_to_abort()
> percpu_ref_kill_and_confirm()
> wait_for_completion()
> nvmet_ctrl_destroy_pr()
> percpu_ref_exit()
> kfree(pc_ref)
> percpu_ref_switch_to_atomic_rcu() /* use after free */
>
> Put the pc_refs that an abort has killed on a per-namespace list.
> nvmet_ctrl_destroy_pr() only flags a pc_ref on that list, and
> nvmet_pr_do_abort() frees flagged ones once it's done with them. The
> list and the flag are written under the xarray lock.
>
> Fixes: 5a47c2080a73 ("nvmet: support reservation feature")
> Cc: Christoph Hellwig <hch@lst.de>
> Cc: Sagi Grimberg <sagi@grimberg.me>
> Cc: Chaitanya Kulkarni <kch@nvidia.com>
> Signed-off-by: Chris Mason <mason@kernel.org>
> Signed-off-by: Jens Axboe <axboe@kernel.dk>
> Signed-off-by: Keith Busch <kbusch@kernel.org>
> ---
> drivers/nvme/target/nvmet.h | 5 ++
> drivers/nvme/target/pr.c | 92 ++++++++++++++++++++++++++-----------
> 2 files changed, 69 insertions(+), 28 deletions(-)
>
> diff --git a/drivers/nvme/target/nvmet.h b/drivers/nvme/target/nvmet.h
> index d7804bf91aaa8..a89b384c4bbc9 100644
> --- a/drivers/nvme/target/nvmet.h
> +++ b/drivers/nvme/target/nvmet.h
> @@ -88,6 +88,8 @@ struct nvmet_pr {
> */
> struct semaphore pr_sem;
> struct list_head registrant_list;
> + /* The pc_refs that a running "preempt and abort" has killed. */
> + struct list_head abort_list;
> };
>
> struct nvmet_pr_per_ctrl_ref {
> @@ -95,6 +97,9 @@ struct nvmet_pr_per_ctrl_ref {
> struct completion free_done;
> struct completion confirm_done;
> uuid_t hostid;
> + /* Both are written under the lock of ns->pr_per_ctrl_refs. */
> + struct list_head abort_entry;
> + bool ctrl_gone;
> };
>
> struct nvmet_ns {
> diff --git a/drivers/nvme/target/pr.c b/drivers/nvme/target/pr.c
> index 14eaa99f98fa3..1c0d6cac1a6b3 100644
> --- a/drivers/nvme/target/pr.c
> +++ b/drivers/nvme/target/pr.c
> @@ -468,18 +468,54 @@ static void nvmet_pr_confirm_ns_pc_ref(struct percpu_ref *ref)
> complete(&pc_ref->confirm_done);
> }
>
> +static void nvmet_pr_free_pc_ref(struct nvmet_pr_per_ctrl_ref *pc_ref)
> +{
> + percpu_ref_exit(&pc_ref->ref);
> + kfree(pc_ref);
> +}
> +
> +/*
> + * The controller goes away. "Preempt and abort" sleeps on the pc_refs on its
> + * list, so leave the free of such a pc_ref to nvmet_pr_do_abort().
> + */
> +static void nvmet_pr_erase_pc_ref(struct nvmet_ns *ns, unsigned long cntlid)
> +{
> + struct nvmet_pr_per_ctrl_ref *pc_ref;
> +
> + xa_lock(&ns->pr_per_ctrl_refs);
> + pc_ref = __xa_erase(&ns->pr_per_ctrl_refs, cntlid);
> + if (pc_ref && !list_empty(&pc_ref->abort_entry)) {
> + pc_ref->ctrl_gone = true;
> + pc_ref = NULL;
> + }
> + xa_unlock(&ns->pr_per_ctrl_refs);
> +
> + if (pc_ref)
> + nvmet_pr_free_pc_ref(pc_ref);
> +}
> +
> static void nvmet_pr_set_ctrl_to_abort(struct nvmet_req *req, uuid_t *hostid)
> {
> struct nvmet_pr_per_ctrl_ref *pc_ref;
> struct nvmet_ns *ns = req->ns;
> unsigned long idx;
>
> + xa_lock(&ns->pr_per_ctrl_refs);
> xa_for_each(&ns->pr_per_ctrl_refs, idx, pc_ref) {
> - if (uuid_equal(&pc_ref->hostid, hostid)) {
> - percpu_ref_kill_and_confirm(&pc_ref->ref,
> - nvmet_pr_confirm_ns_pc_ref);
> - wait_for_completion(&pc_ref->confirm_done);
> - }
> + if (uuid_equal(&pc_ref->hostid, hostid) &&
> + list_empty(&pc_ref->abort_entry))
> + list_add_tail(&pc_ref->abort_entry,
> + &ns->pr.abort_list);
> + }
> + xa_unlock(&ns->pr_per_ctrl_refs);
> +
> + list_for_each_entry(pc_ref, &ns->pr.abort_list, abort_entry) {
> + /* Killed for another host that this command preempts. */
> + if (percpu_ref_is_dying(&pc_ref->ref))
> + continue;
> + percpu_ref_kill_and_confirm(&pc_ref->ref,
> + nvmet_pr_confirm_ns_pc_ref);
> + wait_for_completion(&pc_ref->confirm_done);
> }
> }
>
> @@ -618,20 +654,27 @@ static u16 nvmet_pr_preempt(struct nvmet_req *req,
> static void nvmet_pr_do_abort(struct work_struct *w)
> {
> struct nvmet_req *req = container_of(w, struct nvmet_req, r.abort_work);
> - struct nvmet_pr_per_ctrl_ref *pc_ref;
> + struct nvmet_pr_per_ctrl_ref *pc_ref, *tmp;
> struct nvmet_ns *ns = req->ns;
> - unsigned long idx;
> + bool ctrl_gone;
>
> /*
> * The target does not support abort, just wait per-controller ref to 0.
> */
> - xa_for_each(&ns->pr_per_ctrl_refs, idx, pc_ref) {
> - if (percpu_ref_is_dying(&pc_ref->ref)) {
> - wait_for_completion(&pc_ref->free_done);
> - reinit_completion(&pc_ref->confirm_done);
> - reinit_completion(&pc_ref->free_done);
> - percpu_ref_resurrect(&pc_ref->ref);
> - }
> + list_for_each_entry_safe(pc_ref, tmp, &ns->pr.abort_list, abort_entry) {
> + wait_for_completion(&pc_ref->free_done);
> + reinit_completion(&pc_ref->confirm_done);
> + reinit_completion(&pc_ref->free_done);
> + percpu_ref_resurrect(&pc_ref->ref);
> +
> + /* Off the list, the pc_ref belongs to its controller again. */
> + xa_lock(&ns->pr_per_ctrl_refs);
> + list_del_init(&pc_ref->abort_entry);
> + ctrl_gone = pc_ref->ctrl_gone;
> + xa_unlock(&ns->pr_per_ctrl_refs);
> +
> + if (ctrl_gone)
> + nvmet_pr_free_pc_ref(pc_ref);
> }
>
> up(&ns->pr.pr_sem);
> @@ -1052,6 +1095,8 @@ static int nvmet_pr_alloc_and_insert_pc_ref(struct nvmet_ns *ns,
> init_completion(&pc_ref->free_done);
> init_completion(&pc_ref->confirm_done);
> uuid_copy(&pc_ref->hostid, hostid);
> + INIT_LIST_HEAD(&pc_ref->abort_entry);
> + pc_ref->ctrl_gone = false;
>
> ret = xa_insert(&ns->pr_per_ctrl_refs, idx, pc_ref, GFP_KERNEL);
> if (ret)
> @@ -1067,7 +1112,6 @@ static int nvmet_pr_alloc_and_insert_pc_ref(struct nvmet_ns *ns,
> int nvmet_ctrl_init_pr(struct nvmet_ctrl *ctrl)
> {
> struct nvmet_subsys *subsys = ctrl->subsys;
> - struct nvmet_pr_per_ctrl_ref *pc_ref;
> struct nvmet_ns *ns = NULL;
> unsigned long idx;
> int ret;
> @@ -1095,19 +1139,14 @@ int nvmet_ctrl_init_pr(struct nvmet_ctrl *ctrl)
>
> free_per_ctrl_refs:
> nvmet_for_each_enabled_ns(&subsys->namespaces, idx, ns) {
> - if (ns->pr.enable) {
> - pc_ref = xa_erase(&ns->pr_per_ctrl_refs, ctrl->cntlid);
> - if (pc_ref)
> - percpu_ref_exit(&pc_ref->ref);
> - kfree(pc_ref);
> - }
> + if (ns->pr.enable)
> + nvmet_pr_erase_pc_ref(ns, ctrl->cntlid);
> }
> return ret;
> }
>
> void nvmet_ctrl_destroy_pr(struct nvmet_ctrl *ctrl)
> {
> - struct nvmet_pr_per_ctrl_ref *pc_ref;
> struct nvmet_ns *ns;
> unsigned long idx;
>
> @@ -1115,12 +1154,8 @@ void nvmet_ctrl_destroy_pr(struct nvmet_ctrl *ctrl)
> mutex_destroy(&ctrl->pr_log_mgr.lock);
>
> nvmet_for_each_enabled_ns(&ctrl->subsys->namespaces, idx, ns) {
> - if (ns->pr.enable) {
> - pc_ref = xa_erase(&ns->pr_per_ctrl_refs, ctrl->cntlid);
> - if (pc_ref)
> - percpu_ref_exit(&pc_ref->ref);
> - kfree(pc_ref);
> - }
> + if (ns->pr.enable)
> + nvmet_pr_erase_pc_ref(ns, ctrl->cntlid);
> }
> }
>
> @@ -1136,6 +1171,7 @@ int nvmet_pr_init_ns(struct nvmet_ns *ns)
> atomic_set(&ns->pr.generation, 0);
> sema_init(&ns->pr.pr_sem, 1);
> INIT_LIST_HEAD(&ns->pr.registrant_list);
> + INIT_LIST_HEAD(&ns->pr.abort_list);
> ns->pr.notify_mask = 0;
>
> xa_init(&ns->pr_per_ctrl_refs);
> --
> 2.52.0
---end quoted text---
next prev parent reply other threads:[~2026-11-03 6:04 UTC|newest]
Thread overview: 35+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-29 15:52 [PATCH 00/19] nvme patch pile Keith Busch
2026-10-29 15:52 ` [PATCH 01/19] nvme-loop: delete the controller if a reset fails Keith Busch
2026-11-03 5:50 ` Christoph Hellwig
2026-10-29 15:52 ` [PATCH 02/19] nvmet-tcp: ask for the TLS record type on every receive Keith Busch
2026-10-29 15:52 ` [PATCH 03/19] nvme-fabrics: reconnect if the association is gone Keith Busch
2026-11-03 5:58 ` Christoph Hellwig
2026-10-29 15:52 ` [PATCH 04/19] nvmet-auth: keep the old keys if the new ones fail Keith Busch
2026-11-03 5:59 ` Christoph Hellwig
2026-10-29 15:52 ` [PATCH 05/19] nvmet: accept I/O queues on the controller's port only Keith Busch
2026-11-03 6:00 ` Christoph Hellwig
2026-10-29 15:52 ` [PATCH 06/19] nvmet: check the host NQN against the TLS PSK identity Keith Busch
2026-11-03 6:01 ` Christoph Hellwig
2026-10-29 15:52 ` [PATCH 07/19] nvmet-tcp: let the accept work come to an end Keith Busch
2026-11-03 6:02 ` Christoph Hellwig
2026-10-29 15:52 ` [PATCH 08/19] nvme-tcp: start error recovery when a send fails Keith Busch
2026-10-29 16:17 ` Maurizio Lombardi
2026-10-29 15:52 ` [PATCH 09/19] nvmet: fix pc_ref use-after-free in preempt and abort Keith Busch
2026-11-03 6:04 ` Christoph Hellwig [this message]
2026-10-29 15:52 ` [PATCH 10/19] nvmet: move preempt and abort work off nvmet-wq Keith Busch
2026-10-29 15:52 ` [PATCH 11/19] nvmet-rdma: treat a failed connect as a disconnect Keith Busch
2026-11-03 6:06 ` Christoph Hellwig
2026-10-29 15:52 ` [PATCH 12/19] nvmet-rdma: disconnect only established queues Keith Busch
2026-11-03 6:06 ` Christoph Hellwig
2026-10-29 15:52 ` [PATCH 13/19] nvmet-rdma: take a device off the list last Keith Busch
2026-11-03 6:09 ` Christoph Hellwig
2026-10-29 15:52 ` [PATCH 14/19] nvmet-rdma: queue the release work under the mutex Keith Busch
2026-11-03 6:10 ` Christoph Hellwig
2026-10-29 15:52 ` [PATCH 15/19] nvmet-rdma: disconnect a queue on device removal Keith Busch
2026-11-03 6:12 ` Christoph Hellwig
2026-10-29 15:52 ` [PATCH 16/19] nvmet-rdma: reject queues larger than max_cqe Keith Busch
2026-11-03 6:14 ` Christoph Hellwig
2026-10-29 15:52 ` [PATCH 17/19] nvmet-rdma: don't run the wait list recursively Keith Busch
2026-10-29 15:52 ` [PATCH 18/19] nvmet-rdma: fix a lost run of the wait list Keith Busch
2026-10-29 15:52 ` [PATCH 19/19] nvmet: wait only for executing I/O in preempt and abort Keith Busch
2026-11-03 6:15 ` Christoph Hellwig
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261103060414.GE1862@lst.de \
--to=hch@lst.de \
--cc=axboe@kernel.dk \
--cc=kanie@linux.alibaba.com \
--cc=kbusch@kernel.org \
--cc=kbusch@meta.com \
--cc=kch@nvidia.com \
--cc=linux-nvme@lists.infradead.org \
--cc=mason@kernel.org \
--cc=sagi@grimberg.me \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox