From a2e91107095e5d61641bf2476d9fedbe38efc7e0 Mon Sep 17 00:00:00 2001 From: Sharath Chandra Vurukala Date: Tue, 16 May 2023 11:36:04 +0530 Subject: [PATCH 1/2] rmnet_core: Manually checksum csum_valid = 0 packets The csum_valid bit being set to zero in the v5 csum offload header has two possible meanings. Either 1) the checksum is bad, or 2) the checksum was not calculated. When a packet is received with such a header, we need to manually checksum the packet to avoid reporting potentially valid packets as having bad checksums. Change-Id: I6a85d7a01c844be625c11c80ba381ac4dbd0366d Signed-off-by: Sharath Chandra Vurukala --- core/rmnet_descriptor.c | 148 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 147 insertions(+), 1 deletion(-) diff --git a/core/rmnet_descriptor.c b/core/rmnet_descriptor.c index f7818d90a20c..4a8eac3bef9a 100644 --- a/core/rmnet_descriptor.c +++ b/core/rmnet_descriptor.c @@ -1,4 +1,5 @@ /* Copyright (c) 2013-2020, The Linux Foundation. All rights reserved. + * Copyright (c) 2023 Qualcomm Innovation Center, Inc. All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 and @@ -1396,6 +1397,140 @@ rmnet_frag_data_check_coal_header(struct rmnet_frag_descriptor *frag_desc, return 0; } +static int rmnet_frag_checksum_pkt(struct rmnet_frag_descriptor *frag_desc) +{ + struct rmnet_priv *priv = netdev_priv(frag_desc->dev); + struct rmnet_fragment *frag; + int offset = sizeof(struct rmnet_map_header) + + sizeof(struct rmnet_map_v5_csum_header); + u8 *version, __version; + __wsum csum; + u16 csum_len; + + version = rmnet_frag_header_ptr(frag_desc, offset, sizeof(*version), + &__version); + if (!version) + return -EINVAL; + + if ((*version & 0xF0) == 0x40) { + struct iphdr *iph; + u8 __iph[60]; /* Max IP header size (0xF * 4) */ + + /* We need to access the entire IP header including options + * to validate its checksum. Fortunately, the version byte + * also will tell us the length, so we only need to pull + * once ;) + */ + frag_desc->ip_len = (*version & 0xF) * 4; + iph = rmnet_frag_header_ptr(frag_desc, offset, + frag_desc->ip_len, + __iph); + if (!iph || ip_is_fragment(iph)) + return -EINVAL; + + /* Length needs to be sensible */ + csum_len = ntohs(iph->tot_len); + if (csum_len > frag_desc->len - offset) + return -EINVAL; + + csum_len -= frag_desc->ip_len; + /* IPv4 checksum must be valid */ + if (ip_fast_csum((u8 *)iph, frag_desc->ip_len)) { + priv->stats.csum_sw++; + return 0; + } + + frag_desc->ip_proto = 4; + frag_desc->trans_proto = iph->protocol; + csum = ~csum_tcpudp_magic(iph->saddr, iph->daddr, + csum_len, + frag_desc->trans_proto, 0); + } else if ((*version & 0xF0) == 0x60) { + struct ipv6hdr *ip6h, __ip6h; + int ip_len; + __be16 frag_off; + u8 protocol; + + ip6h = rmnet_frag_header_ptr(frag_desc, offset, sizeof(*ip6h), + &__ip6h); + if (!ip6h) + return -EINVAL; + + frag_desc->ip_proto = 6; + protocol = ip6h->nexthdr; + ip_len = rmnet_frag_ipv6_skip_exthdr(frag_desc, + offset + sizeof(*ip6h), + &protocol, &frag_off); + if (ip_len < 0 || frag_off) + return -EINVAL; + + /* Length needs to be sensible */ + frag_desc->ip_len = (u16)ip_len; + csum_len = ntohs(ip6h->payload_len); + if (csum_len + frag_desc->ip_len > frag_desc->len - offset) + return -EINVAL; + + frag_desc->trans_proto = protocol; + csum = ~csum_ipv6_magic(&ip6h->saddr, &ip6h->daddr, + csum_len, + frag_desc->trans_proto, 0); + } else { + /* Not checksumable */ + return -EINVAL; + } + + /* Protocol check */ + if (frag_desc->trans_proto != IPPROTO_TCP && + frag_desc->trans_proto != IPPROTO_UDP) + return -EINVAL; + + offset += frag_desc->ip_len; + /* Check for UDP zero csum packets */ + if (frag_desc->trans_proto == IPPROTO_UDP) { + struct udphdr *uh, __uh; + + uh = rmnet_frag_header_ptr(frag_desc, offset, sizeof(*uh), + &__uh); + if (!uh) + return -EINVAL; + + if (!uh->check) { + if (frag_desc->ip_proto == 4) { + /* Zero checksum is valid */ + priv->stats.csum_sw++; + return 1; + } + + /* Not valid in IPv6 */ + priv->stats.csum_sw++; + return 0; + } + } + + /* Walk the frags and checksum each chunk */ + list_for_each_entry(frag, &frag_desc->frags, list) { + u32 frag_size = skb_frag_size(&frag->frag); + + if (!csum_len) + break; + + if (offset < frag_size) { + void *addr = skb_frag_address(&frag->frag) + offset; + u32 len = min_t(u32, csum_len, frag_size - offset); + + /* Checksum 'len' bytes and add them in */ + csum = csum_partial(addr, len, csum); + csum_len -= len; + offset = 0; + } else { + offset -= frag_size; + } + } + + priv->stats.csum_sw++; + return !csum_fold(csum); +} + /* Process a QMAPv5 packet header */ int rmnet_frag_process_next_hdr_packet(struct rmnet_frag_descriptor *frag_desc, struct rmnet_port *port, @@ -1440,7 +1575,18 @@ int rmnet_frag_process_next_hdr_packet(struct rmnet_frag_descriptor *frag_desc, priv->stats.csum_ok++; frag_desc->csum_valid = true; } else { - priv->stats.csum_valid_unset++; + int valid = rmnet_frag_checksum_pkt(frag_desc); + + if (valid < 0) { + priv->stats.csum_validation_failed++; + } else if (valid) { + /* All's good */ + priv->stats.csum_ok++; + frag_desc->csum_valid = true; + } else { + /* Checksum is actually bad */ + priv->stats.csum_valid_unset++; + } } if (!rmnet_frag_pull(frag_desc, port, From 916deeb7c72af1dcc4a8816aa02fefa01577302c Mon Sep 17 00:00:00 2001 From: Sharath Chandra Vurukala Date: Wed, 12 Apr 2023 13:53:13 +0530 Subject: [PATCH 2/2] rmnet_core: Strengthen IPv6 fragment check in rmnet_frag_skip_exthdr() Apparently, 4-6 XLAT scenarios can insert an empty fragment header into the IPv6 header chain to indicate that fragmentation is supported in the host IPv4 network. This header contains 0 for both the fragment_offset and MF fields, so simply checking the value of frag_off passed to the rmnet_frag_ipv6_skip_exthdr() function is not sufficient to properly catch this rogue header. Instead, we need to implement a "less clever" version. kernel oops at net/core/skbuff.c:4217! Call trace: skb_segment+0xcf0/0xd2c __udp_gso_segment+0xa4/0x544 udp6_ufo_fragment+0x2dc/0x344 ipv6_gso_segment+0x170/0x350 skb_mac_gso_segment+0xd4/0x1b0 __skb_gso_segment+0xcc/0x12c udp_rcv_segment.76914+0x54/0x16c udpv6_queue_rcv_skb+0x78/0x148 __udp6_lib_rcv+0x38c/0x4cc udpv6_rcv+0x20/0x30 ip6_protocol_deliver_rcu+0x3c0/0x63c ip6_input+0x60/0x184 ip6_rcv_finish+0x84/0x150 ipv6_rcv+0x5c/0x14c __netif_receive_skb+0x80/0x184 Change-Id: Ica2779b1da17bc46d397b42283369f3750edbf82 Signed-off-by: Sean Tranchetti Signed-off-by: Sharath Chandra Vurukala --- core/rmnet_descriptor.c | 30 ++++++++++++++++++++++-------- core/rmnet_descriptor.h | 4 +++- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/core/rmnet_descriptor.c b/core/rmnet_descriptor.c index 4a8eac3bef9a..dc0fb7581bf9 100644 --- a/core/rmnet_descriptor.c +++ b/core/rmnet_descriptor.c @@ -323,12 +323,13 @@ int rmnet_frag_descriptor_add_frags_from(struct rmnet_frag_descriptor *to, EXPORT_SYMBOL(rmnet_frag_descriptor_add_frags_from); int rmnet_frag_ipv6_skip_exthdr(struct rmnet_frag_descriptor *frag_desc, - int start, u8 *nexthdrp, __be16 *fragp) + int start, u8 *nexthdrp, __be16 *frag_offp, + bool *frag_hdrp) { u8 nexthdr = *nexthdrp; - *fragp = 0; - + *frag_offp = 0; + *frag_hdrp = false; while (ipv6_ext_hdr(nexthdr)) { struct ipv6_opt_hdr *hp, __hp; int hdrlen; @@ -350,8 +351,9 @@ int rmnet_frag_ipv6_skip_exthdr(struct rmnet_frag_descriptor *frag_desc, if (!fp) return -EINVAL; - *fragp = *fp; - if (ntohs(*fragp) & ~0x7) + *frag_offp = *fp; + *frag_hdrp = true; + if (ntohs(*frag_offp) & ~0x7) break; hdrlen = 8; } else if (nexthdr == NEXTHDR_AUTH) { @@ -1144,6 +1146,7 @@ rmnet_frag_segment_coal_data(struct rmnet_frag_descriptor *coal_desc, struct ipv6hdr *ip6h, __ip6h; int ip_len; __be16 frag_off; + bool frag_hdr; u8 protocol; ip6h = rmnet_frag_header_ptr(coal_desc, 0, sizeof(*ip6h), @@ -1156,10 +1159,11 @@ rmnet_frag_segment_coal_data(struct rmnet_frag_descriptor *coal_desc, ip_len = rmnet_frag_ipv6_skip_exthdr(coal_desc, sizeof(*ip6h), &protocol, - &frag_off); + &frag_off, + &frag_hdr); coal_desc->trans_proto = protocol; - /* If we run into a problem, or this has a fragment header + /* If we run into a problem, or this is fragmented packet * (which should technically not be possible, if the HW * works as intended...), bail. */ @@ -1168,6 +1172,14 @@ rmnet_frag_segment_coal_data(struct rmnet_frag_descriptor *coal_desc, return; } + if (frag_hdr) { + /* There is a fragment header, but this is not a + * fragmented packet. We can handle this, but it + * cannot be coalesced because of kernel limitations. + */ + gro = false; + } + coal_desc->ip_len = (u16)ip_len; if (coal_desc->ip_len > sizeof(*ip6h)) { /* Don't allow coalescing of any packets with IPv6 @@ -1449,6 +1461,7 @@ static int rmnet_frag_checksum_pkt(struct rmnet_frag_descriptor *frag_desc) struct ipv6hdr *ip6h, __ip6h; int ip_len; __be16 frag_off; + bool frag_hdr; u8 protocol; ip6h = rmnet_frag_header_ptr(frag_desc, offset, sizeof(*ip6h), @@ -1460,7 +1473,8 @@ static int rmnet_frag_checksum_pkt(struct rmnet_frag_descriptor *frag_desc) protocol = ip6h->nexthdr; ip_len = rmnet_frag_ipv6_skip_exthdr(frag_desc, offset + sizeof(*ip6h), - &protocol, &frag_off); + &protocol, &frag_off, + &frag_hdr); if (ip_len < 0 || frag_off) return -EINVAL; diff --git a/core/rmnet_descriptor.h b/core/rmnet_descriptor.h index f7bfa55bffe7..7117e4060d42 100644 --- a/core/rmnet_descriptor.h +++ b/core/rmnet_descriptor.h @@ -1,4 +1,5 @@ /* Copyright (c) 2013-2020, The Linux Foundation. All rights reserved. + * ​​​​Copyright (c) 2023 Qualcomm Innovation Center, Inc. All rights reserved.​ * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 and @@ -75,7 +76,8 @@ int rmnet_frag_descriptor_add_frags_from(struct rmnet_frag_descriptor *to, struct rmnet_frag_descriptor *from, u32 off, u32 len); int rmnet_frag_ipv6_skip_exthdr(struct rmnet_frag_descriptor *frag_desc, - int start, u8 *nexthdrp, __be16 *fragp); + int start, u8 *nexthdrp, __be16 *frag_offp, + bool *frag_hdrp); /* QMAP command packets */ void rmnet_frag_command(struct rmnet_frag_descriptor *frag_desc,