From: Chris J Arges <[email protected]>
Date: Thu, 17 Sep 2026 14:38:21 -0500
> We have observed hung tasks blocked on rtnl_mutex while network namespaces
> were being removed. The namespaces contained many network devices, and the
> host had accumulated a large population of entries on the global per-CPU
> uncached route lists. A perf profile collected during one incident
> attributed most of the cleanup worker's samples to rt_flush_dev():
> 
> ```
> 99.92% kworker/u384:3-  worker_thread
>   `-88.71% process_one_work
>       `-81.02% cleanup_net
>           `-81.00% unregister_netdevice_many_notify
>               `-79.42% notifier_call_chain
>                   `-78.05% fib_netdev_event
>                       `-77.92% rt_flush_dev
> ```
> 
> For each device, rt_flush_dev() visits every possible CPU and scans the
> global uncached route population while its caller holds rtnl_mutex. If N is
> the number of devices, C the number of possible CPUs, and R the number of
> uncached routes, the cost is O(N * (C + R)).
> 
> During namespace cleanup, other processes that issue RTNETLINK operations
> requiring the RTNL lock can stall until cleanup releases the lock.
> 
> A minimal reproducer is available here:
> https://github.com/arges/linux-reproducers/tree/main/rtnl-flush-storm
> 
> This series replaces each per-CPU uncached route list with a hash table
> using the network device as its key. Each table uses 64 buckets.

This sounds a bit overkill.  Also, this series still leaves
O(N * C) loops.

Given unregistering a single device is less common than
destroying netns, I think the right approach should be to
make the route flush once in cleanup_net() + outside RTNL.

Could you try this change ? (only compile-tested)

---8<---
diff --git a/include/net/net_namespace.h b/include/net/net_namespace.h
index 46b4c67e2966..d8ce7dc0fbdc 100644
--- a/include/net/net_namespace.h
+++ b/include/net/net_namespace.h
@@ -489,6 +489,7 @@ struct pernet_operations {
         */
        int (*init)(struct net *net);
        void (*pre_exit)(struct net *net);
+       void (*pre_exit_batch)(struct list_head *net_exit_list);
        void (*exit)(struct net *net);
        void (*exit_batch)(struct list_head *net_exit_list);
        /* Following method is called with RTNL held. */
diff --git a/net/core/net_namespace.c b/net/core/net_namespace.c
index da5f881fbd3b..7fc9bf45f3b6 100644
--- a/net/core/net_namespace.c
+++ b/net/core/net_namespace.c
@@ -160,6 +160,9 @@ static void ops_pre_exit_list(const struct 
pernet_operations *ops,
                list_for_each_entry(net, net_exit_list, exit_list)
                        ops->pre_exit(net);
        }
+
+       if (ops->pre_exit_batch)
+               ops->pre_exit_batch(net_exit_list);
 }
 
 static void ops_exit_rtnl_list(const struct list_head *ops_list,
diff --git a/net/ipv4/fib_frontend.c b/net/ipv4/fib_frontend.c
index 8a3dc04e8cac..b8d76b6279e1 100644
--- a/net/ipv4/fib_frontend.c
+++ b/net/ipv4/fib_frontend.c
@@ -1685,6 +1685,11 @@ static void __net_exit fib_net_pre_exit(struct net *net)
        nl_fib_lookup_exit(net);
 }
 
+static void __net_exit fib_net_pre_exit_batch(struct list_head *net_exit_list)
+{
+       rt_flush_dev(NULL);
+}
+
 static void __net_exit fib_net_exit_rtnl(struct net *net,
                                         struct list_head *dev_kill_list)
 {
@@ -1704,6 +1709,7 @@ static void __net_exit fib_net_exit(struct net *net)
 static struct pernet_operations fib_net_ops = {
        .init = fib_net_init,
        .pre_exit = fib_net_pre_exit,
+       .pre_exit_batch = fib_net_pre_exit_batch,
        .exit_rtnl = fib_net_exit_rtnl,
        .exit = fib_net_exit,
 };
diff --git a/net/ipv4/route.c b/net/ipv4/route.c
index d7da2f1acbb5..d35b66b33bbc 100644
--- a/net/ipv4/route.c
+++ b/net/ipv4/route.c
@@ -1554,14 +1554,28 @@ struct uncached_list {
 
 static DEFINE_PER_CPU_ALIGNED(struct uncached_list, rt_uncached_list);
 
+static void rt_replace_uncached_list(struct rtable *rt)
+{
+       struct net_device *dev = dst_dev(&rt->dst);
+
+       rcu_assign_pointer(rt->dst.dev_rcu, blackhole_netdev);
+       netdev_ref_replace(dev, blackhole_netdev,
+                          &rt->dst.dev_tracker, GFP_ATOMIC);
+}
+
 void rt_add_uncached_list(struct rtable *rt)
 {
        struct uncached_list *ul = raw_cpu_ptr(&rt_uncached_list);
 
-       rt->dst.rt_uncached_list = ul;
-
        spin_lock_bh(&ul->lock);
-       list_add_tail(&rt->dst.rt_uncached, &ul->head);
+
+       if (!check_net(dst_dev_net_rcu(&rt->dst))) {
+               rt_replace_uncached_list(rt);
+       } else {
+               rt->dst.rt_uncached_list = ul;
+               list_add_tail(&rt->dst.rt_uncached, &ul->head);
+       }
+
        spin_unlock_bh(&ul->lock);
 }
 
@@ -1587,6 +1601,9 @@ void rt_flush_dev(struct net_device *dev)
        struct rtable *rt, *safe;
        int cpu;
 
+       if (dev && !check_net(dev_net(dev)))
+               return;
+
        for_each_possible_cpu(cpu) {
                struct uncached_list *ul = &per_cpu(rt_uncached_list, cpu);
 
@@ -1595,11 +1612,11 @@ void rt_flush_dev(struct net_device *dev)
 
                spin_lock_bh(&ul->lock);
                list_for_each_entry_safe(rt, safe, &ul->head, dst.rt_uncached) {
-                       if (rt->dst.dev != dev)
+                       if (rt->dst.dev != dev &&
+                           (dev || check_net(dev_net(rt->dst.dev))))
                                continue;
-                       rcu_assign_pointer(rt->dst.dev_rcu, blackhole_netdev);
-                       netdev_ref_replace(dev, blackhole_netdev,
-                                          &rt->dst.dev_tracker, GFP_ATOMIC);
+
+                       rt_replace_uncached_list(rt);
                        list_del_init(&rt->dst.rt_uncached);
                }
                spin_unlock_bh(&ul->lock);
diff --git a/net/ipv6/route.c b/net/ipv6/route.c
index 7535b09068a0..28233197e1e1 100644
--- a/net/ipv6/route.c
+++ b/net/ipv6/route.c
@@ -135,14 +135,35 @@ struct uncached_list {
 
 static DEFINE_PER_CPU_ALIGNED(struct uncached_list, rt6_uncached_list);
 
+static void rt6_uncached_list_replace(struct rt6_info *rt)
+{
+       struct net_device *dev = dst_dev(&rt->dst);
+       struct inet6_dev *rt_idev = rt->rt6i_idev;
+
+       if (rt_idev) {
+               rt->rt6i_idev = in6_dev_get(blackhole_netdev);
+               in6_dev_put(rt_idev);
+       }
+
+       rcu_assign_pointer(rt->dst.dev_rcu, blackhole_netdev);
+       netdev_ref_replace(dev, blackhole_netdev,
+                          &rt->dst.dev_tracker,
+                          GFP_ATOMIC);
+}
+
 void rt6_uncached_list_add(struct rt6_info *rt)
 {
        struct uncached_list *ul = raw_cpu_ptr(&rt6_uncached_list);
 
-       rt->dst.rt_uncached_list = ul;
-
        spin_lock_bh(&ul->lock);
-       list_add_tail(&rt->dst.rt_uncached, &ul->head);
+
+       if (!check_net(dst_dev_net_rcu(&rt->dst))) {
+               rt6_uncached_list_replace(rt);
+       } else {
+               rt->dst.rt_uncached_list = ul;
+               list_add_tail(&rt->dst.rt_uncached, &ul->head);
+       }
+
        spin_unlock_bh(&ul->lock);
 }
 
@@ -161,6 +182,9 @@ static void rt6_uncached_list_flush_dev(struct net_device 
*dev)
 {
        int cpu;
 
+       if (dev && !check_net(dev_net(dev)))
+               return;
+
        for_each_possible_cpu(cpu) {
                struct uncached_list *ul = per_cpu_ptr(&rt6_uncached_list, cpu);
                struct rt6_info *rt, *safe;
@@ -172,23 +196,17 @@ static void rt6_uncached_list_flush_dev(struct net_device 
*dev)
                list_for_each_entry_safe(rt, safe, &ul->head, dst.rt_uncached) {
                        struct inet6_dev *rt_idev = rt->rt6i_idev;
                        struct net_device *rt_dev = rt->dst.dev;
-                       bool handled = false;
 
-                       if (rt_idev && rt_idev->dev == dev) {
-                               rt->rt6i_idev = in6_dev_get(blackhole_netdev);
-                               in6_dev_put(rt_idev);
-                               handled = true;
+                       if (dev) {
+                               if (rt_dev != dev &&
+                                   (!rt_idev || rt_idev->dev != dev))
+                                       continue;
+                       } else if (check_net(dev_net(rt_dev))) {
+                               continue;
                        }
 
-                       if (rt_dev == dev) {
-                               rt->dst.dev = blackhole_netdev;
-                               netdev_ref_replace(rt_dev, blackhole_netdev,
-                                                  &rt->dst.dev_tracker,
-                                                  GFP_ATOMIC);
-                               handled = true;
-                       }
-                       if (handled)
-                               list_del_init(&rt->dst.rt_uncached);
+                       rt6_uncached_list_replace(rt);
+                       list_del_init(&rt->dst.rt_uncached);
                }
                spin_unlock_bh(&ul->lock);
        }
@@ -6795,6 +6813,11 @@ static int __net_init ip6_route_net_init(struct net *net)
        goto out;
 }
 
+static void __net_exit ip6_route_net_pre_exit_batch(struct list_head 
*net_exit_list)
+{
+       rt6_uncached_list_flush_dev(NULL);
+}
+
 static void __net_exit ip6_route_net_exit(struct net *net)
 {
        kfree(net->ipv6.fib6_null_entry);
@@ -6833,6 +6856,7 @@ static void __net_exit ip6_route_net_exit_late(struct net 
*net)
 
 static struct pernet_operations ip6_route_net_ops = {
        .init = ip6_route_net_init,
+       .pre_exit_batch = ip6_route_net_pre_exit_batch,
        .exit = ip6_route_net_exit,
 };
 
---8<---

Reply via email to