Netdev List
 help / color / mirror / Atom feed
* [PATCH net v3] net/smc: fix lgr/lnk lifetime vs diag reader race
@ 2026-09-08  9:22 Mahanta Jambigi
       [not found] ` <20260909092315.64CC41F00A3A@smtp.kernel.org>
  2026-09-11  0:24 ` netdev-bot+sashiko
  0 siblings, 2 replies; 4+ messages in thread
From: Mahanta Jambigi @ 2026-09-08  9:22 UTC (permalink / raw)
  To: andrew+netdev, davem, edumazet, kuba, pabeni, alibuda, dust.li,
	sidraya
  Cc: pasic, horms, tonylu, guwen, hidayath, stable, netdev, linux-s390,
	linux-rdma, Mahanta Jambigi

The SMC diag dump path reads conn->lgr and conn->lnk while iterating the socket
hash table under a read_lock.  Concurrently, RDMA link failure teardown
(__smc_lgr_terminate -> smc_conn_kill -> smc_close_active_abort ->
smc_conn_free) and the passive close workqueue path (smc_close_passive_work ->
smc_conn_free) can drop the connection-owned references to lgr and lnk while the
socket is still visible in the hash, allowing the diag reader to dereference a
freed lgr or lnk.

Fix this by ensuring that for all non-fallback paths the socket is removed from
the hash table before the lgr/lnk references are dropped in smc_conn_free().

Introduce smc_conn_unhash() with a per-connection 'unhashed' flag so that the
unhash executes exactly once regardless of which path reaches smc_conn_free()
first.  smc_conn_free() calls smc_conn_unhash() before the lgr_put/link_put
sequence, establishing the invariant: any socket still visible to the diag
reader under the hash read_lock has valid conn->lgr and conn->lnk pointers.

__smc_release() is updated to use smc_conn_unhash() for non-fallback sockets (so
the flag is honoured when smc_conn_free() already ran, e.g.  via smc_conn_kill()
ahead of the user-space close()), and keeps the direct sk->sk_prot->unhash()
call only for fallback sockets, which never call smc_conn_free().

The diag path therefore reduces to:
  hold hash read_lock -> read conn->lgr -> if non-NULL, dereference -> done
with no new lock, no extra reference count, and no trylock.

Fixes: f16a7dd5cf27 ("smc: netlink interface for SMC sockets")
Fixes: 9dbe086c69b8 ("net/smc: fix invalid link access in dumping SMC-R connections")
Reviewed-by: Sidraya Jayagond <sidraya@linux.ibm.com>
Signed-off-by: Mahanta Jambigi <mjambigi@linux.ibm.com>
---
Changes in v3:
- redesigned as a single patch; dropped the lgr_lnk_lock spinlock
  approach and the 2-patch split
- fix is now at the socket hash layer: introduce smc_conn_unhash() with
  a per-connection unhashed flag; smc_conn_free() unhashes before dropping
  lgr/lnk refs, so any socket visible to the diag reader under the hash
  read_lock has valid conn->lgr and conn->lnk pointers
- __smc_release() updated to call smc_conn_unhash() for non-fallback
  sockets so the flag is honoured when smc_conn_free() already ran first
- smc_diag.c needs no changes; the hash read_lock invariant is
  sufficient without any per-connection lock in the dump path
- dropped the clcsock/mutex_trylock fix as that will be addressed separately

Changes in v2:
- this is v2 of the 2-patch series; the earlier submission was mislabelled
  [PATCH v3] but was in fact the first version sent to the list
- split into a 2-patch series; patch 1/2 adds per-connection lgr_lnk_lock
  infrastructure to smc_core, patch 2/2 fixes the diag dump path using it
- dropped lock_sock()/release_sock() from __smc_diag_dump(); v1 held the
  socket lock across all lgr/lnk dereferences, requiring the hash read_lock
  to be dropped and re-acquired around each socket
- dropped the restart-from-head loop in smc_diag_dump_proto(); the new
  design does not drop the hash read_lock mid-walk so the hlist truncation
  concern no longer applies
- dropped refcount_inc_not_zero() socket pinning from the dump loop for the
  same reason: the hash read_lock is now held for the full walk
- added per-connection lgr_lnk_lock spinlock to struct smc_connection;
  conn->lgr and conn->lnk are NULLed under this lock in smc_conn_free()
  before borrowed references are released, establishing the invariant: a
  non-NULL conn->lgr seen under lgr_lnk_lock guarantees the lgr is alive
- added lgr_lnk_lock to smc_switch_link_and_count() to protect the conn->lnk
  pointer swap from concurrent diag readers
- replaced mutex_lock() on clcsock_release_lock in smc_diag_msg_common_fill()
  with mutex_trylock(); mutex_lock() was valid in v1 because the hash
  spinlock had been dropped, but the new design holds the hash read_lock
  throughout so only a non-sleeping trylock is safe; a failed trylock leaves
  address fields zeroed, which is acceptable for a monitoring tool
- all conn->lgr and conn->lnk accesses in __smc_diag_dump() use a
  snapshot-then-use pattern: fields are copied into local stack variables
  under lgr_lnk_lock and nla_put() is called after releasing the lock,
  avoiding any sleeping operation under the spinlock

 net/smc/af_smc.c   | 10 +++++++++-
 net/smc/smc.h      |  1 +
 net/smc/smc_core.c | 20 ++++++++++++++++++++
 net/smc/smc_core.h |  1 +
 4 files changed, 32 insertions(+), 1 deletion(-)

diff --git a/net/smc/af_smc.c b/net/smc/af_smc.c
index e9f93b3ab435..8c781a4a4485 100644
--- a/net/smc/af_smc.c
+++ b/net/smc/af_smc.c
@@ -310,7 +310,15 @@ static int __smc_release(struct smc_sock *smc)
 		smc_restore_fallback_changes(smc);
 	}

-	sk->sk_prot->unhash(sk);
+	/* Fallback sockets never call smc_conn_free(), so unhash directly.
+	 * Non-fallback sockets use smc_conn_unhash() so that the conn->unhashed
+	 * flag keeps the unhash exactly once even when smc_conn_free() already ran
+	 * first (e.g. via smc_conn_kill()).
+	 */
+	if (smc->use_fallback)
+		sk->sk_prot->unhash(sk);
+	else
+		smc_conn_unhash(&smc->conn);

 	if (sk->sk_state == SMC_CLOSED) {
 		if (smc->clcsock) {
diff --git a/net/smc/smc.h b/net/smc/smc.h
index 427b6d63b993..075312278835 100644
--- a/net/smc/smc.h
+++ b/net/smc/smc.h
@@ -279,6 +279,7 @@ struct smc_connection {
 	u64			peer_token;	/* SMC-D token of peer */
 	u8			killed;		/* abnormal termination */
 	u8			freed;		/* normal termination */
+	u8			unhashed;	/* removed from sock hash */
 	u8			out_of_sync;	/* out of sync with peer */
 };

diff --git a/net/smc/smc_core.c b/net/smc/smc_core.c
index 04aedd957543..e302221c35e3 100644
--- a/net/smc/smc_core.c
+++ b/net/smc/smc_core.c
@@ -1251,6 +1251,20 @@ static void smc_buf_unuse(struct smc_connection *conn,
 	}
 }

+/* unhash the socket once; owns the single unhash for all non-fallback paths.
+ * Every caller holds lock_sock for this socket, so conn->unhashed is protected
+ * by that lock and no separate synchronisation is needed.
+ */
+void smc_conn_unhash(struct smc_connection *conn)
+{
+	struct smc_sock *smc = container_of(conn, struct smc_sock, conn);
+
+	if (!conn->unhashed) {
+		conn->unhashed = 1;
+		smc->sk.sk_prot->unhash(&smc->sk);
+	}
+}
+
 /* remove a finished connection from its link group */
 void smc_conn_free(struct smc_connection *conn)
 {
@@ -1263,6 +1277,11 @@ void smc_conn_free(struct smc_connection *conn)
 		return;

 	conn->freed = 1;
+	/* Unhash before dropping lgr/lnk refs so the diag reader, which
+	 * iterates under the socket hash read_lock, cannot see a connection whose
+	 * lgr or lnk is being freed concurrently.
+	 */
+	smc_conn_unhash(conn);
 	if (!smc_conn_lgr_valid(conn))
 		/* Connection has already unregistered from
 		 * link group.
@@ -2053,6 +2072,7 @@ int smc_conn_create(struct smc_sock *smc, struct smc_init_info *ini)
 	if (!conn->lgr->is_smcd)
 		smcr_link_hold(conn->lnk); /* link_put in smc_conn_free() */
 	conn->freed = 0;
+	conn->unhashed = 0;
 	conn->local_tx_ctrl.common.type = SMC_CDC_MSG_TYPE;
 	conn->local_tx_ctrl.len = SMC_WR_TX_SIZE;
 	conn->urg_state = SMC_URG_READ;
diff --git a/net/smc/smc_core.h b/net/smc/smc_core.h
index 5c18f08a4c8a..f23d60aef0b7 100644
--- a/net/smc/smc_core.h
+++ b/net/smc/smc_core.h
@@ -595,6 +595,7 @@ void smc_sndbuf_sync_sg_for_device(struct smc_connection *conn);
 void smc_rmb_sync_sg_for_cpu(struct smc_connection *conn);
 int smc_vlan_by_tcpsk(struct socket *clcsock, struct smc_init_info *ini);

+void smc_conn_unhash(struct smc_connection *conn);
 void smc_conn_free(struct smc_connection *conn);
 int smc_conn_create(struct smc_sock *smc, struct smc_init_info *ini);
 int smc_core_init(void);
-- 
2.50.1 (Apple Git-155)

^ permalink raw reply related	[flat|nested] 4+ messages in thread

* Re: [PATCH net v3] net/smc: fix lgr/lnk lifetime vs diag reader race
       [not found] ` <20260909092315.64CC41F00A3A@smtp.kernel.org>
@ 2026-09-10  5:54   ` Mahanta Jambigi
  0 siblings, 0 replies; 4+ messages in thread
From: Mahanta Jambigi @ 2026-09-10  5:54 UTC (permalink / raw)
  To: sashiko-reviews
  Cc: linux-s390, Alexander Gordeev, Christian Borntraeger, linux-rdma,
	Vasily Gorbik, Heiko Carstens, netdev



On 09/09/26 2:53 pm, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
> 
> New issues:
> - [Medium] Premature unhashing of SMC sockets breaks netlink diagnostics for fallback and aborted connections.
> 
> Pre-existing issues:
> - [High] Concurrent link failover clears link memory while diagnostic reader accesses it, causing a NULL pointer dereference.
> --
> 
> commit c0cbdd828a851e80ad0c79e6d2ccf5b0b5d7e16e
> Author: Mahanta Jambigi <mjambigi@linux.ibm.com>
> 
>     net/smc: fix lgr/lnk lifetime vs diag reader race
> 
> This commit aims to resolve a race condition between the SMC diagnostic dump
> reader and connection teardown. It introduces a mechanism to ensure sockets
> are unhashed before their link group and link references are released in
> smc_conn_free(), preventing the diagnostic reader from accessing freed
> structures.
> 
>>     The diag path therefore reduces to:
>>       hold hash read_lock -> read conn->lgr -> if non-NULL, dereference -> done
>>     with no new lock, no extra reference count, and no trylock.
> 
> [Severity: High]
> This isn't a bug introduced by this patch, but does this approach still leave
> the link memory vulnerable to concurrent clearance during a link failover?
> 
> When __smc_diag_dump() in net/smc/smc_diag.c iterates over the hash table, it
> extracts link = smc->conn.lnk. Concurrently, a link failure could trigger
> smcr_link_down() -> smc_switch_conns() -> smc_switch_link_and_count().
> 
> The link is swapped and the old link's reference is dropped locklessly with
> respect to the dump:
> 
> net/smc/smc_core.c:smc_switch_link_and_count() {
>     ...
>     smcr_link_put(conn->lnk);
>     conn->lnk = to_lnk;
>     ...
> }
> 
> Next, smcr_link_down() calls smcr_link_clear(). When the reference count hits
> zero, __smcr_link_clear() zeroes out the link memory:
> 
> net/smc/smc_core.c:__smcr_link_clear() {
>     ...
>     memset(lnk, 0, sizeof(struct smc_link));
>     ...
> }
> 
> The diagnostic reader then evaluates link->smcibdev->ibdev->name. Since the
> link was zeroed, link->smcibdev is NULL, leading to a fatal dereference.
> Can the diagnostic reader safely dereference link->smcibdev without holding
> any connection-level lock that protects the link from concurrent clearance?

This is a false positive. The key is the link refcount accounting.

A link starts with refcnt = 1 (set in smcr_link_init()). Each
connection on that link holds an additional ref taken in
smc_conn_create() via smcr_link_hold(), paired with the
smcr_link_put() in smc_conn_free(). So for a link with N active
connections, refcnt = 1 + N.

smc_switch_link_and_count() drops only the *per-connection* ref
for the migrated connection:

    smcr_link_put(conn->lnk);   /* refcnt: (1+N) -> (1+N-1) */

For refcount_dec_and_test() to return true and trigger
__smcr_link_clear() -> memset(), the refcount would need to reach
zero. But the base ref of 1 (set at link init) is still live, so the
count never hits zero from this put.

The base ref is dropped only inside smcr_link_clear() at line 1381:

    smcr_link_put(lnk); /* theoretically last link_put */

smcr_link_clear() is called from smcr_link_down() *after*
smc_switch_conns() has already migrated all connections away. By
the time the base ref is dropped and __smcr_link_clear() fires,
no connection points to the old link anymore -- so the diag reader
cannot reach a being-zeroed link through conn->lnk.

The ordering in smcr_link_down() is:
  1. smc_switch_conns()   <- all conn->lnk updated, per-conn puts done
  2. smcr_link_clear()    <- base ref dropped, memset runs

Step 2 cannot interleave with step 1; memset is only reachable after
all connection refs have been dropped. The diag reader's conn->lnk
dereference is therefore safe.

> 
>> diff --git a/net/smc/af_smc.c b/net/smc/af_smc.c
>> index e9f93b3ab435b..8c781a4a4485b 100644
>> --- a/net/smc/af_smc.c
>> +++ b/net/smc/af_smc.c
>> @@ -310,7 +310,15 @@ static int __smc_release(struct smc_sock *smc)
> [ ... ]
>> -	sk->sk_prot->unhash(sk);
>> +	/* Fallback sockets never call smc_conn_free(), so unhash directly.
>> +	 * Non-fallback sockets use smc_conn_unhash() so that the conn->unhashed
>> +	 * flag keeps the unhash exactly once even when smc_conn_free() already ran
>> +	 * first (e.g. via smc_conn_kill()).
>> +	 */
> 
> [Severity: Medium]
> Is the assumption that fallback sockets never call smc_conn_free() accurate?
> 
> If a late fallback occurs, it invokes smc_conn_abort(), which calls
> smc_conn_free() before switching to TCP fallback:
> 
> net/smc/af_smc.c:smc_listen_decline() {
>     ...
>     smc_conn_abort(new_smc, local_first);
>     if (reason_code < 0 ||
>         smc_switch_to_fallback(new_smc, reason_code)) {
>     ...
> }
> 
> Does this mean fallback sockets can traverse the smc_conn_free() path?
> 
>> +	if (smc->use_fallback)
>> +		sk->sk_prot->unhash(sk);
>> +	else
>> +		smc_conn_unhash(&smc->conn);
>>  
> [ ... ]
>> diff --git a/net/smc/smc_core.c b/net/smc/smc_core.c
>> index 04aedd957543a..e302221c35e3f 100644
>> --- a/net/smc/smc_core.c
>> +++ b/net/smc/smc_core.c
>> @@ -1263,6 +1277,11 @@ void smc_conn_free(struct smc_connection *conn)
>>  		return;
>>  
>>  	conn->freed = 1;
>> +	/* Unhash before dropping lgr/lnk refs so the diag reader, which
>> +	 * iterates under the socket hash read_lock, cannot see a connection whose
>> +	 * lgr or lnk is being freed concurrently.
>> +	 */
>> +	smc_conn_unhash(conn);
> 
> [Severity: Medium]
> Will this prematurely unhash SMC sockets during connection aborts and late
> fallback transitions?
> 
> Since smc_conn_free() is called by smc_conn_abort(), and late fallback calls
> smc_conn_abort() (e.g., smc_listen_decline() -> smc_conn_abort() ->
> smc_conn_free()), the socket is removed from the SMC hash table despite
> remaining an active, functioning fallback socket.
> 
> Could this hide fallback and aborted sockets from diagnostic monitoring tools
> that rely on the hash table (using SMC_DIAG_MODE_FALLBACK_TCP), breaking
> observability for these sockets?

You are right. smc_conn_abort() calls smc_conn_free() while
use_fallback is still false, so placing smc_conn_unhash() inside
smc_conn_free() prematurely removes the socket from the hash before
smc_switch_to_fallback() runs — making live fallback sockets
invisible to smcss. I'll fix it in the next version.

^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [PATCH net v3] net/smc: fix lgr/lnk lifetime vs diag reader race
  2026-09-08  9:22 [PATCH net v3] net/smc: fix lgr/lnk lifetime vs diag reader race Mahanta Jambigi
       [not found] ` <20260909092315.64CC41F00A3A@smtp.kernel.org>
@ 2026-09-11  0:24 ` netdev-bot+sashiko
  2026-09-11  8:10   ` Mahanta Jambigi
  1 sibling, 1 reply; 4+ messages in thread
From: netdev-bot+sashiko @ 2026-09-11  0:24 UTC (permalink / raw)
  To: mjambigi
  Cc: andrew+netdev, davem, edumazet, kuba, pabeni, alibuda, dust.li,
	sidraya, pasic, horms, tonylu, guwen, hidayath, stable, netdev,
	linux-s390, linux-rdma

Thank you for your contribution! Sashiko AI review found 2 potential
issue(s) to consider:

Critical: 0 · High: 2 (1) · Medium: 0 · Low: 0

- [High] The commit message asserts the invariant "any socket still
  visible to the diag reader under the hash read_lock has valid…
- [High] The unconditional smc_conn_unhash() added at the head of
  smc_conn_free() (net/smc/smc_core.c:1284) removes the socket from the…

Pre-existing issues:
- [High] smc_cdc_msg_validate() (net/smc/smc_cdc.c:308-327) retargets
  conn->lnk to the link a stray failover-validation CDC message arrived…

--

net/smc: fix lgr/lnk lifetime vs diag reader race

This patch adds smc_conn_unhash() plus a per-connection unhashed flag and
calls it at the top of smc_conn_free(), so the socket leaves the SMC socket
hash before the connection-owned lgr and lnk references are dropped.
__smc_release() is changed to use the new helper for non-fallback sockets
and keeps the direct sk->sk_prot->unhash() call for fallback sockets.

A few questions below about the paths that reach smc_conn_free() and about
the invariant described in the changelog.

> diff --git a/net/smc/af_smc.c b/net/smc/af_smc.c
> index e9f93b3ab435b..8c781a4a4485b 100644
> --- a/net/smc/af_smc.c
> +++ b/net/smc/af_smc.c
> @@ -310,7 +310,15 @@ static int __smc_release(struct smc_sock *smc)
>  		smc_restore_fallback_changes(smc);
>  	}
>  
> -	sk->sk_prot->unhash(sk);
> +	/* Fallback sockets never call smc_conn_free(), so unhash directly.
> +	 * Non-fallback sockets use smc_conn_unhash() so that the conn->unhashed
> +	 * flag keeps the unhash exactly once even when smc_conn_free() already ran
> +	 * first (e.g. via smc_conn_kill()).
> +	 */
> +	if (smc->use_fallback)
> +		sk->sk_prot->unhash(sk);
> +	else
> +		smc_conn_unhash(&smc->conn);

[Severity: High]
Is the premise "Fallback sockets never call smc_conn_free()" accurate?
smc_listen_decline() calls smc_conn_abort() first and only afterwards turns
the socket into a fallback socket:

net/smc/af_smc.c:smc_listen_decline() {
	/* RDMA setup failed, switch back to TCP */
	smc_conn_abort(new_smc, local_first);
	if (reason_code < 0 ||
	    smc_switch_to_fallback(new_smc, reason_code)) {
	...
	smc_listen_out_connected(new_smc);
}

net/smc/af_smc.c:smc_conn_abort() {
	...
	smc_conn_free(conn);
	...
}

The client side does the same via smc_connect_rdma()'s connect_abort label
followed by smc_connect_decline_fallback().

Beyond the comment, smc_conn_free() is also reached for connection
*attempts* on sockets that stay alive afterwards, and there is no path that
re-inserts the socket into the hash.  Can this make live sockets invisible
to smc_diag for the rest of their lifetime?

For the server ISM retry loop:

net/smc/af_smc.c:smc_listen_ism_init() {
	rc = smc_buf_create(new_smc, true);
	if (rc) {
		smc_conn_abort(new_smc, ini->first_contact_local);
		...
}

net/smc/af_smc.c:smc_find_ism_v2_device_serv() {
	for (i = 0; i < matches; i++) {
		...
		rc = smc_listen_ism_init(new_smc, ini);
		if (rc) {
			smc_init_info_store_rc(rc, ini);
			/* try next active ISM device */
			continue;
		}
		return; /* matching and usable V2 ISM device found */
	}
}

The failed attempt unhashes the socket through smc_conn_free(), the next
ISM device then succeeds, and smc_conn_create() only does
"conn->unhashed = 0" without calling sk->sk_prot->hash() again.  The same
shape exists for the v2 -> v1 RDMA fallthrough in
smc_find_rdma_v2_device_serv() -> smc_listen_find_device().

smc_diag_dump_proto() walks only that hash:

net/smc/smc_diag.c:smc_diag_dump_proto() {
	read_lock(&prot->h.smc_hash->lock);
	head = &prot->h.smc_hash->ht;
	...
	sk_for_each(sk, head) {
	...
}

so an established SMC-D/SMC-R connection or an active TCP-fallback
connection produced by these paths would no longer show up in ss --smc or
smcss, including the SMC_DIAG_MODE_FALLBACK_TCP reporting.  The same early
unhash also hides sockets that user space still has open after
__smc_lgr_terminate() -> smc_conn_kill() -> smc_close_active_abort() ->
smc_conn_free().  Should the unhash be restricted to terminal teardown, or
should the abort paths re-hash the socket?

> diff --git a/net/smc/smc_core.c b/net/smc/smc_core.c
> index 04aedd957543a..e302221c35e3f 100644
> --- a/net/smc/smc_core.c
> +++ b/net/smc/smc_core.c
> @@ -1251,6 +1251,20 @@ static void smc_buf_unuse(struct smc_connection *conn,
>  	}
>  }
>  
> +/* unhash the socket once; owns the single unhash for all non-fallback paths.
> + * Every caller holds lock_sock for this socket, so conn->unhashed is protected
> + * by that lock and no separate synchronisation is needed.
> + */
> +void smc_conn_unhash(struct smc_connection *conn)
> +{
> +	struct smc_sock *smc = container_of(conn, struct smc_sock, conn);
> +
> +	if (!conn->unhashed) {
> +		conn->unhashed = 1;
> +		smc->sk.sk_prot->unhash(&smc->sk);
> +	}
> +}
> +
>  /* remove a finished connection from its link group */
>  void smc_conn_free(struct smc_connection *conn)
>  {
> @@ -1263,6 +1277,11 @@ void smc_conn_free(struct smc_connection *conn)
>  		return;
>  
>  	conn->freed = 1;
> +	/* Unhash before dropping lgr/lnk refs so the diag reader, which
> +	 * iterates under the socket hash read_lock, cannot see a connection whose
> +	 * lgr or lnk is being freed concurrently.
> +	 */
> +	smc_conn_unhash(conn);

[Severity: High]
The changelog states the invariant as "any socket still visible to the diag
reader under the hash read_lock has valid conn->lgr and conn->lnk
pointers".  Does the SMC-R link failover path still break that for
conn->lnk?

smc_switch_link_and_count() drops the connection-owned link reference and
republishes conn->lnk while the socket stays hashed:

net/smc/smc_core.c:smc_switch_link_and_count() {
	atomic_dec(&conn->lnk->conn_cnt);
	/* link_hold in smc_conn_create() */
	smcr_link_put(conn->lnk);
	conn->lnk = to_lnk;
	...
}

It runs under conn->send_lock and lgr->conns_lock only, never under the
socket hash lock and without unhashing, so a reader that already
snapshotted the old link keeps using it:

net/smc/smc_diag.c:__smc_diag_dump() {
	struct smc_link *link = smc->conn.lnk;
	...
	memcpy(linfo.lnk[0].ibname, link->smcibdev->ibdev->name,
	       sizeof(link->smcibdev->ibdev->name));
	smc_gid_be16_convert(linfo.lnk[0].gid, link->gid);
	...
}

Once the last connection has been migrated off the old link,
smcr_link_clear() -> smcr_link_put() -> __smcr_link_clear() zeroes the
whole structure:

net/smc/smc_core.c:__smcr_link_clear() {
	put_device(&lnk->smcibdev->ibdev->dev);
	smcibdev = lnk->smcibdev;
	memset(lnk, 0, sizeof(struct smc_link));
	lnk->state = SMC_LNK_UNUSED;
	...
}

so link->smcibdev becomes NULL and the dereference above is a NULL
dereference, with the device reference already dropped.  This looks like
the same class of access the cited "Fixes: 9dbe086c69b8 (net/smc: fix
invalid link access in dumping SMC-R connections)" refers to, and the
reader/writer lock sets (hash read_lock versus send_lock/conns_lock/
llc_conf_mutex) do not intersect.  The v2 posting of this series protected
smc_switch_link_and_count() with lgr_lnk_lock; is anything replacing that
protection in v3, or should the invariant in the changelog be narrowed to
conn->lgr?

[Severity: High]
This is a pre-existing issue and not introduced by this patch, but it also
bears on the conn->lnk invariant being asserted: the reference that
smc_conn_free() releases is not always the one the connection holds.

smc_cdc_msg_validate() retargets conn->lnk with no reference transfer,
reached from smc_cdc_msg_recv() when the peer sets
prod_flags.failover_validation and the seqno is stale:

net/smc/smc_cdc.c:smc_cdc_msg_validate() {
	diff = conn->local_rx_ctrl.seqno - recv_seq;
	if (diff < 0) { /* diff larger than 0x7fff */
		...
		spin_lock_bh(&conn->send_lock);
		conn->local_tx_ctrl.conn_state_flags.peer_conn_abort = 1;
		conn->lnk = link;
		spin_unlock_bh(&conn->send_lock);
	...
}

There is no smcr_link_hold() on the new link and no smcr_link_put() on the
old one, while the counted reference was taken in smc_conn_create():

	smcr_link_hold(conn->lnk); /* link_put in smc_conn_free() */

and smc_conn_free() later releases whatever conn->lnk points at:

	if (!lgr->is_smcd)
		smcr_link_put(conn->lnk); /* link_hold in smc_conn_create() */

In a multi-link link group, does this over-put the new link and leak a
reference on the old one?  The over-put can take the new link's refcount to
zero while other connections still cache it in conn->lnk, running
__smcr_link_clear() (put_device() plus memset()) before its QP is
destroyed, after which smcr_link_clear() returns early on the
SMC_LNK_UNUSED state.  The leaked reference on the old link would keep its
__smcr_link_clear() and the nested smc_lgr_put() from ever running.

>  	if (!smc_conn_lgr_valid(conn))
>  		/* Connection has already unregistered from
>  		 * link group.
> @@ -2053,6 +2072,7 @@ int smc_conn_create(struct smc_sock *smc, struct smc_init_info *ini)
>  	if (!conn->lgr->is_smcd)
>  		smcr_link_hold(conn->lnk); /* link_put in smc_conn_free() */
>  	conn->freed = 0;
> +	conn->unhashed = 0;

[ ... ]

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260908092252.3832975-1-mjambigi%40linux.ibm.com

^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [PATCH net v3] net/smc: fix lgr/lnk lifetime vs diag reader race
  2026-09-11  0:24 ` netdev-bot+sashiko
@ 2026-09-11  8:10   ` Mahanta Jambigi
  0 siblings, 0 replies; 4+ messages in thread
From: Mahanta Jambigi @ 2026-09-11  8:10 UTC (permalink / raw)
  To: netdev-bot+sashiko
  Cc: andrew+netdev, davem, edumazet, kuba, pabeni, alibuda, dust.li,
	sidraya, pasic, horms, tonylu, guwen, hidayath, stable, netdev,
	linux-s390, linux-rdma



On 11/09/26 5:54 am, netdev-bot+sashiko@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 2 potential
> issue(s) to consider:
> 
> Critical: 0 · High: 2 (1) · Medium: 0 · Low: 0
> 
> - [High] The commit message asserts the invariant "any socket still
>   visible to the diag reader under the hash read_lock has valid…
> - [High] The unconditional smc_conn_unhash() added at the head of
>   smc_conn_free() (net/smc/smc_core.c:1284) removes the socket from the…
> 
> Pre-existing issues:
> - [High] smc_cdc_msg_validate() (net/smc/smc_cdc.c:308-327) retargets
>   conn->lnk to the link a stray failover-validation CDC message arrived…
> 
> --
> 
> net/smc: fix lgr/lnk lifetime vs diag reader race
> 
> This patch adds smc_conn_unhash() plus a per-connection unhashed flag and
> calls it at the top of smc_conn_free(), so the socket leaves the SMC socket
> hash before the connection-owned lgr and lnk references are dropped.
> __smc_release() is changed to use the new helper for non-fallback sockets
> and keeps the direct sk->sk_prot->unhash() call for fallback sockets.
> 
> A few questions below about the paths that reach smc_conn_free() and about
> the invariant described in the changelog.
> 
>> diff --git a/net/smc/af_smc.c b/net/smc/af_smc.c
>> index e9f93b3ab435b..8c781a4a4485b 100644
>> --- a/net/smc/af_smc.c
>> +++ b/net/smc/af_smc.c
>> @@ -310,7 +310,15 @@ static int __smc_release(struct smc_sock *smc)
>>  		smc_restore_fallback_changes(smc);
>>  	}
>>  
>> -	sk->sk_prot->unhash(sk);
>> +	/* Fallback sockets never call smc_conn_free(), so unhash directly.
>> +	 * Non-fallback sockets use smc_conn_unhash() so that the conn->unhashed
>> +	 * flag keeps the unhash exactly once even when smc_conn_free() already ran
>> +	 * first (e.g. via smc_conn_kill()).
>> +	 */
>> +	if (smc->use_fallback)
>> +		sk->sk_prot->unhash(sk);
>> +	else
>> +		smc_conn_unhash(&smc->conn);
> 
> [Severity: High]
> Is the premise "Fallback sockets never call smc_conn_free()" accurate?
> smc_listen_decline() calls smc_conn_abort() first and only afterwards turns
> the socket into a fallback socket:
> 
> net/smc/af_smc.c:smc_listen_decline() {
> 	/* RDMA setup failed, switch back to TCP */
> 	smc_conn_abort(new_smc, local_first);
> 	if (reason_code < 0 ||
> 	    smc_switch_to_fallback(new_smc, reason_code)) {
> 	...
> 	smc_listen_out_connected(new_smc);
> }
> 
> net/smc/af_smc.c:smc_conn_abort() {
> 	...
> 	smc_conn_free(conn);
> 	...
> }
> 
> The client side does the same via smc_connect_rdma()'s connect_abort label
> followed by smc_connect_decline_fallback().
> 
> Beyond the comment, smc_conn_free() is also reached for connection
> *attempts* on sockets that stay alive afterwards, and there is no path that
> re-inserts the socket into the hash.  Can this make live sockets invisible
> to smc_diag for the rest of their lifetime?
> 
> For the server ISM retry loop:
> 
> net/smc/af_smc.c:smc_listen_ism_init() {
> 	rc = smc_buf_create(new_smc, true);
> 	if (rc) {
> 		smc_conn_abort(new_smc, ini->first_contact_local);
> 		...
> }
> 
> net/smc/af_smc.c:smc_find_ism_v2_device_serv() {
> 	for (i = 0; i < matches; i++) {
> 		...
> 		rc = smc_listen_ism_init(new_smc, ini);
> 		if (rc) {
> 			smc_init_info_store_rc(rc, ini);
> 			/* try next active ISM device */
> 			continue;
> 		}
> 		return; /* matching and usable V2 ISM device found */
> 	}
> }
> 
> The failed attempt unhashes the socket through smc_conn_free(), the next
> ISM device then succeeds, and smc_conn_create() only does
> "conn->unhashed = 0" without calling sk->sk_prot->hash() again.  The same
> shape exists for the v2 -> v1 RDMA fallthrough in
> smc_find_rdma_v2_device_serv() -> smc_listen_find_device().
> 
> smc_diag_dump_proto() walks only that hash:
> 
> net/smc/smc_diag.c:smc_diag_dump_proto() {
> 	read_lock(&prot->h.smc_hash->lock);
> 	head = &prot->h.smc_hash->ht;
> 	...
> 	sk_for_each(sk, head) {
> 	...
> }
> 
> so an established SMC-D/SMC-R connection or an active TCP-fallback
> connection produced by these paths would no longer show up in ss --smc or
> smcss, including the SMC_DIAG_MODE_FALLBACK_TCP reporting.  The same early
> unhash also hides sockets that user space still has open after
> __smc_lgr_terminate() -> smc_conn_kill() -> smc_close_active_abort() ->
> smc_conn_free().  Should the unhash be restricted to terminal teardown, or
> should the abort paths re-hash the socket?

Good catch. The v3 approach was wrong to put the unhash inside
smc_conn_free() — that path is also reached by the ISM/RDMA retry loop
and the fallback abort paths, which must leave the socket hashed.

Fixed in the next version(v4) by restricting the unhash to the two
terminal teardown sites directly: smc_close_active_abort() (covering the
smc_conn_kill() path) and smc_close_passive_work() (covering the passive
close path). smc_conn_free() is left untouched, so retry aborts and
fallback transitions no longer affect the socket's hash membership.

> 
>> diff --git a/net/smc/smc_core.c b/net/smc/smc_core.c
>> index 04aedd957543a..e302221c35e3f 100644
>> --- a/net/smc/smc_core.c
>> +++ b/net/smc/smc_core.c
>> @@ -1251,6 +1251,20 @@ static void smc_buf_unuse(struct smc_connection *conn,
>>  	}
>>  }
>>  
>> +/* unhash the socket once; owns the single unhash for all non-fallback paths.
>> + * Every caller holds lock_sock for this socket, so conn->unhashed is protected
>> + * by that lock and no separate synchronisation is needed.
>> + */
>> +void smc_conn_unhash(struct smc_connection *conn)
>> +{
>> +	struct smc_sock *smc = container_of(conn, struct smc_sock, conn);
>> +
>> +	if (!conn->unhashed) {
>> +		conn->unhashed = 1;
>> +		smc->sk.sk_prot->unhash(&smc->sk);
>> +	}
>> +}
>> +
>>  /* remove a finished connection from its link group */
>>  void smc_conn_free(struct smc_connection *conn)
>>  {
>> @@ -1263,6 +1277,11 @@ void smc_conn_free(struct smc_connection *conn)
>>  		return;
>>  
>>  	conn->freed = 1;
>> +	/* Unhash before dropping lgr/lnk refs so the diag reader, which
>> +	 * iterates under the socket hash read_lock, cannot see a connection whose
>> +	 * lgr or lnk is being freed concurrently.
>> +	 */
>> +	smc_conn_unhash(conn);
> 
> [Severity: High]
> The changelog states the invariant as "any socket still visible to the diag
> reader under the hash read_lock has valid conn->lgr and conn->lnk
> pointers".  Does the SMC-R link failover path still break that for
> conn->lnk?
> 
> smc_switch_link_and_count() drops the connection-owned link reference and
> republishes conn->lnk while the socket stays hashed:
> 
> net/smc/smc_core.c:smc_switch_link_and_count() {
> 	atomic_dec(&conn->lnk->conn_cnt);
> 	/* link_hold in smc_conn_create() */
> 	smcr_link_put(conn->lnk);
> 	conn->lnk = to_lnk;
> 	...
> }
> 
> It runs under conn->send_lock and lgr->conns_lock only, never under the
> socket hash lock and without unhashing, so a reader that already
> snapshotted the old link keeps using it:
> 
> net/smc/smc_diag.c:__smc_diag_dump() {
> 	struct smc_link *link = smc->conn.lnk;
> 	...
> 	memcpy(linfo.lnk[0].ibname, link->smcibdev->ibdev->name,
> 	       sizeof(link->smcibdev->ibdev->name));
> 	smc_gid_be16_convert(linfo.lnk[0].gid, link->gid);
> 	...
> }
> 
> Once the last connection has been migrated off the old link,
> smcr_link_clear() -> smcr_link_put() -> __smcr_link_clear() zeroes the
> whole structure:

Actually it doesn't clear the structure because it *never calls*
__smcr_link_clear().

smc_switch_link_and_count() drops one per-connection hold on from_lnk,
but the structural reference set in smcr_link_init()
(refcount_set(&lnk->refcnt, 1)) is still held — smcr_link_clear() has
not run yet at that point. So refcount_dec_and_test() cannot return
true, __smcr_link_clear() is never reached from this path, and the
memset/NULL-deref scenario does not apply here.

The remaining concern is a data race on the conn->lnk pointer itself:
smc_switch_link_and_count() writes conn->lnk = to_lnk under
conn->send_lock, while the diag reader reads it under the hash read_lock
— two independent locks with no intersection. However, because the old
link's structural refcount is still live, reading the stale old pointer
is safe; the worst outcome is a diag dump reporting stale link info
during an active failover, not a crash or use-after-free. The invariant
in the changelog holds for the no-crash case; it just doesn't guarantee
freshness of conn->lnk during failover.
> 
> net/smc/smc_core.c:__smcr_link_clear() {
> 	put_device(&lnk->smcibdev->ibdev->dev);
> 	smcibdev = lnk->smcibdev;
> 	memset(lnk, 0, sizeof(struct smc_link));
> 	lnk->state = SMC_LNK_UNUSED;
> 	...
> }
> 
> so link->smcibdev becomes NULL and the dereference above is a NULL
> dereference, with the device reference already dropped.  This looks like
> the same class of access the cited "Fixes: 9dbe086c69b8 (net/smc: fix
> invalid link access in dumping SMC-R connections)" refers to, and the
> reader/writer lock sets (hash read_lock versus send_lock/conns_lock/
> llc_conf_mutex) do not intersect.  The v2 posting of this series protected
> smc_switch_link_and_count() with lgr_lnk_lock; is anything replacing that
> protection in v3, or should the invariant in the changelog be narrowed to
> conn->lgr?
> 
> [Severity: High]
> This is a pre-existing issue and not introduced by this patch, but it also
> bears on the conn->lnk invariant being asserted: the reference that
> smc_conn_free() releases is not always the one the connection holds.

Agreed, this is pre-existing and orthogonal to the diag race fix. I'll
address it in a separate patch.

The root cause is that smc_cdc_msg_validate() reassigns conn->lnk
without a reference transfer — no smcr_link_hold() on the incoming link
and no smcr_link_put() on the old one — breaking the invariant
established by smc_conn_create(). The fix is to add the missing hold/put
pair around the reassignment, mirroring what smc_switch_link_and_count()
already does correctly.

^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-09-11  8:11 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-08  9:22 [PATCH net v3] net/smc: fix lgr/lnk lifetime vs diag reader race Mahanta Jambigi
     [not found] ` <20260909092315.64CC41F00A3A@smtp.kernel.org>
2026-09-10  5:54   ` Mahanta Jambigi
2026-09-11  0:24 ` netdev-bot+sashiko
2026-09-11  8:10   ` Mahanta Jambigi

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox