Re: [PATCH] net: bridge: mcast: don't truncate the port group walk on teardown

Nikolay Aleksandrov <[email protected]>
Newsgroups gmane.linux.network.bridge
Message-ID <19b05984-d67c-4c30-aec6-ee8e8be554d9__16170.2420956985$1786538144$gmane$org@blackwall.org>
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: [email protected]
>> Reported-by: TencentOS Corvus AI <[email protected]>
>> Assisted-by: tencentos-corvus-ai:kimi-k3
>> Signed-off-by: Jun Yang <[email protected]>
>> ---
>> 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
>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.