* [PATCH net-next 0/6] net/rds: make connection lifetime reference-counted
@ 2026-09-04 7:02 Allison Henderson
2026-09-04 7:02 ` [PATCH net-next 1/6] net/rds: make rds_destroy_pending() cover single-connection destroy Allison Henderson
` (5 more replies)
0 siblings, 6 replies; 7+ messages in thread
From: Allison Henderson @ 2026-09-04 7:02 UTC (permalink / raw)
To: netdev, linux-rdma, pabeni, edumazet, kuba, horms; +Cc: achender, nicoyip.dev
Hi all,
This set is the next stage of the RDS stabilization work, following
"net/rds: own the fastpath locks across connection teardown", now
net-next. This series is targeted to net-next as well. Though the
series fixes real use-after-frees (one syzbot report and one report
from Chengfeng Ye below), it does so by reworking connection lifetime,
which is a substantial change.
rds_conn_destroy() frees the connection, its paths and its workqueues
on the spot, relying on the documented assumption that "no one else is
referencing the connection". That assumption stopped being true a
long time ago. Connections are destroyed not only on rmmod but also
on IB device removal and on protocol-version mismatch, while pointers
to them still live in socket rs_conn caches, congestion-map conn
lists, CM callbacks and workers, and - for as long as an application
leaves data unread - in every rds_incoming sitting on a receive
queue. Each of those is a use-after-free waiting for its trigger, and
no single Fixes: commit covers the rot, so the series carries
Reported-by tags where there are concrete reports instead.
Patch 1 gives the connection itself a destroy-in-progress marker so
that rds_destroy_pending() covers single-connection destroy, not
just the netns-teardown and module-unload cases. Without it, the
work-requeueing sites can re-arm works on a connection whose
workqueues are about to be destroyed.
Based on UEK commits:
e2f5005adf63 net/rds: Add krefs to struct rds_connection
https://github.com/oracle/linux-uek/commit/e2f5005adf63
6c53ef92f46e net/rds: Merge uses of conn->c_destroy_in_prog & RDS_DESTROY_PENDING
https://github.com/oracle/linux-uek/commit/6c53ef92f46e
Patch 2 splits rds_conn_destroy() into a quiesce phase and a
kref-governed free, so a connection with references still
outstanding stays allocated (quiesced, unhashed and unusable) until
the last reference is dropped.
Based on UEK commits:
2c8569e4c880 ("net/rds: Add krefs to struct rds_connection").
https://github.com/oracle/linux-uek/commit/2c8569e4c880
Patch 3 hands out real references everywhere a connection pointer
previously escaped bare: rds_conn_lookup(), __rds_conn_create()'s
return, the rs_conn sendmsg cache, and the parent's c_passive
pointer.
Based on UEK commits:
2c8569e4c880 ("net/rds: Add krefs to struct rds_connection")
https://github.com/oracle/linux-uek/commit/2c8569e4c880
0e9e3a72b7f7 ("net/rds: rds_sendmsg must use rs_conn only when not being destroyed").
https://github.com/oracle/linux-uek/commit/0e9e3a72b7f7
Patch 4 makes each transport's exit path wait for its own
connections to actually be freed before the module text goes away,
since the free - including the transport's conn_free - is now
asynchronous.
Based on UEK commits:
ece4b4e39afa ("net/rds: wait_event_timeout until zero connections during rmmod")
https://github.com/oracle/linux-uek/commit/ece4b4e39afa
905ec90e6166 ("net/rds: Each RDS transport should keep its own connection count")
https://github.com/oracle/linux-uek/commit/905ec90e6166
Patch 5 is the cleanup fallout of patch 4: the global rds_conn_count
has no remaining consumer besides a workqueue-name seed, so switch
the seed to the per-transport count and remove it.
Based on UEK commits:
905ec90e6166 ("net/rds: Each RDS transport should keep its own connection count").
https://github.com/oracle/linux-uek/commit/905ec90e6166
Patch 6 makes struct rds_incoming hold a reference on i_conn, which
is the fix for the KASAN use-after-free Chengfeng Ye reported [1],
where rds_info_getsockopt() walks a socket receive queue whose incs
point at connections rmmod already freed.
Based on UEK commits:
99b9a3715419 ("net/rds: fix crash by expanding kref coverage to rds_incoming.i_conn").
https://github.com/oracle/linux-uek/commit/99b9a3715419
Patches 2, 3, 4 and 6 are ports of the connection kref work Sharath
Srinivasan did for Oracle UEK, adapted to the upstream code.
The series has been validated with the RDS selftests over both
loopback-TCP and RXE-RDMA transports, plus targeted churn tests that
delete network namespaces and unload the modules under live rds-stress
traffic - the paths this series changes.
[1] https://lore.kernel.org/netdev/20260720184955.3008978-1-nicoyip.dev@gmail.com/
Allison
Allison Henderson (2):
net/rds: make rds_destroy_pending() cover single-connection destroy
net/rds: drop rds_conn_count in favor of t_conn_count
Sharath Srinivasan (4):
net/rds: split connection destroy into quiesce and kref-governed free
net/rds: hold connection references in lookup, sockets and c_passive
net/rds: wait for connections to be freed on transport unload
net/rds: hold a connection reference from struct rds_incoming
net/rds/af_rds.c | 8 ++
net/rds/connection.c | 189 ++++++++++++++++++++++++++++++++++++++-----
net/rds/ib.c | 14 +++-
net/rds/ib_cm.c | 8 +-
net/rds/loop.c | 2 +
net/rds/message.c | 16 +++-
net/rds/rds.h | 25 +++++-
net/rds/recv.c | 21 ++++-
net/rds/send.c | 45 +++++++++--
net/rds/tcp.c | 1 +
net/rds/tcp_listen.c | 5 +-
11 files changed, 297 insertions(+), 37 deletions(-)
--
2.25.1
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH net-next 1/6] net/rds: make rds_destroy_pending() cover single-connection destroy
2026-09-04 7:02 [PATCH net-next 0/6] net/rds: make connection lifetime reference-counted Allison Henderson
@ 2026-09-04 7:02 ` Allison Henderson
2026-09-04 7:02 ` [PATCH net-next 2/6] net/rds: split connection destroy into quiesce and kref-governed free Allison Henderson
` (4 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Allison Henderson @ 2026-09-04 7:02 UTC (permalink / raw)
To: netdev, linux-rdma, pabeni, edumazet, kuba, horms; +Cc: achender, nicoyip.dev
rds_conn_destroy() cancels the path works and then destroys the
per-path workqueue. However, nothing currently stops the
work-requeueing sites from queueing new work on the connection while
that happens. The existing code would suggest that this protection
is supposed to come from rds_destroy_pending(), since all of those
sites already guard the queueing with rds_destroy_pending() under
rcu_read_lock(), and rds_conn_destroy() already issues a
synchronize_rcu() after unhashing the connection. But the predicate
only tests for the two global teardown cases (netns destruction via
check_net(), module unload via ->t_unloading). Because the conn
itself lacks any indication that a destroy is in progress, the
predicate does not cover the destruction of a single connection
outside these two cases.
rds_conn_destroy() is not limited to the global paths: rds_ib
destroys connections whose underlying IB device was removed
(rds_ib_destroy_nodev_conns()) and connections whose peer negotiated
an unsupported protocol version (rds_ib_cm_connect_complete()).
While one of those runs, a concurrent rds_cong_queue_updates() can
still find the connection on the congestion map's m_conn_list (the
conn is only removed from it after the paths are torn down) and call
queue_delayed_work() on a cp_wq that destroy_workqueue() has already
freed. Additionally, the other requeueing sites can likewise re-arm
works that live in the about-to-be-freed connection unless
rds_destroy_pending() has something to guard it with.
The version-mismatch path used to be covered: commit c90ecbfaf50d2
("rds: Use atomic flag to track connections being destroyed")
introduced the RDS_DESTROY_PENDING cp_flags bit for exactly this, and
after commit ebeeb1ad9b8ad ("rds: tcp: use rds_destroy_pending() to
synchronize netns/module teardown and rds connection/workq management")
it was set right before that rds_conn_destroy() call and
tested via rds_ib_is_unloading(). Commit cdc306a5c9cd3 ("rds: make
v3.1 as compat version") then removed the last set_bit while leaving
the test behind, so the bit has been dead ever since and per-conn
destroy has run unguarded.
Bring the protection back at the connection level, where it also
covers the device-removal path that was never guarded: set
conn->c_destroy_in_prog before the unhash + synchronize_rcu() sequence
in rds_conn_destroy() and test it in rds_destroy_pending(). The
existing rcu_read_lock() around every check-and-queue site pairs with
that synchronize_rcu(): once it returns, every new reader observes the
flag and refuses to queue, and anything queued before it is flushed or
cancelled by the existing teardown. Drop the now-unreferenced
RDS_DESTROY_PENDING bit and its dead test.
In the Oracle UEK kernel the equivalent conn->c_destroy_in_prog flag
is part of the larger connection refcounting rework ("net/rds: Add
krefs to struct rds_connection"), including ("net/rds: Merge uses of
conn->c_destroy_in_prog & RDS_DESTROY_PENDING"). This ports the
missing pieces of the requeue guard, which stand on their own.
Fixes: cdc306a5c9cd3 ("rds: make v3.1 as compat version")
Suggested-by: Sharath Srinivasan <sharath.srinivasan@oracle.com>
Assisted-by: Claude-Code:claude-fable-5
Signed-off-by: Allison Henderson <achender@kernel.org>
---
net/rds/connection.c | 8 ++++++++
net/rds/ib.c | 5 +----
net/rds/rds.h | 8 ++++++--
3 files changed, 15 insertions(+), 6 deletions(-)
diff --git a/net/rds/connection.c b/net/rds/connection.c
index b6c4beb50eaf..50e1b6bfceea 100644
--- a/net/rds/connection.c
+++ b/net/rds/connection.c
@@ -574,6 +574,14 @@ void rds_conn_destroy(struct rds_connection *conn)
"%pI4\n", conn, &conn->c_laddr,
&conn->c_faddr);
+ /* Make rds_destroy_pending() true for this conn. Together with
+ * the synchronize_rcu() below this stops the work-requeueing
+ * sites (which all test rds_destroy_pending() under
+ * rcu_read_lock()) from queueing new work on the path
+ * workqueues once we start cancelling and destroying them.
+ */
+ WRITE_ONCE(conn->c_destroy_in_prog, true);
+
/* Ensure conn will not be scheduled for reconnect */
spin_lock_irq(&rds_conn_lock);
hlist_del_init_rcu(&conn->c_hash_node);
diff --git a/net/rds/ib.c b/net/rds/ib.c
index 786f39169bc1..9fe3b9951bd3 100644
--- a/net/rds/ib.c
+++ b/net/rds/ib.c
@@ -525,10 +525,7 @@ static void rds_ib_set_unloading(void)
static bool rds_ib_is_unloading(struct rds_connection *conn)
{
- struct rds_conn_path *cp = &conn->c_path[0];
-
- return (test_bit(RDS_DESTROY_PENDING, &cp->cp_flags) ||
- atomic_read(&rds_ib_unloading) != 0);
+ return atomic_read(&rds_ib_unloading) != 0;
}
void rds_ib_exit(void)
diff --git a/net/rds/rds.h b/net/rds/rds.h
index 2db49573dacd..cede2b03baa5 100644
--- a/net/rds/rds.h
+++ b/net/rds/rds.h
@@ -89,7 +89,6 @@ enum {
#define RDS_RECONNECT_PENDING 1
#define RDS_IN_XMIT 2
#define RDS_RECV_REFILL 3
-#define RDS_DESTROY_PENDING 4
/* Max number of multipaths per RDS connection. Must be a power of 2 */
#define RDS_MPATH_WORKERS 8
@@ -148,6 +147,10 @@ struct rds_connection {
c_pad_to_32:29;
int c_npaths;
bool c_with_sport_idx;
+ /* Set (under RCU) when rds_conn_destroy() starts on this conn;
+ * read through rds_destroy_pending().
+ */
+ bool c_destroy_in_prog;
struct rds_connection *c_passive;
struct rds_transport *c_trans;
@@ -994,7 +997,8 @@ void __rds_put_mr_final(struct kref *kref);
static inline bool rds_destroy_pending(struct rds_connection *conn)
{
- return !check_net(rds_conn_net(conn)) ||
+ return READ_ONCE(conn->c_destroy_in_prog) ||
+ !check_net(rds_conn_net(conn)) ||
(conn->c_trans->t_unloading && conn->c_trans->t_unloading(conn));
}
--
2.25.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH net-next 2/6] net/rds: split connection destroy into quiesce and kref-governed free
2026-09-04 7:02 [PATCH net-next 0/6] net/rds: make connection lifetime reference-counted Allison Henderson
2026-09-04 7:02 ` [PATCH net-next 1/6] net/rds: make rds_destroy_pending() cover single-connection destroy Allison Henderson
@ 2026-09-04 7:02 ` Allison Henderson
2026-09-04 7:02 ` [PATCH net-next 3/6] net/rds: hold connection references in lookup, sockets and c_passive Allison Henderson
` (3 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Allison Henderson @ 2026-09-04 7:02 UTC (permalink / raw)
To: netdev, linux-rdma, pabeni, edumazet, kuba, horms; +Cc: achender, nicoyip.dev
From: Sharath Srinivasan <sharath.srinivasan@oracle.com>
rds_conn_destroy() tears down the transport state and immediately
frees the connection, along with its paths and its workqueues. This
relies on the assumption (documented in the rds_conn_destroy()
comments) that "no one else is referencing the connection", which "we
can only ensure ... in the rmmod path". However, the callers stopped
honoring that long ago. Today, connections can also be destroyed
when the underlying IB device is removed (in
rds_ib_destroy_nodev_conns()), or when a peer negotiates an
unsupported protocol version.
This leaves loose ends since references to these destroyed
connections still exist. Sockets cache their connections in rs_conn,
and congestion updates will walk the maps' m_conn_list. The CM
callbacks and workers may also still hold the pointer.
Prepare to close those holes by making the connection refcounted:
- kref_init() the connection in __rds_conn_create(); the initial
reference belongs to whoever is responsible for destroying the
connection.
- rds_conn_destroy() still quiesces synchronously exactly as before
(workers cancelled, paths dropped and shut down, queued messages
purged, congestion list removal), but the frees - the transport's
conn_free, the path workqueues, the c_path array and the connection
slab object - move to rds_conn_destroy_fini(), which runs when the
last reference is dropped via rds_conn_put().
- Export rds_conn_get()/rds_conn_put() for the reference holders
introduced in the following patches.
With no additional reference holders yet, this only sets up the
refcounting framework and is functionally equivalent to the current
code (the initial reference is the only one). Subsequent patches will
take references at the places that today rely on bare pointers.
Based on Oracle UEK commit 2c8569e4c880 ("net/rds: Add krefs to struct
rds_connection").
Signed-off-by: Sharath Srinivasan <sharath.srinivasan@oracle.com>
[achender: substantial reimplementation for net-next: UEK's
rds_conn_destroy_init()/_fini() split redone against upstream's
rds_conn_destroy()/rds_conn_path_destroy() (no heartbeat/reap/trace
infrastructure, no rds_net, single conn hash); destroy keeps its
one-call external interface; holder coverage split out into follow-up
patches; rewrite commit message]
Assisted-by: Claude-Code:claude-fable-5
Signed-off-by: Allison Henderson <achender@kernel.org>
---
net/rds/connection.c | 89 ++++++++++++++++++++++++++++++++++++--------
net/rds/rds.h | 8 ++++
2 files changed, 82 insertions(+), 15 deletions(-)
diff --git a/net/rds/connection.c b/net/rds/connection.c
index 50e1b6bfceea..f45fd1fb1843 100644
--- a/net/rds/connection.c
+++ b/net/rds/connection.c
@@ -215,6 +215,7 @@ static struct rds_connection *__rds_conn_create(struct net *net,
goto out;
}
+ kref_init(&conn->c_refcount);
INIT_HLIST_NODE(&conn->c_hash_node);
conn->c_laddr = *laddr;
conn->c_isv6 = !ipv6_addr_v4mapped(laddr);
@@ -515,10 +516,12 @@ void rds_conn_shutdown(struct rds_conn_path *cp)
conn->c_trans->conn_slots_available(conn, false);
}
-/* destroy a single rds_conn_path. rds_conn_destroy() iterates over
- * all paths using rds_conn_path_destroy()
+/* quiesce a single rds_conn_path: shut it down and tear down any
+ * queued messages. rds_conn_destroy() iterates over all paths using
+ * rds_conn_path_quiesce(); the transport state and the workqueue are
+ * freed later, from rds_conn_path_free().
*/
-static void rds_conn_path_destroy(struct rds_conn_path *cp)
+static void rds_conn_path_quiesce(struct rds_conn_path *cp)
{
struct rds_message *rm, *rtmp;
@@ -547,6 +550,16 @@ static void rds_conn_path_destroy(struct rds_conn_path *cp)
WARN_ON(delayed_work_pending(&cp->cp_recv_w));
WARN_ON(delayed_work_pending(&cp->cp_conn_w));
WARN_ON(work_pending(&cp->cp_down_w));
+}
+
+/* free a quiesced rds_conn_path's transport state and workqueue; runs
+ * from rds_conn_destroy_fini() once the last connection reference is
+ * dropped.
+ */
+static void rds_conn_path_free(struct rds_conn_path *cp)
+{
+ if (!cp->cp_transport_data)
+ return;
if (cp->cp_wq != rds_wq) {
destroy_workqueue(cp->cp_wq);
@@ -556,16 +569,52 @@ static void rds_conn_path_destroy(struct rds_conn_path *cp)
cp->cp_conn->c_trans->conn_free(cp->cp_transport_data);
}
+/* Free a connection. This runs from rds_conn_put() when the last
+ * reference is dropped, after rds_conn_destroy() has quiesced the
+ * connection and dropped the initial reference.
+ */
+static void rds_conn_destroy_fini(struct kref *kref)
+{
+ struct rds_connection *conn = container_of(kref, struct rds_connection,
+ c_refcount);
+ int npaths = (conn->c_trans->t_mp_capable ? RDS_MPATH_WORKERS : 1);
+ unsigned long flags;
+ int i;
+
+ for (i = 0; i < npaths; i++)
+ rds_conn_path_free(&conn->c_path[i]);
+
+ kfree(conn->c_path);
+ kmem_cache_free(rds_conn_slab, conn);
+
+ spin_lock_irqsave(&rds_conn_lock, flags);
+ rds_conn_count--;
+ spin_unlock_irqrestore(&rds_conn_lock, flags);
+}
+
+void rds_conn_get(struct rds_connection *conn)
+{
+ kref_get(&conn->c_refcount);
+}
+EXPORT_SYMBOL_GPL(rds_conn_get);
+
+void rds_conn_put(struct rds_connection *conn)
+{
+ kref_put(&conn->c_refcount, rds_conn_destroy_fini);
+}
+EXPORT_SYMBOL_GPL(rds_conn_put);
+
/*
* Stop and free a connection.
*
- * This can only be used in very limited circumstances. It assumes that once
- * the conn has been shutdown that no one else is referencing the connection.
- * We can only ensure this in the rmmod path in the current code.
+ * Quiesces the connection synchronously (workers cancelled, transport
+ * connections shut down, queued messages dropped) and drops the
+ * initial reference. The memory - including the transport's
+ * per-connection state and the path workqueues - is freed once the
+ * last rds_conn_put() runs, which may be after this returns.
*/
void rds_conn_destroy(struct rds_connection *conn)
{
- unsigned long flags;
int i;
struct rds_conn_path *cp;
int npaths = (conn->c_trans->t_mp_capable ? RDS_MPATH_WORKERS : 1);
@@ -579,11 +628,23 @@ void rds_conn_destroy(struct rds_connection *conn)
* sites (which all test rds_destroy_pending() under
* rcu_read_lock()) from queueing new work on the path
* workqueues once we start cancelling and destroying them.
+ *
+ * Now that the transport state stays discoverable (e.g. on the
+ * transports' connection lists) until the final rds_conn_put(),
+ * a conn can be handed to rds_conn_destroy() more than once -
+ * e.g. dropped for a protocol version mismatch and then found
+ * again at module unload. Only the first caller proceeds; the
+ * unhash also happens under rds_conn_lock, so a looked-up conn
+ * can never be quiesced twice.
*/
+ spin_lock_irq(&rds_conn_lock);
+ if (conn->c_destroy_in_prog) {
+ spin_unlock_irq(&rds_conn_lock);
+ return;
+ }
WRITE_ONCE(conn->c_destroy_in_prog, true);
/* Ensure conn will not be scheduled for reconnect */
- spin_lock_irq(&rds_conn_lock);
hlist_del_init_rcu(&conn->c_hash_node);
spin_unlock_irq(&rds_conn_lock);
synchronize_rcu();
@@ -591,7 +652,7 @@ void rds_conn_destroy(struct rds_connection *conn)
/* shut the connection down */
for (i = 0; i < npaths; i++) {
cp = &conn->c_path[i];
- rds_conn_path_destroy(cp);
+ rds_conn_path_quiesce(cp);
BUG_ON(!list_empty(&cp->cp_retrans));
}
@@ -602,12 +663,10 @@ void rds_conn_destroy(struct rds_connection *conn)
*/
rds_cong_remove_conn(conn);
- kfree(conn->c_path);
- kmem_cache_free(rds_conn_slab, conn);
-
- spin_lock_irqsave(&rds_conn_lock, flags);
- rds_conn_count--;
- spin_unlock_irqrestore(&rds_conn_lock, flags);
+ /* drop the initial reference; the connection is freed from
+ * rds_conn_destroy_fini() once every holder has dropped theirs
+ */
+ rds_conn_put(conn);
}
EXPORT_SYMBOL_GPL(rds_conn_destroy);
diff --git a/net/rds/rds.h b/net/rds/rds.h
index cede2b03baa5..e27e5e2e3329 100644
--- a/net/rds/rds.h
+++ b/net/rds/rds.h
@@ -137,6 +137,12 @@ struct rds_conn_path {
/* One rds_connection per RDS address pair */
struct rds_connection {
struct hlist_node c_hash_node;
+ /* Free of the connection memory (not the teardown of its
+ * transport state - that stays synchronous in
+ * rds_conn_destroy()) is deferred until the last reference is
+ * dropped via rds_conn_put().
+ */
+ struct kref c_refcount;
struct in6_addr c_laddr;
struct in6_addr c_faddr;
int c_dev_if; /* ifindex used for this conn */
@@ -822,6 +828,8 @@ struct rds_connection *rds_conn_create_outgoing(struct net *net,
u8 tos, gfp_t gfp, int dev_if);
void rds_conn_shutdown(struct rds_conn_path *cpath);
void rds_conn_destroy(struct rds_connection *conn);
+void rds_conn_get(struct rds_connection *conn);
+void rds_conn_put(struct rds_connection *conn);
void rds_conn_drop(struct rds_connection *conn);
void rds_conn_path_drop(struct rds_conn_path *cpath, bool destroy);
void rds_conn_connect_if_down(struct rds_connection *conn);
--
2.25.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH net-next 3/6] net/rds: hold connection references in lookup, sockets and c_passive
2026-09-04 7:02 [PATCH net-next 0/6] net/rds: make connection lifetime reference-counted Allison Henderson
2026-09-04 7:02 ` [PATCH net-next 1/6] net/rds: make rds_destroy_pending() cover single-connection destroy Allison Henderson
2026-09-04 7:02 ` [PATCH net-next 2/6] net/rds: split connection destroy into quiesce and kref-governed free Allison Henderson
@ 2026-09-04 7:02 ` Allison Henderson
2026-09-04 7:02 ` [PATCH net-next 4/6] net/rds: wait for connections to be freed on transport unload Allison Henderson
` (2 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Allison Henderson @ 2026-09-04 7:02 UTC (permalink / raw)
To: netdev, linux-rdma, pabeni, edumazet, kuba, horms; +Cc: achender, nicoyip.dev
From: Sharath Srinivasan <sharath.srinivasan@oracle.com>
Hand out real references everywhere a struct rds_connection pointer
previously escaped bare:
- rds_conn_lookup() takes a reference on the connection it returns
(kref_get_unless_zero(), skipping entries whose free has already
begun and that an RCU traversal may still encounter), and
__rds_conn_create() returns the connection with a reference held
for the caller on every path: lookup hit, fresh creation, lost
creation race, and the passive-loopback lookup, which now also
holds the parent while it dereferences parent->c_passive.
- The rs->rs_conn sendmsg cache owns a reference, which is dropped
when the cache is replaced or the socket is released.
rds_sendmsg() itself holds a reference for the duration of the
call, during which reads and updates of rs_conn are serialized by
rs_lock. So neither a concurrent rds_conn_destroy() nor another
sender replacing the cache can free the connection under a sender.
The connection may still be destroyed while a send is in flight -
when its device is removed or its netns is torn down - but it is
only quiesced; the free is held off by the sender's reference. A
cached connection whose destruction has begun is no longer reused.
Instead, sendmsg drops it and looks up or creates a live one, so a
socket cannot get stuck returning -EAGAIN forever against a
quiesced connection.
- parent->c_passive owns a reference, dropped when the parent is
destroyed.
Serializing the rs_conn cache under rs_lock also resolves a
syzbot-reported KCSAN data race between concurrent rds_sendmsg()
calls on the same socket, each installing the connection it created
into rs->rs_conn with a plain store:
BUG: KCSAN: data-race in rds_sendmsg / rds_sendmsg
write to 0xffff888101dec818 of 8 bytes by task 30904 on cpu 0:
rds_sendmsg+0xc1f/0x1580 net/rds/send.c:1332
write to 0xffff888101dec818 of 8 bytes by task 30905 on cpu 1:
rds_sendmsg+0xc1f/0x1580 net/rds/send.c:1332
value changed: 0x0000000000000000 -> 0xffff88811b61faf0
The cm_id->context back-pointers deliberately remain reference-free:
connection destroy tears down the cm_id before the connection can be
freed, so a CM callback can never see a stale context.
Based on Oracle UEK commits 2c8569e4c880 ("net/rds: Add krefs to
struct rds_connection") and 0e9e3a72b7f7 ("net/rds: rds_sendmsg must
use rs_conn only when not being destroyed").
Reported-by: syzbot+879c1877016972360186@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=879c1877016972360186
Signed-off-by: Sharath Srinivasan <sharath.srinivasan@oracle.com>
[achender: substantial reimplementation for net-next: upstream has no
conn reaper, per-conn workers hold no references (destroy cancels
them synchronously before the final put), and the sendmsg cache is
serialized with rs_lock instead of UEK's socket flag; rewrite commit
message]
Assisted-by: Claude-Code:claude-fable-5
Signed-off-by: Allison Henderson <achender@kernel.org>
---
net/rds/af_rds.c | 8 ++++++
net/rds/connection.c | 62 ++++++++++++++++++++++++++++++++++++++++++--
net/rds/ib_cm.c | 8 +++++-
net/rds/send.c | 42 ++++++++++++++++++++++++++----
net/rds/tcp_listen.c | 5 +++-
5 files changed, 116 insertions(+), 9 deletions(-)
diff --git a/net/rds/af_rds.c b/net/rds/af_rds.c
index d5defe9172e3..0eaa5b976572 100644
--- a/net/rds/af_rds.c
+++ b/net/rds/af_rds.c
@@ -80,6 +80,14 @@ static int rds_release(struct socket *sock)
rds_notify_queue_get(rs, NULL);
rds_notify_msg_zcopy_purge(&rs->rs_zcookie_queue);
+ /* drop the cached connection reference; no sendmsg can race
+ * with us here, the socket is going away
+ */
+ if (rs->rs_conn) {
+ rds_conn_put(rs->rs_conn);
+ rs->rs_conn = NULL;
+ }
+
spin_lock_bh(&rds_sock_lock);
list_del_init(&rs->rs_item);
spin_unlock_bh(&rds_sock_lock);
diff --git a/net/rds/connection.c b/net/rds/connection.c
index f45fd1fb1843..df26959b0fdc 100644
--- a/net/rds/connection.c
+++ b/net/rds/connection.c
@@ -79,7 +79,10 @@ static struct hlist_head *rds_conn_bucket(const struct in6_addr *laddr,
var |= RDS_INFO_CONNECTION_FLAG_##suffix; \
} while (0)
-/* rcu read lock must be held or the connection spinlock */
+/* rcu read lock must be held or the connection spinlock.
+ * On success a reference is taken on the returned connection; the
+ * caller must drop it with rds_conn_put().
+ */
static struct rds_connection *rds_conn_lookup(struct net *net,
struct hlist_head *head,
const struct in6_addr *laddr,
@@ -96,6 +99,13 @@ static struct rds_connection *rds_conn_lookup(struct net *net,
conn->c_tos == tos &&
net == rds_conn_net(conn) &&
conn->c_dev_if == dev_if) {
+ /* An entry whose refcount already dropped to
+ * zero has been unhashed and is about to be
+ * freed; an RCU traversal may still come
+ * across it. Treat it as absent.
+ */
+ if (!kref_get_unless_zero(&conn->c_refcount))
+ continue;
ret = conn;
break;
}
@@ -197,7 +207,14 @@ static struct rds_connection *__rds_conn_create(struct net *net,
* We need a second connection object into which we
* can stick the other QP. */
parent = conn;
+ /* The c_passive pointer holds a reference which is only
+ * dropped one synchronize_rcu() after the pointer is
+ * cleared, so within this RCU section a fetched pointer
+ * is always safe to take a reference on.
+ */
conn = parent->c_passive;
+ if (conn)
+ rds_conn_get(conn);
}
rcu_read_unlock();
if (conn)
@@ -316,12 +333,32 @@ static struct rds_connection *__rds_conn_create(struct net *net,
spin_lock_irqsave(&rds_conn_lock, flags);
if (parent) {
/* Creating passive conn */
- if (parent->c_passive) {
+ if (READ_ONCE(parent->c_destroy_in_prog)) {
+ /* The parent's destroy has begun (it sets the
+ * flag and snatches c_passive under this
+ * lock); do not install a new passive conn
+ * that nothing would ever destroy.
+ */
+ trans->conn_free(conn->c_path[0].cp_transport_data);
+ free_cp = conn->c_path;
+ kmem_cache_free(rds_conn_slab, conn);
+ conn = ERR_PTR(-ENETDOWN);
+ } else if (parent->c_passive) {
+ rds_conn_get(parent->c_passive);
trans->conn_free(conn->c_path[0].cp_transport_data);
free_cp = conn->c_path;
kmem_cache_free(rds_conn_slab, conn);
conn = parent->c_passive;
} else {
+ /* The initial reference belongs to whoever
+ * destroys the conn (the transport's conn
+ * lists, as for any other conn). Take one
+ * for the c_passive pointer - dropped when
+ * the parent is destroyed - and one for our
+ * caller.
+ */
+ rds_conn_get(conn); /* c_passive */
+ rds_conn_get(conn); /* caller */
parent->c_passive = conn;
rds_cong_add_conn(conn);
rds_conn_count++;
@@ -351,6 +388,10 @@ static struct rds_connection *__rds_conn_create(struct net *net,
} else {
conn->c_my_gen_num = rds_gen_num;
conn->c_peer_gen_num = 0;
+ /* the initial reference belongs to whoever
+ * destroys the conn; take one for our caller
+ */
+ rds_conn_get(conn);
hlist_add_head_rcu(&conn->c_hash_node, head);
rds_cong_add_conn(conn);
rds_conn_count++;
@@ -360,6 +401,8 @@ static struct rds_connection *__rds_conn_create(struct net *net,
rcu_read_unlock();
out:
+ if (parent)
+ rds_conn_put(parent);
if (free_cp) {
for (i = 0; i < npaths; i++)
if (free_cp[i].cp_wq != rds_wq)
@@ -616,6 +659,7 @@ EXPORT_SYMBOL_GPL(rds_conn_put);
void rds_conn_destroy(struct rds_connection *conn)
{
int i;
+ struct rds_connection *passive;
struct rds_conn_path *cp;
int npaths = (conn->c_trans->t_mp_capable ? RDS_MPATH_WORKERS : 1);
@@ -646,6 +690,16 @@ void rds_conn_destroy(struct rds_connection *conn)
/* Ensure conn will not be scheduled for reconnect */
hlist_del_init_rcu(&conn->c_hash_node);
+
+ /* Snatch c_passive while holding the lock:
+ * __rds_conn_create() dereferences it under rcu_read_lock()
+ * (and refuses to install a new one once c_destroy_in_prog is
+ * set, which it checks under this lock). After the
+ * synchronize_rcu() below no one can pick the pointer up any
+ * more and its reference can be dropped.
+ */
+ passive = conn->c_passive;
+ conn->c_passive = NULL;
spin_unlock_irq(&rds_conn_lock);
synchronize_rcu();
@@ -663,6 +717,10 @@ void rds_conn_destroy(struct rds_connection *conn)
*/
rds_cong_remove_conn(conn);
+ /* drop the reference our c_passive pointer held, if any */
+ if (passive)
+ rds_conn_put(passive);
+
/* drop the initial reference; the connection is freed from
* rds_conn_destroy_fini() once every holder has dropped theirs
*/
diff --git a/net/rds/ib_cm.c b/net/rds/ib_cm.c
index 4feb0edc360c..24e538c253a7 100644
--- a/net/rds/ib_cm.c
+++ b/net/rds/ib_cm.c
@@ -924,8 +924,14 @@ int rds_ib_cm_handle_connect(struct rdma_cm_id *cm_id,
rds_ib_conn_error(conn, "rdma_accept failed\n");
out:
- if (conn)
+ if (conn) {
mutex_unlock(&conn->c_cm_lock);
+ /* The conn stays reachable through cm_id->context
+ * without a reference of its own: connection destroy
+ * shuts the cm_id down before the conn is freed.
+ */
+ rds_conn_put(conn);
+ }
if (err)
rdma_reject(cm_id, &err, sizeof(int),
IB_CM_REJ_CONSUMER_DEFINED);
diff --git a/net/rds/send.c b/net/rds/send.c
index 1afa981e5c06..036a68372e2f 100644
--- a/net/rds/send.c
+++ b/net/rds/send.c
@@ -1159,13 +1159,14 @@ int rds_sendmsg(struct socket *sock, struct msghdr *msg, size_t payload_len)
DECLARE_SOCKADDR(struct sockaddr_in *, usin, msg->msg_name);
__be16 dport;
struct rds_message *rm = NULL;
- struct rds_connection *conn;
+ struct rds_connection *conn = NULL;
int ret = 0;
int queued = 0, allocated_mr = 0;
int nonblock = msg->msg_flags & MSG_DONTWAIT;
long timeo = sock_sndtimeo(sk, nonblock);
struct rds_conn_path *cpath;
struct in6_addr daddr;
+ unsigned long flags;
__u32 scope_id = 0;
size_t rdma_payload_len = 0;
bool zcopy = ((msg->msg_flags & MSG_ZEROCOPY) &&
@@ -1340,11 +1341,29 @@ int rds_sendmsg(struct socket *sock, struct msghdr *msg, size_t payload_len)
rm->m_daddr = daddr;
/* rds_conn_create has a spinlock that runs with IRQ off.
- * Caching the conn in the socket helps a lot. */
- if (rs->rs_conn && ipv6_addr_equal(&rs->rs_conn->c_faddr, &daddr) &&
- rs->rs_tos == rs->rs_conn->c_tos) {
- conn = rs->rs_conn;
+ * Caching the conn in the socket helps a lot.
+ *
+ * The cached rs_conn holds a connection reference; take one of
+ * our own for the duration of this call (dropped on both exit
+ * paths), so that neither a concurrent sender replacing the
+ * cache nor rds_conn_destroy() can free the connection under
+ * us. A cached connection whose destruction has begun is not
+ * reused: dropping it here lets the next sendmsg look up or
+ * create a live one instead of returning -EAGAIN forever.
+ */
+ spin_lock_irqsave(&rs->rs_lock, flags);
+ conn = rs->rs_conn;
+ if (conn && ipv6_addr_equal(&conn->c_faddr, &daddr) &&
+ rs->rs_tos == conn->c_tos && !rds_destroy_pending(conn)) {
+ rds_conn_get(conn);
} else {
+ conn = NULL;
+ }
+ spin_unlock_irqrestore(&rs->rs_lock, flags);
+
+ if (!conn) {
+ struct rds_connection *old;
+
conn = rds_conn_create_outgoing(sock_net(sock->sk),
&rs->rs_bound_addr, &daddr,
rs->rs_transport, rs->rs_tos,
@@ -1352,9 +1371,17 @@ int rds_sendmsg(struct socket *sock, struct msghdr *msg, size_t payload_len)
scope_id);
if (IS_ERR(conn)) {
ret = PTR_ERR(conn);
+ conn = NULL;
goto out;
}
+ /* hand the cache its own reference */
+ rds_conn_get(conn);
+ spin_lock_irqsave(&rs->rs_lock, flags);
+ old = rs->rs_conn;
rs->rs_conn = conn;
+ spin_unlock_irqrestore(&rs->rs_lock, flags);
+ if (old)
+ rds_conn_put(old);
}
if (conn->c_trans->t_mp_capable) {
@@ -1469,6 +1496,8 @@ int rds_sendmsg(struct socket *sock, struct msghdr *msg, size_t payload_len)
kfree(vct.vec[ind].iov);
kfree(vct.vec);
+ rds_conn_put(conn);
+
return payload_len;
out:
@@ -1476,6 +1505,9 @@ int rds_sendmsg(struct socket *sock, struct msghdr *msg, size_t payload_len)
kfree(vct.vec[ind].iov);
kfree(vct.vec);
+ if (conn)
+ rds_conn_put(conn);
+
/* If the user included a RDMA_MAP cmsg, we allocated a MR on the fly.
* If the sendmsg goes through, we keep the MR. If it fails with EAGAIN
* or in any other way, we need to destroy the MR again */
diff --git a/net/rds/tcp_listen.c b/net/rds/tcp_listen.c
index 13fa60c1985b..0d2ced892a8a 100644
--- a/net/rds/tcp_listen.c
+++ b/net/rds/tcp_listen.c
@@ -153,7 +153,7 @@ int rds_tcp_accept_one(struct rds_tcp_net *rtn)
{
struct socket *listen_sock = rtn->rds_tcp_listen_sock;
struct socket *new_sock = NULL;
- struct rds_connection *conn;
+ struct rds_connection *conn = NULL;
int ret;
struct inet_sock *inet;
struct rds_tcp_connection *rs_tcp = NULL;
@@ -229,6 +229,7 @@ int rds_tcp_accept_one(struct rds_tcp_net *rtn)
if (IS_ERR(conn)) {
ret = PTR_ERR(conn);
+ conn = NULL;
goto out;
}
/* An incoming SYN request came in, and TCP just accepted it.
@@ -343,6 +344,8 @@ int rds_tcp_accept_one(struct rds_tcp_net *rtn)
mutex_unlock(&rs_tcp->t_conn_path_lock);
if (new_sock)
sock_release(new_sock);
+ if (conn)
+ rds_conn_put(conn);
mutex_unlock(&rtn->rds_tcp_accept_lock);
--
2.25.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH net-next 4/6] net/rds: wait for connections to be freed on transport unload
2026-09-04 7:02 [PATCH net-next 0/6] net/rds: make connection lifetime reference-counted Allison Henderson
` (2 preceding siblings ...)
2026-09-04 7:02 ` [PATCH net-next 3/6] net/rds: hold connection references in lookup, sockets and c_passive Allison Henderson
@ 2026-09-04 7:02 ` Allison Henderson
2026-09-04 7:02 ` [PATCH net-next 5/6] net/rds: drop rds_conn_count in favor of t_conn_count Allison Henderson
2026-09-04 7:02 ` [PATCH net-next 6/6] net/rds: hold a connection reference from struct rds_incoming Allison Henderson
5 siblings, 0 replies; 7+ messages in thread
From: Allison Henderson @ 2026-09-04 7:02 UTC (permalink / raw)
To: netdev, linux-rdma, pabeni, edumazet, kuba, horms; +Cc: achender, nicoyip.dev
From: Sharath Srinivasan <sharath.srinivasan@oracle.com>
Since connection free became asynchronous, rds_conn_destroy() only
quiesces the connection; the actual free - including the transport's
conn_free, which lives in the transport module - runs when the last
reference is dropped. The transports' exit paths destroy all of
their connections and then proceed to unload, so a free that is still
pending (a racing lookup-style holder, or simply the destroyer's own
put not yet run when destroy was invoked from another context earlier)
would execute transport module code after that module's text is gone.
Count each transport's live connections in t_conn_count (incremented
when a connection is published in __rds_conn_create(), decremented as
the last step of rds_conn_destroy_fini()) and make the transport exit
paths - rds_ib_exit(), rds_tcp_exit() and rds_loop_exit() - wait for
the count to drop to zero after destroying their connections.
Sockets cannot keep the count elevated here: a bound socket holds a
module reference on its transport (rds_trans_get_preferred()), so a
transport cannot reach its exit path while any socket that could
cache one of its connections in rs_conn still exists. The remaining
holders are short-lived, hence the bounded wait; if it expires
anyway, warn - the pending frees will touch freed module text.
In rds_ib_exit(), tearing down the last connection can also drop the
final reference on a device, which defers rds_ib_dev_free() - again
this module's text - to rds_wq. Flush the workqueue once after the
connections are gone; rds_ib_dev_free() queues nothing further on
rds_wq, so a single pass drains it.
Based on Oracle UEK commits ece4b4e39afa ("net/rds: wait_event_timeout
until zero connections during rmmod") and 905ec90e6166 ("net/rds:
Each RDS transport should keep its own connection count").
Signed-off-by: Sharath Srinivasan <sharath.srinivasan@oracle.com>
[achender: reimplementation for net-next: t_conn_count did not exist
upstream and is introduced here; single global waitqueue instead of
per-transport (the loop transport never goes through
rds_trans_register()); also cover rds_loop_exit(); rewrite commit
message]
Assisted-by: Claude-Code:claude-fable-5
Signed-off-by: Allison Henderson <achender@kernel.org>
---
net/rds/connection.c | 29 +++++++++++++++++++++++++++++
net/rds/ib.c | 9 +++++++++
net/rds/loop.c | 2 ++
net/rds/rds.h | 9 +++++++++
net/rds/tcp.c | 1 +
5 files changed, 50 insertions(+)
diff --git a/net/rds/connection.c b/net/rds/connection.c
index df26959b0fdc..90d660a45662 100644
--- a/net/rds/connection.c
+++ b/net/rds/connection.c
@@ -48,6 +48,8 @@
/* converting this to RCU is a chore for another day.. */
static DEFINE_SPINLOCK(rds_conn_lock);
static unsigned long rds_conn_count;
+/* woken whenever a transport's t_conn_count drops to zero */
+static DECLARE_WAIT_QUEUE_HEAD(rds_conn_freed_waitq);
static struct hlist_head rds_conn_hash[RDS_CONNECTION_HASH_ENTRIES];
static struct kmem_cache *rds_conn_slab;
@@ -362,6 +364,7 @@ static struct rds_connection *__rds_conn_create(struct net *net,
parent->c_passive = conn;
rds_cong_add_conn(conn);
rds_conn_count++;
+ atomic_inc(&conn->c_trans->t_conn_count);
}
} else {
/* Creating normal conn */
@@ -395,6 +398,7 @@ static struct rds_connection *__rds_conn_create(struct net *net,
hlist_add_head_rcu(&conn->c_hash_node, head);
rds_cong_add_conn(conn);
rds_conn_count++;
+ atomic_inc(&conn->c_trans->t_conn_count);
}
}
spin_unlock_irqrestore(&rds_conn_lock, flags);
@@ -621,6 +625,7 @@ static void rds_conn_destroy_fini(struct kref *kref)
struct rds_connection *conn = container_of(kref, struct rds_connection,
c_refcount);
int npaths = (conn->c_trans->t_mp_capable ? RDS_MPATH_WORKERS : 1);
+ struct rds_transport *trans = conn->c_trans;
unsigned long flags;
int i;
@@ -633,7 +638,31 @@ static void rds_conn_destroy_fini(struct kref *kref)
spin_lock_irqsave(&rds_conn_lock, flags);
rds_conn_count--;
spin_unlock_irqrestore(&rds_conn_lock, flags);
+
+ /* only after everything the transport module owns has been
+ * freed above may its unload proceed
+ */
+ if (!atomic_dec_return(&trans->t_conn_count))
+ wake_up_all(&rds_conn_freed_waitq);
+}
+
+/* Wait for all of @trans's connections to be freed; the free runs
+ * asynchronously once rds_conn_destroy() has quiesced a connection.
+ * Called on transport module unload, after the transport destroyed
+ * all of its connections: anything still holding a connection
+ * reference at that point is a short-lived lookup-style holder, so
+ * a bounded wait suffices - but warn if it expires, since the frees
+ * that follow the unload will then touch freed module text.
+ */
+void rds_conn_wait_conns_freed(struct rds_transport *trans)
+{
+ if (!wait_event_timeout(rds_conn_freed_waitq,
+ !atomic_read(&trans->t_conn_count),
+ msecs_to_jiffies(RDS_CONN_FREE_TIMEOUT_MS)))
+ WARN(1, "RDS/%s: %d connection(s) not freed\n",
+ trans->t_name, atomic_read(&trans->t_conn_count));
}
+EXPORT_SYMBOL_GPL(rds_conn_wait_conns_freed);
void rds_conn_get(struct rds_connection *conn)
{
diff --git a/net/rds/ib.c b/net/rds/ib.c
index 9fe3b9951bd3..755690583325 100644
--- a/net/rds/ib.c
+++ b/net/rds/ib.c
@@ -538,6 +538,15 @@ void rds_ib_exit(void)
#endif
rds_ib_unregister_client();
rds_ib_destroy_nodev_conns();
+ rds_conn_wait_conns_freed(&rds_ib_transport);
+
+ /* Tearing down the last connection may have dropped the final
+ * reference on a device, deferring rds_ib_dev_free() to rds_wq.
+ * Drain it before the module goes away; it queues nothing
+ * further on rds_wq.
+ */
+ flush_workqueue(rds_wq);
+
rds_ib_sysctl_exit();
rds_ib_recv_exit();
rds_trans_unregister(&rds_ib_transport);
diff --git a/net/rds/loop.c b/net/rds/loop.c
index e6b0750bbeda..7daf8ed25d69 100644
--- a/net/rds/loop.c
+++ b/net/rds/loop.c
@@ -195,6 +195,8 @@ void rds_loop_exit(void)
WARN_ON(lc->conn->c_passive);
rds_conn_destroy(lc->conn);
}
+
+ rds_conn_wait_conns_freed(&rds_loop_transport);
}
static void rds_loop_kill_conns(struct net *net)
diff --git a/net/rds/rds.h b/net/rds/rds.h
index e27e5e2e3329..35760dd6b077 100644
--- a/net/rds/rds.h
+++ b/net/rds/rds.h
@@ -553,6 +553,12 @@ struct rds_transport {
unsigned int t_prefer_loopback:1,
t_mp_capable:1;
unsigned int t_type;
+ /* Connections of this transport not yet freed; freeing runs
+ * asynchronously once rds_conn_destroy() has quiesced a
+ * connection, so transport module unload has to wait for this
+ * to reach zero (rds_conn_wait_conns_freed()).
+ */
+ atomic_t t_conn_count;
int (*laddr_check)(struct net *net, const struct in6_addr *addr,
__u32 scope_id);
@@ -830,6 +836,9 @@ void rds_conn_shutdown(struct rds_conn_path *cpath);
void rds_conn_destroy(struct rds_connection *conn);
void rds_conn_get(struct rds_connection *conn);
void rds_conn_put(struct rds_connection *conn);
+/* how long transport unload waits for its connections to be freed */
+#define RDS_CONN_FREE_TIMEOUT_MS 10000
+void rds_conn_wait_conns_freed(struct rds_transport *trans);
void rds_conn_drop(struct rds_connection *conn);
void rds_conn_path_drop(struct rds_conn_path *cpath, bool destroy);
void rds_conn_connect_if_down(struct rds_connection *conn);
diff --git a/net/rds/tcp.c b/net/rds/tcp.c
index 774a71f88d37..2685ee21a22d 100644
--- a/net/rds/tcp.c
+++ b/net/rds/tcp.c
@@ -805,6 +805,7 @@ static void rds_tcp_exit(void)
#endif
unregister_pernet_device(&rds_tcp_net_ops);
rds_tcp_destroy_conns();
+ rds_conn_wait_conns_freed(&rds_tcp_transport);
rds_trans_unregister(&rds_tcp_transport);
rds_tcp_recv_exit();
kmem_cache_destroy(rds_tcp_conn_slab);
--
2.25.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH net-next 5/6] net/rds: drop rds_conn_count in favor of t_conn_count
2026-09-04 7:02 [PATCH net-next 0/6] net/rds: make connection lifetime reference-counted Allison Henderson
` (3 preceding siblings ...)
2026-09-04 7:02 ` [PATCH net-next 4/6] net/rds: wait for connections to be freed on transport unload Allison Henderson
@ 2026-09-04 7:02 ` Allison Henderson
2026-09-04 7:02 ` [PATCH net-next 6/6] net/rds: hold a connection reference from struct rds_incoming Allison Henderson
5 siblings, 0 replies; 7+ messages in thread
From: Allison Henderson @ 2026-09-04 7:02 UTC (permalink / raw)
To: netdev, linux-rdma, pabeni, edumazet, kuba, horms; +Cc: achender, nicoyip.dev
The previous patch gave each transport its own connection count in
t_conn_count, incremented and decremented at exactly the points where
the global rds_conn_count is. That leaves rds_conn_count with a
single remaining consumer: the seed of the per-path workqueue names
in __rds_conn_create().
Switch the name seed to t_conn_count, as UEK does, and remove
rds_conn_count. The numbering becomes per-transport instead of
global, so connections of different transports can now receive the
same seed; workqueue names carry no uniqueness requirement, and the
seed was already reused as the count rose and fell. Removing the
counter also removes the rds_conn_lock round-trip that
rds_conn_destroy_fini() took solely to decrement it, leaving the
free path lock-free.
Based on Oracle UEK commit 905ec90e6166 ("net/rds: Each RDS transport
should keep its own connection count").
Assisted-by: Claude-Code:claude-fable-5
Signed-off-by: Allison Henderson <achender@kernel.org>
---
net/rds/connection.c | 13 +++----------
1 file changed, 3 insertions(+), 10 deletions(-)
diff --git a/net/rds/connection.c b/net/rds/connection.c
index 90d660a45662..2c8ca54a91ca 100644
--- a/net/rds/connection.c
+++ b/net/rds/connection.c
@@ -47,7 +47,6 @@
/* converting this to RCU is a chore for another day.. */
static DEFINE_SPINLOCK(rds_conn_lock);
-static unsigned long rds_conn_count;
/* woken whenever a transport's t_conn_count drops to zero */
static DECLARE_WAIT_QUEUE_HEAD(rds_conn_freed_waitq);
static struct hlist_head rds_conn_hash[RDS_CONNECTION_HASH_ENTRIES];
@@ -302,8 +301,9 @@ static struct rds_connection *__rds_conn_create(struct net *net,
is_outgoing);
conn->c_path[i].cp_index = i;
conn->c_path[i].cp_wq =
- alloc_ordered_workqueue("krds_cp_wq#%lu/%d", 0,
- rds_conn_count, i);
+ alloc_ordered_workqueue("krds_cp_wq#%d/%d", 0,
+ atomic_read(&trans->t_conn_count),
+ i);
if (!conn->c_path[i].cp_wq)
conn->c_path[i].cp_wq = rds_wq;
}
@@ -363,7 +363,6 @@ static struct rds_connection *__rds_conn_create(struct net *net,
rds_conn_get(conn); /* caller */
parent->c_passive = conn;
rds_cong_add_conn(conn);
- rds_conn_count++;
atomic_inc(&conn->c_trans->t_conn_count);
}
} else {
@@ -397,7 +396,6 @@ static struct rds_connection *__rds_conn_create(struct net *net,
rds_conn_get(conn);
hlist_add_head_rcu(&conn->c_hash_node, head);
rds_cong_add_conn(conn);
- rds_conn_count++;
atomic_inc(&conn->c_trans->t_conn_count);
}
}
@@ -626,7 +624,6 @@ static void rds_conn_destroy_fini(struct kref *kref)
c_refcount);
int npaths = (conn->c_trans->t_mp_capable ? RDS_MPATH_WORKERS : 1);
struct rds_transport *trans = conn->c_trans;
- unsigned long flags;
int i;
for (i = 0; i < npaths; i++)
@@ -635,10 +632,6 @@ static void rds_conn_destroy_fini(struct kref *kref)
kfree(conn->c_path);
kmem_cache_free(rds_conn_slab, conn);
- spin_lock_irqsave(&rds_conn_lock, flags);
- rds_conn_count--;
- spin_unlock_irqrestore(&rds_conn_lock, flags);
-
/* only after everything the transport module owns has been
* freed above may its unload proceed
*/
--
2.25.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH net-next 6/6] net/rds: hold a connection reference from struct rds_incoming
2026-09-04 7:02 [PATCH net-next 0/6] net/rds: make connection lifetime reference-counted Allison Henderson
` (4 preceding siblings ...)
2026-09-04 7:02 ` [PATCH net-next 5/6] net/rds: drop rds_conn_count in favor of t_conn_count Allison Henderson
@ 2026-09-04 7:02 ` Allison Henderson
5 siblings, 0 replies; 7+ messages in thread
From: Allison Henderson @ 2026-09-04 7:02 UTC (permalink / raw)
To: netdev, linux-rdma, pabeni, edumazet, kuba, horms; +Cc: achender, nicoyip.dev
From: Sharath Srinivasan <sharath.srinivasan@oracle.com>
struct rds_incoming->i_conn stores a pointer to the connection a message
it belongs to, for both received messages and messages the socket sends.
But without taking a reference, nothing keeps that connection alive.
Embedded as the messages m_inc, an inc routinely outlives the connection
it points at, by sitting in the socket's receive queue until the
application reads it, while the connection is destroyed by device removal,
netns teardown or module unload - and every dereference of i_conn after
that point touches freed memory.
Chengfeng Ye reported one way to reach it, where the socket info
callbacks walk a receive queue after rmmod freed the connections:
BUG: KASAN: slab-use-after-free in rds6_inc_info_copy+0x459/0x530 [rds]
Read of size 1 at addr ffff888106031c50 by task poc/101
Call Trace:
rds6_inc_info_copy+0x459/0x530 [rds]
rds6_sock_inc_info+0x2b9/0x3c0 [rds]
rds_info_getsockopt+0x19d/0x380 [rds]
do_sock_getsockopt+0x2ac/0x480
__sys_getsockopt+0x128/0x210
Freed by task 102:
kmem_cache_free+0x1b5/0x3d0
rds_conn_destroy+0x484/0x600 [rds]
rds_loop_exit_net+0x32/0x50 [rds]
unregister_pernet_device+0x2c/0x50
rds_conn_exit+0x13/0xa0 [rds]
rds_exit+0x1a/0xc40 [rds]
__do_sys_delete_module+0x30a/0x4d0
Closing the socket gets there too, with no reader of i_conn other than
RDS itself: freeing an IB inc dereferences i_conn to hand the inc and
its fragments back to the connection's recycle cache, so draining the
receive queue of a socket whose connection is gone crashes in the
transport:
panic
...
rds_ib_recv_cache_put (net/rds/ib_recv.c:703)
rds_ib_inc_free (net/rds/ib_recv.c:207)
rds_clear_recv_queue (net/rds/recv.c:909)
rds_release (net/rds/af_rds.c:212)
__sock_release (net/socket.c:649)
sock_close (net/socket.c:1336)
with the freed connection confirmed by its now-zero reference count:
-trace[9]["inc"].i_conn.c_refcount
(struct kref){
.refcount = (refcount_t){
.refs = (atomic_t){
.counter = (int)0,
},
},
}
Now that connections are reference counted, make every holder of i_conn
own a reference. The pointer is assigned in six places - rds_inc_init()
and rds_inc_path_init() for received messages, rds_recv_incoming() when
it re-points an inc at the connection it arrived on, and
rds_send_queue_rm(), rds_send_probe() and the congestion-map path of
rds_send_xmit() for m_inc - and each of them now takes a reference. The
references are dropped from rds_inc_put() and rds_message_put(), which
are the points where the last user of the pointer goes away.
rds_recv_incoming() takes the new reference before dropping the old one,
so re-pointing an inc at the connection it already refers to cannot free
it. rds_inc_put() drops its reference through a local copy, since
inc_free() may free the memory the inc lives in.
This keeps a connection allocated for as long as messages that arrived
over it are queued on sockets, which is longer than before but costs
only the connection's memory: rds_conn_destroy() still quiesces the
connection synchronously, so a lingering inc holds nothing running.
The final rds_conn_put() runs the free path, which destroys the per-path
workqueues and therefore may sleep, so the last reference has to be
dropped from process context. It always is. While a connection is
alive its hash-table entry holds the initial reference, so a put from a
completion handler or tasklet can never be the last one; that reference
is dropped by rds_conn_destroy(), from process context, after the
connection has been quiesced and its queued messages freed. The
references that survive that point are the ones held by incs on socket
receive queues and by messages on socket send queues, and those are
dropped from recvmsg and from close - process context in both cases.
This is not a stable candidate: reaching the use-after-free requires
freeing a connection out from under a live socket, which needs
CAP_SYS_MODULE, netns teardown or physical device removal, and the fix
depends on the connection reference counting introduced earlier in this
series.
Based on Oracle UEK commit 99b9a3715419 ("net/rds: fix crash by
expanding kref coverage to rds_incoming.i_conn").
Reported-by: Chengfeng Ye <nicoyip.dev@gmail.com>
Closes: https://lore.kernel.org/netdev/20260720184955.3008978-1-nicoyip.dev@gmail.com/
Signed-off-by: Sharath Srinivasan <sharath.srinivasan@oracle.com>
Signed-off-by: Samasth Norway Ananda <samasth.norway.ananda@oracle.com>
[achender: port to net-next: same six assignment sites, but upstream
splits the message free across rds_message_unpin_worker(), so the
m_inc reference is dropped from a shared rds_message_free() helper
that both paths call; rds_recv_incoming() takes the new reference
before dropping the old; rewrite commit message]
Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Allison Henderson <achender@kernel.org>
---
net/rds/message.c | 16 ++++++++++++++--
net/rds/recv.c | 21 ++++++++++++++++++++-
net/rds/send.c | 3 +++
3 files changed, 37 insertions(+), 3 deletions(-)
diff --git a/net/rds/message.c b/net/rds/message.c
index f25f2592586f..29e95028e61e 100644
--- a/net/rds/message.c
+++ b/net/rds/message.c
@@ -182,6 +182,18 @@ static void rds_message_purge(struct rds_message *rm)
kref_put(&rm->atomic.op_rdma_mr->r_kref, __rds_put_mr_final);
}
+static void rds_message_free(struct rds_message *rm)
+{
+ /* get in rds_send_queue_rm(), rds_send_probe() or the congestion
+ * map path of rds_send_xmit(). Messages that were never queued on
+ * a connection have no reference to drop.
+ */
+ if (rm->m_inc.i_conn)
+ rds_conn_put(rm->m_inc.i_conn);
+
+ kfree(rm);
+}
+
static void rds_message_unpin_worker(struct work_struct *work)
{
struct rds_message *rm = container_of(work, struct rds_message,
@@ -192,7 +204,7 @@ static void rds_message_unpin_worker(struct work_struct *work)
if (rm->atomic.op_unpin_deferred)
rds_atomic_op_unpin_page(&rm->atomic);
- kfree(rm);
+ rds_message_free(rm);
}
void rds_message_put(struct rds_message *rm)
@@ -217,7 +229,7 @@ void rds_message_put(struct rds_message *rm)
return;
}
- kfree(rm);
+ rds_message_free(rm);
}
}
EXPORT_SYMBOL_GPL(rds_message_put);
diff --git a/net/rds/recv.c b/net/rds/recv.c
index 6204e577a90a..b031c0b43af8 100644
--- a/net/rds/recv.c
+++ b/net/rds/recv.c
@@ -46,6 +46,7 @@ void rds_inc_init(struct rds_incoming *inc, struct rds_connection *conn,
{
refcount_set(&inc->i_refcount, 1);
INIT_LIST_HEAD(&inc->i_item);
+ rds_conn_get(conn); /* put in rds_inc_put() */
inc->i_conn = conn;
inc->i_conn_path = NULL;
inc->i_saddr = *saddr;
@@ -61,6 +62,7 @@ void rds_inc_path_init(struct rds_incoming *inc, struct rds_conn_path *cp,
{
refcount_set(&inc->i_refcount, 1);
INIT_LIST_HEAD(&inc->i_item);
+ rds_conn_get(cp->cp_conn); /* put in rds_inc_put() */
inc->i_conn = cp->cp_conn;
inc->i_conn_path = cp;
inc->i_saddr = *saddr;
@@ -81,9 +83,19 @@ void rds_inc_put(struct rds_incoming *inc)
{
rdsdebug("put inc %p ref %d\n", inc, refcount_read(&inc->i_refcount));
if (refcount_dec_and_test(&inc->i_refcount)) {
+ struct rds_connection *conn = inc->i_conn;
+
BUG_ON(!list_empty(&inc->i_item));
- inc->i_conn->c_trans->inc_free(inc);
+ /* inc_free() can free the memory @inc lives in, so the
+ * connection reference has to be dropped through the
+ * copy taken above.
+ */
+ conn->c_trans->inc_free(inc);
+ /* get in rds_inc_init(), rds_inc_path_init() or
+ * rds_recv_incoming()
+ */
+ rds_conn_put(conn);
}
}
EXPORT_SYMBOL_GPL(rds_inc_put);
@@ -325,6 +337,13 @@ void rds_recv_incoming(struct rds_connection *conn, struct in6_addr *saddr,
unsigned long flags;
struct rds_conn_path *cp;
+ /* every caller initialized @inc with rds_inc_init() or
+ * rds_inc_path_init() first, so i_conn already holds a reference.
+ * Take the new one before dropping the old, so that re-pointing an
+ * inc at the connection it already refers to cannot free it.
+ */
+ rds_conn_get(conn);
+ rds_conn_put(inc->i_conn);
inc->i_conn = conn;
inc->i_rx_jiffies = jiffies;
if (conn->c_trans->t_mp_capable)
diff --git a/net/rds/send.c b/net/rds/send.c
index 036a68372e2f..045eb3c3f977 100644
--- a/net/rds/send.c
+++ b/net/rds/send.c
@@ -290,6 +290,7 @@ int rds_send_xmit(struct rds_conn_path *cp)
}
rm->data.op_active = 1;
rm->m_inc.i_conn_path = cp;
+ rds_conn_get(cp->cp_conn); /* put in rds_message_put() */
rm->m_inc.i_conn = cp->cp_conn;
cp->cp_xmit_rm = rm;
@@ -947,6 +948,7 @@ static int rds_send_queue_rm(struct rds_sock *rs, struct rds_connection *conn,
/* The code ordering is a little weird, but we're
trying to minimize the time we hold c_lock */
rds_message_populate_header(&rm->m_inc.i_hdr, sport, dport, 0);
+ rds_conn_get(conn); /* put in rds_message_put() */
rm->m_inc.i_conn = conn;
rm->m_inc.i_conn_path = cp;
rds_message_addref(rm);
@@ -1554,6 +1556,7 @@ rds_send_probe(struct rds_conn_path *cp, __be16 sport,
list_add_tail(&rm->m_conn_item, &cp->cp_send_queue);
set_bit(RDS_MSG_ON_CONN, &rm->m_flags);
rds_message_addref(rm);
+ rds_conn_get(cp->cp_conn); /* put in rds_message_put() */
rm->m_inc.i_conn = cp->cp_conn;
rm->m_inc.i_conn_path = cp;
--
2.25.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-09-04 7:02 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-04 7:02 [PATCH net-next 0/6] net/rds: make connection lifetime reference-counted Allison Henderson
2026-09-04 7:02 ` [PATCH net-next 1/6] net/rds: make rds_destroy_pending() cover single-connection destroy Allison Henderson
2026-09-04 7:02 ` [PATCH net-next 2/6] net/rds: split connection destroy into quiesce and kref-governed free Allison Henderson
2026-09-04 7:02 ` [PATCH net-next 3/6] net/rds: hold connection references in lookup, sockets and c_passive Allison Henderson
2026-09-04 7:02 ` [PATCH net-next 4/6] net/rds: wait for connections to be freed on transport unload Allison Henderson
2026-09-04 7:02 ` [PATCH net-next 5/6] net/rds: drop rds_conn_count in favor of t_conn_count Allison Henderson
2026-09-04 7:02 ` [PATCH net-next 6/6] net/rds: hold a connection reference from struct rds_incoming Allison Henderson
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox