Subject: Re: [PATCH net v2] net: skbuff: propagate shared-frag marker through frag-transfer helpers

From: Sultan Alsawaf <[email-masked]>
Date: Wed, 13 May 2026 23:18:10 -0700

On Thu, May 14, 2026 at 06:07:44AM +0900, Hyunwoo Kim wrote:
> Changes in v2:
> - Also propagate SHARED_FRAG in skb_shift()
> - v1: https://lore.kernel.org/all/agRfuVOeMI5pbHhY@v4bel/

Hi Hyunwoo,

I've been working on mitigating this vulnerability as a member of the kernel
team at CIQ, a distro vendor. In particular, we wanted to make sure that there
weren't any lingering places missing SHARED_FRAG propagation.

To that end, I used Claude to discover that skb_gro_receive() remained unpatched
(as you pointed out in the v1 thread). And then I generated a PoC exploiting the
vulnerable skb_gro_receive() path.

The PoC is a modified version of the original fragnesia PoC. It works 100% of
the time, just like the original fragnesia PoC.

I have attached the PoC and a patch that fixes skb_gro_receive(). Please take a
look at them.

Thanks,
Sultan
>From c3ec785f197bf329c443aa547eb70864e2ef29ac Mon Sep 17 00:00:00 2001
From: Sultan Alsawaf <[email-masked]>
Date: Wed, 13 May 2026 21:47:51 -0700
Subject: [PATCH] net: gro: propagate SKBFL_SHARED_FRAG in skb_gro_receive()

skb_gro_receive() moves frag descriptors from the incoming skb to the
GRO accumulator through two frag-transfer paths (the direct frag-move
loop and the head_frag + memcpy path) without propagating the
SKBFL_SHARED_FRAG flag from the incoming skb's shinfo->flags. As a
result, the accumulator ends up holding references to externally owned
or page-cache-backed pages while reporting skb_has_shared_frag() as
false.

This is the same bug class as CVE-2026-46300 (d8cfbcdd07557, "net:
skbuff: propagate shared-frag marker through frag-transfer helpers"),
which fixed the identical omission in __pskb_copy_fclone(),
skb_try_coalesce(), and skb_shift(). skb_gro_receive() was missed in
that fix since it lives in net/core/gro.c rather than net/core/skbuff.c.

The impact is observable through ESP-over-UDP with UDP GRO: splice()
attaches page-cache pages to a UDP skb, setting SKBFL_SHARED_FRAG via
ip_append_page(). When two such datagrams are GRO-merged via
skb_gro_receive(), the flag is dropped. After udp_rcv_segment()
re-segments the merged GSO skb, the fresh segments carry the
page-cache frags without the shared-frag marker. esp_input() then sees
!skb_cloned() && !skb_has_shared_frag() and takes the skip_cow fast
path, decrypting in place over the page-cache pages. Because AES-GCM
CTR decryption runs before the authentication tag is verified, the
page cache is corrupted even though the tag check subsequently fails.

Fix it by propagating SKBFL_SHARED_FRAG from the incoming skb to the
accumulator in both frag-transfer paths, matching what the skbuff.c
helpers already do. The third path (frag_list merge at the "merge:"
label) chains the entire incoming skb onto the accumulator's frag_list
without moving individual frag descriptors, so each sub-skb retains
its own flags and no propagation is needed there.

Fixes: cef401de7be8 ("net: fix possible wrong checksum generation")
Fixes: f4c50a4034e6 ("xfrm: esp: avoid in-place decrypt on shared skb frags")
Cc: [email-masked]
Co-authored-by: Claude Opus 4.6 (1M context) <[email-masked]>
Signed-off-by: Sultan Alsawaf <[email-masked]>
---
 net/core/gro.c | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/net/core/gro.c b/net/core/gro.c
index 31d21de5b15a7..4ac41ced13aeb 100644
--- a/net/core/gro.c
+++ b/net/core/gro.c
@@ -145,6 +145,8 @@ int skb_gro_receive(struct sk_buff *p, struct sk_buff *skb)
 		skb_frag_off_add(frag, offset);
 		skb_frag_size_sub(frag, offset);
 
+		pinfo->flags |= skbinfo->flags & SKBFL_SHARED_FRAG;
+
 		/* all fragments truesize : remove (head size + sk_buff) */
 		new_truesize = SKB_TRUESIZE(skb_end_offset(skb));
 		delta_truesize = skb->truesize - new_truesize;
@@ -176,6 +178,8 @@ int skb_gro_receive(struct sk_buff *p, struct sk_buff *skb)
 		memcpy(frag + 1, skbinfo->frags, sizeof(*frag) * skbinfo->nr_frags);
 		/* We dont need to clear skbinfo->nr_frags here */
 
+		pinfo->flags |= skbinfo->flags & SKBFL_SHARED_FRAG;
+
 		new_truesize = SKB_DATA_ALIGN(sizeof(struct sk_buff));
 		delta_truesize = skb->truesize - new_truesize;
 		skb->truesize = new_truesize;
-- 
2.54.0