From: Wang Zhan <wang.zhan@smartx.com>
To: netdev@vger.kernel.org
Cc: davem@davemloft.net, edumazet@google.com, kuba@kernel.org,
pabeni@redhat.com, horms@kernel.org, keyong.sun@smartx.com,
Willem de Bruijn <willemdebruijn.kernel@gmail.com>,
Jason Wang <jasowangio@gmail.com>,
Andrew Lunn <andrew+netdev@lunn.ch>,
Aaron Conole <aconole@redhat.com>,
Eelco Chaudron <echaudro@redhat.com>,
Ilya Maximets <i.maximets@ovn.org>,
dev@openvswitch.org, Daniel Borkmann <daniel@iogearbox.net>,
Neal Cardwell <ncardwell@google.com>,
Kuniyuki Iwashima <kuniyu@google.com>,
Alice Mikityanska <alice@isovalent.com>,
David Laight <david.laight.linux@gmail.com>,
Wang Zhan <wang.zhan@smartx.com>
Subject: [PATCH net-next v3 5/5] net: net_test: add tests for bounded GSO segmentation
Date: Mon, 28 Sep 2026 12:41:02 +0800 [thread overview]
Message-ID: <20260928044102.1004310-6-wang.zhan@smartx.com> (raw)
In-Reply-To: <20260928044102.1004310-1-wang.zhan@smartx.com>
The GSO engine can now be asked to bound the number of MSS segments which
go into each output skb. Add KUnit coverage for it.
The parameterized GSO test gains a max_segs input and three cases: two
bounds which cut the input into two and three output skbs, and a bound of
one MSS, which must leave the ungrouped output of the unbounded path
alone. It drives skb_segment() directly, because the synthetic protocol
it uses has no gso_segment callback, and stores the bound in the GSO
control block itself.
The TCP test drives __skb_gso_segment() with a bound of two MSS and checks
that every output skb stays GSO, keeps its gso_size, and stays within the
bound. The length test runs a 200 KiB TCP skb through
validate_xmit_skb_list(), the caller which sets the bound, and checks that
the length declared by every output matches the L3 length of that output.
The limit test checks that the GSO size limit which netif_skb_features()
applies follows the packet's L3 protocol, for an IPv4 and an IPv6 skb,
also with the tag inside the frame.
Assisted-by: LLM
Signed-off-by: Wang Zhan <wang.zhan@smartx.com>
---
v3:
- add cases for the length a bounded output declares and for a bound of one
MSS, which must leave the output ungrouped
- cover the IPv4 half of the limit test, checking the protocol's own bit
- skip the TCP cases without CONFIG_INET and bound the expected index in
the parameterized loop
- free the skb and the device on the failure paths
- reserve headroom so the VLAN step needs no atomic allocation
- drop NETIF_F_TSO: the transmit path passes the features without it
v2: https://lore.kernel.org/20260918084651.3022878-5-wang.zhan@smartx.com/
v1: https://lore.kernel.org/20260917063854.2011613-5-wang.zhan@smartx.com/
---
net/core/net_test.c | 347 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 347 insertions(+)
diff --git a/net/core/net_test.c b/net/core/net_test.c
index 9c3a590865d26..a4f61398a90ab 100644
--- a/net/core/net_test.c
+++ b/net/core/net_test.c
@@ -4,7 +4,14 @@
/* GSO */
+#include <linux/if_ether.h>
+#include <linux/if_vlan.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <linux/netdevice.h>
#include <linux/skbuff.h>
+#include <linux/tcp.h>
+#include <net/gso.h>
static const char hdr[] = "abcdefgh";
#define GSO_TEST_SIZE 1000
@@ -34,6 +41,9 @@ enum gso_test_nr {
GSO_TEST_FRAG_LIST_PURE,
GSO_TEST_FRAG_LIST_NON_UNIFORM,
GSO_TEST_GSO_BY_FRAGS,
+ GSO_TEST_BOUNDED,
+ GSO_TEST_BOUNDED_MULTI,
+ GSO_TEST_BOUNDED_ONE_MSS,
};
struct gso_test_case {
@@ -46,10 +56,12 @@ struct gso_test_case {
const unsigned int *frags;
unsigned int nr_frag_skbs;
const unsigned int *frag_skbs;
+ unsigned int max_segs;
/* output as expected */
unsigned int nr_segs;
const unsigned int *segs;
+ bool segs_are_gso;
};
static struct gso_test_case cases[] = {
@@ -135,6 +147,54 @@ static struct gso_test_case cases[] = {
.nr_segs = 4,
.segs = (const unsigned int[]) { 100, 200, 300, 400 },
},
+ {
+ .id = GSO_TEST_BOUNDED,
+ .name = "bounded",
+ .linear_len = GSO_TEST_SIZE,
+ .nr_frags = 3,
+ .frags = (const unsigned int[]) {
+ GSO_TEST_SIZE, GSO_TEST_SIZE, 3,
+ },
+ .max_segs = 2,
+ .nr_segs = 2,
+ .segs = (const unsigned int[]) {
+ 2 * GSO_TEST_SIZE, GSO_TEST_SIZE + 3,
+ },
+ .segs_are_gso = true,
+ },
+ {
+ .id = GSO_TEST_BOUNDED_MULTI,
+ .name = "bounded_multi",
+ .linear_len = 2 * GSO_TEST_SIZE,
+ .nr_frags = 4,
+ .frags = (const unsigned int[]) {
+ GSO_TEST_SIZE, GSO_TEST_SIZE, GSO_TEST_SIZE, 3,
+ },
+ .max_segs = 2,
+ .nr_segs = 3,
+ .segs = (const unsigned int[]) {
+ 2 * GSO_TEST_SIZE, 2 * GSO_TEST_SIZE, GSO_TEST_SIZE + 3,
+ },
+ .segs_are_gso = true,
+ },
+ {
+ /*
+ * One MSS per skb is what the unbounded path produces, so a
+ * bound of a single segment must not change the output.
+ */
+ .id = GSO_TEST_BOUNDED_ONE_MSS,
+ .name = "bounded_one_mss",
+ .linear_len = GSO_TEST_SIZE,
+ .nr_frags = 3,
+ .frags = (const unsigned int[]) {
+ GSO_TEST_SIZE, GSO_TEST_SIZE, 3,
+ },
+ .max_segs = 1,
+ .nr_segs = 4,
+ .segs = (const unsigned int[]) {
+ GSO_TEST_SIZE, GSO_TEST_SIZE, GSO_TEST_SIZE, 3,
+ },
+ },
};
static void gso_test_case_to_desc(struct gso_test_case *t, char *desc)
@@ -226,6 +286,7 @@ static void gso_test_func(struct kunit *test)
if (tcase->id == GSO_TEST_FRAG_LIST_NON_UNIFORM)
features &= ~NETIF_F_SG;
+ SKB_GSO_CB(skb)->max_segs = tcase->max_segs;
segs = skb_segment(skb, features);
if (IS_ERR(segs)) {
KUNIT_FAIL(test, "segs error %pe", segs);
@@ -239,6 +300,7 @@ static void gso_test_func(struct kunit *test)
for (cur = segs, i = 0; cur; cur = next, i++) {
next = cur->next;
+ KUNIT_ASSERT_LT(test, i, tcase->nr_segs);
KUNIT_ASSERT_EQ(test, cur->len, sizeof(hdr) + tcase->segs[i]);
/* segs have skb->data pointing to the mac header */
@@ -247,6 +309,17 @@ static void gso_test_func(struct kunit *test)
/* header was copied to all segs */
KUNIT_ASSERT_EQ(test, memcmp(skb_mac_header(cur), hdr, sizeof(hdr)), 0);
+ if (tcase->segs_are_gso) {
+ KUNIT_EXPECT_TRUE(test, skb_is_gso(cur));
+ KUNIT_EXPECT_EQ(test, skb_shinfo(cur)->gso_size,
+ GSO_TEST_SIZE);
+ KUNIT_EXPECT_LE(test, skb_shinfo(cur)->gso_segs,
+ tcase->max_segs);
+ KUNIT_EXPECT_FALSE(test, skb_shinfo(cur)->gso_type &
+ SKB_GSO_PARTIAL);
+ } else if (tcase->max_segs) {
+ KUNIT_EXPECT_FALSE(test, skb_is_gso(cur));
+ }
/* last seg can be found through segs->prev pointer */
if (!next)
@@ -261,6 +334,277 @@ static void gso_test_func(struct kunit *test)
consume_skb(skb);
}
+#define GSO_TCP_HDR_LEN \
+ (ETH_HLEN + sizeof(struct iphdr) + sizeof(struct tcphdr))
+
+static struct sk_buff *gso_tcp_skb_new(unsigned int payload_len)
+{
+ struct sk_buff *skb;
+ struct ethhdr *eth;
+ struct tcphdr *th;
+ struct iphdr *iph;
+
+ skb = alloc_skb(GSO_TCP_HDR_LEN + payload_len, GFP_KERNEL);
+ if (!skb)
+ return NULL;
+ skb_put_zero(skb, GSO_TCP_HDR_LEN + payload_len);
+
+ skb_reset_mac_header(skb);
+ eth = eth_hdr(skb);
+ eth->h_proto = htons(ETH_P_IP);
+ skb->protocol = eth->h_proto;
+
+ skb_set_network_header(skb, ETH_HLEN);
+ iph = ip_hdr(skb);
+ iph->version = 4;
+ iph->ihl = sizeof(*iph) / 4;
+ iph->protocol = IPPROTO_TCP;
+ iph->tot_len = htons(sizeof(*iph) + sizeof(*th) + payload_len);
+
+ skb_set_transport_header(skb, ETH_HLEN + sizeof(*iph));
+ th = tcp_hdr(skb);
+ th->doff = sizeof(*th) / 4;
+
+ skb->ip_summed = CHECKSUM_PARTIAL;
+ skb->csum_start = skb_transport_header(skb) - skb->head;
+ skb->csum_offset = offsetof(struct tcphdr, check);
+ skb_shinfo(skb)->gso_type = SKB_GSO_TCPV4;
+ skb_shinfo(skb)->gso_size = GSO_TEST_SIZE;
+ skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(payload_len, GSO_TEST_SIZE);
+
+ return skb;
+}
+
+/*
+ * The transmit path passes the features of the device which rejects this
+ * skb, with the GSO bits cleared, so the engine segments it.
+ */
+static void gso_test_tcp_bounded_segment(struct kunit *test)
+{
+ netdev_features_t features = NETIF_F_SG | NETIF_F_HW_CSUM;
+ const unsigned int payload_len = 3 * GSO_TEST_SIZE + 3;
+ struct sk_buff *skb, *segs, *cur, *next;
+ const unsigned int expected[] = {
+ 2 * GSO_TEST_SIZE, GSO_TEST_SIZE + 3,
+ };
+ const unsigned int max_segs = 2;
+ int i = 0;
+
+ if (!IS_ENABLED(CONFIG_INET))
+ kunit_skip(test, "requires CONFIG_INET");
+
+ skb = gso_tcp_skb_new(payload_len);
+ if (!skb) {
+ KUNIT_FAIL(test, "no skb");
+ return;
+ }
+
+ segs = __skb_gso_segment(skb, features, true, max_segs);
+ if (IS_ERR_OR_NULL(segs)) {
+ KUNIT_FAIL(test, "segs error %pe", segs);
+ consume_skb(skb);
+ return;
+ }
+
+ for (cur = segs; cur; cur = next, i++) {
+ next = cur->next;
+
+ KUNIT_ASSERT_LT(test, i, ARRAY_SIZE(expected));
+ KUNIT_EXPECT_EQ(test, cur->len,
+ GSO_TCP_HDR_LEN + expected[i]);
+ KUNIT_EXPECT_TRUE(test, skb_is_gso(cur));
+ KUNIT_EXPECT_EQ(test, skb_shinfo(cur)->gso_size,
+ GSO_TEST_SIZE);
+ KUNIT_EXPECT_LE(test, skb_shinfo(cur)->gso_segs, max_segs);
+
+ consume_skb(cur);
+ }
+
+ KUNIT_EXPECT_EQ(test, i, ARRAY_SIZE(expected));
+ consume_skb(skb);
+}
+
+#define GSO_TCP6_HDR_LEN \
+ (ETH_HLEN + sizeof(struct ipv6hdr) + sizeof(struct tcphdr))
+
+static struct sk_buff *gso_tcp6_skb_new(unsigned int payload_len)
+{
+ struct ipv6hdr *ip6h;
+ struct sk_buff *skb;
+ struct ethhdr *eth;
+ struct tcphdr *th;
+
+ skb = alloc_skb(GSO_TCP6_HDR_LEN + payload_len, GFP_KERNEL);
+ if (!skb)
+ return NULL;
+ skb_reserve(skb, NET_SKB_PAD);
+ skb_put_zero(skb, GSO_TCP6_HDR_LEN + payload_len);
+
+ skb_reset_mac_header(skb);
+ eth = eth_hdr(skb);
+ eth->h_proto = htons(ETH_P_IPV6);
+ skb->protocol = eth->h_proto;
+
+ skb_set_network_header(skb, ETH_HLEN);
+ ip6h = ipv6_hdr(skb);
+ ip6h->version = 6;
+ ip6h->nexthdr = IPPROTO_TCP;
+ ip6h->payload_len = htons(sizeof(*th) + payload_len);
+
+ skb_set_transport_header(skb, ETH_HLEN + sizeof(*ip6h));
+ th = tcp_hdr(skb);
+ th->doff = sizeof(*th) / 4;
+
+ skb->ip_summed = CHECKSUM_PARTIAL;
+ skb->csum_start = skb_transport_header(skb) - skb->head;
+ skb->csum_offset = offsetof(struct tcphdr, check);
+ skb_shinfo(skb)->gso_type = SKB_GSO_TCPV6;
+ skb_shinfo(skb)->gso_size = GSO_TEST_SIZE;
+ skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(payload_len, GSO_TEST_SIZE);
+
+ return skb;
+}
+
+/*
+ * The device GSO size limit is per L3 protocol, so only a lowered limit of
+ * the packet's own protocol may cost it the TSO bit, and a tag inside the
+ * frame, which replaces skb->protocol with the ethertype, must not change
+ * which limit applies. Takes ownership of @skb.
+ */
+static void gso_test_tcp_l3_limit(struct kunit *test, struct net_device *dev,
+ struct sk_buff *skb, netdev_features_t tso,
+ bool ipv6)
+{
+ unsigned int other = GSO_LEGACY_MAX_SIZE;
+ unsigned int own = GSO_MAX_SIZE;
+
+ /* The skb fits the limit of its own L3 protocol, not the other's. */
+ dev->gso_max_size = ipv6 ? own : other;
+ dev->gso_ipv4_max_size = ipv6 ? other : own;
+ KUNIT_EXPECT_TRUE(test, netif_skb_features(skb) & tso);
+
+ /* ...and the other way around. */
+ swap(own, other);
+ dev->gso_max_size = ipv6 ? own : other;
+ dev->gso_ipv4_max_size = ipv6 ? other : own;
+ KUNIT_EXPECT_FALSE(test, netif_skb_features(skb) & tso);
+
+ skb = vlan_insert_tag_set_proto(skb, htons(ETH_P_8021Q), 0);
+ if (!skb) {
+ KUNIT_FAIL(test, "no tagged skb");
+ return;
+ }
+ KUNIT_EXPECT_TRUE(test, skb->protocol == htons(ETH_P_8021Q));
+
+ swap(own, other);
+ dev->gso_max_size = ipv6 ? own : other;
+ dev->gso_ipv4_max_size = ipv6 ? other : own;
+ KUNIT_EXPECT_TRUE(test, netif_skb_features(skb) & tso);
+
+ swap(own, other);
+ dev->gso_max_size = ipv6 ? own : other;
+ dev->gso_ipv4_max_size = ipv6 ? other : own;
+ KUNIT_EXPECT_FALSE(test, netif_skb_features(skb) & tso);
+
+ consume_skb(skb);
+}
+
+static void gso_test_tcp_limit_l3_proto(struct kunit *test)
+{
+ static const struct net_device_ops dummy_netdev_ops = { };
+ const unsigned int payload_len = 100 * 1024;
+ struct net_device *dev;
+ struct sk_buff *skb;
+
+ dev = alloc_etherdev(0);
+ if (!dev) {
+ KUNIT_FAIL(test, "no net_device");
+ return;
+ }
+ dev->netdev_ops = &dummy_netdev_ops;
+ dev->hw_features = NETIF_F_SG | NETIF_F_HW_CSUM | NETIF_F_TSO |
+ NETIF_F_TSO6;
+ dev->features = dev->hw_features;
+ dev->vlan_features = dev->hw_features;
+
+ skb = gso_tcp6_skb_new(payload_len);
+ if (!skb) {
+ KUNIT_FAIL(test, "no IPv6 skb");
+ goto free_dev;
+ }
+ skb->dev = dev;
+ gso_test_tcp_l3_limit(test, dev, skb, NETIF_F_TSO6, true);
+
+ skb = gso_tcp_skb_new(payload_len);
+ if (!skb) {
+ KUNIT_FAIL(test, "no IPv4 skb");
+ goto free_dev;
+ }
+ skb->dev = dev;
+ gso_test_tcp_l3_limit(test, dev, skb, NETIF_F_TSO, false);
+
+free_dev:
+ free_netdev(dev);
+}
+
+/*
+ * A bounded output is a plain GSO skb, so the whole length lands in the 16-bit
+ * L3 length field. A device which accepts GSO skbs above 64 KiB would
+ * otherwise be handed outputs whose length field truncates.
+ */
+static void gso_test_tcp_resegment_l3_len(struct kunit *test)
+{
+ static const struct net_device_ops dummy_netdev_ops = { };
+ const unsigned int payload_len = 200 * 1024;
+ struct sk_buff *skb, *segs, *cur, *next;
+ unsigned int nr_segs = 0;
+ struct net_device *dev;
+ bool again = false;
+
+ if (!IS_ENABLED(CONFIG_INET))
+ kunit_skip(test, "requires CONFIG_INET");
+
+ dev = alloc_etherdev(0);
+ if (!dev) {
+ KUNIT_FAIL(test, "no net_device");
+ return;
+ }
+ dev->netdev_ops = &dummy_netdev_ops;
+ dev->hw_features = NETIF_F_SG | NETIF_F_HW_CSUM | NETIF_F_TSO;
+ dev->features = dev->hw_features;
+ dev->vlan_features = dev->hw_features;
+ dev->gso_ipv4_max_size = 120 * 1024;
+
+ skb = gso_tcp_skb_new(payload_len);
+ if (!skb) {
+ KUNIT_FAIL(test, "no skb");
+ goto free_dev;
+ }
+ skb->dev = dev;
+
+ segs = validate_xmit_skb_list(skb, dev, &again);
+ if (IS_ERR_OR_NULL(segs)) {
+ KUNIT_FAIL(test, "segs error %pe", segs);
+ goto free_dev;
+ }
+
+ for (cur = segs; cur; cur = next, nr_segs++) {
+ next = cur->next;
+ cur->next = NULL;
+
+ KUNIT_EXPECT_TRUE(test, skb_is_gso(cur));
+ KUNIT_EXPECT_EQ(test, ntohs(ip_hdr(cur)->tot_len),
+ cur->len - (skb_network_header(cur) -
+ skb_mac_header(cur)));
+ consume_skb(cur);
+ }
+
+ KUNIT_EXPECT_EQ(test, nr_segs, 4);
+
+free_dev:
+ free_netdev(dev);
+}
+
/* IP tunnel flags */
#include <net/ip_tunnels.h>
@@ -372,6 +716,9 @@ static void ip_tunnel_flags_test_run(struct kunit *test)
static struct kunit_case net_test_cases[] = {
KUNIT_CASE_PARAM(gso_test_func, gso_test_gen_params),
+ KUNIT_CASE(gso_test_tcp_bounded_segment),
+ KUNIT_CASE(gso_test_tcp_limit_l3_proto),
+ KUNIT_CASE(gso_test_tcp_resegment_l3_len),
KUNIT_CASE_PARAM(ip_tunnel_flags_test_run,
ip_tunnel_flags_test_gen_params),
{ },
--
2.47.3
next prev parent reply other threads:[~2026-09-28 4:41 UTC|newest]
Thread overview: 27+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-28 4:40 [PATCH net-next v3 0/5] net: resegment oversized TCP GSO skbs Wang Zhan
2026-09-28 4:40 ` [PATCH net-next v3 1/5] net: core: use the packet's L3 protocol for the GSO size limit Wang Zhan
2026-09-28 23:36 ` Willem de Bruijn
2026-09-29 3:44 ` Wang Zhan
2026-09-29 4:01 ` Wang Zhan
2026-09-29 14:59 ` Willem de Bruijn
2026-09-28 4:40 ` [PATCH net-next v3 2/5] net: core: factor out the GSO device limit check Wang Zhan
2026-09-28 23:37 ` Willem de Bruijn
2026-09-29 7:47 ` Paolo Abeni
2026-09-28 4:41 ` [PATCH net-next v3 3/5] net: gso: support bounded TCP segmentation Wang Zhan
2026-09-28 23:39 ` Willem de Bruijn
2026-09-30 4:41 ` netdev-bot+sashiko
2026-09-28 4:41 ` [PATCH net-next v3 4/5] net: core: resegment oversized TCP GSO skbs Wang Zhan
2026-09-28 23:47 ` Willem de Bruijn
2026-09-29 10:25 ` Wang Zhan
2026-09-29 15:00 ` Willem de Bruijn
2026-09-30 4:41 ` netdev-bot+sashiko
2026-09-28 4:41 ` Wang Zhan [this message]
2026-09-28 23:59 ` [PATCH net-next v3 5/5] net: net_test: add tests for bounded GSO segmentation Willem de Bruijn
2026-09-29 10:30 ` Wang Zhan
2026-09-29 15:01 ` Willem de Bruijn
2026-09-30 4:41 ` netdev-bot+sashiko
2026-09-28 4:45 ` [PATCH net-next v3 0/5] net: resegment oversized TCP GSO skbs netdev-bot+sinfo
2026-09-28 5:49 ` Wang Zhan
2026-09-28 23:34 ` Willem de Bruijn
2026-09-29 7:27 ` Paolo Abeni
2026-09-29 11:50 ` Wang Zhan
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260928044102.1004310-6-wang.zhan@smartx.com \
--to=wang.zhan@smartx.com \
--cc=aconole@redhat.com \
--cc=alice@isovalent.com \
--cc=andrew+netdev@lunn.ch \
--cc=daniel@iogearbox.net \
--cc=davem@davemloft.net \
--cc=david.laight.linux@gmail.com \
--cc=dev@openvswitch.org \
--cc=echaudro@redhat.com \
--cc=edumazet@google.com \
--cc=horms@kernel.org \
--cc=i.maximets@ovn.org \
--cc=jasowangio@gmail.com \
--cc=keyong.sun@smartx.com \
--cc=kuba@kernel.org \
--cc=kuniyu@google.com \
--cc=ncardwell@google.com \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=willemdebruijn.kernel@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.