net: validate malformed IPv6 and TCP headers
Subsystems (5)
cover net: validate malformed IPv6 and TCP headers
From: Zixuan Chai <[email protected]>
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 inputAdditional 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(-)
Signature · 1 line
2.34.1
PATCH 1/2 ipv6: reject truncated extension headers in ipv6_skip_exthdr()
From: Zixuan Chai <[email protected]>
ipv6_skip_exthdr() derives the length of each extension header from packet data. When the packet ends before the declared length, it currently advances the offset past the end of the skb and reports a successful parse.
Check the remaining skb length before advancing over an extension header. Handle the resulting -1 in the consumers that use the offset or classify the first fragment: reject malformed first fragments during IPv6 reassembly, including conntrack reassembly; suppress ICMPv6 replies; and abort XFRM BEET GSO before updating the transport offset.
Update the helper comment to describe the -1 failure result.
--- 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 +- 5 files changed, 23 insertions(+), 13 deletions(-)
include/net/ipv6_frag.h +3 -1
@@ -125,7 +125,9 @@ ipv6frag_thdr_truncated(struct sk_buff *skb, int start, u8 *nexthdrp)int offset; offset =ipv6_skip_exthdr (skb, start, &nexthdr, &frag_off); -if (offset <0 || (frag_off &htons (IP6_OFFSET ))) +if (offset <0 ) +return true; +if (frag_off &htons (IP6_OFFSET ))return false;switch (nexthdr) {case NEXTHDR_TCP :
net/ipv4/esp4_offload.c +6 -2
@@ -168,10 +168,14 @@ static struct sk_buff *xfrm4_beet_gso_segment(struct xfrm_state *x, skb->transport_header -=IPV4_BEET_PHMAXLEN ; } }else {__be16 frag; +int offset; - skb->transport_header += -ipv6_skip_exthdr (skb,0 , &proto, &frag); + offset =ipv6_skip_exthdr (skb,0 , &proto, &frag); +if (offset <0 ) +return ERR_PTR (-EINVAL ); + + skb->transport_header += offset;if (proto ==IPPROTO_TCP )skb_shinfo (skb)->gso_type |=SKB_GSO_TCPV4 ; }
net/ipv6/esp6_offload.c +6 -2
@@ -210,10 +210,14 @@ static struct sk_buff *xfrm6_beet_gso_segment(struct xfrm_state *x,if (proto ==IPPROTO_TCP )skb_shinfo (skb)->gso_type |=SKB_GSO_TCPV6 ; }else {__be16 frag; +int offset; - skb->transport_header += -ipv6_skip_exthdr (skb,0 , &proto, &frag); + offset =ipv6_skip_exthdr (skb,0 , &proto, &frag); +if (offset <0 ) +return ERR_PTR (-EINVAL ); + + skb->transport_header += offset; }if (proto ==IPPROTO_IPIP )
net/ipv6/exthdrs_core.c +7 -7
@@ -48,15 +48,12 @@ EXPORT_SYMBOL(ipv6_ext_hdr);* "nexthdrp" initially points to some place, * where type of the first header can be found. * -* It skips all well-known exthdrs, and returns pointer to the start -* of unparsable area i.e. the first header with unknown type. +* It skips all well-known exthdrs, and returns the offset of the start +* of the first header with an unknown type. * If it is not NULL *nexthdr is updated by type/protocol of this header. * -* NOTES: - if packet terminated with NEXTHDR_NONE it returns NULL. -* - it may return pointer pointing beyond end of packet, -* if the last recognized header is truncated in the middle. -* - if packet is truncated, so that all parsed headers are skipped, -* it returns NULL. +* NOTES: - if packet terminates with NEXTHDR_NONE or is truncated while +* skipping extension headers, it returns -1. * - First fragment header is skipped, not-first ones * are considered as unparsable. * - Reports the offset field of the final fragment header so it is
@@ -107,6 +104,9 @@ int ipv6_skip_exthdr(const struct sk_buff *skb, int start, u8 *nexthdrp,else hdrlen =ipv6_optlen (hp); +if (skb->len - start < hdrlen) +return -1 ; + nexthdr = hp->nexthdr; start += hdrlen; }
net/ipv6/icmp.c +1 -1
@@ -145,7 +145,7 @@ static bool is_ineligible(const struct sk_buff *skb) ptr =ipv6_skip_exthdr (skb, ptr, &nexthdr, &frag_off);if (ptr <0 ) -return false; +return true;if (nexthdr ==IPPROTO_ICMPV6 ) {u8 _type, *tp; tp = skb_header_pointer(skb,
Signature · 1 line
2.34.1
PATCH 2/2 i40e: validate TCP header before ATR access
In reply to Ren WeiFrom: Zixuan Chai <[email protected]>
i40e_atr() uses ipv6_find_hdr() to locate the TCP header. A successful protocol match does not verify that the complete TCP header is present in the skb, so dereferencing the result can access data beyond the packet.
Check that the complete TCP header is available before inspecting it.
--- drivers/net/ethernet/intel/i40e/i40e_txrx.c | 3 +++ 1 file changed, 3 insertions(+)
drivers/net/ethernet/intel/i40e/i40e_txrx.c +3 -0
@@ -2911,6 +2911,9 @@ static void i40e_atr(struct i40e_ring *tx_ring, struct sk_buff *skb,if (l4_proto !=IPPROTO_TCP )return ; +if (unlikely (skb_tail_pointer (skb) < hdr.network + hlen + +sizeof (struct tcphdr ))) +return ; th = (struct tcphdr *)(hdr.network + hlen);
Signature · 1 line
2.34.1
Eric Dumazet
maintainerRe: [PATCH net v1 2/2] i40e: validate TCP header before ATR access
Quoted 25 lines · Ren Wei
From: Zixuan Chai <[email protected]>
i40e_atr()usesipv6_find_hdr()to locate the TCP header. A successful protocol match does not verify that the complete TCP header is present in the skb, so dereferencing the result can access data beyond the packet.Check that the complete TCP header is available before inspecting it.
Fixes fd0a05ce74ef ("i40e: transmit, receive, and NAPI")Reported-by Florian Westphal <[email protected]>Assisted-by LLMSigned-off-by Zixuan Chai <[email protected]>Signed-off-by Ren Wei <[email protected]>--- drivers/net/ethernet/intel/i40e/i40e_txrx.c | 3 +++ 1 file changed, 3 insertions(+)drivers/net/ethernet/intel/i40e/i40e_txrx.c +0
-0
drivers/net/ethernet/intel/i40e/i40e_txrx.c static void i40e_atr(struct i40e_ring *tx_ring, struct sk_buff *skb, · line 2919
Quoted 9 lines
drivers/net/ethernet/intel/i40e/i40e_txrx.c +3
-0@@ -2911,6 +2911,9 @@ static void i40e_atr(struct i40e_ring *tx_ring, struct sk_buff *skb,if (l4_proto !=IPPROTO_TCP )return ; +if (unlikely (skb_tail_pointer (skb) < hdr.network + hlen + +sizeof (struct tcphdr ))) +return ; th = (struct tcphdr *)(hdr.network + hlen);
pw-bot: cr
1) Please do not add bogus Reported-by: / Closes: tags. Florian never reported an issue in i40e, unless this was not public?
2) How can this code be reached with a truncated or non-linear TCP header? Unlike ixgbe_atr(), i40e_atr() returns immediately unless tx_flags & (I40E_TX_FLAGS_IPV4 | I40E_TX_FLAGS_IPV6) is set. Those flags are only set in i40e_tx_enable_csum(), which only acts on CHECKSUM_PARTIAL packets and runs right before i40e_atr().
Furthermore, i40e_tx_enable_csum() (and i40e_tso()) already assumes the network and transport headers are present in the linear skb head and already dereferences ip.v4->version, ip.v6->nexthdr, and l4.tcp->doff before i40e_atr() is ever called. Even within i40e_atr(), hdr.network is already dereferenced prior to your check in the IPv4 branch.
RE: [PATCH net v1 2/2] i40e: validate TCP header before ATR access
In reply to Ren WeiQuoted 54 lines
-----Original Message-----
From: Ren Wei <[email protected]>
Sent: Saturday, September 26, 2026 8:23 PM
To: [email protected]; [email protected]
Cc: [email protected]; [email protected]; [email protected]; [email protected]; [email protected]; [email protected]; [email protected]; [email protected]; [email protected]; [email protected]; Nguyen, Anthony L <[email protected]>; Kitszel, Przemyslaw <[email protected]>; [email protected]; [email protected]; [email protected]; [email protected]; [email protected]
Subject: [PATCH net v1 2/2] i40e: validate TCP header before ATR accessFrom: Zixuan Chai <[email protected]>
i40e_atr()usesipv6_find_hdr()to locate the TCP header. A successful protocol match does not verify that the complete TCP header is present in the skb, so dereferencing the result can access data beyond the packet.Check that the complete TCP header is available before inspecting it.
Fixes fd0a05ce74ef ("i40e: transmit, receive, and NAPI")Reported-by Florian Westphal <[email protected]>devel/[email protected]/
Assisted-by LLMSigned-off-by Zixuan Chai <[email protected]>Signed-off-by Ren Wei <[email protected]>--- drivers/net/ethernet/intel/i40e/i40e_txrx.c | 3 +++ 1 file changed, 3 insertions(+)drivers/net/ethernet/intel/i40e/i40e_txrx.c +3
-1@@ -2911,6 +2911,9 @@ static void i40e_atr(struct i40e_ring *tx_ring, struct sk_buff *skb, if (l4_proto != IPPROTO_TCP) return; +if (unlikely (skb_tail_pointer (skb) < hdr.network + hlen + +sizeof (struct tcphdr ))) +return ; th = (struct tcphdr *)(hdr.network + hlen); --
Quoted 1 line
2.34.1