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 */