mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git
synced 2025-01-16 21:35:07 +00:00
89add40066
Tighten csum_start and csum_offset checks in virtio_net_hdr_to_skb for GSO packets. The function already checks that a checksum requested with VIRTIO_NET_HDR_F_NEEDS_CSUM is in skb linear. But for GSO packets this might not hold for segs after segmentation. Syzkaller demonstrated to reach this warning in skb_checksum_help offset = skb_checksum_start_offset(skb); ret = -EINVAL; if (WARN_ON_ONCE(offset >= skb_headlen(skb))) By injecting a TSO packet: WARNING: CPU: 1 PID: 3539 at net/core/dev.c:3284 skb_checksum_help+0x3d0/0x5b0 ip_do_fragment+0x209/0x1b20 net/ipv4/ip_output.c:774 ip_finish_output_gso net/ipv4/ip_output.c:279 [inline] __ip_finish_output+0x2bd/0x4b0 net/ipv4/ip_output.c:301 iptunnel_xmit+0x50c/0x930 net/ipv4/ip_tunnel_core.c:82 ip_tunnel_xmit+0x2296/0x2c70 net/ipv4/ip_tunnel.c:813 __gre_xmit net/ipv4/ip_gre.c:469 [inline] ipgre_xmit+0x759/0xa60 net/ipv4/ip_gre.c:661 __netdev_start_xmit include/linux/netdevice.h:4850 [inline] netdev_start_xmit include/linux/netdevice.h:4864 [inline] xmit_one net/core/dev.c:3595 [inline] dev_hard_start_xmit+0x261/0x8c0 net/core/dev.c:3611 __dev_queue_xmit+0x1b97/0x3c90 net/core/dev.c:4261 packet_snd net/packet/af_packet.c:3073 [inline] The geometry of the bad input packet at tcp_gso_segment: [ 52.003050][ T8403] skb len=12202 headroom=244 headlen=12093 tailroom=0 [ 52.003050][ T8403] mac=(168,24) mac_len=24 net=(192,52) trans=244 [ 52.003050][ T8403] shinfo(txflags=0 nr_frags=1 gso(size=1552 type=3 segs=0)) [ 52.003050][ T8403] csum(0x60000c7 start=199 offset=1536 ip_summed=3 complete_sw=0 valid=0 level=0) Mitigate with stricter input validation. csum_offset: for GSO packets, deduce the correct value from gso_type. This is already done for USO. Extend it to TSO. Let UFO be: udp[46]_ufo_fragment ignores these fields and always computes the checksum in software. csum_start: finding the real offset requires parsing to the transport header. Do not add a parser, use existing segmentation parsing. Thanks to SKB_GSO_DODGY, that also catches bad packets that are hw offloaded. Again test both TSO and USO. Do not test UFO for the above reason, and do not test UDP tunnel offload. GSO packet are almost always CHECKSUM_PARTIAL. USO packets may be CHECKSUM_NONE since commit 10154dbded6d6 ("udp: Allow GSO transmit from devices with no checksum offload"), but then still these fields are initialized correctly in udp4_hwcsum/udp6_hwcsum_outgoing. So no need to test for ip_summed == CHECKSUM_PARTIAL first. This revises an existing fix mentioned in the Fixes tag, which broke small packets with GSO offload, as detected by kselftests. Link: https://syzkaller.appspot.com/bug?extid=e1db31216c789f552871 Link: https://lore.kernel.org/netdev/20240723223109.2196886-1-kuba@kernel.org Fixes: e269d79c7d35 ("net: missing check virtio") Cc: stable@vger.kernel.org Signed-off-by: Willem de Bruijn <willemb@google.com> Link: https://patch.msgid.link/20240729201108.1615114-1-willemdebruijn.kernel@gmail.com Signed-off-by: Jakub Kicinski <kuba@kernel.org>
243 lines
6.4 KiB
C
243 lines
6.4 KiB
C
/* SPDX-License-Identifier: GPL-2.0 */
|
|
#ifndef _LINUX_VIRTIO_NET_H
|
|
#define _LINUX_VIRTIO_NET_H
|
|
|
|
#include <linux/if_vlan.h>
|
|
#include <linux/ip.h>
|
|
#include <linux/ipv6.h>
|
|
#include <linux/udp.h>
|
|
#include <uapi/linux/tcp.h>
|
|
#include <uapi/linux/virtio_net.h>
|
|
|
|
static inline bool virtio_net_hdr_match_proto(__be16 protocol, __u8 gso_type)
|
|
{
|
|
switch (gso_type & ~VIRTIO_NET_HDR_GSO_ECN) {
|
|
case VIRTIO_NET_HDR_GSO_TCPV4:
|
|
return protocol == cpu_to_be16(ETH_P_IP);
|
|
case VIRTIO_NET_HDR_GSO_TCPV6:
|
|
return protocol == cpu_to_be16(ETH_P_IPV6);
|
|
case VIRTIO_NET_HDR_GSO_UDP:
|
|
case VIRTIO_NET_HDR_GSO_UDP_L4:
|
|
return protocol == cpu_to_be16(ETH_P_IP) ||
|
|
protocol == cpu_to_be16(ETH_P_IPV6);
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static inline int virtio_net_hdr_set_proto(struct sk_buff *skb,
|
|
const struct virtio_net_hdr *hdr)
|
|
{
|
|
if (skb->protocol)
|
|
return 0;
|
|
|
|
switch (hdr->gso_type & ~VIRTIO_NET_HDR_GSO_ECN) {
|
|
case VIRTIO_NET_HDR_GSO_TCPV4:
|
|
case VIRTIO_NET_HDR_GSO_UDP:
|
|
case VIRTIO_NET_HDR_GSO_UDP_L4:
|
|
skb->protocol = cpu_to_be16(ETH_P_IP);
|
|
break;
|
|
case VIRTIO_NET_HDR_GSO_TCPV6:
|
|
skb->protocol = cpu_to_be16(ETH_P_IPV6);
|
|
break;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static inline int virtio_net_hdr_to_skb(struct sk_buff *skb,
|
|
const struct virtio_net_hdr *hdr,
|
|
bool little_endian)
|
|
{
|
|
unsigned int nh_min_len = sizeof(struct iphdr);
|
|
unsigned int gso_type = 0;
|
|
unsigned int thlen = 0;
|
|
unsigned int p_off = 0;
|
|
unsigned int ip_proto;
|
|
|
|
if (hdr->gso_type != VIRTIO_NET_HDR_GSO_NONE) {
|
|
switch (hdr->gso_type & ~VIRTIO_NET_HDR_GSO_ECN) {
|
|
case VIRTIO_NET_HDR_GSO_TCPV4:
|
|
gso_type = SKB_GSO_TCPV4;
|
|
ip_proto = IPPROTO_TCP;
|
|
thlen = sizeof(struct tcphdr);
|
|
break;
|
|
case VIRTIO_NET_HDR_GSO_TCPV6:
|
|
gso_type = SKB_GSO_TCPV6;
|
|
ip_proto = IPPROTO_TCP;
|
|
thlen = sizeof(struct tcphdr);
|
|
nh_min_len = sizeof(struct ipv6hdr);
|
|
break;
|
|
case VIRTIO_NET_HDR_GSO_UDP:
|
|
gso_type = SKB_GSO_UDP;
|
|
ip_proto = IPPROTO_UDP;
|
|
thlen = sizeof(struct udphdr);
|
|
break;
|
|
case VIRTIO_NET_HDR_GSO_UDP_L4:
|
|
gso_type = SKB_GSO_UDP_L4;
|
|
ip_proto = IPPROTO_UDP;
|
|
thlen = sizeof(struct udphdr);
|
|
break;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (hdr->gso_type & VIRTIO_NET_HDR_GSO_ECN)
|
|
gso_type |= SKB_GSO_TCP_ECN;
|
|
|
|
if (hdr->gso_size == 0)
|
|
return -EINVAL;
|
|
}
|
|
|
|
skb_reset_mac_header(skb);
|
|
|
|
if (hdr->flags & VIRTIO_NET_HDR_F_NEEDS_CSUM) {
|
|
u32 start = __virtio16_to_cpu(little_endian, hdr->csum_start);
|
|
u32 off = __virtio16_to_cpu(little_endian, hdr->csum_offset);
|
|
u32 needed = start + max_t(u32, thlen, off + sizeof(__sum16));
|
|
|
|
if (!pskb_may_pull(skb, needed))
|
|
return -EINVAL;
|
|
|
|
if (!skb_partial_csum_set(skb, start, off))
|
|
return -EINVAL;
|
|
|
|
nh_min_len = max_t(u32, nh_min_len, skb_transport_offset(skb));
|
|
p_off = nh_min_len + thlen;
|
|
if (!pskb_may_pull(skb, p_off))
|
|
return -EINVAL;
|
|
} else {
|
|
/* gso packets without NEEDS_CSUM do not set transport_offset.
|
|
* probe and drop if does not match one of the above types.
|
|
*/
|
|
if (gso_type && skb->network_header) {
|
|
struct flow_keys_basic keys;
|
|
|
|
if (!skb->protocol) {
|
|
__be16 protocol = dev_parse_header_protocol(skb);
|
|
|
|
if (!protocol)
|
|
virtio_net_hdr_set_proto(skb, hdr);
|
|
else if (!virtio_net_hdr_match_proto(protocol, hdr->gso_type))
|
|
return -EINVAL;
|
|
else
|
|
skb->protocol = protocol;
|
|
}
|
|
retry:
|
|
if (!skb_flow_dissect_flow_keys_basic(NULL, skb, &keys,
|
|
NULL, 0, 0, 0,
|
|
0)) {
|
|
/* UFO does not specify ipv4 or 6: try both */
|
|
if (gso_type & SKB_GSO_UDP &&
|
|
skb->protocol == htons(ETH_P_IP)) {
|
|
skb->protocol = htons(ETH_P_IPV6);
|
|
goto retry;
|
|
}
|
|
return -EINVAL;
|
|
}
|
|
|
|
p_off = keys.control.thoff + thlen;
|
|
if (!pskb_may_pull(skb, p_off) ||
|
|
keys.basic.ip_proto != ip_proto)
|
|
return -EINVAL;
|
|
|
|
skb_set_transport_header(skb, keys.control.thoff);
|
|
} else if (gso_type) {
|
|
p_off = nh_min_len + thlen;
|
|
if (!pskb_may_pull(skb, p_off))
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
|
|
if (hdr->gso_type != VIRTIO_NET_HDR_GSO_NONE) {
|
|
u16 gso_size = __virtio16_to_cpu(little_endian, hdr->gso_size);
|
|
unsigned int nh_off = p_off;
|
|
struct skb_shared_info *shinfo = skb_shinfo(skb);
|
|
|
|
switch (gso_type & ~SKB_GSO_TCP_ECN) {
|
|
case SKB_GSO_UDP:
|
|
/* UFO may not include transport header in gso_size. */
|
|
nh_off -= thlen;
|
|
break;
|
|
case SKB_GSO_UDP_L4:
|
|
if (!(hdr->flags & VIRTIO_NET_HDR_F_NEEDS_CSUM))
|
|
return -EINVAL;
|
|
if (skb->csum_offset != offsetof(struct udphdr, check))
|
|
return -EINVAL;
|
|
if (skb->len - p_off > gso_size * UDP_MAX_SEGMENTS)
|
|
return -EINVAL;
|
|
if (gso_type != SKB_GSO_UDP_L4)
|
|
return -EINVAL;
|
|
break;
|
|
case SKB_GSO_TCPV4:
|
|
case SKB_GSO_TCPV6:
|
|
if (skb->csum_offset != offsetof(struct tcphdr, check))
|
|
return -EINVAL;
|
|
break;
|
|
}
|
|
|
|
/* Kernel has a special handling for GSO_BY_FRAGS. */
|
|
if (gso_size == GSO_BY_FRAGS)
|
|
return -EINVAL;
|
|
|
|
/* Too small packets are not really GSO ones. */
|
|
if (skb->len - nh_off > gso_size) {
|
|
shinfo->gso_size = gso_size;
|
|
shinfo->gso_type = gso_type;
|
|
|
|
/* Header must be checked, and gso_segs computed. */
|
|
shinfo->gso_type |= SKB_GSO_DODGY;
|
|
shinfo->gso_segs = 0;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static inline int virtio_net_hdr_from_skb(const struct sk_buff *skb,
|
|
struct virtio_net_hdr *hdr,
|
|
bool little_endian,
|
|
bool has_data_valid,
|
|
int vlan_hlen)
|
|
{
|
|
memset(hdr, 0, sizeof(*hdr)); /* no info leak */
|
|
|
|
if (skb_is_gso(skb)) {
|
|
struct skb_shared_info *sinfo = skb_shinfo(skb);
|
|
|
|
/* This is a hint as to how much should be linear. */
|
|
hdr->hdr_len = __cpu_to_virtio16(little_endian,
|
|
skb_headlen(skb));
|
|
hdr->gso_size = __cpu_to_virtio16(little_endian,
|
|
sinfo->gso_size);
|
|
if (sinfo->gso_type & SKB_GSO_TCPV4)
|
|
hdr->gso_type = VIRTIO_NET_HDR_GSO_TCPV4;
|
|
else if (sinfo->gso_type & SKB_GSO_TCPV6)
|
|
hdr->gso_type = VIRTIO_NET_HDR_GSO_TCPV6;
|
|
else if (sinfo->gso_type & SKB_GSO_UDP_L4)
|
|
hdr->gso_type = VIRTIO_NET_HDR_GSO_UDP_L4;
|
|
else
|
|
return -EINVAL;
|
|
if (sinfo->gso_type & SKB_GSO_TCP_ECN)
|
|
hdr->gso_type |= VIRTIO_NET_HDR_GSO_ECN;
|
|
} else
|
|
hdr->gso_type = VIRTIO_NET_HDR_GSO_NONE;
|
|
|
|
if (skb->ip_summed == CHECKSUM_PARTIAL) {
|
|
hdr->flags = VIRTIO_NET_HDR_F_NEEDS_CSUM;
|
|
hdr->csum_start = __cpu_to_virtio16(little_endian,
|
|
skb_checksum_start_offset(skb) + vlan_hlen);
|
|
hdr->csum_offset = __cpu_to_virtio16(little_endian,
|
|
skb->csum_offset);
|
|
} else if (has_data_valid &&
|
|
skb->ip_summed == CHECKSUM_UNNECESSARY) {
|
|
hdr->flags = VIRTIO_NET_HDR_F_DATA_VALID;
|
|
} /* else everything is zero */
|
|
|
|
return 0;
|
|
}
|
|
|
|
#endif /* _LINUX_VIRTIO_NET_H */
|