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