From: Hyunwoo Kim <imv4bel@gmail.com>
To: Eric Dumazet <edumazet@kernel.org>
Cc: David Ahern <dsahern@kernel.org>,
Ido Schimmel <idosch@nvidia.com>,
"David S. Miller" <davem@davemloft.net>,
Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>,
Simon Horman <horms@kernel.org>,
Richard Gobert <richardbgobert@gmail.com>,
Willem de Bruijn <willemb@google.com>,
Kuniyuki Iwashima <kuniyu@google.com>,
netdev@vger.kernel.org, imv4bel@gmail.com
Subject: Re: [PATCH net] ipv6: gro: avoid truncating transport header offsets
Date: Fri, 13 Nov 2026 20:56:40 +0900 [thread overview]
Message-ID: <avb6-Hh6ITrDumOi@v4bel> (raw)
In-Reply-To: <CAL4Wiiq3a4Hpxg8LYQjP3XURKwngxOyDtQFYn48fy0y8OoASvg@mail.gmail.com>
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;
prev parent reply other threads:[~2026-11-13 11:56 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
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 message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=avb6-Hh6ITrDumOi@v4bel \
--to=imv4bel@gmail.com \
--cc=davem@davemloft.net \
--cc=dsahern@kernel.org \
--cc=edumazet@kernel.org \
--cc=horms@kernel.org \
--cc=idosch@nvidia.com \
--cc=kuba@kernel.org \
--cc=kuniyu@google.com \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=richardbgobert@gmail.com \
--cc=willemb@google.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox