* [PATCH net-next 1/4] net: core: factor out the GSO device limit check
2026-09-17 6:38 [PATCH net-next 0/4] net: resegment oversized TCP GSO skbs Wang Zhan
@ 2026-09-17 6:38 ` Wang Zhan
2026-09-17 6:38 ` [PATCH net-next 2/4] net: gso: support bounded TCP segmentation Wang Zhan
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Wang Zhan @ 2026-09-17 6:38 UTC (permalink / raw)
To: netdev
Cc: davem, edumazet, kuba, pabeni, horms, keyong.sun, Andrew Lunn,
Jason Wang, Willem de Bruijn, Neal Cardwell, Kuniyuki Iwashima,
Ilya Maximets, Aaron Conole, Eelco Chaudron, Alice Mikityanska,
Wang Zhan
gso_features_check() decides whether an egress device can offload a GSO
skb as a single TSO frame by comparing the segment count and the frame
length against the device limits. Move that test into a helper so that
the bounded resegmentation path added by a later patch can ask the same
question without repeating the two expressions. Make the size limit
lookup take the protocol as an argument, because that path has to ask
for the limit of a protocol other than the one in skb->protocol.
No functional changes.
Assisted-by: LLM
Signed-off-by: Wang Zhan <wang.zhan@smartx.com>
---
include/linux/netdevice.h | 4 ++--
net/core/dev.c | 14 ++++++++------
2 files changed, 10 insertions(+), 8 deletions(-)
diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h
index 1f0710eef185b..427d0d5b94e49 100644
--- a/include/linux/netdevice.h
+++ b/include/linux/netdevice.h
@@ -5570,10 +5570,10 @@ netif_get_gro_max_size(const struct net_device *dev, const struct sk_buff *skb)
}
static inline unsigned int
-netif_get_gso_max_size(const struct net_device *dev, const struct sk_buff *skb)
+netif_get_gso_max_size(const struct net_device *dev, __be16 protocol)
{
/* pairs with WRITE_ONCE() in netif_set_gso(_ipv4)_max_size() */
- return skb->protocol == htons(ETH_P_IPV6) ?
+ return protocol == htons(ETH_P_IPV6) ?
READ_ONCE(dev->gso_max_size) :
READ_ONCE(dev->gso_ipv4_max_size);
}
diff --git a/net/core/dev.c b/net/core/dev.c
index ecfbd72d5d1a4..428b5d057287c 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -3834,16 +3834,18 @@ static bool skb_gso_has_extension_hdr(const struct sk_buff *skb)
skb_inner_network_header_len(skb) != sizeof(struct ipv6hdr)));
}
+static bool gso_within_device_limits(const struct sk_buff *skb,
+ const struct net_device *dev)
+{
+ return skb_shinfo(skb)->gso_segs <= READ_ONCE(dev->gso_max_segs) &&
+ skb->len < netif_get_gso_max_size(dev, skb->protocol);
+}
+
static netdev_features_t gso_features_check(const struct sk_buff *skb,
struct net_device *dev,
netdev_features_t features)
{
- u16 gso_segs = skb_shinfo(skb)->gso_segs;
-
- if (gso_segs > READ_ONCE(dev->gso_max_segs))
- return features & ~NETIF_F_GSO_MASK;
-
- if (unlikely(skb->len >= netif_get_gso_max_size(dev, skb)))
+ if (!gso_within_device_limits(skb, dev))
return features & ~NETIF_F_GSO_MASK;
if (!skb_shinfo(skb)->gso_type) {
--
2.47.3
^ permalink raw reply related [flat|nested] 5+ messages in thread* [PATCH net-next 2/4] net: gso: support bounded TCP segmentation
2026-09-17 6:38 [PATCH net-next 0/4] net: resegment oversized TCP GSO skbs Wang Zhan
2026-09-17 6:38 ` [PATCH net-next 1/4] net: core: factor out the GSO device limit check Wang Zhan
@ 2026-09-17 6:38 ` Wang Zhan
2026-09-17 6:38 ` [PATCH net-next 3/4] net: core: resegment oversized TCP GSO skbs Wang Zhan
2026-09-17 6:38 ` [PATCH net-next 4/4] net: net_test: add tests for bounded GSO segmentation Wang Zhan
3 siblings, 0 replies; 5+ messages in thread
From: Wang Zhan @ 2026-09-17 6:38 UTC (permalink / raw)
To: netdev
Cc: davem, edumazet, kuba, pabeni, horms, keyong.sun, Andrew Lunn,
Jason Wang, Willem de Bruijn, Neal Cardwell, Kuniyuki Iwashima,
Ilya Maximets, Aaron Conole, Eelco Chaudron, Alice Mikityanska,
Wang Zhan
The bounded resegmentation added by the next patch splits an oversized TCP
GSO skb into several GSO skbs which fit the device limits. That needs the
GSO engine to group several MSS segments into one output skb, so let
callers bound the number of MSS segments each output skb carries and pass
the bound through the existing __skb_gso_segment() entry point. Ordinary
callers use zero for no limit.
skb_segment() only groups several MSS into one output skb when the device
advertises NETIF_F_GSO_PARTIAL, or when the skb has a frag_list which can
be split into uniform pieces, and falls back to one segment per skb
otherwise. A caller which passes a bound asks for that grouping
regardless, so the frag_list check is skipped when max_segs is set. Every
other caller keeps it, and the bounded path is only used for skbs which do
not carry a frag_list.
The output stays a GSO skb: gso_size is the original MSS and gso_segs is
the number of MSS it holds, so a downstream device can still perform
ordinary TSO. Store the bound in the existing skb_gso_cb scratch context,
alongside the call-local data_offset and mac_offset fields, so that the
segmentation methods keep their signature. A zero max_segs value means
that no bound is active; it is not a persistent skb flag. Clear the value
when each output skb copies the input header so the temporary limit is not
propagated to the next GSO call.
Assisted-by: LLM
Signed-off-by: Wang Zhan <wang.zhan@smartx.com>
---
drivers/net/tap.c | 2 +-
include/net/gso.h | 6 ++++--
include/net/udp.h | 2 +-
net/core/gso.c | 5 ++++-
net/core/skbuff.c | 14 ++++++++++++--
net/ipv4/tcp_offload.c | 3 ++-
net/openvswitch/datapath.c | 2 +-
7 files changed, 25 insertions(+), 9 deletions(-)
diff --git a/drivers/net/tap.c b/drivers/net/tap.c
index ff67d99deb39e..57761aa1a983a 100644
--- a/drivers/net/tap.c
+++ b/drivers/net/tap.c
@@ -278,7 +278,7 @@ rx_handler_result_t tap_handle_frame(struct sk_buff **pskb)
if (q->flags & IFF_VNET_HDR)
features |= tap->tap_features;
if (netif_needs_gso(skb, features)) {
- struct sk_buff *segs = __skb_gso_segment(skb, features, false);
+ struct sk_buff *segs = __skb_gso_segment(skb, features, false, 0);
struct sk_buff *next;
if (IS_ERR(segs)) {
diff --git a/include/net/gso.h b/include/net/gso.h
index 29975440cad51..cd4205090dd78 100644
--- a/include/net/gso.h
+++ b/include/net/gso.h
@@ -19,6 +19,7 @@ struct skb_gso_cb {
int encap_level;
__wsum csum;
__u16 csum_start;
+ __u16 max_segs; /* Max MSS segments in one output GSO skb, 0 = no limit */
};
#define SKB_GSO_CB_OFFSET 32
#define SKB_GSO_CB(skb) ((struct skb_gso_cb *)((skb)->cb + SKB_GSO_CB_OFFSET))
@@ -75,12 +76,13 @@ static inline __sum16 gso_make_checksum(struct sk_buff *skb, __wsum res)
}
struct sk_buff *__skb_gso_segment(struct sk_buff *skb,
- netdev_features_t features, bool tx_path);
+ netdev_features_t features, bool tx_path,
+ unsigned int max_segs);
static inline struct sk_buff *skb_gso_segment(struct sk_buff *skb,
netdev_features_t features)
{
- return __skb_gso_segment(skb, features, true);
+ return __skb_gso_segment(skb, features, true, 0);
}
struct sk_buff *skb_eth_gso_segment(struct sk_buff *skb,
diff --git a/include/net/udp.h b/include/net/udp.h
index 1fee17274745f..5bc25dcf25fba 100644
--- a/include/net/udp.h
+++ b/include/net/udp.h
@@ -613,7 +613,7 @@ static inline struct sk_buff *udp_rcv_segment(struct sock *sk,
/* the GSO CB lays after the UDP one, no need to save and restore any
* CB fragment
*/
- segs = __skb_gso_segment(skb, features, false);
+ segs = __skb_gso_segment(skb, features, false, 0);
if (IS_ERR_OR_NULL(segs)) {
drop_count = skb_shinfo(skb)->gso_segs;
goto drop;
diff --git a/net/core/gso.c b/net/core/gso.c
index bcd156372f4df..157f2bfdca128 100644
--- a/net/core/gso.c
+++ b/net/core/gso.c
@@ -77,6 +77,7 @@ static bool skb_needs_check(const struct sk_buff *skb, bool tx_path)
* @skb: buffer to segment
* @features: features for the output path (see dev->features)
* @tx_path: whether it is called in TX path
+ * @max_segs: maximum MSS segments per output GSO skb, 0 means no limit
*
* This function segments the given skb and returns a list of segments.
*
@@ -86,7 +87,8 @@ static bool skb_needs_check(const struct sk_buff *skb, bool tx_path)
* Segmentation preserves SKB_GSO_CB_OFFSET bytes of previous skb cb.
*/
struct sk_buff *__skb_gso_segment(struct sk_buff *skb,
- netdev_features_t features, bool tx_path)
+ netdev_features_t features, bool tx_path,
+ unsigned int max_segs)
{
struct sk_buff *segs;
@@ -117,6 +119,7 @@ struct sk_buff *__skb_gso_segment(struct sk_buff *skb,
SKB_GSO_CB(skb)->mac_offset = skb_headroom(skb);
SKB_GSO_CB(skb)->encap_level = 0;
+ SKB_GSO_CB(skb)->max_segs = min_t(unsigned int, max_segs, U16_MAX);
skb_reset_mac_header(skb);
skb_reset_mac_len(skb);
diff --git a/net/core/skbuff.c b/net/core/skbuff.c
index 9648782fe8cba..da606fdbac3a4 100644
--- a/net/core/skbuff.c
+++ b/net/core/skbuff.c
@@ -4793,6 +4793,7 @@ struct sk_buff *skb_segment(struct sk_buff *head_skb,
struct sk_buff *segs = NULL;
struct sk_buff *tail = NULL;
struct sk_buff *list_skb = skb_shinfo(head_skb)->frag_list;
+ unsigned int max_segs = SKB_GSO_CB(head_skb)->max_segs;
unsigned int mss = skb_shinfo(head_skb)->gso_size;
bool gso_by_frags = mss == GSO_BY_FRAGS;
unsigned int doffset = head_skb->data - skb_mac_header(head_skb);
@@ -4839,7 +4840,7 @@ struct sk_buff *skb_segment(struct sk_buff *head_skb,
csum = !!can_checksum_protocol(features, proto);
if (sg && csum && !gso_by_frags) {
- if (!(features & NETIF_F_GSO_PARTIAL)) {
+ if (!max_segs && !(features & NETIF_F_GSO_PARTIAL)) {
struct sk_buff *iter;
unsigned int frag_len;
@@ -4874,7 +4875,10 @@ struct sk_buff *skb_segment(struct sk_buff *head_skb,
* now.
*/
DEBUG_NET_WARN_ON_ONCE(len / mss > GSO_MAX_SEGS);
- partial_segs = min(len / mss, GSO_MAX_SEGS);
+ if (max_segs)
+ partial_segs = min(len / mss, max_segs);
+ else
+ partial_segs = min(len / mss, GSO_MAX_SEGS);
if (partial_segs > 1)
mss *= partial_segs;
else
@@ -4975,6 +4979,12 @@ struct sk_buff *skb_segment(struct sk_buff *head_skb,
__copy_skb_header(nskb, head_skb);
+ /*
+ * max_segs is a per-call limit, so output skbs must not
+ * inherit it from the input skb.
+ */
+ SKB_GSO_CB(nskb)->max_segs = 0;
+
skb_headers_offset_update(nskb, skb_headroom(nskb) - headroom);
skb_reset_mac_len(nskb);
diff --git a/net/ipv4/tcp_offload.c b/net/ipv4/tcp_offload.c
index e74d99ca9face..a4076318c5352 100644
--- a/net/ipv4/tcp_offload.c
+++ b/net/ipv4/tcp_offload.c
@@ -164,7 +164,8 @@ struct sk_buff *tcp_gso_segment(struct sk_buff *skb,
if (unlikely(skb->len <= mss))
goto out;
- if (skb_gso_ok(skb, features | NETIF_F_GSO_ROBUST)) {
+ if (!SKB_GSO_CB(skb)->max_segs &&
+ skb_gso_ok(skb, features | NETIF_F_GSO_ROBUST)) {
/* Packet is from an untrusted source, reset gso_segs. */
skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(skb->len, mss);
diff --git a/net/openvswitch/datapath.c b/net/openvswitch/datapath.c
index 2187034143255..e793aead68372 100644
--- a/net/openvswitch/datapath.c
+++ b/net/openvswitch/datapath.c
@@ -375,7 +375,7 @@ static int queue_gso_packets(struct datapath *dp, struct sk_buff *skb,
int err;
BUILD_BUG_ON(sizeof(*OVS_CB(skb)) > SKB_GSO_CB_OFFSET);
- segs = __skb_gso_segment(skb, NETIF_F_SG, false);
+ segs = __skb_gso_segment(skb, NETIF_F_SG, false, 0);
if (IS_ERR(segs))
return PTR_ERR(segs);
if (segs == NULL)
--
2.47.3
^ permalink raw reply related [flat|nested] 5+ messages in thread* [PATCH net-next 3/4] net: core: resegment oversized TCP GSO skbs
2026-09-17 6:38 [PATCH net-next 0/4] net: resegment oversized TCP GSO skbs Wang Zhan
2026-09-17 6:38 ` [PATCH net-next 1/4] net: core: factor out the GSO device limit check Wang Zhan
2026-09-17 6:38 ` [PATCH net-next 2/4] net: gso: support bounded TCP segmentation Wang Zhan
@ 2026-09-17 6:38 ` Wang Zhan
2026-09-17 6:38 ` [PATCH net-next 4/4] net: net_test: add tests for bounded GSO segmentation Wang Zhan
3 siblings, 0 replies; 5+ messages in thread
From: Wang Zhan @ 2026-09-17 6:38 UTC (permalink / raw)
To: netdev
Cc: davem, edumazet, kuba, pabeni, horms, keyong.sun, Andrew Lunn,
Jason Wang, Willem de Bruijn, Neal Cardwell, Kuniyuki Iwashima,
Ilya Maximets, Aaron Conole, Eelco Chaudron, Alice Mikityanska,
Wang Zhan
A GSO skb which exceeds an egress device limit loses its GSO feature mask
and is segmented into individual packets. This is unnecessarily expensive
when the device can still offload smaller TCP GSO skbs, which is easy to
hit once one hop of a BIG TCP path raises gso_max_size and the next one
does not.
For an unencapsulated TCP GSO skb which exceeds gso_max_size or
gso_max_segs, work out how many MSS segments each output skb may carry and
resegment the skb with that bound instead. Keep the features computed
without the GSO limit checks, which say whether the device offloads the
GSO type at all. Encapsulated and frag-list skbs, GSO types the device
cannot offload, and bounds below two segments keep the existing full
segmentation path. The result obeys the GSO feature and limit contract the
device already advertises, so apply it automatically, without extra device
state or a userspace control.
The check runs on the skb which is handed to the driver, after
validate_xmit_vlan() and sk_validate_xmit_skb(), and costs one extra
ndo_features_check() on the oversized path, against segmenting the skb
into individual packets. That position is also why the limit follows the
L3 protocol rather than skb->protocol: validate_xmit_vlan() replaces the
latter with the VLAN ethertype when it pushes the tag inside the skb.
Measured on a veth -> bridge -> TAP -> guest virtio-net path, with BIG TCP
enabled on the veth endpoints and left off in the guest, so the skbs which
the veth hop accepts have to be segmented before the TAP device. A single
iperf3 TCP flow, six alternating runs per state (`-t 15 -O 5`, fixed CPU
affinity and port tuple). The middle column is the same tree with the
resegmentation disabled:
protocol no BIG TCP mixed, no reseg mixed, resegmented
TCP/IPv4 51.550 Gbps 15.850 Gbps 52.617 Gbps
TCP/IPv6 52.050 Gbps 15.783 Gbps 51.933 Gbps
Coefficient of variation for the two mixed columns was 0.48% and 0.82%
for IPv4 and 0.44% and 0.44% for IPv6. A BIG TCP hop which feeds a 64 KiB
hop loses 69% of the throughput of a path which never enables BIG TCP at
all; bounded resegmentation recovers it, 3.3x over the existing
segmentation path and within noise of the no BIG TCP baseline.
Assisted-by: LLM
Signed-off-by: Wang Zhan <wang.zhan@smartx.com>
---
net/core/dev.c | 113 ++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 106 insertions(+), 7 deletions(-)
diff --git a/net/core/dev.c b/net/core/dev.c
index 428b5d057287c..ae551d524b877 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -3834,18 +3834,24 @@ static bool skb_gso_has_extension_hdr(const struct sk_buff *skb)
skb_inner_network_header_len(skb) != sizeof(struct ipv6hdr)));
}
+/*
+ * Does @skb fit the GSO limits of @dev? The size limit depends on the L3
+ * protocol, which validate_xmit_vlan() replaces with the VLAN ethertype when
+ * it pushes the tag inside the skb, so look behind the tag.
+ */
static bool gso_within_device_limits(const struct sk_buff *skb,
const struct net_device *dev)
{
return skb_shinfo(skb)->gso_segs <= READ_ONCE(dev->gso_max_segs) &&
- skb->len < netif_get_gso_max_size(dev, skb->protocol);
+ skb->len < netif_get_gso_max_size(dev, vlan_get_protocol(skb));
}
static netdev_features_t gso_features_check(const struct sk_buff *skb,
struct net_device *dev,
- netdev_features_t features)
+ netdev_features_t features,
+ bool check_limits)
{
- if (!gso_within_device_limits(skb, dev))
+ if (check_limits && !gso_within_device_limits(skb, dev))
return features & ~NETIF_F_GSO_MASK;
if (!skb_shinfo(skb)->gso_type) {
@@ -3894,13 +3900,15 @@ static netdev_features_t gso_features_check(const struct sk_buff *skb,
return features;
}
-netdev_features_t netif_skb_features(struct sk_buff *skb)
+static netdev_features_t __netif_skb_features(struct sk_buff *skb,
+ bool check_gso_limits)
{
struct net_device *dev = skb->dev;
netdev_features_t features = dev->features;
if (skb_is_gso(skb))
- features = gso_features_check(skb, dev, features);
+ features = gso_features_check(skb, dev, features,
+ check_gso_limits);
/* If encapsulation offload request, verify we are testing
* hardware encapsulation features instead of standard
@@ -3923,8 +3931,79 @@ netdev_features_t netif_skb_features(struct sk_buff *skb)
return harmonize_features(skb, features);
}
+
+netdev_features_t netif_skb_features(struct sk_buff *skb)
+{
+ return __netif_skb_features(skb, true);
+}
EXPORT_SYMBOL(netif_skb_features);
+static bool skb_can_gso_resegment(struct sk_buff *skb,
+ netdev_features_t features)
+{
+ __be16 protocol;
+
+ if (!net_gso_ok(features | NETIF_F_GSO_ROBUST,
+ skb_shinfo(skb)->gso_type))
+ return false;
+
+ if (!(features & NETIF_F_SG))
+ return false;
+
+ protocol = skb_network_protocol(skb, NULL);
+ if (!protocol || !can_checksum_protocol(features, protocol))
+ return false;
+
+ /*
+ * The TCP frag-list path does not carry the bounded segment
+ * limit through skb_segment_list(). Keep bounded resegmentation
+ * on the regular skb path until that support is added.
+ */
+ if (skb_has_frag_list(skb))
+ return false;
+
+ return true;
+}
+
+static unsigned int
+skb_gso_resegment_max_segs(struct sk_buff *skb, struct net_device *dev,
+ netdev_features_t features)
+{
+ unsigned int mss = skb_shinfo(skb)->gso_size;
+ unsigned int hdr_len, max_segs;
+ unsigned int gso_max_size;
+ struct tcphdr _tcph, *th;
+
+ gso_max_size = netif_get_gso_max_size(dev, vlan_get_protocol(skb));
+
+ if (!skb_is_gso(skb) || !skb_is_gso_tcp(skb) ||
+ skb->encapsulation || mss == GSO_BY_FRAGS ||
+ !skb_mac_header_was_set(skb) ||
+ !skb_transport_header_was_set(skb) ||
+ !skb_can_gso_resegment(skb, features))
+ return 0;
+
+ th = skb_header_pointer(skb, skb_transport_offset(skb), sizeof(_tcph),
+ &_tcph);
+ if (!th || th->doff < sizeof(*th) / 4)
+ return 0;
+
+ hdr_len = skb_transport_header(skb) - skb_mac_header(skb) +
+ th->doff * 4;
+ if (gso_max_size <= hdr_len + mss)
+ return 0;
+
+ /*
+ * gso_within_device_limits() accepts gso_segs == gso_max_segs but
+ * rejects skb->len >= gso_max_size, so only the size bound needs - 1.
+ */
+ max_segs = (gso_max_size - hdr_len - 1) / mss;
+ max_segs = min_t(unsigned int, max_segs,
+ READ_ONCE(dev->gso_max_segs));
+
+ return max_segs > 1 ? max_segs : 0;
+}
+
static int xmit_one(struct sk_buff *skb, struct net_device *dev,
struct netdev_queue *txq, bool more)
{
@@ -4073,6 +4152,7 @@ static struct sk_buff *validate_xmit_unreadable_skb(struct sk_buff *skb,
*/
static struct sk_buff *validate_xmit_skb(struct sk_buff *skb, struct net_device *dev, bool *again)
{
+ unsigned int resegment_max_segs = 0;
netdev_features_t features;
skb = validate_xmit_unreadable_skb(skb, dev);
@@ -4088,10 +4168,29 @@ static struct sk_buff *validate_xmit_skb(struct sk_buff *skb, struct net_device
if (unlikely(!skb))
goto out_null;
- if (netif_needs_gso(skb, features)) {
+ /*
+ * An oversized skb loses its GSO feature bits and is segmented
+ * down to MSS sized skbs below. A TCP skb can instead be split
+ * into GSO skbs which do fit the device, so keep the bits and
+ * bound the resegmentation. The features computed without the
+ * limit checks say whether the device offloads the GSO type at
+ * all.
+ */
+ if (skb_is_gso(skb) && skb_is_gso_tcp(skb) && !skb->encapsulation &&
+ !gso_within_device_limits(skb, dev)) {
+ netdev_features_t offload = __netif_skb_features(skb, false);
+
+ resegment_max_segs =
+ skb_gso_resegment_max_segs(skb, dev, offload);
+ if (resegment_max_segs)
+ features = offload;
+ }
+
+ if (resegment_max_segs || netif_needs_gso(skb, features)) {
struct sk_buff *segs;
- segs = skb_gso_segment(skb, features);
+ segs = __skb_gso_segment(skb, features, true,
+ resegment_max_segs);
if (IS_ERR(segs)) {
goto out_kfree_skb;
} else if (segs) {
--
2.47.3
^ permalink raw reply related [flat|nested] 5+ messages in thread* [PATCH net-next 4/4] net: net_test: add tests for bounded GSO segmentation
2026-09-17 6:38 [PATCH net-next 0/4] net: resegment oversized TCP GSO skbs Wang Zhan
` (2 preceding siblings ...)
2026-09-17 6:38 ` [PATCH net-next 3/4] net: core: resegment oversized TCP GSO skbs Wang Zhan
@ 2026-09-17 6:38 ` Wang Zhan
3 siblings, 0 replies; 5+ messages in thread
From: Wang Zhan @ 2026-09-17 6:38 UTC (permalink / raw)
To: netdev
Cc: davem, edumazet, kuba, pabeni, horms, keyong.sun, Andrew Lunn,
Jason Wang, Willem de Bruijn, Neal Cardwell, Kuniyuki Iwashima,
Ilya Maximets, Aaron Conole, Eelco Chaudron, Alice Mikityanska,
Wang Zhan
The GSO engine can now be asked to bound the number of MSS segments which
go into each output skb. Add KUnit coverage for the bound itself and for a
TCP skb segmented with one.
The parameterized GSO test gains a max_segs input and three cases: a bound
which splits the skb into two outputs, one which spans three outputs, and
a bound of a single MSS, which must leave the ungrouped output of the
unbounded path in place. Output skbs which remain GSO skbs carry the
original gso_size and no more than max_segs segments. 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 limit test checks that the GSO size limit which netif_skb_features()
applies follows the packet's L3 protocol, also after validate_xmit_vlan()
has pushed the VLAN tag inside the skb and skb->protocol is the VLAN
ethertype. It contrasts the two ways the IPv4 and IPv6 limits can be
skewed, and the skb with and without the tag.
Assisted-by: LLM
Signed-off-by: Wang Zhan <wang.zhan@smartx.com>
---
net/core/net_test.c | 231 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 231 insertions(+)
diff --git a/net/core/net_test.c b/net/core/net_test.c
index 9c3a590865d26..8cf478bfbda1b 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,44 @@ 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 +276,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);
@@ -247,6 +298,15 @@ 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);
+ }
/* last seg can be found through segs->prev pointer */
if (!next)
@@ -261,6 +321,175 @@ 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;
+}
+
+static void gso_test_tcp_bounded_segment(struct kunit *test)
+{
+ netdev_features_t features = NETIF_F_SG | NETIF_F_HW_CSUM |
+ NETIF_F_TSO;
+ 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;
+
+ skb = gso_tcp_skb_new(payload_len);
+ KUNIT_ASSERT_NOT_NULL(test, skb);
+
+ segs = __skb_gso_segment(skb, features, true, max_segs);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, segs);
+
+ 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_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, and it has to survive the
+ * VLAN tag which validate_xmit_vlan() can push inside the skb, because that
+ * tag replaces skb->protocol with the VLAN ethertype.
+ */
+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;
+ netdev_features_t features;
+ struct net_device *dev;
+ struct sk_buff *skb;
+
+ dev = alloc_etherdev(0);
+ KUNIT_ASSERT_NOT_NULL(test, dev);
+ dev->netdev_ops = &dummy_netdev_ops;
+ dev->hw_features = NETIF_F_SG | NETIF_F_HW_CSUM | NETIF_F_TSO6;
+ dev->features = dev->hw_features;
+ dev->vlan_features = dev->hw_features;
+
+ skb = gso_tcp6_skb_new(payload_len);
+ KUNIT_ASSERT_NOT_NULL(test, skb);
+ skb->dev = dev;
+
+ /* The skb fits the IPv6 limit but not the IPv4 one. */
+ dev->gso_max_size = GSO_MAX_SIZE;
+ dev->gso_ipv4_max_size = GSO_LEGACY_MAX_SIZE;
+ features = netif_skb_features(skb);
+ KUNIT_EXPECT_TRUE(test, features & NETIF_F_GSO_MASK);
+
+ /* ...and the other way around. */
+ dev->gso_max_size = GSO_LEGACY_MAX_SIZE;
+ dev->gso_ipv4_max_size = GSO_MAX_SIZE;
+ features = netif_skb_features(skb);
+ KUNIT_EXPECT_FALSE(test, features & NETIF_F_GSO_MASK);
+
+ /* Pushing the tag inside must not change either answer. */
+ skb = vlan_insert_tag_set_proto(skb, htons(ETH_P_8021Q), 0);
+ KUNIT_ASSERT_NOT_NULL(test, skb);
+ KUNIT_ASSERT_EQ(test, skb->protocol, htons(ETH_P_8021Q));
+
+ dev->gso_max_size = GSO_MAX_SIZE;
+ dev->gso_ipv4_max_size = GSO_LEGACY_MAX_SIZE;
+ features = netif_skb_features(skb);
+ KUNIT_EXPECT_TRUE(test, features & NETIF_F_GSO_MASK);
+
+ dev->gso_max_size = GSO_LEGACY_MAX_SIZE;
+ dev->gso_ipv4_max_size = GSO_MAX_SIZE;
+ features = netif_skb_features(skb);
+ KUNIT_EXPECT_FALSE(test, features & NETIF_F_GSO_MASK);
+
+ consume_skb(skb);
+ free_netdev(dev);
+}
+
/* IP tunnel flags */
#include <net/ip_tunnels.h>
@@ -372,6 +601,8 @@ 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_PARAM(ip_tunnel_flags_test_run,
ip_tunnel_flags_test_gen_params),
{ },
--
2.47.3
^ permalink raw reply related [flat|nested] 5+ messages in thread