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: ... 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 ... 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. The IP6_MAX_EXT_HDRS_CNT limit in ipv6_gro_pull_exthdrs() prevents this reproducer. However, nested encapsulations can each add an extension header chain, so the accumulated offset can still exceed the range of skb->transport_header. inet_gro_receive() can also be reached with such an offset. Use skb_set_transport_header_careful() in ipv6_gro_receive() and inet_gro_receive() 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. This issue was found through AI-based auditing. 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 --- Changes in v2: - Base v2 on a tree with Eric's [1] patch applied. (Eric) - Handle unrepresentable transport header offsets in inet_gro_receive(). (Eric) - Explain why nested encapsulations still require the checked assignments. (Eric) - State that the issue was found through AI-based auditing. (Eric) - Change the subject prefix from ipv6: gro: to net: gro:. - v1: https://lore.kernel.org/all/avTTQNyvwPP6LtGx@v4bel/ - [1]: https://lore.kernel.org/all/20261113124625.4010206-2-edumazet@kernel.org/ --- net/ipv4/af_inet.c | 5 ++++- net/ipv6/ip6_offload.c | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/net/ipv4/af_inet.c b/net/ipv4/af_inet.c index 8822bae439f88d..7db9b52662f6f5 100644 --- a/net/ipv4/af_inet.c +++ b/net/ipv4/af_inet.c @@ -1530,7 +1530,10 @@ struct sk_buff *inet_gro_receive(struct list_head *head, struct sk_buff *skb) * as we already checked checksum over ipv4 header was 0 */ 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))) { + flush = 1; + goto out; + } pp = indirect_call_gro_receive(tcp4_gro_receive, udp4_gro_receive, ops->callbacks.gro_receive, head, skb); diff --git a/net/ipv6/ip6_offload.c b/net/ipv6/ip6_offload.c index 01642900c0fa33..fe38dc15faa67a 100644 --- a/net/ipv6/ip6_offload.c +++ b/net/ipv6/ip6_offload.c @@ -249,7 +249,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