* [PATCH net v4 0/2] net: virtio: validate outer protocol of UDP tunnel GSO packets @ 2026-11-06 9:53 Eric Dumazet 2026-11-06 9:53 ` [PATCH net v4 1/2] " Eric Dumazet 2026-11-06 9:53 ` [PATCH net v4 2/2] tun: clear stale skb->protocol of NAPI frags skbs Eric Dumazet 0 siblings, 2 replies; 6+ messages in thread From: Eric Dumazet @ 2026-11-06 9:53 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 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. Patch 2 clears a stale skb->protocol left on recycled NAPI frags skbs, which could make the checks of patch 1 reject valid frames. The selftests of v3 will follow in a separate series, with the fixes Sashiko found in them. v4: - Squash v3 patch 2 into patch 1 (Willem). Dropped Michael's Acked-by, as virtio_net_l3_start() now takes a has_eth argument. - Patch 2: clear skb->protocol in tun_napi_alloc_frags(), the only place that needs it (Willem). - Drop the selftests from this series (Willem). v3: https://lore.kernel.org/netdev/20261105115715.1466924-1-edumazet@kernel.org/ - Patch 1: fix a comment, ipv6_find_hdr() stops at AH (Sashiko). - New patch 2: do not use the device type to find the outer L3 header, TUNSETLINK can change it (Sashiko). - New patch 3: clear a stale skb->protocol on IFF_NAPI_FRAGS skbs (Sashiko). - More selftests, and big-endian fixes for them (Sashiko). v2: https://lore.kernel.org/netdev/20261104050131.629967-1-edumazet@kernel.org/ - 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). - Selftest fixes, IFF_TUN tests. v1: https://lore.kernel.org/netdev/20261103203113.550604-1-edumazet@kernel.org/ Eric Dumazet (2): net: virtio: validate outer protocol of UDP tunnel GSO packets tun: clear stale skb->protocol of NAPI frags skbs drivers/net/tun.c | 15 +++- drivers/net/tun_vnet.h | 5 +- drivers/net/virtio_net.c | 2 +- include/linux/virtio_net.h | 169 +++++++++++++++++++++++++++++++++++++ 4 files changed, 186 insertions(+), 5 deletions(-) base-commit: f66469d9905b39d2c260a15d7d6db450f4db67c1 -- 2.53.0 ^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH net v4 1/2] net: virtio: validate outer protocol of UDP tunnel GSO packets 2026-11-06 9:53 [PATCH net v4 0/2] net: virtio: validate outer protocol of UDP tunnel GSO packets Eric Dumazet @ 2026-11-06 9:53 ` Eric Dumazet 2026-11-06 10:38 ` Michael S. Tsirkin 2026-11-09 9:55 ` netdev-bot+sashiko 2026-11-06 9:53 ` [PATCH net v4 2/2] tun: clear stale skb->protocol of NAPI frags skbs Eric Dumazet 1 sibling, 2 replies; 6+ messages in thread From: Eric Dumazet @ 2026-11-06 9:53 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: 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: - the callers tell whether the frame starts with an Ethernet header, with a new has_eth argument of virtio_net_hdr_tnl_to_skb(), instead of using the device type. tun frames packets from its IFF_TAP and IFF_TUN flags, and TUNSETLINK can set the device type to any value. virtio_net and tun_xdp_one() always pass true, tun_get_user() passes whether the device is IFF_TAP. - 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> 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 | 169 +++++++++++++++++++++++++++++++++++++ 4 files changed, 180 insertions(+), 5 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 d6466f96cdd0..b7b6b542f476 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 -- 2.53.0 ^ permalink raw reply related [flat|nested] 6+ messages in thread
* Re: [PATCH net v4 1/2] net: virtio: validate outer protocol of UDP tunnel GSO packets 2026-11-06 9:53 ` [PATCH net v4 1/2] " Eric Dumazet @ 2026-11-06 10:38 ` Michael S. Tsirkin 2026-11-09 9:55 ` netdev-bot+sashiko 1 sibling, 0 replies; 6+ messages in thread From: Michael S. Tsirkin @ 2026-11-06 10:38 UTC (permalink / raw) To: Eric Dumazet Cc: netdev, David S. Miller, Jakub Kicinski, Paolo Abeni, Simon Horman, Willem de Bruijn, Jason Wang, Eugenio Pérez, Hassan Sultan, virtualization On Fri, Nov 06, 2026 at 10:53:39AM +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: 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: > > - the callers tell whether the frame starts with an Ethernet header, > with a new has_eth argument of virtio_net_hdr_tnl_to_skb(), instead > of using the device type. tun frames packets from its IFF_TAP and > IFF_TUN flags, and TUNSETLINK can set the device type to any value. > virtio_net and tun_xdp_one() always pass true, tun_get_user() passes > whether the device is IFF_TAP. > - 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> > Signed-off-by: Eric Dumazet <edumazet@kernel.org> Acked-by: Michael S. Tsirkin <mst@redhat.com> > --- > drivers/net/tun.c | 9 +- > drivers/net/tun_vnet.h | 5 +- > drivers/net/virtio_net.c | 2 +- > include/linux/virtio_net.h | 169 +++++++++++++++++++++++++++++++++++++ > 4 files changed, 180 insertions(+), 5 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 d6466f96cdd0..b7b6b542f476 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 > -- > 2.53.0 ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH net v4 1/2] net: virtio: validate outer protocol of UDP tunnel GSO packets 2026-11-06 9:53 ` [PATCH net v4 1/2] " Eric Dumazet 2026-11-06 10:38 ` Michael S. Tsirkin @ 2026-11-09 9:55 ` netdev-bot+sashiko 2026-11-09 16:24 ` Eric Dumazet 1 sibling, 1 reply; 6+ messages in thread From: netdev-bot+sashiko @ 2026-11-09 9:55 UTC (permalink / raw) To: edumazet Cc: netdev, davem, kuba, pabeni, horms, willemb, mst, jasowangio, eperezma, hassansultan, virtualization Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider. Critical: 0 · High: 0 · Medium: 1 · Low: 0 (1) If you believe none of the issues are legitimate, please reply to each issue of Medium severity or higher and explain why the AI is wrong. If any of them are legitimate and you plan to respin, please let patchwork know by including "pw-bot: cr" as a separate line at the end of your reply (one such reply per series is enough). - [Medium] The new strict outer-offset check rejects UDP-tunnel GSO frames that the kernel's own tun TX path produces whenever the skb… Pre-existing issues: - [Low] In virtio_net_hdr_tnl_to_skb(), outer_l3min = ETH_HLEN + virtio_l3min(outer_isv6) (include/linux/virtio_net.h:491). -- net: virtio: validate outer protocol of UDP tunnel GSO packets This commit makes virtio_net_hdr_tnl_to_skb() check that the frame's outer L3 protocol matches the declared tunnel type. It also checks that the outer UDP header sits exactly at outer_th_offset. Callers pass a new has_eth argument that says whether the frame starts with an Ethernet header. This closes a path where an arbitrary outer ethertype combined with SKB_GSO_UDP_TUNNEL could crash the host in skb_under_panic(). Link: https://lore.kernel.org/netdev/93d0e37c99711bebdf60c120af03010455c2f2e2.1793276116.git.mst@redhat.com/ > diff --git a/include/linux/virtio_net.h b/include/linux/virtio_net.h > index d6466f96cdd00..b7b6b542f4763 100644 > --- a/include/linux/virtio_net.h > +++ b/include/linux/virtio_net.h [ ... ] > @@ -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; [Severity: Medium] Can this exact match reject UDP tunnel GSO frames that tun itself produces when the skb carries a VLAN hwaccel tag? On the producer side, tun_put_user() writes the 802.1Q tag into the frame it copies to userspace or vhost-net. It calls tun_vnet_hdr_tnl_from_skb() with vlan_hlen = VLAN_HLEN. Only csum_start accounts for the tag: include/linux/virtio_net.h:virtio_net_hdr_from_skb() { ... hdr->csum_start = __cpu_to_virtio16(little_endian, skb_checksum_start_offset(skb) + vlan_hlen); ... } The tunnel offsets do not: include/linux/virtio_net.h:virtio_net_hdr_tnl_from_skb() { ... inner_nh = skb->inner_network_header - skb_headroom(skb); outer_th = skb->transport_header - skb_headroom(skb); vhdr->inner_nh_offset = cpu_to_le16(inner_nh); vhdr->outer_th_offset = cpu_to_le16(outer_th); ... } So outer_th_offset and inner_nh_offset both end up 4 bytes short of the real UDP and inner headers in the emitted frame. On the consumer side, with has_eth set, virtio_net_l3_start() walks the in-band VLAN tag with __vlan_get_protocol(). That gives nhoff = 18, so thoff points at the real UDP header (38 for IPv4). The emitted outer_th is 34, this comparison fails, and virtio_net_hdr_tnl_to_skb() returns -EINVAL. Before this patch the same frame was accepted. Only transport_header was off by 4, and ip_rcv_core() and ip6_rcv_core() reset it for local delivery. With this change, would a guest that gets these frames from a host tap drop them in virtnet_receive_done() as "bad gso" and bump rx_frame_errors? A tun/tap writer that re-injects frames it read from another tun would see the same rejection. This looks reachable with VLAN-tagged UDP tunnel GSO traffic. Two examples are a VLAN filtering bridge with a tagged egress port, and a vlan device on top of a tap. tun advertises NETIF_F_HW_VLAN_CTAG_TX/STAG_TX in hw_features and hw_enc_features, so the tag stays in hwaccel until tun_put_user(). The later patch in this series, "tun: clear stale skb->protocol of NAPI frags skbs", does not seem to fix the producer side. Should virtio_net_hdr_tnl_from_skb() add vlan_hlen to outer_th_offset and inner_nh_offset, with that fix landing with or before this check? > +} > + > 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; > + [Severity: Low] This is a pre-existing issue, not one introduced by this patch, but does the has_eth = false path ever see ordinary IFF_TUN frames? Earlier in virtio_net_hdr_tnl_to_skb(), the minimum outer offset still assumes an Ethernet header, whatever has_eth says: outer_l3min = ETH_HLEN + virtio_l3min(outer_isv6); ... if (outer_th < outer_l3min || For IFF_TUN, tun_get_user() passes has_eth = false and the network header is at offset 0. virtio_net_tnl_offsets_ok() therefore needs outer_th == 20 for plain IPv4 or 40 for plain IPv6. Both values are below the 34/54 floor, so these frames are rejected before the new check runs. That leaves the !has_eth branch of virtio_net_l3_start() reachable only for outer IPv4 with at least 16 bytes of options, or outer IPv6 with at least 16 bytes of extension headers. The L3-relative offsets produced by tun TX and by the selftest helper build_virtio_net_hdr_v1_hash_tunnel() (eth_hlen = 0 for !is_tap) are still rejected. The ETH_HLEN floor dates from a2fb4bc4e2a6. Now that callers pass has_eth, should outer_l3min depend on it? [ ... ] -- Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261106095340.2113683-1-edumazet%40kernel.org ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH net v4 1/2] net: virtio: validate outer protocol of UDP tunnel GSO packets 2026-11-09 9:55 ` netdev-bot+sashiko @ 2026-11-09 16:24 ` Eric Dumazet 0 siblings, 0 replies; 6+ messages in thread From: Eric Dumazet @ 2026-11-09 16:24 UTC (permalink / raw) To: netdev-bot+sashiko Cc: netdev, davem, kuba, pabeni, horms, willemb, mst, jasowangio, eperezma, hassansultan, virtualization Le lun. 9 nov. 2026 à 10:55, <netdev-bot+sashiko@kernel.org> a écrit : > > Thank you for your contribution! Sashiko AI review found 1 potential > issue(s) to consider. > > Critical: 0 · High: 0 · Medium: 1 · Low: 0 (1) > > If you believe none of the issues are legitimate, please reply to each > issue of Medium severity or higher and explain why the AI is wrong. > If any of them are legitimate and you plan to respin, please let patchwork > know by including "pw-bot: cr" as a separate line at the end of your reply > (one such reply per series is enough). > > - [Medium] The new strict outer-offset check rejects UDP-tunnel GSO > frames that the kernel's own tun TX path produces whenever the skb… > > Pre-existing issues: > - [Low] In virtio_net_hdr_tnl_to_skb(), outer_l3min = ETH_HLEN + > virtio_l3min(outer_isv6) (include/linux/virtio_net.h:491). > > -- > > net: virtio: validate outer protocol of UDP tunnel GSO packets > > This commit makes virtio_net_hdr_tnl_to_skb() check that the frame's outer > L3 protocol matches the declared tunnel type. It also checks that the outer > UDP header sits exactly at outer_th_offset. Callers pass a new has_eth > argument that says whether the frame starts with an Ethernet header. This > closes a path where an arbitrary outer ethertype combined with > SKB_GSO_UDP_TUNNEL could crash the host in skb_under_panic(). > > Link: https://lore.kernel.org/netdev/93d0e37c99711bebdf60c120af03010455c2f2e2.1793276116.git.mst@redhat.com/ > > > diff --git a/include/linux/virtio_net.h b/include/linux/virtio_net.h > > index d6466f96cdd00..b7b6b542f4763 100644 > > --- a/include/linux/virtio_net.h > > +++ b/include/linux/virtio_net.h > > [ ... ] > > > @@ -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; > > [Severity: Medium] > > Can this exact match reject UDP tunnel GSO frames that tun itself produces > when the skb carries a VLAN hwaccel tag? > > On the producer side, tun_put_user() writes the 802.1Q tag into the frame > it copies to userspace or vhost-net. It calls tun_vnet_hdr_tnl_from_skb() > with vlan_hlen = VLAN_HLEN. Only csum_start accounts for the tag: > > include/linux/virtio_net.h:virtio_net_hdr_from_skb() { > ... > hdr->csum_start = __cpu_to_virtio16(little_endian, > skb_checksum_start_offset(skb) + vlan_hlen); > ... > } > > The tunnel offsets do not: > > include/linux/virtio_net.h:virtio_net_hdr_tnl_from_skb() { > ... > inner_nh = skb->inner_network_header - skb_headroom(skb); > outer_th = skb->transport_header - skb_headroom(skb); > vhdr->inner_nh_offset = cpu_to_le16(inner_nh); > vhdr->outer_th_offset = cpu_to_le16(outer_th); > ... > } > > So outer_th_offset and inner_nh_offset both end up 4 bytes short of the > real UDP and inner headers in the emitted frame. > > On the consumer side, with has_eth set, virtio_net_l3_start() walks the > in-band VLAN tag with __vlan_get_protocol(). That gives nhoff = 18, so > thoff points at the real UDP header (38 for IPv4). The emitted outer_th is > 34, this comparison fails, and virtio_net_hdr_tnl_to_skb() returns -EINVAL. > > Before this patch the same frame was accepted. Only transport_header was > off by 4, and ip_rcv_core() and ip6_rcv_core() reset it for local delivery. > > With this change, would a guest that gets these frames from a host tap > drop them in virtnet_receive_done() as "bad gso" and bump > rx_frame_errors? A tun/tap writer that re-injects frames it read from > another tun would see the same rejection. > > This looks reachable with VLAN-tagged UDP tunnel GSO traffic. Two examples > are a VLAN filtering bridge with a tagged egress port, and a vlan device on > top of a tap. tun advertises NETIF_F_HW_VLAN_CTAG_TX/STAG_TX in > hw_features and hw_enc_features, so the tag stays in hwaccel until > tun_put_user(). > > The later patch in this series, "tun: clear stale skb->protocol of NAPI > frags skbs", does not seem to fix the producer side. Should > virtio_net_hdr_tnl_from_skb() add vlan_hlen to outer_th_offset and > inner_nh_offset, with that fix landing with or before this check? > Right. tun keeps the tag in hwaccel for UDP tunnel GSO packets, and only csum_start accounts for it. The wrong offsets were already a bug: the receiver set inner_network_header 4 bytes off, and a forwarded packet segmented again would use the wrong inner headers. v5 will add a patch before this one, so that virtio_net_hdr_tnl_from_skb() adds vlan_hlen to both offsets. > > +} > > + > > 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; > > + > > [Severity: Low] > > This is a pre-existing issue, not one introduced by this patch, but does > the has_eth = false path ever see ordinary IFF_TUN frames? > > Earlier in virtio_net_hdr_tnl_to_skb(), the minimum outer offset still > assumes an Ethernet header, whatever has_eth says: > > outer_l3min = ETH_HLEN + virtio_l3min(outer_isv6); > ... > if (outer_th < outer_l3min || > > For IFF_TUN, tun_get_user() passes has_eth = false and the network header > is at offset 0. virtio_net_tnl_offsets_ok() therefore needs outer_th == 20 > for plain IPv4 or 40 for plain IPv6. Both values are below the 34/54 floor, > so these frames are rejected before the new check runs. > > That leaves the !has_eth branch of virtio_net_l3_start() reachable only for > outer IPv4 with at least 16 bytes of options, or outer IPv6 with at least > 16 bytes of extension headers. The L3-relative offsets produced by tun TX > and by the selftest helper build_virtio_net_hdr_v1_hash_tunnel() > (eth_hlen = 0 for !is_tap) are still rejected. > > The ETH_HLEN floor dates from a2fb4bc4e2a6. Now that callers pass has_eth, > should outer_l3min depend on it? > This predates this series: UDP tunnel GSO over IFF_TUN has never worked with plain IPv4/IPv6 outer headers. Removing the floor would enable a path that has never been used, which does not belong in this fix. I will send this as a follow-up. pw-bot: cr ^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH net v4 2/2] tun: clear stale skb->protocol of NAPI frags skbs 2026-11-06 9:53 [PATCH net v4 0/2] net: virtio: validate outer protocol of UDP tunnel GSO packets Eric Dumazet 2026-11-06 9:53 ` [PATCH net v4 1/2] " Eric Dumazet @ 2026-11-06 9:53 ` Eric Dumazet 1 sibling, 0 replies; 6+ messages in thread From: Eric Dumazet @ 2026-11-06 9:53 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. 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, and IFF_NAPI_FRAGS needs CAP_NET_ADMIN. Clear skb->protocol in tun_napi_alloc_frags(), like skbs allocated by tun_alloc_skb() or build_skb(). eth_type_trans() sets it later. 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/ Suggested-by: Willem de Bruijn <willemb@google.com> Reviewed-by: Willem de Bruijn <willemb@google.com> Signed-off-by: Eric Dumazet <edumazet@kernel.org> --- drivers/net/tun.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/net/tun.c b/drivers/net/tun.c index 3920b3dff1e8..9aca5056f7c5 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) -- 2.53.0 ^ permalink raw reply related [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-11-09 16:24 UTC | newest] Thread overview: 6+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-11-06 9:53 [PATCH net v4 0/2] net: virtio: validate outer protocol of UDP tunnel GSO packets Eric Dumazet 2026-11-06 9:53 ` [PATCH net v4 1/2] " Eric Dumazet 2026-11-06 10:38 ` Michael S. Tsirkin 2026-11-09 9:55 ` netdev-bot+sashiko 2026-11-09 16:24 ` Eric Dumazet 2026-11-06 9:53 ` [PATCH net v4 2/2] tun: clear stale skb->protocol of NAPI frags skbs Eric Dumazet
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox