From 3e6af2e485e4cb8bcef0690bdc9a60902b77f905 Mon Sep 17 00:00:00 2001 From: Kuniyuki Iwashima Date: Wed, 16 Sep 2026 23:02:20 +0000 Subject: [PATCH 1/6] ip6_gre: Initialise ign->tunnels_wc[0] before register_netdev(). ip6gre_init_net() links the fallback device ip6gre0 to ign->tunnels_wc[0] after publishing it by register_netdev(). While ip6_gre module is being loaded, there is a small race window. If ioctl(SIOCADDTUNNEL) is called before ip6gre0 is linked to the hash table, a new device could be linked to ign->tunnels_wc[0]. Then, the fallback device overwrites ign->tunnels_wc[0], and the new device will be no longer found by ip6gre_tunnel_lookup(), resulting in no RX. Let's link the fallback device before register_netdev(). Signed-off-by: Kuniyuki Iwashima Link: https://patch.msgid.link/20260916230353.367014-2-kuniyu@google.com Signed-off-by: Jakub Kicinski --- net/ipv6/ip6_gre.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/net/ipv6/ip6_gre.c b/net/ipv6/ip6_gre.c index 8ebda0b6a78b..183218100d69 100644 --- a/net/ipv6/ip6_gre.c +++ b/net/ipv6/ip6_gre.c @@ -1607,12 +1607,13 @@ static int __net_init ip6gre_init_net(struct net *net) ip6gre_fb_tunnel_init(ign->fb_tunnel_dev); ign->fb_tunnel_dev->rtnl_link_ops = &ip6gre_link_ops; + rcu_assign_pointer(ign->tunnels_wc[0], + netdev_priv(ign->fb_tunnel_dev)); + err = register_netdev(ign->fb_tunnel_dev); if (err) goto err_reg_dev; - rcu_assign_pointer(ign->tunnels_wc[0], - netdev_priv(ign->fb_tunnel_dev)); return 0; err_reg_dev: From 3b2813cc6b1f89de3c694ad3c3a6cd9c924d580b Mon Sep 17 00:00:00 2001 From: Kuniyuki Iwashima Date: Wed, 16 Sep 2026 23:02:21 +0000 Subject: [PATCH 2/6] ip6_gre: Convert ip6gre_net.tunnels[][] to hlist. struct ip6_tnl has a pointer to the next one. Single linked list requires O(n) iteration for device removal and IPv4 tunnel already uses hlist for its hash table. Let's convert ip6gre_net.tunnels[][] to hlist. Once we convert other users (ip6_tunnel.c, sit.c), we can remove the *next pointer from struct ip6_tnl. Signed-off-by: Kuniyuki Iwashima Link: https://patch.msgid.link/20260916230353.367014-3-kuniyu@google.com Signed-off-by: Jakub Kicinski --- include/net/ip6_tunnel.h | 1 + net/ipv6/ip6_gre.c | 91 +++++++++++++++++++++------------------- 2 files changed, 48 insertions(+), 44 deletions(-) diff --git a/include/net/ip6_tunnel.h b/include/net/ip6_tunnel.h index b99805ee2fd1..d1f0a427e9c8 100644 --- a/include/net/ip6_tunnel.h +++ b/include/net/ip6_tunnel.h @@ -45,6 +45,7 @@ struct __ip6_tnl_parm { /* IPv6 tunnel */ struct ip6_tnl { struct ip6_tnl __rcu *next; /* next tunnel in list */ + struct hlist_node hash_node; struct net_device *dev; /* virtual device associated with tunnel */ netdevice_tracker dev_tracker; struct net *net; /* netns for packet i/o */ diff --git a/net/ipv6/ip6_gre.c b/net/ipv6/ip6_gre.c index 183218100d69..d3a879fa296e 100644 --- a/net/ipv6/ip6_gre.c +++ b/net/ipv6/ip6_gre.c @@ -64,7 +64,7 @@ MODULE_PARM_DESC(log_ecn_error, "Log packets received with corrupted ECN"); static unsigned int ip6gre_net_id __read_mostly; struct ip6gre_net { - struct ip6_tnl __rcu *tunnels[4][IP6_GRE_HASH_SIZE]; + struct hlist_head tunnels[4][IP6_GRE_HASH_SIZE]; struct ip6_tnl __rcu *collect_md_tun; struct ip6_tnl __rcu *collect_md_tun_erspan; @@ -141,20 +141,24 @@ static struct ip6_tnl *ip6gre_tunnel_lookup(struct net_device *dev, const struct in6_addr *remote, const struct in6_addr *local, __be32 key, __be16 gre_proto) { - struct net *net = dev_net(dev); - int link = dev->ifindex; - unsigned int h0 = HASH_ADDR(remote); - unsigned int h1 = HASH_KEY(key); - struct ip6_tnl *t, *cand = NULL; - struct ip6gre_net *ign = net_generic(net, ip6gre_net_id); int dev_type = (gre_proto == htons(ETH_P_TEB) || gre_proto == htons(ETH_P_ERSPAN) || gre_proto == htons(ETH_P_ERSPAN2)) ? ARPHRD_ETHER : ARPHRD_IP6GRE; + unsigned int h0 = HASH_ADDR(remote); + unsigned int h1 = HASH_KEY(key); + struct ip6_tnl *t, *cand = NULL; + struct net *net = dev_net(dev); struct net_device *ndev; + struct hlist_head *head; + int link = dev->ifindex; + struct ip6gre_net *ign; int cand_score = 4; - for_each_ip_tunnel_rcu(t, ign->tunnels_r_l[h0 ^ h1]) { + ign = net_generic(net, ip6gre_net_id); + + head = &ign->tunnels_r_l[h0 ^ h1]; + hlist_for_each_entry_rcu(t, head, hash_node) { if (!ipv6_addr_equal(local, &t->parms.laddr) || !ipv6_addr_equal(remote, &t->parms.raddr) || key != t->parms.i_key || @@ -165,7 +169,8 @@ static struct ip6_tnl *ip6gre_tunnel_lookup(struct net_device *dev, return cand; } - for_each_ip_tunnel_rcu(t, ign->tunnels_r[h0 ^ h1]) { + head = &ign->tunnels_r[h0 ^ h1]; + hlist_for_each_entry_rcu(t, head, hash_node) { if (!ipv6_addr_equal(remote, &t->parms.raddr) || key != t->parms.i_key || !(t->dev->flags & IFF_UP)) @@ -175,7 +180,8 @@ static struct ip6_tnl *ip6gre_tunnel_lookup(struct net_device *dev, return cand; } - for_each_ip_tunnel_rcu(t, ign->tunnels_l[h1]) { + head = &ign->tunnels_l[h1]; + hlist_for_each_entry_rcu(t, head, hash_node) { if ((!ipv6_addr_equal(local, &t->parms.laddr) && (!ipv6_addr_equal(local, &t->parms.raddr) || !ipv6_addr_is_multicast(local))) || @@ -187,7 +193,8 @@ static struct ip6_tnl *ip6gre_tunnel_lookup(struct net_device *dev, return cand; } - for_each_ip_tunnel_rcu(t, ign->tunnels_wc[h1]) { + head = &ign->tunnels_wc[h1]; + hlist_for_each_entry_rcu(t, head, hash_node) { if (t->parms.i_key != key || !(t->dev->flags & IFF_UP)) continue; @@ -215,8 +222,8 @@ static struct ip6_tnl *ip6gre_tunnel_lookup(struct net_device *dev, return NULL; } -static struct ip6_tnl __rcu **__ip6gre_bucket(struct ip6gre_net *ign, - const struct __ip6_tnl_parm *p) +static struct hlist_head *__ip6gre_bucket(struct ip6gre_net *ign, + const struct __ip6_tnl_parm *p) { const struct in6_addr *remote = &p->raddr; const struct in6_addr *local = &p->laddr; @@ -258,56 +265,46 @@ static void ip6erspan_tunnel_unlink_md(struct ip6gre_net *ign, rcu_assign_pointer(ign->collect_md_tun_erspan, NULL); } -static inline struct ip6_tnl __rcu **ip6gre_bucket(struct ip6gre_net *ign, - const struct ip6_tnl *t) +static inline struct hlist_head *ip6gre_bucket(struct ip6gre_net *ign, + const struct ip6_tnl *t) { return __ip6gre_bucket(ign, &t->parms); } static void ip6gre_tunnel_link(struct ip6gre_net *ign, struct ip6_tnl *t) { - struct ip6_tnl __rcu **tp = ip6gre_bucket(ign, t); + struct hlist_head *head = ip6gre_bucket(ign, t); - rcu_assign_pointer(t->next, rtnl_dereference(*tp)); - rcu_assign_pointer(*tp, t); + hlist_add_head_rcu(&t->hash_node, head); } static void ip6gre_tunnel_unlink(struct ip6gre_net *ign, struct ip6_tnl *t) { - struct ip6_tnl __rcu **tp; - struct ip6_tnl *iter; - - for (tp = ip6gre_bucket(ign, t); - (iter = rtnl_dereference(*tp)) != NULL; - tp = &iter->next) { - if (t == iter) { - rcu_assign_pointer(*tp, t->next); - break; - } - } + hlist_del_init_rcu(&t->hash_node); } static struct ip6_tnl *ip6gre_tunnel_find(struct net *net, const struct __ip6_tnl_parm *parms, int type) { + struct ip6gre_net *ign = net_generic(net, ip6gre_net_id); const struct in6_addr *remote = &parms->raddr; const struct in6_addr *local = &parms->laddr; __be32 key = parms->i_key; + struct hlist_head *head; int link = parms->link; struct ip6_tnl *t; - struct ip6_tnl __rcu **tp; - struct ip6gre_net *ign = net_generic(net, ip6gre_net_id); - for (tp = __ip6gre_bucket(ign, parms); - (t = rtnl_dereference(*tp)) != NULL; - tp = &t->next) + head = __ip6gre_bucket(ign, parms); + + hlist_for_each_entry(t, head, hash_node) { if (ipv6_addr_equal(local, &t->parms.laddr) && ipv6_addr_equal(remote, &t->parms.raddr) && key == t->parms.i_key && link == t->parms.link && type == t->dev->type) break; + } return t; } @@ -1551,7 +1548,8 @@ static struct inet6_protocol ip6gre_protocol __read_mostly = { .flags = INET6_PROTO_FINAL, }; -static void __net_exit ip6gre_exit_rtnl_net(struct net *net, struct list_head *head) +static void __net_exit ip6gre_exit_rtnl_net(struct net *net, + struct list_head *dev_kill_list) { struct ip6gre_net *ign = net_generic(net, ip6gre_net_id); struct net_device *dev, *aux; @@ -1561,23 +1559,21 @@ static void __net_exit ip6gre_exit_rtnl_net(struct net *net, struct list_head *h if (dev->rtnl_link_ops == &ip6gre_link_ops || dev->rtnl_link_ops == &ip6gre_tap_ops || dev->rtnl_link_ops == &ip6erspan_tap_ops) - unregister_netdevice_queue(dev, head); + unregister_netdevice_queue(dev, dev_kill_list); for (prio = 0; prio < 4; prio++) { int h; + for (h = 0; h < IP6_GRE_HASH_SIZE; h++) { + struct hlist_head *head = &ign->tunnels[prio][h]; struct ip6_tnl *t; - t = rtnl_net_dereference(net, ign->tunnels[prio][h]); - - while (t) { + hlist_for_each_entry(t, head, hash_node) { /* If dev is in the same netns, it has already * been added to the list by the previous loop. */ if (!net_eq(dev_net(t->dev), net)) - unregister_netdevice_queue(t->dev, head); - - t = rtnl_net_dereference(net, t->next); + unregister_netdevice_queue(t->dev, dev_kill_list); } } } @@ -1587,8 +1583,15 @@ static int __net_init ip6gre_init_net(struct net *net) { struct ip6gre_net *ign = net_generic(net, ip6gre_net_id); struct net_device *ndev; + struct ip6_tnl *t; + int prio, h; int err; + for (prio = 0; prio < 4; prio++) { + for (h = 0; h < IP6_GRE_HASH_SIZE; h++) + INIT_HLIST_HEAD(&ign->tunnels[prio][h]); + } + if (!net_has_fallback_tunnels(net)) return 0; ndev = alloc_netdev(sizeof(struct ip6_tnl), "ip6gre0", @@ -1607,8 +1610,8 @@ static int __net_init ip6gre_init_net(struct net *net) ip6gre_fb_tunnel_init(ign->fb_tunnel_dev); ign->fb_tunnel_dev->rtnl_link_ops = &ip6gre_link_ops; - rcu_assign_pointer(ign->tunnels_wc[0], - netdev_priv(ign->fb_tunnel_dev)); + t = netdev_priv(ign->fb_tunnel_dev); + hlist_add_head_rcu(&t->hash_node, &ign->tunnels_wc[0]); err = register_netdev(ign->fb_tunnel_dev); if (err) From 28328796e0b4ce5dc8f5fc6078823dc42fcf32dd Mon Sep 17 00:00:00 2001 From: Kuniyuki Iwashima Date: Wed, 16 Sep 2026 23:02:22 +0000 Subject: [PATCH 3/6] ip6_gre: Clear ign->fb_tunnel_dev in ip6gre_exit_rtnl_net(). ip6gre_dellink() ignores itn->fb_tunnel_dev, so the per-netns fallback tunnel device cannot be removed by userspace. This also makes default_device_exit_batch() impossible to remove the device since it calls ->dellink(). So, ip6gre_exit_rtnl_net() has to iterate devices in the dying netns and call unregister_netdevice_queue() directly, and ip6gre_tunnel_unlink() must be called at ->ndo_uninit(). For per-netns device unregistration, we want to check if the device is being removed in ->dellink(), for which we must call ip6gre_tunnel_unlink() before unregister_netdevice_queue(). As a prep, let's set itn->fb_tunnel_dev to NULL and remove remove for_each_netdev_safe() in ip6gre_exit_rtnl_net(). Signed-off-by: Kuniyuki Iwashima Link: https://patch.msgid.link/20260916230353.367014-4-kuniyu@google.com Signed-off-by: Jakub Kicinski --- net/ipv6/ip6_gre.c | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/net/ipv6/ip6_gre.c b/net/ipv6/ip6_gre.c index d3a879fa296e..d5d98474aeb5 100644 --- a/net/ipv6/ip6_gre.c +++ b/net/ipv6/ip6_gre.c @@ -374,8 +374,6 @@ static void ip6gre_tunnel_uninit(struct net_device *dev) ip6gre_tunnel_unlink_md(ign, t); ip6gre_tunnel_unlink(ign, t); - if (ign->fb_tunnel_dev == dev) - WRITE_ONCE(ign->fb_tunnel_dev, NULL); dst_cache_reset(&t->dst_cache); netdev_put(dev, &t->dev_tracker); } @@ -1552,29 +1550,20 @@ static void __net_exit ip6gre_exit_rtnl_net(struct net *net, struct list_head *dev_kill_list) { struct ip6gre_net *ign = net_generic(net, ip6gre_net_id); - struct net_device *dev, *aux; int prio; - for_each_netdev_safe(net, dev, aux) - if (dev->rtnl_link_ops == &ip6gre_link_ops || - dev->rtnl_link_ops == &ip6gre_tap_ops || - dev->rtnl_link_ops == &ip6erspan_tap_ops) - unregister_netdevice_queue(dev, dev_kill_list); + WRITE_ONCE(ign->fb_tunnel_dev, NULL); for (prio = 0; prio < 4; prio++) { int h; for (h = 0; h < IP6_GRE_HASH_SIZE; h++) { struct hlist_head *head = &ign->tunnels[prio][h]; + struct hlist_node *tmp; struct ip6_tnl *t; - hlist_for_each_entry(t, head, hash_node) { - /* If dev is in the same netns, it has already - * been added to the list by the previous loop. - */ - if (!net_eq(dev_net(t->dev), net)) - unregister_netdevice_queue(t->dev, dev_kill_list); - } + hlist_for_each_entry_safe(t, tmp, head, hash_node) + unregister_netdevice_queue(t->dev, dev_kill_list); } } } From b7c0d393e108a2f6f56dc36d6c1e03591b0e689e Mon Sep 17 00:00:00 2001 From: Kuniyuki Iwashima Date: Wed, 16 Sep 2026 23:02:23 +0000 Subject: [PATCH 4/6] ip6_gre: Unlink ip6gre_tunnel_unlink() from ->dellink(). With the previous patch, itn->fb_tunnel_dev can be removed via ->dellink(). However, ioctl(SIOCDELTUNNEL) still uses unregister_netdevice(), which requires ip6gre_tunnel_unlink() in ip6gre_tunnel_uninit(). Let's use __ip6gre_dellink() everywhere to remove ip6gre (and variant) devices and remove ip6gre_tunnel_unlink() and ip6gre_tunnel_unlink_md() in ip6gre_tunnel_uninit(). ip6erspan_tunnel_uninit() is also changed accordingly. Signed-off-by: Kuniyuki Iwashima Link: https://patch.msgid.link/20260916230353.367014-5-kuniyu@google.com Signed-off-by: Jakub Kicinski --- net/ipv6/ip6_gre.c | 50 +++++++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/net/ipv6/ip6_gre.c b/net/ipv6/ip6_gre.c index d5d98474aeb5..a9059f3841ac 100644 --- a/net/ipv6/ip6_gre.c +++ b/net/ipv6/ip6_gre.c @@ -79,6 +79,7 @@ static void ip6gre_tunnel_setup(struct net_device *dev); static void ip6gre_tunnel_link(struct ip6gre_net *ign, struct ip6_tnl *t); static void ip6gre_tnl_link_config(struct ip6_tnl *t, int set_mtu); static void ip6erspan_tnl_link_config(struct ip6_tnl *t, int set_mtu); +static void __ip6gre_dellink(struct net_device *dev, struct list_head *head); /* Tunnel hash table */ @@ -359,10 +360,7 @@ failed_free: static void ip6erspan_tunnel_uninit(struct net_device *dev) { struct ip6_tnl *t = netdev_priv(dev); - struct ip6gre_net *ign = net_generic(t->net, ip6gre_net_id); - ip6erspan_tunnel_unlink_md(ign, t); - ip6gre_tunnel_unlink(ign, t); dst_cache_reset(&t->dst_cache); netdev_put(dev, &t->dev_tracker); } @@ -370,10 +368,7 @@ static void ip6erspan_tunnel_uninit(struct net_device *dev) static void ip6gre_tunnel_uninit(struct net_device *dev) { struct ip6_tnl *t = netdev_priv(dev); - struct ip6gre_net *ign = net_generic(t->net, ip6gre_net_id); - ip6gre_tunnel_unlink_md(ign, t); - ip6gre_tunnel_unlink(ign, t); dst_cache_reset(&t->dst_cache); netdev_put(dev, &t->dev_tracker); } @@ -1251,14 +1246,15 @@ static int ip6gre_tunnel_siocdevprivate(struct net_device *dev, struct ifreq *ifr, void __user *data, int cmd) { - int err = 0; - struct ip6_tnl_parm2 p; - struct __ip6_tnl_parm p1; struct ip6_tnl *t = netdev_priv(dev); + struct __ip6_tnl_parm p1 = {}; + LIST_HEAD(dev_kill_list); struct net *net = t->net; - struct ip6gre_net *ign = net_generic(net, ip6gre_net_id); + struct ip6_tnl_parm2 p; + struct ip6gre_net *ign; + int err = 0; - memset(&p1, 0, sizeof(p1)); + ign = net_generic(net, ip6gre_net_id); switch (cmd) { case SIOCGETTUNNEL: @@ -1347,7 +1343,8 @@ static int ip6gre_tunnel_siocdevprivate(struct net_device *dev, goto done; dev = t->dev; } - unregister_netdevice(dev); + + __ip6gre_dellink(dev, &dev_kill_list); err = 0; break; @@ -1356,6 +1353,7 @@ static int ip6gre_tunnel_siocdevprivate(struct net_device *dev, } done: + unregister_netdevice_many(&dev_kill_list); return err; } @@ -1563,7 +1561,7 @@ static void __net_exit ip6gre_exit_rtnl_net(struct net *net, struct ip6_tnl *t; hlist_for_each_entry_safe(t, tmp, head, hash_node) - unregister_netdevice_queue(t->dev, dev_kill_list); + __ip6gre_dellink(t->dev, dev_kill_list); } } } @@ -2056,13 +2054,31 @@ static int ip6gre_changelink(struct net_device *dev, struct nlattr *tb[], return 0; } +static void __ip6gre_dellink(struct net_device *dev, struct list_head *head) +{ + struct ip6_tnl *t = netdev_priv(dev); + struct ip6gre_net *ign; + + ign = net_generic(t->net, ip6gre_net_id); + + if (dev->rtnl_link_ops == &ip6erspan_tap_ops) + ip6erspan_tunnel_unlink_md(ign, t); + else + ip6gre_tunnel_unlink_md(ign, t); + + ip6gre_tunnel_unlink(ign, t); + unregister_netdevice_queue(dev, head); +} + static void ip6gre_dellink(struct net_device *dev, struct list_head *head) { - struct net *net = dev_net(dev); - struct ip6gre_net *ign = net_generic(net, ip6gre_net_id); + struct ip6_tnl *t = netdev_priv(dev); + struct ip6gre_net *ign; + + ign = net_generic(t->net, ip6gre_net_id); if (dev != ign->fb_tunnel_dev) - unregister_netdevice_queue(dev, head); + __ip6gre_dellink(dev, head); } static size_t ip6gre_get_size(const struct net_device *dev) @@ -2304,6 +2320,7 @@ static struct rtnl_link_ops ip6gre_tap_ops __read_mostly = { .validate = ip6gre_tap_validate, .newlink = ip6gre_newlink, .changelink = ip6gre_changelink, + .dellink = ip6gre_dellink, .get_size = ip6gre_get_size, .fill_info = ip6gre_fill_info, .get_link_net = ip6_tnl_get_link_net, @@ -2318,6 +2335,7 @@ static struct rtnl_link_ops ip6erspan_tap_ops __read_mostly = { .validate = ip6erspan_tap_validate, .newlink = ip6erspan_newlink, .changelink = ip6erspan_changelink, + .dellink = ip6gre_dellink, .get_size = ip6gre_get_size, .fill_info = ip6gre_fill_info, .get_link_net = ip6_tnl_get_link_net, From cce829e2aa1d1cbf2f04ab33c3a964220ad19793 Mon Sep 17 00:00:00 2001 From: Kuniyuki Iwashima Date: Wed, 16 Sep 2026 23:02:24 +0000 Subject: [PATCH 5/6] ip6_gre: Protect ip6gre_net.tunnels[][] with mutex. struct ip6_tnl.net is the netns where encapsulated packets flow into. struct ip6_tnl is linked to ip6gre_net.tunnels[][] of netns. During netns dismantle or module unload, ip6gre_exit_rtnl_net() iterates the list and queues devices for destruction regardless of the devices' netns. Thus, once RTNL is removed, the list can be modified concurrently from different netns due to device removal. Let's protect it with per-netns mutex. Note that dev_siocdevprivate() calls netdev_lock_ops() but it must be NOP for tunnel devices to avoid AB-BA deadlock. DEBUG_NET_WARN_ON_ONCE() is added to annotate the locking explicitly. Signed-off-by: Kuniyuki Iwashima Link: https://patch.msgid.link/20260916230353.367014-6-kuniyu@google.com Signed-off-by: Jakub Kicinski --- net/ipv6/ip6_gre.c | 78 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/net/ipv6/ip6_gre.c b/net/ipv6/ip6_gre.c index a9059f3841ac..ac1087330465 100644 --- a/net/ipv6/ip6_gre.c +++ b/net/ipv6/ip6_gre.c @@ -65,6 +65,7 @@ MODULE_PARM_DESC(log_ecn_error, "Log packets received with corrupted ECN"); static unsigned int ip6gre_net_id __read_mostly; struct ip6gre_net { struct hlist_head tunnels[4][IP6_GRE_HASH_SIZE]; + struct mutex tunnels_lock; struct ip6_tnl __rcu *collect_md_tun; struct ip6_tnl __rcu *collect_md_tun_erspan; @@ -1254,8 +1255,12 @@ static int ip6gre_tunnel_siocdevprivate(struct net_device *dev, struct ip6gre_net *ign; int err = 0; + DEBUG_NET_WARN_ON_ONCE(netdev_need_ops_lock(dev)); + ign = net_generic(net, ip6gre_net_id); + mutex_lock(&ign->tunnels_lock); + switch (cmd) { case SIOCGETTUNNEL: if (dev == ign->fb_tunnel_dev) { @@ -1353,6 +1358,8 @@ static int ip6gre_tunnel_siocdevprivate(struct net_device *dev, } done: + mutex_unlock(&ign->tunnels_lock); + unregister_netdevice_many(&dev_kill_list); return err; } @@ -1552,6 +1559,8 @@ static void __net_exit ip6gre_exit_rtnl_net(struct net *net, WRITE_ONCE(ign->fb_tunnel_dev, NULL); + mutex_lock(&ign->tunnels_lock); + for (prio = 0; prio < 4; prio++) { int h; @@ -1564,6 +1573,8 @@ static void __net_exit ip6gre_exit_rtnl_net(struct net *net, __ip6gre_dellink(t->dev, dev_kill_list); } } + + mutex_unlock(&ign->tunnels_lock); } static int __net_init ip6gre_init_net(struct net *net) @@ -1579,6 +1590,8 @@ static int __net_init ip6gre_init_net(struct net *net) INIT_HLIST_HEAD(&ign->tunnels[prio][h]); } + mutex_init(&ign->tunnels_lock); + if (!net_has_fallback_tunnels(net)) return 0; ndev = alloc_netdev(sizeof(struct ip6_tnl), "ip6gre0", @@ -1975,18 +1988,22 @@ static int ip6gre_newlink(struct net_device *dev, struct nlattr **data = params->data; struct nlattr **tb = params->tb; struct ip6gre_net *ign; - int err; + int err = 0; ip6gre_netlink_parms(data, &nt->parms); ign = net_generic(net, ip6gre_net_id); + mutex_lock(&ign->tunnels_lock); + if (nt->parms.collect_md) { if (rtnl_dereference(ign->collect_md_tun)) - return -EEXIST; + err = -EEXIST; } else { if (ip6gre_tunnel_find(net, &nt->parms, dev->type)) - return -EEXIST; + err = -EEXIST; } + if (err) + goto unlock; err = ip6gre_newlink_common(net, dev, tb, data, extack); if (!err) { @@ -1994,6 +2011,10 @@ static int ip6gre_newlink(struct net_device *dev, ip6gre_tunnel_link_md(ign, nt); ip6gre_tunnel_link(net_generic(net, ip6gre_net_id), nt); } + +unlock: + mutex_unlock(&ign->tunnels_lock); + return err; } @@ -2036,22 +2057,32 @@ static int ip6gre_changelink(struct net_device *dev, struct nlattr *tb[], struct netlink_ext_ack *extack) { struct ip6_tnl *t = netdev_priv(dev); - struct ip6gre_net *ign = net_generic(t->net, ip6gre_net_id); struct __ip6_tnl_parm p; + struct ip6gre_net *ign; + int err = 0; + + ign = net_generic(t->net, ip6gre_net_id); if (!rtnl_dev_link_net_capable(dev, t->net)) return -EPERM; + mutex_lock(&ign->tunnels_lock); + t = ip6gre_changelink_common(dev, tb, data, &p, extack); - if (IS_ERR(t)) - return PTR_ERR(t); + if (IS_ERR(t)) { + err = PTR_ERR(t); + goto unlock; + } ip6gre_tunnel_unlink_md(ign, t); ip6gre_tunnel_unlink(ign, t); ip6gre_tnl_change(t, &p, !tb[IFLA_MTU]); ip6gre_tunnel_link_md(ign, t); ip6gre_tunnel_link(ign, t); - return 0; +unlock: + mutex_unlock(&ign->tunnels_lock); + + return err; } static void __ip6gre_dellink(struct net_device *dev, struct list_head *head) @@ -2077,8 +2108,12 @@ static void ip6gre_dellink(struct net_device *dev, struct list_head *head) ign = net_generic(t->net, ip6gre_net_id); + mutex_lock(&ign->tunnels_lock); + if (dev != ign->fb_tunnel_dev) __ip6gre_dellink(dev, head); + + mutex_unlock(&ign->tunnels_lock); } static size_t ip6gre_get_size(const struct net_device *dev) @@ -2234,19 +2269,23 @@ static int ip6erspan_newlink(struct net_device *dev, struct nlattr **data = params->data; struct nlattr **tb = params->tb; struct ip6gre_net *ign; - int err; + int err = 0; ip6gre_netlink_parms(data, &nt->parms); ip6erspan_set_version(data, &nt->parms); ign = net_generic(net, ip6gre_net_id); + mutex_lock(&ign->tunnels_lock); + if (nt->parms.collect_md) { if (rtnl_dereference(ign->collect_md_tun_erspan)) - return -EEXIST; + err = -EEXIST; } else { if (ip6gre_tunnel_find(net, &nt->parms, dev->type)) - return -EEXIST; + err = -EEXIST; } + if (err) + goto unlock; err = ip6gre_newlink_common(net, dev, tb, data, extack); if (!err) { @@ -2254,6 +2293,10 @@ static int ip6erspan_newlink(struct net_device *dev, ip6erspan_tunnel_link_md(ign, nt); ip6gre_tunnel_link(net_generic(net, ip6gre_net_id), nt); } + +unlock: + mutex_unlock(&ign->tunnels_lock); + return err; } @@ -2278,14 +2321,20 @@ static int ip6erspan_changelink(struct net_device *dev, struct nlattr *tb[], struct ip6_tnl *t = netdev_priv(dev); struct __ip6_tnl_parm p; struct ip6gre_net *ign; + int err = 0; if (!rtnl_dev_link_net_capable(dev, t->net)) return -EPERM; ign = net_generic(t->net, ip6gre_net_id); + + mutex_lock(&ign->tunnels_lock); + t = ip6gre_changelink_common(dev, tb, data, &p, extack); - if (IS_ERR(t)) - return PTR_ERR(t); + if (IS_ERR(t)) { + err = PTR_ERR(t); + goto unlock; + } ip6erspan_set_version(data, &p); ip6gre_tunnel_unlink_md(ign, t); @@ -2293,7 +2342,10 @@ static int ip6erspan_changelink(struct net_device *dev, struct nlattr *tb[], ip6erspan_tnl_change(t, &p, !tb[IFLA_MTU]); ip6erspan_tunnel_link_md(ign, t); ip6gre_tunnel_link(ign, t); - return 0; +unlock: + mutex_unlock(&ign->tunnels_lock); + + return err; } static struct rtnl_link_ops ip6gre_link_ops __read_mostly = { From b27a8c62875ea917d0010109e96147d48169116f Mon Sep 17 00:00:00 2001 From: Kuniyuki Iwashima Date: Wed, 16 Sep 2026 23:02:25 +0000 Subject: [PATCH 6/6] ip6_gre: Support per-netns device unregistration. ip6gre_exit_rtnl_net() iterates ip6gre devices and variants whose link_net is dying and queues them for destruction. The devices may reside in different netns. Let's use unregister_netdevice_queue_net() to support per-netns device unregistration. Even after ip6gre_exit_rtnl_net() queues a cross-netns ip6gre device, ip6gre_changelink_common(), ip6gre_dellink(), and ip6gre_tunnel_siocdevprivate() could be called concurrently for it (once RTNL is removed). In such a case, __rtnl_net_unlock() will perform the unregistration. Also, ip6gre_tunnel_siocdevprivate() needs to check check_net(t->net), otherwise it could create a new dev in dying netns after ip6gre_exit_rtnl_net(). In the example below, we can see the fallback tunnel device (ip6gre0) and the cross-netns device (ip6gre1) are unregistered by different processes: # bpftrace -e '#include kprobe:ip6gre_tunnel_uninit { $dev = (struct net_device *)arg0; printf("PID: %d | DEV: %s%s\n", pid, $dev->name, kstack()); } kprobe:ip6gre_exit_rtnl_net { printf("PID: %d%s\n", pid, kstack()); }' & # ip netns add ns1 # ip netns add ns2 # ip -n ns1 link add name ip6gre1 link-netns ns2 \ type ip6gre local 2001:9292::1 remote 2001:9292::2 # ip netns del ns2 PID: 12 ip6gre_exit_rtnl_net+5 ops_undo_list+702 cleanup_net+1122 ... PID: 12 | DEV: ip6gre0 <------ fallback device (itn->fb_tunnel_dev). ip6gre_tunnel_uninit+5 unregister_netdevice_many_notify+7137 unregister_netdevice_many_net+1050 __rtnl_net_unlock+37 ops_undo_list+754 cleanup_net+1122 ... PID: 10 | DEV: ip6gre1 ip6gre_tunnel_uninit+5 unregister_netdevice_many_notify+7137 unregister_netdevice_many_net+1050 rtnl_net_work_func+136 Signed-off-by: Kuniyuki Iwashima Link: https://patch.msgid.link/20260916230353.367014-7-kuniyu@google.com Signed-off-by: Jakub Kicinski --- net/ipv6/ip6_gre.c | 51 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/net/ipv6/ip6_gre.c b/net/ipv6/ip6_gre.c index ac1087330465..c851af22b9fe 100644 --- a/net/ipv6/ip6_gre.c +++ b/net/ipv6/ip6_gre.c @@ -80,7 +80,8 @@ static void ip6gre_tunnel_setup(struct net_device *dev); static void ip6gre_tunnel_link(struct ip6gre_net *ign, struct ip6_tnl *t); static void ip6gre_tnl_link_config(struct ip6_tnl *t, int set_mtu); static void ip6erspan_tnl_link_config(struct ip6_tnl *t, int set_mtu); -static void __ip6gre_dellink(struct net_device *dev, struct list_head *head); +static void __ip6gre_dellink(struct net *net, struct net_device *dev, + struct list_head *head); /* Tunnel hash table */ @@ -285,6 +286,11 @@ static void ip6gre_tunnel_unlink(struct ip6gre_net *ign, struct ip6_tnl *t) hlist_del_init_rcu(&t->hash_node); } +static bool ip6gre_tunnel_unregistering(struct ip6_tnl *t) +{ + return hlist_unhashed(&t->hash_node); +} + static struct ip6_tnl *ip6gre_tunnel_find(struct net *net, const struct __ip6_tnl_parm *parms, int type) @@ -1248,6 +1254,7 @@ static int ip6gre_tunnel_siocdevprivate(struct net_device *dev, int cmd) { struct ip6_tnl *t = netdev_priv(dev); + struct net *orig_net = dev_net(dev); struct __ip6_tnl_parm p1 = {}; LIST_HEAD(dev_kill_list); struct net *net = t->net; @@ -1261,6 +1268,11 @@ static int ip6gre_tunnel_siocdevprivate(struct net_device *dev, mutex_lock(&ign->tunnels_lock); + if (!check_net(net)) { + err = -EBUSY; + goto done; + } + switch (cmd) { case SIOCGETTUNNEL: if (dev == ign->fb_tunnel_dev) { @@ -1302,7 +1314,7 @@ static int ip6gre_tunnel_siocdevprivate(struct net_device *dev, t = ip6gre_tunnel_locate(net, &p1, cmd == SIOCADDTUNNEL); if (dev != ign->fb_tunnel_dev && cmd == SIOCCHGTUNNEL) { - if (t) { + if (t && !ip6gre_tunnel_unregistering(t)) { if (t->dev != dev) { err = -EEXIST; break; @@ -1310,23 +1322,26 @@ static int ip6gre_tunnel_siocdevprivate(struct net_device *dev, } else { t = netdev_priv(dev); - ip6gre_tunnel_unlink(ign, t); - synchronize_net(); - ip6gre_tnl_change(t, &p1, 1); - ip6gre_tunnel_link(ign, t); - netdev_state_change(dev); + if (!ip6gre_tunnel_unregistering(t)) { + ip6gre_tunnel_unlink(ign, t); + synchronize_net(); + ip6gre_tnl_change(t, &p1, 1); + ip6gre_tunnel_link(ign, t); + netdev_state_change(dev); + } } } - if (t) { + if (t && !ip6gre_tunnel_unregistering(t)) { err = 0; memset(&p, 0, sizeof(p)); ip6gre_tnl_parm_to_user(&p, &t->parms); if (copy_to_user(data, &p, sizeof(p))) err = -EFAULT; - } else + } else { err = (cmd == SIOCADDTUNNEL ? -ENOBUFS : -ENOENT); + } break; case SIOCDELTUNNEL: @@ -1349,7 +1364,8 @@ static int ip6gre_tunnel_siocdevprivate(struct net_device *dev, dev = t->dev; } - __ip6gre_dellink(dev, &dev_kill_list); + if (!ip6gre_tunnel_unregistering(t)) + __ip6gre_dellink(orig_net, dev, &dev_kill_list); err = 0; break; @@ -1570,7 +1586,7 @@ static void __net_exit ip6gre_exit_rtnl_net(struct net *net, struct ip6_tnl *t; hlist_for_each_entry_safe(t, tmp, head, hash_node) - __ip6gre_dellink(t->dev, dev_kill_list); + __ip6gre_dellink(net, t->dev, dev_kill_list); } } @@ -2049,6 +2065,9 @@ ip6gre_changelink_common(struct net_device *dev, struct nlattr *tb[], t = nt; } + if (ip6gre_tunnel_unregistering(t)) + return ERR_PTR(-ENODEV); + return t; } @@ -2085,7 +2104,8 @@ unlock: return err; } -static void __ip6gre_dellink(struct net_device *dev, struct list_head *head) +static void __ip6gre_dellink(struct net *net, struct net_device *dev, + struct list_head *head) { struct ip6_tnl *t = netdev_priv(dev); struct ip6gre_net *ign; @@ -2098,7 +2118,7 @@ static void __ip6gre_dellink(struct net_device *dev, struct list_head *head) ip6gre_tunnel_unlink_md(ign, t); ip6gre_tunnel_unlink(ign, t); - unregister_netdevice_queue(dev, head); + unregister_netdevice_queue_net(net, dev, head); } static void ip6gre_dellink(struct net_device *dev, struct list_head *head) @@ -2110,8 +2130,9 @@ static void ip6gre_dellink(struct net_device *dev, struct list_head *head) mutex_lock(&ign->tunnels_lock); - if (dev != ign->fb_tunnel_dev) - __ip6gre_dellink(dev, head); + if (dev != ign->fb_tunnel_dev && + !ip6gre_tunnel_unregistering(t)) + __ip6gre_dellink(dev_net(dev), dev, head); mutex_unlock(&ign->tunnels_lock); }