[RFC PATCH] RDMA/iwcm: allow aborting an active connect awaiting CONNECT_REPLY

Yunseong Kim <[email protected]>
Newsgroups org.kernel.vger.linux-cifs,org.kernel.vger.linux-kernel,org.kernel.vger.linux-rdma
Message-ID <[email protected]>
After a successful connect downcall, iw_cm_connect() returns with
IWCM_F_CONNECT_WAIT still set and the cm_id in IW_CM_STATE_CONN_SENT.
The only thing that clears the flag is the provider delivering
IW_CM_EVENT_CONNECT_REPLY (cm_conn_rep_handler()).  Until that event
arrives, iw_cm_disconnect() and destroy_cm_id() sleep uninterruptibly
in

	wait_event(cm_id_priv->connect_wait,
		   !test_bit(IWCM_F_CONNECT_WAIT, &cm_id_priv->flags));

and both state machines treat IW_CM_STATE_CONN_SENT as BUG(), so the
API has no way to cancel a pending active connect.  If the provider
never generates the reply, because the peer died in the middle of
connection setup or because of a provider bug, every teardown path
(rdma_disconnect(), rdma_destroy_id()) blocks in D state forever and
the ULP cannot recover: there is no way to disconnect after
rdma_connect() was called without risking an unbounded hang.

This class of problem is not theoretical.  The pending smbdirect
change "smb: smbdirect: bound the disconnect wait in destroy_sync" [1]
had to work around it on the ULP side:
smbdirect_socket_destroy_sync() waited unbounded for the socket to
reach SMBDIRECT_SOCKET_DISCONNECTED, a transition that depends on an
asynchronous RDMA CM disconnect event, and when the peer died abruptly
(a killed client, or Soft-RoCE/RXE where no graceful disconnect
completes) that event never arrived.  The destroy ran on the single
ksmbd-conn-release workqueue, every later connection release queued
behind it in D state, and the whole server wedged until hung_task
fired.  That change bounded the wait and drove the socket state
machine to DISCONNECTED locally on timeout; ULPs should not have to
resort to that, the CM should offer a teardown they can rely on.

IWCM_F_CONNECT_WAIT currently guards two different windows:

 * the connect/accept downcall into the provider being in progress;
   teardown must keep waiting for that, it is short and bounded;

 * an issued active connect waiting for CONNECT_REPLY, which is
   potentially unbounded.

Mark the second window with a new flag, IWCM_F_CONNECT_SENT: set by
iw_cm_connect() once the downcall has returned successfully, cleared
by cm_conn_rep_handler().  The teardown waits now complete when either
the downcall has finished (!IWCM_F_CONNECT_WAIT, as before) or the
pending-reply window has been entered (IWCM_F_CONNECT_SENT), and the
previously BUG() CONN_SENT cases become:

 * iw_cm_disconnect(): return -ENOTCONN; there is no established
   connection to disconnect, aborting is the destroy path's job;

 * destroy_cm_id(): abort the pending connect locally by moving to
   DESTROYING and putting the QP into error so the provider tears the
   connection attempt down.

A CONNECT_REPLY that arrives after the abort is dropped: either
cm_work_handler() sees IWCM_F_DROP_EVENTS, or cm_conn_rep_handler()
now recognizes IW_CM_STATE_DESTROYING (the abort and the reply
serialize on cm_id_priv->lock) and frees the event without touching
the QP that destroy_cm_id() already released.  The cm_id memory stays
valid for such a late reply because the provider holds its own
reference (cm_id->add_ref) for as long as it can deliver events.

The passive side has a sibling gap, where after a successful accept
downcall the flag stays set until the provider's ESTABLISHED event
arrives, which this patch deliberately does not change.

[1] https://github.com/smfrench/smb3-kernel/commit/26d0f82a02c8a9c9c8cdfc138acb7ed0bf8e01a9

Suggested-by: Stefan Metzmacher <[email protected]>
Signed-off-by: Yunseong Kim <[email protected]>
---
 drivers/infiniband/core/iwcm.c | 76 +++++++++++++++++++++++++++++-----
 drivers/infiniband/core/iwcm.h |  1 +
 2 files changed, 67 insertions(+), 10 deletions(-)

diff --git a/drivers/infiniband/core/iwcm.c b/drivers/infiniband/core/iwcm.c
index 0b7246ec559e..4fb36ac92cc5 100644
--- a/drivers/infiniband/core/iwcm.c
+++ b/drivers/infiniband/core/iwcm.c
@@ -308,9 +308,18 @@ int iw_cm_disconnect(struct iw_cm_id *cm_id, int abrupt)
 	struct ib_qp *qp = NULL;
 
 	cm_id_priv = container_of(cm_id, struct iwcm_id_private, id);
-	/* Wait if we're currently in a connect or accept downcall */
+	/*
+	 * Wait if we're currently in a connect or accept downcall.  A
+	 * pending active connect whose downcall already returned
+	 * (IWCM_F_CONNECT_SENT) is not waited for: the CONNECT_REPLY that
+	 * would end such a wait comes from the provider and may never
+	 * arrive if the peer died during connection setup, so the
+	 * CONN_SENT state is handled below instead of sleeping without
+	 * bound here.
+	 */
 	wait_event(cm_id_priv->connect_wait,
-		   !test_bit(IWCM_F_CONNECT_WAIT, &cm_id_priv->flags));
+		   !test_bit(IWCM_F_CONNECT_WAIT, &cm_id_priv->flags) ||
+		   test_bit(IWCM_F_CONNECT_SENT, &cm_id_priv->flags));
 
 	spin_lock_irqsave(&cm_id_priv->lock, flags);
 	switch (cm_id_priv->state) {
@@ -338,7 +347,14 @@ int iw_cm_disconnect(struct iw_cm_id *cm_id, int abrupt)
 		 */
 		break;
 	case IW_CM_STATE_CONN_SENT:
-		/* Can only get here if wait above fails */
+		/*
+		 * Active connect still waiting for the provider's
+		 * CONNECT_REPLY: there is no established connection to
+		 * disconnect.  Tell the caller; aborting the pending
+		 * connect is iw_destroy_cm_id()'s job.
+		 */
+		ret = -ENOTCONN;
+		break;
 	default:
 		BUG();
 	}
@@ -375,10 +391,15 @@ static void destroy_cm_id(struct iw_cm_id *cm_id)
 	cm_id_priv = container_of(cm_id, struct iwcm_id_private, id);
 	/*
 	 * Wait if we're currently in a connect or accept downcall. A
-	 * listening endpoint should never block here.
+	 * listening endpoint should never block here.  A pending active
+	 * connect whose downcall already returned (IWCM_F_CONNECT_SENT)
+	 * is not waited for, since its CONNECT_REPLY may never arrive if
+	 * the peer died during connection setup; it is aborted locally
+	 * in the CONN_SENT case below.
 	 */
 	wait_event(cm_id_priv->connect_wait,
-		   !test_bit(IWCM_F_CONNECT_WAIT, &cm_id_priv->flags));
+		   !test_bit(IWCM_F_CONNECT_WAIT, &cm_id_priv->flags) ||
+		   test_bit(IWCM_F_CONNECT_SENT, &cm_id_priv->flags));
 
 	/*
 	 * Since we're deleting the cm_id, drop any events that
@@ -422,6 +443,21 @@ static void destroy_cm_id(struct iw_cm_id *cm_id)
 		spin_lock_irqsave(&cm_id_priv->lock, flags);
 		break;
 	case IW_CM_STATE_CONN_SENT:
+		/*
+		 * Abort a pending active connect: the connect downcall has
+		 * returned (IWCM_F_CONNECT_SENT) but the provider has not
+		 * delivered CONNECT_REPLY.  Move the QP to error so the
+		 * provider tears the connection attempt down.  A late
+		 * CONNECT_REPLY is dropped via IWCM_F_DROP_EVENTS or the
+		 * DESTROYING check in cm_conn_rep_handler(), and the
+		 * provider's own reference (cm_id->add_ref) keeps this
+		 * cm_id alive until that reply has been delivered.
+		 */
+		cm_id_priv->state = IW_CM_STATE_DESTROYING;
+		spin_unlock_irqrestore(&cm_id_priv->lock, flags);
+		(void)iwcm_modify_qp_err(qp);
+		spin_lock_irqsave(&cm_id_priv->lock, flags);
+		break;
 	case IW_CM_STATE_DESTROYING:
 	default:
 		BUG();
@@ -689,9 +725,11 @@ EXPORT_SYMBOL(iw_cm_accept);
 /*
  * Active Side: CM_ID <-- CONN_SENT
  *
- * If successful, results in the generation of a CONNECT_REPLY
- * event. iw_cm_disconnect and iw_cm_destroy will block until the
- * CONNECT_REPLY event is received from the provider.
+ * If successful, results in the generation of a CONNECT_REPLY event.
+ * IWCM_F_CONNECT_SENT marks the window between the connect downcall
+ * returning and that CONNECT_REPLY arriving; during it
+ * iw_cm_disconnect() returns -ENOTCONN and iw_destroy_cm_id() aborts
+ * the pending connect locally instead of blocking on the provider.
  */
 int iw_cm_connect(struct iw_cm_id *cm_id, struct iw_cm_conn_param *iw_param)
 {
@@ -728,8 +766,16 @@ int iw_cm_connect(struct iw_cm_id *cm_id, struct iw_cm_conn_param *iw_param)
 	ret = iw_cm_map(cm_id, true);
 	if (!ret)
 		ret = cm_id->device->ops.iw_connect(cm_id, iw_param);
-	if (!ret)
+	if (!ret) {
+		/*
+		 * The downcall is done; only the provider's CONNECT_REPLY
+		 * is outstanding.  Let teardown waiters proceed so they
+		 * can abort instead of depending on that reply.
+		 */
+		set_bit(IWCM_F_CONNECT_SENT, &cm_id_priv->flags);
+		wake_up_all(&cm_id_priv->connect_wait);
 		return 0;	/* success */
+	}
 
 	spin_lock_irqsave(&cm_id_priv->lock, flags);
 	qp = cm_id_priv->qp;
@@ -882,7 +928,7 @@ static int cm_conn_rep_handler(struct iwcm_id_private *cm_id_priv,
 {
 	struct ib_qp *qp = NULL;
 	unsigned long flags;
-	int ret;
+	int ret = 0;
 
 	spin_lock_irqsave(&cm_id_priv->lock, flags);
 	/*
@@ -890,6 +936,15 @@ static int cm_conn_rep_handler(struct iwcm_id_private *cm_id_priv,
 	 * iw_cm_disconnect will not wait and deadlock this thread
 	 */
 	clear_bit(IWCM_F_CONNECT_WAIT, &cm_id_priv->flags);
+	clear_bit(IWCM_F_CONNECT_SENT, &cm_id_priv->flags);
+	if (cm_id_priv->state == IW_CM_STATE_DESTROYING) {
+		/*
+		 * destroy_cm_id() aborted the pending connect and already
+		 * released the QP; drop the late reply.
+		 */
+		spin_unlock_irqrestore(&cm_id_priv->lock, flags);
+		goto out;
+	}
 	BUG_ON(cm_id_priv->state != IW_CM_STATE_CONN_SENT);
 	if (iw_event->status == 0) {
 		cm_id_priv->id.m_local_addr = iw_event->local_addr;
@@ -908,6 +963,7 @@ static int cm_conn_rep_handler(struct iwcm_id_private *cm_id_priv,
 		cm_id_priv->id.device->ops.iw_rem_ref(qp);
 	ret = cm_id_priv->id.cm_handler(&cm_id_priv->id, iw_event);
 
+out:
 	if (iw_event->private_data_len)
 		kfree(iw_event->private_data);
 
diff --git a/drivers/infiniband/core/iwcm.h b/drivers/infiniband/core/iwcm.h
index b56fb12edece..74ce33616863 100644
--- a/drivers/infiniband/core/iwcm.h
+++ b/drivers/infiniband/core/iwcm.h
@@ -57,5 +57,6 @@ struct iwcm_id_private {
 
 #define IWCM_F_DROP_EVENTS	  1
 #define IWCM_F_CONNECT_WAIT       2
+#define IWCM_F_CONNECT_SENT       3
 
 #endif /* IWCM_H */
-- 
2.43.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.