[PATCH net 1/2] net: cancel pending netdev_work on device unregistration

From: Jiangshan Yi

Date: Thu Jul 30 2026 - 23:55:07 EST


When a netdev is unregistered, pending netdev_work events are not
explicitly canceled. Only events cleared by ndo_stop() (e.g.
VLAN_WORK_LINK_STATE) are handled; core events like
NETDEV_WORK_RX_MODE and driver events like VLAN_WORK_MTU /
VLAN_WORK_FEATURES remain pending, keeping a tracked reference
(dev->work_tracker) on the device.

During netdev_wait_allrefs_any(), RTNL is released to let the system
workqueue run. netdev_work_proc() may then attempt to process the
stale work_tracker, triggering ref_tracker warnings at boot:

WARNING: lib/ref_tracker.c:322 at ref_tracker_free
WARNING: lib/ref_tracker.c:246 at ref_tracker_dir_exit

Add netdev_work_cancel_all() to cancel all pending core and driver
work, releasing the work_tracker before the device enters
netdev_run_todo(). Call it from unregister_netdevice_many_notify()
after rx_mode cleanup.

This is safe because netdev_work_cancel_all() runs under RTNL in
unregister_netdevice_many_notify(), and netdev_work_proc() also
requires RTNL, so the two cannot execute concurrently. If
netdev_work_proc() has already dequeued the device, the cancel is a
no-op; otherwise it releases the tracker before the device enters
netdev_run_todo().

Fixes: 12c765be84d2 ("net: turn the rx_mode work into a generic netdev_work facility")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Jiangshan Yi <yijiangshan@xxxxxxxxxx>
---
net/core/dev.c | 2 +-
net/core/dev.h | 2 ++
net/core/netdev_work.c | 10 ++++++++++
3 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/net/core/dev.c b/net/core/dev.c
index 5933c5d..b461b01 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -12456,7 +12456,7 @@ void unregister_netdevice_many_notify(struct list_head *head,
dev_uc_flush(dev);
dev_mc_flush(dev);

-
+ netdev_work_cancel_all(dev);
netdev_rss_contexts_free(dev);

call_netdevice_notifiers(NETDEV_PRE_UNINIT, dev);
diff --git a/net/core/dev.h b/net/core/dev.h
index 5d0b030..d3ac3ba 100644
--- a/net/core/dev.h
+++ b/net/core/dev.h
@@ -180,6 +180,8 @@ 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,
const struct nlmsghdr *nlh);
diff --git a/net/core/netdev_work.c b/net/core/netdev_work.c
index 3109fae..d69363d 100644
--- a/net/core/netdev_work.c
+++ b/net/core/netdev_work.c
@@ -99,6 +99,16 @@ __netdev_work_core_cancel(struct net_device *dev, unsigned long mask)
return netdev_work_dequeue(dev, &dev->work_core_pending, mask);
}

+/* Cancel all pending core and driver work and release the work_tracker
+ * reference. Must be called under RTNL, which mutually excludes
+ * netdev_work_proc().
+ */
+void netdev_work_cancel_all(struct net_device *dev)
+{
+ __netdev_work_core_cancel(dev, ~0UL);
+ netdev_work_cancel(dev, ~0UL);
+}
+
static void netdev_work_run(struct net_device *dev, unsigned long events,
unsigned long core)
{
--
2.25.1