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
next prev 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