ipv6: mcast: use copy-on-write RCU updates in ip6_mc_source()

pmc->sflist is read locklessly under rcu_read_lock() by
inet6_mc_check() during packet reception in the UDP and RAW
multicast receive paths.

ip6_mc_source() mutated psl->sl_addr and psl->sl_count in-place
when adding or removing a source filter. Additionally, when expanding
the filter buffer, newpsl was published via rcu_assign_pointer()
before writing the new source into the array.

Because 16-byte struct in6_addr writes are not atomic and array
shifting is not synchronized with RCU readers, concurrent readers in
inet6_mc_check() could read torn IPv6 addresses or observe
duplicated/missed source entries.

Fix this by switching ip6_mc_source() to copy-on-write RCU updates:
allocate and fully populate newpsl before publishing it via
rcu_assign_pointer(), and reclaim the old filter via kfree_rcu(),
matching ip6_mc_msfilter().

Also remove the now unused IP6_SFBLOCK macro.

Fixes: 882ba1f73c ("mld: convert ipv6_mc_socklist->sflist to RCU")
Signed-off-by: Eric Dumazet <edumazet@google.com>
Cc: Taehee Yoo <ap420073@gmail.com>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
Link: https://patch.msgid.link/20260828084531.1826790-3-edumazet@google.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
This commit is contained in:
Eric Dumazet 2026-08-28 08:45:28 +00:00 committed by Jakub Kicinski
parent 93b4923984
commit c073d1b070
2 changed files with 56 additions and 44 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

@ -355,12 +355,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 +409,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 +422,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);