Netdev List
 help / color / mirror / Atom feed
From: "Michael S. Tsirkin" <mst@redhat.com>
To: Eric Dumazet <edumazet@kernel.org>
Cc: Eric Dumazet <edumazet@google.com>,
	"David S . Miller" <davem@davemloft.net>,
	Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>,
	Simon Horman <horms@kernel.org>,
	netdev@vger.kernel.org, Weiming Shi <bestswngs@gmail.com>,
	Willem de Bruijn <willemb@google.com>
Subject: Re: [PATCH net] net: always dissect GSO packets in __virtio_net_hdr_to_skb()
Date: Mon, 28 Sep 2026 04:10:21 -0400	[thread overview]
Message-ID: <20260928032419-mutt-send-email-mst@kernel.org> (raw)
In-Reply-To: <CAL4WiiqLV8GFDiUak5i8DyGR9b6PN3yy81iLcdmZAbkJxAHM1w@mail.gmail.com>

On Mon, Sep 28, 2026 at 08:31:53AM +0200, Eric Dumazet wrote:
> On Mon, Sep 28, 2026 at 8:22 AM Eric Dumazet <edumazet@kernel.org> wrote:
> >
> > On Mon, Sep 28, 2026 at 3:31 AM Michael S. Tsirkin <mst@redhat.com> wrote:
> > >
> > > On Sun, Sep 27, 2026 at 07:55:36PM +0000, Eric Dumazet wrote:
> > > > Commit 9e8db5913264 ("net: avoid false positives in untrusted gso
> > > > validation") added a '&& skb->network_header' check before flow-dissecting
> > > > GSO packets without VIRTIO_NET_HDR_F_NEEDS_CSUM in
> > > > __virtio_net_hdr_to_skb(), because some callers (such as tun_get_user(),
> > > > tun_xdp_one(), virtnet_receive_done(), and raw_verify_header()) called
> > > > virtio_net_hdr_*_to_skb() before initializing skb->network_header and
> > > > skb->dev.
> > > >
> > > > However, skb->network_header is an offset from skb->head, not a boolean
> > > > flag. When skb_headroom(skb) is 0 on a device without L2 headers (for
> > > > instance packet_snd() or tpacket_snd() on a tunnel/pure-L3 device where
> > > > LL_RESERVED_SPACE_EX(dev, 0) == 0),
> > >
> > > Hmm. I have:
> > >
> > > LL_RESERVED_SPACE_EX(dev, 0)
> > >         ((((hlen) + READ_ONCE((dev)->needed_headroom)) \
> > >           & ~(HH_DATA_MOD - 1)) + HH_DATA_MOD)
> > >
> > > #define HH_DATA_MOD     16
> > >
> > > So how can LL_RESERVED_SPACE_EX(dev, 0) == 0 ?
> > >
> > >
> >
> > In practice, skb->network_header was 0 on tun_get_user() (including the
> > reported reproducer), tun_xdp_one(), virtnet_receive_done(), and
> > raw_verify_header() because __alloc_skb() / __build_skb_around()
> > zero-initializes skb->network_header to 0 (unlike mac_header and
> > transport_header which are initialized to ~0U), and those callers invoked
> > virtio_net_hdr_*_to_skb() before setting skb->network_header.
> >
> > More generally, because 0 is both the initial value from alloc_skb() and a
> > valid offset whenever skb_headroom(skb) == 0, skb->network_header cannot
> > be used as a boolean to test whether the network header was initialized.
> >
> > I can send a v2 with the corrected commit message if preferred, the
> > patch stays the same.
> 
> Revised changelog would look like this, let me know if it looks ok this time.
> 
> net: always dissect GSO packets in __virtio_net_hdr_to_skb()
> 
> Commit 9e8db5913264 ("net: avoid false positives in untrusted gso
> validation") added a '&& skb->network_header' check before flow-dissecting
> GSO packets without VIRTIO_NET_HDR_F_NEEDS_CSUM in
> __virtio_net_hdr_to_skb(), because some callers (such as tun_get_user(),
> tun_xdp_one(), virtnet_receive_done(), and raw_verify_header()) called
> virtio_net_hdr_*_to_skb() before initializing skb->network_header and
> skb->dev.
> 
> Because __alloc_skb() and __build_skb_around() zero-initialize
> skb->network_header to 0 (unlike mac_header and transport_header which
> are initialized to ~0U), those four callers always had
> skb->network_header == 0 and bypassed flow dissection in
> __virtio_net_hdr_to_skb(). More generally, skb->network_header is an
> offset from skb->head (where 0 is also a valid offset whenever
> skb_headroom(skb) is 0), not a boolean flag.
> 
> Whenever the 'if (gso_type && skb->network_header)' branch was skipped,
> the fallback 'else if (gso_type)' only pulled nh_min_len + thlen (40 bytes
> for TCPv4) without dissecting the packet, without validating ip_proto or
> n_proto, and without setting skb->transport_header.
> 
> If the packet has a malformed network header, it is not rejected and a
> subsequent skb_probe_transport_header() also fails, leaving
> skb->transport_header at ~0U (0xffff). Similarly, if an IPv4 packet
> carries IP options (ihl > 5) or an IPv6 packet carries extension headers,
> pulling only nh_min_len + thlen can leave the TCP header outside
> skb->head. In both cases, tcp_hdrlen(skb) in skb_gso_transport_seglen()
> reads out-of-bounds:
> 
>   BUG: KASAN: slab-out-of-bounds in skb_gso_transport_seglen
>   Read of size 2 by task poc/133
>   skb_gso_transport_seglen (net/core/gso.c:155)
>   skb_gso_validate_mac_len (net/core/gso.c:270)
>   tbf_enqueue (net/sched/sch_tbf.c:260)
>   dev_qdisc_enqueue (net/core/dev.c:4227)
>   __dev_queue_xmit (net/core/dev.c:4884)
> 
> In addition, when skb->protocol is pre-set by a caller before
> __virtio_net_hdr_to_skb(), 'if (!skb->protocol)' is skipped and
> virtio_net_hdr_match_proto() was not checked.
> 
> Fix this by:
> 1. Initializing skb->dev and skb->network_header (plus skb->protocol for
>    IFF_TUN) before virtio_net_hdr_*_to_skb() in tun_get_user(),
>    tun_xdp_one(), virtnet_receive_done(), and raw_verify_header(). In
>    tun_get_user(), drop the redundant skb_reset_mac_header(skb) in the
>    IFF_TUN case since __virtio_net_hdr_to_skb() unconditionally resets
>    mac_header.
> 2. Removing '&& skb->network_header' and the unvalidated
>    'else if (gso_type)' fallback in __virtio_net_hdr_to_skb() so all GSO
>    packets without VIRTIO_NET_HDR_F_NEEDS_CSUM are flow-dissected, have
>    their transport header pulled into linear data, and have
>    skb->transport_header set.
> 3. Validating virtio_net_hdr_match_proto(keys.basic.n_proto, hdr_gso_type)
>    after skb_flow_dissect_flow_keys_basic().

I'm travelling for a week, if possible I'd like a bit of time to review this.

For now, I also asked Claude to find cases where this patch causes any
UAPI change (because if it does, there's a risk it will break some
userspace, right?).  It wrote the below test, which accepts a packet
without your patch and fails with, and claims this packet is valid and
was previously accepted. I tested it quickly and it seems to be true but I can't
analyze it now as I'm sleep deprived due to travel)

--->


/*
 * test_vlan_gso.c - Regression reproducer: VLAN-tagged GSO without NEEDS_CSUM
 *
 * Writes a VLAN-tagged TCPv4 GSO packet (no NEEDS_CSUM) to a TAP device.
 * Accepted without patch, rejected with patch = regression.
 *
 * Validates packet correctness with hex dump, memcmp, and pcap output.
 *
 * Packet layout (virtio_net_hdr + ethernet frame):
 *
 *   virtio_net_hdr (10 bytes):
 *     00/02        flags (0 = none, or 02 = DATA_VALID)
 *     01        gso_type = VIRTIO_NET_HDR_GSO_TCPV4
 *     3a 00     hdr_len = 58 (14 eth + 4 vlan + 20 ip + 20 tcp)
 *     78 05     gso_size = 1400
 *     00 00     csum_start (unused, no NEEDS_CSUM)
 *     00 00     csum_offset (unused)
 *
 *   Ethernet + VLAN (18 bytes):
 *     02:02:02:02:02:02  dst MAC
 *     04:04:04:04:04:04  src MAC
 *     81 00              ethertype = 802.1Q
 *     00 64              VLAN TCI: VID=100
 *     08 00              inner ethertype = IPv4
 *
 *   IPv4 (20 bytes):
 *     45 00 0b 18        v4, IHL=5, tot_len=2840
 *     00 00 00 00        id=0, flags=0, frag_off=0
 *     40 06              TTL=64, proto=TCP
 *     5b de              IP checksum (correct)
 *     0a 00 00 01        src = 10.0.0.1
 *     0a 00 00 02        dst = 10.0.0.2
 *
 *   TCP (20 bytes):
 *     30 39 00 50        sport=12345, dport=80
 *     00 00 00 01        seq=1
 *     00 00 00 00        ack=0
 *     50 10 ff ff        doff=5, flags=ACK, win=65535
 *     83 7b 00 00        checksum (correct), urgent=0
 *
 *   Payload: 2800 bytes of 'A' (0x41)
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/mount.h>
#include <sys/reboot.h>
#include <net/if.h>
#include <linux/if_tun.h>
#include <linux/virtio_net.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/reboot.h>
#include <arpa/inet.h>
#include <stdint.h>

static unsigned int csum_add(const void *data, int len, unsigned int initial)
{
	const unsigned short *p = data;
	unsigned int sum = initial;

	while (len > 1) {
		sum += *p++;
		len -= 2;
	}
	if (len)
		sum += *(const unsigned char *)p;
	return sum;
}

static unsigned short csum_fold(unsigned int sum)
{
	sum = (sum >> 16) + (sum & 0xffff);
	sum += sum >> 16;
	return ~sum;
}

static unsigned short ip_csum(const void *data, int len)
{
	return csum_fold(csum_add(data, len, 0));
}

static unsigned short tcp_csum(struct iphdr *iph, struct tcphdr *tcph,
			       const void *payload, int payload_len)
{
	struct {
		uint32_t saddr;
		uint32_t daddr;
		uint8_t zero;
		uint8_t protocol;
		uint16_t tcp_len;
	} __attribute__((packed)) pseudo;
	unsigned int sum;
	int tcp_len = sizeof(struct tcphdr) + payload_len;

	pseudo.saddr = iph->saddr;
	pseudo.daddr = iph->daddr;
	pseudo.zero = 0;
	pseudo.protocol = IPPROTO_TCP;
	pseudo.tcp_len = htons(tcp_len);

	sum = csum_add(&pseudo, sizeof(pseudo), 0);
	sum = csum_add(tcph, sizeof(struct tcphdr), sum);
	sum = csum_add(payload, payload_len, sum);
	return csum_fold(sum);
}

static void hexdump(const char *label, const unsigned char *data, int len)
{
	int i;

	printf("%s (%d bytes):\n", label, len);
	for (i = 0; i < len; i++) {
		if (i % 16 == 0)
			printf("  %04x: ", i);
		printf("%02x ", data[i]);
		if (i % 16 == 15 || i == len - 1)
			printf("\n");
	}
}

static void write_pcap(const char *path, const unsigned char *pkt, int len)
{
	FILE *f;
	uint32_t val32;
	uint16_t val16;
	uint32_t pkt_hdr[4];

	f = fopen(path, "wb");
	if (!f) {
		printf("cannot write pcap: %s\n", strerror(errno));
		return;
	}

	/* pcap global header */
	val32 = 0xa1b2c3d4; fwrite(&val32, 4, 1, f); /* magic */
	val16 = 2;           fwrite(&val16, 2, 1, f); /* version major */
	val16 = 4;           fwrite(&val16, 2, 1, f); /* version minor */
	val32 = 0;           fwrite(&val32, 4, 1, f); /* thiszone */
	val32 = 0;           fwrite(&val32, 4, 1, f); /* sigfigs */
	val32 = 65535;       fwrite(&val32, 4, 1, f); /* snaplen */
	val32 = 1;           fwrite(&val32, 4, 1, f); /* network (LINKTYPE_ETHERNET) */

	/* packet header */
	pkt_hdr[0] = 0;     /* ts_sec */
	pkt_hdr[1] = 0;     /* ts_usec */
	pkt_hdr[2] = len;   /* incl_len */
	pkt_hdr[3] = len;   /* orig_len */
	fwrite(pkt_hdr, sizeof(pkt_hdr), 1, f);

	/* packet data */
	fwrite(pkt, 1, len, f);
	fclose(f);
	printf("wrote pcap: %s\n", path);
}

static int validate_packet(const unsigned char *frame, int frame_len,
			    int ip_off, int tcp_off, int pay_off)
{
	struct ethhdr *eth = (struct ethhdr *)frame;
	struct iphdr *iph = (struct iphdr *)(frame + ip_off);
	struct tcphdr *tcph = (struct tcphdr *)(frame + tcp_off);
	int payload_len = frame_len - pay_off;
	unsigned short got, expect;
	int ok = 1;

	/* ETH */
	if (ntohs(eth->h_proto) != ETH_P_8021Q) {
		printf("  MISMATCH: ethertype %04x != 8021Q\n",
		       ntohs(eth->h_proto));
		ok = 0;
	}

	/* VLAN: inner ethertype */
	if (frame[sizeof(struct ethhdr) + 2] != 0x08 ||
	    frame[sizeof(struct ethhdr) + 3] != 0x00) {
		printf("  MISMATCH: inner ethertype %02x%02x != 0800\n",
		       frame[sizeof(struct ethhdr) + 2],
		       frame[sizeof(struct ethhdr) + 3]);
		ok = 0;
	}

	/* IP checksum */
	got = iph->check;
	iph->check = 0;
	expect = ip_csum(iph, iph->ihl * 4);
	iph->check = got;
	if (got != expect) {
		printf("  MISMATCH: IP csum %04x != %04x\n",
		       ntohs(got), ntohs(expect));
		ok = 0;
	}

	/* TCP checksum */
	got = tcph->check;
	tcph->check = 0;
	expect = tcp_csum(iph, tcph, frame + pay_off, payload_len);
	tcph->check = got;
	if (got != expect) {
		printf("  MISMATCH: TCP csum %04x != %04x\n",
		       ntohs(got), ntohs(expect));
		ok = 0;
	}

	/* IP header fields */
	if (iph->version != 4 || iph->ihl != 5) {
		printf("  MISMATCH: IP ver/ihl %d/%d\n",
		       iph->version, iph->ihl);
		ok = 0;
	}
	if (iph->protocol != IPPROTO_TCP) {
		printf("  MISMATCH: IP proto %d != TCP\n", iph->protocol);
		ok = 0;
	}
	if (ntohs(iph->tot_len) != frame_len - ip_off) {
		printf("  MISMATCH: IP tot_len %d != %d\n",
		       ntohs(iph->tot_len), frame_len - ip_off);
		ok = 0;
	}

	/* TCP header */
	if (tcph->doff != 5) {
		printf("  MISMATCH: TCP doff %d != 5\n", tcph->doff);
		ok = 0;
	}

	if (ok)
		printf("  packet validation: OK\n");
	return ok;
}

int main(void)
{
	unsigned char buf[4096];
	struct virtio_net_hdr *vhdr;
	struct ethhdr *eth;
	struct iphdr *iph;
	struct tcphdr *tcph;
	struct ifreq ifr;
	int fd, sock, ret;
	int payload_len = 2800;

	int vhdr_off = 0;
	int eth_off = sizeof(struct virtio_net_hdr);
	int vlan_off = eth_off + sizeof(struct ethhdr);
	int ip_off = vlan_off + 4;
	int tcp_off = ip_off + sizeof(struct iphdr);
	int pay_off = tcp_off + sizeof(struct tcphdr);
	int total = pay_off + payload_len;

	/* frame offsets (without virtio_net_hdr) */
	int f_ip_off = ip_off - eth_off;
	int f_tcp_off = tcp_off - eth_off;
	int f_pay_off = pay_off - eth_off;
	int frame_len = total - eth_off;

	mount("proc", "/proc", "proc", 0, NULL);
	mount("sysfs", "/sys", "sysfs", 0, NULL);
	mount("devtmpfs", "/dev", "devtmpfs", 0, NULL);

	memset(buf, 0, total);

	/* virtio_net_hdr - filled per test below */
	vhdr = (struct virtio_net_hdr *)(buf + vhdr_off);

	/* Ethernet header */
	eth = (struct ethhdr *)(buf + eth_off);
	memset(eth->h_dest, 0x02, ETH_ALEN);
	memset(eth->h_source, 0x04, ETH_ALEN);
	eth->h_proto = htons(ETH_P_8021Q);

	/* 802.1Q: VID=100, inner ethertype=IPv4 */
	buf[vlan_off + 0] = 0x00;
	buf[vlan_off + 1] = 0x64;
	buf[vlan_off + 2] = 0x08;
	buf[vlan_off + 3] = 0x00;

	/* IP header */
	iph = (struct iphdr *)(buf + ip_off);
	iph->ihl = 5;
	iph->version = 4;
	iph->tot_len = htons(sizeof(struct iphdr) + sizeof(struct tcphdr) +
			     payload_len);
	iph->ttl = 64;
	iph->protocol = IPPROTO_TCP;
	iph->saddr = htonl(0x0a000001);
	iph->daddr = htonl(0x0a000002);
	iph->check = ip_csum(iph, sizeof(struct iphdr));

	/* TCP header */
	tcph = (struct tcphdr *)(buf + tcp_off);
	tcph->source = htons(12345);
	tcph->dest = htons(80);
	tcph->seq = htonl(1);
	tcph->doff = sizeof(struct tcphdr) / 4;
	tcph->ack = 1;
	tcph->window = htons(65535);

	/* Payload */
	memset(buf + pay_off, 'A', payload_len);

	/* TCP checksum over pseudo-header + TCP header + payload */
	tcph->check = tcp_csum(iph, tcph, buf + pay_off, payload_len);

	/* Set virtio_net_hdr GSO fields for dump (flags adjusted per test) */
	vhdr->gso_type = VIRTIO_NET_HDR_GSO_TCPV4;
	vhdr->gso_size = 1400;
	vhdr->hdr_len = tcp_off - eth_off + sizeof(struct tcphdr);

	/* Validate and dump */
	printf("\n=== Packet validation ===\n");
	validate_packet(buf + eth_off, frame_len, f_ip_off, f_tcp_off, f_pay_off);
	hexdump("virtio_net_hdr (flags=0)", buf, sizeof(struct virtio_net_hdr));
	hexdump("Ethernet frame (first 80 bytes)",
		buf + eth_off, frame_len < 80 ? frame_len : 80);
	write_pcap("/tmp/vlan_gso.pcap", buf + eth_off, frame_len);

	/* Open TAP */
	fd = open("/dev/net/tun", O_RDWR);
	if (fd < 0) {
		perror("open tun");
		goto fail;
	}
	memset(&ifr, 0, sizeof(ifr));
	ifr.ifr_flags = IFF_TAP | IFF_NO_PI | IFF_VNET_HDR;
	strncpy(ifr.ifr_name, "tap0", IFNAMSIZ - 1);
	if (ioctl(fd, TUNSETIFF, &ifr) < 0) {
		perror("TUNSETIFF");
		goto fail;
	}
	sock = socket(AF_INET, SOCK_DGRAM, 0);
	memset(&ifr, 0, sizeof(ifr));
	strncpy(ifr.ifr_name, "tap0", IFNAMSIZ - 1);
	ifr.ifr_flags = IFF_UP;
	ioctl(sock, SIOCSIFFLAGS, &ifr);
	close(sock);

	/* Test 1: flags=0 (no NEEDS_CSUM, no DATA_VALID) */
	vhdr->flags = 0;

	printf("\n=== TAP write tests ===\n");
	ret = write(fd, buf, total);
	if (ret == total)
		printf("PASS: VLAN GSO flags=0 accepted (%d bytes)\n", ret);
	else
		printf("FAIL: VLAN GSO flags=0 rejected: %s\n", strerror(errno));

	/* Test 2: flags=DATA_VALID (realistic: LRO -> bridge -> TAP) */
	vhdr->flags = VIRTIO_NET_HDR_F_DATA_VALID;

	ret = write(fd, buf, total);
	if (ret == total)
		printf("PASS: VLAN GSO DATA_VALID accepted (%d bytes)\n", ret);
	else
		printf("FAIL: VLAN GSO DATA_VALID rejected: %s\n", strerror(errno));

	close(fd);
	sync();
	reboot(LINUX_REBOOT_CMD_POWER_OFF);
	return 0;

fail:
	sync();
	reboot(LINUX_REBOOT_CMD_POWER_OFF);
	return 1;
}

-- 
MST


  reply	other threads:[~2026-09-28  8:10 UTC|newest]

Thread overview: 10+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-27 19:55 [PATCH net] net: always dissect GSO packets in __virtio_net_hdr_to_skb() Eric Dumazet
2026-09-27 20:33 ` Michael S. Tsirkin
2026-09-27 22:11   ` Eric Dumazet
2026-09-28  0:54 ` Willem de Bruijn
2026-09-28  1:31 ` Michael S. Tsirkin
2026-09-28  6:22   ` Eric Dumazet
2026-09-28  6:31     ` Eric Dumazet
2026-09-28  8:10       ` Michael S. Tsirkin [this message]
2026-09-28 10:35         ` Eric Dumazet
2026-09-29  3:55 ` netdev-bot+sashiko

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=20260928032419-mutt-send-email-mst@kernel.org \
    --to=mst@redhat.com \
    --cc=bestswngs@gmail.com \
    --cc=davem@davemloft.net \
    --cc=edumazet@google.com \
    --cc=edumazet@kernel.org \
    --cc=horms@kernel.org \
    --cc=kuba@kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=willemb@google.com \
    /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