Netdev List
 help / color / mirror / Atom feed
* [PATCH net v3 0/8] net: virtio: validate outer protocol of UDP tunnel GSO packets
@ 2026-11-05 11:57 Eric Dumazet
  2026-11-05 11:57 ` [PATCH net v3 1/8] " Eric Dumazet
                   ` (7 more replies)
  0 siblings, 8 replies; 9+ messages in thread
From: Eric Dumazet @ 2026-11-05 11:57 UTC (permalink / raw)
  To: netdev, David S. Miller, Jakub Kicinski, Paolo Abeni
  Cc: Eric Dumazet, Simon Horman, Willem de Bruijn, Michael S. Tsirkin,
	Jason Wang, Eugenio Pérez, Hassan Sultan, virtualization

virtio_net_hdr_tnl_to_skb() does not check that a UDP tunnel GSO
packet really has outer IPv4/IPv6 and UDP headers matching the
declared tunnel type. A guest, or any tap writer, can send a frame
with another outer ethertype (NSH in the report), and the GSO stack
then builds segments with skb->data before skb->head. The host
crashes in skb_under_panic() as soon as a header is pushed.

Patch 1 validates the outer headers, using the parsing helpers from
Michael's series "net: resolve uncommon partial checksums at ingress",
as Willem suggested. The rest of that series (checksum validation and
completion) is left to Michael.

Patch 2 lets the callers tell whether frames start with an Ethernet
header, instead of using the device type, which tun can change with
TUNSETLINK.

Patch 3 clears a stale skb->protocol left on recycled NAPI frags skbs,
which could make the checks of patch 1 reject valid frames.

Patch 4 fixes a selftest helper computing UDP checksums over a length
in network byte order.

Patches 5 to 7 add tun selftests, on tap and IFF_TUN devices: valid
outer IPv4/IPv6 frames are accepted; mismatched or non-UDP outer
headers and misplaced outer transport headers are rejected, also on
retyped devices.

Patch 8 makes the UDP tunnel selftests write the tunnel offsets of the
virtio header in little endian, so they work on big-endian hosts.

v3:
- Patch 1: fix a comment, ipv6_find_hdr() stops at AH (Sashiko).
- New patch 2, and its tests in patch 6: Sashiko found that changing
  the device type with TUNSETLINK bypasses the checks of patch 1.
- New patch 3: Sashiko found that napi_reuse_skb() leaves a stale
  skb->protocol on IFF_NAPI_FRAGS skbs.
- New patch 7: test cases Sashiko found missing in patch 5 (outer
  IPv6, VLAN with IPv6 or with a wrong outer_th_offset, non-UDP outer
  L4). Fixed the patch 5 changelog.
- New patch 8: big-endian hosts (Sashiko).
- Collected Reviewed-by and Acked-by tags.

v2: https://lore.kernel.org/netdev/20261104050131.629967-1-edumazet@kernel.org/
- Patch 1: use Michael's helpers instead of a new one: find the L3
  header from the device type instead of assuming Ethernet when
  skb->protocol is unset, do not trust skb->protocol when it disagrees
  with the frame, and require UDP exactly at outer_th_offset. Stop at
  IPv6 AH headers (Willem).
- Patch 2: clear udph->check before summing and send 0xffff instead
  of a zero checksum (Willem).
- Patch 3: clear the reused buffer before building each frame (Willem).
  Add IFF_TUN tests, where skb->protocol comes from the tun_pi header.

v1: https://lore.kernel.org/netdev/20261103203113.550604-1-edumazet@kernel.org/

Eric Dumazet (8):
  net: virtio: validate outer protocol of UDP tunnel GSO packets
  net: virtio: do not use dev->type to find the outer L3 header
  tun: clear stale skb->protocol of NAPI frags skbs
  selftests: net: tuntap: fix UDP checksum length in
    build_udp_packet_csum()
  selftests: net: tun: test outer protocol of UDP tunnel GSO packets
  selftests: net: tun: test UDP tunnel GSO on retyped devices
  selftests: net: tun: test more outer header mismatches of UDP tunnel
    GSO
  selftests: net: tuntap: write UDP tunnel offsets in little endian

 drivers/net/tun.c                            |  14 +-
 drivers/net/tun_vnet.h                       |   5 +-
 drivers/net/virtio_net.c                     |   2 +-
 include/linux/virtio_net.h                   | 169 +++++++
 tools/testing/selftests/net/tun.c            | 440 +++++++++++++++++++
 tools/testing/selftests/net/tuntap_helpers.h |  29 +-
 6 files changed, 649 insertions(+), 10 deletions(-)


base-commit: 37dfd0efbfffc20a57e41c2eb349ac49b1ca1ff8
-- 
2.53.0


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH net v3 1/8] net: virtio: validate outer protocol of UDP tunnel GSO packets
  2026-11-05 11:57 [PATCH net v3 0/8] net: virtio: validate outer protocol of UDP tunnel GSO packets Eric Dumazet
@ 2026-11-05 11:57 ` Eric Dumazet
  2026-11-05 11:57 ` [PATCH net v3 2/8] net: virtio: do not use dev->type to find the outer L3 header Eric Dumazet
                   ` (6 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: Eric Dumazet @ 2026-11-05 11:57 UTC (permalink / raw)
  To: netdev, David S. Miller, Jakub Kicinski, Paolo Abeni
  Cc: Eric Dumazet, Simon Horman, Willem de Bruijn, Michael S. Tsirkin,
	Jason Wang, Eugenio Pérez, Hassan Sultan, virtualization

virtio_net_hdr_tnl_to_skb() trusts the virtio header to describe a
UDP tunnel over IPv4 or IPv6, but never checks that the frame actually
carries such outer headers. Because VIRTIO_NET_HDR_F_NEEDS_CSUM is
mandatory for tunnel GSO, __virtio_net_hdr_to_skb() skips the flow
dissector, so virtio_net_hdr_match_proto() is never applied either.

A guest (or a tap writer) can then inject a frame with an arbitrary
outer ethertype, for example ETH_P_NSH, with SKB_GSO_UDP_TUNNEL set.
The GSO stack then runs nsh_gso_segment() on top of the UDP tunnel
segmentation, which assumes a different header layout. The result is
segments with skb->data before skb->head, and a host crash in
skb_under_panic() as soon as a later layer pushes a header:

  skbuff: skb_under_panic: ... put:4 ... dev:gretap1
  kernel BUG at net/core/skbuff.c:215!
   skb_push
   __gre_xmit
   gre_tap_xmit
   ...
   tun_get_user

Validate the outer headers with the parsing helpers from Michael's
series "net: resolve uncommon partial checksums at ingress":

- locate the L3 header from the device type: Ethernet with optional
  VLAN tags, or the network header set by the caller. skb->protocol is
  not trusted when it disagrees with the frame.
- require the outer L3 protocol to match the declared tunnel type.
- require the outer UDP header exactly at outer_th_offset, after IPv4
  options or IPv6 extension headers, and reject fragments.

Unlike the original helpers, ipv6_find_hdr() stops at an AH header,
as Willem suggested in the review of that series.

Fixes: a2fb4bc4e2a6 ("net: implement virtio helpers to handle UDP GSO tunneling.")
Reported-by: Hassan Sultan <hassansultan@google.com>
Suggested-by: Willem de Bruijn <willemb@google.com>
Link: https://lore.kernel.org/netdev/93d0e37c99711bebdf60c120af03010455c2f2e2.1793276116.git.mst@redhat.com/
Cc: Willem de Bruijn <willemb@google.com>
Cc: Michael S. Tsirkin <mst@redhat.com>
Co-developed-by: Michael S. Tsirkin <mst@redhat.com>
Signed-off-by: Michael S. Tsirkin <mst@redhat.com>
Reviewed-by: Willem de Bruijn <willemb@google.com>
Acked-by: Michael S. Tsirkin <mst@redhat.com>
Signed-off-by: Eric Dumazet <edumazet@kernel.org>
---
 include/linux/virtio_net.h | 170 +++++++++++++++++++++++++++++++++++++
 1 file changed, 170 insertions(+)

diff --git a/include/linux/virtio_net.h b/include/linux/virtio_net.h
index d6466f96cdd0..357116c2a148 100644
--- a/include/linux/virtio_net.h
+++ b/include/linux/virtio_net.h
@@ -5,7 +5,10 @@
 #include <linux/if_vlan.h>
 #include <linux/ip.h>
 #include <linux/ipv6.h>
+#include <linux/netdevice.h>
 #include <linux/udp.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 +51,147 @@ 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 = IP6_FH_F_AUTH;
+
+		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. IP6_FH_F_AUTH stops
+			 * the walk at AH, so frames with AH are rejected.
+			 */
+			*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;
+}
+
 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)
@@ -282,6 +426,29 @@ 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;
+}
+
 static inline int
 virtio_net_hdr_tnl_to_skb(struct sk_buff *skb,
 			  const struct virtio_net_hdr_v1_hash_tunnel *vhdr,
@@ -338,6 +505,9 @@ virtio_net_hdr_tnl_to_skb(struct sk_buff *skb,
 	if (ret)
 		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
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH net v3 2/8] net: virtio: do not use dev->type to find the outer L3 header
  2026-11-05 11:57 [PATCH net v3 0/8] net: virtio: validate outer protocol of UDP tunnel GSO packets Eric Dumazet
  2026-11-05 11:57 ` [PATCH net v3 1/8] " Eric Dumazet
@ 2026-11-05 11:57 ` Eric Dumazet
  2026-11-05 11:57 ` [PATCH net v3 3/8] tun: clear stale skb->protocol of NAPI frags skbs Eric Dumazet
                   ` (5 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: Eric Dumazet @ 2026-11-05 11:57 UTC (permalink / raw)
  To: netdev, David S. Miller, Jakub Kicinski, Paolo Abeni
  Cc: Eric Dumazet, Simon Horman, Willem de Bruijn, Michael S. Tsirkin,
	Jason Wang, Eugenio Pérez, Hassan Sultan, virtualization

virtio_net_l3_start() decides from skb->dev->type whether the frame
starts with an Ethernet header. tun decides it from the IFF_TAP and
IFF_TUN flags, and TUNSETLINK can set the type of a down tun device
to any value, without changing the flags.

On an IFF_TAP device set to ARPHRD_NONE, the outer protocol checks
look at the IP header at the network header (ETH_HLEN), and never read
the Ethernet header. eth_type_trans() then sets skb->protocol from an
unchecked h_proto, for instance ETH_P_NSH, and the packet can reach
the skb_under_panic() addressed by the previous patch.

On an IFF_TUN device set to ARPHRD_ETHER, the checks validate an
Ethernet header that tun then treats as the start of the L3 header.

Checking in tun that the device type matches the framing is not
enough: the type is read again later, and a concurrent TUNSETLINK can
change it in between, since tun_get_user() only drops packets of a
down device after these checks.

Add a has_eth argument to virtio_net_hdr_tnl_to_skb(), passed down to
virtio_net_l3_start(), so the device type is no longer used:

- virtio_net always receives Ethernet frames.
- tun_get_user() passes whether the device is IFF_TAP.
- tun_xdp_one() always calls eth_type_trans() on its frames.

Fixes: 288f30435132 ("tun: enable gso over UDP tunnel support.")
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/netdev/sashiko-outbox-198573@kernel.org/
Link: https://lore.kernel.org/netdev/179387332014.488856.9111956413610107951@kernel.org/
Signed-off-by: Eric Dumazet <edumazet@kernel.org>
---
 drivers/net/tun.c          |  9 +++++++--
 drivers/net/tun_vnet.h     |  5 +++--
 drivers/net/virtio_net.c   |  2 +-
 include/linux/virtio_net.h | 27 +++++++++++++--------------
 4 files changed, 24 insertions(+), 19 deletions(-)

diff --git a/drivers/net/tun.c b/drivers/net/tun.c
index dcec7311e601..3920b3dff1e8 100644
--- a/drivers/net/tun.c
+++ b/drivers/net/tun.c
@@ -1944,7 +1944,8 @@ static ssize_t tun_get_user(struct tun_struct *tun, struct tun_file *tfile,
 		break;
 	}
 
-	if (tun_vnet_hdr_tnl_to_skb(tun->flags, features, skb, &hdr)) {
+	if (tun_vnet_hdr_tnl_to_skb(tun->flags, features, skb, &hdr,
+				    (tun->flags & TUN_TYPE_MASK) == IFF_TAP)) {
 		atomic_long_inc(&tun->rx_frame_errors);
 		err = -EINVAL;
 		goto free_skb;
@@ -2621,7 +2622,11 @@ static int tun_xdp_one(struct tun_struct *tun,
 	tnl_hdr = (struct virtio_net_hdr_v1_hash_tunnel *)gso;
 	skb->dev = tun->dev;
 	skb_set_network_header(skb, ETH_HLEN);
-	if (tun_vnet_hdr_tnl_to_skb(tun->flags, features, skb, tnl_hdr)) {
+	/* Frames always start with an Ethernet header here, as the
+	 * eth_type_trans() call below expects.
+	 */
+	if (tun_vnet_hdr_tnl_to_skb(tun->flags, features, skb, tnl_hdr,
+				    true)) {
 		atomic_long_inc(&tun->rx_frame_errors);
 		kfree_skb(skb);
 		ret = -EINVAL;
diff --git a/drivers/net/tun_vnet.h b/drivers/net/tun_vnet.h
index f4c652b1fa44..3364e08dd6e0 100644
--- a/drivers/net/tun_vnet.h
+++ b/drivers/net/tun_vnet.h
@@ -197,9 +197,10 @@ static inline netdev_features_t tun_vnet_hdr_guest_features(int vnet_hdr_sz)
 static inline int
 tun_vnet_hdr_tnl_to_skb(unsigned int flags, netdev_features_t features,
 			struct sk_buff *skb,
-			const struct virtio_net_hdr_v1_hash_tunnel *hdr)
+			const struct virtio_net_hdr_v1_hash_tunnel *hdr,
+			bool has_eth)
 {
-	return virtio_net_hdr_tnl_to_skb(skb, hdr,
+	return virtio_net_hdr_tnl_to_skb(skb, hdr, has_eth,
 				features & NETIF_F_GSO_UDP_TUNNEL,
 				features & NETIF_F_GSO_UDP_TUNNEL_CSUM,
 				tun_vnet_is_little_endian(flags));
diff --git a/drivers/net/virtio_net.c b/drivers/net/virtio_net.c
index dd2c41ef6fbe..ed20e76fbc4c 100644
--- a/drivers/net/virtio_net.c
+++ b/drivers/net/virtio_net.c
@@ -2533,7 +2533,7 @@ static void virtnet_receive_done(struct virtnet_info *vi, struct receive_queue *
 
 	skb->dev = dev;
 	skb_set_network_header(skb, ETH_HLEN);
-	if (virtio_net_hdr_tnl_to_skb(skb, &hdr->tnl_hdr, vi->rx_tnl,
+	if (virtio_net_hdr_tnl_to_skb(skb, &hdr->tnl_hdr, true, vi->rx_tnl,
 				      vi->rx_tnl_csum,
 				      virtio_is_little_endian(vi->vdev))) {
 		net_warn_ratelimited("%s: bad gso: type: %x, size: %u, flags %x tunnel %d tnl csum %d\n",
diff --git a/include/linux/virtio_net.h b/include/linux/virtio_net.h
index 357116c2a148..b7b6b542f476 100644
--- a/include/linux/virtio_net.h
+++ b/include/linux/virtio_net.h
@@ -87,20 +87,18 @@ static inline struct iphdr *virtio_net_ip4_parse(struct sk_buff *skb,
 }
 
 /* Where does the L3 header start, and what protocol is it?
- * Handles Ethernet and VLAN headers; other devices use the
- * network header offset the caller set up.
+ * With @has_eth, the frame starts with an Ethernet header, possibly
+ * followed by VLAN tags. Otherwise the L3 header starts at the network
+ * header offset the caller set up.
+ *
+ * The caller knows how the frame will be parsed later: the device
+ * type does not tell, as tun can change it with TUNSETLINK.
  */
 static inline bool
-virtio_net_l3_start(struct sk_buff *skb, int *nhoff, __be16 *proto)
+virtio_net_l3_start(struct sk_buff *skb, bool has_eth, 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) {
+	if (has_eth) {
 		__be16 h_proto;
 
 		if (!pskb_may_pull(skb, ETH_HLEN))
@@ -431,7 +429,7 @@ static inline unsigned int virtio_l3min(bool is_ipv6)
  * carries, with UDP following it at @outer_th.
  */
 static inline bool
-virtio_net_tnl_offsets_ok(struct sk_buff *skb, bool outer_isv6,
+virtio_net_tnl_offsets_ok(struct sk_buff *skb, bool has_eth, bool outer_isv6,
 			  unsigned int outer_th)
 {
 	int nhoff;
@@ -439,7 +437,7 @@ virtio_net_tnl_offsets_ok(struct sk_buff *skb, bool outer_isv6,
 	unsigned int thoff;
 	int ip_proto;
 
-	if (!virtio_net_l3_start(skb, &nhoff, &proto))
+	if (!virtio_net_l3_start(skb, has_eth, &nhoff, &proto))
 		return false;
 	if (outer_isv6 != (proto == htons(ETH_P_IPV6)))
 		return false;
@@ -452,6 +450,7 @@ virtio_net_tnl_offsets_ok(struct sk_buff *skb, bool outer_isv6,
 static inline int
 virtio_net_hdr_tnl_to_skb(struct sk_buff *skb,
 			  const struct virtio_net_hdr_v1_hash_tunnel *vhdr,
+			  bool has_eth,
 			  bool tnl_hdr_negotiated,
 			  bool tnl_csum_negotiated,
 			  bool little_endian)
@@ -505,7 +504,7 @@ virtio_net_hdr_tnl_to_skb(struct sk_buff *skb,
 	if (ret)
 		return ret;
 
-	if (!virtio_net_tnl_offsets_ok(skb, outer_isv6, outer_th))
+	if (!virtio_net_tnl_offsets_ok(skb, has_eth, outer_isv6, outer_th))
 		return -EINVAL;
 
 	/* In case of USO, the inner protocol is still unknown and
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH net v3 3/8] tun: clear stale skb->protocol of NAPI frags skbs
  2026-11-05 11:57 [PATCH net v3 0/8] net: virtio: validate outer protocol of UDP tunnel GSO packets Eric Dumazet
  2026-11-05 11:57 ` [PATCH net v3 1/8] " Eric Dumazet
  2026-11-05 11:57 ` [PATCH net v3 2/8] net: virtio: do not use dev->type to find the outer L3 header Eric Dumazet
@ 2026-11-05 11:57 ` Eric Dumazet
  2026-11-05 11:57 ` [PATCH net v3 4/8] selftests: net: tuntap: fix UDP checksum length in build_udp_packet_csum() Eric Dumazet
                   ` (4 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: Eric Dumazet @ 2026-11-05 11:57 UTC (permalink / raw)
  To: netdev, David S. Miller, Jakub Kicinski, Paolo Abeni
  Cc: Eric Dumazet, Simon Horman, Willem de Bruijn, Michael S. Tsirkin,
	Jason Wang, Eugenio Pérez, Hassan Sultan, virtualization

With IFF_NAPI_FRAGS, tun_napi_alloc_frags() gets its skb from
napi_get_frags(). If GRO merged the previous frame with
GRO_MERGED_FREE, that skb was recycled by napi_reuse_skb(), which does
not reset skb->protocol set by napi_frags_skb() for the previous frame.

On the IFF_TAP path, tun_get_user() calls tun_vnet_hdr_tnl_to_skb()
before eth_type_trans(), so the virtio header checks see the protocol
of the previous frame: the outer protocol check of UDP tunnel GSO
packets rejects a valid frame when the two ethertypes differ, and
GSO packets without VIRTIO_NET_HDR_F_NEEDS_CSUM are dissected with
the wrong protocol.

This needs GRO to hold packets across writes, for example with
napi_defer_hard_irqs and gro_flush_timeout set on the device.

Clear skb->protocol for IFF_TAP frames, as for skbs allocated by
tun_alloc_skb(). eth_type_trans() sets it right after.

Fixes: 90e33d459407 ("tun: enable napi_gro_frags() for TUN/TAP driver")
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/netdev/179386753589.488856.11455181721004759454@kernel.org/
Signed-off-by: Eric Dumazet <edumazet@kernel.org>
---
 drivers/net/tun.c | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/drivers/net/tun.c b/drivers/net/tun.c
index 3920b3dff1e8..6b648f21018c 100644
--- a/drivers/net/tun.c
+++ b/drivers/net/tun.c
@@ -1941,6 +1941,11 @@ static ssize_t tun_get_user(struct tun_struct *tun, struct tun_file *tfile,
 			goto drop;
 		}
 		skb_set_network_header(skb, ETH_HLEN);
+		/* A NAPI frags skb recycled by napi_reuse_skb() keeps the
+		 * protocol of a previous frame. eth_type_trans() sets it
+		 * below, the virtio header checks must not see a stale one.
+		 */
+		skb->protocol = 0;
 		break;
 	}
 
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH net v3 4/8] selftests: net: tuntap: fix UDP checksum length in build_udp_packet_csum()
  2026-11-05 11:57 [PATCH net v3 0/8] net: virtio: validate outer protocol of UDP tunnel GSO packets Eric Dumazet
                   ` (2 preceding siblings ...)
  2026-11-05 11:57 ` [PATCH net v3 3/8] tun: clear stale skb->protocol of NAPI frags skbs Eric Dumazet
@ 2026-11-05 11:57 ` Eric Dumazet
  2026-11-05 11:57 ` [PATCH net v3 5/8] selftests: net: tun: test outer protocol of UDP tunnel GSO packets Eric Dumazet
                   ` (3 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: Eric Dumazet @ 2026-11-05 11:57 UTC (permalink / raw)
  To: netdev, David S. Miller, Jakub Kicinski, Paolo Abeni
  Cc: Eric Dumazet, Simon Horman, Willem de Bruijn, Michael S. Tsirkin,
	Jason Wang, Eugenio Pérez, Hassan Sultan, virtualization

build_udp_packet_csum() passes udph->len, which is in network byte
order, as the number of bytes to checksum. On little-endian hosts this
reads up to 64KB past the UDP header and computes a wrong checksum.

Existing callers did not crash only because they use packet buffers
of MAX_VNET_TUNNEL_PACKET_SZ bytes.

Use ntohs(udph->len), as tap.c does in its own build_udp_packet().

While at it, clear udph->check before summing instead of relying on
the caller, and send 0xffff instead of a computed zero checksum, as
the kernel does with CSUM_MANGLED_0.

Fixes: a942fcd72e97 ("selftest: tun: Introduce tuntap_helpers.h header for TUN/TAP testing")
Reviewed-by: Willem de Bruijn <willemb@google.com>
Signed-off-by: Eric Dumazet <edumazet@kernel.org>
---
 tools/testing/selftests/net/tuntap_helpers.h | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/tools/testing/selftests/net/tuntap_helpers.h b/tools/testing/selftests/net/tuntap_helpers.h
index d6c0437136ec..ecad83fae158 100644
--- a/tools/testing/selftests/net/tuntap_helpers.h
+++ b/tools/testing/selftests/net/tuntap_helpers.h
@@ -330,14 +330,22 @@ static inline void build_udp_packet_csum(uint8_t *buf, int family,
 	size_t ipalen = ip_addr_len(family);
 	uint32_t sum;
 
+	udph->check = 0;
+
 	/* No extension IPv4 and IPv6 headers addresses are the last fields */
 	sum = add_csum(buf - 2 * ipalen, 2 * ipalen);
 	sum += htons(IPPROTO_UDP) + udph->len;
 
 	if (!csum_off)
-		sum += add_csum(buf, udph->len);
+		sum += add_csum(buf, ntohs(udph->len));
 
 	udph->check = finish_ip_csum(sum);
+
+	/* A zero UDP checksum means no checksum: send 0xffff instead,
+	 * as the kernel does with CSUM_MANGLED_0.
+	 */
+	if (!csum_off && !udph->check)
+		udph->check = 0xffff;
 }
 
 static inline int build_udp_packet(uint8_t *buf, uint16_t sport, uint16_t dport,
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH net v3 5/8] selftests: net: tun: test outer protocol of UDP tunnel GSO packets
  2026-11-05 11:57 [PATCH net v3 0/8] net: virtio: validate outer protocol of UDP tunnel GSO packets Eric Dumazet
                   ` (3 preceding siblings ...)
  2026-11-05 11:57 ` [PATCH net v3 4/8] selftests: net: tuntap: fix UDP checksum length in build_udp_packet_csum() Eric Dumazet
@ 2026-11-05 11:57 ` Eric Dumazet
  2026-11-05 11:57 ` [PATCH net v3 6/8] selftests: net: tun: test UDP tunnel GSO on retyped devices Eric Dumazet
                   ` (2 subsequent siblings)
  7 siblings, 0 replies; 9+ messages in thread
From: Eric Dumazet @ 2026-11-05 11:57 UTC (permalink / raw)
  To: netdev, David S. Miller, Jakub Kicinski, Paolo Abeni
  Cc: Eric Dumazet, Simon Horman, Willem de Bruijn, Michael S. Tsirkin,
	Jason Wang, Eugenio Pérez, Hassan Sultan, virtualization

Add tests writing single 4in4 Geneve GSO frames to tun devices with
UDP tunnel offloads enabled.

tun_vnet_udptnl_proto uses a tap device:

- outer_proto_ok: an outer IPv4 header, with or without an 802.1Q tag,
  is accepted.
- outer_proto_bad: an outer ethertype that does not match the declared
  tunnel type (IPv6 on an IPv4 tunnel, or NSH with or without a VLAN
  tag) is rejected with EINVAL.

tun_vnet_udptnl_proto_l3 uses an IFF_TUN device with a tun_pi header,
so skb->protocol comes from userspace and there is no Ethernet header.
The outer IPv4 header carries options, as virtio_net_hdr_tnl_to_skb()
expects room for an Ethernet header before the outer transport header.

- outer_proto_ok: tun_pi and the frame agree on IPv4, accepted.
- outer_proto_bad: tun_pi claims IPv6, or the IP version of the frame
  is not 4 while tun_pi claims IPv4: rejected with EINVAL.

Without the first patch of this series, both outer_proto_bad tests fail
because the kernel accepts the frames.

Reviewed-by: Willem de Bruijn <willemb@google.com>
Signed-off-by: Eric Dumazet <edumazet@kernel.org>
---
 tools/testing/selftests/net/tun.c | 269 ++++++++++++++++++++++++++++++
 1 file changed, 269 insertions(+)

diff --git a/tools/testing/selftests/net/tun.c b/tools/testing/selftests/net/tun.c
index 34de2580e32e..81410afbbcc8 100644
--- a/tools/testing/selftests/net/tun.c
+++ b/tools/testing/selftests/net/tun.c
@@ -683,6 +683,275 @@ TEST_F(tun_vnet_gso, vlan_tcpv4_gso_no_csum_64k)
 	ASSERT_EQ(ret, len);
 }
 
+#ifndef ETH_P_NSH
+#define ETH_P_NSH 0x894F
+#endif
+
+FIXTURE(tun_vnet_udptnl_proto)
+{
+	char ifname[IFNAMSIZ];
+	uint8_t *pkt;
+	int fd;
+};
+
+FIXTURE_SETUP(tun_vnet_udptnl_proto)
+{
+	int flags = IFF_TAP | IFF_NO_PI | IFF_VNET_HDR;
+	int features = TUN_F_CSUM | TUN_F_UDP_TUNNEL_GSO |
+		       TUN_F_UDP_TUNNEL_GSO_CSUM | TUN_F_USO4 | TUN_F_USO6;
+
+	self->fd = -1;
+	if (unshare(CLONE_NEWNET))
+		SKIP(return, "Cannot create network namespace: %s",
+		     strerror(errno));
+
+	self->pkt = calloc(1, MAX_VNET_TUNNEL_PACKET_SZ);
+	ASSERT_NE(self->pkt, NULL);
+
+	memset(self->ifname, 0, sizeof(self->ifname));
+	self->fd = tun_open(self->ifname, flags, TUN_VNET_TNL_SIZE, features,
+			    param_hwaddr_outer_src);
+	ASSERT_GE(self->fd, 0);
+}
+
+FIXTURE_TEARDOWN(tun_vnet_udptnl_proto)
+{
+	if (self->fd >= 0)
+		close(self->fd);
+	free(self->pkt);
+}
+
+/* Build a 4in4 Geneve GSO frame whose outer ethertype is @l3proto,
+ * optionally behind an 802.1Q tag.
+ */
+static int build_udptnl_frame(uint8_t *buf, bool vlan, uint16_t l3proto,
+			      int payload_len)
+{
+	struct virtio_net_hdr_v1_hash_tunnel *vh = (void *)buf;
+	int vlan_len = vlan ? 4 : 0;
+	int hlen = ETH_HLEN + vlan_len + UDP_TUNNEL_GENEVE_4IN4_HDRLEN;
+	uint8_t *outer_udph, *cur = buf;
+	int pktlen;
+
+	/* The buffer is reused: do not leave bytes from a previous frame. */
+	memset(buf, 0, TUN_VNET_TNL_SIZE + hlen + payload_len);
+
+	cur += build_virtio_net_hdr_v1_hash_tunnel(cur, true, hlen, 1400,
+						   AF_INET, AF_INET);
+	vh->outer_th_offset += vlan_len;
+	vh->inner_nh_offset += vlan_len;
+	vh->hash_hdr.hdr.csum_start += vlan_len;
+
+	if (vlan) {
+		uint16_t vlan_tag[2] = { htons(100), htons(l3proto) };
+
+		cur += build_eth(cur, ETH_P_8021Q, param_hwaddr_outer_dst,
+				 param_hwaddr_outer_src);
+		memcpy(cur, vlan_tag, sizeof(vlan_tag));
+		cur += sizeof(vlan_tag);
+	} else {
+		cur += build_eth(cur, l3proto, param_hwaddr_outer_dst,
+				 param_hwaddr_outer_src);
+	}
+
+	pktlen = UDP_TUNNEL_GENEVE_4IN4_HDRLEN - sizeof(struct iphdr) +
+		 payload_len;
+	cur += build_ipv4_header(cur, IPPROTO_UDP, pktlen,
+				 &param_ipaddr4_outer_dst,
+				 &param_ipaddr4_outer_src);
+
+	outer_udph = cur;
+	cur += build_udp_header(cur, UDP_SRC_PORT, VN_PORT,
+				pktlen - sizeof(struct udphdr));
+	cur += build_geneve_header(cur, VN_ID);
+	cur += build_eth(cur, ETH_P_IP, param_hwaddr_inner_dst,
+			 param_hwaddr_inner_src);
+	cur += build_ipv4_header(cur, IPPROTO_UDP,
+				 sizeof(struct udphdr) + payload_len,
+				 &param_ipaddr4_inner_dst,
+				 &param_ipaddr4_inner_src);
+	cur += build_udp_packet(cur, UDP_DST_PORT, UDP_SRC_PORT, payload_len,
+				AF_INET, false);
+	build_udp_packet_csum(outer_udph, AF_INET, false);
+
+	return cur - buf;
+}
+
+TEST_F(tun_vnet_udptnl_proto, outer_proto_ok)
+{
+	uint8_t *pkt = self->pkt;
+	int len, ret;
+
+	len = build_udptnl_frame(pkt, false, ETH_P_IP, 2800);
+	ret = write(self->fd, pkt, len);
+	ASSERT_EQ(ret, len);
+
+	/* The outer IPv4 header can follow a VLAN tag. */
+	len = build_udptnl_frame(pkt, true, ETH_P_IP, 2800);
+	ret = write(self->fd, pkt, len);
+	ASSERT_EQ(ret, len);
+}
+
+TEST_F(tun_vnet_udptnl_proto, outer_proto_bad)
+{
+	uint8_t *pkt = self->pkt;
+	int len, ret;
+
+	/* Outer ethertype does not match the declared IPv4 tunnel. */
+	len = build_udptnl_frame(pkt, false, ETH_P_IPV6, 2800);
+	ret = write(self->fd, pkt, len);
+	ASSERT_EQ(ret, -1);
+	ASSERT_EQ(errno, EINVAL);
+
+	/* Outer header is not IP at all. */
+	len = build_udptnl_frame(pkt, false, ETH_P_NSH, 2800);
+	ret = write(self->fd, pkt, len);
+	ASSERT_EQ(ret, -1);
+	ASSERT_EQ(errno, EINVAL);
+
+	/* Same, behind a VLAN tag. */
+	len = build_udptnl_frame(pkt, true, ETH_P_NSH, 2800);
+	ret = write(self->fd, pkt, len);
+	ASSERT_EQ(ret, -1);
+	ASSERT_EQ(errno, EINVAL);
+}
+
+/* IFF_TUN device with a tun_pi header: skb->protocol comes from
+ * userspace, and there is no Ethernet header.
+ */
+FIXTURE(tun_vnet_udptnl_proto_l3)
+{
+	char ifname[IFNAMSIZ];
+	uint8_t *pkt;
+	int fd;
+};
+
+FIXTURE_SETUP(tun_vnet_udptnl_proto_l3)
+{
+	int flags = IFF_TUN | IFF_VNET_HDR;
+	int features = TUN_F_CSUM | TUN_F_UDP_TUNNEL_GSO |
+		       TUN_F_UDP_TUNNEL_GSO_CSUM | TUN_F_USO4 | TUN_F_USO6;
+
+	self->fd = -1;
+	if (unshare(CLONE_NEWNET))
+		SKIP(return, "Cannot create network namespace: %s",
+		     strerror(errno));
+
+	self->pkt = calloc(1, MAX_VNET_TUNNEL_PACKET_SZ);
+	ASSERT_NE(self->pkt, NULL);
+
+	memset(self->ifname, 0, sizeof(self->ifname));
+	self->fd = tun_open(self->ifname, flags, TUN_VNET_TNL_SIZE, features,
+			    NULL);
+	ASSERT_GE(self->fd, 0);
+}
+
+FIXTURE_TEARDOWN(tun_vnet_udptnl_proto_l3)
+{
+	if (self->fd >= 0)
+		close(self->fd);
+	free(self->pkt);
+}
+
+#define IPV4_OPT_NOP 1
+#define IPV4_OPT_LEN 16
+
+/* Build a tun_pi header carrying @pi_proto, followed by a 4in4 Geneve
+ * GSO frame whose outer IP header claims version @ip_version.
+ *
+ * virtio_net_hdr_tnl_to_skb() wants room for an Ethernet header before
+ * the outer transport header, even without one: pad the outer IPv4
+ * header with NOP options.
+ */
+static int build_udptnl_l3_frame(uint8_t *buf, uint16_t pi_proto,
+				 int ip_version, int payload_len)
+{
+	struct tun_pi *pi = (struct tun_pi *)buf;
+	uint8_t *vnet = buf + sizeof(*pi);
+	struct virtio_net_hdr_v1_hash_tunnel *vh = (void *)vnet;
+	int hlen = UDP_TUNNEL_GENEVE_4IN4_HDRLEN + IPV4_OPT_LEN;
+	uint8_t *cur, *outer_iph, *outer_udph;
+	struct iphdr *iph;
+	int pktlen;
+
+	/* The buffer is reused: do not leave bytes from a previous frame. */
+	memset(buf, 0, sizeof(*pi) + TUN_VNET_TNL_SIZE + hlen + payload_len);
+	pi->proto = htons(pi_proto);
+
+	cur = vnet + build_virtio_net_hdr_v1_hash_tunnel(vnet, false, hlen,
+							 1400, AF_INET,
+							 AF_INET);
+	vh->outer_th_offset += IPV4_OPT_LEN;
+	vh->inner_nh_offset += IPV4_OPT_LEN;
+	vh->hash_hdr.hdr.csum_start += IPV4_OPT_LEN;
+
+	/* Build the frame without options first: build_udp_packet_csum()
+	 * expects the IP addresses right before the UDP header.
+	 */
+	outer_iph = cur;
+	pktlen = UDP_TUNNEL_GENEVE_4IN4_HDRLEN - sizeof(struct iphdr) +
+		 payload_len;
+	cur += build_ipv4_header(cur, IPPROTO_UDP, pktlen,
+				 &param_ipaddr4_outer_dst,
+				 &param_ipaddr4_outer_src);
+
+	outer_udph = cur;
+	cur += build_udp_header(cur, UDP_SRC_PORT, VN_PORT,
+				pktlen - sizeof(struct udphdr));
+	cur += build_geneve_header(cur, VN_ID);
+	cur += build_eth(cur, ETH_P_IP, param_hwaddr_inner_dst,
+			 param_hwaddr_inner_src);
+	cur += build_ipv4_header(cur, IPPROTO_UDP,
+				 sizeof(struct udphdr) + payload_len,
+				 &param_ipaddr4_inner_dst,
+				 &param_ipaddr4_inner_src);
+	cur += build_udp_packet(cur, UDP_DST_PORT, UDP_SRC_PORT, payload_len,
+				AF_INET, false);
+	build_udp_packet_csum(outer_udph, AF_INET, false);
+
+	/* Then insert the options after the fixed outer IPv4 header. */
+	memmove(outer_udph + IPV4_OPT_LEN, outer_udph, cur - outer_udph);
+	memset(outer_udph, IPV4_OPT_NOP, IPV4_OPT_LEN);
+	cur += IPV4_OPT_LEN;
+
+	iph = (struct iphdr *)outer_iph;
+	iph->version = ip_version;
+	iph->ihl += IPV4_OPT_LEN / 4;
+	iph->tot_len = htons(ntohs(iph->tot_len) + IPV4_OPT_LEN);
+	iph->check = 0;
+	iph->check = build_ip_csum(outer_iph, iph->ihl << 2, 0);
+
+	return cur - buf;
+}
+
+TEST_F(tun_vnet_udptnl_proto_l3, outer_proto_ok)
+{
+	uint8_t *pkt = self->pkt;
+	int len, ret;
+
+	len = build_udptnl_l3_frame(pkt, ETH_P_IP, 4, 2800);
+	ret = write(self->fd, pkt, len);
+	ASSERT_EQ(ret, len);
+}
+
+TEST_F(tun_vnet_udptnl_proto_l3, outer_proto_bad)
+{
+	uint8_t *pkt = self->pkt;
+	int len, ret;
+
+	/* tun_pi protocol does not match the declared IPv4 tunnel. */
+	len = build_udptnl_l3_frame(pkt, ETH_P_IPV6, 4, 2800);
+	ret = write(self->fd, pkt, len);
+	ASSERT_EQ(ret, -1);
+	ASSERT_EQ(errno, EINVAL);
+
+	/* tun_pi claims IPv4, but the frame says otherwise. */
+	len = build_udptnl_l3_frame(pkt, ETH_P_IP, 6, 2800);
+	ret = write(self->fd, pkt, len);
+	ASSERT_EQ(ret, -1);
+	ASSERT_EQ(errno, EINVAL);
+}
+
 FIXTURE(tun_vnet_udptnl)
 {
 	char ifname[IFNAMSIZ];
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH net v3 6/8] selftests: net: tun: test UDP tunnel GSO on retyped devices
  2026-11-05 11:57 [PATCH net v3 0/8] net: virtio: validate outer protocol of UDP tunnel GSO packets Eric Dumazet
                   ` (4 preceding siblings ...)
  2026-11-05 11:57 ` [PATCH net v3 5/8] selftests: net: tun: test outer protocol of UDP tunnel GSO packets Eric Dumazet
@ 2026-11-05 11:57 ` Eric Dumazet
  2026-11-05 11:57 ` [PATCH net v3 7/8] selftests: net: tun: test more outer header mismatches of UDP tunnel GSO Eric Dumazet
  2026-11-05 11:57 ` [PATCH net v3 8/8] selftests: net: tuntap: write UDP tunnel offsets in little endian Eric Dumazet
  7 siblings, 0 replies; 9+ messages in thread
From: Eric Dumazet @ 2026-11-05 11:57 UTC (permalink / raw)
  To: netdev, David S. Miller, Jakub Kicinski, Paolo Abeni
  Cc: Eric Dumazet, Simon Horman, Willem de Bruijn, Michael S. Tsirkin,
	Jason Wang, Eugenio Pérez, Hassan Sultan, virtualization

Use TUNSETLINK to make the device type disagree with the tun/tap
framing, and check that the outer headers of UDP tunnel GSO frames are
still validated according to the framing:

- tun_vnet_udptnl_proto.outer_proto_link_type: tap device set to
  ARPHRD_NONE. Frames with an NSH outer ethertype, with or without a
  VLAN tag, are rejected. A valid outer IPv4 frame is accepted.
- tun_vnet_udptnl_proto_l3.outer_proto_link_type: IFF_TUN device set
  to ARPHRD_ETHER. An Ethernet frame behind the tun_pi header is
  rejected.

Without the patch "net: virtio: do not use dev->type to find the outer
L3 header", the frames expected to be rejected are accepted.

Signed-off-by: Eric Dumazet <edumazet@kernel.org>
---
 tools/testing/selftests/net/tun.c | 74 +++++++++++++++++++++++++++++++
 1 file changed, 74 insertions(+)

diff --git a/tools/testing/selftests/net/tun.c b/tools/testing/selftests/net/tun.c
index 81410afbbcc8..cfeca53863ce 100644
--- a/tools/testing/selftests/net/tun.c
+++ b/tools/testing/selftests/net/tun.c
@@ -816,6 +816,59 @@ TEST_F(tun_vnet_udptnl_proto, outer_proto_bad)
 	ASSERT_EQ(errno, EINVAL);
 }
 
+/* Change the link type of a tun device, which has to be down for it. */
+static int tun_set_link_type(int fd, const char *dev, int type)
+{
+	struct ifreq ifr = { 0 };
+	int sk, ret = -1;
+
+	sk = socket(PF_INET, SOCK_DGRAM, 0);
+	if (sk < 0)
+		return -1;
+
+	strncpy(ifr.ifr_name, dev, IFNAMSIZ - 1);
+	if (ioctl(sk, SIOCGIFFLAGS, &ifr) < 0)
+		goto out;
+	ifr.ifr_flags &= ~IFF_UP;
+	if (ioctl(sk, SIOCSIFFLAGS, &ifr) < 0)
+		goto out;
+	if (ioctl(fd, TUNSETLINK, type) < 0)
+		goto out;
+	ifr.ifr_flags |= IFF_UP;
+	if (ioctl(sk, SIOCSIFFLAGS, &ifr) < 0)
+		goto out;
+	ret = 0;
+out:
+	close(sk);
+	return ret;
+}
+
+TEST_F(tun_vnet_udptnl_proto, outer_proto_link_type)
+{
+	uint8_t *pkt = self->pkt;
+	int len, ret;
+
+	/* The frames still start with an Ethernet header, but the device
+	 * type no longer says so: the Ethernet header must still be
+	 * checked.
+	 */
+	ASSERT_EQ(tun_set_link_type(self->fd, self->ifname, ARPHRD_NONE), 0);
+
+	len = build_udptnl_frame(pkt, false, ETH_P_NSH, 2800);
+	ret = write(self->fd, pkt, len);
+	ASSERT_EQ(ret, -1);
+	ASSERT_EQ(errno, EINVAL);
+
+	len = build_udptnl_frame(pkt, true, ETH_P_NSH, 2800);
+	ret = write(self->fd, pkt, len);
+	ASSERT_EQ(ret, -1);
+	ASSERT_EQ(errno, EINVAL);
+
+	len = build_udptnl_frame(pkt, false, ETH_P_IP, 2800);
+	ret = write(self->fd, pkt, len);
+	ASSERT_EQ(ret, len);
+}
+
 /* IFF_TUN device with a tun_pi header: skb->protocol comes from
  * userspace, and there is no Ethernet header.
  */
@@ -952,6 +1005,27 @@ TEST_F(tun_vnet_udptnl_proto_l3, outer_proto_bad)
 	ASSERT_EQ(errno, EINVAL);
 }
 
+TEST_F(tun_vnet_udptnl_proto_l3, outer_proto_link_type)
+{
+	struct tun_pi *pi = (struct tun_pi *)self->pkt;
+	int len, ret;
+
+	/* The device type now claims an Ethernet header, but IFF_TUN
+	 * frames still start with the L3 header: an Ethernet frame
+	 * behind tun_pi must not pass for a valid tunnel packet.
+	 */
+	ASSERT_EQ(tun_set_link_type(self->fd, self->ifname, ARPHRD_ETHER), 0);
+
+	len = build_udptnl_frame(self->pkt + sizeof(*pi), false, ETH_P_IP,
+				 2800);
+	pi->flags = 0;
+	pi->proto = htons(ETH_P_IP);
+	len += sizeof(*pi);
+	ret = write(self->fd, self->pkt, len);
+	ASSERT_EQ(ret, -1);
+	ASSERT_EQ(errno, EINVAL);
+}
+
 FIXTURE(tun_vnet_udptnl)
 {
 	char ifname[IFNAMSIZ];
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH net v3 7/8] selftests: net: tun: test more outer header mismatches of UDP tunnel GSO
  2026-11-05 11:57 [PATCH net v3 0/8] net: virtio: validate outer protocol of UDP tunnel GSO packets Eric Dumazet
                   ` (5 preceding siblings ...)
  2026-11-05 11:57 ` [PATCH net v3 6/8] selftests: net: tun: test UDP tunnel GSO on retyped devices Eric Dumazet
@ 2026-11-05 11:57 ` Eric Dumazet
  2026-11-05 11:57 ` [PATCH net v3 8/8] selftests: net: tuntap: write UDP tunnel offsets in little endian Eric Dumazet
  7 siblings, 0 replies; 9+ messages in thread
From: Eric Dumazet @ 2026-11-05 11:57 UTC (permalink / raw)
  To: netdev, David S. Miller, Jakub Kicinski, Paolo Abeni
  Cc: Eric Dumazet, Simon Horman, Willem de Bruijn, Michael S. Tsirkin,
	Jason Wang, Eugenio Pérez, Hassan Sultan, virtualization

Add tap tests for the outer header checks of UDP tunnel GSO packets
not covered yet:

- outer_proto_ok_ipv6: a 4in6 frame with an outer IPv6 header is
  accepted.
- outer_proto_bad_family: an IPv4 ethertype on a declared IPv6 tunnel
  is rejected.
- outer_proto_bad_vlan_ipv6: an IPv6 ethertype behind a VLAN tag on a
  declared IPv4 tunnel is rejected.
- outer_proto_bad_vlan_offset: a VLAN tagged frame whose
  outer_th_offset ignores the tag is rejected.
- outer_proto_bad_l4: an outer IPv4 header carrying GRE instead of UDP
  is rejected.

Without the first patch of this series, the four outer_proto_bad
tests fail because the kernel accepts the frames.

Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/netdev/179386753718.488856.13433005125725728519@kernel.org/
Signed-off-by: Eric Dumazet <edumazet@kernel.org>
---
 tools/testing/selftests/net/tun.c | 97 +++++++++++++++++++++++++++++++
 1 file changed, 97 insertions(+)

diff --git a/tools/testing/selftests/net/tun.c b/tools/testing/selftests/net/tun.c
index cfeca53863ce..a135d3aa10d1 100644
--- a/tools/testing/selftests/net/tun.c
+++ b/tools/testing/selftests/net/tun.c
@@ -869,6 +869,103 @@ TEST_F(tun_vnet_udptnl_proto, outer_proto_link_type)
 	ASSERT_EQ(ret, len);
 }
 
+/* Build a 4in6 Geneve GSO frame whose outer ethertype is @l3proto. */
+static int build_udptnl6_frame(uint8_t *buf, uint16_t l3proto,
+			       int payload_len)
+{
+	int hlen = UDP_TUNNEL_GENEVE_4IN6_HDRLEN;
+	uint8_t *outer_udph, *cur = buf;
+	int pktlen;
+
+	memset(buf, 0, TUN_VNET_TNL_SIZE + hlen + payload_len);
+
+	cur += build_virtio_net_hdr_v1_hash_tunnel(cur, true, hlen, 1400,
+						   AF_INET6, AF_INET);
+	cur += build_eth(cur, l3proto, param_hwaddr_outer_dst,
+			 param_hwaddr_outer_src);
+
+	pktlen = UDP_TUNNEL_GENEVE_4IN6_HDRLEN - ETH_HLEN -
+		 sizeof(struct ipv6hdr) + payload_len;
+	cur += build_ipv6_header(cur, IPPROTO_UDP, 0, pktlen,
+				 &param_ipaddr6_outer_dst,
+				 &param_ipaddr6_outer_src);
+
+	outer_udph = cur;
+	cur += build_udp_header(cur, UDP_SRC_PORT, VN_PORT,
+				pktlen - sizeof(struct udphdr));
+	cur += build_geneve_header(cur, VN_ID);
+	cur += build_eth(cur, ETH_P_IP, param_hwaddr_inner_dst,
+			 param_hwaddr_inner_src);
+	cur += build_ipv4_header(cur, IPPROTO_UDP,
+				 sizeof(struct udphdr) + payload_len,
+				 &param_ipaddr4_inner_dst,
+				 &param_ipaddr4_inner_src);
+	cur += build_udp_packet(cur, UDP_DST_PORT, UDP_SRC_PORT, payload_len,
+				AF_INET, false);
+	build_udp_packet_csum(outer_udph, AF_INET6, false);
+
+	return cur - buf;
+}
+
+TEST_F(tun_vnet_udptnl_proto, outer_proto_ok_ipv6)
+{
+	int len, ret;
+
+	len = build_udptnl6_frame(self->pkt, ETH_P_IPV6, 2800);
+	ret = write(self->fd, self->pkt, len);
+	ASSERT_EQ(ret, len);
+}
+
+/* An IPv4 ethertype on a declared IPv6 tunnel. */
+TEST_F(tun_vnet_udptnl_proto, outer_proto_bad_family)
+{
+	int len, ret;
+
+	len = build_udptnl6_frame(self->pkt, ETH_P_IP, 2800);
+	ret = write(self->fd, self->pkt, len);
+	ASSERT_EQ(ret, -1);
+	ASSERT_EQ(errno, EINVAL);
+}
+
+TEST_F(tun_vnet_udptnl_proto, outer_proto_bad_vlan_ipv6)
+{
+	int len, ret;
+
+	len = build_udptnl_frame(self->pkt, true, ETH_P_IPV6, 2800);
+	ret = write(self->fd, self->pkt, len);
+	ASSERT_EQ(ret, -1);
+	ASSERT_EQ(errno, EINVAL);
+}
+
+/* outer_th_offset does not account for the VLAN tag. */
+TEST_F(tun_vnet_udptnl_proto, outer_proto_bad_vlan_offset)
+{
+	struct virtio_net_hdr_v1_hash_tunnel *vh = (void *)self->pkt;
+	int len, ret;
+
+	len = build_udptnl_frame(self->pkt, true, ETH_P_IP, 2800);
+	vh->outer_th_offset -= 4;
+	ret = write(self->fd, self->pkt, len);
+	ASSERT_EQ(ret, -1);
+	ASSERT_EQ(errno, EINVAL);
+}
+
+/* The outer IPv4 header does not carry UDP. */
+TEST_F(tun_vnet_udptnl_proto, outer_proto_bad_l4)
+{
+	struct iphdr *iph;
+	int len, ret;
+
+	len = build_udptnl_frame(self->pkt, false, ETH_P_IP, 2800);
+	iph = (struct iphdr *)(self->pkt + TUN_VNET_TNL_SIZE + ETH_HLEN);
+	iph->protocol = IPPROTO_GRE;
+	iph->check = 0;
+	iph->check = build_ip_csum((uint8_t *)iph, iph->ihl << 2, 0);
+	ret = write(self->fd, self->pkt, len);
+	ASSERT_EQ(ret, -1);
+	ASSERT_EQ(errno, EINVAL);
+}
+
 /* IFF_TUN device with a tun_pi header: skb->protocol comes from
  * userspace, and there is no Ethernet header.
  */
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* [PATCH net v3 8/8] selftests: net: tuntap: write UDP tunnel offsets in little endian
  2026-11-05 11:57 [PATCH net v3 0/8] net: virtio: validate outer protocol of UDP tunnel GSO packets Eric Dumazet
                   ` (6 preceding siblings ...)
  2026-11-05 11:57 ` [PATCH net v3 7/8] selftests: net: tun: test more outer header mismatches of UDP tunnel GSO Eric Dumazet
@ 2026-11-05 11:57 ` Eric Dumazet
  7 siblings, 0 replies; 9+ messages in thread
From: Eric Dumazet @ 2026-11-05 11:57 UTC (permalink / raw)
  To: netdev, David S. Miller, Jakub Kicinski, Paolo Abeni
  Cc: Eric Dumazet, Simon Horman, Willem de Bruijn, Michael S. Tsirkin,
	Jason Wang, Eugenio Pérez, Hassan Sultan, virtualization

outer_th_offset and inner_nh_offset of struct
virtio_net_hdr_v1_hash_tunnel are __le16, and the kernel always reads
them with le16_to_cpu(). build_virtio_net_hdr_v1_hash_tunnel() writes
them in host order, and tun.c adjusts them with native arithmetic, so
all UDP tunnel GSO tests fail on big-endian hosts: the kernel sees
offsets such as 0x2200 and rejects the frames before the checks the
tests target.

Write the offsets with htole16(), and add vnet_tnl_offset_add() to
adjust them. csum_start stays in host order: it follows the tun vnet
header endianness, which the tests do not change.

Fixes: a942fcd72e97 ("selftest: tun: Introduce tuntap_helpers.h header for TUN/TAP testing")
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/netdev/179387332145.488856.1149073246990672028@kernel.org/
Signed-off-by: Eric Dumazet <edumazet@kernel.org>
---
 tools/testing/selftests/net/tun.c            | 10 +++++-----
 tools/testing/selftests/net/tuntap_helpers.h | 19 +++++++++++++++----
 2 files changed, 20 insertions(+), 9 deletions(-)

diff --git a/tools/testing/selftests/net/tun.c b/tools/testing/selftests/net/tun.c
index a135d3aa10d1..5f797a1bc604 100644
--- a/tools/testing/selftests/net/tun.c
+++ b/tools/testing/selftests/net/tun.c
@@ -738,8 +738,8 @@ static int build_udptnl_frame(uint8_t *buf, bool vlan, uint16_t l3proto,
 
 	cur += build_virtio_net_hdr_v1_hash_tunnel(cur, true, hlen, 1400,
 						   AF_INET, AF_INET);
-	vh->outer_th_offset += vlan_len;
-	vh->inner_nh_offset += vlan_len;
+	vnet_tnl_offset_add(&vh->outer_th_offset, vlan_len);
+	vnet_tnl_offset_add(&vh->inner_nh_offset, vlan_len);
 	vh->hash_hdr.hdr.csum_start += vlan_len;
 
 	if (vlan) {
@@ -944,7 +944,7 @@ TEST_F(tun_vnet_udptnl_proto, outer_proto_bad_vlan_offset)
 	int len, ret;
 
 	len = build_udptnl_frame(self->pkt, true, ETH_P_IP, 2800);
-	vh->outer_th_offset -= 4;
+	vnet_tnl_offset_add(&vh->outer_th_offset, -4);
 	ret = write(self->fd, self->pkt, len);
 	ASSERT_EQ(ret, -1);
 	ASSERT_EQ(errno, EINVAL);
@@ -1031,8 +1031,8 @@ static int build_udptnl_l3_frame(uint8_t *buf, uint16_t pi_proto,
 	cur = vnet + build_virtio_net_hdr_v1_hash_tunnel(vnet, false, hlen,
 							 1400, AF_INET,
 							 AF_INET);
-	vh->outer_th_offset += IPV4_OPT_LEN;
-	vh->inner_nh_offset += IPV4_OPT_LEN;
+	vnet_tnl_offset_add(&vh->outer_th_offset, IPV4_OPT_LEN);
+	vnet_tnl_offset_add(&vh->inner_nh_offset, IPV4_OPT_LEN);
 	vh->hash_hdr.hdr.csum_start += IPV4_OPT_LEN;
 
 	/* Build the frame without options first: build_udp_packet_csum()
diff --git a/tools/testing/selftests/net/tuntap_helpers.h b/tools/testing/selftests/net/tuntap_helpers.h
index ecad83fae158..dc85cd9bc0ae 100644
--- a/tools/testing/selftests/net/tuntap_helpers.h
+++ b/tools/testing/selftests/net/tuntap_helpers.h
@@ -3,6 +3,7 @@
 #ifndef _TUNTAP_HELPERS_H
 #define _TUNTAP_HELPERS_H
 
+#include <endian.h>
 #include <errno.h>
 #include <linux/if_packet.h>
 #include <linux/ipv6.h>
@@ -368,6 +369,7 @@ static inline int build_virtio_net_hdr_v1_hash_tunnel(uint8_t *buf, bool is_tap,
 	struct virtio_net_hdr_v1_hash_tunnel *vh_tunnel = (void *)buf;
 	struct virtio_net_hdr_v1 *vh = &vh_tunnel->hash_hdr.hdr;
 	int outer_iphlen, inner_iphlen, eth_hlen, gso_type;
+	int outer_th, inner_nh;
 
 	eth_hlen = is_tap ? ETH_HLEN : 0;
 	outer_iphlen = (outer_family == AF_INET) ? sizeof(struct iphdr) :
@@ -375,11 +377,14 @@ static inline int build_virtio_net_hdr_v1_hash_tunnel(uint8_t *buf, bool is_tap,
 	inner_iphlen = (inner_family == AF_INET) ? sizeof(struct iphdr) :
 						   sizeof(struct ipv6hdr);
 
-	vh_tunnel->outer_th_offset = eth_hlen + outer_iphlen;
-	vh_tunnel->inner_nh_offset = vh_tunnel->outer_th_offset + ETH_HLEN +
-				     GENEVE_HLEN + sizeof(struct udphdr);
+	outer_th = eth_hlen + outer_iphlen;
+	inner_nh = outer_th + ETH_HLEN + GENEVE_HLEN + sizeof(struct udphdr);
 
-	vh->csum_start = vh_tunnel->inner_nh_offset + inner_iphlen;
+	/* The tunnel offsets are always little endian. */
+	vh_tunnel->outer_th_offset = htole16(outer_th);
+	vh_tunnel->inner_nh_offset = htole16(inner_nh);
+
+	vh->csum_start = inner_nh + inner_iphlen;
 	vh->csum_offset = __builtin_offsetof(struct udphdr, check);
 	vh->flags = VIRTIO_NET_HDR_F_NEEDS_CSUM;
 	vh->hdr_len = hdr_len;
@@ -395,4 +400,10 @@ static inline int build_virtio_net_hdr_v1_hash_tunnel(uint8_t *buf, bool is_tap,
 	return sizeof(struct virtio_net_hdr_v1_hash_tunnel);
 }
 
+/* Add @delta to a little endian tunnel offset of the virtio header. */
+static inline void vnet_tnl_offset_add(__le16 *off, int delta)
+{
+	*off = htole16(le16toh(*off) + delta);
+}
+
 #endif /* _TUNTAP_HELPERS_H */
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2026-11-05 11:57 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-05 11:57 [PATCH net v3 0/8] net: virtio: validate outer protocol of UDP tunnel GSO packets Eric Dumazet
2026-11-05 11:57 ` [PATCH net v3 1/8] " Eric Dumazet
2026-11-05 11:57 ` [PATCH net v3 2/8] net: virtio: do not use dev->type to find the outer L3 header Eric Dumazet
2026-11-05 11:57 ` [PATCH net v3 3/8] tun: clear stale skb->protocol of NAPI frags skbs Eric Dumazet
2026-11-05 11:57 ` [PATCH net v3 4/8] selftests: net: tuntap: fix UDP checksum length in build_udp_packet_csum() Eric Dumazet
2026-11-05 11:57 ` [PATCH net v3 5/8] selftests: net: tun: test outer protocol of UDP tunnel GSO packets Eric Dumazet
2026-11-05 11:57 ` [PATCH net v3 6/8] selftests: net: tun: test UDP tunnel GSO on retyped devices Eric Dumazet
2026-11-05 11:57 ` [PATCH net v3 7/8] selftests: net: tun: test more outer header mismatches of UDP tunnel GSO Eric Dumazet
2026-11-05 11:57 ` [PATCH net v3 8/8] selftests: net: tuntap: write UDP tunnel offsets in little endian Eric Dumazet

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox