On Tue, 2026-02-24 at 16:16 +0000, Simon Horman wrote:
> This is an AI-generated review of your patch. The human sending this
> email has considered the AI review valid, or at least plausible.
> 
> For local reproduction steps see 
> https://urldefense.com/v3/__https://netdev-ai.bots.linux.dev/ai-local.html__;!!ACWV5N9M2RV99hQ!Ki9pK6KFAOgmcbdBBDA6lG7dXM_7sJiBAcp2aRSeDi8bRG9ACJajfiLdK-YgNvXyOPqrJpC5y7k9fq6huS8$
>  
> ---
> net/rds: Refactor __rds_conn_create for blocking transport cleanup
> 
> The next patch will delegate fanout operations to a background worker,
> which adds cancel_work_sync() to rds_tcp_conn_free(). This is called
> during a connection cleanup and requires an operations to be blocking.
> However, the error path of __rds_conn_create() currently calls
> trans->conn_free() while holding rds_conn_lock (spinlock) and
> rcu_read_lock, which creates an atomic context where cancel_work_sync()
> cannot sleep.
> 
> Link: 
> https://urldefense.com/v3/__https://ci.syzbot.org/series/1a5ef180-c02c-401d-9df7-670b18570a55__;!!ACWV5N9M2RV99hQ!Ki9pK6KFAOgmcbdBBDA6lG7dXM_7sJiBAcp2aRSeDi8bRG9ACJajfiLdK-YgNvXyOPqrJpC5y7k9noDt20w$
>  
> 
> > diff --git a/net/rds/connection.c b/net/rds/connection.c
> > --- a/net/rds/connection.c
> > +++ b/net/rds/connection.c
> > @@ -170,6 +170,7 @@ static struct rds_connection *__rds_conn_create(struct 
> > net *net,
> >     struct hlist_head *head = rds_conn_bucket(laddr, faddr);
> >     struct rds_transport *loop_trans;
> >     struct rds_conn_path *free_cp = NULL;
> > +   struct rds_transport *free_trans = NULL;
> >     unsigned long flags;
> >     int ret, i;
> >     int npaths = (trans->t_mp_capable ? RDS_MPATH_WORKERS : 1);
> > @@ -305,7 +306,7 @@
> >     if (parent) {
> >             /* Creating passive conn */
> >             if (parent->c_passive) {
> > -                   trans->conn_free(conn->c_path[0].cp_transport_data);
> > +                   free_trans = trans;
> >                     free_cp = conn->c_path;
> >                     kmem_cache_free(rds_conn_slab, conn);
> >                     conn = parent->c_passive;
> 
> [ ... ]
> 
> > @@ -321,18 +322,7 @@
> >             found = rds_conn_lookup(net, head, laddr, faddr, trans,
> >                                     tos, dev_if);
> >             if (found) {
> > -                   struct rds_conn_path *cp;
> > -                   int i;
> > -
> > -                   for (i = 0; i < npaths; i++) {
> > -                           cp = &conn->c_path[i];
> > -                           /* The ->conn_alloc invocation may have
> > -                            * allocated resource for all paths, so all
> > -                            * of them may have to be freed here.
> > -                            */
> > -                           if (cp->cp_transport_data)
> > -                                   trans->conn_free(cp->cp_transport_data);
> > -                   }
> > +                   free_trans = trans;
> >                     free_cp = conn->c_path;
> >                     kmem_cache_free(rds_conn_slab, conn);
> >                     conn = found;
> 
> Does this create a use-after-free in other threads that iterate the
> transport list?
> 
> Looking at the race-loss path when rds_conn_lookup() finds an existing
> connection, the old code called trans->conn_free() before
> kmem_cache_free(conn). Now the patch defers conn_free() to after
> locks are released at the out: label, but kmem_cache_free(conn) still
> executes inside the critical section (line 327 after this change).
> 
> This creates a window where rds_tcp_connection objects remain on
> rds_tcp_conn_list with tc->t_cpath->cp_conn pointing to the
> already-freed conn object.
> 
> The timeline would be:
> 
> CPU 0 (__rds_conn_create race-loss)
>   spin_lock_irqsave(&rds_conn_lock)
>   found = rds_conn_lookup()  // finds existing conn
>   free_trans = trans
>   free_cp = conn->c_path
>   kmem_cache_free(rds_conn_slab, conn)  // conn freed
>   conn = found
>   spin_unlock_irqrestore(&rds_conn_lock)
>   rcu_read_unlock()
>   // ... later at out: label
>   free_trans->conn_free(...)  // removes tc from list, but too late
> 
> CPU 1 (rds_tcp_kill_sock)
>   spin_lock_irq(&rds_tcp_conn_lock)
>   list_for_each_entry_safe(tc, _tc, &rds_tcp_conn_list, t_tcp_node)
>     c_net = read_pnet(&tc->t_cpath->cp_conn->c_net)  // UAF here!
> 
> The commit message states "connections that 'lose' the race are safe to
> clean up outside the critical section since they were never added to
> the hashtable, and therefore are inaccessible to other threads."
> 
> However, while the rds_connection was never added to the RDS hashtable,
> the transport data (tc) WAS added to rds_tcp_conn_list during
> rds_tcp_conn_alloc(). Looking at that function in net/rds/tcp.c:
> 
> rds_tcp_conn_alloc() {
>     // ... allocate tc structures ...
>     spin_lock_irq(&rds_tcp_conn_lock);
>     for (i = 0; i < RDS_MPATH_WORKERS; i++) {
>         tc = conn->c_path[i].cp_transport_data;
>         tc->t_tcp_node_detached = false;
>         list_add_tail(&tc->t_tcp_node, &rds_tcp_conn_list);
>     }
>     spin_unlock_irq(&rds_tcp_conn_lock);
> }
> 
> So the tc objects are accessible to other threads via the transport
> list, even though the conn itself never made it into the RDS hashtable.
> 
> Functions that iterate rds_tcp_conn_list include:
> - rds_tcp_kill_sock() at net/rds/tcp.c:610
> - rds_tcp_destroy_conns()
> - rds_tcp_sysctl_reset()
> 
> All of these dereference tc->t_cpath->cp_conn during iteration.
> 
> In the old code, conn_free() ran BEFORE kmem_cache_free(conn), so the tc
> was removed from the list before conn was freed. No UAF window existed.
> 
> Could the kmem_cache_free(rds_conn_slab, conn) be moved to after
> conn_free() at the out: label, or should conn_free() be called before
> kmem_cache_free() inside the critical section for the race-loss paths?
I think it probably needs to go after the the out label.  Will update.  Thank 
you!

Allison

> 
> > @@ -349,9 +339,23 @@
> > 
> >  out:
> >     if (free_cp) {
> > -           for (i = 0; i < npaths; i++)
> > +           for (i = 0; i < npaths; i++) {
> > +                   /*
> > +                    * The trans->conn_alloc call may have allocated
> > +                    * resources for the cp paths, which will need to
> > +                    * be freed before freeing cp itself.  We do this here
> > +                    * after the out: label so it runs outside the
> > +                    * rds_conn_lock spinlock and rcu_read_lock section,
> > +                    * since both destroy_workqueue() and conn_free can
> > +                    * block. The local free_trans pointer is only set in
> > +                    * the race-loss paths where conn_alloc() succeeded.
> > +                    */
> > +                   if (free_trans && free_cp[i].cp_transport_data)
> > +                           free_trans->conn_free
> > +                                   (free_cp[i].cp_transport_data);
> >                     if (free_cp[i].cp_wq != rds_wq)
> >                             destroy_workqueue(free_cp[i].cp_wq);
> > +           }
> >             kfree(free_cp);
> >     }

Reply via email to