* [PATCH net v2 0/3] net: virtio: validate outer protocol of UDP tunnel GSO packets
@ 2026-11-04 5:01 Eric Dumazet
2026-11-04 5:01 ` [PATCH net v2 1/3] " Eric Dumazet
` (2 more replies)
0 siblings, 3 replies; 7+ messages in thread
From: Eric Dumazet @ 2026-11-04 5:01 UTC (permalink / raw)
To: netdev, David S. Miller, Jakub Kicinski, Paolo Abeni
Cc: Eric Dumazet, Simon Horman, Willem de Bruijn, Michael S. Tsirkin,
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 fixes a selftest helper computing UDP checksums over a length
in network byte order.
Patch 3 adds tun selftests, on tap and IFF_TUN devices: valid outer
IPv4 frames (plain and VLAN tagged) are accepted, mismatched or non-IP
outer headers are rejected.
v2:
- 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 (3):
net: virtio: validate outer protocol of UDP tunnel GSO packets
selftests: net: tuntap: fix UDP checksum length in
build_udp_packet_csum()
selftests: net: tun: test outer protocol of UDP tunnel GSO packets
include/linux/virtio_net.h | 169 ++++++++++++
tools/testing/selftests/net/tun.c | 269 +++++++++++++++++++
tools/testing/selftests/net/tuntap_helpers.h | 10 +-
3 files changed, 447 insertions(+), 1 deletion(-)
base-commit: 787268686c64be60023743b527645a907e38ccca
--
2.53.0
^ permalink raw reply [flat|nested] 7+ messages in thread* [PATCH net v2 1/3] net: virtio: validate outer protocol of UDP tunnel GSO packets 2026-11-04 5:01 [PATCH net v2 0/3] net: virtio: validate outer protocol of UDP tunnel GSO packets Eric Dumazet @ 2026-11-04 5:01 ` Eric Dumazet 2026-11-04 15:32 ` Willem de Bruijn 2026-11-04 15:36 ` Michael S. Tsirkin 2026-11-04 5:01 ` [PATCH net v2 2/3] selftests: net: tuntap: fix UDP checksum length in build_udp_packet_csum() Eric Dumazet 2026-11-04 5:01 ` [PATCH net v2 3/3] selftests: net: tun: test outer protocol of UDP tunnel GSO packets Eric Dumazet 2 siblings, 2 replies; 7+ messages in thread From: Eric Dumazet @ 2026-11-04 5:01 UTC (permalink / raw) To: netdev, David S. Miller, Jakub Kicinski, Paolo Abeni Cc: Eric Dumazet, Simon Horman, Willem de Bruijn, Michael S. Tsirkin, 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> Signed-off-by: Eric Dumazet <edumazet@kernel.org> --- include/linux/virtio_net.h | 169 +++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/include/linux/virtio_net.h b/include/linux/virtio_net.h index d6466f96cdd0..b9c8babb1fd7 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,146 @@ 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, 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; +} + 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 +425,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 +504,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] 7+ messages in thread
* Re: [PATCH net v2 1/3] net: virtio: validate outer protocol of UDP tunnel GSO packets 2026-11-04 5:01 ` [PATCH net v2 1/3] " Eric Dumazet @ 2026-11-04 15:32 ` Willem de Bruijn 2026-11-04 15:36 ` Michael S. Tsirkin 1 sibling, 0 replies; 7+ messages in thread From: Willem de Bruijn @ 2026-11-04 15:32 UTC (permalink / raw) To: Eric Dumazet, netdev, David S. Miller, Jakub Kicinski, Paolo Abeni Cc: Eric Dumazet, Simon Horman, Willem de Bruijn, Michael S. Tsirkin, Eugenio Pérez, Hassan Sultan, virtualization Eric Dumazet wrote: > 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> > Signed-off-by: Eric Dumazet <edumazet@kernel.org> Reviewed-by: Willem de Bruijn <willemb@google.com> Thanks Eric! My bot points out a preexisting issue with this udp tunnel feature. _Not_ reason for changing this patch. virtio_net_hdr_tnl_to_skb already does not support IFF_TUN, as it has outer_l3min = ETH_HLEN + virtio_l3min(outer_isv6); So the only reachable path with IFF_TUN is L3 with IPV4_OPT_LEN 16 (or greater) as indeed test patch 3/3 introduces. ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH net v2 1/3] net: virtio: validate outer protocol of UDP tunnel GSO packets 2026-11-04 5:01 ` [PATCH net v2 1/3] " Eric Dumazet 2026-11-04 15:32 ` Willem de Bruijn @ 2026-11-04 15:36 ` Michael S. Tsirkin 1 sibling, 0 replies; 7+ messages in thread From: Michael S. Tsirkin @ 2026-11-04 15:36 UTC (permalink / raw) To: Eric Dumazet Cc: netdev, David S. Miller, Jakub Kicinski, Paolo Abeni, Simon Horman, Willem de Bruijn, Eugenio Pérez, Hassan Sultan, virtualization On Wed, Nov 04, 2026 at 06:01:29AM +0100, Eric Dumazet wrote: > 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> > Signed-off-by: Eric Dumazet <edumazet@kernel.org> Acked-by: Michael S. Tsirkin <mst@redhat.com> > --- > include/linux/virtio_net.h | 169 +++++++++++++++++++++++++++++++++++++ > 1 file changed, 169 insertions(+) > > diff --git a/include/linux/virtio_net.h b/include/linux/virtio_net.h > index d6466f96cdd0..b9c8babb1fd7 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,146 @@ 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, 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; > +} > + > 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 +425,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 +504,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 [flat|nested] 7+ messages in thread
* [PATCH net v2 2/3] selftests: net: tuntap: fix UDP checksum length in build_udp_packet_csum() 2026-11-04 5:01 [PATCH net v2 0/3] net: virtio: validate outer protocol of UDP tunnel GSO packets Eric Dumazet 2026-11-04 5:01 ` [PATCH net v2 1/3] " Eric Dumazet @ 2026-11-04 5:01 ` Eric Dumazet 2026-11-04 5:01 ` [PATCH net v2 3/3] selftests: net: tun: test outer protocol of UDP tunnel GSO packets Eric Dumazet 2 siblings, 0 replies; 7+ messages in thread From: Eric Dumazet @ 2026-11-04 5:01 UTC (permalink / raw) To: netdev, David S. Miller, Jakub Kicinski, Paolo Abeni Cc: Eric Dumazet, Simon Horman, Willem de Bruijn, Michael S. Tsirkin, 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] 7+ messages in thread
* [PATCH net v2 3/3] selftests: net: tun: test outer protocol of UDP tunnel GSO packets 2026-11-04 5:01 [PATCH net v2 0/3] net: virtio: validate outer protocol of UDP tunnel GSO packets Eric Dumazet 2026-11-04 5:01 ` [PATCH net v2 1/3] " Eric Dumazet 2026-11-04 5:01 ` [PATCH net v2 2/3] selftests: net: tuntap: fix UDP checksum length in build_udp_packet_csum() Eric Dumazet @ 2026-11-04 5:01 ` Eric Dumazet 2026-11-04 15:36 ` Willem de Bruijn 2 siblings, 1 reply; 7+ messages in thread From: Eric Dumazet @ 2026-11-04 5:01 UTC (permalink / raw) To: netdev, David S. Miller, Jakub Kicinski, Paolo Abeni Cc: Eric Dumazet, Simon Horman, Willem de Bruijn, Michael S. Tsirkin, 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. 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, + ¶m_ipaddr4_outer_dst, + ¶m_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, + ¶m_ipaddr4_inner_dst, + ¶m_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, + ¶m_ipaddr4_outer_dst, + ¶m_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, + ¶m_ipaddr4_inner_dst, + ¶m_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] 7+ messages in thread
* Re: [PATCH net v2 3/3] selftests: net: tun: test outer protocol of UDP tunnel GSO packets 2026-11-04 5:01 ` [PATCH net v2 3/3] selftests: net: tun: test outer protocol of UDP tunnel GSO packets Eric Dumazet @ 2026-11-04 15:36 ` Willem de Bruijn 0 siblings, 0 replies; 7+ messages in thread From: Willem de Bruijn @ 2026-11-04 15:36 UTC (permalink / raw) To: Eric Dumazet, netdev, David S. Miller, Jakub Kicinski, Paolo Abeni Cc: Eric Dumazet, Simon Horman, Willem de Bruijn, Michael S. Tsirkin, Eugenio Pérez, Hassan Sultan, virtualization Eric Dumazet wrote: > 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. > > Signed-off-by: Eric Dumazet <edumazet@kernel.org> Reviewed-by: Willem de Bruijn <willemb@google.com> ^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-11-04 15:37 UTC | newest] Thread overview: 7+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-11-04 5:01 [PATCH net v2 0/3] net: virtio: validate outer protocol of UDP tunnel GSO packets Eric Dumazet 2026-11-04 5:01 ` [PATCH net v2 1/3] " Eric Dumazet 2026-11-04 15:32 ` Willem de Bruijn 2026-11-04 15:36 ` Michael S. Tsirkin 2026-11-04 5:01 ` [PATCH net v2 2/3] selftests: net: tuntap: fix UDP checksum length in build_udp_packet_csum() Eric Dumazet 2026-11-04 5:01 ` [PATCH net v2 3/3] selftests: net: tun: test outer protocol of UDP tunnel GSO packets Eric Dumazet 2026-11-04 15:36 ` Willem de Bruijn
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox