Skip to contentSkip to conversation

more virtio-net csum hardening

RFC PATCH In review Michael S. Tsirkin 4 participants 14 messages Oct 29 – Nov 4

Subsystems (8)
lore
Share & tools
lore

Michael S. Tsirkin

author maintainer
#0 lore raw

v7 cover more virtio-net csum hardening

Sending this one as an RFC: two issues are still open.

I'm going to be working on other things in the next couple of weeks, but I thought I'd post this in case anyone wants to take this up.

Issues:

First, the general issue is not addressed 100%: if a tunnel inner packet happens to have a valid header at inner_nh_offset, it will pass all checks, but the transport header can in fact overlap e.g. the real TCP header and the checksum will overwrite that. I do not have a good idea how to address this without either a heavy protocol-specific anchor derivation or dropping well-formed offload.

Second, completing the checksum at input breaks nf/BPF programs that might assume that the packet carries CHECKSUM_PARTIAL,
then tweak the payload of a packet in flight and rely on the checksum being fixed up later.

The sysctl added in the last patch could serve as a work-around, but it is a very blunt tool.

The issues were mostly found by code inspection, but all reproduced by me in a VM.

------

As reported first by Paulos Yibelo <[email protected]> (after which others, credited below, found more ways the bug can affect users):

allowing untrusted hardware to specify csum start/offset for CHECKSUM_PARTIAL was not a good idea, because, if it edits the packet outside the actual checksum, and depending on the setup, the networking stack could be looking at the packet both before and after the edit, and seeing different results.

Previously, commit 6785011f8b16 ("ipv4: validate checksum_start before completing checksum") validated all known instances where this causes the most grief, but the real fix is to make sure that either only actual checksum fields are ever edited,
or the checksum is completed before the net stack looks at it, so it is always consistent.

This basically goes back to
what we did before f43798c27684 ("tun: Allow GSO using virtio_net_hdr"), except in a narrow list of specified known good cases.

Summary of the patchset:

These are probabably net material (and seem to be too big for stable, or I would recommend backporting there, too):

net: account for ingress-resolved virtio net hdr state
- accounting, not the fix itself, dependency of the fix
um: vector: trim skbs before verifying the header
- fixes the um receive path: without it the checksum would be completed over the unreceived tail of the buffer
virtio_net: parse tunnel csum_start little-endian
- bugfix, a dependency of validating the header
net: resolve uncommon partial checksums at ingress
- fix itself
net: add a sysctl to opt out of virtio net hdr checksum validation
- reducing risk of the fix by adding a sysctl to disable it

These are cleanups, can be deferred to net-next, too:

net: add ip4_parse()

Fixes f43798c27684 ("tun: Allow GSO using virtio_net_hdr")

Changes since v1:
- reorder patches to fix bisectability, addressing a comment by Björn Töpel.
- code refactoring, different patch splitup,
a sysctl knob to disable as suggested by Willem de Bruijn.
- fix a bunch of minor issues spotted by LLMs

Changes since v2:

virtio_net: parse tunnel csum_start little-endian
- new patch. correctly parsing the header is a dependency of validating it, otherwise i do not know what to validate

net: account for ingress-resolved virtio net hdr state
- make rx_csum_sw counter 64 bit so it won't wrap (spotted by LLM review)

net: resolve uncommon partial checksums at ingress
- tunnel fixes to bugs noticed by Willem de Bruijn and Chris Arges
- restore the csum_offset checks as suggested by Willem

net: add a sysctl to opt out of virtio net hdr checksum validation
- the knob is no longer writable by root inside a user
namespace: changing it now requires capable(CAP_NET_ADMIN) suggested by Willem de Bruijn

net: add ip4_parse()
- comment converted to kernel-doc format (Willem de Bruijn)

um: vector: trim skbs before verifying the header
- new patch, found by code review

Changes since v3:

net: account for ingress-resolved virtio net hdr state
- um: vector: honor virtio net hdr conversion failures folded in (Björn Töpel)
- count on tap and virtio_net, too (Willem de Bruijn)

net: resolve uncommon partial checksums at ingress
- walk the frame on loopback, too: loopback_xmit() runs eth_type_trans() on it (Willem de Bruijn)
- return the mismatch handling directly, GSO is implied in the tunnel path so drop there outright, and fold the helper into its one remaining caller (Willem de Bruijn)
- read the IP version through struct iphdr (Willem de Bruijn)
- document the ipv6_find_hdr() flags choice

net: add a sysctl to opt out of virtio net hdr checksum validation
- renamed to virtio_net_hdr_validate, in case more validation is gated by the same knob later (Willem de Bruijn)
- point .data at the value like the neighboring entries, the per-netns registration rebases it, and the handler reads the value directly - no ctl_table copy (Björn Töpel, Willem de Bruijn)
- check the opt-out before parsing: with the knob off, packets the parser cannot handle were still dropped (Willem de Bruijn)
- include virtio_net.h in net/core/dev.c so the static key definition sees its declaration (build warning reported by Jakub Kicinski)

net: add ip4_parse()
- add the missing kernel-doc Return: section
(Jakub Kicinski)

Changes since v4:

net: account for ingress-resolved virtio net hdr state
- count on tun only: dropped the tap, macvtap, ipvtap and virtio_net counters (Willem de Bruijn)
- the driver side branches are in their final form here now, moved from "net: resolve uncommon partial checksums at ingress" (Willem de Bruijn)

net: resolve uncommon partial checksums at ingress
- no longer touches the drivers (Willem de Bruijn)
- a packet too small to be GSO: ignore the tunnel GSO request, same as plain GSO ones, and complete wild metadata in place (sashiko)
- document the zero copy interaction (sashiko)
- honor a preset skb->protocol on ethernet: the stack uses it, except on loopback which re-parses the frame (sashiko)

net: add a sysctl to opt out of virtio net hdr checksum validation
- document the netns lifetime requirement (sashiko)

net: add ip4_parse()
- pull the whole header, options included, so that the transport header offset is within the linear data (Willem de Bruijn)

Changes since v5:

net: resolve uncommon partial checksums at ingress
- read both skb->protocol and the frame ethertype: do not validate when they disagree, instead of preferring the preset protocol; same check after the IP version inference where there is no L2 header (Willem de Bruijn)
- pull the IPv4 options in the helper as introduced, with the transport header offset computed before the pull (Willem de Bruijn)
- fold the trivial protocol calculation cleanup, formerly a separate patch, in (Willem de Bruijn)

net: add ip4_parse()
- the options pull moved to the helper introduction in "net: resolve uncommon partial checksums at ingress"

Changes since v6:

all patches
- rebased on the current net tree: v6 no longer applied (Jakub Kicinski)

net: resolve uncommon partial checksums at ingress
- validate header before the tunnel metadata is installed (Willem de Bruijn)
- some cosmetic tweaks

Tested: full suite on the final tree, tunnel included. um part compiled only.

Michael S. Tsirkin (5):
net: account for ingress-resolved virtio net hdr state
um: vector: trim skbs before verifying the header
virtio_net: parse tunnel csum_start little-endian
net: resolve uncommon partial checksums at ingress
net: add a sysctl to opt out of virtio net hdr checksum validation

 Documentation/admin-guide/sysctl/net.rst |  19 ++
 arch/um/drivers/vector_kern.c            |   6 +-
 arch/um/drivers/vector_transports.c      |   4 +-
 drivers/net/tap.c                        |   4 +-
 drivers/net/tun.c                        |  41 ++-
 drivers/net/virtio_net.c                 |   2 +-
 include/linux/virtio_net.h               | 319 ++++++++++++++++++++++-
 include/net/netns/core.h                 |   1 +
 net/core/dev.c                           |   7 +
 net/core/net_namespace.c                 |   1 +
 net/core/sysctl_net_core.c               |  50 ++++
 net/packet/af_packet.c                   |   4 +-
 12 files changed, 441 insertions(+), 17 deletions(-)
Signature · 1 line
MST
author maintainer
#1 lore raw

RFC PATCH v7 1/5 net: account for ingress-resolved virtio net hdr state

The following patch will add ingress resolution of uncommon virtio net hdr checksum metadata (software checksum completion at ingress) which adds performance overhead to the packet processing.

We should make it visible, so a hypothetical workload relying on uncommon metadata can be distinguished from none at all. This way issues in the parsing will be reported and not quietly pay the host-CPU checksum overhead instead of offload.

Flip every call to virtio_net_hdr_to_skb to test for negative errno only. The um vector raw transport even ignored the return value
altogether: rejected packets went on into the stack with their metadata unvalidated, so check it there as well.

Have tun count these events per device as rx_csum_sw, exposed via ethtool -S.

---

Notes (changelog):
v4->v5:
count on tun only: dropped the tap, macvtap, ipvtap and virtio_net counters (Willem de Bruijn)

 arch/um/drivers/vector_transports.c |  4 ++-
 drivers/net/tap.c                   |  4 +--
 drivers/net/tun.c                   | 41 +++++++++++++++++++++++++++--
 drivers/net/virtio_net.c            |  2 +-
 net/packet/af_packet.c              |  4 +--
 5 files changed, 47 insertions(+), 8 deletions(-)
arch/um/drivers/vector_transports.c +3 -1
index e1fb2a76fdff..8b7548458663 100644
@@ -210,7 +210,9 @@ static int raw_verify_header(
 		return 1;
 
 	skb_set_network_header(skb, ETH_HLEN);
-	virtio_net_hdr_to_skb(skb, vheader, virtio_legacy_is_little_endian());
+	if (virtio_net_hdr_to_skb(skb, vheader,
+			virtio_legacy_is_little_endian()) < 0)
+		return -1;
 	return 0;
 }
 
drivers/net/tap.c +2 -2
index 2f19fb07bb2d..101994870bac 100644
@@ -650,7 +650,7 @@ static ssize_t tap_get_user(struct tap_queue *q, void *msg_control,
 
 	if (vnet_hdr_len) {
 		err = tun_vnet_hdr_to_skb(q->flags, skb, &vnet_hdr);
-		if (err) {
+		if (err < 0) {
 			rcu_read_unlock();
 			drop_reason = SKB_DROP_REASON_DEV_HDR;
 			goto err_kfree;
@@ -1090,7 +1090,7 @@ static int tap_get_user_xdp(struct tap_queue *q, struct xdp_buff *xdp)
 
 	if (vnet_hdr_len) {
 		err = tun_vnet_hdr_to_skb(q->flags, skb, gso);
-		if (err) {
+		if (err < 0) {
 			rcu_read_unlock();
 			goto err_kfree;
 		}
drivers/net/tun.c +39 -2

73 lines not on this page · raw message

drivers/net/virtio_net.c +1 -1
index dd2c41ef6fbe..dd3f39516f90 100644
@@ -2535,7 +2535,7 @@ static void virtnet_receive_done(struct virtnet_info *vi, struct receive_queue *
 	skb_set_network_header(skb, ETH_HLEN);
 	if (virtio_net_hdr_tnl_to_skb(skb, &hdr->tnl_hdr, vi->rx_tnl,
 				      vi->rx_tnl_csum,
-				      virtio_is_little_endian(vi->vdev))) {
+				      virtio_is_little_endian(vi->vdev)) < 0) {
 		net_warn_ratelimited("%s: bad gso: type: %x, size: %u, flags %x tunnel %d tnl csum %d\n",
 				     dev->name, hdr->hdr.gso_type,
 				     hdr->hdr.gso_size, hdr->hdr.flags,
net/packet/af_packet.c +2 -2
index 823e0911c910..c0cd36be53d6 100644
@@ -2921,7 +2921,7 @@ static int tpacket_snd(struct packet_sock *po, struct msghdr *msg)
 		}
 
 		if (has_vnet_hdr) {
-			if (virtio_net_hdr_to_skb(skb, &vnet_hdr, vio_le())) {
+			if (virtio_net_hdr_to_skb(skb, &vnet_hdr, vio_le()) < 0) {
 				tp_len = -EINVAL;
 				goto tpacket_error;
 			}
@@ -3144,7 +3144,7 @@ static int packet_snd(struct socket *sock, struct msghdr *msg, size_t len)
 
 	if (vnet_hdr_sz) {
 		err = virtio_net_hdr_to_skb(skb, &vnet_hdr, vio_le());
-		if (err)
+		if (err < 0)
 			goto out_free;
 		len += vnet_hdr_sz;
 	}
Signature · 1 line
MST
#2 lore raw

Re: [PATCH RFC v7 1/5] net: account for ingress-resolved virtio net hdr state

Sashiko has reviewed this patch and found no issues.
Quoted 14 lines

The following patch will add ingress resolution of uncommon virtio net hdr checksum metadata (software checksum completion at ingress) which adds performance overhead to the packet processing.

We should make it visible, so a hypothetical workload relying on uncommon metadata can be distinguished from none at all. This way issues in the parsing will be reported and not quietly pay the host-CPU checksum overhead instead of offload.

Flip every call to virtio_net_hdr_to_skb to test for negative errno only. The um vector raw transport even ignored the return value
altogether: rejected packets went on into the stack with their metadata unvalidated, so check it there as well.

Quoted 5 lines

Have tun count these events per device as rx_csum_sw, exposed via ethtool -S.

Sashiko has reviewed this patch and found no issues.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/305bcadb19f4a539a318c6b87c829fbe43d0ffdb.1793276116.git.mst@redhat.com?part=1
author maintainer
#3 lore raw

RFC PATCH v7 2/5 um: vector: trim skbs before verifying the header

In reply to Michael S. Tsirkin

The vector receive path over-allocates the skb and only trims it to the received length after the transport header is verified.

A follow up patch will complete the packet checksum in software within virtio_net_hdr_to_skb so we need the packet trimmed (otherwise the checksum would be computed incorrectly over the unreceived tail of the buffer).

Trim first, so the import sees the packet as it was received.

---
 arch/um/drivers/vector_kern.c | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)
arch/um/drivers/vector_kern.c +3 -3
index e902e78c5ff6..828bd61995b8 100644
@@ -873,6 +873,7 @@ static int vector_legacy_rx(struct vector_private *vp)
 
 	if (skb != NULL) {
 		if (pkt_len > vp->header_size) {
+			pskb_trim(skb, pkt_len - vp->rx_header_size);
 			if (vp->header_size > 0) {
 				header_check = vp->verify_header(
 					vp->header_rxbuffer, skb, vp);
@@ -887,7 +888,6 @@ static int vector_legacy_rx(struct vector_private *vp)
 					skb->ip_summed = CHECKSUM_UNNECESSARY;
 				}
 			}
-			pskb_trim(skb, pkt_len - vp->rx_header_size);
 			skb->protocol = eth_type_trans(skb, skb->dev);
 			vp->dev->stats.rx_bytes += skb->len;
 			vp->dev->stats.rx_packets++;
@@ -989,6 +989,8 @@ static int vector_mmsg_rx(struct vector_private *vp, int budget)
 	for (i = 0; i < packet_count; i++) {
 		skb = (*skbuff_vector);
 		if (mmsg_vector->msg_len > vp->header_size) {
+			pskb_trim(skb,
+				mmsg_vector->msg_len - vp->rx_header_size);
 			if (vp->header_size > 0) {
 				header_check = vp->verify_header(
 					mmsg_vector->msg_hdr.msg_iov->iov_base,
@@ -1013,8 +1015,6 @@ static int vector_mmsg_rx(struct vector_private *vp, int budget)
 					skb->ip_summed = CHECKSUM_UNNECESSARY;
 				}
 			}
-			pskb_trim(skb,
-				mmsg_vector->msg_len - vp->rx_header_size);
 			skb->protocol = eth_type_trans(skb, skb->dev);
 			/*
 			 * We do not need to lock on updating stats here
Signature · 1 line
MST
#4 lore raw

Re: [PATCH RFC v7 2/5] um: vector: trim skbs before verifying the header

Sashiko has reviewed this patch and found no issues.
Quoted 8 lines

The vector receive path over-allocates the skb and only trims it to the received length after the transport header is verified.

A follow up patch will complete the packet checksum in software within virtio_net_hdr_to_skb so we need the packet trimmed (otherwise the checksum would be computed incorrectly over the unreceived tail of the buffer).

Quoted 4 lines

Trim first, so the import sees the packet as it was received.

Sashiko has reviewed this patch and found no issues.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/305bcadb19f4a539a318c6b87c829fbe43d0ffdb.1793276116.git.mst@redhat.com?part=2
author maintainer
#5 lore raw

RFC PATCH v7 3/5 virtio_net: parse tunnel csum_start little-endian

In reply to Michael S. Tsirkin

All tunnel related virtio features are > 32, so none of this functionality can be enabled on legacy mixed-endian device.

Accordingly, all tunnel header fields, csum_start included, are little-endian.

The nested virtio net header import parses csum_start that way, but the inner transport offset is parsed with the endianness specified by the interface.

If tun is misconfigured to legacy big-endian mode while the tunnel features are enabled, the two parses of this field disagree, and the value stored in skb->inner_transport_header - which is only bounds-checked from below - can point past the end of the packet, where GSO segmentation later dereferences it.

To fix, parse the field little-endian in both places.

Fixes a2fb4bc4e2a6 ("net: implement virtio helpers to handle UDP GSO tunneling.")
Cc Paolo Abeni <[email protected]>
---
 include/linux/virtio_net.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
include/linux/virtio_net.h +1 -1
index d6466f96cdd0..013c6c7e2dea 100644
@@ -324,7 +324,7 @@ virtio_net_hdr_tnl_to_skb(struct sk_buff *skb,
 	inner_l3min = virtio_l3min(inner_isv6);
 	outer_l3min = ETH_HLEN + virtio_l3min(outer_isv6);
 
-	inner_th = __virtio16_to_cpu(little_endian, hdr->csum_start);
+	inner_th = __virtio16_to_cpu(true, hdr->csum_start);
 	inner_nh = le16_to_cpu(vhdr->inner_nh_offset);
 	outer_th = le16_to_cpu(vhdr->outer_th_offset);
 	if (outer_th < outer_l3min ||
Signature · 1 line
MST
#6 lore raw

Re: [PATCH RFC v7 3/5] virtio_net: parse tunnel csum_start little-endian

Sashiko has reviewed this patch and found no issues.
Quoted 16 lines

All tunnel related virtio features are > 32, so none of this functionality can be enabled on legacy mixed-endian device.

Accordingly, all tunnel header fields, csum_start included, are little-endian.

The nested virtio net header import parses csum_start that way, but the inner transport offset is parsed with the endianness specified by the interface.

If tun is misconfigured to legacy big-endian mode while the tunnel features are enabled, the two parses of this field disagree, and the value stored in skb->inner_transport_header - which is only bounds-checked from below - can point past the end of the packet, where GSO segmentation later dereferences it.

Quoted 5 lines

To fix, parse the field little-endian in both places.

Fixes a2fb4bc4e2a6 ("net: implement virtio helpers to handle UDP GSO tunneling.")
Cc Paolo Abeni <[email protected]>

Sashiko has reviewed this patch and found no issues.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/305bcadb19f4a539a318c6b87c829fbe43d0ffdb.1793276116.git.mst@redhat.com?part=3
author maintainer
#7 lore raw

RFC PATCH v7 4/5 net: resolve uncommon partial checksums at ingress

In reply to Michael S. Tsirkin

As reported first by Paulos Yibelo <[email protected]> (after which others, credited below, found more ways the bug can affect users) 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 shouldn't happen.

There's some risk here:
- non-gso packets which can't be parsed get checksum calculated in software, adding performance overhead
- gso packets which can't be parsed get dropped

Note that for zero copy, userspace can change the data after the checksum has been calculated. This is fundamental to zero copy by design, and the only outcome is a bad checksum and a packet drop.

To mitigate the risks, the following patch will add a sysctl knob to bypass the resolution. This is reasonable if the setup is such that there is no protocol processing at all.

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

Packets without VIRTIO_NET_HDR_F_NEEDS_CSUM are unaffected.

Note a fundamental limitation of this approach: an nf/BPF program that tweaks the payload of a packet in flight may rely on the checksum being fixed up later, assuming the packet carries CHECKSUM_PARTIAL.

For metadata that fails validation the checksum
is now completed at input, so such edits leave a stale checksum and the packet is dropped at the receiver. Protocols whose transport header we cannot parse (anything but TCP and UDP) are affected even when the metadata is self-consistent.

The sysctl added in the next patch is the escape hatch for these setups, but it is a very blunt tool.

Fixes f43798c27684 ("tun: Allow GSO using virtio_net_hdr")

---

Notes (changelog):
v4->v5:
caller changes moved to "account for ingress-resolved virtio net hdr state" (Willem de Bruijn)
a packet too small to be GSO: ignore the tunnel GSO request, complete wild metadata in place (sashiko) document the zero copy interaction (sashiko) honor a preset skb->protocol on ethernet: the stack uses it, except on loopback which re-parses the frame (sashiko)
v5->v6:
read both skb->protocol and the frame ethertype: do not validate when they disagree, instead of preferring the preset protocol; same check after the IP version inference where there is no L2 header (Willem de Bruijn) pull the IPv4 options in the helper as introduced, with the transport header offset computed before the pull (Willem de Bruijn)
fold the trivial protocol calculation cleanup, formerly a separate patch, in (Willem de Bruijn)

 include/linux/virtio_net.h | 315 ++++++++++++++++++++++++++++++++++++-
 1 file changed, 311 insertions(+), 4 deletions(-)
include/linux/virtio_net.h +311 -4
index 013c6c7e2dea..54048759cc6e 100644
@@ -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,6 +52,216 @@ 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.  The whole header,
+ * options included, is guaranteed linear on return.
+ */
+static inline struct iphdr *virtio_net_ip4_parse(struct sk_buff *skb,
+						  unsigned int nhoff,
+						  unsigned int *thoff)
+{
+	unsigned int ihl;
+	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;
+
+	ihl = iph->ihl * sizeof(u32);
+	*thoff = nhoff + ihl;
+	/* Pull the options as well: the transport header offset
+	 * returned above must be within the linear data.  iph goes
+	 * stale across the pull.
+	 */
+	if (!pskb_may_pull(skb, nhoff + ihl))
+		return NULL;
+
+	return (struct iphdr *)(skb->data + nhoff);
+}
+
+/* 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 should always be set here already,
+	 * but let's not crash when it is not.
+	 */
+	if (WARN_ON_ONCE(!skb->dev))
+		return false;
+
+	if (skb->dev->type == ARPHRD_ETHER ||
+	    skb->dev->type == ARPHRD_LOOPBACK) {
+		__be16 h_proto;
+
+		if (!pskb_may_pull(skb, ETH_HLEN))
+			return false;
+		h_proto = eth_hdr(skb)->h_proto;
+		/* skb->protocol is a sender claim, and parts of the
+		 * stack parse the packet per it: when it disagrees with
+		 * the frame we do not know which view the packet will
+		 * be parsed as, so do not validate.
+		 */
+		if (skb->protocol && skb->protocol != h_proto)
+			return false;
+		*nhoff = ETH_HLEN;
+		*proto = h_proto;
+		if (eth_type_vlan(*proto)) {
+			*proto = __vlan_get_protocol(skb, *proto, nhoff);
+			if (!*proto)
+				return false;
+		}
+	} else {
+		const struct iphdr *iph;
+
+		*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;
+		/* No L2 header to cross-check with: take the protocol
+		 * from the IP version at the network header.
+		 */
+		if (!pskb_may_pull(skb, *nhoff + 1))
+			return false;
+		iph = (const struct iphdr *)(skb->data + *nhoff);
+		if (iph->version == 4)
+			*proto = htons(ETH_P_IP);
+		else if (iph->version == 6)
+			*proto = htons(ETH_P_IPV6);
+		else
+			return false;
+		if (skb->protocol && skb->protocol != *proto)
+			return false;
+	}
+	return true;
+}
+
+/* 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;
+			/* Find the transport header behind any
+			 * extension headers, AH included.
+			 */
+			*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;
+}
+
+/* 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 (!virtio_net_l4_start(skb, nhoff, proto, &thoff, &ip_proto))
+		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)
+{
+	int nhoff;
+	__be16 proto;
+
+	if (!virtio_net_l3_start(skb, &nhoff, &proto))
+		return false;
+
+	return virtio_net_csum_partial_ok_at_offset(skb, hdr_gso_type, nhoff,
+						   proto);
+}
+
+/*
+ * 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 +408,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 (likely(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);
 }
 
 /* This function must be called after virtio_net_hdr_from_skb(). */
@@ -282,6 +520,35 @@ static inline unsigned int virtio_l3min(bool is_ipv6)
 	return is_ipv6 ? sizeof(struct ipv6hdr) : sizeof(struct iphdr);
 }
 
+/* Do the tunnel offsets match the outer headers in the frame?
+ * The L3 header the tunnel claims must be the one the frame
+ * carries, with UDP following it at @outer_th.
+ */
+static inline bool
+virtio_net_tnl_offsets_ok(struct sk_buff *skb, bool outer_isv6,
+			  unsigned int outer_th)
+{
+	int nhoff;
+	__be16 proto;
+	unsigned int thoff;
+	int ip_proto;
+
+	if (!virtio_net_l3_start(skb, &nhoff, &proto))
+		return false;
+	if (outer_isv6 != (proto == htons(ETH_P_IPV6)))
+		return false;
+	if (!virtio_net_l4_start(skb, nhoff, proto, &thoff, &ip_proto))
+		return false;
+
+	return ip_proto == IPPROTO_UDP && outer_th == thoff;
+}
+
+/*
+ * Return value:
+ * 0 - success
+ * >0 - checksum was completed in software (slow path)
+ * <0 - failure
+ */
 static inline int
 virtio_net_hdr_tnl_to_skb(struct sk_buff *skb,
 			  const struct virtio_net_hdr_v1_hash_tunnel *vhdr,
@@ -290,9 +557,11 @@ virtio_net_hdr_tnl_to_skb(struct sk_buff *skb,
 			  bool little_endian)
 {
 	const struct virtio_net_hdr *hdr = (const struct virtio_net_hdr *)vhdr;
+	typeof(skb->transport_header) old_transport = skb->transport_header;
 	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;
 
@@ -335,9 +604,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
@@ -352,9 +624,29 @@ 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);
+
+	/*
+	 * We know VIRTIO_NET_HDR_F_NEEDS_CSUM was set so __virtio_net_hdr_to_skb
+	 * set CHECKSUM_PARTIAL.
+	 */
+	if (unlikely(!virtio_net_csum_partial_ok_at_offset(skb, gso_inner_type,
+							  inner_nh,
+							  inner_proto))) {
+		/* __virtio_net_hdr_to_skb set the transport header to
+		 * csum_start, restore it to a known good value.
+		 */
+		skb->transport_header = old_transport;
+
+		return __virtio_net_resolve_csum_partial_mismatch(skb);
+	}
+
+	skb_set_inner_protocol(skb, inner_proto);
 	if (hdr->flags & VIRTIO_NET_HDR_F_UDP_TUNNEL_CSUM) {
+		/* Checked here, after the validation: a packet completed
+		 * in software never uses the feature, so an unnegotiated
+		 * request only matters for metadata good enough to act on.
+		 */
 		if (!tnl_csum_negotiated)
 			return -EINVAL;
 
@@ -368,6 +660,21 @@ 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;
+
+	if (!skb_is_gso(skb)) {
+		/* Too small to segment: complete the checksum in place
+		 * instead of dropping - senders do tag small packets
+		 * with GSO headers, e.g. packetdrill sets gso_type and
+		 * gso_size with --mss, whatever the packet size.
+		 * The outer UDP checksum is not calculated, but a zero
+		 * one is fine: UDP over IPv4 allows a disabled
+		 * checksum, and so do tunnels over IPv6 where the
+		 * receiver opts in (RFC 6935, RFC 6936).
+		 */
+		skb_shinfo(skb)->gso_type &= ~(SKB_GSO_UDP_TUNNEL_CSUM |
+					       SKB_GSO_UDP_TUNNEL);
+	}
+
 	return 0;
 }
 
Signature · 1 line
MST
#8 lore raw

Re: [PATCH RFC v7 4/5] net: resolve uncommon partial checksums at ingress

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [High] net: skb->protocol mismatch in virtio_net_l3_start() when unset - [High] net: uncompleted csum in…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] net: skb->protocol mismatch in virtio_net_l3_start() when unset
- [High] net: uncompleted csum in virtio_net_hdr_tnl_to_skb() for non-GSO --

commit e848d2c54d731e54131a7b7019dc30d4e0367ad9
Author: Michael S. Tsirkin <[email protected]>

net: resolve uncommon partial checksums at ingress

This patch validates csum_start and csum_offset in virtio_net_hdr_to_skb() and virtio_net_hdr_tnl_to_skb() against parsed TCP and UDP headers, completing partial checksums in software for non-GSO packets when validation fails and dropping unrecognized GSO packets.

Quoted 4 lines
include/linux/virtio_net.h +0 -0
index 013c6c7e2dea0..54048759cc6ec 100644

[ ... ]

Quoted 46 lines
include/linux/virtio_net.h +46 -0
+static inline bool
+virtio_net_l3_start(struct sk_buff *skb, int *nhoff, __be16 *proto)
+{
+	/* skb->dev should always be set here already,
+	 * but let's not crash when it is not.
+	 */
+	if (WARN_ON_ONCE(!skb->dev))
+		return false;
+
+	if (skb->dev->type == ARPHRD_ETHER ||
+	    skb->dev->type == ARPHRD_LOOPBACK) {
+		__be16 h_proto;
+
+		if (!pskb_may_pull(skb, ETH_HLEN))
+			return false;
+		h_proto = eth_hdr(skb)->h_proto;
+		/* skb->protocol is a sender claim, and parts of the
+		 * stack parse the packet per it: when it disagrees with
+		 * the frame we do not know which view the packet will
+		 * be parsed as, so do not validate.
+		 */
+		if (skb->protocol && skb->protocol != h_proto)
+			return false;
+		*nhoff = ETH_HLEN;
+		*proto = h_proto;
+		if (eth_type_vlan(*proto)) {
+			*proto = __vlan_get_protocol(skb, *proto, nhoff);
+			if (!*proto)
+				return false;
+		}
+	} else {
+		const struct iphdr *iph;
+
+		*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;
+		/* No L2 header to cross-check with: take the protocol
+		 * from the IP version at the network header.
+		 */
+		if (!pskb_may_pull(skb, *nhoff + 1))
+			return false;
+		iph = (const struct iphdr *)(skb->data + *nhoff);
+		if (iph->version == 4)

include/linux/virtio_net.h static inline int virtio_net_hdr_set_proto(struct sk_buff *skb, · line 149

Quoted 10 lines
include/linux/virtio_net.h +10 -0
+			*proto = htons(ETH_P_IP);
+		else if (iph->version == 6)
+			*proto = htons(ETH_P_IPV6);
+		else
+			return false;
+		if (skb->protocol && skb->protocol != *proto)
+			return false;
+	}
+	return true;
+}

[Severity: High]
Can virtio_net_l3_start() validate a packet as ETH_P_IPV6 when skb->protocol is unset (0), only for the caller to later set skb->protocol to ETH_P_IP or leave it as 0?

In net/packet/af_packet.c, both packet_snd() and tpacket_snd() invoke virtio_net_hdr_to_skb() before setting skb->protocol:

net/packet/af_packet.c:packet_snd() {
	...
	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);

	if (vnet_hdr_sz)
		virtio_net_hdr_set_proto(skb, &vnet_hdr);
	...
}

When a packet is sent with proto == 0 (or htons(ETH_P_ALL)) on a non-ARPHRD_ETHER, non-ARPHRD_LOOPBACK device (such as tun in IFF_TUN mode, sit, ipip, gre, or wg), skb->protocol is 0 when virtio_net_hdr_to_skb() runs.

If the L3 header has iph->version == 6, virtio_net_l3_start() sets *proto = htons(ETH_P_IPV6) and allows skb->protocol == 0. Then virtio_net_csum_partial_ok_at_offset() walks the IPv6 header and any IPv6 extension headers, and for VIRTIO_NET_HDR_GSO_UDP or VIRTIO_NET_HDR_GSO_UDP_L4 it only checks ip_proto == IPPROTO_UDP:

include/linux/virtio_net.h:virtio_net_csum_partial_ok_at_offset() {
	...
	case VIRTIO_NET_HDR_GSO_UDP:
	case VIRTIO_NET_HDR_GSO_UDP_L4:
		return ip_proto == IPPROTO_UDP;
	...
}

After virtio_net_hdr_to_skb() succeeds and retains CHECKSUM_PARTIAL at the IPv6 transport offset, packet_parse_headers() leaves skb->protocol as 0 on devices without header_ops->parse_protocol (or SOCK_DGRAM), and virtio_net_hdr_set_proto() unconditionally overwrites skb->protocol with ETH_P_IP:

include/linux/virtio_net.h:virtio_net_hdr_set_proto() {
	if (skb->protocol)
		return 0;

	switch (hdr->gso_type & ~VIRTIO_NET_HDR_GSO_ECN) {
	case VIRTIO_NET_HDR_GSO_TCPV4:
	case VIRTIO_NET_HDR_GSO_UDP:
	case VIRTIO_NET_HDR_GSO_UDP_L4:
		skb->protocol = cpu_to_be16(ETH_P_IP);
		break;
	...
}

The packet then enters packet_xmit() with skb->protocol == htons(ETH_P_IP) despite having been validated as IPv6, so inet_gso_segment() and flow dissection parse the IPv6 header as IPv4 while csum_start points to the IPv6 extension-header-derived offset. Similarly, when hdr->gso_type is VIRTIO_NET_HDR_GSO_NONE in packet_snd() / tpacket_snd(), or when pi.proto == 0 in tun_get_user() on an IFF_TUN device without IFF_NO_PI, skb->protocol remains 0 after validation.

Should virtio_net_l3_start() reject skb->protocol == 0 (or ensure skb->protocol is populated with the validated protocol)?

[ ... ]

Quoted 57 lines
include/linux/virtio_net.h +33 -3
@@ -335,9 +604,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
@@ -352,9 +624,29 @@ 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);
+
+	/*
+	 * We know VIRTIO_NET_HDR_F_NEEDS_CSUM was set so __virtio_net_hdr_to_skb
+	 * set CHECKSUM_PARTIAL.
+	 */
+	if (unlikely(!virtio_net_csum_partial_ok_at_offset(skb, gso_inner_type,
+							  inner_nh,
+							  inner_proto))) {
+		/* __virtio_net_hdr_to_skb set the transport header to
+		 * csum_start, restore it to a known good value.
+		 */
+		skb->transport_header = old_transport;
+
+		return __virtio_net_resolve_csum_partial_mismatch(skb);
+	}
+
+	skb_set_inner_protocol(skb, inner_proto);
 	if (hdr->flags & VIRTIO_NET_HDR_F_UDP_TUNNEL_CSUM) {
+		/* Checked here, after the validation: a packet completed
+		 * in software never uses the feature, so an unnegotiated
+		 * request only matters for metadata good enough to act on.
+		 */
 		if (!tnl_csum_negotiated)
 			return -EINVAL;
 
@@ -368,6 +660,21 @@ 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;
+
+	if (!skb_is_gso(skb)) {
+		/* Too small to segment: complete the checksum in place
+		 * instead of dropping - senders do tag small packets
+		 * with GSO headers, e.g. packetdrill sets gso_type and
+		 * gso_size with --mss, whatever the packet size.
+		 * The outer UDP checksum is not calculated, but a zero

include/linux/virtio_net.h virtio_net_hdr_tnl_to_skb(struct sk_buff *skb, · line 679

Quoted 10 lines
include/linux/virtio_net.h +8 -0
+		 * one is fine: UDP over IPv4 allows a disabled
+		 * checksum, and so do tunnels over IPv6 where the
+		 * receiver opts in (RFC 6935, RFC 6936).
+		 */
+		skb_shinfo(skb)->gso_type &= ~(SKB_GSO_UDP_TUNNEL_CSUM |
+					       SKB_GSO_UDP_TUNNEL);
+	}
+
 	return 0;
 }

[Severity: High]
In virtio_net_hdr_tnl_to_skb(), when a packet tagged with a tunnel GSO type is too small to segment (!skb_is_gso(skb)), the comment states "Too small to
segment: complete the checksum in place instead of dropping", but doesn't this block only clear the tunnel GSO flags while leaving CHECKSUM_PARTIAL uncompleted?

When __virtio_net_hdr_to_skb() sees skb->len - nh_off <= gso_size, it does not set shinfo->gso_size, so skb_is_gso(skb) is false. If virtio_net_csum_partial_ok_at_offset() succeeds, virtio_net_hdr_tnl_to_skb() reaches the !skb_is_gso(skb) block without calling skb_checksum_help() (or __virtio_net_resolve_csum_partial_mismatch()):

1. Because inner_nh (from vhdr->inner_nh_offset) is an untrusted offset inside the outer UDP payload and no tunnel header between outer_th and inner_nh is verified, a sender can place a fake 20-byte IPv4 header inside the UDP payload of a non-GSO UDP packet with csum_start = inner_nh + 20. Both virtio_net_tnl_offsets_ok() and virtio_net_csum_partial_ok_at_offset() succeed, SKB_GSO_UDP_TUNNEL is cleared, and the non-GSO UDP packet retains CHECKSUM_PARTIAL at an arbitrary offset inside its UDP payload (which can later overwrite 2 bytes of payload after netfilter/BPF inspection and causes udp4_csum_init() / udp6_csum_init() to treat the outer UDP checksum as unnecessary).

2. If VIRTIO_NET_HDR_F_UDP_TUNNEL_CSUM was set, clearing SKB_GSO_UDP_TUNNEL_CSUM without completing the inner checksum leaves the outer UDP header with an uncompleted non-zero pseudo-header checksum while CHECKSUM_PARTIAL still points to the inner transport header.

3. skb->encapsulation remains set to 1 even though SKB_GSO_UDP_TUNNEL and SKB_GSO_UDP_TUNNEL_CSUM are cleared.

4. Conversely, if a !skb_is_gso(skb) packet fails virtio_net_tnl_offsets_ok() or the inner_th < inner_nh + inner_l3min check earlier in virtio_net_hdr_tnl_to_skb(), it returns -EINVAL instead of restoring skb->transport_header = old_transport and completing the checksum via __virtio_net_resolve_csum_partial_mismatch(skb).

Should virtio_net_hdr_tnl_to_skb() resolve !skb_is_gso(skb) packets via __virtio_net_resolve_csum_partial_mismatch(skb) after restoring skb->transport_header = old_transport?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/305bcadb19f4a539a318c6b87c829fbe43d0ffdb.1793276116.git.mst@redhat.com?part=4
maintainer
#9 lore raw

Re: [PATCH RFC v7 4/5] net: resolve uncommon partial checksums at ingress

In reply to Michael S. Tsirkin
Quoted 78 lines · Michael S. Tsirkin

As reported first by Paulos Yibelo <[email protected]> (after which others, credited below, found more ways the bug can affect users) 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 shouldn't happen.

There's some risk here:
- non-gso packets which can't be parsed get checksum calculated in software, adding performance overhead
- gso packets which can't be parsed get dropped

Note that for zero copy, userspace can change the data after the checksum has been calculated. This is fundamental to zero copy by design, and the only outcome is a bad checksum and a packet drop.

To mitigate the risks, the following patch will add a sysctl knob to bypass the resolution. This is reasonable if the setup is such that there is no protocol processing at all.

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

Packets without VIRTIO_NET_HDR_F_NEEDS_CSUM are unaffected.

Note a fundamental limitation of this approach: an nf/BPF program that tweaks the payload of a packet in flight may rely on the checksum being fixed up later, assuming the packet carries CHECKSUM_PARTIAL.

For metadata that fails validation the checksum
is now completed at input, so such edits leave a stale checksum and the packet is dropped at the receiver. Protocols whose transport header we cannot parse (anything but TCP and UDP) are affected even when the metadata is self-consistent.

The sysctl added in the next patch is the escape hatch for these setups, but it is a very blunt tool.

Fixes f43798c27684 ("tun: Allow GSO using virtio_net_hdr")
include/linux/virtio_net.h +23 -0
+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);

include/linux/virtio_net.h static inline int virtio_net_hdr_set_proto(struct sk_buff *skb, · line 187

Quoted 10 lines
include/linux/virtio_net.h +10 -0
+		*ip_proto = iph->nexthdr;
+		if (*ip_proto != IPPROTO_TCP && *ip_proto != IPPROTO_UDP) {
+			*thoff = nhoff;
+			/* Find the transport header behind any
+			 * extension headers, AH included.
+			 */
+			*ip_proto = ipv6_find_hdr(skb, thoff, -1, NULL,
+						  &flags);
+			if (*ip_proto < 0 || flags & IP6_FH_F_FRAG)
+				return false;

Minor, not cause for respinning on its own.

flags should include IP6_FH_F_AUTH so ipv6_find_hdr() stops at NEXTHDR_AUTH instead of skipping it:

- CHECKSUM_PARTIAL and GSO behind IPsec AH are invalid: AH authenticates the inner L4 header including its checksum field

Quoted 15 lines
include/linux/virtio_net.h +10 -0
+		}
+		return true;
+	}
+
+	return false;
+}
+
@@ -368,6 +660,21 @@ 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;
+
+	if (!skb_is_gso(skb)) {
+		/* Too small to segment: complete the checksum in place

include/linux/virtio_net.h virtio_net_hdr_tnl_to_skb(struct sk_buff *skb, · line 675

Quoted 10 lines
include/linux/virtio_net.h +10 -0
+		 * instead of dropping - senders do tag small packets
+		 * with GSO headers, e.g. packetdrill sets gso_type and
+		 * gso_size with --mss, whatever the packet size.
+		 * The outer UDP checksum is not calculated, but a zero
+		 * one is fine: UDP over IPv4 allows a disabled
+		 * checksum, and so do tunnels over IPv6 where the
+		 * receiver opts in (RFC 6935, RFC 6936).
+		 */
+		skb_shinfo(skb)->gso_type &= ~(SKB_GSO_UDP_TUNNEL_CSUM |
+					       SKB_GSO_UDP_TUNNEL);

Maybe this carve-out for non-gso packet is best moved to its own patch, as that is where the remaining issues (to merging the rest) seem to be.

1. Moving !tnl_csum_negotiated below virtio_net_csum_partial_ok_at_offset() accepts packets with an unnegotiated VIRTIO_NET_HDR_F_UDP_TUNNEL_CSUM flag whenever inner validation falls back to __virtio_net_resolve_csum_partial_mismatch().
2. __virtio_net_resolve_csum_partial_mismatch() calls skb_checksum_help(), which completes only the inner checksum at hdr->csum_start and sets skb->ip_summed = CHECKSUM_NONE, leaving the outer UDP uh->check with an uncompleted pseudo-header checksum seed.

Despite the comment, this block neither completes any checksum in place nor is the outer UDP checksum zero when VIRTIO_NET_HDR_F_UDP_TUNNEL_CSUM is set:

- With VIRTIO_NET_HDR_F_UDP_TUNNEL_CSUM, the outer uh->check carries the non-zero pseudo-header checksum seed (as expected by __skb_udp_tunnel_segment()), not 0.
- Clearing SKB_GSO_UDP_TUNNEL_CSUM on a !skb_is_gso(skb) packet leaves skb->ip_summed == CHECKSUM_PARTIAL at inner_th with nothing completing the outer uh->check on transmit/forwarding (validate_xmit_skb() only completes/offloads the single inner checksum, emitting a corrupt outer UDP checksum). Even zeroing uh->check would be invalid over IPv6 unless the receiver explicitly opted into RFC 6936 (UDP_NO_CHECK6_RX).

Quoted 8 lines
include/linux/virtio_net.h +2 -0
+	}
+
 	return 0;
 }
 
Signature · 1 line
MST
author maintainer
#10 lore raw

Re: [PATCH RFC v7 4/5] net: resolve uncommon partial checksums at ingress

Quoted 137 lines · Willem de Bruijn
Quoted 78 lines · Michael S. Tsirkin

As reported first by Paulos Yibelo <[email protected]> (after which others, credited below, found more ways the bug can affect users) 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 shouldn't happen.

There's some risk here:
- non-gso packets which can't be parsed get checksum calculated in software, adding performance overhead
- gso packets which can't be parsed get dropped

Note that for zero copy, userspace can change the data after the checksum has been calculated. This is fundamental to zero copy by design, and the only outcome is a bad checksum and a packet drop.

To mitigate the risks, the following patch will add a sysctl knob to bypass the resolution. This is reasonable if the setup is such that there is no protocol processing at all.

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

Packets without VIRTIO_NET_HDR_F_NEEDS_CSUM are unaffected.

Note a fundamental limitation of this approach: an nf/BPF program that tweaks the payload of a packet in flight may rely on the checksum being fixed up later, assuming the packet carries CHECKSUM_PARTIAL.

For metadata that fails validation the checksum
is now completed at input, so such edits leave a stale checksum and the packet is dropped at the receiver. Protocols whose transport header we cannot parse (anything but TCP and UDP) are affected even when the metadata is self-consistent.

The sysctl added in the next patch is the escape hatch for these setups, but it is a very blunt tool.

Fixes f43798c27684 ("tun: Allow GSO using virtio_net_hdr")
include/linux/virtio_net.h +23 -0
+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);

include/linux/virtio_net.h static inline int virtio_net_hdr_set_proto(struct sk_buff *skb, · line 187

Quoted 10 lines
include/linux/virtio_net.h +10 -0
+		*ip_proto = iph->nexthdr;
+		if (*ip_proto != IPPROTO_TCP && *ip_proto != IPPROTO_UDP) {
+			*thoff = nhoff;
+			/* Find the transport header behind any
+			 * extension headers, AH included.
+			 */
+			*ip_proto = ipv6_find_hdr(skb, thoff, -1, NULL,
+						  &flags);
+			if (*ip_proto < 0 || flags & IP6_FH_F_FRAG)
+				return false;

Minor, not cause for respinning on its own.

flags should include IP6_FH_F_AUTH so ipv6_find_hdr() stops at NEXTHDR_AUTH instead of skipping it:

- CHECKSUM_PARTIAL and GSO behind IPsec AH are invalid: AH authenticates the inner L4 header including its checksum field

Quoted 15 lines
include/linux/virtio_net.h +10 -0
+		}
+		return true;
+	}
+
+	return false;
+}
+
@@ -368,6 +660,21 @@ 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;
+
+	if (!skb_is_gso(skb)) {
+		/* Too small to segment: complete the checksum in place

include/linux/virtio_net.h virtio_net_hdr_tnl_to_skb(struct sk_buff *skb, · line 675

Quoted 10 lines
include/linux/virtio_net.h +10 -0
+		 * instead of dropping - senders do tag small packets
+		 * with GSO headers, e.g. packetdrill sets gso_type and
+		 * gso_size with --mss, whatever the packet size.
+		 * The outer UDP checksum is not calculated, but a zero
+		 * one is fine: UDP over IPv4 allows a disabled
+		 * checksum, and so do tunnels over IPv6 where the
+		 * receiver opts in (RFC 6935, RFC 6936).
+		 */
+		skb_shinfo(skb)->gso_type &= ~(SKB_GSO_UDP_TUNNEL_CSUM |
+					       SKB_GSO_UDP_TUNNEL);

Maybe this carve-out for non-gso packet is best moved to its own patch, as that is where the remaining issues (to merging the rest) seem to be.

1. Moving !tnl_csum_negotiated below virtio_net_csum_partial_ok_at_offset() accepts packets with an unnegotiated VIRTIO_NET_HDR_F_UDP_TUNNEL_CSUM flag whenever inner validation falls back to __virtio_net_resolve_csum_partial_mismatch().
2. __virtio_net_resolve_csum_partial_mismatch() calls skb_checksum_help(), which completes only the inner checksum at hdr->csum_start and sets skb->ip_summed = CHECKSUM_NONE, leaving the outer UDP uh->check with an uncompleted pseudo-header checksum seed.

Quoted 6 lines

Despite the comment, this block neither completes any checksum in place nor is the outer UDP checksum zero when VIRTIO_NET_HDR_F_UDP_TUNNEL_CSUM is set:

- With VIRTIO_NET_HDR_F_UDP_TUNNEL_CSUM, the outer uh->check carries the non-zero pseudo-header checksum seed (as expected by __skb_udp_tunnel_segment()), not 0.

Oh yea i kept tweaking this and forgot to update the ocmment.

Quoted 6 lines

- Clearing SKB_GSO_UDP_TUNNEL_CSUM on a !skb_is_gso(skb) packet leaves skb->ip_summed == CHECKSUM_PARTIAL at inner_th with nothing completing the outer uh->check on transmit/forwarding (validate_xmit_skb() only completes/offloads the single inner checksum, emitting a corrupt outer UDP checksum). Even zeroing uh->check would be invalid over IPv6 unless the receiver explicitly opted into RFC 6936 (UDP_NO_CHECK6_RX).

Indeed.

Quoted 9 lines
Quoted 8 lines
include/linux/virtio_net.h +2 -0
+	}
+
 	return 0;
 }
 
Signature · 1 line
MST
#11 lore raw

Re: [PATCH RFC v7 4/5] net: resolve uncommon partial checksums at ingress

In reply to Michael S. Tsirkin
Quoted 39 lines · Michael S. Tsirkin

As reported first by Paulos Yibelo <[email protected]> (after which others, credited below, found more ways the bug can affect users) 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 shouldn't happen.

There's some risk here:
- non-gso packets which can't be parsed get checksum calculated in software, adding performance overhead
- gso packets which can't be parsed get dropped

Note that for zero copy, userspace can change the data after the checksum has been calculated. This is fundamental to zero copy by design, and the only outcome is a bad checksum and a packet drop.

To mitigate the risks, the following patch will add a sysctl knob to bypass the resolution. This is reasonable if the setup is such that there is no protocol processing at all.

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

Packets without VIRTIO_NET_HDR_F_NEEDS_CSUM are unaffected.

Note a fundamental limitation of this approach: an nf/BPF program that tweaks the payload of a packet in flight may rely on the checksum being fixed up later, assuming the packet carries CHECKSUM_PARTIAL.

Quoted 9 lines

For metadata that fails validation the checksum
is now completed at input, so such edits leave a stale checksum and the packet is dropped at the receiver. Protocols whose transport header we cannot parse (anything but TCP and UDP) are affected even when the metadata is self-consistent.

The sysctl added in the next patch is the escape hatch for these setups, but it is a very blunt tool.

v7: Tested-by: Chris Arges <[email protected]>

Quoted 420 lines
Fixes f43798c27684 ("tun: Allow GSO using virtio_net_hdr")

---

Notes (changelog):
v4->v5:
caller changes moved to "account for ingress-resolved virtio net hdr state" (Willem de Bruijn)
a packet too small to be GSO: ignore the tunnel GSO request, complete wild metadata in place (sashiko) document the zero copy interaction (sashiko) honor a preset skb->protocol on ethernet: the stack uses it, except on loopback which re-parses the frame (sashiko)
v5->v6:
read both skb->protocol and the frame ethertype: do not validate when they disagree, instead of preferring the preset protocol; same check after the IP version inference where there is no L2 header (Willem de Bruijn) pull the IPv4 options in the helper as introduced, with the transport header offset computed before the pull (Willem de Bruijn)
fold the trivial protocol calculation cleanup, formerly a separate patch, in (Willem de Bruijn)

 include/linux/virtio_net.h | 315 ++++++++++++++++++++++++++++++++++++-
 1 file changed, 311 insertions(+), 4 deletions(-)
include/linux/virtio_net.h +311 -4
index 013c6c7e2dea..54048759cc6e 100644
@@ -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,6 +52,216 @@ 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.  The whole header,
+ * options included, is guaranteed linear on return.
+ */
+static inline struct iphdr *virtio_net_ip4_parse(struct sk_buff *skb,
+						  unsigned int nhoff,
+						  unsigned int *thoff)
+{
+	unsigned int ihl;
+	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;
+
+	ihl = iph->ihl * sizeof(u32);
+	*thoff = nhoff + ihl;
+	/* Pull the options as well: the transport header offset
+	 * returned above must be within the linear data.  iph goes
+	 * stale across the pull.
+	 */
+	if (!pskb_may_pull(skb, nhoff + ihl))
+		return NULL;
+
+	return (struct iphdr *)(skb->data + nhoff);
+}
+
+/* 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 should always be set here already,
+	 * but let's not crash when it is not.
+	 */
+	if (WARN_ON_ONCE(!skb->dev))
+		return false;
+
+	if (skb->dev->type == ARPHRD_ETHER ||
+	    skb->dev->type == ARPHRD_LOOPBACK) {
+		__be16 h_proto;
+
+		if (!pskb_may_pull(skb, ETH_HLEN))
+			return false;
+		h_proto = eth_hdr(skb)->h_proto;
+		/* skb->protocol is a sender claim, and parts of the
+		 * stack parse the packet per it: when it disagrees with
+		 * the frame we do not know which view the packet will
+		 * be parsed as, so do not validate.
+		 */
+		if (skb->protocol && skb->protocol != h_proto)
+			return false;
+		*nhoff = ETH_HLEN;
+		*proto = h_proto;
+		if (eth_type_vlan(*proto)) {
+			*proto = __vlan_get_protocol(skb, *proto, nhoff);
+			if (!*proto)
+				return false;
+		}
+	} else {
+		const struct iphdr *iph;
+
+		*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;
+		/* No L2 header to cross-check with: take the protocol
+		 * from the IP version at the network header.
+		 */
+		if (!pskb_may_pull(skb, *nhoff + 1))
+			return false;
+		iph = (const struct iphdr *)(skb->data + *nhoff);
+		if (iph->version == 4)
+			*proto = htons(ETH_P_IP);
+		else if (iph->version == 6)
+			*proto = htons(ETH_P_IPV6);
+		else
+			return false;
+		if (skb->protocol && skb->protocol != *proto)
+			return false;
+	}
+	return true;
+}
+
+/* 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;
+			/* Find the transport header behind any
+			 * extension headers, AH included.
+			 */
+			*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;
+}
+
+/* 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 (!virtio_net_l4_start(skb, nhoff, proto, &thoff, &ip_proto))
+		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)
+{
+	int nhoff;
+	__be16 proto;
+
+	if (!virtio_net_l3_start(skb, &nhoff, &proto))
+		return false;
+
+	return virtio_net_csum_partial_ok_at_offset(skb, hdr_gso_type, nhoff,
+						   proto);
+}
+
+/*
+ * 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 +408,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 (likely(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);
 }
 
 /* This function must be called after virtio_net_hdr_from_skb(). */
@@ -282,6 +520,35 @@ static inline unsigned int virtio_l3min(bool is_ipv6)
 	return is_ipv6 ? sizeof(struct ipv6hdr) : sizeof(struct iphdr);
 }
 
+/* Do the tunnel offsets match the outer headers in the frame?
+ * The L3 header the tunnel claims must be the one the frame
+ * carries, with UDP following it at @outer_th.
+ */
+static inline bool
+virtio_net_tnl_offsets_ok(struct sk_buff *skb, bool outer_isv6,
+			  unsigned int outer_th)
+{
+	int nhoff;
+	__be16 proto;
+	unsigned int thoff;
+	int ip_proto;
+
+	if (!virtio_net_l3_start(skb, &nhoff, &proto))
+		return false;
+	if (outer_isv6 != (proto == htons(ETH_P_IPV6)))
+		return false;
+	if (!virtio_net_l4_start(skb, nhoff, proto, &thoff, &ip_proto))
+		return false;
+
+	return ip_proto == IPPROTO_UDP && outer_th == thoff;
+}
+
+/*
+ * Return value:
+ * 0 - success
+ * >0 - checksum was completed in software (slow path)
+ * <0 - failure
+ */
 static inline int
 virtio_net_hdr_tnl_to_skb(struct sk_buff *skb,
 			  const struct virtio_net_hdr_v1_hash_tunnel *vhdr,
@@ -290,9 +557,11 @@ virtio_net_hdr_tnl_to_skb(struct sk_buff *skb,
 			  bool little_endian)
 {
 	const struct virtio_net_hdr *hdr = (const struct virtio_net_hdr *)vhdr;
+	typeof(skb->transport_header) old_transport = skb->transport_header;
 	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;
 
@@ -335,9 +604,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
@@ -352,9 +624,29 @@ 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);
+
+	/*
+	 * We know VIRTIO_NET_HDR_F_NEEDS_CSUM was set so __virtio_net_hdr_to_skb
+	 * set CHECKSUM_PARTIAL.
+	 */
+	if (unlikely(!virtio_net_csum_partial_ok_at_offset(skb, gso_inner_type,
+							  inner_nh,
+							  inner_proto))) {
+		/* __virtio_net_hdr_to_skb set the transport header to
+		 * csum_start, restore it to a known good value.
+		 */
+		skb->transport_header = old_transport;
+
+		return __virtio_net_resolve_csum_partial_mismatch(skb);
+	}
+
+	skb_set_inner_protocol(skb, inner_proto);
 	if (hdr->flags & VIRTIO_NET_HDR_F_UDP_TUNNEL_CSUM) {
+		/* Checked here, after the validation: a packet completed
+		 * in software never uses the feature, so an unnegotiated
+		 * request only matters for metadata good enough to act on.
+		 */
 		if (!tnl_csum_negotiated)
 			return -EINVAL;
 
@@ -368,6 +660,21 @@ 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;
+
+	if (!skb_is_gso(skb)) {
+		/* Too small to segment: complete the checksum in place
+		 * instead of dropping - senders do tag small packets
+		 * with GSO headers, e.g. packetdrill sets gso_type and
+		 * gso_size with --mss, whatever the packet size.
+		 * The outer UDP checksum is not calculated, but a zero
+		 * one is fine: UDP over IPv4 allows a disabled
+		 * checksum, and so do tunnels over IPv6 where the
+		 * receiver opts in (RFC 6935, RFC 6936).
+		 */
+		skb_shinfo(skb)->gso_type &= ~(SKB_GSO_UDP_TUNNEL_CSUM |
+					       SKB_GSO_UDP_TUNNEL);
+	}
+
 	return 0;
 }
 
Signature · 1 line
MST
author maintainer
#12 lore raw

RFC PATCH v7 5/5 net: add a sysctl to opt out of virtio net hdr checksum validation

In reply to Michael S. Tsirkin

The ingress validation as implemented by the previous patch is stricter than what it replaced: GSO
packets whose checksum metadata matches no transport header are dropped, and non-GSO ones get their checksum completed in place. Such packets used to pass with the metadata unvalidated, and with no overhead.

I judge that this should not affect any deployments, but there is still some risk that it does. In case a deployment depends on them anyway, provide a per-netns switch to get the old behavior back:

echo 0 > /proc/sys/net/core/virtio_net_hdr_validate

The default is validating.

---

Notes (changelog):
v4->v5:
document the netns lifetime requirement (sashiko)

 Documentation/admin-guide/sysctl/net.rst | 19 +++++++++
 include/linux/virtio_net.h               | 25 ++++++++++++
 include/net/netns/core.h                 |  1 +
 net/core/dev.c                           |  7 ++++
 net/core/net_namespace.c                 |  1 +
 net/core/sysctl_net_core.c               | 50 ++++++++++++++++++++++++
 6 files changed, 103 insertions(+)
Documentation/admin-guide/sysctl/net.rst +19 -0
index fe43e8595958..3d89772cb7fe 100644
@@ -246,6 +246,25 @@ processes are dropped unless socket option SOF_TIMESTAMPING_OPT_TSONLY is set.
 Default: 1 (on)
 
 
+virtio_net_hdr_validate
+-----------------------
+
+When processing packets with a virtio net header (through tun, tap, a
+packet socket or a virtio network device) that request checksum offload
+(VIRTIO_NET_HDR_F_NEEDS_CSUM), validate the packet: complete the checksum
+in software, or drop the packet where the header does not match it.
+
+When disabled, the header is trusted not to corrupt the packet, and the
+checksum is offloaded to hardware (or not verified if it never reaches
+hardware). This is only safe if no protocol is processing the packets.
+
+The setting is per network namespace, but only writable by the host
+root: root inside a network namespace created by an unprivileged user
+cannot disable the validation.
+
+Default: 1 (on)
+
+
 wmem_default
 ------------
 
include/linux/virtio_net.h +25 -0
index 54048759cc6e..b2bd5ab3fdb6 100644
@@ -5,6 +5,7 @@
 #include <linux/if_vlan.h>
 #include <linux/ip.h>
 #include <linux/ipv6.h>
+#include <linux/jump_label.h>
 #include <linux/netdevice.h>
 #include <linux/udp.h>
 #include <net/gso.h>
@@ -87,6 +88,18 @@ static inline struct iphdr *virtio_net_ip4_parse(struct sk_buff *skb,
 	return (struct iphdr *)(skb->data + nhoff);
 }
 
+/* Defined in net/core/dev.c, enabled while any network namespace
+ * has net.core.virtio_net_hdr_validate set to 0.
+ */
+DECLARE_STATIC_KEY_FALSE(virtio_net_hdr_validate_disabled_key);
+
+/*The caller must keep the netns of skb->dev alive. */
+static inline bool virtio_net_hdr_validate_disabled(struct sk_buff *skb)
+{
+	return static_branch_unlikely(&virtio_net_hdr_validate_disabled_key) &&
+	       !READ_ONCE(dev_net(skb->dev)->core.sysctl_virtio_net_hdr_validate);
+}
+
 /* 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.
@@ -200,6 +213,12 @@ virtio_net_csum_partial_ok_at_offset(struct sk_buff *skb, u8 hdr_gso_type,
 	unsigned int thoff;
 	int ip_proto;
 
+	/* Failsafe: a namespace opted out of the validation,
+	 * trust the checksum metadata as before.
+	 */
+	if (virtio_net_hdr_validate_disabled(skb))
+		return true;
+
 	if (!virtio_net_l4_start(skb, nhoff, proto, &thoff, &ip_proto))
 		return false;
 
@@ -242,6 +261,9 @@ virtio_net_csum_partial_ok(struct sk_buff *skb, u8 hdr_gso_type)
 	int nhoff;
 	__be16 proto;
 
+	if (virtio_net_hdr_validate_disabled(skb))
+		return true;
+
 	if (!virtio_net_l3_start(skb, &nhoff, &proto))
 		return false;
 
@@ -533,6 +555,9 @@ virtio_net_tnl_offsets_ok(struct sk_buff *skb, bool outer_isv6,
 	unsigned int thoff;
 	int ip_proto;
 
+	if (virtio_net_hdr_validate_disabled(skb))
+		return true;
+
 	if (!virtio_net_l3_start(skb, &nhoff, &proto))
 		return false;
 	if (outer_isv6 != (proto == htons(ETH_P_IPV6)))
include/net/netns/core.h +1 -0
index 9ef3d70e5e9c..02ce232fe9a9 100644
@@ -18,6 +18,7 @@ struct netns_core {
 	u8	sysctl_txrehash;
 	u8	sysctl_tstamp_allow_data;
 	u8	sysctl_bypass_prot_mem;
+	u8	sysctl_virtio_net_hdr_validate;
 
 #ifdef CONFIG_PROC_FS
 	struct prot_inuse __percpu *prot_inuse;
net/core/dev.c +7 -0
index 87a79d6004c8..960c97980544 100644
@@ -128,6 +128,7 @@
 #include <linux/if_vlan.h>
 #include <linux/ip.h>
 #include <net/ip.h>
+#include <linux/virtio_net.h>
 #include <net/mpls.h>
 #include <net/route.h>
 #include <linux/ipv6.h>
@@ -2427,6 +2428,12 @@ EXPORT_SYMBOL(tcf_sw_enabled_key);
 
 DEFINE_STATIC_KEY_FALSE(netstamp_needed_key);
 EXPORT_SYMBOL(netstamp_needed_key);
+
+/* Enabled while any network namespace has net.core.virtio_net_hdr_validate
+ * set to 0.
+ */
+DEFINE_STATIC_KEY_FALSE(virtio_net_hdr_validate_disabled_key);
+EXPORT_SYMBOL(virtio_net_hdr_validate_disabled_key);
 #ifdef CONFIG_JUMP_LABEL
 static atomic_t netstamp_needed_deferred;
 static atomic_t netstamp_wanted;
net/core/net_namespace.c +1 -0
index da5f881fbd3b..8a21baaa7e6d 100644
@@ -396,6 +396,7 @@ static __net_init void preinit_net_sysctl(struct net *net)
 	net->core.sysctl_optmem_max = 128 * 1024;
 	net->core.sysctl_txrehash = SOCK_TXREHASH_ENABLED;
 	net->core.sysctl_tstamp_allow_data = 1;
+	net->core.sysctl_virtio_net_hdr_validate = 1;
 	net->core.sysctl_txq_reselection = msecs_to_jiffies(1000);
 }
 
net/core/sysctl_net_core.c +50 -0

74 lines not on this page · raw message

Signature · 1 line
MST
#13 lore raw

Re: [PATCH RFC v7 5/5] net: add a sysctl to opt out of virtio net hdr checksum validation

Sashiko has reviewed this patch and found no issues.
Quoted 11 lines

The ingress validation as implemented by the previous patch is stricter than what it replaced: GSO
packets whose checksum metadata matches no transport header are dropped, and non-GSO ones get their checksum completed in place. Such packets used to pass with the metadata unvalidated, and with no overhead.

I judge that this should not affect any deployments, but there is still some risk that it does. In case a deployment depends on them anyway, provide a per-netns switch to get the old behavior back:

Quoted 7 lines

echo 0 > /proc/sys/net/core/virtio_net_hdr_validate

The default is validating.

Sashiko has reviewed this patch and found no issues.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/305bcadb19f4a539a318c6b87c829fbe43d0ffdb.1793276116.git.mst@redhat.com?part=5