[PATCH net-next 03/14] ipv6: annotate data-races around addrconf sysctls

From: Jiayuan Chen

Date: Sun Nov 08 2026 - 22:01:52 EST


The forwarding setting of conf/all, the per-device autoconf, accept_ra,
accept_ra_min_lft, addr_gen_mode, ra_defrtr_metric, disable_ipv6 and mtu6
settings, and the per-netns idgen_retries and idgen_delay, can be read
concurrently with the sysctl handlers that update them with WRITE_ONCE().
Add READ_ONCE() to the readers.

KCSAN reports it when net.ipv6.conf.<dev>.autoconf is changed while
Router Advertisements are received on the device:

BUG: KCSAN: data-race in addrconf_prefix_rcv / do_proc_int_conv

write (marked) to 0xffff8d7250116df4 of 4 bytes by task 767 on cpu 2:
do_proc_int_conv+0xa9/0xd0
proc_vec_conv+0x21/0x40
proc_vec+0x21e/0x6f0
proc_dointvec+0x16/0x30
proc_sys_call_handler+0x282/0x3a0
proc_sys_write+0x13/0x20
...

read to 0xffff8d7250116df4 of 4 bytes by interrupt on cpu 1:
addrconf_prefix_rcv+0x474/0xf00
ndisc_router_discovery+0xf4f/0x1f80
ndisc_rcv+0x2a2/0x3c0
icmpv6_rcv+0xb46/0xd40
ip6_protocol_deliver_rcu+0x787/0x9a0
ip6_input_finish+0xb0/0x1b0
ip6_input+0xa8/0x250
ip6_mc_input+0x249/0x780
ipv6_rcv+0x1b1/0x1c0
__netif_receive_skb_one_core+0x8f/0xe0
...

value changed: 0x00000001 -> 0x00000000

Signed-off-by: Jiayuan Chen <jiayuan.chen@xxxxxxxxx>
---
net/ipv6/addrconf.c | 22 ++++++++++++----------
net/ipv6/ndisc.c | 2 +-
net/ipv6/route.c | 8 ++++----
3 files changed, 17 insertions(+), 15 deletions(-)

diff --git a/net/ipv6/addrconf.c b/net/ipv6/addrconf.c
index d9d13e78bf13b..40f94124641d4 100644
--- a/net/ipv6/addrconf.c
+++ b/net/ipv6/addrconf.c
@@ -1104,7 +1104,7 @@ ipv6_add_addr(struct inet6_dev *idev, struct ifa6_config *cfg,
goto out;
}

- if (idev->cnf.disable_ipv6) {
+ if (READ_ONCE(idev->cnf.disable_ipv6)) {
NL_SET_ERR_MSG_MOD(extack, "IPv6 is disabled on this device");
err = -EACCES;
goto out;
@@ -2215,7 +2215,7 @@ void addrconf_dad_failure(struct sk_buff *skb, struct inet6_ifaddr *ifp)
.scope = ifp->scope,
};

- if (retries > net->ipv6.sysctl.idgen_retries) {
+ if (retries > READ_ONCE(net->ipv6.sysctl.idgen_retries)) {
net_info_ratelimited("%s: privacy stable address generation failed because of DAD conflicts!\n",
ifp->idev->dev->name);
goto errdad;
@@ -2248,7 +2248,7 @@ void addrconf_dad_failure(struct sk_buff *skb, struct inet6_ifaddr *ifp)
spin_unlock_bh(&ifp2->lock);

if (start_dad)
- addrconf_mod_dad_work(ifp2, net->ipv6.sysctl.idgen_delay);
+ addrconf_mod_dad_work(ifp2, READ_ONCE(net->ipv6.sysctl.idgen_delay));
in6_ifa_put(ifp2);
lock_errdad:
spin_lock_bh(&ifp->lock);
@@ -2706,8 +2706,10 @@ static void manage_tempaddrs(struct inet6_dev *idev,

static bool is_addr_mode_generate_stable(struct inet6_dev *idev)
{
- return idev->cnf.addr_gen_mode == IN6_ADDR_GEN_MODE_STABLE_PRIVACY ||
- idev->cnf.addr_gen_mode == IN6_ADDR_GEN_MODE_RANDOM;
+ u32 mode = READ_ONCE(idev->cnf.addr_gen_mode);
+
+ return mode == IN6_ADDR_GEN_MODE_STABLE_PRIVACY ||
+ mode == IN6_ADDR_GEN_MODE_RANDOM;
}

int addrconf_prefix_rcv_add_addr(struct net *net, struct net_device *dev,
@@ -2735,7 +2737,7 @@ int addrconf_prefix_rcv_add_addr(struct net *net, struct net_device *dev,
#ifdef CONFIG_IPV6_OPTIMISTIC_DAD
if ((READ_ONCE(net->ipv6.devconf_all->optimistic_dad) ||
READ_ONCE(in6_dev->cnf.optimistic_dad)) &&
- !net->ipv6.devconf_all->forwarding && sllao)
+ !READ_ONCE(net->ipv6.devconf_all->forwarding) && sllao)
cfg.ifa_flags |= IFA_F_OPTIMISTIC;
#endif

@@ -2857,7 +2859,7 @@ void addrconf_prefix_rcv(struct net_device *dev, u8 *opt, int len, bool sllao)
return;
}

- if (valid_lft != 0 && valid_lft < in6_dev->cnf.accept_ra_min_lft)
+ if (valid_lft != 0 && valid_lft < READ_ONCE(in6_dev->cnf.accept_ra_min_lft))
goto put;

/*
@@ -2928,7 +2930,7 @@ void addrconf_prefix_rcv(struct net_device *dev, u8 *opt, int len, bool sllao)
/* Try to figure out our local address for this prefix */

ignore_autoconf = READ_ONCE(in6_dev->cnf.ra_honor_pio_pflag) && pinfo->preferpd;
- if (pinfo->autoconf && in6_dev->cnf.autoconf && !ignore_autoconf) {
+ if (pinfo->autoconf && READ_ONCE(in6_dev->cnf.autoconf) && !ignore_autoconf) {
struct in6_addr addr;
bool tokenized = false, dev_addr_generated = false;

@@ -3434,7 +3436,7 @@ static int ipv6_generate_stable_address(struct in6_addr *address,

if (ipv6_reserved_interfaceid(temp)) {
dad_count++;
- if (dad_count > dev_net(idev->dev)->ipv6.sysctl.idgen_retries)
+ if (dad_count > READ_ONCE(dev_net(idev->dev)->ipv6.sysctl.idgen_retries))
return -1;
goto retry;
}
@@ -3805,7 +3807,7 @@ static int addrconf_notify(struct notifier_block *this, unsigned long event,
* when the interface up, the changed MTU must be
* reflected in the idev as well as routers.
*/
- if (idev->cnf.mtu6 != dev->mtu &&
+ if (READ_ONCE(idev->cnf.mtu6) != dev->mtu &&
dev->mtu >= IPV6_MIN_MTU) {
rt6_mtu_change(dev, dev->mtu);
WRITE_ONCE(idev->cnf.mtu6, dev->mtu);
diff --git a/net/ipv6/ndisc.c b/net/ipv6/ndisc.c
index 0ed1a619372bf..b69e6d66e8b1a 100644
--- a/net/ipv6/ndisc.c
+++ b/net/ipv6/ndisc.c
@@ -1375,7 +1375,7 @@ static enum skb_drop_reason ndisc_router_discovery(struct sk_buff *skb)
}
}
/* Set default route metric as specified by user */
- defrtr_usr_metric = in6_dev->cnf.ra_defrtr_metric;
+ defrtr_usr_metric = READ_ONCE(in6_dev->cnf.ra_defrtr_metric);
/* delete the route if lifetime is 0 or if metric needs change */
if (rt && (lifetime == 0 || rt->fib6_metric != defrtr_usr_metric)) {
ip6_del_rt_reason(net, rt,
diff --git a/net/ipv6/route.c b/net/ipv6/route.c
index 04098cf32eabe..91a003417697c 100644
--- a/net/ipv6/route.c
+++ b/net/ipv6/route.c
@@ -2065,7 +2065,7 @@ static bool rt6_mtu_change_route_allowed(struct inet6_dev *idev,
if (dmtu >= mtu)
return true;

- if (dmtu == idev->cnf.mtu6)
+ if (dmtu == READ_ONCE(idev->cnf.mtu6))
return true;

return false;
@@ -3666,7 +3666,7 @@ int fib6_nh_init(struct net *net, struct fib6_nh *fib6_nh,
if (!dev)
goto out;

- if (!idev || idev->cnf.disable_ipv6) {
+ if (!idev || READ_ONCE(idev->cnf.disable_ipv6)) {
NL_SET_ERR_MSG(extack, "IPv6 is disabled on nexthop device");
err = -EACCES;
goto out;
@@ -4533,7 +4533,7 @@ static void __rt6_purge_dflt_routers(struct net *net,
struct inet6_dev *idev = dev ? __in6_dev_get(dev) : NULL;

if (rt->fib6_flags & (RTF_DEFAULT | RTF_ADDRCONF) &&
- (!idev || idev->cnf.accept_ra != 2) &&
+ (!idev || READ_ONCE(idev->cnf.accept_ra) != 2) &&
fib6_info_hold_safe(rt)) {
rcu_read_unlock();
ip6_del_rt(net, rt, false);
@@ -5075,7 +5075,7 @@ static int fib6_nh_mtu_change(struct fib6_nh *nh, void *_arg)
return 0;

if (mtu >= arg->mtu ||
- (mtu < arg->mtu && mtu == idev->cnf.mtu6))
+ (mtu < arg->mtu && mtu == READ_ONCE(idev->cnf.mtu6)))
fib6_metric_set(f6i, RTAX_MTU, arg->mtu);

spin_lock_bh(&rt6_exception_lock);
--
2.43.0