From: "Michael S. Tsirkin" <mst@redhat.com>
To: Willem de Bruijn <willemdebruijn.kernel@gmail.com>
Cc: linux-kernel@vger.kernel.org, "Jason Wang" <jasowangio@gmail.com>,
"Eugenio Pérez" <eperezma@redhat.com>,
"Xuan Zhuo" <xuanzhuo@linux.alibaba.com>,
"Andrew Lunn" <andrew+netdev@lunn.ch>,
"David S. Miller" <davem@davemloft.net>,
"Eric Dumazet" <edumazet@kernel.org>,
"Jakub Kicinski" <kuba@kernel.org>,
"Paolo Abeni" <pabeni@redhat.com>,
netdev@vger.kernel.org, virtualization@lists.linux.dev,
"Max Krasnyansky" <maxk@qualcomm.com>,
"Rusty Russell" <rusty@rustcorp.com.au>
Subject: Re: [PATCH net-next 3/4] net: resolve uncommon partial checksums at ingress
Date: Tue, 13 Oct 2026 16:15:06 -0400 [thread overview]
Message-ID: <20261013145937-mutt-send-email-mst@kernel.org> (raw)
In-Reply-To: <CAF=yD-KdTn2rHyp_j8ORr9F5hpm=TzaPRan5+gvLiYBd9HP-sg@mail.gmail.com>
On Tue, Oct 13, 2026 at 09:03:04AM -0600, Willem de Bruijn wrote:
> On Tue, Oct 13, 2026 at 5:06 AM Michael S. Tsirkin <mst@redhat.com> wrote:
> >
> > On Tue, Oct 13, 2026 at 06:58:41AM -0400, Michael S. Tsirkin wrote:
> > > 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@gmail.com>
> > > Signed-off-by: Michael S. Tsirkin <mst@redhat.com>
> > > Assisted-by: LLM
> >
> >
> > I forgot reported-by tags:
> >
> > Reported-by: Paulos Yibelo <habte.yibelo@gmail.com>
> > Reported-by: Chris Arges <carges@cloudflare.com>
> >
> >
> > > ---
> > > 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);
>
> Skip this rare case of extension headers?
but then it will be dropped if it's gso. so we can't.
> > > + if (ip_proto < 0 || flags & IP6_FH_F_FRAG)
> > > + return false;
> > > + }
> > > + } else {
> > > + return false;
> > > + }
> > > +
>
> > > 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);
>
> Instead of this variable csum_partial_ok and this branch, could
> virtio_net_csum_partial_ok just call skb_checksum_help directly and
> drop VIRTIO_NET_HDR_F_NEEDS_CSUM?
no, hdr is const. and I think I like it that way. we could
copy flags I guess - I'll play with it a bit.
> > > + 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);
>
> Would be great if we can avoid the extra arg and having to update the callers.
There are 2 callers why is this a concern?
But yes the extra bool is kinda ugly. i'll look into refactoring
this but i do not think the diff will be smaller - likely larger.
> > > }
> > >
> > > /* 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);
>
> Can drop
It is here because inner_proto is used below, seems silly to re-compute
when we have it.
> > > 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;
>
> This is a risk the commit message will have to call out.
I tried but good point, not enough. Will do.
> I don't see a simple way around it. We discussed segmenting here too,
> but that requires intrusive changes to all callers too.
>
> Alternatively,
>
> 1. call segmentation, but only to see it remove the SKB_GSO_DODGY and
> thus verify that the TSO/USO packet is valid. Still drop otherwise.
> This also does not add overhead, because it would be called later
> anyway.
Yes, I implemented this originally. It's an even more messy code but the
big issue is that it's not clear it is worth it. So we get gso with
a bad checksum. Currently it goes out on the wire corrupted. Ideally
we would do exactly that, but that would mean much more work that
SKB_GSO_DODGY play. Conversely we could just ignore the
checksum hints. Then the packet would go on wire because we
"fixed" it but it just has a chance to cause even more problems.
So... shrug. I opted for the simpler way.
So... shrug. I opted for the simpler way. If a real workload shows
up, the counters in the last patch will tell us and we can revisit.
I will add this to the cover letter I feel, not commit log material,
right?
> 2. a failsafe off-by-default sysctl + static_branch that skips the
> drop when set. Messy, but so is the situation we're dealing with.
good idea, I'll add that.
--
MST
next prev parent reply other threads:[~2026-10-13 20:15 UTC|newest]
Thread overview: 27+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-13 10:58 [PATCH net-next 0/4] more virtio-net csum hardening Michael S. Tsirkin
2026-10-13 10:58 ` [PATCH net-next 1/4] net: add ip4_parse() Michael S. Tsirkin
2026-10-13 11:15 ` sashiko-bot
2026-10-13 10:58 ` [PATCH net-next 2/4] net: use ip4_parse() in receive validation Michael S. Tsirkin
2026-10-13 11:15 ` sashiko-bot
2026-10-13 10:58 ` [PATCH net-next 3/4] net: resolve uncommon partial checksums at ingress Michael S. Tsirkin
2026-10-13 11:06 ` Michael S. Tsirkin
2026-10-13 15:03 ` Willem de Bruijn
2026-10-13 18:35 ` Michael S. Tsirkin
2026-10-13 20:32 ` Willem de Bruijn
2026-10-13 20:38 ` Michael S. Tsirkin
2026-10-13 20:45 ` Willem de Bruijn
2026-10-13 20:51 ` Michael S. Tsirkin
2026-10-13 20:15 ` Michael S. Tsirkin [this message]
2026-10-13 11:15 ` sashiko-bot
2026-10-13 10:58 ` [PATCH net-next 4/4] net: account for ingress-resolved virtio net hdr state Michael S. Tsirkin
2026-10-13 11:15 ` sashiko-bot
2026-10-13 17:45 ` Björn Töpel
2026-10-13 18:12 ` Michael S. Tsirkin
2026-10-13 11:05 ` [PATCH net-next 0/4] more virtio-net csum hardening Michael S. Tsirkin
2026-10-13 18:51 ` Chris Arges
2026-10-13 14:41 ` Willem de Bruijn
2026-10-13 18:41 ` Michael S. Tsirkin
2026-10-13 20:29 ` Willem de Bruijn
2026-10-13 20:36 ` Michael S. Tsirkin
2026-10-13 20:54 ` Willem de Bruijn
2026-10-13 20:56 ` Michael S. Tsirkin
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=20261013145937-mutt-send-email-mst@kernel.org \
--to=mst@redhat.com \
--cc=andrew+netdev@lunn.ch \
--cc=davem@davemloft.net \
--cc=edumazet@kernel.org \
--cc=eperezma@redhat.com \
--cc=jasowangio@gmail.com \
--cc=kuba@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=maxk@qualcomm.com \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=rusty@rustcorp.com.au \
--cc=virtualization@lists.linux.dev \
--cc=willemdebruijn.kernel@gmail.com \
--cc=xuanzhuo@linux.alibaba.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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.