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
next prev parent reply other threads:[~2026-10-20 12:04 UTC|newest]
Thread overview: 34+ 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-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 20:56 ` 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
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 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.