Re: [PATCH net] ipv6: serialize address publication with addrconf_ifdown
flat view
From: Ido Schimmel <idosch@nvidia.com>
Date: 2026-10-04 13:51:30
Also in:
lkml
Subsystem:
networking [general], networking [ipv4/ipv6], the rest · Maintainers:
"David S. Miller", Eric Dumazet, Jakub Kicinski, Paolo Abeni, David Ahern, Ido Schimmel, Linus Torvalds
On Thu, Oct 01, 2026 at 02:21:50PM +0900, Daehyeon Ko wrote:
ipv6_add_addr() inserts an inet6_ifaddr into the per-net hash before linking it into idev->addr_list. addrconf_ifdown() clears the hash first and snapshots the device list later. A nonblocking add can therefore enter between the two teardown observations. When a non-loopback device's MTU falls below IPV6_MIN_MTU, teardown marks and detaches the idev. A late add can then link the ifaddr into the dead idev. On v7.2, a deterministic interleaving left the object hash-visible with idev->dead=1. ipv6_get_ifaddr() returned it, and later device deletion waited indefinitely with usage count 3. During network namespace exit that wait can stall the single-thread netns cleanup workqueue. MTU changes require CAP_NET_ADMIN in the affected network namespace. Where unprivileged user namespaces are permitted, a local user can obtain that capability in a new user and network namespace. Publish the hash and device-list memberships while holding addrconf_hash_lock followed by idev->lock. Recheck idev->dead and disable_ipv6 before either publication. If teardown wins, the add fails before publishing the object or taking a list reference.
Adding to the per-idev list under the per-netns hash lock looks weird. Can't we instead do the following? 1. Make sure that the dead indication is written under the idev lock. 2. Extend the critical section of the idev lock so that it also covers the addition to the per-netns hash table and only if the device is not dead. Something like [1] (untested). Also, note that this doesn't solve the problem of addrconf_ifdown() and ipv6_add_addr() interleaving when the former doesn't mark the device as dead (e.g., upon NETDEV_DOWN). ipv6_add_addr() can add an address to the hash table after addrconf_ifdown() cleared the hash table, but before it cleared the per-idev list. It will trigger the WARN_ON() in inet6_ifa_finish_destroy(): WARN_ON(!hlist_unhashed(&ifp->addr_lst)); Can be fixed in a second patch [2] (untested) in the series.
The same forced interleaving now returns -ENODEV and device deletion completes without a KASAN or LOCKDEP diagnostic. A real Router Advertisement separately reached ipv6_add_addr() with can_block=false; bounded natural stress did not reproduce the full race. A reproducer is
"bounded natural stress did not reproduce the full race" is LLM speak for "mdelay()s were placed in ipv6_add_addr()"?
available on request.
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Blaming 8814c4b53381 ("[IPV6] ADDRCONF: Convert addrconf_lock to RCU.") seems
more appropriate.
Cc: stable@vger.kernel.org Assisted-by: LLM Signed-off-by: Daehyeon Ko <redacted>
[1]
diff --git a/net/ipv6/addrconf.c b/net/ipv6/addrconf.c
index 3dba94bd2ba0..51add8f91139 100644
--- a/net/ipv6/addrconf.c
+++ b/net/ipv6/addrconf.c@@ -1171,14 +1171,18 @@ ipv6_add_addr(struct inet6_dev *idev, struct ifa6_config *cfg, rcu_read_lock(); - err = ipv6_add_addr_hash(idev->dev, ifa); + write_lock_bh(&idev->lock); + + if (idev->dead) + err = -ENODEV; + else + err = ipv6_add_addr_hash(idev->dev, ifa); if (err < 0) { + write_unlock_bh(&idev->lock); rcu_read_unlock(); goto out; } - write_lock_bh(&idev->lock); - /* Add to inet6_dev unicast addr list. */ ipv6_link_dev_addr(idev, ifa);
@@ -3900,7 +3904,9 @@ static int addrconf_ifdown(struct net_device *dev, bool unregister) * Do not dev_put! */ if (unregister) { + write_lock_bh(&idev->lock); WRITE_ONCE(idev->dead, 1); + write_unlock_bh(&idev->lock); /* protected by rtnl_lock */ RCU_INIT_POINTER(dev->ip6_ptr, NULL);
[2]
diff --git a/net/ipv6/addrconf.c b/net/ipv6/addrconf.c
index 3dba94bd2ba0..eae51b5de690 100644
--- a/net/ipv6/addrconf.c
+++ b/net/ipv6/addrconf.c@@ -4022,6 +4028,9 @@ static int addrconf_ifdown(struct net_device *dev, bool unregister) } if (!keep) { + spin_lock_bh(&net->ipv6.addrconf_hash_lock); + hlist_del_init_rcu(&ifa->addr_lst); + spin_unlock_bh(&net->ipv6.addrconf_hash_lock); write_lock_bh(&idev->lock); list_del_rcu(&ifa->if_list); write_unlock_bh(&idev->lock);