[PATCH mptcp-next 5/6] mptcp: trim the duplicated skb head at receive enqueue

Geliang Tang <[email protected]> Mon, 27 Jul 2026 19:29:20 +0800
Newsgroups dev.linux.lists.mptcp
Message-ID <81bd1e2fa9c959927bf0d1cae718a9da72164b5b.1785150300.git.tanggeliang@kylinos.cn>
From: Geliang Tang <[email protected]>

After the CB offset field removal, the msk receive queue tracks the
consumed position with the msk-level copied_seq and each skb map_seq
points at its first byte (skb->data[0]). When the same DSN range is
delivered more than once - a partially-acked segment, or duplicate
data arriving on a second subflow - the skb is queued whole: its
map_seq sits below msk->ack_seq and the duplicated bytes stay
physically at the front of the skb.

The linear readers (recvmsg, read_sock, read_done) cope with that by
computing a per-skb offset = copied_seq - map_seq and skipping it. But
consumers that treat the receive queue as a single contiguous byte
stream cannot: the TLS strparser builds an anchor whose frag_list is
the receive-queue skbs and reads it with a plain skb_copy_bits(), which
has no per-skb offset knowledge. A record spanning such an skb boundary
then reads the duplicated prefix and gets corrupted.

Physically drop the duplicated leading bytes at enqueue time instead,
so the receive queue is always contiguous. Add mptcp_trim_dup_head(),
modelled on tcp_trim_head()/__pskb_trim_head() but tolerating a
non-empty linear area: it pulls the linear head first, then eats the
remaining bytes from the paged frags, and the caller bumps map_seq
accordingly. The skb truesize is left unchanged on purpose - dropping
only skb->len/data_len keeps the memory accounting over-reserved, hence
always safe, at both callers and avoids any rmem/fwd_alloc fixup.

Two callers trim the overlap:
 - __mptcp_move_skb() partial-packet branch (map_seq < ack_seq <
   end_seq). The skb is not owned yet, so on the -ENOMEM unclone
   failure refund the truesize borrowed by mptcp_borrow_fwdmem()
   before dropping it.
 - __mptcp_ofo_queue() uncoalesced branch. The skb is already msk
   owned, so mptcp_drop() refunds it; skip advancing ack_seq on
   failure.

After trimming, map_seq equals the queue tail end_seq, so the segment
can be coalesced normally.

Signed-off-by: Geliang Tang <[email protected]>
---
 net/mptcp/protocol.c | 79 +++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 78 insertions(+), 1 deletion(-)

diff --git a/net/mptcp/protocol.c b/net/mptcp/protocol.c
index b9e44c64c620..09b888ff33c7 100644
--- a/net/mptcp/protocol.c
+++ b/net/mptcp/protocol.c
@@ -11,6 +11,7 @@
 #include <linux/netdevice.h>
 #include <linux/sched/signal.h>
 #include <linux/atomic.h>
+#include <linux/skbuff_ref.h>
 #include <net/aligned_data.h>
 #include <net/rps.h>
 #include <net/sock.h>
@@ -429,6 +430,62 @@ void __mptcp_sync_rcv_sequence(struct sock *sk)
 	MPTCP_SKB_CB(skb)->end_seq = MPTCP_SKB_CB(skb)->map_seq + skb->len;
 }
 
+static int mptcp_trim_dup_head(struct sk_buff *skb, int delta)
+{
+	struct skb_shared_info *shinfo;
+	int headlen, eat, i, k;
+
+	if (delta <= 0)
+		return 0;
+
+	/* Received skbs are not expected to carry a frag_list; the frag loop
+	 * below only handles the linear area and the paged frags.
+	 */
+	DEBUG_NET_WARN_ON_ONCE(skb_has_frag_list(skb));
+
+	if (skb_unclone_keeptruesize(skb, GFP_ATOMIC))
+		return -ENOMEM;
+
+	/* Eat the linear head first, then the paged frags. Note the skb
+	 * truesize is left unchanged on purpose: dropping only skb->len /
+	 * skb->data_len keeps the memory accounting over-reserved (hence
+	 * always safe) at both callers.
+	 */
+	headlen = skb_headlen(skb);
+	eat = min(delta, headlen);
+	if (eat) {
+		__skb_pull(skb, eat);
+		delta -= eat;
+	}
+	if (!delta)
+		return 0;
+
+	shinfo = skb_shinfo(skb);
+	eat = delta;
+	k = 0;
+	for (i = 0; i < shinfo->nr_frags; i++) {
+		int size = skb_frag_size(&shinfo->frags[i]);
+
+		if (size <= eat) {
+			skb_frag_unref(skb, i);
+			eat -= size;
+		} else {
+			shinfo->frags[k] = shinfo->frags[i];
+			if (eat) {
+				skb_frag_off_add(&shinfo->frags[k], eat);
+				skb_frag_size_sub(&shinfo->frags[k], eat);
+				eat = 0;
+			}
+			k++;
+		}
+	}
+	shinfo->nr_frags = k;
+
+	skb->data_len -= delta;
+	skb->len -= delta;
+	return 0;
+}
+
 static bool __mptcp_move_skb(struct sock *sk, struct sk_buff *skb)
 {
 	u32 copy_len = MPTCP_SKB_CB(skb)->end_seq - MPTCP_SKB_CB(skb)->map_seq;
@@ -474,6 +531,16 @@ static bool __mptcp_move_skb(struct sock *sk, struct sk_buff *skb)
 		/* Partial packet: map_seq < ack_seq < end_seq. */
 		int delta = (u32)msk->ack_seq - MPTCP_SKB_CB(skb)->map_seq;
 
+		if (mptcp_trim_dup_head(skb, delta)) {
+			/* skb is not owned yet: mptcp_borrow_fwdmem() added its
+			 * truesize to sk_forward_alloc and cleared skb->sk, so
+			 * mptcp_drop() won't refund it. Do it here.
+			 */
+			sk_forward_alloc_add(sk, -skb->truesize);
+			mptcp_drop(sk, skb);
+			return false;
+		}
+		MPTCP_SKB_CB(skb)->map_seq += delta;
 		copy_len -= delta;
 		goto add_queue;
 	}
@@ -889,10 +956,20 @@ static bool __mptcp_ofo_queue(struct mptcp_sock *msk)
 		if (!tail || !mptcp_try_coalesce(sk, tail, skb)) {
 			int delta = ack_seq - MPTCP_SKB_CB(skb)->map_seq;
 
-			/* skip overlapping data, if any */
+			/* Physically trim the overlapping prefix, if any,
+			 * so the receive queue stays contiguous.
+			 */
 			pr_debug("uncoalesced seq=%x ack seq=%x delta=%d\n",
 				 MPTCP_SKB_CB(skb)->map_seq, ack_seq,
 				 delta);
+			if (mptcp_trim_dup_head(skb, delta)) {
+				/* skb is msk-owned here; mptcp_drop() refunds
+				 * it. Skip advancing ack_seq/bytes_received.
+				 */
+				mptcp_drop(sk, skb);
+				continue;
+			}
+			MPTCP_SKB_CB(skb)->map_seq += delta;
 			__skb_queue_tail(&sk->sk_receive_queue, skb);
 		}
 		msk->bytes_received += seq_delta;
-- 
2.53.0