Merge branch 'ipv6-mcast-rcu-and-timer-fixes'

Eric Dumazet says:

====================
ipv6: mcast: RCU and timer fixes

This series addresses several RCU synchronization and timer calculation
issues identified in IPv6 multicast (MLD) handling within net/ipv6/mcast.c
while I was working on fixing a syzbot report in net/ipv4/icmp.c.

Patch 1 fixes an RCU reader diversion in ip6_mc_del1_src() where mutating
psf->sf_next to insert an unlinked source node into the tombstone list
diverted concurrent lockless readers (e.g. ipv6_chk_mcast_addr()) into
pmc->mca_tomb, causing them to miss remaining active sources.

Patch 2 converts ip6_mc_source() to use copy-on-write RCU updates.
Previously, source additions and deletions modified the socket's
psl->sl_addr array in-place, causing concurrent lockless readers in
inet6_mc_check() (UDP/RAW receive path) to observe torn 16-byte IPv6
addresses or duplicated/missed sources.

Patch 3 fixes delay calculation in igmp6_join_group() when canceling an
existing delayed work, preventing unsigned jiffies underflows when the
timer has already expired and clamping the delay to the unsolicited report
interval.

Patch 4 ensures rcu_assign_pointer() is consistently used for __rcu list
updates in __ipv6_dev_mc_dec(), ipv6_sock_mc_drop(), __ipv6_sock_mc_close(),
and related helpers.

Patch 5 switches igmp6_mc_seq_show() to use jiffies_delta_to_clock_t()
with a signed long delta, preventing underflows in /proc/net/igmp6 timer
duration reporting.
====================

Link: https://patch.msgid.link/20260828084531.1826790-1-edumazet@google.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
This commit is contained in:
Jakub Kicinski 2026-08-31 17:54:47 -07:00
commit ec12bdd568
2 changed files with 88 additions and 62 deletions

View File

@ -88,8 +88,6 @@ struct ip6_sf_socklist {
struct in6_addr sl_addr[] __counted_by(sl_max);
};
#define IP6_SFBLOCK 10 /* allocate this many at once */
struct ipv6_mc_socklist {
struct in6_addr addr;
int ifindex;

View File

@ -240,7 +240,8 @@ static int __ipv6_sock_mc_join(struct sock *sk, int ifindex,
return err;
}
mc_lst->next = np->ipv6_mc_list;
rcu_assign_pointer(mc_lst->next,
sock_dereference(np->ipv6_mc_list, sk));
rcu_assign_pointer(np->ipv6_mc_list, mc_lst);
return 0;
@ -300,7 +301,8 @@ int ipv6_sock_mc_drop(struct sock *sk, int ifindex, const struct in6_addr *addr)
lnk = &mc_lst->next) {
if ((ifindex == 0 || mc_lst->ifindex == ifindex) &&
ipv6_addr_equal(&mc_lst->addr, addr)) {
*lnk = mc_lst->next;
rcu_assign_pointer(*lnk,
sock_dereference(mc_lst->next, sk));
__ipv6_sock_mc_drop(sk, mc_lst);
return 0;
}
@ -333,7 +335,8 @@ void __ipv6_sock_mc_close(struct sock *sk)
struct ipv6_mc_socklist *mc_lst;
while ((mc_lst = sock_dereference(np->ipv6_mc_list, sk)) != NULL) {
np->ipv6_mc_list = mc_lst->next;
rcu_assign_pointer(np->ipv6_mc_list,
sock_dereference(mc_lst->next, sk));
__ipv6_sock_mc_drop(sk, mc_lst);
}
}
@ -355,12 +358,12 @@ int ip6_mc_source(int add, int omode, struct sock *sk,
{
struct ipv6_pinfo *inet6 = inet6_sk(sk);
struct in6_addr *source, *group;
struct ip6_sf_socklist *newpsl, *psl;
struct net *net = sock_net(sk);
struct ipv6_mc_socklist *pmc;
struct ip6_sf_socklist *psl;
struct inet6_dev *idev;
int leavegroup = 0;
int i, j, rv;
int i, j;
int err;
source = &((struct sockaddr_in6 *)&pgsr->gsr_source)->sin6_addr;
@ -409,13 +412,11 @@ int ip6_mc_source(int add, int omode, struct sock *sk,
if (!add) {
if (!psl)
goto done; /* err = -EADDRNOTAVAIL */
rv = !0;
for (i = 0; i < psl->sl_count; i++) {
rv = !ipv6_addr_equal(&psl->sl_addr[i], source);
if (rv == 0)
if (ipv6_addr_equal(&psl->sl_addr[i], source))
break;
}
if (rv) /* source not found */
if (i == psl->sl_count) /* source not found */
goto done; /* err = -EADDRNOTAVAIL */
/* special case - (INCLUDE, empty) == LEAVE_GROUP */
@ -424,58 +425,74 @@ int ip6_mc_source(int add, int omode, struct sock *sk,
goto done;
}
atomic_sub(struct_size(psl, sl_addr, psl->sl_max),
&sk->sk_omem_alloc);
if (psl->sl_count == 1) {
newpsl = NULL;
} else {
newpsl = sock_kmalloc(sk, struct_size(newpsl, sl_addr,
psl->sl_count - 1),
GFP_KERNEL);
if (!newpsl) {
atomic_add(struct_size(psl, sl_addr, psl->sl_max),
&sk->sk_omem_alloc);
err = -ENOBUFS;
goto done;
}
newpsl->sl_max = psl->sl_count - 1;
newpsl->sl_count = psl->sl_count - 1;
for (j = 0; j < i; j++)
newpsl->sl_addr[j] = psl->sl_addr[j];
for (j = i + 1; j < psl->sl_count; j++)
newpsl->sl_addr[j - 1] = psl->sl_addr[j];
}
/* update the interface filter */
ip6_mc_del_src(idev, group, omode, 1, source, 1);
for (j = i+1; j < psl->sl_count; j++)
psl->sl_addr[j-1] = psl->sl_addr[j];
psl->sl_count--;
rcu_assign_pointer(pmc->sflist, newpsl);
kfree_rcu(psl, rcu);
err = 0;
goto done;
}
/* else, add a new source to the filter */
if (psl && psl->sl_count >= sysctl_mld_max_msf) {
if (psl && psl->sl_count >= READ_ONCE(sysctl_mld_max_msf)) {
err = -ENOBUFS;
goto done;
}
if (!psl || psl->sl_count == psl->sl_max) {
struct ip6_sf_socklist *newpsl;
int count = IP6_SFBLOCK;
if (psl) {
for (i = 0; i < psl->sl_count; i++) {
if (ipv6_addr_equal(&psl->sl_addr[i], source))
goto done; /* err = -EADDRNOTAVAIL */
}
}
if (psl)
count += psl->sl_max;
newpsl = sock_kmalloc(sk, struct_size(newpsl, sl_addr, count),
GFP_KERNEL);
if (!newpsl) {
err = -ENOBUFS;
goto done;
}
newpsl->sl_max = count;
newpsl->sl_count = count - IP6_SFBLOCK;
if (psl) {
for (i = 0; i < psl->sl_count; i++)
newpsl->sl_addr[i] = psl->sl_addr[i];
atomic_sub(struct_size(psl, sl_addr, psl->sl_max),
&sk->sk_omem_alloc);
}
rcu_assign_pointer(pmc->sflist, newpsl);
kfree_rcu(psl, rcu);
psl = newpsl;
i = psl ? psl->sl_count + 1 : 1;
newpsl = sock_kmalloc(sk, struct_size(newpsl, sl_addr, i),
GFP_KERNEL);
if (!newpsl) {
err = -ENOBUFS;
goto done;
}
rv = 1; /* > 0 for insert logic below if sl_count is 0 */
for (i = 0; i < psl->sl_count; i++) {
rv = !ipv6_addr_equal(&psl->sl_addr[i], source);
if (rv == 0) /* There is an error in the address. */
goto done;
newpsl->sl_max = i;
newpsl->sl_count = i;
if (psl) {
for (j = 0; j < psl->sl_count; j++)
newpsl->sl_addr[j] = psl->sl_addr[j];
}
for (j = psl->sl_count-1; j >= i; j--)
psl->sl_addr[j+1] = psl->sl_addr[j];
psl->sl_addr[i] = *source;
psl->sl_count++;
err = 0;
newpsl->sl_addr[i - 1] = *source;
/* update the interface list */
ip6_mc_add_src(idev, group, omode, 1, source, 1);
if (psl)
atomic_sub(struct_size(psl, sl_addr, psl->sl_max),
&sk->sk_omem_alloc);
rcu_assign_pointer(pmc->sflist, newpsl);
kfree_rcu(psl, rcu);
err = 0;
done:
mutex_unlock(&idev->mc_lock);
in6_dev_put(idev);
@ -784,9 +801,11 @@ static void mld_del_delrec(struct inet6_dev *idev, struct ifmcaddr6 *im)
if (!pmc)
return;
if (pmc_prev)
rcu_assign_pointer(pmc_prev->next, pmc->next);
rcu_assign_pointer(pmc_prev->next,
mc_dereference(pmc->next, idev));
else
rcu_assign_pointer(idev->mc_tomb, pmc->next);
rcu_assign_pointer(idev->mc_tomb,
mc_dereference(pmc->next, idev));
im->idev = pmc->idev;
if (im->mca_sfmode == MCAST_INCLUDE) {
@ -966,7 +985,7 @@ static int __ipv6_dev_mc_inc(struct net_device *dev,
return -ENOMEM;
}
rcu_assign_pointer(mc->next, idev->mc_list);
rcu_assign_pointer(mc->next, mc_dereference(idev->mc_list, idev));
rcu_assign_pointer(idev->mc_list, mc);
mld_del_delrec(idev, mc);
@ -1000,7 +1019,8 @@ int __ipv6_dev_mc_dec(struct inet6_dev *idev, const struct in6_addr *addr)
WRITE_ONCE(ma->mca_users, new_users);
if (new_users == 0) {
*map = ma->next;
rcu_assign_pointer(*map,
mc_dereference(ma->next, idev));
igmp6_group_dropped(ma);
inet6_ifmcaddr_notify(idev->dev, ma,
@ -2351,14 +2371,18 @@ static int ip6_mc_del1_src(struct ifmcaddr6 *pmc, int sfmode,
if (psf->sf_oldin && !(pmc->mca_flags & MAF_NOREPORT) &&
!mld_in_v1_mode(idev)) {
psf->sf_crcount = idev->mc_qrv;
rcu_assign_pointer(psf->sf_next,
mc_dereference(pmc->mca_tomb, idev));
rcu_assign_pointer(pmc->mca_tomb, psf);
rv = 1;
} else {
kfree_rcu(psf, rcu);
struct ip6_sf_list *dpsf = kmalloc_obj(*dpsf);
if (dpsf) {
*dpsf = *psf;
dpsf->sf_crcount = idev->mc_qrv;
rcu_assign_pointer(dpsf->sf_next,
mc_dereference(pmc->mca_tomb, idev));
rcu_assign_pointer(pmc->mca_tomb, dpsf);
rv = 1;
}
}
kfree_rcu(psf, rcu);
}
return rv;
}
@ -2621,7 +2645,7 @@ static void ip6_mc_clear_src(struct ifmcaddr6 *pmc)
static void igmp6_join_group(struct ifmcaddr6 *ma)
{
unsigned long delay;
unsigned long delay, interval;
mc_assert_locked(ma->idev);
@ -2630,13 +2654,17 @@ static void igmp6_join_group(struct ifmcaddr6 *ma)
igmp6_send(&ma->mca_addr, ma->idev->dev, ICMPV6_MGM_REPORT);
delay = get_random_u32_below(unsolicited_report_interval(ma->idev));
interval = unsolicited_report_interval(ma->idev);
delay = interval;
if (cancel_delayed_work(&ma->mca_work)) {
refcount_dec(&ma->mca_refcnt);
delay = ma->mca_work.timer.expires - jiffies;
}
if (delay >= interval)
delay = get_random_u32_below(interval);
if (!mod_delayed_work(mld_wq, &ma->mca_work, delay))
refcount_inc(&ma->mca_refcnt);
WRITE_ONCE(ma->mca_flags, ma->mca_flags |
@ -3001,7 +3029,7 @@ static int igmp6_mc_seq_show(struct seq_file *seq, void *v)
struct ifmcaddr6 *im = (struct ifmcaddr6 *)v;
struct igmp6_mc_iter_state *state = igmp6_mc_seq_private(seq);
unsigned int mca_flags = READ_ONCE(im->mca_flags);
unsigned long expires = READ_ONCE(im->mca_work.timer.expires);
long delta = READ_ONCE(im->mca_work.timer.expires) - jiffies;
seq_printf(seq,
"%-4d %-15s %pi6 %5d %08X %ld\n",
@ -3009,7 +3037,7 @@ static int igmp6_mc_seq_show(struct seq_file *seq, void *v)
&im->mca_addr,
READ_ONCE(im->mca_users), mca_flags,
(mca_flags & MAF_TIMER_RUNNING) ?
jiffies_to_clock_t(expires - jiffies) : 0);
jiffies_delta_to_clock_t(delta) : 0);
return 0;
}