Linux RDMA and InfiniBand development
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Yehyeong Lee" <yhlee@isslab.korea.ac.kr>
Cc: linux-rdma@vger.kernel.org, target-devel@vger.kernel.org
Subject: Re: [PATCH v4 3/3] IB/isert: tear connections down from an ib_client on device removal
Date: Tue, 03 Nov 2026 03:56:48 +0000	[thread overview]
Message-ID: <sashiko-outbox-194551@kernel.org> (raw)
In-Reply-To: <20261103032924.1073138-4-yhlee@isslab.korea.ac.kr>

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Critical] IB/isert: race between isert_teardown_conn() and isert_connect_error()
- [High] IB/isert: QP destroyed after accept in isert_connect_request() on removal
- [High] IB/isert: new isert_device created after isert_remove_one() completes
- [High] IB/isert: race between isert_teardown_conn() and isert_accept_np()
--

commit b4ccbe89413aac4cc5fc070bd4b49d33beb67439
Author: Yehyeong Lee <yhlee@isslab.korea.ac.kr>

IB/isert: tear connections down from an ib_client on device removal

This commit registers an ib_client removal callback to tear down iSER
connections without holding the rdma_cm_id handler_mutex, waiting for the
last isert_device reference to be dropped and reference counting isert_np.

> diff --git a/drivers/infiniband/ulp/isert/ib_isert.c b/drivers/infiniband/ulp/isert/ib_isert.c
> index c6f417bb20690..5486fbdc6aa39 100644
> --- a/drivers/infiniband/ulp/isert/ib_isert.c
> +++ b/drivers/infiniband/ulp/isert/ib_isert.c

[ ... ]

> @@ -456,20 +466,42 @@ isert_connect_request(struct rdma_cm_id *cma_id, struct rdma_cm_event *event)
>  		goto out_rsp_dma_map;
>  	}
>  
> +	if (READ_ONCE(device->removing)) {
> +		isert_info("device %p is being removed, rejecting conn %p\n",
> +			   device, isert_conn);
> +		ret = -ENODEV;
> +		goto out_destroy_qp;
> +	}
> +
>  	ret = isert_login_post_recv(isert_conn);
>  	if (ret)
>  		goto out_destroy_qp;
>  
>  	ret = isert_rdma_accept(isert_conn);
>  	if (ret)
> -		goto out_destroy_qp;
> -
> +		goto out_drain_qp;
> +	accepted = true;
> +
> +	mutex_lock(&isert_conn_mutex);
> +	if (device->removing) {
> +		mutex_unlock(&isert_conn_mutex);
> +		isert_info("device %p is being removed, dropping conn %p\n",
> +			   device, isert_conn);
> +		ret = -ENODEV;
> +		goto out_drain_qp;
> +	}
> +	isert_conn->np = isert_np;
> +	kref_get(&isert_np->kref);
> +	list_add_tail(&isert_conn->conn_list, &isert_conn_list);
>  	mutex_lock(&isert_np->mutex);
>  	list_add_tail(&isert_conn->node, &isert_np->accepted);
>  	mutex_unlock(&isert_np->mutex);
> +	mutex_unlock(&isert_conn_mutex);
>  
>  	return 0;
>  
> +out_drain_qp:
> +	ib_drain_qp(isert_conn->qp);
>  out_destroy_qp:
>  	isert_destroy_qp(isert_conn);
>  out_rsp_dma_map:
> @@ -478,7 +510,9 @@ isert_connect_request(struct rdma_cm_id *cma_id, struct rdma_cm_event *event)
>  	isert_device_put(device);
>  out:
>  	kfree(isert_conn);
> -	rdma_reject(cma_id, NULL, 0, IB_CM_REJ_CONSUMER_DEFINED);
> +	/* cma destroys an accepted cm id when this handler returns an error. */
> +	if (!accepted)
> +		rdma_reject(cma_id, NULL, 0, IB_CM_REJ_CONSUMER_DEFINED);
>  	return ret;
>  }

[Severity: High]
As reported by Sashiko on v3 [1], if device->removing becomes true between
the unlocked READ_ONCE(device->removing) check and mutex_lock(&isert_conn_mutex)
in isert_connect_request(), isert_rdma_accept() has already succeeded, yet
the error path jumps to out_drain_qp -> out_destroy_qp and calls
ib_destroy_qp(isert_conn->qp) before returning -ENODEV to the RDMA CM
CONNECT_REQUEST handler.

On iWARP transports, cma_accept_iw() -> iw_cm_accept() sets
IWCM_F_CONNECT_WAIT, stores cm_id_priv->qp = qp, and takes a provider QP
reference via iw_add_ref(qp) that is not released until iw_destroy_cm_id() or
cm_close_handler() runs on iwcm_wq after the CONNECT_REQUEST callback
returns. Won't calling ib_destroy_qp() here before returning from
isert_connect_request() deadlock in iWARP provider destroy_qp callbacks that
wait for the QP reference count to drop to zero (such as siw_destroy_qp() or
_irdma_destroy_qp()), or deadlock destroy_cm_id() on iwcm_wq waiting for
IWCM_F_CONNECT_WAIT to clear after iw_conn_req_handler() returns an error?

Also on IB/RoCE, doesn't destroying isert_conn->qp directly with
ib_destroy_qp() after cma_accept_ib() has already sent the CM REP leave
cma_id->qp pointing to a freed QP while the CM ID is still being torn down?

[ ... ]

> @@ -628,17 +695,17 @@ isert_np_cma_handler(struct isert_np *isert_np,
>  	return -1;
>  }
>  
> -static int
> -isert_disconnected_handler(struct rdma_cm_id *cma_id,
> -			   enum rdma_cm_event_type event)
> +static void
> +isert_teardown_conn(struct isert_conn *isert_conn)
>  {
> -	struct isert_conn *isert_conn = cma_id->qp->qp_context;
>  	bool terminated = false;
>  
>  	mutex_lock(&isert_conn->mutex);
>  	switch (isert_conn->state) {
>  	case ISER_CONN_TERMINATING:
> +	case ISER_CONN_DOWN:
>  		break;
> +	case ISER_CONN_INIT:
>  	case ISER_CONN_UP:
>  		terminated = isert_conn_terminate(isert_conn);
>  		break;
> @@ -652,10 +719,20 @@ isert_disconnected_handler(struct rdma_cm_id *cma_id,
>  	}
>  	mutex_unlock(&isert_conn->mutex);
>  
> -	if (terminated) {
> -		ib_drain_qp(isert_conn->qp);
> -		isert_handle_unbound_conn(isert_conn);
> -	}
> +	if (!terminated)
> +		return;
> +
> +	ib_drain_qp(isert_conn->qp);
> +	isert_handle_unbound_conn(isert_conn);
> +}

[Severity: Critical]
Can isert_teardown_conn() called from isert_remove_one() race with
isert_connect_error()?

Previously, connection teardown and connection error handling both ran under
the cm_id handler_mutex, and ISER_CONN_INIT was not handled in
isert_disconnected_handler(). With this change, isert_remove_one() takes a
reference on isert_conn via kref_get_unless_zero(&isert_conn->kref) and calls
isert_teardown_conn() outside the cm_id handler_mutex, while
isert_connect_error() does not take isert_conn->mutex or update
isert_conn->state:

drivers/infiniband/ulp/isert/ib_isert.c:isert_connect_error() {
	mutex_lock(&isert_np->mutex);
	owned = !list_empty(&isert_conn->node);
	list_del_init(&isert_conn->node);
	mutex_unlock(&isert_np->mutex);

	if (!owned) {
		isert_info("conn %p is being released already\n", isert_conn);
		return 0;
	}

	ib_drain_qp(isert_conn->qp);
	isert_conn->cm_id = NULL;
	isert_put_conn(isert_conn);

	return -1;
}

If isert_connect_error() runs after isert_remove_one() grabs a kref on a
connection in ISER_CONN_INIT and before isert_remove_one() calls
isert_teardown_conn(), isert_connect_error() sets isert_conn->cm_id = NULL,
drops only the initial kref, and returns -1 (causing RDMA CM to destroy
cma_id), leaving isert_conn->state as ISER_CONN_INIT. When
isert_remove_one() then calls isert_teardown_conn() ->
isert_conn_terminate(), won't rdma_disconnect(isert_conn->cm_id) dereference
a NULL (or freed) cm_id?

Conversely, if isert_teardown_conn() runs isert_conn_terminate() first, it
sets isert_conn->state = ISER_CONN_TERMINATING and drops isert_conn->mutex to
call ib_drain_qp(isert_conn->qp) before isert_handle_unbound_conn() unlinks
isert_conn->node. While ib_drain_qp() is sleeping, can't a concurrent
isert_connect_error() see !list_empty(&isert_conn->node), unlink it, call
ib_drain_qp(isert_conn->qp) concurrently on the same QP, clear
isert_conn->cm_id = NULL, and free cma_id?

Also, if RDMA_CM_EVENT_UNREACHABLE triggers isert_connect_error() after
isert_connected_handler() has promoted the connection to ISER_CONN_UP (with
isert_conn->promoted == true and an extra kref), isert_connect_error()
unlinks isert_conn->node without dropping the promoted reference. Won't that
leak isert_conn and hang wait_for_completion(&removal_comp) in
isert_remove_one()?

[ ... ]

> @@ -2480,6 +2546,7 @@ isert_accept_np(struct iscsi_np *np, struct iscsit_conn *conn)
>  	isert_conn = list_first_entry(&isert_np->pending,
>  			struct isert_conn, node);
>  	list_del_init(&isert_conn->node);
> +	isert_conn->promoted = false;
>  	mutex_unlock(&isert_np->mutex);
>  
>  	conn->context = isert_conn;

[Severity: High]
What happens if isert_teardown_conn() races with isert_accept_np() on a
connection in ISER_CONN_UP?

In isert_teardown_conn(), when isert_conn->state is ISER_CONN_UP, it sets
isert_conn->state = ISER_CONN_TERMINATING under isert_conn->mutex, unlocks
isert_conn->mutex, and calls ib_drain_qp(isert_conn->qp) before calling
isert_handle_unbound_conn() to unlink isert_conn->node from
isert_np->pending.

During that ib_drain_qp() window, isert_accept_np() only acquires
isert_np->mutex (without acquiring isert_conn->mutex or checking
isert_conn->state):

	mutex_lock(&isert_np->mutex);
	if (list_empty(&isert_np->pending)) {
		mutex_unlock(&isert_np->mutex);
		goto accept_wait;
	}
	isert_conn = list_first_entry(&isert_np->pending,
			struct isert_conn, node);
	list_del_init(&isert_conn->node);
	isert_conn->promoted = false;
	mutex_unlock(&isert_np->mutex);

	conn->context = isert_conn;
	isert_conn->conn = conn;
	isert_conn->state = ISER_CONN_BOUND;

If isert_accept_np() pops the connection from isert_np->pending while
ib_drain_qp() is in progress, it overwrites isert_conn->state from
ISER_CONN_TERMINATING to ISER_CONN_BOUND. Then isert_handle_unbound_conn()
sees list_empty(&isert_conn->node) and does nothing, while
isert_teardown_conn() never called iscsit_cause_connection_reinstatement()
because the state was ISER_CONN_UP when it checked. If the login request PDU
already arrived before the QP drained and finishes a single-stage login, the
login timer is stopped and iscsi_target_rx_thread() blocks in
isert_get_rx_pdu() -> wait_for_completion_interruptible(&comp) on the
already-drained QP. Doesn't this leave the connection stuck and hang
isert_remove_one() at wait_for_completion(&removal_comp)?

[ ... ]

> @@ -2717,8 +2783,61 @@ static struct iscsit_transport iser_target_transport = {
>  	.iscsit_get_sup_prot_ops = isert_get_sup_prot_ops,
>  };
>  
> +static void
> +isert_remove_one(struct ib_device *ib_device, void *client_data)
> +{
> +	DECLARE_COMPLETION_ONSTACK(removal_comp);
> +	struct isert_device *device, *found = NULL;
> +	struct isert_conn *isert_conn, *tmp;
> +	LIST_HEAD(removal_list);
> +
> +	mutex_lock(&device_list_mutex);
> +	list_for_each_entry(device, &device_list, dev_node) {
> +		if (device->ib_device != ib_device)
> +			continue;
> +		device->refcount++;
> +		device->removal_comp = &removal_comp;
> +		found = device;
> +		break;
> +	}
> +	mutex_unlock(&device_list_mutex);
> +
> +	if (!found)
> +		return;
> +
> +	mutex_lock(&isert_conn_mutex);
> +	WRITE_ONCE(found->removing, true);
> +	list_for_each_entry(isert_conn, &isert_conn_list, conn_list) {
> +		if (isert_conn->device != found)
> +			continue;
> +		/* A connection with no reference left is already releasing. */
> +		if (!kref_get_unless_zero(&isert_conn->kref))
> +			continue;
> +		list_add_tail(&isert_conn->removal_node, &removal_list);
> +	}
> +	mutex_unlock(&isert_conn_mutex);
> +
> +	list_for_each_entry_safe(isert_conn, tmp, &removal_list, removal_node) {
> +		list_del_init(&isert_conn->removal_node);
> +		isert_info("removing conn %p state %d\n",
> +			   isert_conn, isert_conn->state);
> +		isert_teardown_conn(isert_conn);
> +		isert_put_conn(isert_conn);
> +	}
> +
> +	isert_device_put(found);
> +	wait_for_completion(&removal_comp);
> +}

[Severity: High]
As reported by Sashiko on v2 [2], because isert_ib_client is registered after
cma_client, disable_device() invokes isert_remove_one() before
cma_remove_one() destroys listening RDMA CM IDs.

In ib_isert.c, struct isert_device is only present on device_list while its
refcount is non-zero: if there are no active connections when
isert_remove_one() runs (or once existing connections drop the refcount to 0
in isert_device_put() and complete removal_comp), the isert_device is removed
from device_list and freed, and isert_remove_one() returns.

If an incoming RDMA_CM_EVENT_CONNECT_REQUEST arrives on a listener before
cma_remove_one() runs, won't isert_device_get() find no matching entry in
device_list, allocate a fresh struct isert_device with removing == false, and
accept the new connection after isert_remove_one() has already completed?
When cma_remove_one() then delivers RDMA_CM_EVENT_DEVICE_REMOVAL to that
connection, isert_disconnected_handler() queues release_work asynchronously,
and isert_connect_release() calls rdma_destroy_id() (which unblocks
cma_remove_one()) before destroying the QP, returning the shared CQ to the
pool, unmapping DMA buffers, and deallocating the PD.

[1] https://lore.kernel.org/r/20261102061608.617186-3-yhlee@isslab.korea.ac.kr
[2] https://lore.kernel.org/r/sashiko-outbox-184744@kernel.org

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261103032924.1073138-1-yhlee@isslab.korea.ac.kr?part=3

  reply	other threads:[~2026-11-03  3:56 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-03  3:29 [PATCH v4 0/3] IB/isert: fix the device removal use-after-free Yehyeong Lee
2026-11-03  3:29 ` [PATCH v4 1/3] IB/isert: do not publish connections being released on a second list Yehyeong Lee
2026-11-03  3:53   ` sashiko-bot
2026-11-03  3:29 ` [PATCH v4 2/3] IB/isert: drain the QP without holding isert_conn->mutex Yehyeong Lee
2026-11-03  3:52   ` sashiko-bot
2026-11-03  3:29 ` [PATCH v4 3/3] IB/isert: tear connections down from an ib_client on device removal Yehyeong Lee
2026-11-03  3:56   ` sashiko-bot [this message]
2026-11-03 13:04 ` [PATCH v4 0/3] IB/isert: fix the device removal use-after-free Yehyeong Lee

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=sashiko-outbox-194551@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=linux-rdma@vger.kernel.org \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=target-devel@vger.kernel.org \
    --cc=yhlee@isslab.korea.ac.kr \
    /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