Netdev List
 help / color / mirror / Atom feed
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;

      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