summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakub Kicinski <kuba@kernel.org>2026-11-12 10:38:03 -0800
committerJakub Kicinski <kuba@kernel.org>2026-11-12 10:38:03 -0800
commit7683df331d296819a8f596dd752bbf95aa2e3478 (patch)
tree473f39f0573a1a63dda53c329a41ee78a6c02753
parentbf83b257423bb557b970df3d7ece32fbd85f3bd4 (diff)
parent73e08d50eb139e7329032c89a4cb105cf43ef278 (diff)
downloadlinux-7683df331d296819a8f596dd752bbf95aa2e3478.tar.gz
linux-7683df331d296819a8f596dd752bbf95aa2e3478.zip
Merge branch 'net-virtio-validate-outer-protocol-of-udp-tunnel-gso-packets'
Eric Dumazet says: ==================== net: virtio: validate outer protocol of UDP tunnel GSO packets 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 fixes the producer side, virtio_net_hdr_tnl_from_skb(), for VLAN tagged packets: - the tunnel offsets did not account for the VLAN tag tun inserts in the frame, and were 4 bytes short. - the outer family came from skb->protocol, which is the VLAN protocol when the tag is in the packet data, so an outer IPv6 header was reported as IPv4. Without it, patch 3 would drop such packets. Kernels without this fix still send such headers, so it should go to stable along with patch 3. Patch 2 clears a stale skb->protocol left on recycled NAPI frags skbs, which could make the checks of patch 3 reject valid frames. Patch 3 validates the outer headers, using the parsing helpers from Michael's series "net: resolve uncommon partial checksums at ingress", as Willem suggested. The callers tell whether frames start with an Ethernet header, as the device type of a tun device can be changed with TUNSETLINK. The rest of Michael's series (checksum validation and completion) is left to him. The selftests of v3 will follow in a separate series, with the fixes Sashiko found in them. ==================== Link: https://patch.msgid.link/20261110213628.3476051-1-edumazet@kernel.org Signed-off-by: Jakub Kicinski <kuba@kernel.org>
-rw-r--r--drivers/net/tun.c15
-rw-r--r--drivers/net/tun_vnet.h5
-rw-r--r--drivers/net/virtio_net.c2
-rw-r--r--include/linux/virtio_net.h181
4 files changed, 195 insertions, 8 deletions
diff --git a/drivers/net/tun.c b/drivers/net/tun.c
index ab1b91a9a99e..86fd44e10755 100644
--- a/drivers/net/tun.c
+++ b/drivers/net/tun.c
@@ -1500,6 +1500,12 @@ static struct sk_buff *tun_napi_alloc_frags(struct tun_file *tfile,
if (!skb)
return ERR_PTR(-ENOMEM);
+ /* An skb recycled by napi_reuse_skb() keeps the protocol of a
+ * previous frame. eth_type_trans() sets it after the virtio
+ * header checks, which must not see a stale one.
+ */
+ skb->protocol = 0;
+
linear = iov_iter_single_seg_count(it);
err = __skb_grow(skb, linear);
if (err)
@@ -1944,7 +1950,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 +2628,11 @@ build:
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 d6466f96cdd0..fc9667176f23 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,145 @@ 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?
+ * 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, bool has_eth, int *nhoff,
+ __be16 *proto)
+{
+ if (has_eth) {
+ __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,9 +424,33 @@ 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 has_eth, bool outer_isv6,
+ unsigned int outer_th)
+{
+ int nhoff;
+ __be16 proto;
+ unsigned int thoff;
+ int ip_proto;
+
+ if (!virtio_net_l3_start(skb, has_eth, &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,
+ bool has_eth,
bool tnl_hdr_negotiated,
bool tnl_csum_negotiated,
bool little_endian)
@@ -338,6 +504,9 @@ virtio_net_hdr_tnl_to_skb(struct sk_buff *skb,
if (ret)
return ret;
+ 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
* `inner_isv6` is just a guess, additional parsing is needed.
* The previous validation ensures that accessing an ipv4 inner
@@ -453,7 +622,10 @@ virtio_net_hdr_tnl_from_skb(const struct sk_buff *skb,
if (feature_hdrlen && hdr->hdr_len)
__virtio_net_set_tnl_hdrlen(skb, hdr);
- if (skb->protocol == htons(ETH_P_IPV6))
+ /* skb->protocol is the VLAN protocol when the tag is in the
+ * packet data.
+ */
+ if (vlan_get_protocol(skb) == htons(ETH_P_IPV6))
hdr->gso_type |= VIRTIO_NET_HDR_GSO_UDP_TUNNEL_IPV6;
else
hdr->gso_type |= VIRTIO_NET_HDR_GSO_UDP_TUNNEL_IPV4;
@@ -461,8 +633,11 @@ virtio_net_hdr_tnl_from_skb(const struct sk_buff *skb,
if (skb_shinfo(skb)->gso_type & SKB_GSO_UDP_TUNNEL_CSUM)
hdr->flags |= VIRTIO_NET_HDR_F_UDP_TUNNEL_CSUM;
- inner_nh = skb->inner_network_header - skb_headroom(skb);
- outer_th = skb->transport_header - skb_headroom(skb);
+ /* Like csum_start, the offsets must account for a VLAN tag
+ * the caller inserts in the frame.
+ */
+ inner_nh = skb->inner_network_header - skb_headroom(skb) + vlan_hlen;
+ outer_th = skb->transport_header - skb_headroom(skb) + vlan_hlen;
vhdr->inner_nh_offset = cpu_to_le16(inner_nh);
vhdr->outer_th_offset = cpu_to_le16(outer_th);
return 0;