diff options
| author | Jakub Kicinski <kuba@kernel.org> | 2026-11-12 10:38:03 -0800 |
|---|---|---|
| committer | Jakub Kicinski <kuba@kernel.org> | 2026-11-12 10:38:03 -0800 |
| commit | 7683df331d296819a8f596dd752bbf95aa2e3478 (patch) | |
| tree | 473f39f0573a1a63dda53c329a41ee78a6c02753 | |
| parent | bf83b257423bb557b970df3d7ece32fbd85f3bd4 (diff) | |
| parent | 73e08d50eb139e7329032c89a4cb105cf43ef278 (diff) | |
| download | linux-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.c | 15 | ||||
| -rw-r--r-- | drivers/net/tun_vnet.h | 5 | ||||
| -rw-r--r-- | drivers/net/virtio_net.c | 2 | ||||
| -rw-r--r-- | include/linux/virtio_net.h | 181 |
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; |
