mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Hangbin Liu <hangbin.liu@linux.dev>
To: Junyoung Jang <graypanda.inzag@gmail.com>
Cc: David Ahern <dsahern@kernel.org>,
	Ido Schimmel <idosch@nvidia.com>,
	"David S. Miller" <davem@davemloft.net>,
	Eric Dumazet <edumazet@kernel.org>,
	Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>,
	Simon Horman <horms@kernel.org>,
	netdev@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: Re: [PATCH net] igmp: use the joined device when leaving a multicast group
Date: Tue, 10 Nov 2026 13:58:54 +0800	[thread overview]
Message-ID: <avKyno2Pcsrfu0u4@fedora> (raw)
In-Reply-To: <20261109125312.2208874-1-graypanda.inzag@gmail.com>

On Mon, Nov 09, 2026 at 09:53:11PM +0900, Junyoung Jang wrote:
> ip_mc_leave_group() resolves in_dev from the leave request before
> matching the request against the socket's multicast membership list.
> This lookup can return NULL even while the interface recorded in the
> matched membership (iml->multi.imr_ifindex) is still alive:
> 
> 1. An application joins a group using a local IPv4 address, that
>    secondary address is later removed while the interface stays up,
>    and the application leaves using the same 8-byte ip_mreq.
> 2. An unprivileged user joins a group with a 12-byte ip_mreqn
>    specifying a valid imr_ifindex and a nonlocal imr_address, then
>    leaves with an 8-byte ip_mreq containing that nonlocal address
>    and no ifindex.
> 
> Because imr_address is nonzero, the early -ENODEV check is bypassed.
> The loop then removes and frees the socket's ip_mc_socklist entry
> (iml) and returns 0, but skips ip_mc_dec_group() because in_dev is
> NULL. Closing the socket cannot release the group via
> ip_mc_drop_socket() because the socket membership entry has already
> been unlinked. This leaks the interface's struct ip_mc_list reference
> and multicast MAC filter beyond the socket's lifetime, allowing an
> unprivileged user to repeatedly leak multicast group entries on a
> live interface.
> 
> Fix this by looking up in_dev from the matched membership's saved
> imr_ifindex under RTNL, matching ip_mc_drop_socket(). If the interface
> was genuinely removed, inetdev_by_index() still returns NULL and the
> existing hot-unplug cleanup behavior is preserved.
> 
> Fixes: 4eba7bb1d72d ("ipv4: igmp: Allow removing groups from a removed interface")
> Signed-off-by: Junyoung Jang <graypanda.inzag@gmail.com>
> ---
> Tested on Linux 7.4-rc1 in QEMU/KVM as an unprivileged user (UID 1000)
> for both the unprivileged ifindex/nonlocal-address mismatch and the
> secondary address removal scenario.
> 
> Unpatched 7.4-rc1 guest output:
>   IGMP_VM_BEGIN kernel=7.4.0-rc1 expect=--expect-leak
>   IGMP_VM_CASE=nonlocal-address
>   uid=1000
>   control_explicit_ifindex
>   after_drop=0
>   after_close=0
>   case_address_only
>   join=0
>   drop=0
>   after_drop=1
>   after_close=1
>   joins_ok=16
>   drops_ok=16
>   closes_ok=16
>   groups_after=16
>   result=PASS
>   IGMP_VM_CASE=removed-secondary-address
>   uid=1000
>   address_removed=1
>   primary_present=1
>   after_remove=1
>   drop=0
>   after_drop=1
>   after_close=1
>   result=PASS
> 
> Patched 7.4-rc1 guest output:
>   IGMP_VM_BEGIN kernel=7.4.0-rc1 expect=--expect-fixed
>   IGMP_VM_CASE=nonlocal-address
>   uid=1000
>   control_explicit_ifindex
>   after_drop=0
>   after_close=0
>   case_address_only
>   join=0
>   drop=0
>   after_drop=0
>   after_close=0
>   joins_ok=16
>   drops_ok=16
>   closes_ok=16
>   groups_after=0
>   result=PASS
>   IGMP_VM_CASE=removed-secondary-address
>   uid=1000
>   address_removed=1
>   primary_present=1
>   after_remove=1
>   drop=0
>   after_drop=0
>   after_close=0
>   result=PASS
> 
> Reproducer (igmp-multicast-reference-leak.c):
> 
> /* SPDX-License-Identifier: GPL-2.0-only */
> #define _GNU_SOURCE
> #include <arpa/inet.h>
> #include <errno.h>
> #include <ifaddrs.h>
> #include <net/if.h>
> #include <netinet/in.h>
> #include <stdbool.h>
> #include <stdio.h>
> #include <stdlib.h>
> #include <string.h>
> #include <sys/socket.h>
> #include <unistd.h>
> 
> /*
>  * Run in a disposable network namespace. The affected kernel retains
>  * groups after a successful address-only leave and socket close.
>  */
> #define BATCH_GROUPS 16
> 
> _Static_assert(sizeof(struct ip_mreqn) == 12, "ip_mreqn must include ifindex");
> _Static_assert(sizeof(struct ip_mreq) == 8, "ip_mreq must omit ifindex");
> 
> struct observation {
> 	int before;
> 	int after_join;
> 	int after_drop;
> 	int after_close;
> };
> 
> struct accumulation {
> 	int before;
> 	int after;
> 	int joins;
> 	int drops;
> 	int closes;
> };
> 
> struct interface_state {
> 	bool up;
> 	bool primary_present;
> 	bool additional_present;
> };
> 
> static void die(const char *operation)
> {
> 	fprintf(stderr, "%s: %s\n", operation, strerror(errno));
> 	exit(EXIT_FAILURE);
> }
> 
> static struct in_addr address(const char *text)
> {
> 	struct in_addr addr;
> 
> 	if (inet_pton(AF_INET, text, &addr) != 1) {
> 		fprintf(stderr, "invalid IPv4 address: %s\n", text);
> 		exit(EXIT_FAILURE);
> 	}
> 	return addr;
> }
> 
> static struct interface_state churn_interface_state(void)
> {
> 	struct in_addr primary = address("192.0.2.1");
> 	struct in_addr additional = address("192.0.2.10");
> 	struct interface_state state = { 0 };
> 	struct ifaddrs *interfaces, *ifa;
> 
> 	if (getifaddrs(&interfaces))
> 		die("getifaddrs");
> 	for (ifa = interfaces; ifa; ifa = ifa->ifa_next) {
> 		struct sockaddr_in *in;
> 
> 		/* IPv4 aliases may be reported as eth0:1 by getifaddrs(). */
> 		if (strncmp(ifa->ifa_name, "eth0", 4) ||
> 		    (ifa->ifa_name[4] && ifa->ifa_name[4] != ':'))
> 			continue;
> 		state.up |= !!(ifa->ifa_flags & IFF_UP);
> 		if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_INET)
> 			continue;
> 		in = (struct sockaddr_in *)ifa->ifa_addr;
> 		state.primary_present |= in->sin_addr.s_addr == primary.s_addr;
> 		state.additional_present |= in->sin_addr.s_addr == additional.s_addr;
> 	}
> 	freeifaddrs(interfaces);
> 	return state;
> }
> 
> /* /proc/net/igmp prints each group as a hexadecimal __be32 value. */
> static int group_present(unsigned int ifindex, struct in_addr group)
> {
> 	char line[256], device[IFNAMSIZ];
> 	unsigned int index, group_count, raw_group, users;
> 	bool on_interface = false;
> 	int found = 0;
> 	FILE *file;
> 
> 	file = fopen("/proc/net/igmp", "r");
> 	if (!file)
> 		die("fopen /proc/net/igmp");
> 
> 	while (fgets(line, sizeof(line), file)) {
> 		if (sscanf(line, " %u %15s : %u", &index, device,
> 			   &group_count) == 3) {
> 			on_interface = index == ifindex;
> 			continue;
> 		}
> 		if (on_interface &&
> 		    sscanf(line, " %x %u", &raw_group, &users) == 2 &&
> 		    raw_group == group.s_addr) {
> 			found = 1;
> 			break;
> 		}
> 	}
> 	if (ferror(file))
> 		die("read /proc/net/igmp");
> 	if (fclose(file))
> 		die("fclose /proc/net/igmp");
> 	return found;
> }
> 
> static struct ip_mreqn join_request(struct in_addr group,
> 				    struct in_addr interface_addr, int ifindex)
> {
> 	struct ip_mreqn request = {
> 		.imr_multiaddr = group,
> 		.imr_address = interface_addr,
> 		.imr_ifindex = ifindex,
> 	};
> 
> 	return request;
> }
> 
> static struct observation probe(const char *name, struct in_addr group,
> 				struct in_addr interface_addr,
> 				unsigned int ifindex, bool drop_with_ifindex)
> {
> 	struct ip_mreqn join = join_request(group, interface_addr, ifindex);
> 	struct ip_mreq drop = {
> 		.imr_multiaddr = group,
> 		.imr_interface = interface_addr,
> 	};
> 	struct observation result;
> 	int fd;
> 
> 	printf("%s\n", name);
> 	result.before = group_present(ifindex, group);
> 	printf("before=%d\n", result.before);
> 	fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
> 	if (fd < 0)
> 		die("socket");
> 	printf("socket=%d\n", fd);
> 	if (setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP,
> 		       &join, sizeof(join)))
> 		die("IP_ADD_MEMBERSHIP");
> 	printf("join=0\n");
> 	result.after_join = group_present(ifindex, group);
> 	printf("after_join=%d\n", result.after_join);
> 
> 	if (drop_with_ifindex) {
> 		if (setsockopt(fd, IPPROTO_IP, IP_DROP_MEMBERSHIP,
> 			       &join, sizeof(join)))
> 			die("IP_DROP_MEMBERSHIP (ip_mreqn)");
> 	} else if (setsockopt(fd, IPPROTO_IP, IP_DROP_MEMBERSHIP,
> 			      &drop, sizeof(drop))) {
> 		die("IP_DROP_MEMBERSHIP (ip_mreq)");
> 	}
> 	printf("drop=0\n");
> 	result.after_drop = group_present(ifindex, group);
> 	printf("after_drop=%d\n", result.after_drop);
> 	if (close(fd))
> 		die("close");
> 	printf("close=0\n");
> 	result.after_close = group_present(ifindex, group);
> 	printf("after_close=%d\n", result.after_close);
> 	return result;
> }
> 
> static int count_batch_groups(unsigned int ifindex)
> {
> 	char text[INET_ADDRSTRLEN];
> 	int count = 0;
> 
> 	for (int i = 1; i <= BATCH_GROUPS; i++) {
> 		snprintf(text, sizeof(text), "239.250.34.%d", i);
> 		count += group_present(ifindex, address(text));
> 	}
> 	return count;
> }
> 
> static struct accumulation accumulate_groups(struct in_addr interface_addr,
> 					     unsigned int ifindex)
> {
> 	char text[INET_ADDRSTRLEN];
> 	struct accumulation result = { 0 };
> 	struct ip_mreqn join;
> 	struct ip_mreq drop;
> 	int fd;
> 
> 	result.before = count_batch_groups(ifindex);
> 	printf("groups_before=%d\n", result.before);
> 	for (int i = 1; i <= BATCH_GROUPS; i++) {
> 		snprintf(text, sizeof(text), "239.250.34.%d", i);
> 		join = join_request(address(text), interface_addr, ifindex);
> 		drop.imr_multiaddr = join.imr_multiaddr;
> 		drop.imr_interface = interface_addr;
> 		fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
> 		if (fd < 0)
> 			die("batch socket");
> 		if (setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP,
> 			       &join, sizeof(join)))
> 			die("batch IP_ADD_MEMBERSHIP");
> 		result.joins++;
> 		if (setsockopt(fd, IPPROTO_IP, IP_DROP_MEMBERSHIP,
> 			       &drop, sizeof(drop)))
> 			die("batch IP_DROP_MEMBERSHIP");
> 		result.drops++;
> 		if (close(fd))
> 			die("batch close");
> 		result.closes++;
> 	}
> 	result.after = count_batch_groups(ifindex);
> 	printf("joins_ok=%d\ndrops_ok=%d\ncloses_ok=%d\ngroups_after=%d\n",
> 	       result.joins, result.drops, result.closes, result.after);
> 	return result;
> }
> 
> static int run_original(bool expect_leak, unsigned int ifindex)
> {
> 	struct in_addr interface_addr = address("203.0.113.77");
> 	struct observation control, affected;
> 	struct accumulation batch;
> 	bool pass;
> 
> 	control = probe("control_explicit_ifindex", address("239.250.33.1"),
> 			interface_addr, ifindex, true);
> 	affected = probe("case_address_only", address("239.250.33.2"),
> 			 interface_addr, ifindex, false);
> 	batch = accumulate_groups(interface_addr, ifindex);
> 	pass = control.before == 0 && control.after_join == 1 &&
> 	       control.after_drop == 0 && control.after_close == 0 &&
> 	       affected.before == 0 && affected.after_join == 1 &&
> 	       affected.after_drop == expect_leak &&
> 	       affected.after_close == expect_leak &&
> 	       batch.before == 0 && batch.joins == BATCH_GROUPS &&
> 	       batch.drops == BATCH_GROUPS && batch.closes == BATCH_GROUPS &&
> 	       batch.after == (expect_leak ? BATCH_GROUPS : 0);
> 	printf("result=%s\n", pass ? "PASS" : "FAIL");
> 	return pass ? EXIT_SUCCESS : EXIT_FAILURE;
> }
> 
> /*
>  * The caller first adds 192.0.2.1/24 to eth0, then 192.0.2.10/24 as a
>  * secondary address. After "ready_to_remove_address=1", it deletes only
>  * the secondary address and sends one byte on stdin; eth0 remains up.
>  */
> static int run_address_churn(bool expect_leak, unsigned int ifindex)
> {
> 	struct in_addr group = address("239.250.35.1");
> 	struct ip_mreq request = {
> 		.imr_multiaddr = group,
> 		.imr_interface = address("192.0.2.10"),
> 	};
> 	int before, after_join, after_remove, after_drop, after_close, fd;
> 	struct interface_state before_state, after_state;
> 	char token;
> 	ssize_t n;
> 	bool pass;
> 
> 	printf("case_address_churn\n");
> 	before_state = churn_interface_state();
> 	printf("address_present_before=%d\ninterface_up_before=%d\n",
> 	       before_state.additional_present, before_state.up);
> 	before = group_present(ifindex, group);
> 	printf("before=%d\n", before);
> 	fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
> 	if (fd < 0)
> 		die("socket");
> 	printf("socket=%d\n", fd);
> 	if (setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP,
> 		       &request, sizeof(request)))
> 		die("address-churn IP_ADD_MEMBERSHIP");
> 	printf("join=0\n");
> 	after_join = group_present(ifindex, group);
> 	printf("after_join=%d\n", after_join);
> 	printf("ready_to_remove_address=1\n");
> 	if (fflush(stdout))
> 		die("fflush");
> 	do {
> 		n = read(STDIN_FILENO, &token, 1);
> 	} while (n < 0 && errno == EINTR);
> 	if (n != 1) {
> 		fprintf(stderr, "expected one-byte address-removal signal\n");
> 		return EXIT_FAILURE;
> 	}
> 	after_state = churn_interface_state();
> 	printf("address_removed=%d\ninterface_up=%d\nprimary_present=%d\n",
> 	       !after_state.additional_present, after_state.up,
> 	       after_state.primary_present);
> 	after_remove = group_present(ifindex, group);
> 	printf("after_remove=%d\n", after_remove);
> 	if (setsockopt(fd, IPPROTO_IP, IP_DROP_MEMBERSHIP,
> 		       &request, sizeof(request)))
> 		die("address-churn IP_DROP_MEMBERSHIP");
> 	printf("drop=0\n");
> 	after_drop = group_present(ifindex, group);
> 	printf("after_drop=%d\n", after_drop);
> 	if (close(fd))
> 		die("close");
> 	printf("close=0\n");
> 	after_close = group_present(ifindex, group);
> 	printf("after_close=%d\n", after_close);
> 	pass = before_state.up && before_state.primary_present &&
> 	       before_state.additional_present && after_state.up &&
> 	       after_state.primary_present && !after_state.additional_present &&
> 	       before == 0 && after_join == 1 && after_remove == 1 &&
> 	       after_drop == expect_leak && after_close == expect_leak;
> 	printf("result=%s\n", pass ? "PASS" : "FAIL");
> 	return pass ? EXIT_SUCCESS : EXIT_FAILURE;
> }
> 
> int main(int argc, char **argv)
> {
> 	bool churn = argc == 3 && !strcmp(argv[1], "--address-churn");
> 	const char *expectation = churn ? argv[2] : argc == 2 ? argv[1] : NULL;
> 	const char *ifname = churn ? "eth0" : "lo";
> 	bool expect_leak;
> 	unsigned int ifindex;
> 
> 	if (!expectation ||
> 	    (strcmp(expectation, "--expect-leak") &&
> 	     strcmp(expectation, "--expect-fixed"))) {
> 		fprintf(stderr,
> 			"usage: %s [--address-churn] --expect-leak|--expect-fixed\n",
> 			argv[0]);
> 		return 2;
> 	}
> 	expect_leak = !strcmp(expectation, "--expect-leak");
> 	ifindex = if_nametoindex(ifname);
> 	if (!ifindex)
> 		die("if_nametoindex");
> 	printf("uid=%u\ninterface=%s ifindex=%u\nexpectation=%s\n",
> 	       (unsigned int)getuid(), ifname, ifindex,
> 	       expect_leak ? "leak" : "fixed");
> 
> 	if (churn)
> 		return run_address_churn(expect_leak, ifindex);
> 	return run_original(expect_leak, ifindex);
> }
> 
>  net/ipv4/igmp.c | 2 ++
>  1 file changed, 2 insertions(+)
> 
> diff --git a/net/ipv4/igmp.c b/net/ipv4/igmp.c
> index 144fca158..52cde8782 100644
> --- a/net/ipv4/igmp.c
> +++ b/net/ipv4/igmp.c
> @@ -2463,6 +2463,8 @@ int ip_mc_leave_group(struct sock *sk, struct ip_mreqn *imr)
>  				iml->multi.imr_address.s_addr)
>  			continue;
>  
> +		/* The drop request may not resolve to the joined device. */
> +		in_dev = inetdev_by_index(net, iml->multi.imr_ifindex);
>  		(void) ip_mc_leave_src(sk, iml, in_dev);
>  
>  		*imlp = iml->next_rcu;
> -- 
> 2.43.0

Reviewed-by: Hangbin Liu <liuhangbin@kylinos.cn>

      parent reply	other threads:[~2026-11-10  5:59 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-09 12:53 Junyoung Jang
2026-11-09 12:55 ` netdev-bot+sinfo
2026-11-09 13:08   ` Junyoung Jang
2026-11-10  5:58 ` Hangbin Liu [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=avKyno2Pcsrfu0u4@fedora \
    --to=hangbin.liu@linux.dev \
    --cc=davem@davemloft.net \
    --cc=dsahern@kernel.org \
    --cc=edumazet@kernel.org \
    --cc=graypanda.inzag@gmail.com \
    --cc=horms@kernel.org \
    --cc=idosch@nvidia.com \
    --cc=kuba@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®