* [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[parent not found: <20260909092315.64CC41F00A3A@smtp.kernel.org>]
* 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