Netdev List
 help / color / mirror / Atom feed
* [PATCH net v2] net: bridge: mcast: don't truncate the port group walk on teardown
@ 2026-08-26  1:41 Jun Yang
  2026-08-26  7:48 ` Nikolay Aleksandrov
  2026-08-27 19:31 ` Jakub Kicinski
  0 siblings, 2 replies; 4+ messages in thread
From: Jun Yang @ 2026-08-26  1:41 UTC (permalink / raw)
  To: Nikolay Aleksandrov, Ido Schimmel, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni
  Cc: Simon Horman, stable, TencentOS Corvus AI, bridge, netdev,
	Jun Yang

From: Jun Yang <junvyyang@tencent.com>

__br_multicast_disable_port_ctx() and br_multicast_del_port() walk
port->mglist with hlist_for_each_entry_safe(). However,
br_multicast_find_del_pg() can also delete other entries from the same
list through br_multicast_fwd_src_remove() or __fwd_del_star_excl().

If such an entry is the iterator's saved next node, hlist_del_init()
clears its ->next and terminates the walk early. The reproducer triggers
this in both teardown walks, leaving port groups in the bridge mdb with
a dangling ->key.port after del_nbp() frees the port:

  BUG: KASAN: slab-use-after-free in __mdb_fill_info+0x1191/0x1320
   __mdb_fill_info+0x1191/0x1320
   br_mdb_dump+0x594/0xe40
   rtnl_mdb_dump+0x1cf/0x5d0

Use hlist_del_init_rcu() to unlink the group while preserving ->next.
br_multicast_del_pg() and the teardown walks run under
br->multicast_lock. The GC worker must acquire the same lock before
detaching the group for destruction, so the node remains alive while
the walk uses the preserved pointer.

Fixes: b08123684bd5 ("net: bridge: mcast: install S,G entries automatically based on reports")
Cc: stable@vger.kernel.org
Suggested-by: Nikolay Aleksandrov <razor@blackwall.org>
Reported-by: TencentOS Corvus AI <corvus@tencent.com>
Assisted-by: tencentos-corvus-ai:kimi-k3
Signed-off-by: Jun Yang <junvyyang@tencent.com>
---
v2: delete with hlist_del_init_rcu() to preserve ->next for an in-progress
    teardown walk, per Nikolay's suggestion, instead of restarting the walks.
    Tested on 7.2-rc7 with KASAN: the original use-after-free is no longer
    reported.

v1: https://lore.kernel.org/all/20260812113435.1854275-1-junvyyang@tencent.com/

 net/bridge/br_multicast.c | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/net/bridge/br_multicast.c b/net/bridge/br_multicast.c
index 00aa9b2879d6..8022c7f12169 100644
--- a/net/bridge/br_multicast.c
+++ b/net/bridge/br_multicast.c
@@ -808,7 +808,11 @@ 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);
+	/* Keep ->next (held under multicast_lock, freed later by the GC work):
+	 * a port->mglist teardown walk may have latched this node as its next,
+	 * and deleting other groups of the same port must not truncate it.
+	 */
+	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);
-- 
2.43.7


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

* Re: [PATCH net v2] net: bridge: mcast: don't truncate the port group walk on teardown
  2026-08-26  1:41 [PATCH net v2] net: bridge: mcast: don't truncate the port group walk on teardown Jun Yang
@ 2026-08-26  7:48 ` Nikolay Aleksandrov
  2026-08-27 19:31 ` Jakub Kicinski
  1 sibling, 0 replies; 4+ messages in thread
From: Nikolay Aleksandrov @ 2026-08-26  7:48 UTC (permalink / raw)
  To: Jun Yang, Ido Schimmel, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni
  Cc: Simon Horman, stable, TencentOS Corvus AI, bridge, netdev,
	Jun Yang

On 26/08/2026 04:41, Jun Yang wrote:
> From: Jun Yang <junvyyang@tencent.com>
> 
> __br_multicast_disable_port_ctx() and br_multicast_del_port() walk
> port->mglist with hlist_for_each_entry_safe(). However,
> br_multicast_find_del_pg() can also delete other entries from the same
> list through br_multicast_fwd_src_remove() or __fwd_del_star_excl().
> 
> If such an entry is the iterator's saved next node, hlist_del_init()
> clears its ->next and terminates the walk early. The reproducer triggers
> this in both teardown walks, leaving port groups in the bridge mdb with
> a dangling ->key.port after del_nbp() frees the port:
> 
>    BUG: KASAN: slab-use-after-free in __mdb_fill_info+0x1191/0x1320
>     __mdb_fill_info+0x1191/0x1320
>     br_mdb_dump+0x594/0xe40
>     rtnl_mdb_dump+0x1cf/0x5d0
> 
> Use hlist_del_init_rcu() to unlink the group while preserving ->next.
> br_multicast_del_pg() and the teardown walks run under
> br->multicast_lock. The GC worker must acquire the same lock before
> detaching the group for destruction, so the node remains alive while
> the walk uses the preserved pointer.
> 
> Fixes: b08123684bd5 ("net: bridge: mcast: install S,G entries automatically based on reports")
> Cc: stable@vger.kernel.org
> Suggested-by: Nikolay Aleksandrov <razor@blackwall.org>
> Reported-by: TencentOS Corvus AI <corvus@tencent.com>
> Assisted-by: tencentos-corvus-ai:kimi-k3
> Signed-off-by: Jun Yang <junvyyang@tencent.com>
> ---
> v2: delete with hlist_del_init_rcu() to preserve ->next for an in-progress
>      teardown walk, per Nikolay's suggestion, instead of restarting the walks.
>      Tested on 7.2-rc7 with KASAN: the original use-after-free is no longer
>      reported.
> 
> v1: https://lore.kernel.org/all/20260812113435.1854275-1-junvyyang@tencent.com/
> 
>   net/bridge/br_multicast.c | 6 +++++-
>   1 file changed, 5 insertions(+), 1 deletion(-)
> 
> diff --git a/net/bridge/br_multicast.c b/net/bridge/br_multicast.c
> index 00aa9b2879d6..8022c7f12169 100644
> --- a/net/bridge/br_multicast.c
> +++ b/net/bridge/br_multicast.c
> @@ -808,7 +808,11 @@ 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);
> +	/* Keep ->next (held under multicast_lock, freed later by the GC work):
> +	 * a port->mglist teardown walk may have latched this node as its next,
> +	 * and deleting other groups of the same port must not truncate it.
> +	 */
> +	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);

Acked-by: Nikolay Aleksandrov <razor@blackwall.org>


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

* Re: [PATCH net v2] net: bridge: mcast: don't truncate the port group walk on teardown
  2026-08-26  1:41 [PATCH net v2] net: bridge: mcast: don't truncate the port group walk on teardown Jun Yang
  2026-08-26  7:48 ` Nikolay Aleksandrov
@ 2026-08-27 19:31 ` Jakub Kicinski
  2026-08-28  9:04   ` Nikolay Aleksandrov
  1 sibling, 1 reply; 4+ messages in thread
From: Jakub Kicinski @ 2026-08-27 19:31 UTC (permalink / raw)
  To: littleddfu
  Cc: Jakub Kicinski, razor, idosch, davem, edumazet, pabeni, horms,
	stable, corvus, bridge, netdev, junvyyang

This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
net: bridge: mcast: don't truncate the port group walk on teardown

This changes br_multicast_del_pg() to unlink the port group from
port->mglist with hlist_del_init_rcu() instead of hlist_del_init(), so
that a nested deletion of another group of the same port does not clear
the ->next pointer that an in-progress teardown walk has already latched.
The intent is to stop br_multicast_del_port() and
__br_multicast_disable_port_ctx() from terminating early and leaving port
groups behind with a dangling ->key.port.

> diff --git a/net/bridge/br_multicast.c b/net/bridge/br_multicast.c
> index 75e1e2a8fc83a..452ac68c4a835 100644
> --- a/net/bridge/br_multicast.c
> +++ b/net/bridge/br_multicast.c
> @@ -808,7 +808,11 @@ 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);
> +	/* Keep ->next (held under multicast_lock, freed later by the GC work):
> +	 * a port->mglist teardown walk may have latched this node as its next,
> +	 * and deleting other groups of the same port must not truncate it.
> +	 */
> +	hlist_del_init_rcu(&pg->mglist);

[Severity: High]

Preserving ->next keeps the walk going, but doesn't it now make the walk
visit a group that was already deleted as a side effect?

hlist_del_init_rcu() only clears ->pprev, so the latched next node is
still reachable by the iterator:

	include/linux/rculist.h:
	static inline void hlist_del_init_rcu(struct hlist_node *n)
	{
		if (!hlist_unhashed(n)) {
			__hlist_del(n);
			WRITE_ONCE(n->pprev, NULL);
		}
	}

Taking br_multicast_del_port() as the example, all under
br->multicast_lock:

br_multicast_del_port()
   hlist_for_each_entry_safe(pg, n, &port->mglist, mglist)  /* n = A->next = B */
      br_multicast_find_del_pg(br, A)
         br_multicast_del_pg(mp, A, pp)
            br_multicast_del_group_src() -> br_multicast_fwd_src_remove()
            or br_multicast_star_g_handle_mode() -> __fwd_del_star_excl()
            or br_multicast_sg_del_exclude_ports()
               -> deletes B (same port) via br_multicast_del_pg()
                  rcu_assign_pointer(*pp, B->next);   /* B off mp->ports */
                  hlist_del_init_rcu(&B->mglist);     /* B->next preserved */
   pg = n = B                                          /* loop continues into B */
      br_multicast_find_del_pg(br, B)

In br_multicast_find_del_pg() the mdb entry lookup still succeeds, since
mdb entries are only unhashed under br->multicast_lock, but the scan over
mp->ports can no longer find B because br_multicast_del_pg() already did
rcu_assign_pointer(*pp, pg->next) for it:

	for (pp = &mp->ports;
	     (p = mlock_dereference(*pp, br)) != NULL;
	     pp = &p->next) {
		if (p != pg)
			continue;

		br_multicast_del_pg(mp, pg, pp);
		return;
	}

	WARN_ON(1);

So control falls through to the trailing WARN_ON(1). br_multicast_del_port()
applies no filter at all before calling br_multicast_find_del_pg(), and the
filter in __br_multicast_disable_port_ctx():

	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);

doesn't exclude a kernel-installed, non-permanent S,G group either.

Does that mean the reproducer described in the commit message now produces
a WARN backtrace on the same path instead of the KASAN use-after-free, and
a panic when booted with panic_on_warn=1?

Would it make sense to also skip nodes that are already unlinked in both
teardown walks, e.g.:

	hlist_for_each_entry_safe(pg, n, &port->mglist, mglist) {
		if (hlist_unhashed(&pg->mglist))
			continue;
		br_multicast_find_del_pg(br, pg);
	}

or otherwise restart the walk, rather than relying on the preserved ->next
alone?

[Severity: High]

This isn't a bug introduced by this patch, but the same list has a second
deletion site that still uses the non-RCU helper and frees immediately,
br_multicast_del_port_group():

	void br_multicast_del_port_group(struct net_bridge_port_group *p)
	{
		struct net_bridge_port *port = p->key.port;
		__u16 vid = p->key.addr.vid;

		hlist_del_init(&p->mglist);
		if (!br_multicast_is_star_g(&p->key.addr))
			rhashtable_remove_fast(&port->br->sg_port_tbl, &p->rhnode,
					       br_sg_port_rht_params);
		kfree(p);
		br_multicast_port_ngroups_dec(port, vid);
	}

port->mglist is also walked locklessly by br_multicast_list_adjacent(),
which only holds rcu_read_lock():

	hlist_for_each_entry_rcu(group, &port->mglist, mglist) {
		...
		entry->addr = group->key.addr;

That reader is exported and used from net/batman-adv/multicast.c. The group
is already published on port->mglist by br_multicast_new_port_group()
(hlist_add_head(&p->mglist, &port->mglist)) before the only caller of
br_multicast_del_port_group() runs, the err_del_port_group label in
br_mdb_add_group_star_g() in net/bridge/br_mdb.c, reached when
br_mdb_add_group_srcs() fails. br_mdb_config_src_list_init() does not
reject a duplicated source, so a MDBE_ATTR_SRC_LIST carrying the same
source twice without NLM_F_REPLACE makes br_mdb_add_group_src() return
-EEXIST; an allocation failure returning -ENOSPC is a second trigger.

spin_lock_bh(&br->multicast_lock) held by the deleting side does not
exclude an RCU reader on another CPU. Can br_multicast_list_adjacent()
then read group->key.addr and follow group->mglist.next after the plain
kfree(p), and does the hlist_del_init() there also truncate that RCU walk
by NULLing ->next?

Since this patch establishes that port->mglist deletions must preserve
->next and defer the free, would it be worth converting this site to
hlist_del_init_rcu() plus a deferred free, in the way
br_multicast_destroy_port_group() uses kfree_rcu()?

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

* Re: [PATCH net v2] net: bridge: mcast: don't truncate the port group walk on teardown
  2026-08-27 19:31 ` Jakub Kicinski
@ 2026-08-28  9:04   ` Nikolay Aleksandrov
  0 siblings, 0 replies; 4+ messages in thread
From: Nikolay Aleksandrov @ 2026-08-28  9:04 UTC (permalink / raw)
  To: Jakub Kicinski, littleddfu
  Cc: idosch, davem, edumazet, pabeni, horms, stable, corvus, bridge,
	netdev, junvyyang

On 27/08/2026 22:31, Jakub Kicinski 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.
> ---
> net: bridge: mcast: don't truncate the port group walk on teardown
> 
> This changes br_multicast_del_pg() to unlink the port group from
> port->mglist with hlist_del_init_rcu() instead of hlist_del_init(), so
> that a nested deletion of another group of the same port does not clear
> the ->next pointer that an in-progress teardown walk has already latched.
> The intent is to stop br_multicast_del_port() and
> __br_multicast_disable_port_ctx() from terminating early and leaving port
> groups behind with a dangling ->key.port.
> 
>> diff --git a/net/bridge/br_multicast.c b/net/bridge/br_multicast.c
>> index 75e1e2a8fc83a..452ac68c4a835 100644
>> --- a/net/bridge/br_multicast.c
>> +++ b/net/bridge/br_multicast.c
>> @@ -808,7 +808,11 @@ 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);
>> +	/* Keep ->next (held under multicast_lock, freed later by the GC work):
>> +	 * a port->mglist teardown walk may have latched this node as its next,
>> +	 * and deleting other groups of the same port must not truncate it.
>> +	 */
>> +	hlist_del_init_rcu(&pg->mglist);
> 
> [Severity: High]
> 
> Preserving ->next keeps the walk going, but doesn't it now make the walk
> visit a group that was already deleted as a side effect?
> 
> hlist_del_init_rcu() only clears ->pprev, so the latched next node is
> still reachable by the iterator:
> 
> 	include/linux/rculist.h:
> 	static inline void hlist_del_init_rcu(struct hlist_node *n)
> 	{
> 		if (!hlist_unhashed(n)) {
> 			__hlist_del(n);
> 			WRITE_ONCE(n->pprev, NULL);
> 		}
> 	}
> 
> Taking br_multicast_del_port() as the example, all under
> br->multicast_lock:
> 
> br_multicast_del_port()
>     hlist_for_each_entry_safe(pg, n, &port->mglist, mglist)  /* n = A->next = B */
>        br_multicast_find_del_pg(br, A)
>           br_multicast_del_pg(mp, A, pp)
>              br_multicast_del_group_src() -> br_multicast_fwd_src_remove()
>              or br_multicast_star_g_handle_mode() -> __fwd_del_star_excl()
>              or br_multicast_sg_del_exclude_ports()
>                 -> deletes B (same port) via br_multicast_del_pg()
>                    rcu_assign_pointer(*pp, B->next);   /* B off mp->ports */
>                    hlist_del_init_rcu(&B->mglist);     /* B->next preserved */
>     pg = n = B                                          /* loop continues into B */
>        br_multicast_find_del_pg(br, B)
> 
> In br_multicast_find_del_pg() the mdb entry lookup still succeeds, since
> mdb entries are only unhashed under br->multicast_lock, but the scan over
> mp->ports can no longer find B because br_multicast_del_pg() already did
> rcu_assign_pointer(*pp, pg->next) for it:
> 
> 	for (pp = &mp->ports;
> 	     (p = mlock_dereference(*pp, br)) != NULL;
> 	     pp = &p->next) {
> 		if (p != pg)
> 			continue;
> 
> 		br_multicast_del_pg(mp, pg, pp);
> 		return;
> 	}
> 
> 	WARN_ON(1);
> 
> So control falls through to the trailing WARN_ON(1). br_multicast_del_port()
> applies no filter at all before calling br_multicast_find_del_pg(), and the
> filter in __br_multicast_disable_port_ctx():
> 
> 	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);
> 
> doesn't exclude a kernel-installed, non-permanent S,G group either.
> 
> Does that mean the reproducer described in the commit message now produces
> a WARN backtrace on the same path instead of the KASAN use-after-free, and
> a panic when booted with panic_on_warn=1?
> 
> Would it make sense to also skip nodes that are already unlinked in both
> teardown walks, e.g.:
> 
> 	hlist_for_each_entry_safe(pg, n, &port->mglist, mglist) {
> 		if (hlist_unhashed(&pg->mglist))
> 			continue;
> 		br_multicast_find_del_pg(br, pg);
> 	}
> 
> or otherwise restart the walk, rather than relying on the preserved ->next
> alone?
> 

Oh, right, I missed that WARN_ON() and it will fire, yes. But instead of
sprinkling these hlist_unhashed checks around, you can add 1 to the beginning
of br_multicast_find_del_pg() with a comment why it exists

> [Severity: High]
> 
> This isn't a bug introduced by this patch, but the same list has a second
> deletion site that still uses the non-RCU helper and frees immediately,
> br_multicast_del_port_group():
> 
> 	void br_multicast_del_port_group(struct net_bridge_port_group *p)
> 	{
> 		struct net_bridge_port *port = p->key.port;
> 		__u16 vid = p->key.addr.vid;
> 
> 		hlist_del_init(&p->mglist);
> 		if (!br_multicast_is_star_g(&p->key.addr))
> 			rhashtable_remove_fast(&port->br->sg_port_tbl, &p->rhnode,
> 					       br_sg_port_rht_params);
> 		kfree(p);
> 		br_multicast_port_ngroups_dec(port, vid);
> 	}
> 
> port->mglist is also walked locklessly by br_multicast_list_adjacent(),
> which only holds rcu_read_lock():
> 
> 	hlist_for_each_entry_rcu(group, &port->mglist, mglist) {
> 		...
> 		entry->addr = group->key.addr;
> 
> That reader is exported and used from net/batman-adv/multicast.c. The group
> is already published on port->mglist by br_multicast_new_port_group()
> (hlist_add_head(&p->mglist, &port->mglist)) before the only caller of
> br_multicast_del_port_group() runs, the err_del_port_group label in
> br_mdb_add_group_star_g() in net/bridge/br_mdb.c, reached when
> br_mdb_add_group_srcs() fails. br_mdb_config_src_list_init() does not
> reject a duplicated source, so a MDBE_ATTR_SRC_LIST carrying the same
> source twice without NLM_F_REPLACE makes br_mdb_add_group_src() return
> -EEXIST; an allocation failure returning -ENOSPC is a second trigger.
> 
> spin_lock_bh(&br->multicast_lock) held by the deleting side does not
> exclude an RCU reader on another CPU. Can br_multicast_list_adjacent()
> then read group->key.addr and follow group->mglist.next after the plain
> kfree(p), and does the hlist_del_init() there also truncate that RCU walk
> by NULLing ->next?
> 
> Since this patch establishes that port->mglist deletions must preserve
> ->next and defer the free, would it be worth converting this site to
> hlist_del_init_rcu() plus a deferred free, in the way
> br_multicast_destroy_port_group() uses kfree_rcu()?

Ouch, that is actually a separate and valid bug, the cleanup shouldn't be
using hlist_del_init + immediate kfree(). I will take care of this one.

Cheers,
  Nik



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

end of thread, other threads:[~2026-08-28  9:04 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-26  1:41 [PATCH net v2] net: bridge: mcast: don't truncate the port group walk on teardown Jun Yang
2026-08-26  7:48 ` Nikolay Aleksandrov
2026-08-27 19:31 ` Jakub Kicinski
2026-08-28  9:04   ` Nikolay Aleksandrov

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