Netdev List
 help / color / mirror / Atom feed
* [PATCH] net: bridge: mcast: don't truncate the port group walk on teardown
@ 2026-08-12 11:34 Jun Yang
  2026-08-12 12:17 ` Nikolay Aleksandrov
  0 siblings, 1 reply; 3+ messages in thread
From: Jun Yang @ 2026-08-12 11:34 UTC (permalink / raw)
  To: Nikolay Aleksandrov, Ido Schimmel
  Cc: stable, Jun Yang, TencentOS Corvus AI, David S. Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman, bridge,
	netdev, linux-kernel

__br_multicast_disable_port_ctx() and br_multicast_del_port() walk
port->mglist with hlist_for_each_entry_safe(), which only guarantees that
the *current* node may be removed by the loop body.

The body is br_multicast_find_del_pg() -> br_multicast_del_pg(), and that
deletes further port groups of the very same port. br_multicast_del_pg()
drops the group's sources, and br_multicast_fwd_src_remove()
(net/bridge/br_multicast.c:583) deletes the (S,G) port group installed on
that same port; br_multicast_star_g_handle_mode() -> __fwd_del_star_excl()
(net/bridge/br_multicast.c:330) deletes the automatically installed
MDB_PG_FLAGS_STAR_EXCL entries, again on that same port. All of those sit
on the same port->mglist.

When one of them happens to be the node the iterator already latched as
"next", hlist_del_init() clears its ->next, the walk sees NULL and stops.
Every port group after it is silently left on the port. port->mglist is
head-inserted, so this needs the cascade victim to be older than the (*,G)
entry owning the source - a user-added, non-permanent (S,G) MDB entry added
before the (*,G) join produces exactly that ordering.

Hitting it once truncates the disable walk in
__br_multicast_disable_port_ctx() and once more truncates the flush in
br_multicast_del_port(), so del_nbp() goes on to free the port with port
groups still on port->mglist - and still linked in the bridge's mdb, with a
dangling ->key.port. Any subsequent mdb dump reads the freed port:

  BUG: KASAN: slab-use-after-free in __mdb_fill_info+0x1191/0x1320
  Read of size 8 at addr ffff88803065d008 by task bridge/9527
   __mdb_fill_info+0x1191/0x1320
   br_mdb_dump+0x594/0xe40
   rtnl_mdb_dump+0x1cf/0x5d0
  Freed by task 0:
   kfree+0x265/0x740
   kobject_put+0x212/0x6a0
   rcu_core+0x5c6/0x1140
  Last potentially related work creation:
   __call_rcu_common.constprop.0+0xb7/0x9e0
   br_del_if+0xdd/0x260

Don't rely on the pre-latched next pointer. br_multicast_del_port() deletes
everything, so just take the current list head each round. The filtered
walk in __br_multicast_disable_port_ctx() keeps its iterator but restarts
whenever the latched node has left the list; port groups are only freed by
the multicast GC work, which takes br->multicast_lock, so the node is still
valid memory for that check.

Fixes: b08123684bd5 ("net: bridge: mcast: install S,G entries automatically based on reports")
Cc: stable@vger.kernel.org
Reported-by: TencentOS Corvus AI <corvus@tencent.com>
Assisted-by: tencentos-corvus-ai:kimi-k3
Signed-off-by: Jun Yang <junvyyang@tencent.com>
---
A KASAN reproducer for this issue is available if requested.

 net/bridge/br_multicast.c | 33 +++++++++++++++++++++++++--------
 1 file changed, 25 insertions(+), 8 deletions(-)

diff --git a/net/bridge/br_multicast.c b/net/bridge/br_multicast.c
index 00aa9b2879d6..624dfca4066b 100644
--- a/net/bridge/br_multicast.c
+++ b/net/bridge/br_multicast.c
@@ -2065,12 +2065,17 @@ void br_multicast_del_port(struct net_bridge_port *port)
 {
 	struct net_bridge *br = port->br;
 	struct net_bridge_port_group *pg;
-	struct hlist_node *n;
 
-	/* Take care of the remaining groups, only perm ones should be left */
+	/* Take care of the remaining groups, only perm ones should be left.
+	 * Deleting one can delete others on this same port->mglist, so
+	 * always restart from the head.
+	 */
 	spin_lock_bh(&br->multicast_lock);
-	hlist_for_each_entry_safe(pg, n, &port->mglist, mglist)
+	while (!hlist_empty(&port->mglist)) {
+		pg = hlist_entry(port->mglist.first,
+				 struct net_bridge_port_group, mglist);
 		br_multicast_find_del_pg(br, pg);
+	}
 	spin_unlock_bh(&br->multicast_lock);
 	flush_work(&br->mcast_gc_work);
 	br_multicast_port_ctx_deinit(&port->multicast_ctx);
@@ -2126,11 +2132,23 @@ static void __br_multicast_disable_port_ctx(struct net_bridge_mcast_port *pmctx)
 	struct hlist_node *n;
 	bool del = false;
 
-	hlist_for_each_entry_safe(pg, n, &pmctx->port->mglist, mglist)
-		if (!(pg->flags & MDB_PG_FLAGS_PERMANENT) &&
-		    (!br_multicast_port_ctx_is_vlan(pmctx) ||
-		     pg->key.addr.vid == pmctx->vlan->vid))
-			br_multicast_find_del_pg(pmctx->port->br, pg);
+	/* br_multicast_find_del_pg() can delete further entries of this same
+	 * port->mglist, so the node latched in @n may be unlinked by the loop
+	 * body. Port groups are only freed by the GC work under multicast_lock,
+	 * so @n is still valid here; if it left the list, restart.
+	 */
+restart:
+	hlist_for_each_entry_safe(pg, n, &pmctx->port->mglist, mglist) {
+		if ((pg->flags & MDB_PG_FLAGS_PERMANENT) ||
+		    (br_multicast_port_ctx_is_vlan(pmctx) &&
+		     pg->key.addr.vid != pmctx->vlan->vid))
+			continue;
+
+		br_multicast_find_del_pg(pmctx->port->br, pg);
+
+		if (n && hlist_unhashed(n))
+			goto restart;
+	}
 
 	del |= br_ip4_multicast_rport_del(pmctx);
 	timer_delete(&pmctx->ip4_mc_router_timer);
-- 
2.55.0


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

* Re: [PATCH] net: bridge: mcast: don't truncate the port group walk on teardown
  2026-08-12 11:34 [PATCH] net: bridge: mcast: don't truncate the port group walk on teardown Jun Yang
@ 2026-08-12 12:17 ` Nikolay Aleksandrov
  2026-08-12 12:21   ` Nikolay Aleksandrov
  0 siblings, 1 reply; 3+ messages in thread
From: Nikolay Aleksandrov @ 2026-08-12 12:17 UTC (permalink / raw)
  To: Jun Yang, Ido Schimmel
  Cc: stable, Jun Yang, TencentOS Corvus AI, David S. Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman, bridge,
	netdev, linux-kernel

On 12/08/2026 14:34, Jun Yang wrote:
> __br_multicast_disable_port_ctx() and br_multicast_del_port() walk
> port->mglist with hlist_for_each_entry_safe(), which only guarantees that
> the *current* node may be removed by the loop body.
> 
> The body is br_multicast_find_del_pg() -> br_multicast_del_pg(), and that
> deletes further port groups of the very same port. br_multicast_del_pg()
> drops the group's sources, and br_multicast_fwd_src_remove()
> (net/bridge/br_multicast.c:583) deletes the (S,G) port group installed on
> that same port; br_multicast_star_g_handle_mode() -> __fwd_del_star_excl()
> (net/bridge/br_multicast.c:330) deletes the automatically installed
> MDB_PG_FLAGS_STAR_EXCL entries, again on that same port. All of those sit
> on the same port->mglist.
> 
> When one of them happens to be the node the iterator already latched as
> "next", hlist_del_init() clears its ->next, the walk sees NULL and stops.
> Every port group after it is silently left on the port. port->mglist is
> head-inserted, so this needs the cascade victim to be older than the (*,G)
> entry owning the source - a user-added, non-permanent (S,G) MDB entry added
> before the (*,G) join produces exactly that ordering.
> 
> Hitting it once truncates the disable walk in
> __br_multicast_disable_port_ctx() and once more truncates the flush in
> br_multicast_del_port(), so del_nbp() goes on to free the port with port
> groups still on port->mglist - and still linked in the bridge's mdb, with a
> dangling ->key.port. Any subsequent mdb dump reads the freed port:
> 
>    BUG: KASAN: slab-use-after-free in __mdb_fill_info+0x1191/0x1320
>    Read of size 8 at addr ffff88803065d008 by task bridge/9527
>     __mdb_fill_info+0x1191/0x1320
>     br_mdb_dump+0x594/0xe40
>     rtnl_mdb_dump+0x1cf/0x5d0
>    Freed by task 0:
>     kfree+0x265/0x740
>     kobject_put+0x212/0x6a0
>     rcu_core+0x5c6/0x1140
>    Last potentially related work creation:
>     __call_rcu_common.constprop.0+0xb7/0x9e0
>     br_del_if+0xdd/0x260
> 
> Don't rely on the pre-latched next pointer. br_multicast_del_port() deletes
> everything, so just take the current list head each round. The filtered
> walk in __br_multicast_disable_port_ctx() keeps its iterator but restarts
> whenever the latched node has left the list; port groups are only freed by
> the multicast GC work, which takes br->multicast_lock, so the node is still
> valid memory for that check.
> 
> Fixes: b08123684bd5 ("net: bridge: mcast: install S,G entries automatically based on reports")
> Cc: stable@vger.kernel.org
> Reported-by: TencentOS Corvus AI <corvus@tencent.com>
> Assisted-by: tencentos-corvus-ai:kimi-k3
> Signed-off-by: Jun Yang <junvyyang@tencent.com>
> ---
> A KASAN reproducer for this issue is available if requested.
> 
>   net/bridge/br_multicast.c | 33 +++++++++++++++++++++++++--------
>   1 file changed, 25 insertions(+), 8 deletions(-)
> 
> diff --git a/net/bridge/br_multicast.c b/net/bridge/br_multicast.c
> index 00aa9b2879d6..624dfca4066b 100644
> --- a/net/bridge/br_multicast.c
> +++ b/net/bridge/br_multicast.c
> @@ -2065,12 +2065,17 @@ void br_multicast_del_port(struct net_bridge_port *port)
>   {
>   	struct net_bridge *br = port->br;
>   	struct net_bridge_port_group *pg;
> -	struct hlist_node *n;
>   
> -	/* Take care of the remaining groups, only perm ones should be left */
> +	/* Take care of the remaining groups, only perm ones should be left.
> +	 * Deleting one can delete others on this same port->mglist, so
> +	 * always restart from the head.
> +	 */
>   	spin_lock_bh(&br->multicast_lock);
> -	hlist_for_each_entry_safe(pg, n, &port->mglist, mglist)
> +	while (!hlist_empty(&port->mglist)) {
> +		pg = hlist_entry(port->mglist.first,
> +				 struct net_bridge_port_group, mglist);
>   		br_multicast_find_del_pg(br, pg);
> +	}
>   	spin_unlock_bh(&br->multicast_lock);
>   	flush_work(&br->mcast_gc_work);
>   	br_multicast_port_ctx_deinit(&port->multicast_ctx);
> @@ -2126,11 +2132,23 @@ static void __br_multicast_disable_port_ctx(struct net_bridge_mcast_port *pmctx)
>   	struct hlist_node *n;
>   	bool del = false;
>   
> -	hlist_for_each_entry_safe(pg, n, &pmctx->port->mglist, mglist)
> -		if (!(pg->flags & MDB_PG_FLAGS_PERMANENT) &&
> -		    (!br_multicast_port_ctx_is_vlan(pmctx) ||
> -		     pg->key.addr.vid == pmctx->vlan->vid))
> -			br_multicast_find_del_pg(pmctx->port->br, pg);
> +	/* br_multicast_find_del_pg() can delete further entries of this same
> +	 * port->mglist, so the node latched in @n may be unlinked by the loop
> +	 * body. Port groups are only freed by the GC work under multicast_lock,
> +	 * so @n is still valid here; if it left the list, restart.
> +	 */
> +restart:
> +	hlist_for_each_entry_safe(pg, n, &pmctx->port->mglist, mglist) {
> +		if ((pg->flags & MDB_PG_FLAGS_PERMANENT) ||
> +		    (br_multicast_port_ctx_is_vlan(pmctx) &&
> +		     pg->key.addr.vid != pmctx->vlan->vid))
> +			continue;
> +
> +		br_multicast_find_del_pg(pmctx->port->br, pg);
> +
> +		if (n && hlist_unhashed(n))
> +			goto restart;
> +	}
>   
>   	del |= br_ip4_multicast_rport_del(pmctx);
>   	timer_delete(&pmctx->ip4_mc_router_timer);

Thanks for the report, but instead of all these restarts and checks,
can't we just do:
diff --git a/net/bridge/br_multicast.c b/net/bridge/br_multicast.c
index 75e1e2a8fc83..62c4008c5bb8 100644
--- a/net/bridge/br_multicast.c
+++ b/net/bridge/br_multicast.c
@@ -808,7 +808,8 @@ void br_multicast_del_pg(struct net_bridge_mdb_entry *mp,
         struct hlist_node *tmp;
  
         rcu_assign_pointer(*pp, pg->next);
-       hlist_del_init(&pg->mglist);
+       /* use _rcu to preserve the next pointer because it might be in use */
+       hlist_del_init_rcu(&pg->mglist);
         br_multicast_eht_clean_sets(pg);
         hlist_for_each_entry_safe(ent, tmp, &pg->src_list, node)
                 br_multicast_del_group_src(ent, false);


I have old patches that remove the mcast open-coded list implementations, I must
revive them and clean all of this up finally. :)

Cheers,
  Nik


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

* Re: [PATCH] net: bridge: mcast: don't truncate the port group walk on teardown
  2026-08-12 12:17 ` Nikolay Aleksandrov
@ 2026-08-12 12:21   ` Nikolay Aleksandrov
  0 siblings, 0 replies; 3+ messages in thread
From: Nikolay Aleksandrov @ 2026-08-12 12:21 UTC (permalink / raw)
  To: Jun Yang, Ido Schimmel
  Cc: stable, Jun Yang, TencentOS Corvus AI, David S. Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman, bridge,
	netdev, linux-kernel

On 12/08/2026 15:17, Nikolay Aleksandrov wrote:
> On 12/08/2026 14:34, Jun Yang wrote:
>> __br_multicast_disable_port_ctx() and br_multicast_del_port() walk
>> port->mglist with hlist_for_each_entry_safe(), which only guarantees that
>> the *current* node may be removed by the loop body.
>>
>> The body is br_multicast_find_del_pg() -> br_multicast_del_pg(), and that
>> deletes further port groups of the very same port. br_multicast_del_pg()
>> drops the group's sources, and br_multicast_fwd_src_remove()
>> (net/bridge/br_multicast.c:583) deletes the (S,G) port group installed on
>> that same port; br_multicast_star_g_handle_mode() -> __fwd_del_star_excl()
>> (net/bridge/br_multicast.c:330) deletes the automatically installed
>> MDB_PG_FLAGS_STAR_EXCL entries, again on that same port. All of those sit
>> on the same port->mglist.
>>
>> When one of them happens to be the node the iterator already latched as
>> "next", hlist_del_init() clears its ->next, the walk sees NULL and stops.
>> Every port group after it is silently left on the port. port->mglist is
>> head-inserted, so this needs the cascade victim to be older than the (*,G)
>> entry owning the source - a user-added, non-permanent (S,G) MDB entry added
>> before the (*,G) join produces exactly that ordering.
>>
>> Hitting it once truncates the disable walk in
>> __br_multicast_disable_port_ctx() and once more truncates the flush in
>> br_multicast_del_port(), so del_nbp() goes on to free the port with port
>> groups still on port->mglist - and still linked in the bridge's mdb, with a
>> dangling ->key.port. Any subsequent mdb dump reads the freed port:
>>
>>    BUG: KASAN: slab-use-after-free in __mdb_fill_info+0x1191/0x1320
>>    Read of size 8 at addr ffff88803065d008 by task bridge/9527
>>     __mdb_fill_info+0x1191/0x1320
>>     br_mdb_dump+0x594/0xe40
>>     rtnl_mdb_dump+0x1cf/0x5d0
>>    Freed by task 0:
>>     kfree+0x265/0x740
>>     kobject_put+0x212/0x6a0
>>     rcu_core+0x5c6/0x1140
>>    Last potentially related work creation:
>>     __call_rcu_common.constprop.0+0xb7/0x9e0
>>     br_del_if+0xdd/0x260
>>
>> Don't rely on the pre-latched next pointer. br_multicast_del_port() deletes
>> everything, so just take the current list head each round. The filtered
>> walk in __br_multicast_disable_port_ctx() keeps its iterator but restarts
>> whenever the latched node has left the list; port groups are only freed by
>> the multicast GC work, which takes br->multicast_lock, so the node is still
>> valid memory for that check.
>>
>> Fixes: b08123684bd5 ("net: bridge: mcast: install S,G entries automatically based on reports")
>> Cc: stable@vger.kernel.org
>> Reported-by: TencentOS Corvus AI <corvus@tencent.com>
>> Assisted-by: tencentos-corvus-ai:kimi-k3
>> Signed-off-by: Jun Yang <junvyyang@tencent.com>
>> ---
>> A KASAN reproducer for this issue is available if requested.
>>
>>   net/bridge/br_multicast.c | 33 +++++++++++++++++++++++++--------
>>   1 file changed, 25 insertions(+), 8 deletions(-)
>>
>> diff --git a/net/bridge/br_multicast.c b/net/bridge/br_multicast.c
>> index 00aa9b2879d6..624dfca4066b 100644
>> --- a/net/bridge/br_multicast.c
>> +++ b/net/bridge/br_multicast.c
>> @@ -2065,12 +2065,17 @@ void br_multicast_del_port(struct net_bridge_port *port)
>>   {
>>       struct net_bridge *br = port->br;
>>       struct net_bridge_port_group *pg;
>> -    struct hlist_node *n;
>> -    /* Take care of the remaining groups, only perm ones should be left */
>> +    /* Take care of the remaining groups, only perm ones should be left.
>> +     * Deleting one can delete others on this same port->mglist, so
>> +     * always restart from the head.
>> +     */
>>       spin_lock_bh(&br->multicast_lock);
>> -    hlist_for_each_entry_safe(pg, n, &port->mglist, mglist)
>> +    while (!hlist_empty(&port->mglist)) {
>> +        pg = hlist_entry(port->mglist.first,
>> +                 struct net_bridge_port_group, mglist);
>>           br_multicast_find_del_pg(br, pg);
>> +    }
>>       spin_unlock_bh(&br->multicast_lock);
>>       flush_work(&br->mcast_gc_work);
>>       br_multicast_port_ctx_deinit(&port->multicast_ctx);
>> @@ -2126,11 +2132,23 @@ static void __br_multicast_disable_port_ctx(struct net_bridge_mcast_port *pmctx)
>>       struct hlist_node *n;
>>       bool del = false;
>> -    hlist_for_each_entry_safe(pg, n, &pmctx->port->mglist, mglist)
>> -        if (!(pg->flags & MDB_PG_FLAGS_PERMANENT) &&
>> -            (!br_multicast_port_ctx_is_vlan(pmctx) ||
>> -             pg->key.addr.vid == pmctx->vlan->vid))
>> -            br_multicast_find_del_pg(pmctx->port->br, pg);
>> +    /* br_multicast_find_del_pg() can delete further entries of this same
>> +     * port->mglist, so the node latched in @n may be unlinked by the loop
>> +     * body. Port groups are only freed by the GC work under multicast_lock,
>> +     * so @n is still valid here; if it left the list, restart.
>> +     */
>> +restart:
>> +    hlist_for_each_entry_safe(pg, n, &pmctx->port->mglist, mglist) {
>> +        if ((pg->flags & MDB_PG_FLAGS_PERMANENT) ||
>> +            (br_multicast_port_ctx_is_vlan(pmctx) &&
>> +             pg->key.addr.vid != pmctx->vlan->vid))
>> +            continue;
>> +
>> +        br_multicast_find_del_pg(pmctx->port->br, pg);
>> +
>> +        if (n && hlist_unhashed(n))
>> +            goto restart;
>> +    }
>>       del |= br_ip4_multicast_rport_del(pmctx);
>>       timer_delete(&pmctx->ip4_mc_router_timer);
> 
> Thanks for the report, but instead of all these restarts and checks,
> can't we just do:
> diff --git a/net/bridge/br_multicast.c b/net/bridge/br_multicast.c
> index 75e1e2a8fc83..62c4008c5bb8 100644
> --- a/net/bridge/br_multicast.c
> +++ b/net/bridge/br_multicast.c
> @@ -808,7 +808,8 @@ void br_multicast_del_pg(struct net_bridge_mdb_entry *mp,
>          struct hlist_node *tmp;
> 
>          rcu_assign_pointer(*pp, pg->next);
> -       hlist_del_init(&pg->mglist);
> +       /* use _rcu to preserve the next pointer because it might be in use */

Just to be clear - I'd expand the comment to include why it is safe to do so and under
what conditions (multicast_lock held)

> +       hlist_del_init_rcu(&pg->mglist);
>          br_multicast_eht_clean_sets(pg);
>          hlist_for_each_entry_safe(ent, tmp, &pg->src_list, node)
>                  br_multicast_del_group_src(ent, false);
> 
> 
> I have old patches that remove the mcast open-coded list implementations, I must
> revive them and clean all of this up finally. :)
> 
> Cheers,
>   Nik
> 




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

end of thread, other threads:[~2026-08-12 12:21 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-12 11:34 [PATCH] net: bridge: mcast: don't truncate the port group walk on teardown Jun Yang
2026-08-12 12:17 ` Nikolay Aleksandrov
2026-08-12 12:21   ` Nikolay Aleksandrov

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