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

From: Qiliang Yuan

Date: Fri Oct 16 2026 - 02:11:50 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, including the initial one, from one global
sequence, so that a cached route never matches the fib6_sernum of
another netns.

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 3 0
datagrams sent after move 8187 0

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

diff --git a/net/ipv6/af_inet6.c b/net/ipv6/af_inet6.c
index a9b8b4b42a1af..db4cccfdc565c 100644
--- a/net/ipv6/af_inet6.c
+++ b/net/ipv6/af_inet6.c
@@ -945,7 +945,6 @@ static int __net_init inet6_net_init(struct net *net)
net->ipv6.sysctl.max_dst_opts_len = IP6_DEFAULT_MAX_DST_OPTS_LEN;
net->ipv6.sysctl.max_hbh_opts_len = IP6_DEFAULT_MAX_HBH_OPTS_LEN;
net->ipv6.sysctl.fib_notify_on_flag_change = 0;
- atomic_set(&net->ipv6.fib6_sernum, 1);

net->ipv6.sysctl.ioam6_id = IOAM6_DEFAULT_ID;
net->ipv6.sysctl.ioam6_id_wide = IOAM6_DEFAULT_ID_WIDE;
diff --git a/net/ipv6/ip6_fib.c b/net/ipv6/ip6_fib.c
index 0dc6ebee6e568..c66466be64451 100644
--- a/net/ipv6/ip6_fib.c
+++ b/net/ipv6/ip6_fib.c
@@ -89,13 +89,21 @@ static void fib6_walker_unlink(struct net *net, struct fib6_walker *w)
write_unlock_bh(&net->ipv6.fib6_walker_lock);
}

+/*
+ * Draw every fib6_sernum 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.
+ */
+static atomic_t fib6_sernum_seq;
+
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));
+ } while (!atomic_try_cmpxchg(&fib6_sernum_seq, &old, new));
+ atomic_set(&net->ipv6.fib6_sernum, new);

return new;
}
@@ -2466,6 +2474,8 @@ static int __net_init fib6_net_init(struct net *net)
size_t size = sizeof(struct hlist_head) * FIB6_TABLE_HASHSZ;
int err;

+ fib6_new_sernum(net);
+
err = fib6_notifier_init(net);
if (err)
return err;

--
2.43.0