Re: [PATCH net] ipv6: serialize address publication with addrconf_ifdown

From: Ido Schimmel

Date: Sun Oct 04 2026 - 09:52:57 EST


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@xxxxxxxxxxxxxxx
> Assisted-by: LLM
> Signed-off-by: Daehyeon Ko <4ncienth@xxxxxxxxx>

[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);