Skip to contentSkip to conversation

smb: smbdirect: fix listener backlog leak and teardown races

PATCH Superseded Stefan Metzmacher 2 participants 5 messages Oct 5

Subsystems (4)

v1 of 4 Superseded Latest is v4 →

lore
Share & tools
lore

Stefan Metzmacher

author reviewer
#0 lore raw

cover smb: smbdirect: fix listener backlog leak and teardown races

Hi Namjae,

These fix a set of problems in the shared smbdirect module (fs/smb/smbdirect/), reached through the listener/accept path, which in practice is driven by ksmbd for incoming SMB Direct connections.

The main one is a pre-authentication remote denial of service: the listener keeps every accepted connection on a fixed-size backlog (listen.pending, limit 10 for ksmbd). A connection is only removed from that backlog on the success path (promotion to listen.ready after a completed negotiate, or dequeue by accept()). A connection that is accepted at the RDMA level but then fails before it is accepted by the upper layer - negotiate timeout, peer disconnect, or an invalid negotiate request - is never removed, so its backlog slot is leaked. After about ten such events the listener rejects every new connection with -EBUSY and SMB Direct stays unusable until the service is restarted. An unauthenticated peer can trigger this with ~10 aborted connections.

This was reported by 레드팀하고싶어요 <[email protected]>:

https://lore.kernel.org/linux-cifs/CANTrAmxL5sWU8Sn29JAGZvSq5PBB2OA+VKA0MHsZJMesagtfzA@mail.gmail.com/

The series:

- smbdirect_socket_destroy_sync() no longer waits for RDMA_CM_EVENT_DISCONNECTED after rdma_disconnect(). That wait could take very long (until the rdma cm gives up, e.g. a peer that just vanished) or never complete if the event already happened and the status was overwritten. rdma_destroy_id() in smbdirect_socket_destroy() is enough to stop further events; the rest of the disconnect protocol is handled by the rdma core asynchronously. This also makes releasing orphaned sockets (next patch) non-blocking.

- A failed, not-yet-accepted child of a listener now moves itself to a new listen.orphaned list at the end of its cleanup work and queues listen.purge_orphaned_work, which releases it, so it no longer leaks its backlog slot (nor its struct sock / RDMA resources) for the lifetime of the listener. sc->accept.listener is only changed under listener->listen.lock, and whoever clears it owns the release; the listener is freed via kfree_rcu() so a child can dereference it under rcu_read_lock(). This is the DoS fix.

- smbdirect_socket_accept() no longer hands out a socket that failed after it was put on the ready list (e.g. the peer disconnected in the meantime); it checks first_error / the status under listen.lock, orphans such a socket and tries the next one.

I tested the reproducer and the problem is fixed
and I run various xfstests.

I think these are important and should go into 7.3

Stefan Metzmacher (3):
smb: smbdirect: don't wait for RDMA_CM_EVENT_DISCONNECTED in
smbdirect_socket_destroy_sync()
smb: smbdirect: release failed pending sockets of a listener
smb: smbdirect: don't hand out already failed sockets in
smbdirect_socket_accept()

 fs/smb/smbdirect/accept.c     | 122 ++++++++++++++++++++++++++--------
 fs/smb/smbdirect/connection.c |  12 ++--
 fs/smb/smbdirect/internal.h   |   2 +
 fs/smb/smbdirect/listen.c     | 116 ++++++++++++++++++++++++++++++--
 fs/smb/smbdirect/socket.c     | 113 +++++++++++++++++++++++++++----
 fs/smb/smbdirect/socket.h     |  37 +++++++++++
 6 files changed, 352 insertions(+), 50 deletions(-)
Signature · 1 line
2.43.0
author reviewer
#1 lore raw

PATCH 1/3 smb: smbdirect: don't wait for RDMA_CM_EVENT_DISCONNECTED in smbdirect_socket_destroy_sync()

smbdirect_socket_destroy_sync() waited for RDMA_CM_EVENT_DISCONNECTED after smbdirect_socket_cleanup_work() called rdma_disconnect(). That can take very long, e.g. if the peer just disappeared, until the rdma cm gives up, or forever if the event already happened, but the status was overwritten afterwards.

This is not needed: smbdirect_socket_destroy() drains the qp under rdma_lock_handler(), destroys it and calls rdma_destroy_id(), which only waits for a currently running event handler and makes sure no further events are delivered. The rest of the disconnect protocol (DREQ/DREP and timewait for IB, abrupt close for iWarp) is handled by the rdma core asynchronously. drivers/nvme/host/rdma.c also just calls rdma_disconnect() and ib_drain_qp() before rdma_destroy_id().

So smbdirect_socket_destroy() now also accepts SMBDIRECT_SOCKET_DISCONNECTING and changes the status to SMBDIRECT_SOCKET_DISCONNECTED itself under rdma_lock_handler().

We could still get RDMA_CM_EVENT_DISCONNECTED in the small windows between rdma_unlock_handler() and rdma_destroy_id(), but in that case smbdirect_connection_rdma_event_handler() is basically a no-op.

Fixes 422a2436697d ("smb: smbdirect: introduce smbdirect_socket_destroy[_sync]()")
Cc Namjae Jeon <[email protected]>
Cc Paulo Alcantara <[email protected]>
Cc Tom Talpey <[email protected]>
Assisted-by Claude:claude-opus-5-5
---
 fs/smb/smbdirect/connection.c | 12 ++++----
 fs/smb/smbdirect/socket.c     | 56 ++++++++++++++++++++++++++++-------
 2 files changed, 52 insertions(+), 16 deletions(-)
fs/smb/smbdirect/connection.c +6 -6
index afd31fa12a36..f48c857bda5a 100644
@@ -83,9 +83,9 @@ static int smbdirect_connection_rdma_event_handler(struct rdma_cm_id *id,
 		 * smbdirect_socket_schedule_cleanup[_status]() =>
 		 * smbdirect_socket_cleanup_work().
 		 *
-		 * As otherwise we'd set SMBDIRECT_SOCKET_DISCONNECTING,
-		 * but never ever get RDMA_CM_EVENT_DISCONNECTED and
-		 * never reach SMBDIRECT_SOCKET_DISCONNECTED.
+		 * As otherwise we'd set SMBDIRECT_SOCKET_DISCONNECTING
+		 * and call rdma_disconnect(), but never ever get
+		 * RDMA_CM_EVENT_DISCONNECTED.
 		 */
 		if (event->event == RDMA_CM_EVENT_DEVICE_REMOVAL)
 			smbdirect_socket_schedule_cleanup_status(sc,
@@ -113,9 +113,9 @@ static int smbdirect_connection_rdma_event_handler(struct rdma_cm_id *id,
 		 * smbdirect_socket_schedule_cleanup_status() =>
 		 * smbdirect_socket_cleanup_work().
 		 *
-		 * As otherwise we'd set SMBDIRECT_SOCKET_DISCONNECTING,
-		 * but never ever get RDMA_CM_EVENT_DISCONNECTED and
-		 * never reach SMBDIRECT_SOCKET_DISCONNECTED.
+		 * As otherwise we'd set SMBDIRECT_SOCKET_DISCONNECTING
+		 * and call rdma_disconnect(), but never ever get
+		 * RDMA_CM_EVENT_DISCONNECTED.
 		 *
 		 * This is also a normal disconnect so
 		 * SMBDIRECT_LOG_INFO should be good enough
fs/smb/smbdirect/socket.c +46 -10

76 lines not on this page · raw message

Signature · 1 line
2.43.0
#2 lore raw

Re: [PATCH 1/3] smb: smbdirect: don't wait for RDMA_CM_EVENT_DISCONNECTED in smbdirect_socket_destroy_sync()

Sashiko has reviewed this patch and found no issues. It looks great!
Quoted 14 lines

smbdirect_socket_destroy_sync() waited for RDMA_CM_EVENT_DISCONNECTED after smbdirect_socket_cleanup_work() called rdma_disconnect(). That can take very long, e.g. if the peer just disappeared, until the rdma cm gives up, or forever if the event already happened, but the status was overwritten afterwards.

This is not needed: smbdirect_socket_destroy() drains the qp under rdma_lock_handler(), destroys it and calls rdma_destroy_id(), which only waits for a currently running event handler and makes sure no further events are delivered. The rest of the disconnect protocol (DREQ/DREP and timewait for IB, abrupt close for iWarp) is handled by the rdma core asynchronously. drivers/nvme/host/rdma.c also just calls rdma_disconnect() and ib_drain_qp() before rdma_destroy_id().

Quoted 7 lines

So smbdirect_socket_destroy() now also accepts

[ ... ]
Assisted-by Claude:claude-opus-5-5

Sashiko has reviewed this patch and found no issues. It looks great!

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1
author reviewer
#3 lore raw

PATCH 2/3 smb: smbdirect: release failed pending sockets of a listener

In reply to Stefan Metzmacher

A child of a listener only left the listen.pending or listen.ready list via smbdirect_socket_accept() or when the listener was destroyed. So every connection that failed before it was accepted (negotiate timeout, peer disconnect, invalid negotiate request, ...) stayed on the list and counted against the backlog until the listener was destroyed. After about backlog (10 for ksmbd) such connections smbdirect_listen_connect_request() rejected every new connection with -EBUSY, so an unauthenticated peer was able to permanently stop ksmbd from accepting SMB-Direct connections.

Now a failed child moves itself to a new listen.orphaned list at the end of smbdirect_socket_cleanup_work(), after the disconnect was started, and queues listen.purge_orphaned_work, which releases it with smbdirect_socket_release(), without holding the listener's rdma_lock_handler() lock, in the same way smbdirect_socket_destroy() does for the remaining children. Orphaned sockets still count against the backlog until they are released.

sc->accept.listener is now only changed under listener->listen.lock and whoever clears it is responsible for releasing the socket:
smbdirect_socket_accept(), purge_orphaned_work or the destroy of the listener.

In order to make that work:

- a struct smbdirect_socket that is a listener is now freed via kfree_rcu(), so that a child can dereference sc->accept.listener under rcu_read_lock(), even if the listener is released concurrently.

- smbdirect_listen_connect_request() only sets nsc->accept.listener after smbdirect_accept_connect_request() succeeded, so that a failure within it can't let nsc orphan itself before we release it.

- smbdirect_accept_negotiate_recv_work() no longer moves a socket to the ready list if it already failed or no longer belongs to the listener. Every accepting socket has a listener, so if sc->accept.listener is already cleared, it no longer sends a negotiate response, as the socket is about to be released.

- smbdirect_socket_destroy() of the listener disables purge_orphaned_work and also releases the orphaned sockets.

Fixes dc691b91ad16 ("smb: smbdirect: introduce smbdirect_socket_{listen,accept}()")
Cc Namjae Jeon <[email protected]>
Cc Paulo Alcantara <[email protected]>
Cc Tom Talpey <[email protected]>
Assisted-by Claude:claude-opus-5-5
---
 fs/smb/smbdirect/accept.c   |  67 ++++++++++++++-------
 fs/smb/smbdirect/internal.h |   2 +
 fs/smb/smbdirect/listen.c   | 116 ++++++++++++++++++++++++++++++++++--
 fs/smb/smbdirect/socket.c   |  57 +++++++++++++++++-
 fs/smb/smbdirect/socket.h   |  37 ++++++++++++
 5 files changed, 252 insertions(+), 27 deletions(-)
fs/smb/smbdirect/accept.c +47 -20
index 1b30ca8476c1..1da95cdd1ebd 100644
@@ -302,6 +302,7 @@ static void smbdirect_accept_negotiate_recv_work(struct work_struct *work)
 	struct smbdirect_socket *sc =
 		container_of(work, struct smbdirect_socket, connect.work);
 	struct smbdirect_socket_parameters *sp = &sc->parameters;
+	struct smbdirect_socket *lsc;
 	struct smbdirect_recv_io *recv_io;
 	struct smbdirect_negotiate_req *nreq;
 	unsigned long flags;
@@ -464,29 +465,55 @@ static void smbdirect_accept_negotiate_recv_work(struct work_struct *work)
 	 */
 	sp->max_fragmented_send_size = max_fragmented_size;
 
-	if (sc->accept.listener) {
-		struct smbdirect_socket *lsc = sc->accept.listener;
-		unsigned long flags;
+	/*
+	 * Every accepting socket was created by
+	 * smbdirect_listen_connect_request() and has a
+	 * listener. If it's already cleared, the listener
+	 * (or its purge_orphaned_work) is responsible for
+	 * releasing us, so we must not send a negotiate
+	 * response.
+	 *
+	 * The memory of the listener is freed via kfree_rcu(),
+	 * see smbdirect_listen_orphan_socket().
+	 */
+	rcu_read_lock();
+	lsc = READ_ONCE(sc->accept.listener);
+	if (!lsc) {
+		rcu_read_unlock();
+		return;
+	}
 
-		spin_lock_irqsave(&lsc->listen.lock, flags);
-		list_del(&sc->accept.list);
-		list_add_tail(&sc->accept.list, &lsc->listen.ready);
+	spin_lock_irqsave(&lsc->listen.lock, flags);
+	/*
+	 * The listener (or its purge_orphaned_work)
+	 * may have cleared sc->accept.listener in the
+	 * meantime and is responsible for releasing us.
+	 *
+	 * If we already failed, we're either
+	 * already on the orphaned list or
+	 * smbdirect_socket_cleanup_work() will
+	 * move us there.
+	 *
+	 * In both cases we must not move us
+	 * to the ready list.
+	 */
+	if (sc->accept.listener == lsc && !READ_ONCE(sc->first_error)) {
+		list_move_tail(&sc->accept.list, &lsc->listen.ready);
 		wake_up(&lsc->listen.wait_queue);
-		spin_unlock_irqrestore(&lsc->listen.lock, flags);
-
-		/*
-		 * smbdirect_socket_accept() will call
-		 * smbdirect_accept_negotiate_finish(nsc, 0);
-		 *
-		 * So that we don't send the negotiation
-		 * response that grants credits to the peer
-		 * before the socket is accepted by the
-		 * application.
-		 */
-		return;
 	}
+	spin_unlock_irqrestore(&lsc->listen.lock, flags);
+	rcu_read_unlock();
 
-	ntstatus = le32_to_cpu(STATUS_SUCCESS);
+	/*
+	 * smbdirect_socket_accept() will call
+	 * smbdirect_accept_negotiate_finish(nsc, 0);
+	 *
+	 * So that we don't send the negotiation
+	 * response that grants credits to the peer
+	 * before the socket is accepted by the
+	 * application.
+	 */
+	return;
 
 not_supported:
 	smbdirect_accept_negotiate_finish(sc, ntstatus);
@@ -839,7 +866,7 @@ struct smbdirect_socket *smbdirect_socket_accept(struct smbdirect_socket *lsc,
 				       struct smbdirect_socket,
 				       accept.list);
 	if (nsc) {
-		nsc->accept.listener = NULL;
+		WRITE_ONCE(nsc->accept.listener, NULL);
 		list_del_init_careful(&nsc->accept.list);
 		arg->is_empty = list_empty_careful(&lsc->listen.ready);
 	}
fs/smb/smbdirect/internal.h +2 -0
index e9959e6dc13a..69d0c9f899de 100644
@@ -74,6 +74,8 @@ void __smbdirect_socket_schedule_cleanup(struct smbdirect_socket *sc,
 
 void smbdirect_socket_destroy_sync(struct smbdirect_socket *sc);
 
+void smbdirect_listen_orphan_socket(struct smbdirect_socket *sc);
+
 int smbdirect_socket_wait_for_credits(struct smbdirect_socket *sc,
 				      enum smbdirect_socket_status expected_status,
 				      int unexpected_errno,
fs/smb/smbdirect/listen.c +111 -5

164 lines not on this page · raw message

fs/smb/smbdirect/socket.c +55 -2

99 lines not on this page · raw message

fs/smb/smbdirect/socket.h +37 -0
index c09eddd8ad16..36623980ec17 100644
@@ -151,6 +151,12 @@ struct smbdirect_socket {
 		 * the disconnect refcount.
 		 */
 		struct kref destroy;
+
+		/*
+		 * smbdirect_socket_release_destroy() uses
+		 * kfree_rcu(), see accept.listener.
+		 */
+		struct rcu_head rcu;
 	} refs;
 
 	/* RDMA related */
@@ -216,6 +222,16 @@ struct smbdirect_socket {
 		 * only be > 0.
 		 */
 		int backlog;
+		/*
+		 * Sockets on pending or ready that failed
+		 * move themselves to orphaned and queue
+		 * purge_orphaned_work, which releases them.
+		 * So that they are freed while the listener
+		 * is still alive. They still count against
+		 * the backlog until they are released.
+		 */
+		struct list_head orphaned;
+		struct work_struct purge_orphaned_work;
 	} listen;
 
 	/*
@@ -226,7 +242,25 @@ struct smbdirect_socket {
 	 * connection.
 	 */
 	struct {
+		/*
+		 * This is only set, protected by
+		 * listener->listen.lock, while the socket
+		 * is owned by the listener (on its pending,
+		 * ready or orphaned list). The one who clears
+		 * it is responsible for releasing the socket.
+		 *
+		 * The memory of a struct smbdirect_socket is
+		 * freed via kfree_rcu(), so the listener
+		 * can be dereferenced under rcu_read_lock(),
+		 * even if accept.listener is cleared and
+		 * the listener is released concurrently.
+		 */
 		struct smbdirect_socket *listener;
+		/*
+		 * Note even with listener being NULL,
+		 * the socket can be on listeners.listen.pending
+		 * before smbdirect_accept_connect_request() finished.
+		 */
 		struct list_head list;
 	} accept;
 
@@ -588,6 +622,9 @@ static __always_inline void smbdirect_socket_init(struct smbdirect_socket *sc)
 	spin_lock_init(&sc->listen.lock);
 	INIT_LIST_HEAD(&sc->listen.pending);
 	INIT_LIST_HEAD(&sc->listen.ready);
+	INIT_LIST_HEAD(&sc->listen.orphaned);
+	INIT_WORK(&sc->listen.purge_orphaned_work, __smbdirect_socket_disabled_work);
+	disable_work_sync(&sc->listen.purge_orphaned_work);
 	sc->listen.backlog = -1; /* not a listener */
 	init_waitqueue_head(&sc->listen.wait_queue);
 
Signature · 1 line
2.43.0
author reviewer
#4 lore raw

PATCH 3/3 smb: smbdirect: don't hand out already failed sockets in smbdirect_socket_accept()

In reply to Stefan Metzmacher

A socket on the ready list is in SMBDIRECT_SOCKET_NEGOTIATE_RUNNING, but it may fail before smbdirect_socket_accept() picks it up, e.g. RDMA_CM_EVENT_DISCONNECTED already moved it to SMBDIRECT_SOCKET_DISCONNECTED.

smbdirect_socket_accept() unconditionally overwrote the status with SMBDIRECT_SOCKET_CONNECTED and handed out the already disconnected socket. smbdirect_socket_cleanup_work() then called rdma_disconnect() on it again, and before the recent commit smbdirect_socket_destroy_sync() waited forever for an RDMA_CM_EVENT_DISCONNECTED that already happened.

Now we check first_error and change the status to SMBDIRECT_SOCKET_CONNECTED only via cmpxchg() from SMBDIRECT_SOCKET_NEGOTIATE_RUNNING, while we still hold listen.lock. If the socket already failed, we move it to the orphaned list and queue purge_orphaned_work, like smbdirect_listen_orphan_socket() does, and try the next one. If we only found failed sockets, we wait for the next one.

Fixes dc691b91ad16 ("smb: smbdirect: introduce smbdirect_socket_{listen,accept}()")
Cc Namjae Jeon <[email protected]>
Cc Paulo Alcantara <[email protected]>
Cc Tom Talpey <[email protected]>
Assisted-by Claude:claude-opus-5-5
---
 fs/smb/smbdirect/accept.c | 55 ++++++++++++++++++++++++++++++++++-----
 1 file changed, 48 insertions(+), 7 deletions(-)
fs/smb/smbdirect/accept.c +48 -7
index 1da95cdd1ebd..ee16be95424d 100644
@@ -834,8 +834,12 @@ struct smbdirect_socket *smbdirect_socket_accept(struct smbdirect_socket *lsc,
 						 struct proto_accept_arg *arg)
 {
 	struct smbdirect_socket *nsc;
+	bool orphaned;
 	unsigned long flags;
 
+again:
+	orphaned = false;
+
 	if (lsc->status != SMBDIRECT_SOCKET_LISTENING) {
 		arg->err = -EINVAL;
 		return NULL;
@@ -862,9 +866,38 @@ struct smbdirect_socket *smbdirect_socket_accept(struct smbdirect_socket *lsc,
 	}
 
 	spin_lock_irqsave(&lsc->listen.lock, flags);
-	nsc = list_first_entry_or_null(&lsc->listen.ready,
-				       struct smbdirect_socket,
-				       accept.list);
+	while ((nsc = list_first_entry_or_null(&lsc->listen.ready,
+					       struct smbdirect_socket,
+					       accept.list))) {
+		/*
+		 * nsc may have failed after it was moved
+		 * to the ready list, e.g. RDMA_CM_EVENT_DISCONNECTED
+		 * already moved it to SMBDIRECT_SOCKET_DISCONNECTED.
+		 * We must not overwrite that with
+		 * SMBDIRECT_SOCKET_CONNECTED and hand
+		 * out an already disconnected socket.
+		 *
+		 * Doing this under listen.lock means nsc
+		 * still belongs to us, so we can just move
+		 * a failed socket to the orphaned list,
+		 * like smbdirect_listen_orphan_socket() does,
+		 * and try the next one.
+		 */
+		if (!READ_ONCE(nsc->first_error) &&
+		    cmpxchg(&nsc->status,
+			    SMBDIRECT_SOCKET_NEGOTIATE_RUNNING,
+			    SMBDIRECT_SOCKET_CONNECTED) ==
+		    SMBDIRECT_SOCKET_NEGOTIATE_RUNNING)
+			break;
+
+		smbdirect_log_rdma_event(nsc, SMBDIRECT_LOG_INFO,
+			"orphaning failed socket status=%s first_error=%1pe\n",
+			smbdirect_socket_status_string(nsc->status),
+			SMBDIRECT_DEBUG_ERR_PTR(nsc->first_error));
+		list_move_tail(&nsc->accept.list, &lsc->listen.orphaned);
+		queue_work(lsc->workqueues.cleanup, &lsc->listen.purge_orphaned_work);
+		orphaned = true;
+	}
 	if (nsc) {
 		WRITE_ONCE(nsc->accept.listener, NULL);
 		list_del_init_careful(&nsc->accept.list);
@@ -872,6 +905,12 @@ struct smbdirect_socket *smbdirect_socket_accept(struct smbdirect_socket *lsc,
 	}
 	spin_unlock_irqrestore(&lsc->listen.lock, flags);
 	if (!nsc) {
+		/*
+		 * If we only found failed sockets,
+		 * we wait for the next one.
+		 */
+		if (orphaned)
+			goto again;
 		arg->err = -EAGAIN;
 		return NULL;
 	}
@@ -882,12 +921,14 @@ struct smbdirect_socket *smbdirect_socket_accept(struct smbdirect_socket *lsc,
 	 * so it didn't grant any credits to us.
 	 *
 	 * The caller expects a connected socket
-	 * now as there are no credits anyway.
+	 * now as there are no credits anyway,
+	 * above we already changed to SMBDIRECT_SOCKET_CONNECTED
+	 * under the lsc->listen.lock and with cmpxchg.
 	 *
-	 * Then we send the negotiation response in
-	 * order to grant credits to the peer.
+	 * Now we send the negotiation response in
+	 * order to grant credits to the peer,
+	 * as the socket is now visible to the application layer.
 	 */
-	nsc->status = SMBDIRECT_SOCKET_CONNECTED;
 	smbdirect_accept_negotiate_finish(nsc, 0);
 
 	return nsc;
Signature · 1 line
2.43.0