* [PATCH net] net: gso: complete partial checksums of fraglist segments
@ 2026-10-16 17:10 Pierre-Yves Le Borgne via B4 Relay
2026-10-16 17:34 ` Eric Dumazet
0 siblings, 1 reply; 2+ messages in thread
From: Pierre-Yves Le Borgne via B4 Relay @ 2026-10-16 17:10 UTC (permalink / raw)
To: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Neal Cardwell, Kuniyuki Iwashima, Steffen Klassert,
Willem de Bruijn, Felix Fietkau, David Ahern
Cc: Alice Mikityanska, netdev, linux-kernel, stable, Pierre-Yves Le Borgne
From: Pierre-Yves Le Borgne <pylaterreur@gmail.com>
Since commit f0b8c3034556 ("udp: do not transition UDP GRO fraglist
partial checksums to unnecessary"), fraglist GRO keeps CHECKSUM_PARTIAL
on the packets it aggregates, and skb_segment_list() gives every segment
the ip_summed of the head. Before it, UDP fraglist GRO turned them into
CHECKSUM_UNNECESSARY, which left their checksums unfinished whatever the
output device. Packets arrive with CHECKSUM_PARTIAL from veth, or from
virtio-net when the host sets VIRTIO_NET_HDR_F_NEEDS_CSUM, which it does
for packets that the host itself or another VM on the same bridge sent
with checksum offload.
validate_xmit_skb() completes CHECKSUM_PARTIAL checksums only for packets
that need no segmentation, and relies on the GSO callbacks for the
others: skb_segment() computes the checksums of its segments when the
device can't. The fraglist path doesn't, so when the output device has no
checksum offload, e.g. a tun device without TUN_F_CSUM, the segments leave
with only the pseudo-header sum in their TCP or UDP checksum field and are
dropped by the receiver. Only the packets that GRO aggregated are hit, so
sparse traffic gets through while bulk transfers stall.
This was seen with OpenWrt running as a VM, as OpenWrt turns rx-gro-list
on by default: WireGuard packets from the host, which the VM routed into
a VPN tun device, reached the VPN peer with unfinished UDP checksums, all
with the same value. TCP is affected the same way.
Complete the checksums of the segments once the protocol code has fixed
their headers up, as validate_xmit_skb() does for unsegmented packets.
skb_segment_list() itself is too early for UDP, as
__udp_gso_segment_list() restores the length in the head's UDP header
only after it. Segments that are GSO packets themselves, which TCP
fraglist GRO can chain, are left as they were: skb_checksum_help()
refuses GSO packets.
Fixes: 9fd1ff5d2ac7 ("udp: Support UDP fraglist GRO/GSO.")
Fixes: bee88cd5bd83 ("net: add support for segmenting TCP fraglist GSO packets")
Cc: stable@vger.kernel.org
Closes: https://github.com/Ysurac/openmptcprouter/issues/4402
Assisted-by: LLM
Signed-off-by: Pierre-Yves Le Borgne <pylaterreur@gmail.com>
---
Tested on net/main 191aa41548b2 in UML: frames written with
VIRTIO_NET_HDR_F_NEEDS_CSUM to a tap opened with IFF_NAPI | IFF_VNET_HDR
(virtio_net_hdr_to_skb() and napi_gro_receive(), as on virtio-net RX),
rx-gro-list on, routed with and without SNAT to a tun device read
without vnet header. Packets that left the tun with the bare
pseudo-header sum as checksum, with 2000 frames written per case:
UDPv4 TCPv4 UDPv6 TCPv6
without, no NAT 1030 836 1106 1424
without, SNAT 250 721 683 196
with, either 0 0 0 0
With rx-gro-list off, both kernels complete every checksum. TCP from a
TSO veth sender through a router with rx-gro-list on, out of a veth with
and without tx checksum offload, still works. The error path was
exercised by injecting failures into 1 in 8 checksummed segments, under
KASAN, kmemleak and lockdep. The same reproducer showed the problem, and
an earlier version of this fix working, on an OpenWrt 6.18 kernel in a
VM.
Callers of skb_gso_segment() that pass no offload features (xfrm,
nfnetlink_queue, wireguard, ovpn) now get completed checksums from the
fraglist path too, as they already do from skb_segment().
Not done: allyesconfig and full allmodconfig builds (not enough disk
here); the changed objects build without new warnings on x86_64
allmodconfig with W=1 and sparse (GCC 15). No selftest: the
reproducer needs a NAPI tap fed with virtio_net_hdr frames and a tun
reader that checks checksums, about 300 lines of C. I can turn it into
one under tools/testing/selftests/net if that is wanted.
This patch was developed with an LLM coding agent (Claude Code). It was
asked to find out why forwarded WireGuard traffic got bad UDP checksums
in the OpenMPTCProuter report linked above, then to fix that and test
the fix; it did the analysis, wrote the fix and this changelog, and ran
the tests above.
---
include/linux/skbuff.h | 2 ++
net/core/skbuff.c | 40 ++++++++++++++++++++++++++++++++++++++++
net/ipv4/tcp_offload.c | 4 +++-
net/ipv4/udp_offload.c | 6 ++++--
net/ipv6/tcpv6_offload.c | 4 +++-
5 files changed, 52 insertions(+), 4 deletions(-)
diff --git a/include/linux/skbuff.h b/include/linux/skbuff.h
index a3ff380d43f0..9c3dbbeb1e6e 100644
--- a/include/linux/skbuff.h
+++ b/include/linux/skbuff.h
@@ -4288,6 +4288,8 @@ void skb_scrub_packet(struct sk_buff *skb, bool xnet);
struct sk_buff *skb_segment(struct sk_buff *skb, netdev_features_t features);
struct sk_buff *skb_segment_list(struct sk_buff *skb, netdev_features_t features,
unsigned int offset);
+struct sk_buff *skb_segment_list_csum_help(struct sk_buff *segs,
+ netdev_features_t features);
struct sk_buff *skb_vlan_untag(struct sk_buff *skb);
int skb_ensure_writable(struct sk_buff *skb, unsigned int write_len);
int skb_ensure_writable_head_tail(struct sk_buff *skb, struct net_device *dev);
diff --git a/net/core/skbuff.c b/net/core/skbuff.c
index 41beaf625421..be3fa938e1da 100644
--- a/net/core/skbuff.c
+++ b/net/core/skbuff.c
@@ -4782,6 +4782,46 @@ struct sk_buff *skb_segment_list(struct sk_buff *skb,
}
EXPORT_SYMBOL_GPL(skb_segment_list);
+/**
+ * skb_segment_list_csum_help - finish the checksums of fraglist segments
+ * @segs: segments built by skb_segment_list(), with their final headers
+ * @features: features of the output path
+ *
+ * The segments built by skb_segment_list() all have the ip_summed of the
+ * GRO head. If the aggregated packets were CHECKSUM_PARTIAL, e.g. received
+ * from virtio-net or veth, compute the checksums that the output device
+ * can't offload, as skb_segment() does for the segments it builds.
+ * Segments that are GSO packets themselves (TCP fraglist GRO can chain
+ * those) are left alone, as skb_checksum_help() can't handle them.
+ *
+ * Return: @segs, or an error pointer after freeing the segments.
+ */
+struct sk_buff *skb_segment_list_csum_help(struct sk_buff *segs,
+ netdev_features_t features)
+{
+ struct sk_buff *seg;
+ int err;
+
+ if (segs->ip_summed != CHECKSUM_PARTIAL)
+ return segs;
+
+ for (seg = segs; seg; seg = seg->next) {
+ if (skb_is_gso(seg))
+ continue;
+
+ err = skb_csum_hwoffload_help(seg, features);
+ if (err) {
+ kfree_skb_list(segs->next);
+ segs->next = NULL;
+ /* drop the extra reference skb_segment_list() took */
+ kfree_skb(segs);
+ return ERR_PTR(err);
+ }
+ }
+
+ return segs;
+}
+
/**
* skb_segment - Perform protocol segmentation on skb.
* @head_skb: buffer to segment
diff --git a/net/ipv4/tcp_offload.c b/net/ipv4/tcp_offload.c
index e74d99ca9fac..0bb73e7eeff0 100644
--- a/net/ipv4/tcp_offload.c
+++ b/net/ipv4/tcp_offload.c
@@ -92,7 +92,9 @@ static struct sk_buff *__tcp4_gso_segment_list(struct sk_buff *skb,
if (IS_ERR(skb))
return skb;
- return __tcpv4_gso_segment_list_csum(skb);
+ skb = __tcpv4_gso_segment_list_csum(skb);
+
+ return skb_segment_list_csum_help(skb, features);
}
static struct sk_buff *tcp4_gso_segment(struct sk_buff *skb,
diff --git a/net/ipv4/udp_offload.c b/net/ipv4/udp_offload.c
index cf07c3c6611a..4a90cdb1a9eb 100644
--- a/net/ipv4/udp_offload.c
+++ b/net/ipv4/udp_offload.c
@@ -473,9 +473,11 @@ static struct sk_buff *__udp_gso_segment_list(struct sk_buff *skb,
udp_set_len_short(udp_hdr(skb), sizeof(struct udphdr) + mss);
if (is_ipv6)
- return __udpv6_gso_segment_list_csum(skb);
+ skb = __udpv6_gso_segment_list_csum(skb);
else
- return __udpv4_gso_segment_list_csum(skb);
+ skb = __udpv4_gso_segment_list_csum(skb);
+
+ return skb_segment_list_csum_help(skb, features);
}
struct sk_buff *__udp_gso_segment(struct sk_buff *gso_skb,
diff --git a/net/ipv6/tcpv6_offload.c b/net/ipv6/tcpv6_offload.c
index eec3778855eb..e664904020e7 100644
--- a/net/ipv6/tcpv6_offload.c
+++ b/net/ipv6/tcpv6_offload.c
@@ -162,7 +162,9 @@ static struct sk_buff *__tcp6_gso_segment_list(struct sk_buff *skb,
if (IS_ERR(skb))
return skb;
- return __tcpv6_gso_segment_list_csum(skb);
+ skb = __tcpv6_gso_segment_list_csum(skb);
+
+ return skb_segment_list_csum_help(skb, features);
}
static struct sk_buff *tcp6_gso_segment(struct sk_buff *skb,
---
base-commit: 191aa41548b2d2b6af534a6d5cf84a552435c74c
change-id: 20261016-fraglist-csum-9851f352f634
Best regards,
--
Pierre-Yves Le Borgne <pylaterreur@gmail.com>
^ permalink raw reply [flat|nested] 2+ messages in thread
* Re: [PATCH net] net: gso: complete partial checksums of fraglist segments
2026-10-16 17:10 [PATCH net] net: gso: complete partial checksums of fraglist segments Pierre-Yves Le Borgne via B4 Relay
@ 2026-10-16 17:34 ` Eric Dumazet
0 siblings, 0 replies; 2+ messages in thread
From: Eric Dumazet @ 2026-10-16 17:34 UTC (permalink / raw)
To: pylaterreur
Cc: David S. Miller, Jakub Kicinski, Paolo Abeni, Simon Horman,
Neal Cardwell, Kuniyuki Iwashima, Steffen Klassert,
Willem de Bruijn, Felix Fietkau, David Ahern, Alice Mikityanska,
netdev, linux-kernel, stable
Le ven. 16 oct. 2026 à 19:10, Pierre-Yves Le Borgne via B4 Relay
<devnull+pylaterreur.gmail.com@kernel.org> a écrit :
>
> From: Pierre-Yves Le Borgne <pylaterreur@gmail.com>
>
> Since commit f0b8c3034556 ("udp: do not transition UDP GRO fraglist
> partial checksums to unnecessary"), fraglist GRO keeps CHECKSUM_PARTIAL
> on the packets it aggregates, and skb_segment_list() gives every segment
> the ip_summed of the head. Before it, UDP fraglist GRO turned them into
> CHECKSUM_UNNECESSARY, which left their checksums unfinished whatever the
> output device. Packets arrive with CHECKSUM_PARTIAL from veth, or from
> virtio-net when the host sets VIRTIO_NET_HDR_F_NEEDS_CSUM, which it does
> for packets that the host itself or another VM on the same bridge sent
> with checksum offload.
>
> validate_xmit_skb() completes CHECKSUM_PARTIAL checksums only for packets
> that need no segmentation, and relies on the GSO callbacks for the
> others: skb_segment() computes the checksums of its segments when the
> device can't. The fraglist path doesn't, so when the output device has no
> checksum offload, e.g. a tun device without TUN_F_CSUM, the segments leave
> with only the pseudo-header sum in their TCP or UDP checksum field and are
> dropped by the receiver. Only the packets that GRO aggregated are hit, so
> sparse traffic gets through while bulk transfers stall.
>
> This was seen with OpenWrt running as a VM, as OpenWrt turns rx-gro-list
> on by default: WireGuard packets from the host, which the VM routed into
> a VPN tun device, reached the VPN peer with unfinished UDP checksums, all
> with the same value. TCP is affected the same way.
>
> Complete the checksums of the segments once the protocol code has fixed
> their headers up, as validate_xmit_skb() does for unsegmented packets.
> skb_segment_list() itself is too early for UDP, as
> __udp_gso_segment_list() restores the length in the head's UDP header
> only after it. Segments that are GSO packets themselves, which TCP
> fraglist GRO can chain, are left as they were: skb_checksum_help()
> refuses GSO packets.
>
I do not think TCP fraglist GRO can chain GSO packets:
tcp4_check_fraglist_gro() / tcp6_check_fraglist_gro() set
is_flist = !sk && !skb_is_gso(skb), and a GSO skb arriving on a flist
flow gets is_flist = 0 and is flushed by the is_flist mismatch check in
tcp_gro_receive(). If you know of a path where this happens, please
describe it, otherwise please drop the skb_is_gso() test and this part
of the changelog.
> Fixes: 9fd1ff5d2ac7 ("udp: Support UDP fraglist GRO/GSO.")
> Fixes: bee88cd5bd83 ("net: add support for segmenting TCP fraglist GSO packets")
> Cc: stable@vger.kernel.org
> Closes: https://github.com/Ysurac/openmptcprouter/issues/4402
> Assisted-by: LLM
> Signed-off-by: Pierre-Yves Le Borgne <pylaterreur@gmail.com>
> ---
> Tested on net/main 191aa41548b2 in UML: frames written with
> VIRTIO_NET_HDR_F_NEEDS_CSUM to a tap opened with IFF_NAPI | IFF_VNET_HDR
> (virtio_net_hdr_to_skb() and napi_gro_receive(), as on virtio-net RX),
> rx-gro-list on, routed with and without SNAT to a tun device read
> without vnet header. Packets that left the tun with the bare
> pseudo-header sum as checksum, with 2000 frames written per case:
>
> UDPv4 TCPv4 UDPv6 TCPv6
> without, no NAT 1030 836 1106 1424
> without, SNAT 250 721 683 196
> with, either 0 0 0 0
>
> With rx-gro-list off, both kernels complete every checksum. TCP from a
> TSO veth sender through a router with rx-gro-list on, out of a veth with
> and without tx checksum offload, still works. The error path was
> exercised by injecting failures into 1 in 8 checksummed segments, under
> KASAN, kmemleak and lockdep. The same reproducer showed the problem, and
> an earlier version of this fix working, on an OpenWrt 6.18 kernel in a
> VM.
>
> Callers of skb_gso_segment() that pass no offload features (xfrm,
> nfnetlink_queue, wireguard, ovpn) now get completed checksums from the
> fraglist path too, as they already do from skb_segment().
>
> Not done: allyesconfig and full allmodconfig builds (not enough disk
> here); the changed objects build without new warnings on x86_64
> allmodconfig with W=1 and sparse (GCC 15). No selftest: the
> reproducer needs a NAPI tap fed with virtio_net_hdr frames and a tun
> reader that checks checksums, about 300 lines of C. I can turn it into
> one under tools/testing/selftests/net if that is wanted.
>
> This patch was developed with an LLM coding agent (Claude Code). It was
> asked to find out why forwarded WireGuard traffic got bad UDP checksums
> in the OpenMPTCProuter report linked above, then to fix that and test
> the fix; it did the analysis, wrote the fix and this changelog, and ran
> the tests above.
> ---
> include/linux/skbuff.h | 2 ++
> net/core/skbuff.c | 40 ++++++++++++++++++++++++++++++++++++++++
> net/ipv4/tcp_offload.c | 4 +++-
> net/ipv4/udp_offload.c | 6 ++++--
> net/ipv6/tcpv6_offload.c | 4 +++-
> 5 files changed, 52 insertions(+), 4 deletions(-)
>
> diff --git a/include/linux/skbuff.h b/include/linux/skbuff.h
> index a3ff380d43f0..9c3dbbeb1e6e 100644
> --- a/include/linux/skbuff.h
> +++ b/include/linux/skbuff.h
> @@ -4288,6 +4288,8 @@ void skb_scrub_packet(struct sk_buff *skb, bool xnet);
> struct sk_buff *skb_segment(struct sk_buff *skb, netdev_features_t features);
> struct sk_buff *skb_segment_list(struct sk_buff *skb, netdev_features_t features,
> unsigned int offset);
> +struct sk_buff *skb_segment_list_csum_help(struct sk_buff *segs,
> + netdev_features_t features);
> struct sk_buff *skb_vlan_untag(struct sk_buff *skb);
> int skb_ensure_writable(struct sk_buff *skb, unsigned int write_len);
> int skb_ensure_writable_head_tail(struct sk_buff *skb, struct net_device *dev);
> diff --git a/net/core/skbuff.c b/net/core/skbuff.c
> index 41beaf625421..be3fa938e1da 100644
> --- a/net/core/skbuff.c
> +++ b/net/core/skbuff.c
> @@ -4782,6 +4782,46 @@ struct sk_buff *skb_segment_list(struct sk_buff *skb,
> }
> EXPORT_SYMBOL_GPL(skb_segment_list);
>
> +/**
> + * skb_segment_list_csum_help - finish the checksums of fraglist segments
> + * @segs: segments built by skb_segment_list(), with their final headers
> + * @features: features of the output path
> + *
> + * The segments built by skb_segment_list() all have the ip_summed of the
> + * GRO head. If the aggregated packets were CHECKSUM_PARTIAL, e.g. received
> + * from virtio-net or veth, compute the checksums that the output device
> + * can't offload, as skb_segment() does for the segments it builds.
> + * Segments that are GSO packets themselves (TCP fraglist GRO can chain
> + * those) are left alone, as skb_checksum_help() can't handle them.
> + *
> + * Return: @segs, or an error pointer after freeing the segments.
> + */
> +struct sk_buff *skb_segment_list_csum_help(struct sk_buff *segs,
> + netdev_features_t features)
> +{
> + struct sk_buff *seg;
> + int err;
> +
> + if (segs->ip_summed != CHECKSUM_PARTIAL)
> + return segs;
Common case is a device able to checksum these packets. Please add an
early return when @features allows it, instead of calling
skb_csum_hwoffload_help() on up to 64 segments for nothing.
> +
> + for (seg = segs; seg; seg = seg->next) {
> + if (skb_is_gso(seg))
> + continue;
> +
> + err = skb_csum_hwoffload_help(seg, features);
This relies on seg->csum_start being correct. skb_segment_list() adjusts
nskb->transport_header by len_diff, but not csum_start. len_diff should
be 0 in practice, but validate_xmit_skb() resets the transport header
from csum_start before calling skb_csum_hwoffload_help(), this path
does not.
> + if (err) {
> + kfree_skb_list(segs->next);
> + segs->next = NULL;
> + /* drop the extra reference skb_segment_list() took */
> + kfree_skb(segs);
This is correct, but it depends on the skb_get() done at the end of
skb_segment_list(). Please make this coupling more visible (a comment
in skb_segment_list(),
> + return ERR_PTR(err);
> + }
> + }
> +
> + return segs;
> +}
> +
> /**
> * skb_segment - Perform protocol segmentation on skb.
> * @head_skb: buffer to segment
> diff --git a/net/ipv4/tcp_offload.c b/net/ipv4/tcp_offload.c
> index e74d99ca9fac..0bb73e7eeff0 100644
> --- a/net/ipv4/tcp_offload.c
> +++ b/net/ipv4/tcp_offload.c
> @@ -92,7 +92,9 @@ static struct sk_buff *__tcp4_gso_segment_list(struct sk_buff *skb,
> if (IS_ERR(skb))
> return skb;
>
> - return __tcpv4_gso_segment_list_csum(skb);
> + skb = __tcpv4_gso_segment_list_csum(skb);
> +
> + return skb_segment_list_csum_help(skb, features);
> }
>
> static struct sk_buff *tcp4_gso_segment(struct sk_buff *skb,
> diff --git a/net/ipv4/udp_offload.c b/net/ipv4/udp_offload.c
> index cf07c3c6611a..4a90cdb1a9eb 100644
> --- a/net/ipv4/udp_offload.c
> +++ b/net/ipv4/udp_offload.c
> @@ -473,9 +473,11 @@ static struct sk_buff *__udp_gso_segment_list(struct sk_buff *skb,
> udp_set_len_short(udp_hdr(skb), sizeof(struct udphdr) + mss);
>
> if (is_ipv6)
> - return __udpv6_gso_segment_list_csum(skb);
> + skb = __udpv6_gso_segment_list_csum(skb);
> else
> - return __udpv4_gso_segment_list_csum(skb);
> + skb = __udpv4_gso_segment_list_csum(skb);
> +
> + return skb_segment_list_csum_help(skb, features);
> }
>
> struct sk_buff *__udp_gso_segment(struct sk_buff *gso_skb,
> diff --git a/net/ipv6/tcpv6_offload.c b/net/ipv6/tcpv6_offload.c
> index eec3778855eb..e664904020e7 100644
> --- a/net/ipv6/tcpv6_offload.c
> +++ b/net/ipv6/tcpv6_offload.c
> @@ -162,7 +162,9 @@ static struct sk_buff *__tcp6_gso_segment_list(struct sk_buff *skb,
> if (IS_ERR(skb))
> return skb;
>
> - return __tcpv6_gso_segment_list_csum(skb);
> + skb = __tcpv6_gso_segment_list_csum(skb);
> +
> + return skb_segment_list_csum_help(skb, features);
> }
>
> static struct sk_buff *tcp6_gso_segment(struct sk_buff *skb,
>
> ---
> base-commit: 191aa41548b2d2b6af534a6d5cf84a552435c74c
> change-id: 20261016-fraglist-csum-9851f352f634
>
> Best regards,
> --
> Pierre-Yves Le Borgne <pylaterreur@gmail.com>
>
>
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-10-16 17:34 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-16 17:10 [PATCH net] net: gso: complete partial checksums of fraglist segments Pierre-Yves Le Borgne via B4 Relay
2026-10-16 17:34 ` Eric Dumazet
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®