Linux virtualization list
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Michael S. Tsirkin" <mst@redhat.com>
Cc: virtualization@lists.linux.dev, "Eugenio Perez" <eperezma@redhat.com>
Subject: Re: [PATCH net v3 5/8] net: resolve uncommon partial checksums at ingress
Date: Tue, 20 Oct 2026 12:04:01 +0000	[thread overview]
Message-ID: <sashiko-outbox-178921@kernel.org> (raw)
In-Reply-To: <9c08d12b3a7819ae03ded6a154e4f908276e93d4.1792411304.git.mst@redhat.com>

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] net: skb_checksum_help() in virtio_net_hdr_to_skb() skips shared frag copy
- [High] net: virtio_net_hdr_tnl_to_skb() corrupts non-GSO skb state on tunnel hdr
- [High] net: virtio_net_l4_start() skips IPv6 NEXTHDR_AUTH by omitting IP6_FH_F_AUTH
- [High] net: virtio_net_l3_start() L2/L3 offset and protocol mismatch on lo and AF_PACKET
--

commit 338a1355fe27c4686a83ea9e73bf390729be2308
Author: Michael S. Tsirkin <mst@redhat.com>

net: resolve uncommon partial checksums at ingress

This commit validates csum_start and csum_offset at ingress so that
CHECKSUM_PARTIAL is only retained when matching a parsed TCP or UDP
header. Non-GSO packets with unrecognized checksum offsets are completed
immediately in software via skb_checksum_help(), while unrecognized GSO
packets are dropped.

> diff --git a/include/linux/virtio_net.h b/include/linux/virtio_net.h
> index 013c6c7e2dea0..97b554ef7123f 100644
> --- a/include/linux/virtio_net.h
> +++ b/include/linux/virtio_net.h
[ ... ]
> @@ -48,6 +52,198 @@ static inline int virtio_net_hdr_set_proto(struct sk_buff *skb,
[ ... ]
> +/* Where does the L3 header start, and what protocol is it?
> + * Handles Ethernet and VLAN headers; other devices use the
> + * network header offset the caller set up.
> + */
> +static inline bool
> +virtio_net_l3_start(struct sk_buff *skb, int *nhoff, __be16 *proto)
> +{
> +	/* skb->dev is only guaranteed to be set since 44378d02c5aa
> +	 * ("net: always dissect GSO packets in
> +	 * __virtio_net_hdr_to_skb()"), do not crash when it is not.
> +	 */
> +	if (WARN_ON_ONCE(!skb->dev))
> +		return false;
> +
> +	if (skb->dev->type == ARPHRD_ETHER) {
> +		if (!pskb_may_pull(skb, ETH_HLEN))
> +			return false;
> +		*nhoff = ETH_HLEN;
> +		*proto = eth_hdr(skb)->h_proto;
> +		if (eth_type_vlan(*proto)) {
> +			*proto = __vlan_get_protocol(skb, *proto, nhoff);
> +			if (!*proto)
> +				return false;
> +		}
> +	} else {
> +		*nhoff = skb_network_offset(skb);
> +		/* An unset network header shows up as an offset
> +		 * outside of the packet.
> +		 */
> +		if (*nhoff < 0 || (unsigned int)*nhoff >= skb->len)
> +			return false;
> +		*proto = skb->protocol;
> +		if (!*proto) {
> +			u8 version;
> +
> +			/* A packet socket bound to ETH_P_ALL leaves the
> +			 * protocol unset: infer it from the IP version
> +			 * nibble at the network header.
> +			 */
> +			if (!pskb_may_pull(skb, *nhoff + 1))
> +				return false;
> +			version = skb->data[*nhoff] >> 4;
> +			if (version == 4)
> +				*proto = htons(ETH_P_IP);
> +			else if (version == 6)
> +				*proto = htons(ETH_P_IPV6);
> +		}
> +	}
> +	return true;
> +}

[Severity: High]
Can virtio_net_l3_start() disagree with the networking stack on the L3
offset and protocol when packets are sent via AF_PACKET (packet_snd() or
tpacket_snd()) with PACKET_VNET_HDR enabled?

First, on loopback (where dev->type is ARPHRD_LOOPBACK, but
dev->hard_header_len is ETH_HLEN and loopback_xmit() calls
eth_type_trans()), virtio_net_l3_start() takes the else branch and sets
*nhoff = skb_network_offset(skb) (14) without inspecting eth_hdr(skb) or
VLAN tags:

drivers/net/loopback.c:loopback_xmit() {
	skb->protocol = eth_type_trans(skb, dev);
}

If a packet on lo has eth_hdr(skb)->h_proto = htons(ETH_P_8021Q) and a
4-byte 802.1P priority tag (VLAN ID 0) with h_vlan_TCI = htons(0x6000) and
h_vlan_encapsulated_proto = htons(ETH_P_IP), virtio_net_l3_start() and
virtio_net_l4_start() can validate a fake IPv6 header starting at the VLAN
tag (offset 14), while loopback_xmit() -> __netif_receive_skb_core() ->
skb_vlan_untag() strips the 4-byte tag and delivers the inner IPv4 packet
at offset 18 with CHECKSUM_PARTIAL retained at the unvalidated offset.

Second, on ARPHRD_ETHER devices, virtio_net_l3_start() always uses
eth_hdr(skb)->h_proto and advances *nhoff past VLAN tags via
__vlan_get_protocol(), whereas AF_PACKET SOCK_RAW preserves a
user-supplied sll_protocol in skb->protocol and packet_parse_headers()
only advances skb->network_header when eth_type_vlan(skb->protocol) is
true:

net/packet/af_packet.c:packet_parse_headers() {
	if (!skb->protocol && sock->type == SOCK_RAW)
		skb->protocol = dev_parse_header_protocol(skb);

	skb_probe_transport_header(skb);

	/* Move network header to the right position for VLAN tagged packets */
	if (likely(skb->dev->type == ARPHRD_ETHER) &&
	    eth_type_vlan(skb->protocol) &&
	    vlan_get_protocol_and_depth(skb, skb->protocol, &depth) != 0)
		skb_set_network_header(skb, depth);
}

If sll_protocol is ETH_P_IP while eth_hdr(skb)->h_proto is ETH_P_8021Q (or
vice versa), wouldn't virtio_net_l3_start() validate csum_start against a
different protocol and network header offset than what skb->protocol and
skb->network_header represent for downstream processing?

> +
> +/* Walk the L3 header at @nhoff: where does its transport header
> + * start, and which protocol is it?  Returns false if the header
> + * is truncated, malformed or fragmented.
> + */
> +static inline bool
> +virtio_net_l4_start(struct sk_buff *skb, unsigned int nhoff, __be16 proto,
> +		    unsigned int *thoff, int *ip_proto)
> +{
> +	if (proto == htons(ETH_P_IP)) {
> +		const struct iphdr *iph = virtio_net_ip4_parse(skb, nhoff, thoff);
> +
> +		if (!iph || ip_is_fragment(iph))
> +			return false;
> +		*ip_proto = iph->protocol;
> +		return true;
> +	}
> +
> +	if (proto == htons(ETH_P_IPV6)) {
> +		const struct ipv6hdr *iph;
> +		int flags = 0;
> +
> +		if (!pskb_may_pull(skb, nhoff + sizeof(*iph)))
> +			return false;
> +		iph = (const void *)(skb->data + nhoff);
> +		if (iph->version != 6)
> +			return false;
> +		*thoff = nhoff + sizeof(*iph);
> +		*ip_proto = iph->nexthdr;
> +		if (*ip_proto != IPPROTO_TCP && *ip_proto != IPPROTO_UDP) {
> +			*thoff = nhoff;
> +			*ip_proto = ipv6_find_hdr(skb, thoff, -1, NULL,
> +						  &flags);
> +			if (*ip_proto < 0 || flags & IP6_FH_F_FRAG)
> +				return false;
> +		}
> +		return true;
> +	}
> +
> +	return false;
> +}

[Severity: High]
Should flags in virtio_net_l4_start() be initialized to IP6_FH_F_AUTH
before calling ipv6_find_hdr()?

In ipv6_find_hdr(), NEXTHDR_AUTH is only treated as a terminal header when
IP6_FH_F_AUTH is passed in flags; otherwise it skips ipv6_authlen(hp)
bytes and continues walking into the following header:

net/ipv6/exthdrs_core.c:ipv6_find_hdr() {
		} else if (nexthdr == NEXTHDR_AUTH) {
			if (flags && (*flags & IP6_FH_F_AUTH) && (target < 0))
				break;
			hdrlen = ipv6_authlen(hp);
		} else
}

Downstream parsers such as nft_set_pktinfo_ipv6() (which sets flags =
IP6_FH_F_AUTH) and __skb_flow_dissect() stop at NEXTHDR_AUTH rather than
skipping it. With flags = 0, an IPv6 packet carrying NEXTHDR_AUTH followed
by TCP or UDP passes virtio_net_csum_partial_ok_at_offset() and retains
CHECKSUM_PARTIAL, allowing later checksum completion to overwrite the 2
bytes at csum_start + csum_offset after netfilter or flow-dissector
inspection, and also allowing IPv6+AH GSO packets through even though
ipv6_gso_pull_exthdrs() does not support IPPROTO_AH.

[ ... ]
> +/*
> + * Handle a packet whose checksum metadata does not match the packet:
> + * GSO packets are dropped, as segmentation needs one checksum per
> + * segment; others are completed in software.
> + *
> + * Return 1 if the checksum was completed in software, else a negative
> + * errno.
> + */
> +static inline int virtio_net_resolve_csum_partial_mismatch(struct sk_buff *skb)
> +{
> +	if (skb_is_gso(skb))
> +		return -EINVAL;
> +
> +	return skb_checksum_help(skb) ?: 1;
> +}

[Severity: High]
When virtio_net_resolve_csum_partial_mismatch() calls skb_checksum_help()
from virtio_net_hdr_to_skb() or virtio_net_hdr_tnl_to_skb(), does
skb_checksum_help() miss copying shared zerocopy fragments because
skb_zcopy_init() has not been called yet?

In skb_checksum_help(), shared fragments are linearized before computing
the checksum so that userspace cannot modify the payload pages after the
checksum is calculated:

net/core/dev.c:skb_checksum_help() {
	/* Before computing a checksum, we should make sure no frag could
	 * be modified by an external entity : checksum could be wrong.
	 */
	if (skb_has_shared_frag(skb)) {
		ret = __skb_linearize(skb);
		if (ret)
			goto out;
	}
}

However, skb_has_shared_frag(skb) checks SKBFL_SHARED_FRAG in
skb_shinfo(skb)->flags, which is only set when skb_zcopy_init() runs.
In tun_get_user(), tap_get_user(), and tpacket_snd(), the userspace pages
are attached to the skb first, then virtio_net_hdr_to_skb() (or
tun_vnet_hdr_tnl_to_skb() / tun_vnet_hdr_to_skb()) is called, and
skb_zcopy_init() is only called afterwards:

drivers/net/tun.c:tun_get_user() {
	err = tun_vnet_hdr_tnl_to_skb(tun->flags, features, skb, &hdr);
	...
	/* copy skb_ubuf_info for callback when skb has no error */
	if (zerocopy) {
		skb_zcopy_init(skb, msg_control);
	}
}

drivers/net/tap.c:tap_get_user() {
	if (vnet_hdr_len) {
		err = tun_vnet_hdr_to_skb(q->flags, skb, &vnet_hdr);
		...
	}
	...
	/* copy skb_ubuf_info for callback when skb has no error */
	if (zerocopy) {
		skb_zcopy_init(skb, msg_control);
	}
}

net/packet/af_packet.c:tpacket_snd() {
		if (has_vnet_hdr) {
			if (virtio_net_hdr_to_skb(skb, &vnet_hdr, vio_le()) < 0) {
				tp_len = -EINVAL;
				goto tpacket_error;
			}
		}
		...
		uarg->ubuf.flags = SKBFL_ZEROCOPY_FRAG;
		refcount_set(&uarg->ubuf.refcnt, 1);

		/* Hold a sk_wmem_alloc reference until completion */
		refcount_inc(&po->sk.sk_wmem_alloc);
		skb_zcopy_init(skb, &uarg->ubuf);
}

Because SKBFL_SHARED_FRAG is not set yet when
virtio_net_resolve_csum_partial_mismatch() calls skb_checksum_help(),
skb_has_shared_frag(skb) returns false, __skb_linearize(skb) is skipped,
and the packet proceeds with skb->ip_summed = CHECKSUM_NONE while its
fragments remain backed by writable userspace pages.

[ ... ]
> @@ -335,9 +586,12 @@ virtio_net_hdr_tnl_to_skb(struct sk_buff *skb,
>  	/* Let the basic parsing deal with plain GSO features. */
>  	ret = __virtio_net_hdr_to_skb(skb, hdr, true,
>  				      hdr->gso_type & ~gso_tunnel_type);
> -	if (ret)
> +	if (ret < 0)
>  		return ret;
>  
> +	if (!virtio_net_tnl_offsets_ok(skb, outer_isv6, outer_th))
> +		return -EINVAL;
> +
>  	/* In case of USO, the inner protocol is still unknown and
>  	 * `inner_isv6` is just a guess, additional parsing is needed.
>  	 * The previous validation ensures that accessing an ipv4 inner
> @@ -353,7 +607,7 @@ virtio_net_hdr_tnl_to_skb(struct sk_buff *skb,
>  	}
>  
>  	skb_set_inner_protocol(skb, inner_isv6 ? htons(ETH_P_IPV6) :
> -						 htons(ETH_P_IP));
> +			    htons(ETH_P_IP));
>  	if (hdr->flags & VIRTIO_NET_HDR_F_UDP_TUNNEL_CSUM) {
>  		if (!tnl_csum_negotiated)
>  			return -EINVAL;
> @@ -368,7 +622,17 @@ virtio_net_hdr_tnl_to_skb(struct sk_buff *skb,
>  	skb->inner_mac_header = inner_nh + skb_headroom(skb);
>  	skb->transport_header = outer_th + skb_headroom(skb);
>  	skb->encapsulation = 1;
> -	return 0;
> +
> +	/*
> +	 * We know VIRTIO_NET_HDR_F_NEEDS_CSUM was set so __virtio_net_hdr_to_skb
> +	 * set CHECKSUM_PARTIAL.
> +	 */
> +	inner_proto = inner_isv6 ? htons(ETH_P_IPV6) : htons(ETH_P_IP);
> +	if (virtio_net_csum_partial_ok_at_offset(skb, gso_inner_type, inner_nh,
> +						 inner_proto))
> +		return 0;
> +
> +	return virtio_net_resolve_csum_partial_mismatch(skb);
>  }

[Severity: High]
What happens in virtio_net_hdr_tnl_to_skb() when a packet carries a tunnel
GSO type in hdr->gso_type, but its length is not greater than gso_size?

In __virtio_net_hdr_to_skb(), small packets do not populate
shinfo->gso_size or shinfo->gso_type:

include/linux/virtio_net.h:__virtio_net_hdr_to_skb() {
		/* Too small packets are not really GSO ones. */
		if (skb->len - nh_off > gso_size) {
			shinfo->gso_size = gso_size;
			shinfo->gso_type = gso_type;

			/* Header must be checked, and gso_segs computed. */
			shinfo->gso_type |= SKB_GSO_DODGY;
			shinfo->gso_segs = 0;
		}
}

Because shinfo->gso_size remains 0, skb_is_gso(skb) is false, but
virtio_net_hdr_tnl_to_skb() still sets shinfo->gso_type |=
SKB_GSO_UDP_TUNNEL_CSUM (or SKB_GSO_UDP_TUNNEL), sets inner header offsets
from vhdr->inner_nh_offset and hdr->csum_start, overwrites
skb->transport_header, and sets skb->encapsulation = 1.

If virtio_net_csum_partial_ok_at_offset() then returns false,
virtio_net_resolve_csum_partial_mismatch(skb) sees !skb_is_gso(skb),
completes the inner checksum via skb_checksum_help(), and returns 1
without cleaning up the tunnel state on the skb. That leaves the non-GSO
skb with skb->encapsulation = 1, unvalidated inner header offsets, a
non-zero shinfo->gso_type with gso_size == 0 (which triggers
WARN_ON_ONCE(1) in tun_vnet_hdr_tnl_from_skb() if forwarded to another
tun/virtio interface), and an uncomputed outer UDP checksum when
VIRTIO_NET_HDR_F_UDP_TUNNEL_CSUM was set.

Conversely, if the payload at the caller-chosen vhdr->inner_nh_offset
happens to look like a valid IP + TCP/UDP header,
virtio_net_csum_partial_ok_at_offset() returns true on this non-GSO UDP
packet, allowing CHECKSUM_PARTIAL to be retained at an arbitrary offset
inside the UDP payload. Should virtio_net_hdr_tnl_to_skb() reject packets
where !skb_is_gso(skb) after __virtio_net_hdr_to_skb()?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/cover.1792411304.git.mst@redhat.com?part=5

  reply	other threads:[~2026-10-20 12:04 UTC|newest]

Thread overview: 40+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-19 12:03 [PATCH net v3 0/8] more virtio-net csum hardening Michael S. Tsirkin
2026-10-19 12:03 ` [PATCH net v3 1/8] net: account for ingress-resolved virtio net hdr state Michael S. Tsirkin
2026-10-20 12:03   ` sashiko-bot
2026-10-20 19:27   ` Willem de Bruijn
2026-10-20 19:38     ` Michael S. Tsirkin
2026-10-20 20:02       ` Willem de Bruijn
2026-10-20 20:09         ` Michael S. Tsirkin
2026-10-19 12:03 ` [PATCH net v3 2/8] um: vector: honor virtio net hdr conversion failures Michael S. Tsirkin
2026-10-20 12:03   ` sashiko-bot
2026-10-20 19:28   ` Willem de Bruijn
2026-10-19 12:03 ` [PATCH net v3 3/8] um: vector: trim skbs before verifying the header Michael S. Tsirkin
2026-10-20 12:04   ` sashiko-bot
2026-10-19 12:03 ` [PATCH net v3 4/8] virtio_net: parse tunnel csum_start little-endian Michael S. Tsirkin
2026-10-20 12:04   ` sashiko-bot
2026-10-19 12:03 ` [PATCH net v3 5/8] net: resolve uncommon partial checksums at ingress Michael S. Tsirkin
2026-10-20 12:04   ` sashiko-bot [this message]
2026-10-20 19:44   ` Willem de Bruijn
2026-10-20 21:49     ` Michael S. Tsirkin
2026-10-21  0:05       ` Willem de Bruijn
2026-10-19 12:03 ` [PATCH net v3 6/8] net: add a sysctl to opt out of virtio net hdr checksum validation Michael S. Tsirkin
2026-10-19 14:19   ` Björn Töpel
2026-10-20 14:04     ` Michael S. Tsirkin
2026-10-20 12:04   ` sashiko-bot
2026-10-20 19:54   ` Willem de Bruijn
2026-10-20 20:01     ` Michael S. Tsirkin
2026-10-20 20:19       ` Willem de Bruijn
2026-10-20 20:22         ` Michael S. Tsirkin
2026-10-20 23:12           ` Willem de Bruijn
2026-10-20 23:21             ` Michael S. Tsirkin
2026-10-20 20:56     ` Michael S. Tsirkin
2026-10-20 23:13       ` Willem de Bruijn
2026-10-20 23:20         ` Michael S. Tsirkin
2026-10-19 12:03 ` [PATCH net v3 7/8] net: add ip4_parse() Michael S. Tsirkin
2026-10-20 12:04   ` sashiko-bot
2026-10-19 12:03 ` [PATCH net v3 8/8] virtio_net: cleanup protocol calculation Michael S. Tsirkin
2026-10-20 12:04   ` sashiko-bot
2026-10-19 12:09 ` [PATCH net v3 0/8] more virtio-net csum hardening netdev-bot+sinfo
2026-10-19 12:21   ` Michael S. Tsirkin
2026-10-19 16:51 ` Chris Arges
2026-10-21  2:32 ` Jakub Kicinski

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=sashiko-outbox-178921@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=eperezma@redhat.com \
    --cc=mst@redhat.com \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=virtualization@lists.linux.dev \
    /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