[PATCH net-next 3/4] net: resolve uncommon partial checksums at ingress

From: Michael S. Tsirkin

Date: Tue Oct 13 2026 - 07:24:09 EST


A virtio net header can place CHECKSUM_PARTIAL at any in-packet
offset. If that state is retained, later checksum completion can
modify the packet after protocol processing has made decisions from it.

Let's fix that: let's validate csum_start and csum_offset and only keep
CHECKSUM_PARTIAL when csum_start and csum_offset match a parsed TCP or
UDP header, while handling VLAN headers, IPv4 options and IPv6 extension
headers. Complete other non-GSO checksums immediately, before any
protocol processing runs.

We can't easily "complete the checksum" for GSO we don't recognize - GSO
assumes knowledge of the protocol. We could segment ignoring the
checksum then do whatever csum_start and csum_offset say, but that
seems like an overkill for something that should happen.

UDP tunnel headers get the same handling on the inner header.

Packets without VIRTIO_NET_HDR_F_NEEDS_CSUM are unaffected.

Suggested-by: Willem de Bruijn <willemdebruijn.kernel@xxxxxxxxx>
Signed-off-by: Michael S. Tsirkin <mst@xxxxxxxxxx>
Assisted-by: LLM
---
include/linux/virtio_net.h | 151 ++++++++++++++++++++++++++++++++-----
1 file changed, 133 insertions(+), 18 deletions(-)

diff --git a/include/linux/virtio_net.h b/include/linux/virtio_net.h
index d6466f96cdd00..b181183118b3a 100644
--- a/include/linux/virtio_net.h
+++ b/include/linux/virtio_net.h
@@ -5,7 +5,11 @@
#include <linux/if_vlan.h>
#include <linux/ip.h>
#include <linux/ipv6.h>
+#include <linux/netdevice.h>
#include <linux/udp.h>
+#include <net/gso.h>
+#include <net/ip.h>
+#include <net/ipv6.h>
#include <net/tcp.h>
#include <uapi/linux/tcp.h>
#include <uapi/linux/virtio_net.h>
@@ -48,15 +52,111 @@ static inline int virtio_net_hdr_set_proto(struct sk_buff *skb,
return 0;
}

+/* Does checksum metadata match a known TCP or UDP header? */
+static inline bool
+virtio_net_csum_partial_ok_at_offset(struct sk_buff *skb, u8 hdr_gso_type,
+ unsigned int nhoff, __be16 proto)
+{
+ unsigned int thoff;
+ int ip_proto;
+
+ if (proto == htons(ETH_P_IP)) {
+ const struct iphdr *iph = ip4_parse(skb, nhoff, &thoff);
+
+ if (!iph || ip_is_fragment(iph))
+ return false;
+ ip_proto = iph->protocol;
+ } else 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;
+ }
+ } else {
+ return false;
+ }
+
+ if (skb_checksum_start_offset(skb) != thoff)
+ return false;
+
+ switch (ip_proto) {
+ case IPPROTO_TCP:
+ if (skb->csum_offset != offsetof(struct tcphdr, check) ||
+ !pskb_may_pull(skb, thoff + sizeof(struct tcphdr)))
+ return false;
+ break;
+ case IPPROTO_UDP:
+ if (skb->csum_offset != offsetof(struct udphdr, check) ||
+ !pskb_may_pull(skb, thoff + sizeof(struct udphdr)))
+ return false;
+ break;
+ default:
+ return false;
+ }
+
+ switch (hdr_gso_type & ~VIRTIO_NET_HDR_GSO_ECN) {
+ case VIRTIO_NET_HDR_GSO_NONE:
+ return true;
+ case VIRTIO_NET_HDR_GSO_TCPV4:
+ return proto == htons(ETH_P_IP) && ip_proto == IPPROTO_TCP;
+ case VIRTIO_NET_HDR_GSO_TCPV6:
+ return proto == htons(ETH_P_IPV6) && ip_proto == IPPROTO_TCP;
+ case VIRTIO_NET_HDR_GSO_UDP:
+ case VIRTIO_NET_HDR_GSO_UDP_L4:
+ return ip_proto == IPPROTO_UDP;
+ default:
+ return false;
+ }
+}
+
+static inline bool
+virtio_net_csum_partial_ok(struct sk_buff *skb, u8 hdr_gso_type)
+{
+ unsigned int nhoff;
+ __be16 proto;
+
+ 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);
+ proto = skb->protocol;
+ }
+
+ return virtio_net_csum_partial_ok_at_offset(skb, hdr_gso_type, nhoff,
+ proto);
+}
+
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)
+ bool little_endian, u8 hdr_gso_type,
+ bool need_csum_check)
{
- unsigned int nh_min_len = sizeof(struct iphdr);
+ typeof(skb->transport_header) old_transport = skb->transport_header;
unsigned int gso_type = 0;
unsigned int thlen = 0;
unsigned int p_off = 0;
unsigned int ip_proto;
+ bool csum_partial_ok = true;

if (hdr_gso_type != VIRTIO_NET_HDR_GSO_NONE) {
switch (hdr_gso_type & ~VIRTIO_NET_HDR_GSO_ECN) {
@@ -69,7 +169,6 @@ static inline int __virtio_net_hdr_to_skb(struct sk_buff *skb,
gso_type = SKB_GSO_TCPV6;
ip_proto = IPPROTO_TCP;
thlen = sizeof(struct tcphdr);
- nh_min_len = sizeof(struct ipv6hdr);
break;
case VIRTIO_NET_HDR_GSO_UDP:
gso_type = SKB_GSO_UDP;
@@ -104,11 +203,11 @@ static inline int __virtio_net_hdr_to_skb(struct sk_buff *skb,

if (!skb_partial_csum_set(skb, start, off))
return -EINVAL;
- if (skb_transport_offset(skb) < nh_min_len)
- return -EINVAL;
+ if (need_csum_check)
+ csum_partial_ok =
+ virtio_net_csum_partial_ok(skb, hdr_gso_type);

- nh_min_len = skb_transport_offset(skb);
- p_off = nh_min_len + thlen;
+ p_off = skb_transport_offset(skb) + thlen;
if (!pskb_may_pull(skb, p_off))
return -EINVAL;
} else if (gso_type) {
@@ -158,8 +257,6 @@ static inline int __virtio_net_hdr_to_skb(struct sk_buff *skb,
case SKB_GSO_UDP_L4:
if (!(hdr->flags & VIRTIO_NET_HDR_F_NEEDS_CSUM))
return -EINVAL;
- if (skb->csum_offset != offsetof(struct udphdr, check))
- return -EINVAL;
if (skb->len - p_off > gso_size * UDP_MAX_SEGMENTS)
return -EINVAL;
if (gso_type != SKB_GSO_UDP_L4)
@@ -167,10 +264,6 @@ static inline int __virtio_net_hdr_to_skb(struct sk_buff *skb,
break;
case SKB_GSO_TCPV4:
case SKB_GSO_TCPV6:
- if (skb->ip_summed == CHECKSUM_PARTIAL &&
- skb->csum_offset != offsetof(struct tcphdr, check))
- return -EINVAL;
-
BUILD_BUG_ON(TCP_MIN_GSO_SIZE * GSO_MAX_SEGS < GSO_MAX_SIZE);
gso_size = max(gso_size, TCP_MIN_GSO_SIZE);
break;
@@ -191,6 +284,17 @@ static inline int __virtio_net_hdr_to_skb(struct sk_buff *skb,
}
}

+ if ((hdr->flags & VIRTIO_NET_HDR_F_NEEDS_CSUM) && !csum_partial_ok) {
+ int err;
+
+ if (skb_is_gso(skb))
+ return -EINVAL;
+
+ err = skb_checksum_help(skb);
+ skb->transport_header = old_transport;
+ return err;
+ }
+
return 0;
}

@@ -198,7 +302,8 @@ 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);
+ return __virtio_net_hdr_to_skb(skb, hdr, little_endian,
+ hdr->gso_type, true);
}

/* This function must be called after virtio_net_hdr_from_skb(). */
@@ -293,6 +398,7 @@ virtio_net_hdr_tnl_to_skb(struct sk_buff *skb,
unsigned int inner_nh, outer_th, inner_th;
unsigned int inner_l3min, outer_l3min;
u8 gso_inner_type, gso_tunnel_type;
+ __be16 inner_proto;
bool outer_isv6, inner_isv6;
int ret;

@@ -334,7 +440,7 @@ 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);
+ hdr->gso_type & ~gso_tunnel_type, false);
if (ret)
return ret;

@@ -352,8 +458,8 @@ virtio_net_hdr_tnl_to_skb(struct sk_buff *skb,
return -EINVAL;
}

- skb_set_inner_protocol(skb, inner_isv6 ? htons(ETH_P_IPV6) :
- htons(ETH_P_IP));
+ inner_proto = inner_isv6 ? htons(ETH_P_IPV6) : htons(ETH_P_IP);
+ skb_set_inner_protocol(skb, inner_proto);
if (hdr->flags & VIRTIO_NET_HDR_F_UDP_TUNNEL_CSUM) {
if (!tnl_csum_negotiated)
return -EINVAL;
@@ -368,7 +474,16 @@ 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;
+
+ if (virtio_net_csum_partial_ok_at_offset(skb, gso_inner_type, inner_nh,
+ inner_proto))
+ return 0;
+
+ if (skb_is_gso(skb))
+ return -EINVAL;
+
+ ret = skb_checksum_help(skb);
+ return ret ?: 1;
}

/* Checksum-related fields validation for the driver */
--
MST