[PATCH net v3] udp: revalidate socket family before publishing an IPv6 cork
From: Daehyeon Ko
Date: Tue Sep 01 2026 - 21:04:31 EST
udpv6_sendmsg() prepares IPv6 flow and route state before taking the socket
lock when a datagram is corked. IPV6_ADDRFORM takes the same lock, but it
can convert the socket to AF_INET during that lockless preparation because
no cork has been published yet.
If the conversion wins, udpv6_sendmsg() can publish an AF_INET6 cork on an
AF_INET socket. Uncorking through the IPv4 operations then writes a
20-byte IPv4 header into the 40-byte IPv6 reservation while the retained
IPv6 dst routes the skb through ip6_output(). ip6_finish_output2()
consequently consumes the unwritten 20-byte tail.
An unprivileged reproducer triggered the mixed state on 12 of 10,000
sockets. KMSAN reported an uninitialized-value read in
ip6_finish_output2() on three fresh boots, with the allocation origin in
__alloc_skb() through __ip6_append_data(). The same process recovered the
20-byte region from the TX timestamp error queue on all three boots; one
contained recognizable nonzero stale heap data.
There is also socket state to handle before cork publication. A sendto()
with an AF_UNSPEC address clears its explicit destination and later selects
the stored peer with connected set. On an ADDRFORM-eligible socket with a
mapped peer and a suitable IPv6 route, ip6_sk_dst_lookup_flow() can
therefore publish an IPv6 dst in the socket cache. A native connected send
racing a new mapped connect can reach the same store. Revalidating only
after lock_sock() prevents the mixed cork, but does not cover the non-cork
path or undo a dst stored before conversion.
Close both orderings. After a connected IPv6 lookup, reject a socket which
has already been converted and reset any dst the lookup may have stored.
When UDP ADDRFORM publishes the AF_INET family, reset the socket dst after
the family write. Thus, if the send stores and checks first, the later
conversion clears the cache; if conversion and its reset happen first, the
send observes AF_INET after its store and clears it. Keep the locked
family check before cork publication to prevent the original mixed cork.
With a mapped loopback route supplied before dropping to uid 65534 with no
capabilities, an unpatched KASAN control reached ip6_sk_dst_store_flow()
from the AF_UNSPEC send. On patched current net, the sequential conversion
released that dst from do_ipv6_setsockopt(). Corked and non-corked races
both reached the dst store, then returned EAFNOSUPPORT when conversion won;
the converted socket's follow-up send used only udp_sendmsg(). No KASAN,
Oops, warning, or panic followed. The concurrent mapped-connect variant
was validated from the same source ordering, but was not forced in a
dedicated runtime interleaving.
Fixes: 03485f2adcde ("udpv6: Add lockless sendmsg() support")
Closes: https://lore.kernel.org/netdev/20260825160630.1888866-1-4ncienth@xxxxxxxxx/
Link: https://lore.kernel.org/netdev/a86aa34a-8bea-46d2-a785-6c423ac00b1d@xxxxxxxxxx/
Cc: stable@xxxxxxxxxxxxxxx
Assisted-by: LLM
Signed-off-by: Daehyeon Ko <4ncienth@xxxxxxxxx>
---
Changes in v3:
- Address Paolo and Sashiko's finding that AF_UNSPEC and a concurrent mapped
connect can publish an IPv6 socket dst before the family revalidation.
- Revalidate after a connected route lookup and reset a dst stored after an
already completed conversion.
- Reset the UDP socket dst after ADDRFORM publishes AF_INET, covering the
opposite store-before-convert ordering and the non-cork path.
- Retain the post-lock family check which prevents late AF_INET6 cork
publication.
- Add route-enabled, zero-capability KASAN matrix and cork/non-cork race
validation. The separate privileged sockmap mismatch remains out of scope.
- State that the concurrent mapped-connect variant was source-validated but
was not forced in a dedicated runtime interleaving.
v2: https://lore.kernel.org/netdev/20260829132125.1160893-1-4ncienth@xxxxxxxxx/
v1: https://lore.kernel.org/netdev/20260825160630.1888866-1-4ncienth@xxxxxxxxx/
Testing notes:
- Build and runtime validation used x86_64. Sparse was unavailable;
allmodconfig and multi-architecture builds were not run.
- The tested reproducer is available privately on request and is not included
in this public AI-assisted security-fix submission.
---
net/ipv6/ipv6_sockglue.c | 1 +
net/ipv6/udp.c | 11 +++++++++++
2 files changed, 12 insertions(+)
diff --git a/net/ipv6/ipv6_sockglue.c b/net/ipv6/ipv6_sockglue.c
index b4c977434c2e..59ca446d3156 100644
--- a/net/ipv6/ipv6_sockglue.c
+++ b/net/ipv6/ipv6_sockglue.c
@@ -611,6 +611,7 @@ int do_ipv6_setsockopt(struct sock *sk, int level, int optname,
WRITE_ONCE(sk->sk_prot, &udp_prot);
WRITE_ONCE(sk->sk_socket->ops, &inet_dgram_ops);
WRITE_ONCE(sk->sk_family, PF_INET);
+ sk_dst_reset(sk);
}
/* Disable all options not to allocate memory anymore,
diff --git a/net/ipv6/udp.c b/net/ipv6/udp.c
index 93478d1ad576..8ba552f1a6b2 100644
--- a/net/ipv6/udp.c
+++ b/net/ipv6/udp.c
@@ -1705,6 +1705,12 @@ int udpv6_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
dst = NULL;
goto out;
}
+ if (unlikely(connected &&
+ READ_ONCE(sk->sk_family) != AF_INET6)) {
+ sk_dst_reset(sk);
+ err = -EAFNOSUPPORT;
+ goto out;
+ }
if (ipc6.hlimit < 0)
ipc6.hlimit = ip6_sk_dst_hoplimit(np, fl6, dst);
@@ -1729,6 +1735,11 @@ int udpv6_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
}
lock_sock(sk);
+ if (unlikely(sk->sk_family != AF_INET6)) {
+ release_sock(sk);
+ err = -EAFNOSUPPORT;
+ goto out;
+ }
if (unlikely(up->pending)) {
/* The socket is already corked while preparing it. */
/* ... which is an evident application bug. --ANK */
base-commit: 70f3995830d3f1e79faa14eb0605914f778feca9
--
2.55.0