diff --git a/net/core/dev.c b/net/core/dev.c index 4ea6ea4b111e..8eb9bf1ff9eb 100644 --- a/net/core/dev.c +++ b/net/core/dev.c @@ -3996,6 +3996,53 @@ __netif_skb_features(struct sk_buff *skb, bool check_gso_limits) } EXPORT_SYMBOL(__netif_skb_features); +static unsigned int +skb_gso_output_max_segs(struct sk_buff *skb, struct net_device *dev) +{ + unsigned int mss = skb_shinfo(skb)->gso_size; + unsigned int gso_max_size, hdr_len, max_segs; + netdev_features_t features; + struct tcphdr _tcph, *th; + + if (!skb_is_gso(skb) || !skb_is_gso_tcp(skb) || + skb->encapsulation || skb_has_frag_list(skb)) + return 0; + + /* + * The transport offset can be unset or stale, so hdr_len is only taken + * from a header at the checksum start. + */ + if (!skb_transport_header_was_set(skb) || + skb_checksum_start(skb) != skb_transport_header(skb)) + return 0; + + th = skb_header_pointer(skb, skb_transport_offset(skb), sizeof(_tcph), + &_tcph); + if (!th || __tcp_hdrlen(th) < sizeof(*th)) + return 0; + + hdr_len = skb_transport_header(skb) - skb_mac_header(skb) + + __tcp_hdrlen(th); + + /* The output is still a GSO skb: recheck without the limit checks */ + features = __netif_skb_features(skb, false) | NETIF_F_GSO_ROBUST; + if (!net_gso_ok(features, skb_shinfo(skb)->gso_type)) + return 0; + + gso_max_size = netif_get_gso_max_size(dev, skb); + gso_max_size = min(gso_max_size, GSO_LEGACY_MAX_SIZE); + + /* + * gso_within_dev_limits() accepts gso_segs == gso_max_segs but + * rejects skb->len >= gso_max_size, so only the size limit needs the + * - 1; the inner min() keeps that subtraction from wrapping. + */ + max_segs = (gso_max_size - min(gso_max_size, hdr_len + 1)) / mss; + max_segs = min(max_segs, READ_ONCE(dev->gso_max_segs)); + + return max_segs; +} + static int xmit_one(struct sk_buff *skb, struct net_device *dev, struct netdev_queue *txq, bool more) { @@ -4159,9 +4206,13 @@ static struct sk_buff *validate_xmit_skb(struct sk_buff *skb, struct net_device goto out_null; if (netif_needs_gso(skb, features)) { + unsigned int max_segs = 0; struct sk_buff *segs; - segs = skb_gso_segment(skb, features); + if (unlikely(!gso_within_dev_limits(skb, dev))) + max_segs = skb_gso_output_max_segs(skb, dev); + + segs = __skb_gso_segment(skb, features, true, max_segs); if (IS_ERR(segs)) { goto out_kfree_skb; } else if (segs) {