Skip to contentSkip to conversation

udp: gso: do not drop small packets when PMTU reduces

PATCH Superseded Yan Zhai 2 participants 3 messages Jan 30, 2025

Subsystems (4)

v2 of 3 Superseded Latest is v3 →

lore
Share & tools
lore

Yan Zhai

author
#0 lore raw

PATCH v2 1/1 udp: gso: do not drop small packets when PMTU reduces

Commit 4094871db1d6 ("udp: only do GSO if # of segs > 1") avoided GSO for small packets. But the kernel currently dismisses GSO requests only after checking MTU/PMTU on gso_size. This means any packets, regardless of their payload sizes, could be dropped when PMTU becomes smaller than requested gso_size. We encountered this issue in production and it caused a reliability problem that new QUIC connection cannot be established before PMTU cache expired, while non GSO sockets still worked fine at the same time.

Ideally, do not check any GSO related constraints when payload size is smaller than requested gso_size, and return EMSGSIZE instead of EINVAL on MTU/PMTU check failure to be more specific on the error cause.

Fixes 4094871db1d6 ("udp: only do GSO if # of segs > 1")

--
v1->v2: add a missing MTU check when fall back to no GSO mode suggested by Willem de Bruijn <[email protected]>; Fixed up commit message to be more precise.

v1: https://lore.kernel.org/all/Z5cgWh%[email protected]/

---
 net/ipv4/udp.c                       | 28 +++++++++++++++++++---------
 net/ipv6/udp.c                       | 28 +++++++++++++++++++---------
 tools/testing/selftests/net/udpgso.c | 14 ++++++++++++++
 3 files changed, 52 insertions(+), 18 deletions(-)
net/ipv4/udp.c +19 -9
index c472c9a57cf6..0b5010238d05 100644
@@ -1141,9 +1141,20 @@ static int udp_send_skb(struct sk_buff *skb, struct flowi4 *fl4,
 		const int hlen = skb_network_header_len(skb) +
 				 sizeof(struct udphdr);
 
+		if (datalen <= cork->gso_size) {
+			/*
+			 * check MTU again: it's skipped previously when
+			 * gso_size != 0
+			 */
+			if (hlen + datalen > cork->fragsize) {
+				kfree_skb(skb);
+				return -EMSGSIZE;
+			}
+			goto no_gso;
+		}
 		if (hlen + cork->gso_size > cork->fragsize) {
 			kfree_skb(skb);
-			return -EINVAL;
+			return -EMSGSIZE;
 		}
 		if (datalen > cork->gso_size * UDP_MAX_SEGMENTS) {
 			kfree_skb(skb);
@@ -1158,17 +1169,16 @@ static int udp_send_skb(struct sk_buff *skb, struct flowi4 *fl4,
 			return -EIO;
 		}
 
-		if (datalen > cork->gso_size) {
-			skb_shinfo(skb)->gso_size = cork->gso_size;
-			skb_shinfo(skb)->gso_type = SKB_GSO_UDP_L4;
-			skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(datalen,
-								 cork->gso_size);
+		skb_shinfo(skb)->gso_size = cork->gso_size;
+		skb_shinfo(skb)->gso_type = SKB_GSO_UDP_L4;
+		skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(datalen,
+							 cork->gso_size);
 
-			/* Don't checksum the payload, skb will get segmented */
-			goto csum_partial;
-		}
+		/* Don't checksum the payload, skb will get segmented */
+		goto csum_partial;
 	}
 
+no_gso:
 	if (is_udplite)  				 /*     UDP-Lite      */
 		csum = udplite_csum(skb);
 
net/ipv6/udp.c +19 -9
index 6671daa67f4f..d97befa7f80d 100644
@@ -1389,9 +1389,20 @@ static int udp_v6_send_skb(struct sk_buff *skb, struct flowi6 *fl6,
 		const int hlen = skb_network_header_len(skb) +
 				 sizeof(struct udphdr);
 
+		if (datalen <= cork->gso_size) {
+			/*
+			 * check MTU again: it's skipped previously when
+			 * gso_size != 0
+			 */
+			if (hlen + datalen > cork->fragsize) {
+				kfree_skb(skb);
+				return -EMSGSIZE;
+			}
+			goto no_gso;
+		}
 		if (hlen + cork->gso_size > cork->fragsize) {
 			kfree_skb(skb);
-			return -EINVAL;
+			return -EMSGSIZE;
 		}
 		if (datalen > cork->gso_size * UDP_MAX_SEGMENTS) {
 			kfree_skb(skb);
@@ -1406,17 +1417,16 @@ static int udp_v6_send_skb(struct sk_buff *skb, struct flowi6 *fl6,
 			return -EIO;
 		}
 
-		if (datalen > cork->gso_size) {
-			skb_shinfo(skb)->gso_size = cork->gso_size;
-			skb_shinfo(skb)->gso_type = SKB_GSO_UDP_L4;
-			skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(datalen,
-								 cork->gso_size);
+		skb_shinfo(skb)->gso_size = cork->gso_size;
+		skb_shinfo(skb)->gso_type = SKB_GSO_UDP_L4;
+		skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(datalen,
+							 cork->gso_size);
 
-			/* Don't checksum the payload, skb will get segmented */
-			goto csum_partial;
-		}
+		/* Don't checksum the payload, skb will get segmented */
+		goto csum_partial;
 	}
 
+no_gso:
 	if (is_udplite)
 		csum = udplite_csum(skb);
 	else if (udp_get_no_check6_tx(sk)) {   /* UDP csum disabled */
tools/testing/selftests/net/udpgso.c +14 -0
index 3f2fca02fec5..fb73f1c331fb 100644
@@ -102,6 +102,13 @@ struct testcase testcases_v4[] = {
 		.gso_len = CONST_MSS_V4,
 		.r_num_mss = 1,
 	},
+	{
+		/* datalen <= MSS < gso_len: will fall back to no GSO */
+		.tlen = CONST_MSS_V4,
+		.gso_len = CONST_MSS_V4 + 1,
+		.r_num_mss = 0,
+		.r_len_last = CONST_MSS_V4,
+	},
 	{
 		/* send a single MSS + 1B */
 		.tlen = CONST_MSS_V4 + 1,
@@ -205,6 +212,13 @@ struct testcase testcases_v6[] = {
 		.gso_len = CONST_MSS_V6,
 		.r_num_mss = 1,
 	},
+	{
+		/* datalen <= MSS < gso_len: will fall back to no GSO */
+		.tlen = CONST_MSS_V6,
+		.gso_len = CONST_MSS_V6 + 1,
+		.r_num_mss = 0,
+		.r_len_last = CONST_MSS_V6,
+	},
 	{
 		/* send a single MSS + 1B */
 		.tlen = CONST_MSS_V6 + 1,
Signature · 1 line
2.30.2
maintainer
#1 lore raw
Quoted 35 lines · Yan Zhai

Commit 4094871db1d6 ("udp: only do GSO if # of segs > 1") avoided GSO for small packets. But the kernel currently dismisses GSO requests only after checking MTU/PMTU on gso_size. This means any packets, regardless of their payload sizes, could be dropped when PMTU becomes smaller than requested gso_size. We encountered this issue in production and it caused a reliability problem that new QUIC connection cannot be established before PMTU cache expired, while non GSO sockets still worked fine at the same time.

Ideally, do not check any GSO related constraints when payload size is smaller than requested gso_size, and return EMSGSIZE instead of EINVAL on MTU/PMTU check failure to be more specific on the error cause.

Fixes 4094871db1d6 ("udp: only do GSO if # of segs > 1")

--
v1->v2: add a missing MTU check when fall back to no GSO mode suggested by Willem de Bruijn <[email protected]>; Fixed up commit message to be more precise.

v1: https://lore.kernel.org/all/Z5cgWh%[email protected]/

---
 net/ipv4/udp.c                       | 28 +++++++++++++++++++---------
 net/ipv6/udp.c                       | 28 +++++++++++++++++++---------
 tools/testing/selftests/net/udpgso.c | 14 ++++++++++++++
 3 files changed, 52 insertions(+), 18 deletions(-)
net/ipv4/udp.c +0 -0
index c472c9a57cf6..0b5010238d05 100644
@@ -1141,9 +1141,20 @@ static int udp_send_skb(struct sk_buff *skb, struct flowi4 *fl4,
 		const int hlen = skb_network_header_len(skb) +
 				 sizeof(struct udphdr);
 

net/ipv4/udp.c static int udp_send_skb(struct sk_buff *skb, struct flowi4 *fl4, · line 1153

Quoted 10 lines
net/ipv4/udp.c +10 -0
+		if (datalen <= cork->gso_size) {
+			/*
+			 * check MTU again: it's skipped previously when
+			 * gso_size != 0
+			 */
+			if (hlen + datalen > cork->fragsize) {
+				kfree_skb(skb);
+				return -EMSGSIZE;
+			}
+			goto no_gso;

This is almost the same as the test below.

How about just

    if (hlen + min(cork->gso_size, datalen) > cork->fragsize)

And don't bypass the subsequent checks with a goto, or modify the rest of the code.

Quoted 89 lines
net/ipv4/udp.c +9 -9
+		}
 		if (hlen + cork->gso_size > cork->fragsize) {
 			kfree_skb(skb);
-			return -EINVAL;
+			return -EMSGSIZE;
 		}
 		if (datalen > cork->gso_size * UDP_MAX_SEGMENTS) {
 			kfree_skb(skb);
@@ -1158,17 +1169,16 @@ static int udp_send_skb(struct sk_buff *skb, struct flowi4 *fl4,
 			return -EIO;
 		}
 
-		if (datalen > cork->gso_size) {
-			skb_shinfo(skb)->gso_size = cork->gso_size;
-			skb_shinfo(skb)->gso_type = SKB_GSO_UDP_L4;
-			skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(datalen,
-								 cork->gso_size);
+		skb_shinfo(skb)->gso_size = cork->gso_size;
+		skb_shinfo(skb)->gso_type = SKB_GSO_UDP_L4;
+		skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(datalen,
+							 cork->gso_size);
 
-			/* Don't checksum the payload, skb will get segmented */
-			goto csum_partial;
-		}
+		/* Don't checksum the payload, skb will get segmented */
+		goto csum_partial;
 	}
 
+no_gso:
 	if (is_udplite)  				 /*     UDP-Lite      */
 		csum = udplite_csum(skb);
 
net/ipv6/udp.c +19 -9
index 6671daa67f4f..d97befa7f80d 100644
@@ -1389,9 +1389,20 @@ static int udp_v6_send_skb(struct sk_buff *skb, struct flowi6 *fl6,
 		const int hlen = skb_network_header_len(skb) +
 				 sizeof(struct udphdr);
 
+		if (datalen <= cork->gso_size) {
+			/*
+			 * check MTU again: it's skipped previously when
+			 * gso_size != 0
+			 */
+			if (hlen + datalen > cork->fragsize) {
+				kfree_skb(skb);
+				return -EMSGSIZE;
+			}
+			goto no_gso;
+		}
 		if (hlen + cork->gso_size > cork->fragsize) {
 			kfree_skb(skb);
-			return -EINVAL;
+			return -EMSGSIZE;
 		}
 		if (datalen > cork->gso_size * UDP_MAX_SEGMENTS) {
 			kfree_skb(skb);
@@ -1406,17 +1417,16 @@ static int udp_v6_send_skb(struct sk_buff *skb, struct flowi6 *fl6,
 			return -EIO;
 		}
 
-		if (datalen > cork->gso_size) {
-			skb_shinfo(skb)->gso_size = cork->gso_size;
-			skb_shinfo(skb)->gso_type = SKB_GSO_UDP_L4;
-			skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(datalen,
-								 cork->gso_size);
+		skb_shinfo(skb)->gso_size = cork->gso_size;
+		skb_shinfo(skb)->gso_type = SKB_GSO_UDP_L4;
+		skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(datalen,
+							 cork->gso_size);
 
-			/* Don't checksum the payload, skb will get segmented */
-			goto csum_partial;
-		}
+		/* Don't checksum the payload, skb will get segmented */
+		goto csum_partial;
 	}
 
+no_gso:
 	if (is_udplite)
 		csum = udplite_csum(skb);
 	else if (udp_get_no_check6_tx(sk)) {   /* UDP csum disabled */
tools/testing/selftests/net/udpgso.c +0 -0
index 3f2fca02fec5..fb73f1c331fb 100644

tools/testing/selftests/net/udpgso.c struct testcase testcases_v4[] = { · line 111

Quoted 10 lines
tools/testing/selftests/net/udpgso.c +7 -0
@@ -102,6 +102,13 @@ struct testcase testcases_v4[] = {
 		.gso_len = CONST_MSS_V4,
 		.r_num_mss = 1,
 	},
+	{
+		/* datalen <= MSS < gso_len: will fall back to no GSO */
+		.tlen = CONST_MSS_V4,
+		.gso_len = CONST_MSS_V4 + 1,
+		.r_num_mss = 0,
+		.r_len_last = CONST_MSS_V4,
+	},

Please also add a test where datalen > MSS < gso_len (with .tfail = true).

Quoted 21 lines
tools/testing/selftests/net/udpgso.c +7 -0
 	{
 		/* send a single MSS + 1B */
 		.tlen = CONST_MSS_V4 + 1,
@@ -205,6 +212,13 @@ struct testcase testcases_v6[] = {
 		.gso_len = CONST_MSS_V6,
 		.r_num_mss = 1,
 	},
+	{
+		/* datalen <= MSS < gso_len: will fall back to no GSO */
+		.tlen = CONST_MSS_V6,
+		.gso_len = CONST_MSS_V6 + 1,
+		.r_num_mss = 0,
+		.r_len_last = CONST_MSS_V6,
+	},
 	{
 		/* send a single MSS + 1B */
 		.tlen = CONST_MSS_V6 + 1,
Signature · 1 line
2.30.2
author
#2 lore raw
Quoted 150 lines · Willem de Bruijn
Quoted 35 lines · Yan Zhai

Commit 4094871db1d6 ("udp: only do GSO if # of segs > 1") avoided GSO for small packets. But the kernel currently dismisses GSO requests only after checking MTU/PMTU on gso_size. This means any packets, regardless of their payload sizes, could be dropped when PMTU becomes smaller than requested gso_size. We encountered this issue in production and it caused a reliability problem that new QUIC connection cannot be established before PMTU cache expired, while non GSO sockets still worked fine at the same time.

Ideally, do not check any GSO related constraints when payload size is smaller than requested gso_size, and return EMSGSIZE instead of EINVAL on MTU/PMTU check failure to be more specific on the error cause.

Fixes 4094871db1d6 ("udp: only do GSO if # of segs > 1")

--
v1->v2: add a missing MTU check when fall back to no GSO mode suggested by Willem de Bruijn <[email protected]>; Fixed up commit message to be more precise.

v1: https://lore.kernel.org/all/Z5cgWh%[email protected]/

---
 net/ipv4/udp.c                       | 28 +++++++++++++++++++---------
 net/ipv6/udp.c                       | 28 +++++++++++++++++++---------
 tools/testing/selftests/net/udpgso.c | 14 ++++++++++++++
 3 files changed, 52 insertions(+), 18 deletions(-)
net/ipv4/udp.c +0 -0
index c472c9a57cf6..0b5010238d05 100644
@@ -1141,9 +1141,20 @@ static int udp_send_skb(struct sk_buff *skb, struct flowi4 *fl4,
              const int hlen = skb_network_header_len(skb) +
                               sizeof(struct udphdr);
 

net/ipv4/udp.c static int udp_send_skb(struct sk_buff *skb, struct flowi4 *fl4, · line 1153

Quoted 10 lines
net/ipv4/udp.c +10 -0
+             if (datalen <= cork->gso_size) {
+                     /*
+                      * check MTU again: it's skipped previously when
+                      * gso_size != 0
+                      */
+                     if (hlen + datalen > cork->fragsize) {
+                             kfree_skb(skb);
+                             return -EMSGSIZE;
+                     }
+                     goto no_gso;

This is almost the same as the test below.

How about just

    if (hlen + min(cork->gso_size, datalen) > cork->fragsize)

And don't bypass the subsequent checks with a goto, or modify the rest of the code.

Quoted 93 lines
net/ipv4/udp.c +9 -9
+             }
              if (hlen + cork->gso_size > cork->fragsize) {
                      kfree_skb(skb);
-                     return -EINVAL;
+                     return -EMSGSIZE;
              }
              if (datalen > cork->gso_size * UDP_MAX_SEGMENTS) {
                      kfree_skb(skb);
@@ -1158,17 +1169,16 @@ static int udp_send_skb(struct sk_buff *skb, struct flowi4 *fl4,
                      return -EIO;
              }
 
-             if (datalen > cork->gso_size) {
-                     skb_shinfo(skb)->gso_size = cork->gso_size;
-                     skb_shinfo(skb)->gso_type = SKB_GSO_UDP_L4;
-                     skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(datalen,
-                                                              cork->gso_size);
+             skb_shinfo(skb)->gso_size = cork->gso_size;
+             skb_shinfo(skb)->gso_type = SKB_GSO_UDP_L4;
+             skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(datalen,
+                                                      cork->gso_size);
 
-                     /* Don't checksum the payload, skb will get segmented */
-                     goto csum_partial;
-             }
+             /* Don't checksum the payload, skb will get segmented */
+             goto csum_partial;
      }
 
+no_gso:
      if (is_udplite)                                  /*     UDP-Lite      */
              csum = udplite_csum(skb);
 
net/ipv6/udp.c +19 -9
index 6671daa67f4f..d97befa7f80d 100644
@@ -1389,9 +1389,20 @@ static int udp_v6_send_skb(struct sk_buff *skb, struct flowi6 *fl6,
              const int hlen = skb_network_header_len(skb) +
                               sizeof(struct udphdr);
 
+             if (datalen <= cork->gso_size) {
+                     /*
+                      * check MTU again: it's skipped previously when
+                      * gso_size != 0
+                      */
+                     if (hlen + datalen > cork->fragsize) {
+                             kfree_skb(skb);
+                             return -EMSGSIZE;
+                     }
+                     goto no_gso;
+             }
              if (hlen + cork->gso_size > cork->fragsize) {
                      kfree_skb(skb);
-                     return -EINVAL;
+                     return -EMSGSIZE;
              }
              if (datalen > cork->gso_size * UDP_MAX_SEGMENTS) {
                      kfree_skb(skb);
@@ -1406,17 +1417,16 @@ static int udp_v6_send_skb(struct sk_buff *skb, struct flowi6 *fl6,
                      return -EIO;
              }
 
-             if (datalen > cork->gso_size) {
-                     skb_shinfo(skb)->gso_size = cork->gso_size;
-                     skb_shinfo(skb)->gso_type = SKB_GSO_UDP_L4;
-                     skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(datalen,
-                                                              cork->gso_size);
+             skb_shinfo(skb)->gso_size = cork->gso_size;
+             skb_shinfo(skb)->gso_type = SKB_GSO_UDP_L4;
+             skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(datalen,
+                                                      cork->gso_size);
 
-                     /* Don't checksum the payload, skb will get segmented */
-                     goto csum_partial;
-             }
+             /* Don't checksum the payload, skb will get segmented */
+             goto csum_partial;
      }
 
+no_gso:
      if (is_udplite)
              csum = udplite_csum(skb);
      else if (udp_get_no_check6_tx(sk)) {   /* UDP csum disabled */
tools/testing/selftests/net/udpgso.c +1 -0
index 3f2fca02fec5..fb73f1c331fb 100644
@@ -102,6 +102,13 @@ struct testcase testcases_v4[] = {
              .gso_len = CONST_MSS_V4,
              .r_num_mss = 1,
      },
+     {
Quoted 9 lines

tools/testing/selftests/net/udpgso.c struct testcase testcases_v4[] = { · line 111

Quoted 6 lines
tools/testing/selftests/net/udpgso.c +6 -0
+             /* datalen <= MSS < gso_len: will fall back to no GSO */
+             .tlen = CONST_MSS_V4,
+             .gso_len = CONST_MSS_V4 + 1,
+             .r_num_mss = 0,
+             .r_len_last = CONST_MSS_V4,
+     },

Please also add a test where datalen > MSS < gso_len (with .tfail = true).

ACK on both comments.

Thanks
Yan

Quoted 23 lines
Quoted 21 lines
tools/testing/selftests/net/udpgso.c +7 -1
      {
              /* send a single MSS + 1B */
              .tlen = CONST_MSS_V4 + 1,
@@ -205,6 +212,13 @@ struct testcase testcases_v6[] = {
              .gso_len = CONST_MSS_V6,
              .r_num_mss = 1,
      },
+     {
+             /* datalen <= MSS < gso_len: will fall back to no GSO */
+             .tlen = CONST_MSS_V6,
+             .gso_len = CONST_MSS_V6 + 1,
+             .r_num_mss = 0,
+             .r_len_last = CONST_MSS_V6,
+     },
      {
              /* send a single MSS + 1B */
              .tlen = CONST_MSS_V6 + 1,
--

2.30.2