[PATCH net-next v2 2/3] ipv6: take fib6_sernum from a sequence shared by all netns

From: Qiliang Yuan

Date: Sun Oct 18 2026 - 23:33:00 EST


A per-cpu IPv6 route of a nexthop object records fib6_sernum of the
netns it was created in, and rt6_is_valid() compares it with fib6_sernum
of the netns its device belongs to when the route is checked. Every netns
starts its fib6_sernum at 1 and increments it on each tree change, so
different netns often hold the same value.

When a device moves to another netns, such a route remains usable until
nexthop_free_rcu() releases it, and fib6_info_destroy_rcu() queues that
callback from inside an RCU callback, after the device has moved. If the
fib6_sernum of the new netns matches the cached one in that window,
ip6_dst_check() accepts the route and a socket in the old netns keeps
sending through a device that now belongs to another netns.

Take every fib6_sernum bump from one sequence shared by all netns, which
only hands out values from 2 up: every netns still starts at 1, and 0
means there is no sernum to check. Every netns bumps its fib6_sernum
when a route is inserted, so before a route through it can be cached,
and the new netns does so before the moved device can transmit, when
NETDEV_UP adds a route through it. A cached route therefore never
matches the fib6_sernum of the netns its device moves to.
fib6_new_sernum() callers only hold their own table's tb6_lock, so two
tables of one netns can store their values in either order, and
fib6_sernum is no longer monotonic per netns. It is only compared for
equality, so this costs at most an extra invalidation.

A connected UDP socket in netns A caches a route through an IPv6 nexthop
object on a veth. The veth is moved to netns B and brought up with one
RTM_NEWLINK, then the socket sends 200000 datagrams. A route is added to
and removed from B 0 to 20 times before the move, 63 trials:

before after
trials sending after move 1 0
datagrams sent after move 251 0

Fixes: 8f34e53b60b3 ("ipv6: Use global sernum for dst validation with nexthop objects")
Signed-off-by: Qiliang Yuan <odys.yuan@xxxxxxxxx>
---
net/ipv6/ip6_fib.c | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/net/ipv6/ip6_fib.c b/net/ipv6/ip6_fib.c
index 0dc6ebee6e568..28d20d6def401 100644
--- a/net/ipv6/ip6_fib.c
+++ b/net/ipv6/ip6_fib.c
@@ -89,13 +89,22 @@ static void fib6_walker_unlink(struct net *net, struct fib6_walker *w)
write_unlock_bh(&net->ipv6.fib6_walker_lock);
}

+/*
+ * Draw fib6_sernum bumps from one sequence shared by all netns, so that a
+ * route cached against one netns never matches the sernum of another one,
+ * e.g. after its device moved. Every netns starts at 1 and the sequence only
+ * hands out values from 2 up: 0 means no sernum to check.
+ */
+static atomic_t fib6_sernum_seq = ATOMIC_INIT(1);
+
static int fib6_new_sernum(struct net *net)
{
- int new, old = atomic_read(&net->ipv6.fib6_sernum);
+ int new, old = atomic_read(&fib6_sernum_seq);

do {
- new = old < INT_MAX ? old + 1 : 1;
- } while (!atomic_try_cmpxchg(&net->ipv6.fib6_sernum, &old, new));
+ new = old < INT_MAX ? old + 1 : 2;
+ } while (!atomic_try_cmpxchg(&fib6_sernum_seq, &old, new));
+ atomic_set(&net->ipv6.fib6_sernum, new);

return new;
}

--
2.43.0