Thread (12 messages) 12 messages, 3 authors, 18h ago

Re: [PATCH v1 net-next 4/5] ipv4: Batch rt_flush_dev() for dying netns.

From: Kuniyuki Iwashima <kuniyu@google.com>
Date: 2026-09-28 16:33:17

On Sun, Sep 27, 2026 at 3:52 PM Eric Dumazet [off-list ref] wrote:
On Sun, Sep 27, 2026 at 10:24 PM Kuniyuki Iwashima [off-list ref] wrote:
quoted
IPv4 uncached routes are linked to the global per-cpu lists,
rt_uncached_list.

When unregistering a netdev, rt_flush_dev() iterates over the
potentially long lists to find uncached routes tied to the device
and swap it with blackhole_netdev.

Since it is called for every device in dying netns under RTNL,
it adds O(N_dev x (N_cpu + N_route)) costs to netns dismantle.

Let's call it (almost) once per cleanup_net().

When rt_flush_dev() is called with NULL from ->pre_exit_batch(),
it purges every entry in dying netns, reducing the cost to
O(N_cpu + N_route).

Since ->pre_exit_batch() is called before synchronize_rcu(),
we must prevent adding a new route for dying netns, so now
rt_add_uncached_list() checks !check_net() and swaps the device
with blackhole_netdev.

When rt_flush_dev() is later called again from fib_netdev_event()
via NETDEV_UNREGISTER, it just returns.

Note that net_pre_exit_done() cannot be replaced with !check_net()
because:

  1. some ->pre_exit() call unregister_netdevice() before
     fib_net_ops (e.g. ovs_pre_exit_net(), l2tp_pre_exit_net()).

  2. ->dellink() could call unregister_netdevice() for another
     netdev in a dying netns queued for the next cleanup_net()
     batch, for which ->pre_exit_batch() has not been called
     yet (e.g. veth).

Signed-off-by: Kuniyuki Iwashima <kuniyu@google.com>
---
 net/ipv4/fib_frontend.c |  6 ++++++
 net/ipv4/route.c        | 28 +++++++++++++++++++++++-----
 2 files changed, 29 insertions(+), 5 deletions(-)
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..cbe328b3f254 100644
--- a/net/ipv4/route.c
+++ b/net/ipv4/route.c
@@ -1554,14 +1554,29 @@ 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);

+       /* Set once and never cleared: non-NULL marks an uncached route. */
        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)))
+               list_add_tail(&rt->dst.rt_uncached, &ul->head);
+       else
+               rt_replace_uncached_list(rt);
+
        spin_unlock_bh(&ul->lock);
 }
Hi Kuniyuki,

Doing this in ->pre_exit_batch() and rt_add_uncached_list() /
rt6_uncached_list_add() is too early.

1) check_net(net) becomes false as soon as the last put_net() drops
   __ns_ref to 0, while devices in the netns are still UP and sockets /
   packets are still active (until default_device_exit_batch()).
   Swapping rt->dst.dev_rcu to blackhole_netdev on live routes (while
   rt->dst.input and rt->dst.output are not dst_discard) changes
   skb_dst_dev_net_rcu(skb) / dst_dev_net_rcu(&rt->dst) to &init_net.
   RX paths (__inet_lookup_skb, __inet6_lookup_skb, tcp_v4_rcv,
   icmp_rcv, ip_error, ...) will then look up sockets in init_net or
   send RST/ICMP from init_net, and TX paths (ip_route_output_flow,
   icmp6_dst_alloc, ip6_output) will see blackhole_netdev / init_net.

2) In patch 3/5, ops_undo_list() is also called from setup_net()
   (out_undo) when an ->init() callback fails. At that point
   check_net(net) is still true (__ns_ref == 1), so ->pre_exit_batch()
   will skip net (or not be called at all if setup_net() failed before
   fib_net_ops), yet ops_undo_list() sets NET_PRE_EXIT_DONE because
   ops_list == &pernet_list, causing default_device_exit_batch() to
   skip rt_flush_dev(dev).

3) In rt_flush_dev() / rt6_uncached_list_flush_dev(), the lockless
   if (list_empty(&ul->head)) check can race with a concurrent
   rt_add_uncached_list() that observed check_net(net) == true just
   before the last put_net() but has not yet executed list_add_tail().
   Since NET_PRE_EXIT_DONE disables all subsequent rt_flush_dev(dev)
   calls (including the retry in netdev_wait_allrefs_any()), that dev
   reference would never be released.
Ah, 1) & 3) are problematic exactly.

Instead of ->pre_exit_batch(), could we batch this at the
unregister_netdevice_many_notify() stage (after the NETDEV_UNREGISTER
loop, when all devices in the batch are already down, unlisted, and
marked NETREG_UNREGISTERING)? That would avoid ->pre_exit_batch(),
net->undo_state, and touching rt_add_uncached_list(), while also
speeding up any batched device unregistration.
I think it's doable, we can skip each dev's call by

if (dev && dev->reg_state == NETREG_UNREGISTERING)
  return;

and do batching later.  I think it can be done in netdev_run_todo()
after unlocking  __rtnl_lock() and if (!list_empty()), then we could
check like

if (!dev && list_empty(&rt->dst.dev.todo_list))
  /* purge */
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help