[PATCH net v4] xfrm: esp: reset outer TCP GSO metadata on espintcp input
DORMANTno replies
From: Guoshuai Yang <hidden>
Date: 2026-10-27 22:57:18
Also in:
lkml, stable
Subsystem:
networking [general], networking [ipsec], the rest · Maintainers:
"David S. Miller", Eric Dumazet, Jakub Kicinski, Paolo Abeni, Steffen Klassert, Herbert Xu, Linus Torvalds
When ESP packets are received over TCP (RFC 8229 espintcp), strparser
slices individual length-prefixed ESP records from the outer TCP stream by
cloning the incoming TCP skb(s) in __strp_recv().
If the outer TCP skb was GRO-coalesced before reaching strparser, its
skb_shinfo carries the outer TCP stream's GSO metadata (gso_size, gso_segs,
and gso_type). When an ESP record spans two outer TCP segments, strparser
chains the spillover clone onto skb_shinfo(head)->frag_list.
Subsequently in esp_input() and esp6_input(), skb_cow_data() unshares the
head skb via pskb_expand_head() (preserving the outer TCP gso_size and
gso_type in the new private skb_shinfo) and calls skb_copy() on each cloned
frag_list member, which copies the outer TCP GSO fields via
skb_copy_header() onto a linear frag_list child skb allocated from slab
with head_frag == 0.
Furthermore, on kernels with TCP fraglist GRO support, if an outer TCP GRO
aggregate carrying SKB_GSO_FRAGLIST reaches the espintcp socket unsegmented
(for example, when rx-gro-list is enabled on a device in an outer netns
where tcp4/6_check_fraglist_gro() finds no local socket and the aggregate
is forwarded across a NETIF_F_GSO_FRAGLIST-capable veth device into the
netns hosting the espintcp socket) and is chained onto
skb_shinfo(head)->frag_list by __strp_recv(), skb_cow_data() invokes
skb_copy() / skb_copy_expand() on that frag_list member and hits
WARN_ON_ONCE(skb_shinfo(skb)->gso_type & SKB_GSO_FRAGLIST), failing with
-ENOMEM before decryption even begins. (On older stable trees without TCP
fraglist GRO, the pre-pass is a no-op.)
After ESP decryption and header removal, the resulting single inner packet
still has skb_is_gso(skb) == true with the stale outer TCP gso_size, while
skb_headlen(head_skb) is now smaller than that gso_size due to stripping
the outer framing and ESP header. When this packet passes through
gro_cells_receive() (which flushes frag_list packets via GRO_NORMAL
without resetting gso_size) and is forwarded, validate_xmit_skb() invokes
tcp_gso_segment() -> skb_segment(). Because head_skb is shorter than the
stale gso_size and SKB_GSO_DODGY is not set, skb_segment() attempts to map
the slab-backed frag_list member as a scatter-gather page fragment where
list_skb->head_frag == 0. On current mainline kernels, skb_segment()
triggers DEBUG_NET_WARN_ON_ONCE(1) and fails with -EINVAL, causing
validate_xmit_skb() to drop all multi-segment espintcp packets. On stable
kernels prior to the removal of BUG_ON(!list_skb->head_frag) in
skb_segment() (such as 6.12.94 where this trace was captured), it crashes
the kernel:
Internal error: Oops - undefined instruction: 0 [#1] SMP ARM
PC is at skb_segment+0x574/0xd20
LR is at skb_segment+0x884/0xd20
Call trace:
skb_segment from tcp_gso_segment
tcp_gso_segment from inet_gso_segment
inet_gso_segment from skb_mac_gso_segment
skb_mac_gso_segment from __skb_gso_segment
__skb_gso_segment from validate_xmit_skb
validate_xmit_skb from __dev_queue_xmit
__dev_queue_xmit from ip_finish_output2
ip_finish_output2 from ip_output
ip_output from ip_sublist_rcv_finish
ip_sublist_rcv_finish from ip_sublist_rcv
ip_list_rcv from __netif_receive_skb_list_core
__netif_receive_skb_list_core from __netif_receive_skb_list
__netif_receive_skb_list from gro_normal_list
gro_normal_list from gro_cell_poll
Similarly, if a decrypted inner packet with a frag_list later takes the
ip_do_fragment() frag_list fast path, each detached frag_list member is
transmitted as a standalone IP fragment and would still carry the stale
outer TCP GSO state when reaching validate_xmit_skb(). And even when an
ESP record does not have a frag_list, leaving the stale outer TCP gso_size
on the decrypted inner skb prevents dev_gro_receive() from initializing
gso_size to the inner packet's skb_gro_len().
We cannot reset GSO state earlier in espintcp_rcv() / handle_esp() because
the skb may still be a clone sharing skb_shinfo with the outer TCP skb held
by strparser. Instead, in esp_input() and esp6_input():
1. Prior to skb_cow_data(), unshare/unclone and reset GSO metadata on any
frag_list member carrying SKB_GSO_FRAGLIST (unsharing any shared
predecessor frag_list members along the walk) so skb_copy() /
skb_copy_expand() inside skb_cow_data() does not hit WARN_ON_ONCE() and
drop the packet.
2. At skip_cow, where the head skb and all frag_list members are guaranteed
to be uncloned with private skb_shinfo, call skb_gso_reset() on both the
head skb and each skb_walk_frags() member.
Fixes: e27cca96cd68 ("xfrm: add espintcp (RFC 8229)")
Cc: <redacted>
Signed-off-by: Guoshuai Yang <redacted>
---
v4:
- In esp_reset_espintcp_fraglist_gso(), unshare any shared frag_list
member before checking SKB_GSO_FRAGLIST so *fragp = frag never writes
through a shared predecessor's ->next pointer.
- Clarify in the commit message that on current mainline kernels
skb_segment() triggers DEBUG_NET_WARN_ON_ONCE(1) and returns -EINVAL
(dropping the packet in validate_xmit_skb()), whereas the BUG_ON()
oops trace was captured on 6.12.94 LTS.
- Clarify the cross-netns veth forwarding topology required for TCP
fraglist GRO (SKB_GSO_FRAGLIST) aggregates to reach esp_input()
unsegmented.
- Link to v3: https://lore.kernel.org/netdev/20261023065311.65031-1-yangguoshuai@gmail.com/ (local)
v3:
- Unshare/unclone and reset GSO metadata on any frag_list member carrying
SKB_GSO_FRAGLIST before skb_cow_data() so skb_copy() / skb_copy_expand()
does not hit WARN_ON_ONCE(SKB_GSO_FRAGLIST) and return -ENOMEM.
- Move the shared espintcp GSO reset helpers into include/net/esp.h.
- Link to v2: https://lore.kernel.org/netdev/20261020030450.7170-1-yangguoshuai@gmail.com/ (local)
v2:
- Also call skb_gso_reset() on each skb_walk_frags() member at skip_cow
in esp_input() and esp6_input() so detached frag_list members on the
ip_do_fragment() fast path do not retain stale outer TCP GSO state.
- Specify target tree 'net' in subject prefix.
- Link to v1: https://lore.kernel.org/netdev/20261018005218.53267-1-yangguoshuai@gmail.com/ (local)
include/net/esp.h | 51 +++++++++++++++++++++++++++++++++++++++++++++++
net/ipv4/esp4.c | 9 +++++++++
net/ipv6/esp6.c | 9 +++++++++
3 files changed, 69 insertions(+)
diff --git a/include/net/esp.h b/include/net/esp.h
index 3229507..83c75ef 100644
--- a/include/net/esp.h
+++ b/include/net/esp.h@@ -28,6 +28,57 @@ static inline void esp_output_fill_trailer(u8 *tail, int tfclen, int plen, __u8 tail[plen - 1] = proto; } +static inline int esp_reset_espintcp_fraglist_gso(struct sk_buff *skb) +{ + struct sk_buff **fragp; + struct sk_buff *frag; + + skb_walk_frags(skb, frag) { + if (skb_shinfo(frag)->gso_type & SKB_GSO_FRAGLIST) + break; + } + if (!frag) + return 0; + + if (skb_unclone(skb, GFP_ATOMIC)) + return -ENOMEM; + + for (fragp = &skb_shinfo(skb)->frag_list; (frag = *fragp); + fragp = &frag->next) { + if (skb_shared(frag)) { + struct sk_buff *nfrag; + + nfrag = skb_clone(frag, GFP_ATOMIC); + if (!nfrag) + return -ENOMEM; + + nfrag->next = frag->next; + consume_skb(frag); + frag = nfrag; + *fragp = frag; + } + + if (!(skb_shinfo(frag)->gso_type & SKB_GSO_FRAGLIST)) + continue; + + if (skb_unclone(frag, GFP_ATOMIC)) + return -ENOMEM; + + skb_gso_reset(frag); + } + + return 0; +} + +static inline void esp_reset_espintcp_gso(struct sk_buff *skb) +{ + struct sk_buff *iter; + + skb_gso_reset(skb); + skb_walk_frags(skb, iter) + skb_gso_reset(iter); +} + struct esp_info { struct ip_esp_hdr *esph; __be64 seqno;
diff --git a/net/ipv4/esp4.c b/net/ipv4/esp4.c
index e76db58..92980cd 100644
--- a/net/ipv4/esp4.c
+++ b/net/ipv4/esp4.c@@ -893,6 +893,12 @@ static int esp_input(struct xfrm_state *x, struct sk_buff *skb) } } + if (x->encap && x->encap->encap_type == TCP_ENCAP_ESPINTCP) { + err = esp_reset_espintcp_fraglist_gso(skb); + if (err) + goto out; + } + err = skb_cow_data(skb, 0, &trailer); if (err < 0) goto out;
@@ -900,6 +906,9 @@ static int esp_input(struct xfrm_state *x, struct sk_buff *skb) nfrags = err; skip_cow: + if (x->encap && x->encap->encap_type == TCP_ENCAP_ESPINTCP) + esp_reset_espintcp_gso(skb); + err = -ENOMEM; tmp = esp_alloc_tmp(aead, nfrags, seqhilen); if (!tmp)
diff --git a/net/ipv6/esp6.c b/net/ipv6/esp6.c
index b1c9b36..65470d3 100644
--- a/net/ipv6/esp6.c
+++ b/net/ipv6/esp6.c@@ -936,6 +936,12 @@ static int esp6_input(struct xfrm_state *x, struct sk_buff *skb) } } + if (x->encap && x->encap->encap_type == TCP_ENCAP_ESPINTCP) { + ret = esp_reset_espintcp_fraglist_gso(skb); + if (ret) + goto out; + } + nfrags = skb_cow_data(skb, 0, &trailer); if (nfrags < 0) { ret = -EINVAL;
@@ -943,6 +949,9 @@ static int esp6_input(struct xfrm_state *x, struct sk_buff *skb) } skip_cow: + if (x->encap && x->encap->encap_type == TCP_ENCAP_ESPINTCP) + esp_reset_espintcp_gso(skb); + ret = -ENOMEM; tmp = esp_alloc_tmp(aead, nfrags, seqhilen); if (!tmp)
--
2.54.0 (Apple Git-157)