* [PATCH net] ipv6: gro: avoid truncating transport header offsets
@ 2026-11-11 20:42 Hyunwoo Kim
2026-11-11 20:47 ` netdev-bot+sinfo
2026-11-13 9:14 ` Eric Dumazet
0 siblings, 2 replies; 4+ messages in thread
From: Hyunwoo Kim @ 2026-11-11 20:42 UTC (permalink / raw)
To: David Ahern, Ido Schimmel, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Richard Gobert,
Willem de Bruijn
Cc: Kuniyuki Iwashima, netdev, imv4bel
skb->transport_header is a 16-bit offset from skb->head, and U16_MAX is
reserved as the unset transport header value. A long IPv6 extension
header chain can make the final offset exceed the field's range.
skb_set_transport_header() then truncates it.
In the UDP reproducer, UDP_GRO is enabled on the receiving socket and
the first packet is held on the GRO list. The truncated offset makes
udp_hdr(p) point to the source MAC address. The first four bytes of the
source MAC address match the UDP source and destination ports, allowing
the second packet in the same flow to pass the port comparison.
gro_receive_network_flush() then derives the first packet's IPv6 header
address from the truncated transport header offset. ipv6_gro_flush()
consequently reads from an address 65,536 bytes before the first packet's
actual IPv6 header. Both UDP and TCP GRO call
gro_receive_network_flush().
KASAN reports:
BUG: KASAN: use-after-free in udp_gro_receive+0x1f37/0x2680
Read of size 4 at addr ffff8881065f005e by task swapper/1/0
...
Call Trace:
<IRQ>
...
udp_gro_receive+0x1f37/0x2680
udp6_gro_receive+0x23e/0x11d0
ipv6_gro_receive+0x1200/0x2750
...
dev_gro_receive+0x1a7c/0x2600
gro_receive_skb+0x271/0x970
virtnet_receive_done+0x3b5/0x12c0
receive_buf+0x33a/0x4b40
...
virtnet_poll+0xc69/0x1980
...
__napi_poll+0xa0/0x530
net_rx_action+0x8ff/0x1280
...
</IRQ>
The out-of-bounds address was in a free page, so KASAN reported a
use-after-free. This classification reflects the state of that page,
not the lifetime of the skb. The invalid read was caused by truncation
of the 16-bit transport header offset.
Use skb_set_transport_header_careful() to validate the final offset
before assigning it. If the offset cannot be represented, the skb is
not held as a new GRO head and is instead delivered through the normal
receive path.
Fixes: 4b0ebbca3e16 ("net: gro: move L3 flush checks to tcp_gro_receive and udp_gro_receive_segment")
Cc: stable@vger.kernel.org
Assisted-by: LLM
Signed-off-by: Hyunwoo Kim <imv4bel@gmail.com>
---
net/ipv6/ip6_offload.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/net/ipv6/ip6_offload.c b/net/ipv6/ip6_offload.c
index 700f1861d8eb7d..de4bab42359ba9 100644
--- a/net/ipv6/ip6_offload.c
+++ b/net/ipv6/ip6_offload.c
@@ -241,7 +241,8 @@ INDIRECT_CALLABLE_SCOPE struct sk_buff *ipv6_gro_receive(struct list_head *head,
skb_gro_pull(skb, sizeof(*iph));
}
- skb_set_transport_header(skb, skb_gro_offset(skb));
+ if (!skb_set_transport_header_careful(skb, skb_gro_offset(skb)))
+ goto out;
NAPI_GRO_CB(skb)->proto = proto;
--
2.43.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH net] ipv6: gro: avoid truncating transport header offsets
2026-11-11 20:42 [PATCH net] ipv6: gro: avoid truncating transport header offsets Hyunwoo Kim
@ 2026-11-11 20:47 ` netdev-bot+sinfo
2026-11-13 9:14 ` Eric Dumazet
1 sibling, 0 replies; 4+ messages in thread
From: netdev-bot+sinfo @ 2026-11-11 20:47 UTC (permalink / raw)
To: Hyunwoo Kim
Cc: David Ahern, Ido Schimmel, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Richard Gobert,
Willem de Bruijn, Kuniyuki Iwashima, netdev
Hi!
This is an automated message. This series looks like a fix, but its
commit messages seem to be missing some information:
- How the issue was discovered, e.g. hit in production, hit during
development, syzbot report, manual code inspection, LLM or static
analysis tool scan.
Please do not repost the series just to address the above. Instead,
reply to this email with the missing information, so that reviewers
can take it into account. If the series needs another revision for
other reasons, please include the information in the commit messages
then.
The evaluation is done by an LLM so it may be wrong, if you think
that is the case please reply and explain.
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH net] ipv6: gro: avoid truncating transport header offsets
2026-11-11 20:42 [PATCH net] ipv6: gro: avoid truncating transport header offsets Hyunwoo Kim
2026-11-11 20:47 ` netdev-bot+sinfo
@ 2026-11-13 9:14 ` Eric Dumazet
2026-11-13 11:56 ` Hyunwoo Kim
1 sibling, 1 reply; 4+ messages in thread
From: Eric Dumazet @ 2026-11-13 9:14 UTC (permalink / raw)
To: Hyunwoo Kim
Cc: David Ahern, Ido Schimmel, David S. Miller, Jakub Kicinski,
Paolo Abeni, Simon Horman, Richard Gobert, Willem de Bruijn,
Kuniyuki Iwashima, netdev
Le mer. 11 nov. 2026 à 21:42, Hyunwoo Kim <imv4bel@gmail.com> a écrit :
>
> skb->transport_header is a 16-bit offset from skb->head, and U16_MAX is
> reserved as the unset transport header value. A long IPv6 extension
> header chain can make the final offset exceed the field's range.
> skb_set_transport_header() then truncates it.
>
> In the UDP reproducer, UDP_GRO is enabled on the receiving socket and
> the first packet is held on the GRO list. The truncated offset makes
> udp_hdr(p) point to the source MAC address. The first four bytes of the
> source MAC address match the UDP source and destination ports, allowing
> the second packet in the same flow to pass the port comparison.
>
> gro_receive_network_flush() then derives the first packet's IPv6 header
> address from the truncated transport header offset. ipv6_gro_flush()
> consequently reads from an address 65,536 bytes before the first packet's
> actual IPv6 header. Both UDP and TCP GRO call
> gro_receive_network_flush().
>
> KASAN reports:
>
> BUG: KASAN: use-after-free in udp_gro_receive+0x1f37/0x2680
> Read of size 4 at addr ffff8881065f005e by task swapper/1/0
> ...
> Call Trace:
> <IRQ>
> ...
> udp_gro_receive+0x1f37/0x2680
> udp6_gro_receive+0x23e/0x11d0
> ipv6_gro_receive+0x1200/0x2750
> ...
> dev_gro_receive+0x1a7c/0x2600
> gro_receive_skb+0x271/0x970
> virtnet_receive_done+0x3b5/0x12c0
> receive_buf+0x33a/0x4b40
> ...
> virtnet_poll+0xc69/0x1980
> ...
> __napi_poll+0xa0/0x530
> net_rx_action+0x8ff/0x1280
> ...
> </IRQ>
>
> The out-of-bounds address was in a free page, so KASAN reported a
> use-after-free. This classification reflects the state of that page,
> not the lifetime of the skb. The invalid read was caused by truncation
> of the 16-bit transport header offset.
>
> Use skb_set_transport_header_careful() to validate the final offset
> before assigning it. If the offset cannot be represented, the skb is
> not held as a new GRO head and is instead delivered through the normal
> receive path.
>
> Fixes: 4b0ebbca3e16 ("net: gro: move L3 flush checks to tcp_gro_receive and udp_gro_receive_segment")
> Cc: stable@vger.kernel.org
> Assisted-by: LLM
> Signed-off-by: Hyunwoo Kim <imv4bel@gmail.com>
> ---
> net/ipv6/ip6_offload.c | 3 ++-
> 1 file changed, 2 insertions(+), 1 deletion(-)
>
> diff --git a/net/ipv6/ip6_offload.c b/net/ipv6/ip6_offload.c
> index 700f1861d8eb7d..de4bab42359ba9 100644
> --- a/net/ipv6/ip6_offload.c
> +++ b/net/ipv6/ip6_offload.c
> @@ -241,7 +241,8 @@ INDIRECT_CALLABLE_SCOPE struct sk_buff *ipv6_gro_receive(struct list_head *head,
> skb_gro_pull(skb, sizeof(*iph));
> }
>
> - skb_set_transport_header(skb, skb_gro_offset(skb));
> + if (!skb_set_transport_header_careful(skb, skb_gro_offset(skb)))
> + goto out;
>
> NAPI_GRO_CB(skb)->proto = proto;
>
> --
> 2.43.0
Thanks for the report.
This part looks correct to me, but I think the fix is incomplete.
1) The root cause is that GRO accepts an unbounded extension header
chain. The non-GRO receive path does not, since
3744b0964d52 ("ipv6: Implement limits on extension header parsing")
ip6_protocol_deliver_rcu() gives up after IP6_MAX_EXT_HDRS_CNT (12)
extension headers, which bounds a chain to ~24KB. That commit did not
touch ipv6_gro_pull_exthdrs().
There is no point trying to aggregate a packet the stack is going to
drop anyway. I am going to send a patch [1] applying the same limit in
ipv6_gro_pull_exthdrs(), with your Reported-by.
This should be enough for your reproducer, could you test it once
posted ?
2) The limit does not replace the _careful() check : with nested
encapsulations, each level can add its own chain, so checking at the
points where the offsets are stored still makes sense.
But then inet_gro_receive() has the same pattern:
skb_gro_pull(skb, sizeof(*iph));
skb_set_transport_header(skb, skb_gro_offset(skb));
and can be reached with a large offset as well :
a) IPv6 + exthdr chain + IPPROTO_IPIP
ipv6_gro_receive() -> ip4ip6_gro_receive() -> inet_gro_receive()
b) IPv6 + exthdr chain + UDP tunnel (or GRE) + inner IPv4.
The outer transport header offset still fits in 16 bits, so your
new check passes, but the inner one might not.
I have not tried to reproduce these, this is from code inspection only.
Inner IPv6 (ip6ip6, or IPv6 in a tunnel) goes through ipv6_gro_receive()
again, so that one is covered by your patch.
Please send a V2 on top of my patch, handling inet_gro_receive()
as well.
3) Unrelated to your patch, but while looking at this : the non-GRO
path has the same kind of unchecked 16-bit arithmetic after a tunnel
decap, since skb->head is not reset and offsets accumulate :
ip6_rcv_core(): skb->transport_header = skb->network_header + sizeof(*hdr);
ip_rcv_core(): skb->transport_header = skb->network_header + iph->ihl*4;
For IPv6 a wrap seems to end up in a failed pskb_pull() in
ip6_protocol_deliver_rcu(), but for IPv4 skb_network_header_len()
would be negative in ip_local_deliver_finish(), and __skb_pull() is
not going to like it.
With the 12 extension headers limit this needs several levels of
nested IPv6 tunnels, each with a maximal chain, so I do not think
this is a practical concern for current kernels. Still, these two
spots could use a checked helper. This can be a separate patch
targeting net-next, I can take care of it if you prefer.
Also, as the bot pointed out, please state in the changelog how the
issue was found. "Assisted-by: LLM" does not really tell us if this
was a fuzzer, a code audit, or something else.
Thanks.
pw-bot: cr
[1]
diff --git a/net/ipv6/ip6_offload.c b/net/ipv6/ip6_offload.c
index 700f1861d8eb..01642900c0fa 100644
--- a/net/ipv6/ip6_offload.c
+++ b/net/ipv6/ip6_offload.c
@@ -26,6 +26,7 @@ static int ipv6_gro_pull_exthdrs(struct sk_buff
*skb, int off, int proto)
{
const struct net_offload *ops = NULL;
struct ipv6_opt_hdr *opth;
+ int exthdr_cnt = 0;
for (;;) {
int len;
@@ -38,6 +39,13 @@ static int ipv6_gro_pull_exthdrs(struct sk_buff
*skb, int off, int proto)
if (!(ops->flags & INET6_PROTO_GSO_EXTHDR))
break;
+ /* Same limit as ip6_protocol_deliver_rcu().
+ * @proto is left pointing to an extension header,
+ * so that the caller does not aggregate this packet.
+ */
+ if (unlikely(exthdr_cnt++ >= IP6_MAX_EXT_HDRS_CNT))
+ break;
+
opth = skb_gro_header(skb, off + sizeof(*opth), off);
if (unlikely(!opth))
break;
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH net] ipv6: gro: avoid truncating transport header offsets
2026-11-13 9:14 ` Eric Dumazet
@ 2026-11-13 11:56 ` Hyunwoo Kim
0 siblings, 0 replies; 4+ messages in thread
From: Hyunwoo Kim @ 2026-11-13 11:56 UTC (permalink / raw)
To: Eric Dumazet
Cc: David Ahern, Ido Schimmel, David S. Miller, Jakub Kicinski,
Paolo Abeni, Simon Horman, Richard Gobert, Willem de Bruijn,
Kuniyuki Iwashima, netdev, imv4bel
On Fri, Nov 13, 2026 at 10:14:22AM +0100, Eric Dumazet wrote:
> Le mer. 11 nov. 2026 à 21:42, Hyunwoo Kim <imv4bel@gmail.com> a écrit :
> >
> > skb->transport_header is a 16-bit offset from skb->head, and U16_MAX is
> > reserved as the unset transport header value. A long IPv6 extension
> > header chain can make the final offset exceed the field's range.
> > skb_set_transport_header() then truncates it.
> >
> > In the UDP reproducer, UDP_GRO is enabled on the receiving socket and
> > the first packet is held on the GRO list. The truncated offset makes
> > udp_hdr(p) point to the source MAC address. The first four bytes of the
> > source MAC address match the UDP source and destination ports, allowing
> > the second packet in the same flow to pass the port comparison.
> >
> > gro_receive_network_flush() then derives the first packet's IPv6 header
> > address from the truncated transport header offset. ipv6_gro_flush()
> > consequently reads from an address 65,536 bytes before the first packet's
> > actual IPv6 header. Both UDP and TCP GRO call
> > gro_receive_network_flush().
> >
> > KASAN reports:
> >
> > BUG: KASAN: use-after-free in udp_gro_receive+0x1f37/0x2680
> > Read of size 4 at addr ffff8881065f005e by task swapper/1/0
> > ...
> > Call Trace:
> > <IRQ>
> > ...
> > udp_gro_receive+0x1f37/0x2680
> > udp6_gro_receive+0x23e/0x11d0
> > ipv6_gro_receive+0x1200/0x2750
> > ...
> > dev_gro_receive+0x1a7c/0x2600
> > gro_receive_skb+0x271/0x970
> > virtnet_receive_done+0x3b5/0x12c0
> > receive_buf+0x33a/0x4b40
> > ...
> > virtnet_poll+0xc69/0x1980
> > ...
> > __napi_poll+0xa0/0x530
> > net_rx_action+0x8ff/0x1280
> > ...
> > </IRQ>
> >
> > The out-of-bounds address was in a free page, so KASAN reported a
> > use-after-free. This classification reflects the state of that page,
> > not the lifetime of the skb. The invalid read was caused by truncation
> > of the 16-bit transport header offset.
> >
> > Use skb_set_transport_header_careful() to validate the final offset
> > before assigning it. If the offset cannot be represented, the skb is
> > not held as a new GRO head and is instead delivered through the normal
> > receive path.
> >
> > Fixes: 4b0ebbca3e16 ("net: gro: move L3 flush checks to tcp_gro_receive and udp_gro_receive_segment")
> > Cc: stable@vger.kernel.org
> > Assisted-by: LLM
> > Signed-off-by: Hyunwoo Kim <imv4bel@gmail.com>
> > ---
> > net/ipv6/ip6_offload.c | 3 ++-
> > 1 file changed, 2 insertions(+), 1 deletion(-)
> >
> > diff --git a/net/ipv6/ip6_offload.c b/net/ipv6/ip6_offload.c
> > index 700f1861d8eb7d..de4bab42359ba9 100644
> > --- a/net/ipv6/ip6_offload.c
> > +++ b/net/ipv6/ip6_offload.c
> > @@ -241,7 +241,8 @@ INDIRECT_CALLABLE_SCOPE struct sk_buff *ipv6_gro_receive(struct list_head *head,
> > skb_gro_pull(skb, sizeof(*iph));
> > }
> >
> > - skb_set_transport_header(skb, skb_gro_offset(skb));
> > + if (!skb_set_transport_header_careful(skb, skb_gro_offset(skb)))
> > + goto out;
> >
> > NAPI_GRO_CB(skb)->proto = proto;
> >
> > --
> > 2.43.0
>
>
> Thanks for the report.
>
> This part looks correct to me, but I think the fix is incomplete.
>
> 1) The root cause is that GRO accepts an unbounded extension header
> chain. The non-GRO receive path does not, since
>
> 3744b0964d52 ("ipv6: Implement limits on extension header parsing")
>
> ip6_protocol_deliver_rcu() gives up after IP6_MAX_EXT_HDRS_CNT (12)
> extension headers, which bounds a chain to ~24KB. That commit did not
> touch ipv6_gro_pull_exthdrs().
>
> There is no point trying to aggregate a packet the stack is going to
> drop anyway. I am going to send a patch [1] applying the same limit in
> ipv6_gro_pull_exthdrs(), with your Reported-by.
>
>
> This should be enough for your reproducer, could you test it once
> posted ?
I ran A/B tests with your [1] patch. It fixes the issue.
>
> 2) The limit does not replace the _careful() check : with nested
> encapsulations, each level can add its own chain, so checking at the
> points where the offsets are stored still makes sense.
>
> But then inet_gro_receive() has the same pattern:
>
> skb_gro_pull(skb, sizeof(*iph));
> skb_set_transport_header(skb, skb_gro_offset(skb));
>
> and can be reached with a large offset as well :
>
> a) IPv6 + exthdr chain + IPPROTO_IPIP
> ipv6_gro_receive() -> ip4ip6_gro_receive() -> inet_gro_receive()
>
> b) IPv6 + exthdr chain + UDP tunnel (or GRE) + inner IPv4.
> The outer transport header offset still fits in 16 bits, so your
> new check passes, but the inner one might not.
>
> I have not tried to reproduce these, this is from code inspection only.
>
> Inner IPv6 (ip6ip6, or IPv6 in a tunnel) goes through ipv6_gro_receive()
> again, so that one is covered by your patch.
>
> Please send a V2 on top of my patch, handling inet_gro_receive()
> as well.
Ok, I will send v2 shortly on top of your [1] patch.
>
> 3) Unrelated to your patch, but while looking at this : the non-GRO
> path has the same kind of unchecked 16-bit arithmetic after a tunnel
> decap, since skb->head is not reset and offsets accumulate :
>
> ip6_rcv_core(): skb->transport_header = skb->network_header + sizeof(*hdr);
> ip_rcv_core(): skb->transport_header = skb->network_header + iph->ihl*4;
>
> For IPv6 a wrap seems to end up in a failed pskb_pull() in
> ip6_protocol_deliver_rcu(), but for IPv4 skb_network_header_len()
> would be negative in ip_local_deliver_finish(), and __skb_pull() is
> not going to like it.
>
> With the 12 extension headers limit this needs several levels of
> nested IPv6 tunnels, each with a maximal chain, so I do not think
> this is a practical concern for current kernels. Still, these two
> spots could use a checked helper. This can be a separate patch
> targeting net-next, I can take care of it if you prefer.
I would appreciate it if you could handle this issue.
>
> Also, as the bot pointed out, please state in the changelog how the
> issue was found. "Assisted-by: LLM" does not really tell us if this
> was a fuzzer, a code audit, or something else.
I found this issue through AI-assisted auditing. Some of my other patches
under review came from AI-assisted auditing, and others from AI-assisted
fuzzing tuned for race detection.
I will note this in future commit messages.
Best regards,
Hyunwoo Kim
>
> Thanks.
>
> pw-bot: cr
>
> [1]
> diff --git a/net/ipv6/ip6_offload.c b/net/ipv6/ip6_offload.c
> index 700f1861d8eb..01642900c0fa 100644
> --- a/net/ipv6/ip6_offload.c
> +++ b/net/ipv6/ip6_offload.c
> @@ -26,6 +26,7 @@ static int ipv6_gro_pull_exthdrs(struct sk_buff
> *skb, int off, int proto)
> {
> const struct net_offload *ops = NULL;
> struct ipv6_opt_hdr *opth;
> + int exthdr_cnt = 0;
>
> for (;;) {
> int len;
> @@ -38,6 +39,13 @@ static int ipv6_gro_pull_exthdrs(struct sk_buff
> *skb, int off, int proto)
> if (!(ops->flags & INET6_PROTO_GSO_EXTHDR))
> break;
>
> + /* Same limit as ip6_protocol_deliver_rcu().
> + * @proto is left pointing to an extension header,
> + * so that the caller does not aggregate this packet.
> + */
> + if (unlikely(exthdr_cnt++ >= IP6_MAX_EXT_HDRS_CNT))
> + break;
> +
> opth = skb_gro_header(skb, off + sizeof(*opth), off);
> if (unlikely(!opth))
> break;
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-11-13 11:56 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-11 20:42 [PATCH net] ipv6: gro: avoid truncating transport header offsets Hyunwoo Kim
2026-11-11 20:47 ` netdev-bot+sinfo
2026-11-13 9:14 ` Eric Dumazet
2026-11-13 11:56 ` Hyunwoo Kim
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox