* [PATCH net v2 1/2] net: gro: do not undo remote checksum offload on merged packets
[not found] <20261030125955.3107383-1-edumazet@kernel.org>
@ 2026-10-30 12:59 ` Eric Dumazet
2026-11-02 13:00 ` netdev-bot+sashiko
2026-10-30 12:59 ` [PATCH net v2 2/2] net: gro: invalidate frag0 after a pull Eric Dumazet
1 sibling, 1 reply; 5+ messages in thread
From: Eric Dumazet @ 2026-10-30 12:59 UTC (permalink / raw)
To: David S . Miller, Jakub Kicinski, Paolo Abeni
Cc: Simon Horman, netdev, edumazet, Willem de Bruijn,
Steffen Klassert, Tom Herbert, Cen Zhang, AutonomousCodeSecurity,
stable
skb_gro_flush_final_remcsum() calls skb_gro_remcsum_cleanup() to undo
the checksum adjustment done by skb_gro_remcsum_process() when the
packet was not consumed by GRO.
When the inner handler merged the packet (NAPI_GRO_CB(skb)->same_flow
is set), grc->offset can no longer be trusted:
- skb_gro_receive() either gives the frags or the head of the skb
to the held packet, or uses __skb_pull(skb, offset) before adding
the skb to the frag_list of the held packet.
- skb_gro_receive_list() uses skb_pull(skb, skb_gro_offset(skb)).
grc->offset is relative to the original skb->data, so the 2-byte
read-modify-write done by skb_gro_remcsum_cleanup() can land in
the payload of a packet which now belongs to the GRO packet.
pskb_may_pull() can also be called on this frag_list member.
With fraglist GRO, this can also be a use-after-free:
pskb_may_pull(skb, skb_gro_offset(skb)) in skb_gro_receive_list()
or udp_gro_receive_segment() releases the first page fragment if it
only contains headers. NAPI_GRO_CB(skb)->frag0 and frag0_len are not
updated, so skb_gro_remcsum_cleanup() takes the fast path and writes
through frag0 into a page the skb no longer holds a reference on.
The non merged case, where frag0 is used after this pull, is handled
in the following patch.
Skipping the cleanup for a merged skb is correct:
- With skb_gro_receive(), the skb is freed, or its headers are
rebuilt from the head skb in skb_segment().
- skb_segment_list() only copies the link-layer and tunnel headers
from the head skb, so fraglist members keep their own inner headers.
grc->delta is only set with VXLAN_F_REMCSUM_NOPARTIAL, where
skb_remcsum_process() writes the same adjusted checksum in the non
GRO path. Keeping it is the intended result.
This affects VXLAN and GUE with remote checksum offload enabled.
Fixes: 26c4f7da3e41 ("net: Fix remcsum in GRO path to not change packet")
Cc: stable@vger.kernel.org
Tested-by: Cen Zhang <cenzhang@linux.microsoft.com>
Signed-off-by: Eric Dumazet <edumazet@kernel.org>
---
include/net/gro.h | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/include/net/gro.h b/include/net/gro.h
index 2300b6da05b2..00b49b6b9e74 100644
--- a/include/net/gro.h
+++ b/include/net/gro.h
@@ -351,7 +351,13 @@ static inline void skb_gro_remcsum_cleanup(struct sk_buff *skb,
void *ptr;
size_t plen = grc->offset + sizeof(u16);
- if (!grc->delta)
+ /* Once merged, grc->offset can not be trusted: the frags or head
+ * of this skb now belong to the GRO packet, or skb->data was pulled
+ * past the headers before adding it to a frag_list.
+ * Fraglist members keep their adjusted inner checksum, which is the
+ * value skb_remcsum_process() would have written.
+ */
+ if (!grc->delta || NAPI_GRO_CB(skb)->same_flow)
return;
ptr = skb_gro_header(skb, plen, grc->offset);
--
2.53.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH net v2 2/2] net: gro: invalidate frag0 after a pull
[not found] <20261030125955.3107383-1-edumazet@kernel.org>
2026-10-30 12:59 ` [PATCH net v2 1/2] net: gro: do not undo remote checksum offload on merged packets Eric Dumazet
@ 2026-10-30 12:59 ` Eric Dumazet
2026-11-02 13:00 ` netdev-bot+sashiko
1 sibling, 1 reply; 5+ messages in thread
From: Eric Dumazet @ 2026-10-30 12:59 UTC (permalink / raw)
To: David S . Miller, Jakub Kicinski, Paolo Abeni
Cc: Simon Horman, netdev, edumazet, Willem de Bruijn,
Steffen Klassert, Tom Herbert, Cen Zhang, AutonomousCodeSecurity,
stable, Taesoo Kim
Since commit bd56a29c7a4e ("net: gro: change skb_gro_network_header()"),
skb_gro_header_slow() no longer invalidates NAPI_GRO_CB(skb)->frag0
after pskb_may_pull(). skb_gro_may_pull() keeps comparing against the
frag0_len value set in skb_gro_reset_offset(), assuming that after a
slow pull, all later accesses are beyond frag0_len.
This is not true:
- udp4_gro_receive() and udp6_gro_receive() read the network header
with skb_gro_network_header() after udp_gro_udphdr() pulled the
UDP header.
- vxlan_gro_receive() calls skb_gro_remcsum_cleanup() at a lower
offset after the inner handlers pulled their headers.
- skb_gro_remcsum_process() can pull beyond the inner Ethernet header
before eth_gro_receive() reads it.
- napi_frags_skb() calls __skb_pull(skb, ETH_HLEN) after a slow pull,
so frag0 still points to the Ethernet header.
If the pull consumed the first page fragment, __skb_pull_tail() released
it. If the head had to grow, pskb_expand_head() freed the old one.
These reads, and the 2-byte write in skb_gro_remcsum_cleanup(), then
access memory the skb no longer holds a reference on.
esp4_gro_receive(), esp6_gro_receive(), skb_gro_receive_list() and
udp_gro_receive_segment() also pull directly and never invalidated
frag0.
Add skb_gro_frag0_invalidate(), which only clears frag0_len, and call
it before each of these pulls. This is a single store in the slow path,
and frag0 stays non NULL, so skb_gro_network_header() does not need
a NULL test.
frag0 must be invalidated even if the pull fails: __pskb_pull_tail()
can return NULL after pskb_expand_head() freed the old head, for
instance if a frag_list member has to be cloned or pulled.
Fixes: bd56a29c7a4e ("net: gro: change skb_gro_network_header()")
Fixes: 9fd1ff5d2ac7 ("udp: Support UDP fraglist GRO/GSO.")
Cc: stable@vger.kernel.org
Reported-by: Taesoo Kim <tsgatesv@gmail.com>
Reported-by: Cen Zhang <cenzhang@linux.microsoft.com>
Tested-by: Cen Zhang <cenzhang@linux.microsoft.com>
Signed-off-by: Eric Dumazet <edumazet@kernel.org>
---
include/net/gro.h | 10 ++++++++++
net/core/gro.c | 1 +
net/ipv4/esp4_offload.c | 1 +
net/ipv4/udp_offload.c | 1 +
net/ipv6/esp6_offload.c | 1 +
5 files changed, 14 insertions(+)
diff --git a/include/net/gro.h b/include/net/gro.h
index 00b49b6b9e74..6f6affc8ab91 100644
--- a/include/net/gro.h
+++ b/include/net/gro.h
@@ -160,9 +160,19 @@ static inline bool skb_gro_may_pull(const struct sk_buff *skb,
return likely(hlen <= NAPI_GRO_CB(skb)->frag0_len);
}
+/* A pull can release the page fragment frag0 points to, or reallocate
+ * skb->head, even if it fails. Call this before the pull, to make sure later
+ * skb_gro_header() and skb_gro_network_header() calls no longer use frag0.
+ */
+static inline void skb_gro_frag0_invalidate(struct sk_buff *skb)
+{
+ NAPI_GRO_CB(skb)->frag0_len = 0;
+}
+
static inline void *skb_gro_header_slow(struct sk_buff *skb, unsigned int hlen,
unsigned int offset)
{
+ skb_gro_frag0_invalidate(skb);
if (!pskb_may_pull(skb, hlen))
return NULL;
diff --git a/net/core/gro.c b/net/core/gro.c
index 1b033855a20d..bc88652e353a 100644
--- a/net/core/gro.c
+++ b/net/core/gro.c
@@ -238,6 +238,7 @@ int skb_gro_receive_list(struct sk_buff *p, struct sk_buff *skb)
NAPI_GRO_CB(skb)->flush))
return -E2BIG;
+ skb_gro_frag0_invalidate(skb);
if (!pskb_may_pull(skb, skb_gro_offset(skb))) {
NAPI_GRO_CB(skb)->flush = 1;
return -ENOMEM;
diff --git a/net/ipv4/esp4_offload.c b/net/ipv4/esp4_offload.c
index abd77162f5e7..2309468cfc3d 100644
--- a/net/ipv4/esp4_offload.c
+++ b/net/ipv4/esp4_offload.c
@@ -37,6 +37,7 @@ static struct sk_buff *esp4_gro_receive(struct list_head *head,
__be32 seq;
__be32 spi;
+ skb_gro_frag0_invalidate(skb);
if (!pskb_pull(skb, offset))
return NULL;
diff --git a/net/ipv4/udp_offload.c b/net/ipv4/udp_offload.c
index cf07c3c6611a..cd9d7426b227 100644
--- a/net/ipv4/udp_offload.c
+++ b/net/ipv4/udp_offload.c
@@ -743,6 +743,7 @@ static struct sk_buff *udp_gro_receive_segment(struct list_head *head,
pp = p;
} else {
if (NAPI_GRO_CB(skb)->is_flist) {
+ skb_gro_frag0_invalidate(skb);
if (!pskb_may_pull(skb, skb_gro_offset(skb))) {
NAPI_GRO_CB(skb)->flush = 1;
return NULL;
diff --git a/net/ipv6/esp6_offload.c b/net/ipv6/esp6_offload.c
index 22895521a57d..ca947cb94fb7 100644
--- a/net/ipv6/esp6_offload.c
+++ b/net/ipv6/esp6_offload.c
@@ -64,6 +64,7 @@ static struct sk_buff *esp6_gro_receive(struct list_head *head,
if (NAPI_GRO_CB(skb)->proto == IPPROTO_UDP)
encap_type = UDP_ENCAP_ESPINUDP;
+ skb_gro_frag0_invalidate(skb);
if (!pskb_pull(skb, offset))
return NULL;
--
2.53.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* Re: [PATCH net v2 1/2] net: gro: do not undo remote checksum offload on merged packets
2026-10-30 12:59 ` [PATCH net v2 1/2] net: gro: do not undo remote checksum offload on merged packets Eric Dumazet
@ 2026-11-02 13:00 ` netdev-bot+sashiko
0 siblings, 0 replies; 5+ messages in thread
From: netdev-bot+sashiko @ 2026-11-02 13:00 UTC (permalink / raw)
To: edumazet
Cc: davem, kuba, pabeni, horms, netdev, willemb, steffen.klassert,
tom, cenzhang, AutonomousCodeSecurity, stable
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider.
Critical: 0 (1) · High: 0 · Medium: 1 · Low: 0
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] The commit message says that skipping
skb_gro_remcsum_cleanup() for merged skbs is correct, and that…
Pre-existing issues:
- [Critical] At this commit, skb_gro_remcsum_cleanup() can still write
through a stale NAPI_GRO_CB(skb)->frag0 when a fraglist handler has…
--
net: gro: do not undo remote checksum offload on merged packets
With this patch, skb_gro_remcsum_cleanup() returns early when
NAPI_GRO_CB(skb)->same_flow is set. A merged skb therefore no longer has
its remote checksum offload adjustment undone. Once an skb has been
merged, grc->offset does not point at the original header any more. The
2-byte write could then land in payload or, with fraglist GRO, in a page
the skb no longer references.
> diff --git a/include/net/gro.h b/include/net/gro.h
> index 2300b6da05b27..00b49b6b9e74f 100644
> --- a/include/net/gro.h
> +++ b/include/net/gro.h
> @@ -351,7 +351,13 @@ static inline void skb_gro_remcsum_cleanup(struct sk_buff *skb,
> void *ptr;
> size_t plen = grc->offset + sizeof(u16);
>
> - if (!grc->delta)
> + /* Once merged, grc->offset can not be trusted: the frags or head
> + * of this skb now belong to the GRO packet, or skb->data was pulled
> + * past the headers before adding it to a frag_list.
> + * Fraglist members keep their adjusted inner checksum, which is the
> + * value skb_remcsum_process() would have written.
> + */
> + if (!grc->delta || NAPI_GRO_CB(skb)->same_flow)
> return;
[Severity: Medium]
The new comment says fraglist members keep "the value
skb_remcsum_process() would have written". The commit message says
the same thing:
grc->delta is only set with VXLAN_F_REMCSUM_NOPARTIAL, where
skb_remcsum_process() writes the same adjusted checksum in the non
GRO path. Keeping it is the intended result.
Is that still true when skb_segment_list() segments the fraglist train
before any remote checksum processing runs?
Two paths seem to do this.
The first is a transit VXLAN or GUE train that was aggregated because a
wildcard-bound local tunnel socket matched in udp4_gro_lookup_skb(), and
is then forwarded. 26c4f7da3e41, the commit named in Fixes:, was written
for this case: GRO must not change a packet that may be forwarded.
The second is GUE local delivery. fou sockets never call
udp_allow_gso(), so udp_unexpected_gso() returns true for fraglist
packets:
include/linux/udp.h:udp_unexpected_gso() {
...
if (skb_shinfo(skb)->gso_type & SKB_GSO_FRAGLIST &&
!udp_test_bit(ACCEPT_FRAGLIST, sk))
return true;
...
}
As a result, udp_queue_rcv_skb() segments the train before
gue_udp_recv() runs.
In both cases, skb_segment_list() copies the head's outer and tunnel
headers onto every member, including the RCO flag and start/offset.
__copy_skb_header() also copies the head's remcsum_offload bit, which
skb_gro_flush_final_remcsum() has cleared. That means the check below no
longer stops the adjustment from running a second time:
net/ipv4/fou_core.c:gue_remcsum() {
...
if (skb->remcsum_offload)
return guehdr;
...
}
vxlan_remcsum() on a remote VTEP has the same check. If remcsum_adjust()
runs on a field that already holds the final checksum, won't it produce
the pseudo-header sum instead? Every member except the head would then
carry a bad inner checksum. This requires rx-gro-list enabled on the
ingress device and a NOPARTIAL remote checksum offload flow.
This patch did not introduce the wrong member checksum. The old cleanup
never restored the members either; it wrote into the payload or a stale
frag0 page instead.
Could the comment and the commit message qualify the claim that keeping
the adjusted value is correct? The member headers still appear to be in
the linear headroom after skb_pull(), so they could be restored.
Also, GUE sets grc->delta as well:
net/ipv4/fou_core.c:gue_gro_receive() {
...
guehdr = gue_gro_remcsum(skb, off, guehdr,
data + doffset, hdrlen, &grc,
!!(fou->flags &
FOU_F_REMCSUM_NOPARTIAL));
...
}
So is "only set with VXLAN_F_REMCSUM_NOPARTIAL" accurate, or should it
mention FOU_F_REMCSUM_NOPARTIAL too?
>
> ptr = skb_gro_header(skb, plen, grc->offset);
[Severity: Critical]
This is a pre-existing issue and was not introduced by this patch. At
this commit, the non-merged case can still write through a stale frag0.
In udp_gro_receive_segment():
net/ipv4/udp_offload.c:udp_gro_receive_segment() {
...
if (NAPI_GRO_CB(skb)->is_flist) {
if (!pskb_may_pull(skb, skb_gro_offset(skb))) {
NAPI_GRO_CB(skb)->flush = 1;
return NULL;
}
if ((skb->ip_summed != p->ip_summed) ||
(skb->csum_level != p->csum_level)) {
NAPI_GRO_CB(skb)->flush = 1;
return NULL;
}
...
}
pskb_may_pull() can release a header-only frags[0] through
__pskb_pull_tail()->skb_frag_unref() without updating frag0 or
frag0_len.
The function can then return NULL on the ip_summed/csum_level mismatch,
or skb_gro_receive_list() can fail after its own pull. In either case
same_flow stays 0, so the new check does not apply.
With the stale frag0_len, skb_gro_header() takes the fast path. Can
remcsum_unadjust() then do a 2-byte read-modify-write into a page the
skb no longer holds a reference on? With page_pool, that page may
already have been recycled into another RX buffer.
The commit message says this case is handled in the following patch.
"net: gro: invalidate frag0 after a pull" appears to add
skb_gro_frag0_invalidate() in three places: before the pskb_may_pull()
calls in udp_gro_receive_segment() and skb_gro_receive_list(), and in
skb_gro_header_slow(). Once that patch is applied, cleanup on a
non-merged skb takes the slow path and writes at
skb->data + grc->offset.
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261030125955.3107383-1-edumazet%40kernel.org
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH net v2 2/2] net: gro: invalidate frag0 after a pull
2026-10-30 12:59 ` [PATCH net v2 2/2] net: gro: invalidate frag0 after a pull Eric Dumazet
@ 2026-11-02 13:00 ` netdev-bot+sashiko
2026-11-02 16:18 ` Eric Dumazet
0 siblings, 1 reply; 5+ messages in thread
From: netdev-bot+sashiko @ 2026-11-02 13:00 UTC (permalink / raw)
To: edumazet
Cc: davem, kuba, pabeni, horms, netdev, willemb, steffen.klassert,
tom, cenzhang, AutonomousCodeSecurity, stable
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider.
Critical: 0 · High: 1 (1) · Medium: 0 · Low: 0
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [High] net: gro: packet corruption in gro_pull_from_frag0() after
failed pull
Pre-existing issues:
- [High] This problem wasn't introduced by this patch, but
`napi_gro_frags()` (net/core/gro.c:783-794) never checks the result of…
--
net: gro: invalidate frag0 after a pull
This patch adds skb_gro_frag0_invalidate(), which clears
NAPI_GRO_CB(skb)->frag0_len. It is called before the slow pulls in
skb_gro_header_slow(), skb_gro_receive_list(), esp4_gro_receive(),
esp6_gro_receive() and udp_gro_receive_segment(). This keeps later GRO
header accesses from using a frag0 pointer that the pull may have
released or reallocated.
> diff --git a/include/net/gro.h b/include/net/gro.h
> index 00b49b6b9e74f..6f6affc8ab91c 100644
> --- a/include/net/gro.h
> +++ b/include/net/gro.h
> @@ -160,9 +160,19 @@ static inline bool skb_gro_may_pull(const struct sk_buff *skb,
> return likely(hlen <= NAPI_GRO_CB(skb)->frag0_len);
> }
>
> +/* A pull can release the page fragment frag0 points to, or reallocate
> + * skb->head, even if it fails. Call this before the pull, to make sure later
> + * skb_gro_header() and skb_gro_network_header() calls no longer use frag0.
> + */
> +static inline void skb_gro_frag0_invalidate(struct sk_buff *skb)
> +{
> + NAPI_GRO_CB(skb)->frag0_len = 0;
> +}
> +
> static inline void *skb_gro_header_slow(struct sk_buff *skb, unsigned int hlen,
> unsigned int offset)
> {
> + skb_gro_frag0_invalidate(skb);
> if (!pskb_may_pull(skb, hlen))
> return NULL;
[Severity: High]
Does clearing frag0_len here, while frag0 itself is left pointing at
the old location, break gro_pull_from_frag0() when the slow pull in
skb_gro_header_slow() fails?
Consider an skb that enters GRO with skb_headlen(skb) == 0. In that
case skb_gro_reset_offset() points frag0 at the start of frags[0],
which is packet offset 0.
Now add remote checksum offload nopartial: vxlan_gro_receive() or
vxlan_gpe_gro_receive() with VXLAN_F_REMCSUM_NOPARTIAL, or
gue_gro_receive() with FOU_F_REMCSUM_NOPARTIAL. There,
skb_gro_remcsum_process() records grc->offset and grc->delta through
the frag0 fast path. The inner eth_gro_receive() and
inet_gro_receive()/ipv6_gro_receive() handlers then advance
skb_gro_offset() through frag0 too, so skb_headlen() is still 0.
Suppose the next inner header does not fit in frag0_len, for instance
in tcp_gro_pull_header() or udp_gro_udphdr(). skb_gro_header_slow()
now sets frag0_len to 0 and calls pskb_may_pull(). On a truncated
packet where hlen > skb->len, this fails without pulling anything.
That leaves skb_headlen() at 0 and frag0_len at 0.
On the way out, skb_gro_flush_final_remcsum() calls
skb_gro_remcsum_cleanup(), which does:
ptr = skb_gro_header(skb, plen, grc->offset);
Before this patch that was served from frag0. Now skb_gro_may_pull()
fails, and pskb_may_pull(skb, plen) really copies plen bytes into the
linear area. Here plen = grc->offset + sizeof(u16), which is below
skb_gro_offset(skb) once the inner headers were pulled. frags[0] is
advanced by plen, but NAPI_GRO_CB(skb)->frag0 still points at packet
offset 0.
Since the skb is flushed, dev_gro_receive() ends up in
gro_try_pull_from_frag0(). That computes
grow = skb_gro_offset(skb) - skb_headlen(skb) > 0 and calls
gro_pull_from_frag0():
memcpy(skb_tail_pointer(skb), NAPI_GRO_CB(skb)->frag0, grow);
This copies packet bytes 0..grow to skb->data + plen, then advances
frags[0] by grow again. The skb handed up the stack as GRO_NORMAL
has its inner headers overwritten with outer header bytes, and the
real bytes at plen..skb_gro_offset() are lost. A remote sender can
trigger this with a short encapsulated packet.
gro_pull_from_frag0() assumes frag0 is the current start of frags[0],
which only holds while skb_headlen() is 0. Should it copy from
skb_frag_address(&pinfo->frags[0]) instead? Or should
skb_gro_remcsum_cleanup() and gro_try_pull_from_frag0() handle a
partially linearized skb in some other way?
[Severity: High]
This patch didn't introduce this, but since the failure path of
skb_gro_header_slow() is being changed here: what happens in
napi_gro_frags() when this pull fails for the Ethernet header?
In net/core/gro.c, napi_frags_skb() takes the slow path and returns
NULL when the pull fails:
net/core/gro.c:napi_frags_skb() {
...
if (unlikely(!skb_gro_may_pull(skb, hlen))) {
eth = skb_gro_header_slow(skb, hlen, 0);
if (unlikely(!eth)) {
net_warn_ratelimited("%s: dropping impossible skb from %s\n",
__func__, napi->dev->name);
napi_reuse_skb(napi, skb);
return NULL;
...
}
The caller doesn't check for NULL, though:
net/core/gro.c:napi_gro_frags() {
struct sk_buff *skb = napi_frags_skb(napi);
trace_napi_gro_frags_entry(skb);
ret = napi_frags_finish(napi, skb, dev_gro_receive(&napi->gro, skb));
...
}
dev_gro_receive() starts with skb_get_hash_raw(skb) and then reads
skb->protocol. Can this be a NULL pointer dereference in NAPI softirq
context? One way to reach it would be skb->len < ETH_HLEN, where
pskb_may_pull() fails with PKT_TOO_SMALL. Another would be
__pskb_pull_tail() returning NULL.
napi_gro_frags() used to have an "if (!skb) return GRO_DROP;" check.
Commit 1d11fa696733 ("net-gro: remove GRO_DROP") removed it on the
grounds that NULL could only come from a failed napi_get_frags()
allocation. That reasoning doesn't cover the header pull failure return
shown above.
The drivers checked so far (tun, r8152, e1000) all enforce a minimum
frame length before calling napi_gro_frags(). The other callers have
not been checked.
Should napi_gro_frags() check for a NULL skb again?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261030125955.3107383-1-edumazet%40kernel.org
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH net v2 2/2] net: gro: invalidate frag0 after a pull
2026-11-02 13:00 ` netdev-bot+sashiko
@ 2026-11-02 16:18 ` Eric Dumazet
0 siblings, 0 replies; 5+ messages in thread
From: Eric Dumazet @ 2026-11-02 16:18 UTC (permalink / raw)
To: netdev-bot+sashiko
Cc: davem, kuba, pabeni, horms, netdev, willemb, steffen.klassert,
tom, cenzhang, AutonomousCodeSecurity, stable
Le lun. 2 nov. 2026 à 14:00, <netdev-bot+sashiko@kernel.org> a écrit :
>
> Thank you for your contribution! Sashiko AI review found 1 potential
> issue(s) to consider.
>
> Critical: 0 · High: 1 (1) · Medium: 0 · Low: 0
>
> If you believe none of the issues are legitimate, please reply to each
> issue of Medium severity or higher and explain why the AI is wrong.
> If any of them are legitimate and you plan to respin, please let patchwork
> know by including "pw-bot: cr" as a separate line at the end of your reply
> (one such reply per series is enough).
>
> - [High] net: gro: packet corruption in gro_pull_from_frag0() after
> failed pull
>
> Pre-existing issues:
> - [High] This problem wasn't introduced by this patch, but
> `napi_gro_frags()` (net/core/gro.c:783-794) never checks the result of…
>
> --
>
> net: gro: invalidate frag0 after a pull
>
> This patch adds skb_gro_frag0_invalidate(), which clears
> NAPI_GRO_CB(skb)->frag0_len. It is called before the slow pulls in
> skb_gro_header_slow(), skb_gro_receive_list(), esp4_gro_receive(),
> esp6_gro_receive() and udp_gro_receive_segment(). This keeps later GRO
> header accesses from using a frag0 pointer that the pull may have
> released or reallocated.
>
> > diff --git a/include/net/gro.h b/include/net/gro.h
> > index 00b49b6b9e74f..6f6affc8ab91c 100644
> > --- a/include/net/gro.h
> > +++ b/include/net/gro.h
> > @@ -160,9 +160,19 @@ static inline bool skb_gro_may_pull(const struct sk_buff *skb,
> > return likely(hlen <= NAPI_GRO_CB(skb)->frag0_len);
> > }
> >
> > +/* A pull can release the page fragment frag0 points to, or reallocate
> > + * skb->head, even if it fails. Call this before the pull, to make sure later
> > + * skb_gro_header() and skb_gro_network_header() calls no longer use frag0.
> > + */
> > +static inline void skb_gro_frag0_invalidate(struct sk_buff *skb)
> > +{
> > + NAPI_GRO_CB(skb)->frag0_len = 0;
> > +}
> > +
> > static inline void *skb_gro_header_slow(struct sk_buff *skb, unsigned int hlen,
> > unsigned int offset)
> > {
> > + skb_gro_frag0_invalidate(skb);
> > if (!pskb_may_pull(skb, hlen))
> > return NULL;
>
> [Severity: High]
> Does clearing frag0_len here, while frag0 itself is left pointing at
> the old location, break gro_pull_from_frag0() when the slow pull in
> skb_gro_header_slow() fails?
>
> Consider an skb that enters GRO with skb_headlen(skb) == 0. In that
> case skb_gro_reset_offset() points frag0 at the start of frags[0],
> which is packet offset 0.
>
> Now add remote checksum offload nopartial: vxlan_gro_receive() or
> vxlan_gpe_gro_receive() with VXLAN_F_REMCSUM_NOPARTIAL, or
> gue_gro_receive() with FOU_F_REMCSUM_NOPARTIAL. There,
> skb_gro_remcsum_process() records grc->offset and grc->delta through
> the frag0 fast path. The inner eth_gro_receive() and
> inet_gro_receive()/ipv6_gro_receive() handlers then advance
> skb_gro_offset() through frag0 too, so skb_headlen() is still 0.
>
> Suppose the next inner header does not fit in frag0_len, for instance
> in tcp_gro_pull_header() or udp_gro_udphdr(). skb_gro_header_slow()
> now sets frag0_len to 0 and calls pskb_may_pull(). On a truncated
> packet where hlen > skb->len, this fails without pulling anything.
> That leaves skb_headlen() at 0 and frag0_len at 0.
>
> On the way out, skb_gro_flush_final_remcsum() calls
> skb_gro_remcsum_cleanup(), which does:
>
> ptr = skb_gro_header(skb, plen, grc->offset);
>
> Before this patch that was served from frag0. Now skb_gro_may_pull()
> fails, and pskb_may_pull(skb, plen) really copies plen bytes into the
> linear area. Here plen = grc->offset + sizeof(u16), which is below
> skb_gro_offset(skb) once the inner headers were pulled. frags[0] is
> advanced by plen, but NAPI_GRO_CB(skb)->frag0 still points at packet
> offset 0.
>
> Since the skb is flushed, dev_gro_receive() ends up in
> gro_try_pull_from_frag0(). That computes
> grow = skb_gro_offset(skb) - skb_headlen(skb) > 0 and calls
> gro_pull_from_frag0():
>
> memcpy(skb_tail_pointer(skb), NAPI_GRO_CB(skb)->frag0, grow);
Right, this was added in v2 when moving the invalidation before the
pull. After a failed pull, skb_gro_remcsum_cleanup() can pull fewer
bytes than skb_gro_offset(), while frag0 still points to the start
of the packet.
>
> This copies packet bytes 0..grow to skb->data + plen, then advances
> frags[0] by grow again. The skb handed up the stack as GRO_NORMAL
> has its inner headers overwritten with outer header bytes, and the
> real bytes at plen..skb_gro_offset() are lost. A remote sender can
> trigger this with a short encapsulated packet.
>
> gro_pull_from_frag0() assumes frag0 is the current start of frags[0],
> which only holds while skb_headlen() is 0. Should it copy from
> skb_frag_address(&pinfo->frags[0]) instead? Or should
> skb_gro_remcsum_cleanup() and gro_try_pull_from_frag0() handle a
> partially linearized skb in some other way?
>
> [Severity: High]
> This patch didn't introduce this, but since the failure path of
> skb_gro_header_slow() is being changed here: what happens in
> napi_gro_frags() when this pull fails for the Ethernet header?
>
> In net/core/gro.c, napi_frags_skb() takes the slow path and returns
> NULL when the pull fails:
>
> net/core/gro.c:napi_frags_skb() {
> ...
> if (unlikely(!skb_gro_may_pull(skb, hlen))) {
> eth = skb_gro_header_slow(skb, hlen, 0);
> if (unlikely(!eth)) {
> net_warn_ratelimited("%s: dropping impossible skb from %s\n",
> __func__, napi->dev->name);
> napi_reuse_skb(napi, skb);
> return NULL;
> ...
> }
>
> The caller doesn't check for NULL, though:
>
> net/core/gro.c:napi_gro_frags() {
> struct sk_buff *skb = napi_frags_skb(napi);
>
> trace_napi_gro_frags_entry(skb);
>
> ret = napi_frags_finish(napi, skb, dev_gro_receive(&napi->gro, skb));
> ...
> }
>
> dev_gro_receive() starts with skb_get_hash_raw(skb) and then reads
> skb->protocol. Can this be a NULL pointer dereference in NAPI softirq
> context? One way to reach it would be skb->len < ETH_HLEN, where
> pskb_may_pull() fails with PKT_TOO_SMALL. Another would be
> __pskb_pull_tail() returning NULL.
>
> napi_gro_frags() used to have an "if (!skb) return GRO_DROP;" check.
> Commit 1d11fa696733 ("net-gro: remove GRO_DROP") removed it on the
> grounds that NULL could only come from a failed napi_get_frags()
> allocation. That reasoning doesn't cover the header pull failure return
> shown above.
>
> The drivers checked so far (tun, r8152, e1000) all enforce a minimum
> frame length before calling napi_gro_frags(). The other callers have
> not been checked.
>
> Should napi_gro_frags() check for a NULL skb again?
I do not think this can happen.
napi_get_frags() allocates the skb with GRO_MAX_HEAD bytes of
tailroom, and these skbs have no frag_list, so __pskb_pull_tail()
can not fail when pulling ETH_HLEN bytes.
pskb_may_pull() can only fail if skb->len < ETH_HLEN, meaning the
driver passed a frame shorter than an Ethernet header, which drivers
must not do. tun, where the length comes from user space, checks it
in tun_get_user().
This is why napi_frags_skb() calls it an "impossible skb". This
could be cleaned up in net-next, but is not a stable fix.
pw-bot: cr
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-11-02 16:18 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
[not found] <20261030125955.3107383-1-edumazet@kernel.org>
2026-10-30 12:59 ` [PATCH net v2 1/2] net: gro: do not undo remote checksum offload on merged packets Eric Dumazet
2026-11-02 13:00 ` netdev-bot+sashiko
2026-10-30 12:59 ` [PATCH net v2 2/2] net: gro: invalidate frag0 after a pull Eric Dumazet
2026-11-02 13:00 ` netdev-bot+sashiko
2026-11-02 16:18 ` Eric Dumazet
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox