Linux-NVME Archive on lore.kernel.org
 help / color / mirror / Atom feed
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---


  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