[PATCH net v4] ipv6: prevent stale UDP packets after ADDRFORM
From: Daehyeon Ko
Date: Thu Sep 03 2026 - 13:59:23 EST
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@xxxxxxxxx
Link: https://lore.kernel.org/r/20260829144847.1738294-1-4ncienth@xxxxxxxxx
Link: https://lore.kernel.org/r/20260901141352.236286-1-pabeni@xxxxxxxxxx
Link: https://lore.kernel.org/r/20260902055802.3724915-1-4ncienth@xxxxxxxxx
Link: https://lore.kernel.org/r/CANn89i+CSVHtjDk_NQT_Q6NpwJ76WQ=CExXp1xY15iPQuZSp+w@xxxxxxxxxxxxxx
Link: https://lore.kernel.org/r/84b011a3-7662-4f6e-ad4d-6bf81d2e1b30@xxxxxxxxx
Cc: stable@xxxxxxxxxxxxxxx
Assisted-by: LLM
Signed-off-by: Daehyeon Ko <4ncienth@xxxxxxxxx>
---
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@xxxxxxxxx
- validate option offsets and lengths in __ip_options_echo()
v2: https://lore.kernel.org/r/20260829144847.1738294-1-4ncienth@xxxxxxxxx
- use the network header version instead of skb->protocol
v1: https://lore.kernel.org/r/20260825221922.85651-1-4ncienth@xxxxxxxxx
- 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@xxxxxxxxxx
---
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