]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
net: avoid theoretical races with ref drain
authorJakub Kicinski <kuba@kernel.org>
Thu, 6 Aug 2026 02:28:21 +0000 (19:28 -0700)
committerJakub Kicinski <kuba@kernel.org>
Thu, 6 Aug 2026 16:37:03 +0000 (09:37 -0700)
Technically, it's illegal to take a ref on a netdev just because
we have a pointer on which we already hold a ref, with no other
protection. This is because our simple per-cpu refcount
implementation cannot atomically read the count.

Let's make sure we cancel outstanding work and never queue more
work for a device we know is dead. This way taking a ref on
a dev we know is on the netdev_work_list is always going to be safe.

Jiangshan Yi reports that the issues is caught by ref tracker infra
leading to a warning:
  WARNING: lib/ref_tracker.c:322 at ref_tracker_free
  WARNING: lib/ref_tracker.c:246 at ref_tracker_dir_exit

Reported-by: Jiangshan Yi <yijiangshan@kylinos.cn>
Link: https://lore.kernel.org/20260731035135.3917308-2-yijiangshan@kylinos.cn
Fixes: 12c765be84d2 ("net: turn the rx_mode work into a generic netdev_work facility")
Link: https://patch.msgid.link/20260806022821.2079945-1-kuba@kernel.org
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
net/core/dev.c
net/core/dev.h
net/core/netdev_work.c

index c49d2ce51285103161c2580af5ba6839eb52bb0c..af260ff5462a6e997b63f8de5ff6dffbe9dd1779 100644 (file)
@@ -12436,6 +12436,7 @@ void unregister_netdevice_many_notify(struct list_head *head,
                dev_tcx_uninstall(dev);
                dev_xdp_uninstall(dev);
                dev_memory_provider_uninstall(dev);
+               netdev_work_cancel_all(dev);
                netdev_unlock_ops(dev);
                bpf_dev_bound_netdev_unregister(dev);
 
index 5d0b0305d3baafc339e86c7680760c2ec29e256d..b757faead4d1a3e445e54d2f468c38c9e09b6762 100644 (file)
@@ -179,6 +179,7 @@ enum netdev_work_core {
 void __netdev_work_core_sched(struct net_device *dev, unsigned long event);
 unsigned long
 __netdev_work_core_cancel(struct net_device *dev, unsigned long mask);
+void netdev_work_cancel_all(struct net_device *dev);
 
 void __dev_notify_flags(struct net_device *dev, unsigned int old_flags,
                        unsigned int gchanges, u32 portid,
index 3109fae132ad18baf858dedeb135c8176b72794e..e721a06d58df1f87d41463d7363dcf15b6ae4492 100644 (file)
@@ -31,6 +31,10 @@ static void netdev_work_enqueue(struct net_device *dev, unsigned long events,
                return;
 
        spin_lock_bh(&netdev_work_lock);
+       if (!dev_isalive(dev)) {
+               spin_unlock_bh(&netdev_work_lock);
+               return;
+       }
        if (list_empty(&dev->work_node)) {
                list_add_tail(&dev->work_node, &netdev_work_list);
                netdev_hold(dev, &dev->work_tracker, GFP_ATOMIC);
@@ -61,6 +65,18 @@ netdev_work_dequeue(struct net_device *dev, unsigned long *pending,
        return events;
 }
 
+void netdev_work_cancel_all(struct net_device *dev)
+{
+       spin_lock_bh(&netdev_work_lock);
+       dev->work_pending = 0;
+       dev->work_core_pending = 0;
+       if (!list_empty(&dev->work_node)) {
+               list_del_init(&dev->work_node);
+               netdev_put(dev, &dev->work_tracker);
+       }
+       spin_unlock_bh(&netdev_work_lock);
+}
+
 void netdev_work_sched(struct net_device *dev, unsigned long events)
 {
        netdev_work_enqueue(dev, events, 0);