IPV6_ADDRFORM can convert an established IPv6 UDP socket to IPv4 while packets created under the IPv6 protocol remain queued. The subsequent IPv4 receive paths then interpret both the IPv6 header and inet6_skb_parm as their IPv4 counterparts. For regular data, IP_RETOPTS can use the overlaid IPv6 extension-header offsets as IPv4 option metadata and copy a packet-controlled length past a 40-byte stack buffer. The same write is reachable from an IPv6 error skb after conversion when SOF_TIMESTAMPING_OPT_CMSG enables IPv4 ancillary data on the error path. UDP keeps received datagrams on sk_receive_queue and moves them to reader_queue while servicing recvmsg, including MSG_PEEK. Checking those queues alone is not sufficient because UDP receive does not take the socket lock. A packet can pass IPv6 lookup before the check and be published after the protocol switch. Block new IPv6 UDP receive before inspecting both data queues. Wait for network receive handlers which passed the block to finish, then scan the receive and reader queues under their established lock order. Clear the block and return EBUSY if either contains an IPv6 skb. Keep it set after a successful conversion so a stale early-demux socket cannot publish another IPv6 skb. IPv4 skbs already queued on the dual-stack socket remain valid and do not prevent conversion. Not every IPv6 error producer is covered by the network grace period. Make the IPv4 error consumer skip IPv6 error skbs only on a UDP socket marked by ADDRFORM. This preserves ordinary IPv4 error skbs, including timestamps taken after tunnel encapsulation, while preventing stale IPv6 control blocks from reaching IPv4 cmsg parsing. A queue-only prototype delivered an IPv6 datagram after a successful conversion on iteration 23 of a delayed-enqueue race. With the receive gate in place, the same test observed no post-conversion delivery in 10,000 iterations. Receive and MSG_PEEK reader queues returned EBUSY, queued IPv4 data survived conversion, and the IPv6 error-queue reproducer no longer reached __ip_options_echo(). Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") Closes: https://lore.kernel.org/r/20260825221922.85651-1-4ncienth@gmail.com Link: https://lore.kernel.org/r/20260829144847.1738294-1-4ncienth@gmail.com Link: https://lore.kernel.org/r/20260901141352.236286-1-pabeni@redhat.com Link: https://lore.kernel.org/r/20260902055802.3724915-1-4ncienth@gmail.com Link: https://lore.kernel.org/r/CANn89i+CSVHtjDk_NQT_Q6NpwJ76WQ=CExXp1xY15iPQuZSp+w@mail.gmail.com Link: https://lore.kernel.org/r/84b011a3-7662-4f6e-ad4d-6bf81d2e1b30@linux.dev Cc: stable@vger.kernel.org Assisted-by: LLM Signed-off-by: Daehyeon Ko <4ncienth@gmail.com> --- v4: - replace the v3 __ip_options_echo() validation with an IPV6_ADDRFORM-specific fix - gate IPv6 UDP receive, wait for in-flight network handlers, and reject conversion only when receive or reader queues contain IPv6 skbs - preserve queued IPv4 data across conversion - skip IPv6 error skbs in the IPv4 error consumer only for UDP datagram sockets converted by ADDRFORM, while preserving following IPv4 errors - cover the post-check enqueue race which broke a queue-only prototype after 23 iterations; the gated version had no witness in 10,000 - leave the distinct ipmr control-block bug to Zhiling Zou's existing series, as requested by Eric Dumazet and Ido Schimmel - drop all v3 ip_options.c changes, including the path where syzbot CI reported a pskb_network_may_pull() warning v3: https://lore.kernel.org/r/20260902055802.3724915-1-4ncienth@gmail.com - validate option offsets and lengths in __ip_options_echo() v2: https://lore.kernel.org/r/20260829144847.1738294-1-4ncienth@gmail.com - use the network header version instead of skb->protocol v1: https://lore.kernel.org/r/20260825221922.85651-1-4ncienth@gmail.com - reject SOL_IP cmsgs unless skb->protocol is ETH_P_IP syzbot CI report on v3: https://lore.kernel.org/r/6a989da2.2e567484.299ffc.0001.GAE@google.com --- include/linux/udp.h | 1 + net/ipv4/ip_sockglue.c | 8 ++++++++ net/ipv6/ipv6_sockglue.c | 37 +++++++++++++++++++++++++++++++++++++ net/ipv6/udp.c | 5 +++++ 4 files changed, 51 insertions(+) diff --git a/include/linux/udp.h b/include/linux/udp.h index 998906ec3b32a..f775f78d6bf47 100644 --- a/include/linux/udp.h +++ b/include/linux/udp.h @@ -67,6 +67,7 @@ enum { UDP_FLAGS_ACCEPT_FRAGLIST, UDP_FLAGS_ACCEPT_L4, UDP_FLAGS_ENCAP_ENABLED, /* This socket enabled encap */ + UDP_FLAGS_ADDRFORM, /* Block IPv6 receive during/after ADDRFORM */ }; /* per NUMA structure for lockless producer usage. */ diff --git a/net/ipv4/ip_sockglue.c b/net/ipv4/ip_sockglue.c index a55ef327ec932..fcd102ae8bedb 100644 --- a/net/ipv4/ip_sockglue.c +++ b/net/ipv4/ip_sockglue.c @@ -533,9 +533,17 @@ int ip_recv_error(struct sock *sk, struct msghdr *msg, int len) int copied; err = -EAGAIN; +again: skb = sock_dequeue_err_skb(sk); if (!skb) goto out; + if (unlikely(sk->sk_type == SOCK_DGRAM && + sk->sk_protocol == IPPROTO_UDP && + udp_test_bit(ADDRFORM, sk) && + skb->protocol == htons(ETH_P_IPV6))) { + consume_skb(skb); + goto again; + } copied = skb->len; if (copied > len) { diff --git a/net/ipv6/ipv6_sockglue.c b/net/ipv6/ipv6_sockglue.c index b4c977434c2e0..d307cea9ebc5c 100644 --- a/net/ipv6/ipv6_sockglue.c +++ b/net/ipv6/ipv6_sockglue.c @@ -373,6 +373,33 @@ static int ipv6_set_opt_hdr(struct sock *sk, int optname, sockptr_t optval, return err; } +static bool udp6_addrform_queue_has_ipv6(struct sock *sk) +{ + struct sk_buff_head *reader_queue = &udp_sk(sk)->reader_queue; + struct sk_buff *skb; + bool found = false; + + spin_lock_bh(&reader_queue->lock); + spin_lock(&sk->sk_receive_queue.lock); + skb_queue_walk(reader_queue, skb) { + if (skb->protocol == htons(ETH_P_IPV6)) { + found = true; + goto unlock; + } + } + skb_queue_walk(&sk->sk_receive_queue, skb) { + if (skb->protocol == htons(ETH_P_IPV6)) { + found = true; + break; + } + } +unlock: + spin_unlock(&sk->sk_receive_queue.lock); + spin_unlock_bh(&reader_queue->lock); + + return found; +} + int do_ipv6_setsockopt(struct sock *sk, int level, int optname, sockptr_t optval, unsigned int optlen) { @@ -587,6 +614,16 @@ int do_ipv6_setsockopt(struct sock *sk, int level, int optname, break; } + if (sk->sk_protocol == IPPROTO_UDP) { + udp_set_bit(ADDRFORM, sk); + synchronize_net(); + if (udp6_addrform_queue_has_ipv6(sk)) { + udp_clear_bit(ADDRFORM, sk); + retv = -EBUSY; + break; + } + } + __ipv6_sock_mc_close(sk); __ipv6_sock_ac_close(sk); diff --git a/net/ipv6/udp.c b/net/ipv6/udp.c index 93478d1ad5769..ccd8ae92f52ba 100644 --- a/net/ipv6/udp.c +++ b/net/ipv6/udp.c @@ -822,6 +822,11 @@ static int udpv6_queue_rcv_one_skb(struct sock *sk, struct sk_buff *skb) struct udp_sock *up = udp_sk(sk); struct net *net = sock_net(sk); + if (unlikely(udp_test_bit(ADDRFORM, sk))) { + drop_reason = SKB_DROP_REASON_NO_SOCKET; + goto drop; + } + if (!xfrm6_policy_check(sk, XFRM_POLICY_IN, skb)) { drop_reason = SKB_DROP_REASON_XFRM_POLICY; goto drop; base-commit: 66817a9794263cd2a5dc4e99bf8e5fcc5ff7181e -- 2.55.0