]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
net: cpsw: Execute ndo_set_rx_mode callback in a work queue
authorKevin Hao <haokexin@gmail.com>
Tue, 3 Feb 2026 02:18:31 +0000 (10:18 +0800)
committerJakub Kicinski <kuba@kernel.org>
Wed, 4 Feb 2026 03:45:52 +0000 (19:45 -0800)
Commit 1767bb2d47b7 ("ipv6: mcast: Don't hold RTNL for
IPV6_ADD_MEMBERSHIP and MCAST_JOIN_GROUP.") removed the RTNL lock for
IPV6_ADD_MEMBERSHIP and MCAST_JOIN_GROUP operations. However, this
change triggered the following call trace on my BeagleBone Black board:
  WARNING: net/8021q/vlan_core.c:236 at vlan_for_each+0x120/0x124, CPU#0: rpcbind/481
  RTNL: assertion failed at net/8021q/vlan_core.c (236)
  Modules linked in:
  CPU: 0 UID: 997 PID: 481 Comm: rpcbind Not tainted 6.19.0-rc7-next-20260130-yocto-standard+ #35 PREEMPT
  Hardware name: Generic AM33XX (Flattened Device Tree)
  Call trace:
   unwind_backtrace from show_stack+0x28/0x2c
   show_stack from dump_stack_lvl+0x30/0x38
   dump_stack_lvl from __warn+0xb8/0x11c
   __warn from warn_slowpath_fmt+0x130/0x194
   warn_slowpath_fmt from vlan_for_each+0x120/0x124
   vlan_for_each from cpsw_add_mc_addr+0x54/0x98
   cpsw_add_mc_addr from __hw_addr_ref_sync_dev+0xc4/0xec
   __hw_addr_ref_sync_dev from __dev_mc_add+0x78/0x88
   __dev_mc_add from igmp6_group_added+0x84/0xec
   igmp6_group_added from __ipv6_dev_mc_inc+0x1fc/0x2f0
   __ipv6_dev_mc_inc from __ipv6_sock_mc_join+0x124/0x1b4
   __ipv6_sock_mc_join from do_ipv6_setsockopt+0x84c/0x1168
   do_ipv6_setsockopt from ipv6_setsockopt+0x88/0xc8
   ipv6_setsockopt from do_sock_setsockopt+0xe8/0x19c
   do_sock_setsockopt from __sys_setsockopt+0x84/0xac
   __sys_setsockopt from ret_fast_syscall+0x0/0x54

This trace occurs because vlan_for_each() is called within
cpsw_ndo_set_rx_mode(), which expects the RTNL lock to be held.
Since modifying vlan_for_each() to operate without the RTNL lock is not
straightforward, and because ndo_set_rx_mode() is invoked both with and
without the RTNL lock across different code paths, simply adding
rtnl_lock() in cpsw_ndo_set_rx_mode() is not a viable solution.

To resolve this issue, we opt to execute the actual processing within
a work queue, following the approach used by the icssg-prueth driver.

Please note: To reproduce this issue, I manually reverted the changes to
am335x-bone-common.dtsi from commit c477358e66a3 ("ARM: dts: am335x-bone:
switch to new cpsw switch drv") in order to revert to the legacy cpsw
driver.

Fixes: 1767bb2d47b7 ("ipv6: mcast: Don't hold RTNL for IPV6_ADD_MEMBERSHIP and MCAST_JOIN_GROUP.")
Signed-off-by: Kevin Hao <haokexin@gmail.com>
Cc: stable@vger.kernel.org
Link: https://patch.msgid.link/20260203-bbb-v5-2-ea0ea217a85c@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
drivers/net/ethernet/ti/cpsw.c

index 54c24cd3d3be639c90111856efaf154cd77f7ee4..b0e18bdc2c851086d6db6a3d7b9659c23c73b969 100644 (file)
@@ -305,12 +305,19 @@ static int cpsw_purge_all_mc(struct net_device *ndev, const u8 *addr, int num)
        return 0;
 }
 
-static void cpsw_ndo_set_rx_mode(struct net_device *ndev)
+static void cpsw_ndo_set_rx_mode_work(struct work_struct *work)
 {
-       struct cpsw_priv *priv = netdev_priv(ndev);
+       struct cpsw_priv *priv = container_of(work, struct cpsw_priv, rx_mode_work);
        struct cpsw_common *cpsw = priv->cpsw;
+       struct net_device *ndev = priv->ndev;
        int slave_port = -1;
 
+       rtnl_lock();
+       if (!netif_running(ndev))
+               goto unlock_rtnl;
+
+       netif_addr_lock_bh(ndev);
+
        if (cpsw->data.dual_emac)
                slave_port = priv->emac_port + 1;
 
@@ -318,7 +325,7 @@ static void cpsw_ndo_set_rx_mode(struct net_device *ndev)
                /* Enable promiscuous mode */
                cpsw_set_promiscious(ndev, true);
                cpsw_ale_set_allmulti(cpsw->ale, IFF_ALLMULTI, slave_port);
-               return;
+               goto unlock_addr;
        } else {
                /* Disable promiscuous mode */
                cpsw_set_promiscious(ndev, false);
@@ -331,6 +338,18 @@ static void cpsw_ndo_set_rx_mode(struct net_device *ndev)
        /* add/remove mcast address either for real netdev or for vlan */
        __hw_addr_ref_sync_dev(&ndev->mc, ndev, cpsw_add_mc_addr,
                               cpsw_del_mc_addr);
+
+unlock_addr:
+       netif_addr_unlock_bh(ndev);
+unlock_rtnl:
+       rtnl_unlock();
+}
+
+static void cpsw_ndo_set_rx_mode(struct net_device *ndev)
+{
+       struct cpsw_priv *priv = netdev_priv(ndev);
+
+       schedule_work(&priv->rx_mode_work);
 }
 
 static unsigned int cpsw_rxbuf_total_len(unsigned int len)
@@ -1472,6 +1491,7 @@ static int cpsw_probe_dual_emac(struct cpsw_priv *priv)
        priv_sl2->ndev = ndev;
        priv_sl2->dev  = &ndev->dev;
        priv_sl2->msg_enable = netif_msg_init(debug_level, CPSW_DEBUG);
+       INIT_WORK(&priv_sl2->rx_mode_work, cpsw_ndo_set_rx_mode_work);
 
        if (is_valid_ether_addr(data->slave_data[1].mac_addr)) {
                memcpy(priv_sl2->mac_addr, data->slave_data[1].mac_addr,
@@ -1653,6 +1673,7 @@ static int cpsw_probe(struct platform_device *pdev)
        priv->dev  = dev;
        priv->msg_enable = netif_msg_init(debug_level, CPSW_DEBUG);
        priv->emac_port = 0;
+       INIT_WORK(&priv->rx_mode_work, cpsw_ndo_set_rx_mode_work);
 
        if (is_valid_ether_addr(data->slave_data[0].mac_addr)) {
                memcpy(priv->mac_addr, data->slave_data[0].mac_addr, ETH_ALEN);
@@ -1758,6 +1779,8 @@ clean_runtime_disable_ret:
 static void cpsw_remove(struct platform_device *pdev)
 {
        struct cpsw_common *cpsw = platform_get_drvdata(pdev);
+       struct net_device *ndev;
+       struct cpsw_priv *priv;
        int i, ret;
 
        ret = pm_runtime_resume_and_get(&pdev->dev);
@@ -1770,9 +1793,15 @@ static void cpsw_remove(struct platform_device *pdev)
                return;
        }
 
-       for (i = 0; i < cpsw->data.slaves; i++)
-               if (cpsw->slaves[i].ndev)
-                       unregister_netdev(cpsw->slaves[i].ndev);
+       for (i = 0; i < cpsw->data.slaves; i++) {
+               ndev = cpsw->slaves[i].ndev;
+               if (!ndev)
+                       continue;
+
+               priv = netdev_priv(ndev);
+               unregister_netdev(ndev);
+               disable_work_sync(&priv->rx_mode_work);
+       }
 
        cpts_release(cpsw->cpts);
        cpdma_ctlr_destroy(cpsw->dma);