Netdev List
 help / color / mirror / Atom feed
* [PATCH net] udp: do not use a cross-family sk_rx_dst in early demux
@ 2026-11-06 11:01 Eric Dumazet
  2026-11-07 19:52 ` Kuniyuki Iwashima
                   ` (2 more replies)
  0 siblings, 3 replies; 5+ messages in thread
From: Eric Dumazet @ 2026-11-06 11:01 UTC (permalink / raw)
  To: David S . Miller, Jakub Kicinski, Paolo Abeni, Kuniyuki Iwashima,
	Willem de Bruijn
  Cc: Simon Horman, netdev, Eric Dumazet

A dual-stack UDP socket connected to a v4-mapped address can receive
both IPv4 packets and IPv6 packets using v4-mapped addresses:
ip6_rcv_core() no longer rejects a v4-mapped source since commit
dcc32f4f183a ("ipv6: weaken the v4mapped source check"), which kept
that check only in tcp_v6_conn_request(). ipv6_portaddr_hash() hashes
v4-mapped addresses like IPv4 ones, so both early demux handlers find
the socket, and both __udp4_lib_rcv() and __udp6_lib_rcv() store their
input dst in the same sk->sk_rx_dst.

udp_v6_early_demux() validates the cached dst with dst_check(), but
ipv4_dst_check() ignores the cookie, so an IPv4 dst gets attached to
an IPv6 skb. ip6_rcv_finish_core() then skips the route lookup and
ip_local_deliver() parses the IPv6 header as an IPv4 one: the IPv6
next header and hop limit are read as a fragment offset, and the IPv4
protocol is taken from a source address byte.

The reverse direction looks possible too: per-cpu dsts of IPv6 routes
using nexthop objects have rt->sernum set, and ip6_dst_check() then
ignores the cookie, so udp_v4_early_demux() can attach an IPv6 dst to
an IPv4 skb.

Kyle Zeng reported that such a packet can reach XFRM input still
holding the prefetched, non refcounted, socket set by early demux,
which can then outlive its RCU grace period.

Only use the cached dst if its family matches the packet. Otherwise
fall back to a regular route lookup.

TCP does not have the same problem: sk_ehashfn() hashes a v4-mapped
socket with inet_ehashfn(), while IPv6 lookups use inet6_ehashfn(),
so IPv6 packets do not normally find a v4-mapped TCP socket.

Fixes: 5425077d73e0 ("net: ipv6: Add early demux handler for UDP unicast")
Fixes: c9f2c1ae123a ("udp6: fix socket leak on early demux")
Signed-off-by: Eric Dumazet <edumazet@kernel.org>
---
This was found while reviewing an AI-generated XFRM patch from Kyle
Zeng, which did not state how the issue was discovered. The
cross-family dst was then identified by manual code inspection.

The IPv4 dst on IPv6 skb case was reproduced on an unmodified kernel,
without error injection, using two netns joined by a veth pair:

 - target: AF_INET6 UDP socket bound to ::ffff:10.0.0.1 port 5000,
   connected to ::ffff:10.0.0.2 port 6000.
 - peer: send a few IPv4 UDP datagrams 10.0.0.2:6000 -> 10.0.0.1:5000.
 - peer: inject with AF_PACKET IPv6/UDP packets from ::ffff:10.0.0.2
   port 6000 to ::ffff:10.0.0.1 port 5000.

Before this patch, IpReasmReqds in the target netns increases once per
injected IPv6 packet. After it, these packets are dropped by IPv6
routing (Ip6InNoRoutes), and the socket still receives the IPv4
datagrams. The IPv6 dst on IPv4 skb case was not reproduced.

reuseport_dualstack, udpgso.sh, udpgro.sh and udpgro_fwd.sh selftests
pass with this patch.

 net/ipv4/udp.c | 5 ++++-
 net/ipv6/udp.c | 5 ++++-
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/net/ipv4/udp.c b/net/ipv4/udp.c
index 303d0102519ac1aeac0b04d53e70f846c3a43779..2e5562f353f85d880a15793750d01037303f376e 100644
--- a/net/ipv4/udp.c
+++ b/net/ipv4/udp.c
@@ -2850,8 +2850,11 @@ enum skb_drop_reason udp_v4_early_demux(struct sk_buff *skb)
 	skb->destructor = sock_pfree;
 	dst = rcu_dereference(sk->sk_rx_dst);
 
-	if (dst)
+	/* A dual-stack socket can cache an IPv6 dst. */
+	if (dst && dst->ops->family == AF_INET)
 		dst = dst_check(dst, 0);
+	else
+		dst = NULL;
 	if (dst) {
 		u32 itag = 0;
 
diff --git a/net/ipv6/udp.c b/net/ipv6/udp.c
index 59f9872c355211704ba0899b1953d97d4ffb4617..9dcefa162522352465d25c234e70b8adedea7cd7 100644
--- a/net/ipv6/udp.c
+++ b/net/ipv6/udp.c
@@ -1249,8 +1249,11 @@ void udp_v6_early_demux(struct sk_buff *skb)
 	skb->destructor = sock_pfree;
 	dst = rcu_dereference(sk->sk_rx_dst);
 
-	if (dst)
+	/* A dual-stack socket can cache an IPv4 dst. */
+	if (dst && dst->ops->family == AF_INET6)
 		dst = dst_check(dst, sk->sk_rx_dst_cookie);
+	else
+		dst = NULL;
 	if (dst) {
 		/* set noref for now.
 		 * any place which wants to hold dst has to call
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 5+ messages in thread

* Re: [PATCH net] udp: do not use a cross-family sk_rx_dst in early demux
  2026-11-06 11:01 [PATCH net] udp: do not use a cross-family sk_rx_dst in early demux Eric Dumazet
@ 2026-11-07 19:52 ` Kuniyuki Iwashima
  2026-11-09 23:02 ` netdev-bot+sashiko
  2026-11-11  0:50 ` patchwork-bot+netdevbpf
  2 siblings, 0 replies; 5+ messages in thread
From: Kuniyuki Iwashima @ 2026-11-07 19:52 UTC (permalink / raw)
  To: Eric Dumazet
  Cc: David S . Miller, Jakub Kicinski, Paolo Abeni, Willem de Bruijn,
	Simon Horman, netdev

On Fri, 06 Nov 2026 12:01:35 +0100, Eric Dumazet wrote:
> A dual-stack UDP socket connected to a v4-mapped address can receive
> both IPv4 packets and IPv6 packets using v4-mapped addresses:
> ip6_rcv_core() no longer rejects a v4-mapped source since commit
> dcc32f4f183a ("ipv6: weaken the v4mapped source check"), which kept
> that check only in tcp_v6_conn_request(). ipv6_portaddr_hash() hashes
> v4-mapped addresses like IPv4 ones, so both early demux handlers find
> the socket, and both __udp4_lib_rcv() and __udp6_lib_rcv() store their
> input dst in the same sk->sk_rx_dst.
> 
> udp_v6_early_demux() validates the cached dst with dst_check(), but
> ipv4_dst_check() ignores the cookie, so an IPv4 dst gets attached to
> an IPv6 skb. ip6_rcv_finish_core() then skips the route lookup and
> ip_local_deliver() parses the IPv6 header as an IPv4 one: the IPv6
> next header and hop limit are read as a fragment offset, and the IPv4
> protocol is taken from a source address byte.
> 
> The reverse direction looks possible too: per-cpu dsts of IPv6 routes
> using nexthop objects have rt->sernum set, and ip6_dst_check() then
> ignores the cookie, so udp_v4_early_demux() can attach an IPv6 dst to
> an IPv4 skb.
> 
> Kyle Zeng reported that such a packet can reach XFRM input still
> holding the prefetched, non refcounted, socket set by early demux,
> which can then outlive its RCU grace period.
> 
> Only use the cached dst if its family matches the packet. Otherwise
> fall back to a regular route lookup.
> 
> TCP does not have the same problem: sk_ehashfn() hashes a v4-mapped
> socket with inet_ehashfn(), while IPv6 lookups use inet6_ehashfn(),
> so IPv6 packets do not normally find a v4-mapped TCP socket.
> 
> Fixes: 5425077d73e0 ("net: ipv6: Add early demux handler for UDP unicast")
> Fixes: c9f2c1ae123a ("udp6: fix socket leak on early demux")
> Signed-off-by: Eric Dumazet <edumazet@kernel.org>

Reviewed-by: Kuniyuki Iwashima <kuniyu@google.com>

^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH net] udp: do not use a cross-family sk_rx_dst in early demux
  2026-11-06 11:01 [PATCH net] udp: do not use a cross-family sk_rx_dst in early demux Eric Dumazet
  2026-11-07 19:52 ` Kuniyuki Iwashima
@ 2026-11-09 23:02 ` netdev-bot+sashiko
  2026-11-10  7:13   ` Eric Dumazet
  2026-11-11  0:50 ` patchwork-bot+netdevbpf
  2 siblings, 1 reply; 5+ messages in thread
From: netdev-bot+sashiko @ 2026-11-09 23:02 UTC (permalink / raw)
  To: edumazet; +Cc: davem, kuba, pabeni, kuniyu, willemb, horms, netdev

Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider.

Critical: 0 · High: 0 · Medium: 1 · Low: 0

If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).

- [Medium] The commit message says "TCP does not have the same problem",
  but tcp_v6_early_demux() (net/ipv6/ip6_input.c) uses the same pattern…

--

udp: do not use a cross-family sk_rx_dst in early demux

With this patch, udp_v4_early_demux() and udp_v6_early_demux() use the
cached sk->sk_rx_dst only when dst->ops->family matches the packet's
family. Without the check, a dual-stack socket connected to a v4-mapped
address can attach an IPv4 dst to an IPv6 skb, or the reverse, because
the dst_check() callbacks can ignore the cookie.

> TCP does not have the same problem: sk_ehashfn() hashes a v4-mapped
> socket with inet_ehashfn(), while IPv6 lookups use inet6_ehashfn(),
> so IPv6 packets do not normally find a v4-mapped TCP socket.

[Severity: Medium]
Is this statement about TCP fully accurate? tcp_v6_early_demux() in
net/ipv6/ip6_input.c uses the same pattern this patch removes from UDP.
It never checks the family of the cached dst:

net/ipv6/ip6_input.c:tcp_v6_early_demux() {
    ...
		if (sk_fullsock(sk)) {
			struct dst_entry *dst = rcu_dereference(sk->sk_rx_dst);

			if (dst)
				dst = dst_check(dst, sk->sk_rx_dst_cookie);
			if (dst &&
			    sk->sk_rx_dst_ifindex == skb->skb_iif)
				skb_dst_set_noref(skb, dst);
		}
    ...
}

A v4-mapped AF_INET6 TCP socket caches an IPv4 rtable in sk_rx_dst.
That covers a dual-stack child accepted over IPv4 and a socket connected
to ::ffff:a.b.c.d. ipv4_dst_check() ignores the cookie, so dst_check()
here does not reject that rtable.

Take a spoofed IPv6 segment from ::ffff:C to ::ffff:S with matching ports
(ip6_rcv_core() now accepts it). inet6_match() would accept the
v4-mapped socket for it. The only thing that keeps the socket out of the
lookup is the sk_hash comparison:

net/ipv6/inet6_hashtables.c:__inet6_lookup_established() {
    ...
	hash = inet6_ehashfn(net, daddr, hnum, saddr, sport);
    ...
		if (sk->sk_hash != hash)
			continue;
		if (!inet6_match(net, sk, saddr, daddr, ports, dif, sdif))
			continue;
    ...
}

The socket's sk_hash comes from inet_ehashfn() in sk_ehashfn(), keyed by
inet_ehash_secret. The lookup uses inet6_ehashfn(), keyed by
tcp_ipv6_hash_secret.

These are two independently keyed 32-bit hashes. A collision for a given
tuple is unlikely but possible. The secrets are fixed for each boot, so a
tuple that collides once keeps colliding.

If such a collision happens, the IPv4 dst would be attached to the IPv6
skb. Then ip6_rcv_finish_core() would skip ip6_route_input(), and
ip_local_deliver() would parse the IPv6 header as IPv4. Is that the same
misparse this patch fixes for UDP?

The XFRM non-refcounted socket issue does not apply here, because
__inet6_lookup_established() takes a reference and the destructor is
sock_edemux.

Could the same family guard go into tcp_v6_early_demux() (AF_INET6) and
tcp_v4_early_demux() (AF_INET)? Otherwise, the commit message could say
that TCP relies on the hash mismatch rather than on an explicit check.

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261106110135.2161204-1-edumazet%40kernel.org

^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH net] udp: do not use a cross-family sk_rx_dst in early demux
  2026-11-09 23:02 ` netdev-bot+sashiko
@ 2026-11-10  7:13   ` Eric Dumazet
  0 siblings, 0 replies; 5+ messages in thread
From: Eric Dumazet @ 2026-11-10  7:13 UTC (permalink / raw)
  To: netdev-bot+sashiko; +Cc: davem, kuba, pabeni, kuniyu, willemb, horms, netdev

Le mar. 10 nov. 2026 à 00:02, <netdev-bot+sashiko@kernel.org> a écrit :
>
> Thank you for your contribution! Sashiko AI review found 1 potential
> issue(s) to consider.
>
> Critical: 0 · High: 0 · Medium: 1 · Low: 0
>
> If you believe none of the issues are legitimate, please reply to each
> issue of Medium severity or higher and explain why the AI is wrong.
> If any of them are legitimate and you plan to respin, please let patchwork
> know by including "pw-bot: cr" as a separate line at the end of your reply
> (one such reply per series is enough).
>
> - [Medium] The commit message says "TCP does not have the same problem",
>   but tcp_v6_early_demux() (net/ipv6/ip6_input.c) uses the same pattern…
>
> --
>
> udp: do not use a cross-family sk_rx_dst in early demux
>
> With this patch, udp_v4_early_demux() and udp_v6_early_demux() use the
> cached sk->sk_rx_dst only when dst->ops->family matches the packet's
> family. Without the check, a dual-stack socket connected to a v4-mapped
> address can attach an IPv4 dst to an IPv6 skb, or the reverse, because
> the dst_check() callbacks can ignore the cookie.
>
> > TCP does not have the same problem: sk_ehashfn() hashes a v4-mapped
> > socket with inet_ehashfn(), while IPv6 lookups use inet6_ehashfn(),
> > so IPv6 packets do not normally find a v4-mapped TCP socket.
>
> [Severity: Medium]
> Is this statement about TCP fully accurate? tcp_v6_early_demux() in
> net/ipv6/ip6_input.c uses the same pattern this patch removes from UDP.
> It never checks the family of the cached dst:
>
> net/ipv6/ip6_input.c:tcp_v6_early_demux() {
>     ...
>                 if (sk_fullsock(sk)) {
>                         struct dst_entry *dst = rcu_dereference(sk->sk_rx_dst);
>
>                         if (dst)
>                                 dst = dst_check(dst, sk->sk_rx_dst_cookie);
>                         if (dst &&
>                             sk->sk_rx_dst_ifindex == skb->skb_iif)
>                                 skb_dst_set_noref(skb, dst);
>                 }
>     ...
> }
>
> A v4-mapped AF_INET6 TCP socket caches an IPv4 rtable in sk_rx_dst.
> That covers a dual-stack child accepted over IPv4 and a socket connected
> to ::ffff:a.b.c.d. ipv4_dst_check() ignores the cookie, so dst_check()
> here does not reject that rtable.
>
> Take a spoofed IPv6 segment from ::ffff:C to ::ffff:S with matching ports
> (ip6_rcv_core() now accepts it). inet6_match() would accept the
> v4-mapped socket for it. The only thing that keeps the socket out of the
> lookup is the sk_hash comparison:
>
> net/ipv6/inet6_hashtables.c:__inet6_lookup_established() {
>     ...
>         hash = inet6_ehashfn(net, daddr, hnum, saddr, sport);
>     ...
>                 if (sk->sk_hash != hash)
>                         continue;
>                 if (!inet6_match(net, sk, saddr, daddr, ports, dif, sdif))
>                         continue;
>     ...
> }
>
> The socket's sk_hash comes from inet_ehashfn() in sk_ehashfn(), keyed by
> inet_ehash_secret. The lookup uses inet6_ehashfn(), keyed by
> tcp_ipv6_hash_secret.
>
> These are two independently keyed 32-bit hashes. A collision for a given
> tuple is unlikely but possible. The secrets are fixed for each boot, so a
> tuple that collides once keeps colliding.
>
> If such a collision happens, the IPv4 dst would be attached to the IPv6
> skb. Then ip6_rcv_finish_core() would skip ip6_route_input(), and
> ip_local_deliver() would parse the IPv6 header as IPv4. Is that the same
> misparse this patch fixes for UDP?
>
> The XFRM non-refcounted socket issue does not apply here, because
> __inet6_lookup_established() takes a reference and the destructor is
> sock_edemux.
>
> Could the same family guard go into tcp_v6_early_demux() (AF_INET6) and
> tcp_v4_early_demux() (AF_INET)? Otherwise, the commit message could say
> that TCP relies on the hash mismatch rather than on an explicit check.
>
> --
> Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261106110135.2161204-1-edumazet%40kernel.org

The analysis is correct in theory, which is why the changelog says
"do not normally".

For TCP, an IPv6 lookup can only find a v4-mapped socket if
inet6_ehashfn() of the v4-mapped tuple equals inet_ehashfn() of the
IPv4 tuple. These are two independently keyed 32-bit hashes, and the
sender knows neither secret. It has no way to tell which tuple
collides, and each attempt needs its own established IPv4 connection.

UDP is different: ipv6_portaddr_hash() puts both families in the same
slot on purpose, which is what made this bug trivial to trigger.

I would merge this patch as is, thanks.

^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH net] udp: do not use a cross-family sk_rx_dst in early demux
  2026-11-06 11:01 [PATCH net] udp: do not use a cross-family sk_rx_dst in early demux Eric Dumazet
  2026-11-07 19:52 ` Kuniyuki Iwashima
  2026-11-09 23:02 ` netdev-bot+sashiko
@ 2026-11-11  0:50 ` patchwork-bot+netdevbpf
  2 siblings, 0 replies; 5+ messages in thread
From: patchwork-bot+netdevbpf @ 2026-11-11  0:50 UTC (permalink / raw)
  To: Eric Dumazet; +Cc: davem, kuba, pabeni, kuniyu, willemb, horms, netdev

Hello:

This patch was applied to netdev/net.git (main)
by Jakub Kicinski <kuba@kernel.org>:

On Fri,  6 Nov 2026 12:01:35 +0100 you wrote:
> A dual-stack UDP socket connected to a v4-mapped address can receive
> both IPv4 packets and IPv6 packets using v4-mapped addresses:
> ip6_rcv_core() no longer rejects a v4-mapped source since commit
> dcc32f4f183a ("ipv6: weaken the v4mapped source check"), which kept
> that check only in tcp_v6_conn_request(). ipv6_portaddr_hash() hashes
> v4-mapped addresses like IPv4 ones, so both early demux handlers find
> the socket, and both __udp4_lib_rcv() and __udp6_lib_rcv() store their
> input dst in the same sk->sk_rx_dst.
> 
> [...]

Here is the summary with links:
  - [net] udp: do not use a cross-family sk_rx_dst in early demux
    https://git.kernel.org/netdev/net/c/5636e6732056

You are awesome, thank you!
-- 
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html



^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-11-11  0:50 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-06 11:01 [PATCH net] udp: do not use a cross-family sk_rx_dst in early demux Eric Dumazet
2026-11-07 19:52 ` Kuniyuki Iwashima
2026-11-09 23:02 ` netdev-bot+sashiko
2026-11-10  7:13   ` Eric Dumazet
2026-11-11  0:50 ` patchwork-bot+netdevbpf

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox