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 v4 4/7] net: resolve uncommon partial checksums at ingress
Date: Thu, 22 Oct 2026 07:50:38 +0000	[thread overview]
Message-ID: <sashiko-outbox-181140@kernel.org> (raw)
In-Reply-To: <5de86f1afe1165a1a13ac4ef0615b19f4a10cb7a.1792565282.git.mst@redhat.com>

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] net: virtio_net_hdr_to_skb() runs skb_checksum_help() before skb_zcopy_init()
- [High] net: skb->protocol ignored on Ethernet/loopback in virtio_net_l3_start()
- [High] virtio_net: csum validation bypass on non-GSO skb in virtio_net_hdr_tnl_to_skb()
--

commit 69b2836e804af5188d86e52f3cfbb8777bb548d1
Author: Michael S. Tsirkin <mst@redhat.com>

net: resolve uncommon partial checksums at ingress

This patch validates csum_start and csum_offset on incoming packets with
virtio net headers, retaining CHECKSUM_PARTIAL only when the offsets match a
parsed TCP or UDP header. Non-GSO packets with unrecognized partial checksum
offsets have their checksums 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..ec6f47ce1c974 100644
> --- a/include/linux/virtio_net.h
> +++ b/include/linux/virtio_net.h
> @@ -48,6 +52,202 @@ static inline int virtio_net_hdr_set_proto(struct sk_buff *skb,
>  	return 0;
>  }
>  
> +/* TODO: nothing virtio specific here, move to ip.h */
> +
> +/*
> + * Pull and minimally validate the IPv4 header at @nhoff.
> + * Returns the header and stores the transport header offset in
> + * @thoff, or NULL if truncated or malformed.  Only the fixed header
> + * is guaranteed linear; callers that need the options or the
> + * transport header must pull further.
> + */
> +static inline struct iphdr *virtio_net_ip4_parse(struct sk_buff *skb,
> +						  unsigned int nhoff,
> +						  unsigned int *thoff)
> +{
> +	struct iphdr *iph;
> +
> +	if (!pskb_may_pull(skb, nhoff + sizeof(struct iphdr)))
> +		return NULL;
> +
> +	iph = (struct iphdr *)(skb->data + nhoff);
> +	if (iph->version != 4 ||
> +	    iph->ihl < sizeof(struct iphdr) / sizeof(u32))
> +		return NULL;
> +
> +	*thoff = nhoff + iph->ihl * sizeof(u32);
> +	return iph;
> +}
> +
> +/* 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;
> +
> +	/* Loopback carries ethernet frames, too:
> +	 * loopback_xmit() calls eth_type_trans() on them.
> +	 */
> +	if (skb->dev->type == ARPHRD_ETHER ||
> +	    skb->dev->type == ARPHRD_LOOPBACK) {
> +		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;
> +		}

[Severity: High]
What happens here when an AF_PACKET SOCK_RAW socket with PACKET_VNET_HDR sends
a frame whose skb->protocol disagrees with eth_hdr(skb)->h_proto?

In net/packet/af_packet.c, both packet_snd() and tpacket_snd() (via
tpacket_fill_skb()) set skb->protocol from the socket or sll_protocol and
place skb->network_header at ETH_HLEN before calling virtio_net_hdr_to_skb(),
and then call packet_parse_headers():

net/packet/af_packet.c:packet_snd() {
	...
	if (proto != htons(ETH_P_ALL))
		skb->protocol = proto;
	skb->dev = dev;
	...
	if (vnet_hdr_sz) {
		err = virtio_net_hdr_to_skb(skb, &vnet_hdr, vio_le());
		if (err < 0)
			goto out_free;
		len += vnet_hdr_sz;
	}

	packet_parse_headers(skb, sock);
	...
}

In packet_parse_headers(), a pre-set skb->protocol is preserved, and
skb->network_header is only advanced past VLAN tags when skb->dev->type is
ARPHRD_ETHER and 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);
}

Because virtio_net_l3_start() ignores a pre-set skb->protocol on ARPHRD_ETHER
and ARPHRD_LOOPBACK devices and unconditionally parses eth_hdr(skb)->h_proto
and VLAN tags via __vlan_get_protocol(), a sender can set sll_protocol to
htons(ETH_P_IP) while placing htons(ETH_P_8021Q) in eth_hdr(skb)->h_proto
(or send a VLAN-tagged frame on ARPHRD_LOOPBACK).

In that case, virtio_net_l3_start() advances *nhoff to 18 and validates
csum_start and csum_offset against a crafted IPv4 and UDP/TCP header embedded
inside the real IPv4 header at offset 14, keeping CHECKSUM_PARTIAL on the
skb. Downstream paths that rely on skb->protocol and skb_network_offset(skb)
at ETH_HLEN (such as nf_hook_egress(), sch_handle_egress(),
ipvlan_process_outbound(), and skb_mac_gso_segment()) will then inspect or
segment the packet at offset 14 before checksum completion overwrites the two
bytes at csum_start + csum_offset.

Should virtio_net_l3_start() honor a pre-set skb->protocol (and avoid
advancing past VLAN headers on ARPHRD_LOOPBACK when skb->network_header will
remain at ETH_HLEN) so that checksum validation parses the same L3 header and
offset as the rest of the stack?

[ ... ]

> +/*
> + * 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.
> + */
> +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;
> +}
> +
>  static inline int __virtio_net_hdr_to_skb(struct sk_buff *skb,
>  					  const struct virtio_net_hdr *hdr,
>  					  bool little_endian, u8 hdr_gso_type)
> @@ -194,11 +394,35 @@ static inline int __virtio_net_hdr_to_skb(struct sk_buff *skb,
>  	return 0;
>  }
>  
> +/*
> + * Return value:
> + * 0 - success
> + * >0 - checksum was completed in software (slow path)
> + * <0 - failure
> + */
>  static inline int virtio_net_hdr_to_skb(struct sk_buff *skb,
>  					const struct virtio_net_hdr *hdr,
>  					bool little_endian)
>  {
> -	return __virtio_net_hdr_to_skb(skb, hdr, little_endian, hdr->gso_type);
> +	typeof(skb->transport_header) old_transport = skb->transport_header;
> +	int ret = __virtio_net_hdr_to_skb(skb, hdr, little_endian,
> +					  hdr->gso_type);
> +
> +	if (ret < 0)
> +		return ret;
> +
> +	if (skb->ip_summed != CHECKSUM_PARTIAL ||
> +	    virtio_net_csum_partial_ok(skb, hdr->gso_type))
> +		return 0;
> +
> +	/*
> +	 * Here we do not trust the checksum metadata.
> +	 * __virtio_net_hdr_to_skb set the transport header to csum_start
> +	 * from virtio hdr, restore it to a known good value.
> +	 */
> +	skb->transport_header = old_transport;
> +
> +	return __virtio_net_resolve_csum_partial_mismatch(skb);
>  }

[Severity: High]
Can calling skb_checksum_help() inside virtio_net_hdr_to_skb() bypass the
shared fragment linearization check on zerocopy SKBs?

In net/core/dev.c, skb_checksum_help() checks skb_has_shared_frag(skb) (which
tests SKBFL_SHARED_FRAG in skb_shinfo(skb)->flags) and linearizes the skb so
that userspace-mapped pages cannot be mutated during or after checksum
calculation:

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;
	}
	...
	*(__sum16 *)(skb->data + offset) = csum_fold(csum) ?: CSUM_MANGLED_0;
out_set_summed:
	skb->ip_summed = CHECKSUM_NONE;
}

However, in tap_get_user() (drivers/net/tap.c), tun_get_user()
(drivers/net/tun.c), and tpacket_snd() (net/packet/af_packet.c), userspace
pages are attached to skb_shinfo(skb)->frags[] first (via
zerocopy_sg_from_iter() or tpacket_fill_skb()), then virtio_net_hdr_to_skb()
is called, and skb_zcopy_init() (which sets SKBFL_ZEROCOPY_FRAG, i.e.,
SKBFL_ZEROCOPY_ENABLE | SKBFL_SHARED_FRAG) is only called afterwards:

drivers/net/tap.c:tap_get_user() {
	...
	if (zerocopy)
		err = zerocopy_sg_from_iter(skb, from);
	...
	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);
	}
}

When a non-GSO zerocopy packet takes the slow path in virtio_net_hdr_to_skb()
-> __virtio_net_resolve_csum_partial_mismatch() -> skb_checksum_help(),
SKBFL_SHARED_FRAG is not yet set on the skb. As a result,
skb_has_shared_frag(skb) evaluates to false, __skb_linearize(skb) is skipped,
skb_checksum_help() computes the checksum directly over writable userspace
pages and sets skb->ip_summed = CHECKSUM_NONE, and skb_zcopy_init() then marks
the fragments as shared after the checksum has already been finalized.

[ ... ]

> @@ -335,9 +589,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 +610,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 +625,18 @@ 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;
> +
> +	/* A tunnel header implies GSO, so drop. */
> +	return -EINVAL;
>  }

[Severity: High]
Does virtio_net_hdr_tnl_to_skb() need to verify that skb_is_gso(skb) is true
after calling __virtio_net_hdr_to_skb()?

In __virtio_net_hdr_to_skb(), when skb->len - nh_off <= gso_size, the packet
is treated as non-GSO and shinfo->gso_size and shinfo->gso_type are left as 0,
while skb->ip_summed remains CHECKSUM_PARTIAL at hdr->csum_start:

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;
		}
	}

	return 0;
}

If a sender on tun (tun_get_user() or tun_xdp_one()) or virtio_net
(virtnet_receive_done()) submits a small UDP packet with a tunnel GSO type,
VIRTIO_NET_HDR_F_NEEDS_CSUM, and gso_size >= skb->len - nh_off,
__virtio_net_hdr_to_skb() returns 0 with skb_is_gso(skb) false.

virtio_net_hdr_tnl_to_skb() then sets SKB_GSO_UDP_TUNNEL (or
SKB_GSO_UDP_TUNNEL_CSUM) on the non-GSO skb and validates csum_start against
the untrusted vhdr->inner_nh_offset inside the UDP payload via
virtio_net_csum_partial_ok_at_offset(). By placing a fake 20-byte IPv4 header
stub at inner_nh_offset inside the UDP payload and setting csum_start to
inner_nh + 20, the packet passes validation and enters the stack as a non-GSO
UDP packet with CHECKSUM_PARTIAL pointing inside the UDP payload (and with
shinfo->gso_type non-zero while shinfo->gso_size is 0, which also triggers
WARN_ON_ONCE(1) in tun_vnet_hdr_tnl_from_skb() if forwarded to a tun device
without NETIF_F_GSO_UDP_TUNNEL).

[ ... ]

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

  parent reply	other threads:[~2026-10-22  7:50 UTC|newest]

Thread overview: 22+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-21  7:50 [PATCH net v4 0/7] more virtio-net csum hardening Michael S. Tsirkin
2026-10-21  7:49 ` [PATCH net v4 1/7] net: account for ingress-resolved virtio net hdr state Michael S. Tsirkin
2026-10-21  7:49   ` [PATCH net v4 2/7] um: vector: trim skbs before verifying the header Michael S. Tsirkin
2026-10-21 16:08     ` Willem de Bruijn
2026-10-22  7:50     ` sashiko-bot
2026-10-21  7:49   ` [PATCH net v4 3/7] virtio_net: parse tunnel csum_start little-endian Michael S. Tsirkin
2026-10-22  7:50     ` sashiko-bot
2026-10-21  7:50   ` [PATCH net v4 4/7] net: resolve uncommon partial checksums at ingress Michael S. Tsirkin
2026-10-21 16:13     ` Willem de Bruijn
2026-10-22  7:50     ` sashiko-bot [this message]
2026-10-21  7:50   ` [PATCH net v4 5/7] net: add a sysctl to opt out of virtio net hdr checksum validation Michael S. Tsirkin
2026-10-21 16:14     ` Willem de Bruijn
2026-10-22  7:50     ` sashiko-bot
2026-10-21  7:50   ` [PATCH net v4 6/7] net: add ip4_parse() Michael S. Tsirkin
2026-10-21 16:18     ` Willem de Bruijn
2026-10-22  7:50     ` sashiko-bot
2026-10-21  7:50   ` [PATCH net v4 7/7] virtio_net: cleanup protocol calculation Michael S. Tsirkin
2026-10-22  7:50     ` sashiko-bot
2026-10-21 16:08   ` [PATCH net v4 1/7] net: account for ingress-resolved virtio net hdr state Willem de Bruijn
2026-10-21 16:28     ` Michael S. Tsirkin
2026-10-22  7:50   ` sashiko-bot
2026-10-21 18:49 ` [PATCH net v4 0/7] more virtio-net csum hardening Chris Arges

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-181140@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