* [PATCH net-next v2 0/3] net: make moving a device to another netns cheaper
@ 2026-10-19 3:31 Qiliang Yuan
2026-10-19 3:31 ` [PATCH net-next v2 1/3] ipv4: draw rt_genid from a sequence shared by all netns Qiliang Yuan
` (2 more replies)
0 siblings, 3 replies; 9+ messages in thread
From: Qiliang Yuan @ 2026-10-19 3:31 UTC (permalink / raw)
To: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, David Ahern, Ido Schimmel, Nikolay Aleksandrov
Cc: Kuniyuki Iwashima, netdev, linux-kernel, Qiliang Yuan
Moving a device to another netns waits in rcu_barrier() with RTNL held.
Container and sandbox managers move one end of a veth pair for every
new network namespace, so this barrier limits how fast such namespaces
can be created on the whole system, to about 75 per second in a 16
vCPU guest no matter how many threads create them.
The barrier was added for NETDEV_UNREGISTER_FINAL, which is long gone,
but it still keeps cached IPv4 routes through the moved device from
being accepted in the old netns. Routes through nexthop objects are not
covered by it: their cached dst entries are released by an RCU callback
queued from another RCU callback, so a socket in the old netns can still
send through a device that now belongs to another netns whenever the
route generation numbers of the two netns happen to match.
Patches 1 and 2 draw the IPv4 rt_genid and the IPv6 fib6_sernum bumps
from sequences shared by all netns, so a cached route no longer matches
another netns. This fixes that leak for nexthop objects and leaves
nothing that relies on the barrier. Patch 3 removes it. Patches 1 and 2
fix bugs that exist today and can go to net on their own if preferred.
Creating 400 netns, each with a veth pair whose host end is moved back
to the initial netns, a tap device, addresses and routes, median of 5
runs:
before after
1 thread 68 /s 385 /s
4 threads 74 /s 368 /s
16 threads 76 /s 263 /s
Signed-off-by: Qiliang Yuan <odys.yuan@gmail.com>
---
V1 -> V2:
- 2/3: keep the initial fib6_sernum of each netns and never hand it out
from the shared sequence, instead of drawing a value at netns creation
(Eric Dumazet); 1/3: do the same for rt_genid
- 2/3: note that fib6_sernum is no longer monotonic per netns (Eric
Dumazet); 1/3: likewise for rt_genid
- Drop Kuniyuki's Reviewed-by from 1/3 and 2/3, which changed
- Rebase onto net-next and remeasure
v1: https://lore.kernel.org/r/20261016-bug-net-netns-move-rcu-barrier-v1-0-4157d798658f@gmail.com
---
Qiliang Yuan (3):
ipv4: draw rt_genid from a sequence shared by all netns
ipv6: take fib6_sernum from a sequence shared by all netns
net: remove rcu_barrier() from __dev_change_net_namespace()
include/net/net_namespace.h | 5 +----
net/core/dev.c | 1 -
net/ipv4/route.c | 20 ++++++++++++++++++++
net/ipv6/ip6_fib.c | 15 ++++++++++++---
4 files changed, 33 insertions(+), 8 deletions(-)
---
base-commit: 98c2ad4f5d805dbc1ea436a334ee59311806a48c
change-id: 20261016-bug-net-netns-move-rcu-barrier-d998c6395830
Best regards,
--
Qiliang Yuan <odys.yuan@gmail.com>
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH net-next v2 1/3] ipv4: draw rt_genid from a sequence shared by all netns
2026-10-19 3:31 [PATCH net-next v2 0/3] net: make moving a device to another netns cheaper Qiliang Yuan
@ 2026-10-19 3:31 ` Qiliang Yuan
2026-10-20 4:14 ` Kuniyuki Iwashima
2026-10-21 2:44 ` netdev-bot+sashiko
2026-10-19 3:31 ` [PATCH net-next v2 2/3] ipv6: take fib6_sernum " Qiliang Yuan
2026-10-19 3:31 ` [PATCH net-next v2 3/3] net: remove rcu_barrier() from __dev_change_net_namespace() Qiliang Yuan
2 siblings, 2 replies; 9+ messages in thread
From: Qiliang Yuan @ 2026-10-19 3:31 UTC (permalink / raw)
To: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, David Ahern, Ido Schimmel, Nikolay Aleksandrov
Cc: Kuniyuki Iwashima, netdev, linux-kernel, Qiliang Yuan
An IPv4 route records the rt_genid of the netns it was created in, and
rt_is_expired() compares it with the rt_genid of the netns its device
belongs to when the route is checked. Every netns starts its rt_genid
at 0 and increments it on each cache flush, so different netns often
hold the same value.
When a device moves to another netns, routes cached through it remain
usable until dst_dev_put() points them to blackhole_netdev. For a route
using a nexthop object this happens in nexthop_free_rcu(), which
free_fib_info_rcu() queues from inside an RCU callback, so the
rcu_barrier() in __dev_change_net_namespace() does not wait for it. If
the rt_genid of the new netns matches the cached one in that window,
ipv4_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 rt_genid bump from one sequence shared by all netns, which
never hands out 0, the value every netns starts with. A netns bumps its
rt_genid on NETDEV_UP before its devices can carry traffic, so a cached
route never matches the rt_genid of the netns its device moves to.
Concurrent bumps of one netns can leave the older of two values, so
rt_genid is no longer monotonic per netns. It is only compared for
equality, so this costs at most an extra route lookup.
A connected UDP socket in netns A caches a route through a 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. B's rt_genid is
bumped 0 to 20 times before the move, 63 trials:
before after
trials sending after move 2 0
datagrams sent after move 2616 0
Fixes: 4c7e8084fd46 ("ipv4: Plumb support for nexthop object in a fib_info")
Signed-off-by: Qiliang Yuan <odys.yuan@gmail.com>
---
include/net/net_namespace.h | 5 +----
net/ipv4/route.c | 20 ++++++++++++++++++++
2 files changed, 21 insertions(+), 4 deletions(-)
diff --git a/include/net/net_namespace.h b/include/net/net_namespace.h
index 46b4c67e2966f..9c78f1d90fb62 100644
--- a/include/net/net_namespace.h
+++ b/include/net/net_namespace.h
@@ -556,10 +556,7 @@ static inline int rt_genid_ipv6(const struct net *net)
}
#endif
-static inline void rt_genid_bump_ipv4(struct net *net)
-{
- atomic_inc(&net->ipv4.rt_genid);
-}
+void rt_genid_bump_ipv4(struct net *net);
extern void (*__fib6_flush_trees)(struct net *net);
static inline void rt_genid_bump_ipv6(struct net *net)
diff --git a/net/ipv4/route.c b/net/ipv4/route.c
index 595a3b7f17830..25bebbb30bf3c 100644
--- a/net/ipv4/route.c
+++ b/net/ipv4/route.c
@@ -393,6 +393,26 @@ static inline int ip_rt_proc_init(void)
}
#endif /* CONFIG_PROC_FS */
+/*
+ * Draw rt_genid bumps from one sequence shared by all netns. A route keeps
+ * the genid of the netns it was created in, and is checked against the netns
+ * its device is in now, so distinct values make that check fail once the
+ * device has moved. Every netns starts at 0, which the sequence never hands
+ * out, and bumps on NETDEV_UP before any of its devices can carry traffic.
+ */
+static atomic_t rt_genid_seq;
+
+void rt_genid_bump_ipv4(struct net *net)
+{
+ int genid;
+
+ do {
+ genid = atomic_inc_return(&rt_genid_seq);
+ } while (!genid);
+ atomic_set(&net->ipv4.rt_genid, genid);
+}
+EXPORT_SYMBOL_GPL(rt_genid_bump_ipv4);
+
static inline bool rt_is_expired(const struct rtable *rth)
{
bool res;
--
2.43.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH net-next v2 2/3] ipv6: take fib6_sernum from a sequence shared by all netns
2026-10-19 3:31 [PATCH net-next v2 0/3] net: make moving a device to another netns cheaper Qiliang Yuan
2026-10-19 3:31 ` [PATCH net-next v2 1/3] ipv4: draw rt_genid from a sequence shared by all netns Qiliang Yuan
@ 2026-10-19 3:31 ` Qiliang Yuan
2026-10-20 4:17 ` Kuniyuki Iwashima
2026-10-21 2:44 ` netdev-bot+sashiko
2026-10-19 3:31 ` [PATCH net-next v2 3/3] net: remove rcu_barrier() from __dev_change_net_namespace() Qiliang Yuan
2 siblings, 2 replies; 9+ messages in thread
From: Qiliang Yuan @ 2026-10-19 3:31 UTC (permalink / raw)
To: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, David Ahern, Ido Schimmel, Nikolay Aleksandrov
Cc: Kuniyuki Iwashima, netdev, linux-kernel, Qiliang Yuan
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@gmail.com>
---
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
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH net-next v2 3/3] net: remove rcu_barrier() from __dev_change_net_namespace()
2026-10-19 3:31 [PATCH net-next v2 0/3] net: make moving a device to another netns cheaper Qiliang Yuan
2026-10-19 3:31 ` [PATCH net-next v2 1/3] ipv4: draw rt_genid from a sequence shared by all netns Qiliang Yuan
2026-10-19 3:31 ` [PATCH net-next v2 2/3] ipv6: take fib6_sernum " Qiliang Yuan
@ 2026-10-19 3:31 ` Qiliang Yuan
2026-10-21 2:44 ` netdev-bot+sashiko
2 siblings, 1 reply; 9+ messages in thread
From: Qiliang Yuan @ 2026-10-19 3:31 UTC (permalink / raw)
To: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, David Ahern, Ido Schimmel, Nikolay Aleksandrov
Cc: Kuniyuki Iwashima, netdev, linux-kernel, Qiliang Yuan
Commit 6549dd43c043 ("net: dev: fix the incorrect hold of net
namespace's lo device") added rcu_barrier() before
NETDEV_UNREGISTER_FINAL in dev_change_net_namespace(), so that dst
entries released by RCU callbacks were on the dst garbage list before
dst_ifdown() moved them to the loopback device of the old netns.
Commit 070f2d7e264a ("net: Drop NETDEV_UNREGISTER_FINAL") removed the
event but kept the barrier, and dead dst entries now point to
blackhole_netdev instead of a loopback device.
The barrier waits for the RCU callbacks pending on every CPU while RTNL
is held, which takes a full grace period on each netns move. Container
and sandbox managers move one end of a veth pair for every new network
namespace, so this limits how fast such namespaces can be created on the
whole system, regardless of how many threads create them.
Remove the barrier. The RCU callbacks queued by NETDEV_UNREGISTER
handlers free memory and drop device references, which the move does not
wait for. Cached IPv4 and IPv6 routes through the moved device are
rejected once its netns changes, since they no longer match the rt_genid
or fib6_sernum of another netns.
Creating 400 netns, each with a veth pair whose host end is moved back
to the initial netns, a tap device, addresses and routes, in a 16 vCPU
guest, median of 5 runs:
before after
1 thread 69 /s 385 /s
4 threads 74 /s 368 /s
16 threads 76 /s 263 /s
Signed-off-by: Qiliang Yuan <odys.yuan@gmail.com>
Reviewed-by: Eric Dumazet <edumazet@kernel.org>
Reviewed-by: Kuniyuki Iwashima <kuniyu@google.com>
---
net/core/dev.c | 1 -
1 file changed, 1 deletion(-)
diff --git a/net/core/dev.c b/net/core/dev.c
index 8eb9bf1ff9eb6..ef172905109ee 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -12994,7 +12994,6 @@ int __dev_change_net_namespace(struct net_device *dev, struct net *net,
* the device is just moving and can keep their slaves up.
*/
call_netdevice_notifiers(NETDEV_UNREGISTER, dev);
- rcu_barrier();
new_nsid = peernet2id_alloc(dev_net(dev), net, GFP_KERNEL);
--
2.43.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH net-next v2 1/3] ipv4: draw rt_genid from a sequence shared by all netns
2026-10-19 3:31 ` [PATCH net-next v2 1/3] ipv4: draw rt_genid from a sequence shared by all netns Qiliang Yuan
@ 2026-10-20 4:14 ` Kuniyuki Iwashima
2026-10-21 2:44 ` netdev-bot+sashiko
1 sibling, 0 replies; 9+ messages in thread
From: Kuniyuki Iwashima @ 2026-10-20 4:14 UTC (permalink / raw)
To: Qiliang Yuan
Cc: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, David Ahern, Ido Schimmel, Nikolay Aleksandrov,
netdev, linux-kernel
On Mon, 19 Oct 2026 11:31:15 +0800, Qiliang Yuan wrote:
> An IPv4 route records the rt_genid of the netns it was created in, and
> rt_is_expired() compares it with the rt_genid of the netns its device
> belongs to when the route is checked. Every netns starts its rt_genid
> at 0 and increments it on each cache flush, so different netns often
> hold the same value.
>
> When a device moves to another netns, routes cached through it remain
> usable until dst_dev_put() points them to blackhole_netdev. For a route
> using a nexthop object this happens in nexthop_free_rcu(), which
> free_fib_info_rcu() queues from inside an RCU callback, so the
> rcu_barrier() in __dev_change_net_namespace() does not wait for it. If
> the rt_genid of the new netns matches the cached one in that window,
> ipv4_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 rt_genid bump from one sequence shared by all netns, which
> never hands out 0, the value every netns starts with. A netns bumps its
> rt_genid on NETDEV_UP before its devices can carry traffic, so a cached
> route never matches the rt_genid of the netns its device moves to.
> Concurrent bumps of one netns can leave the older of two values, so
> rt_genid is no longer monotonic per netns. It is only compared for
> equality, so this costs at most an extra route lookup.
>
> A connected UDP socket in netns A caches a route through a 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. B's rt_genid is
> bumped 0 to 20 times before the move, 63 trials:
>
> before after
> trials sending after move 2 0
> datagrams sent after move 2616 0
>
> Fixes: 4c7e8084fd46 ("ipv4: Plumb support for nexthop object in a fib_info")
> Signed-off-by: Qiliang Yuan <odys.yuan@gmail.com>
Reviewed-by: Kuniyuki Iwashima <kuniyu@google.com>
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH net-next v2 2/3] ipv6: take fib6_sernum from a sequence shared by all netns
2026-10-19 3:31 ` [PATCH net-next v2 2/3] ipv6: take fib6_sernum " Qiliang Yuan
@ 2026-10-20 4:17 ` Kuniyuki Iwashima
2026-10-21 2:44 ` netdev-bot+sashiko
1 sibling, 0 replies; 9+ messages in thread
From: Kuniyuki Iwashima @ 2026-10-20 4:17 UTC (permalink / raw)
To: Qiliang Yuan
Cc: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, David Ahern, Ido Schimmel, Nikolay Aleksandrov,
netdev, linux-kernel
On Mon, 19 Oct 2026 11:31:16 +0800, Qiliang Yuan wrote:
> 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@gmail.com>
Reviewed-by: Kuniyuki Iwashima <kuniyu@google.com>
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH net-next v2 1/3] ipv4: draw rt_genid from a sequence shared by all netns
2026-10-19 3:31 ` [PATCH net-next v2 1/3] ipv4: draw rt_genid from a sequence shared by all netns Qiliang Yuan
2026-10-20 4:14 ` Kuniyuki Iwashima
@ 2026-10-21 2:44 ` netdev-bot+sashiko
1 sibling, 0 replies; 9+ messages in thread
From: netdev-bot+sashiko @ 2026-10-21 2:44 UTC (permalink / raw)
To: odys.yuan
Cc: davem, edumazet, kuba, pabeni, horms, dsahern, idosch, razor,
kuniyu, netdev, linux-kernel
Thank you for your contribution! Sashiko AI review found 2 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 1 · Low: 1
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] The commit says "a cached route never matches the rt_genid of
the netns its device moves to".
- [Low] The comment added above rt_genid_seq in net/ipv4/route.c, and the
commit message, say each netns "bumps on NETDEV_UP before any of its…
--
ipv4: draw rt_genid from a sequence shared by all netns
This commit stops incrementing rt_genid separately in each netns. Instead,
every bump takes its value from one global sequence that never returns 0.
The goal is that a route cached through a device can never match the
rt_genid of the netns the device moves into.
> diff --git a/net/ipv4/route.c b/net/ipv4/route.c
> index 595a3b7f17830..25bebbb30bf3c 100644
> --- a/net/ipv4/route.c
> +++ b/net/ipv4/route.c
> @@ -393,6 +393,26 @@ static inline int ip_rt_proc_init(void)
> }
> #endif /* CONFIG_PROC_FS */
>
> +/*
> + * Draw rt_genid bumps from one sequence shared by all netns. A route keeps
> + * the genid of the netns it was created in, and is checked against the netns
> + * its device is in now, so distinct values make that check fail once the
> + * device has moved. Every netns starts at 0, which the sequence never hands
> + * out, and bumps on NETDEV_UP before any of its devices can carry traffic.
> + */
[Severity: Low]
Is the last sentence of this comment accurate? The commit message makes
the same claim:
"A netns bumps its rt_genid on NETDEV_UP before its devices can carry
traffic"
In net/core/dev.c, __dev_open() marks the device up and activates it
before any notifier runs:
netif_set_up(dev, true);
dev_set_rx_mode(dev);
dev_activate(dev);
netif_open() only sends NETDEV_UP after __dev_open() has returned:
rtmsg_ifinfo(RTM_NEWLINK, dev, IFF_UP | IFF_RUNNING, GFP_KERNEL, 0, NULL);
call_netdevice_notifiers(NETDEV_UP, dev);
So rt_cache_flush() in fib_netdev_event() runs after the device can
already transmit.
The NETDEV_UP bump also doesn't always happen. For a device with no
in_device, for example one whose MTU is too small for IPv4,
fib_netdev_event() returns early:
in_dev = __in_dev_get_rtnl(dev);
if (!in_dev)
return NOTIFY_DONE;
The nonzero genid on cached routes seems to come from the FIB insertion
paths instead, such as fib_table_insert()->rt_cache_flush() and the
NETDEV_UP case of fib_inetaddr_event().
The next patch in the series, "net: remove rcu_barrier() from
__dev_change_net_namespace()", depends on this invariant. Could the
comment and the commit message describe the ordering that actually holds?
> +static atomic_t rt_genid_seq;
> +
> +void rt_genid_bump_ipv4(struct net *net)
> +{
> + int genid;
> +
> + do {
> + genid = atomic_inc_return(&rt_genid_seq);
> + } while (!genid);
> + atomic_set(&net->ipv4.rt_genid, genid);
> +}
[Severity: Medium]
The commit message says:
"so a cached route never matches the rt_genid of the netns its device
moves to."
Doesn't that only hold modulo 2^32? rt_genid_seq is one 32-bit atomic_t
shared by every netns, and rtable::rt_genid is an int. The loop only
skips 0, so values start repeating after 2^32 - 1 bumps.
Anyone who can create a user namespace and a netns can advance the
shared counter by writing /proc/sys/net/ipv4/route/flush. Each write
makes ipv4_sysctl_rtcache_flush() call rt_cache_flush(), and
sysctl_route_net_init() keeps the flush entry for netns owned by non-init
user namespaces:
/* Don't export non-whitelisted sysctls to unprivileged users */
if (net->user_ns != &init_user_ns) {
if (tbl[0].procname != ipv4_route_flush_procname)
table_size = 0;
}
Suppose a route is cached in netns A with genid X, and its device then
moves to netns B. If B's genid equals X before
free_fib_info_rcu()->nexthop_free_rcu() gets to dst_dev_put(), the check
in rt_is_expired() passes:
res = rth->rt_genid != rt_genid_ipv4(dev_net_rcu(rth->dst.dev));
ipv4_dst_check() then accepts the dst, and the socket in A keeps sending
through a device that B now owns.
This is hard to hit, because B's bump has to land on the unknown X
within a few RCU grace periods. However, the follow-up patch "net: remove
rcu_barrier() from __dev_change_net_namespace()" removes the barrier. It
then relies on genid uniqueness alone to reject every FIB-cached route
through the moved device, not just routes that use nexthop objects.
Would an approach that doesn't depend on a wrapping 32-bit value work
here? For example, the move could invalidate the cached dsts, or call
dst_dev_put() on them, synchronously. Another option would be to check
the netns the route was created in.
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261019-bug-net-netns-move-rcu-barrier-v2-0-b6eb7780c492%40gmail.com
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH net-next v2 2/3] ipv6: take fib6_sernum from a sequence shared by all netns
2026-10-19 3:31 ` [PATCH net-next v2 2/3] ipv6: take fib6_sernum " Qiliang Yuan
2026-10-20 4:17 ` Kuniyuki Iwashima
@ 2026-10-21 2:44 ` netdev-bot+sashiko
1 sibling, 0 replies; 9+ messages in thread
From: netdev-bot+sashiko @ 2026-10-21 2:44 UTC (permalink / raw)
To: odys.yuan
Cc: davem, edumazet, kuba, pabeni, horms, dsahern, idosch, razor,
kuniyu, netdev, linux-kernel
Thank you for your contribution! Sashiko AI review found 2 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 2 (1) · Low: 0
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] fib6_new_sernum() (net/ipv6/ip6_fib.c) now takes every
fib6_sernum value from one global 31-bit sequence, fib6_sernum_seq.
- [Medium] Every netns still starts at the same constant 1
(`atomic_set(&net->ipv6.fib6_sernum, 1)` in inet6_net_init,…
Pre-existing issues:
- [Medium] This problem wasn't introduced by this patch, but the patch
leans on it.
--
ipv6: take fib6_sernum from a sequence shared by all netns
This patch changes fib6_new_sernum() so that it takes values from one
global atomic sequence shared by all network namespaces. Before, each
netns had its own counter. The goal is that a per-cpu nexthop route cached
in one netns can no longer pass rt6_is_valid() against the sernum of the
netns its device moved to.
> 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.
> + */
[Severity: Medium]
Every netns still starts with the same value in inet6_net_init():
net/ipv6/af_inet6.c:inet6_net_init() {
...
atomic_set(&net->ipv6.fib6_sernum, 1);
...
}
So 1 is the one value that can still be shared across namespaces. The
commit message relies on two ordering claims to rule this out:
"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."
Are both of these claims true?
In fib6_add(), fib6_add_rt2node() publishes the route to RCU readers
first:
rcu_assign_pointer(rt->fib6_node, fn);
rcu_assign_pointer(*ins, rt);
The sernum is bumped only after that, back in fib6_add():
__fib6_update_sernum_upto_root(rt, fib6_new_sernum(info->nl_net));
Could a concurrent lookup on another CPU reach ip6_rt_pcpu_alloc() in
between and record the pre-bump value?
pcpu_rt->sernum = rt_genid_ipv6(dev_net(dev));
For the first route insert in netns A, that value would be 1.
On the destination side, addrconf_notify() can skip route insertion on
NETDEV_UP:
case NETDEV_UP:
case NETDEV_CHANGE:
if (idev && idev->cnf.disable_ipv6)
break;
if (dev->priv_flags & IFF_NO_ADDRCONF) {
...
break;
}
It also skips when addrconf_link_ready() fails. In those cases netns B can
still be at 1 when the moved device first transmits.
Would a socket in A that cached the nexthop route with sernum 1 then pass
rt6_is_valid() against B's 1? If so, it would keep sending through the
device that now belongs to B.
Would seeding each netns's initial fib6_sernum from fib6_sernum_seq close
this?
> +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));
[Severity: Medium]
Does the "never matches" guarantee in the comment above still hold once
fib6_sernum_seq wraps from INT_MAX back to 2?
Netns A keeps the same value s for as long as its tree does not change.
Nexthop routes newly cached there keep copying s in ip6_rt_pcpu_alloc().
Meanwhile the shared counter advances on every fib6_add()/fib6_del(),
fib6_flush_trees(), nexthop update and PMTU exception insert in any netns.
The PMTU case goes through rt6_insert_exception()->fib6_update_sernum(),
and a remote host can trigger it with ICMPv6 Packet Too Big.
After INT_MAX - 1 draws, s is handed out again. It could go to netns B,
for example through the NETDEV_UP route insert after a device moves from
A to B.
rt6_is_valid() only compares integers:
return rt6->sernum == rt_genid_ipv6(dev_net(rt6->dst.dev));
and ip6_dst_check() accepts the dst on that result alone:
if (rt->sernum)
return rt6_is_valid(rt) ? dst : NULL;
If this happens before nexthop_free_rcu() releases A's per-cpu routes,
could a socket in A keep transmitting through the device that now belongs
to B?
Before this patch, no other netns could advance A's or B's counter. Now
any netns can push the shared sequence through its cycle. That includes
a netns owned by an unprivileged user namespace that loops
RTM_NEWROUTE/RTM_DELROUTE.
The commit message doesn't mention this limit. The next patch in the
series, "net: remove rcu_barrier() from __dev_change_net_namespace()",
appears to rely on the same guarantee and leaves fib6_new_sernum()
unchanged.
To get the guarantee the comment describes, would the cached route need to
be bound to its netns, or would a wider generation counter be needed?
> + atomic_set(&net->ipv6.fib6_sernum, new);
>
> return new;
> }
[Severity: Medium]
This is a pre-existing issue and was not introduced by this patch. Still,
the new scheme depends on rt6_is_valid() correctly reading the netns that
the device belongs to right now, and rt6_is_valid() uses plain dev_net()
with no RCU protection:
return rt6->sernum == rt_genid_ipv6(dev_net(rt6->dst.dev));
ip6_dst_check() calls it before taking rcu_read_lock():
if (rt->sernum)
return rt6_is_valid(rt) ? dst : NULL;
rcu_read_lock();
The IPv4 counterpart, rt_is_expired(), does this read under RCU:
rcu_read_lock();
res = rth->rt_genid != rt_genid_ipv4(dev_net_rcu(rth->dst.dev));
rcu_read_unlock();
ip6_dst_check() is reached from sk_dst_check(), which gets the dst through
sk_dst_get(). That helper drops RCU before ->check() runs, for example on
udpv6_sendmsg()->ip6_sk_dst_lookup_flow(). The dst keeps the net_device
alive, but not the netns behind dev->nd_net.
Suppose the reader is preempted after loading nd_net, and in the meantime
the device leaves that netns and the netns is torn down. Could the reader
then read net->ipv6.fib6_sernum from freed memory?
Would it make sense to use rcu_read_lock() and dev_net_rcu() here, as the
IPv4 side does?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261019-bug-net-netns-move-rcu-barrier-v2-0-b6eb7780c492%40gmail.com
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH net-next v2 3/3] net: remove rcu_barrier() from __dev_change_net_namespace()
2026-10-19 3:31 ` [PATCH net-next v2 3/3] net: remove rcu_barrier() from __dev_change_net_namespace() Qiliang Yuan
@ 2026-10-21 2:44 ` netdev-bot+sashiko
0 siblings, 0 replies; 9+ messages in thread
From: netdev-bot+sashiko @ 2026-10-21 2:44 UTC (permalink / raw)
To: odys.yuan
Cc: davem, edumazet, kuba, pabeni, horms, dsahern, idosch, razor,
kuniyu, netdev, linux-kernel
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider.
Critical: 0 · High: 1 · Medium: 0 · Low: 0
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [High] net: stale IPv6 nexthop exception dst and wrong-netns stats
after netns move
--
> diff --git a/net/core/dev.c b/net/core/dev.c
> --- a/net/core/dev.c
> +++ b/net/core/dev.c
> @@ -12994,7 +12994,6 @@ int __dev_change_net_namespace(struct net_device *dev, struct net *net,
> * the device is just moving and can keep their slaves up.
> */
> call_netdevice_notifiers(NETDEV_UNREGISTER, dev);
> - rcu_barrier();
>
> new_nsid = peernet2id_alloc(dev_net(dev), net, GFP_KERNEL);
>
>
[Severity: High]
Removing the rcu_barrier() here looks like it opens a window for IPv6
nexthop exception routes that the changelog does not cover.
Consider a route in net_old that uses a nexthop group with more than
one member, where one member, nh1, is bound to dev. When dev is moved,
nexthop_flush_dev() calls remove_nexthop() on nh1. Because the group
still has other members, remove_nh_grp_entry() only drops nh1 from the
group. The fib6_info stays, its fn_sernum is not bumped, and nh1's
exception bucket is not flushed synchronously.
That flush happens later, in this chain:
nexthop_free_rcu()
-> nexthop_free_single()
-> fib6_nh_release()
-> fib6_nh_flush_exceptions()
remove_nexthop() queues it through nexthop_put() and call_rcu_hurry().
Under RTNL, synchronize_net() is synchronize_rcu_expedited(), which
does not wait for that callback. Without the barrier,
dev_net_set(dev, net) can therefore run first. Two things then go
wrong:
- RTF_CACHE exceptions created by ip6_rt_cache_alloc() have
rt->sernum == 0. ip6_dst_check() then skips rt6_is_valid() and goes
through rt6_check(), which still passes because rt->from is alive in
net_old with an unchanged cookie. A socket in net_old can keep
transmitting through dev after it has moved to the new netns, until
the callback finally calls dst_dev_put(). The fib6_sernum argument
in the changelog does not seem to hold for these entries.
- rt6_remove_exception() uses dev_net(rt6_ex->rt6i->dst.dev) to pick
which rt6_stats->fib_rt_cache to decrement. When it runs after the
move, it decrements the new netns counter instead of net_old's,
leaving both counters wrong.
Was this case considered? If the barrier goes, it seems the nexthop
code would need to flush nh1's exceptions synchronously when nh1 is
removed from a group because its device is going away, instead of
relying on the RCU callback. Alternatively, rt6_remove_exception()
could avoid deriving the netns from the dst device. Which direction
would you prefer?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261019-bug-net-netns-move-rcu-barrier-v2-0-b6eb7780c492%40gmail.com
^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-10-21 2:44 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-19 3:31 [PATCH net-next v2 0/3] net: make moving a device to another netns cheaper Qiliang Yuan
2026-10-19 3:31 ` [PATCH net-next v2 1/3] ipv4: draw rt_genid from a sequence shared by all netns Qiliang Yuan
2026-10-20 4:14 ` Kuniyuki Iwashima
2026-10-21 2:44 ` netdev-bot+sashiko
2026-10-19 3:31 ` [PATCH net-next v2 2/3] ipv6: take fib6_sernum " Qiliang Yuan
2026-10-20 4:17 ` Kuniyuki Iwashima
2026-10-21 2:44 ` netdev-bot+sashiko
2026-10-19 3:31 ` [PATCH net-next v2 3/3] net: remove rcu_barrier() from __dev_change_net_namespace() Qiliang Yuan
2026-10-21 2:44 ` netdev-bot+sashiko
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®