Re: [PATCH net v3] tcp: prevent cross-family skb processing after disconnect

From: Eric Dumazet

Date: Tue Nov 03 2026 - 06:15:55 EST


Le mar. 3 nov. 2026 à 11:40, Zhiyu Zhang <zhiyuzhang999@xxxxxxxxx> a écrit :
>
> From: Eric Dumazet <edumazet@xxxxxxxxxx>
>
> A successful connect() of an IPv6 socket to a v4-mapped address switches
> icsk_af_ops, sk_backlog_rcv and af_specific to their IPv4-mapped
> variants. A later connect(AF_UNSPEC) calls tcp_disconnect(), which does
> not restore them.
>
> The socket can then be reused for a native IPv6 connection while
> tcp_v4_do_rcv() remains installed. IPv6 packets drained from its backlog
> are consequently passed to the IPv4 receive path.
>
> There is also a race in the opposite direction. tcp_v6_rcv() can look up
> the socket and initialize TCP_SKB_CB(skb)->header.h6 before taking the
> socket lock. If the socket is disconnected and reconnected to a
> v4-mapped peer in that window, release_sock() can drain the queued IPv6
> skb through the newly installed tcp_v4_do_rcv().
>
> tcp_v4_send_reset() and tcp_v4_send_ack() then interpret header.h6 as
> IPv4 options. __ip_options_echo() can consequently overflow its 40-byte
> destination buffer.
>
> Commit 8cc3aef0cb19 ("tcp: Do not allow buggy transitions between ehash
> and lhash2.") prevents listen() after connect(AF_UNSPEC), but a native
> IPv6 connect is still allowed.
>
> Add tcp_v6_disconnect() to restore the IPv6 handlers after disconnecting
> a v4-mapped socket, and use the same helper in the connect error path.
> Reject non-IPv4 skbs in tcp_v4_do_rcv() before accessing IPv4
> control-block data. In the reverse race, the skb belongs to the
> disconnected connection, so drop it instead of redirecting it to the
> IPv6 receive path.
>
> MPTCP subflows override sk_prot, while two teardown paths call
> tcp_disconnect() directly. Route them through the protocol-specific
> disconnect operation so IPv6 MPTCP subflows receive the same
> restoration.
>
> Tested with TCP and MPTCP by reconnecting an AF_INET6 socket in both
> directions after connect(AF_UNSPEC). A socket filter was used to widen
> the native-to-mapped race window and verify that the stale IPv6 skb did
> not reach tcp_v4_send_reset().
>
> Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
> Reported-by: Tong Liu <lyutoon@xxxxxxxxx>
> Reported-by: Qi Tang <tpluszz77@xxxxxxxxx>
> Closes: https://lore.kernel.org/netdev/CALf2hKvp0NPVLwr=a3+jUHtPohxbra8gox+aYAfWathJnn-J+Q@xxxxxxxxxxxxxx/
> Link: https://lore.kernel.org/netdev/CAL4WiiqFJpmcV4=TWRpukCq6q=-729658FwGvieNcPXe97kLvQ@xxxxxxxxxxxxxx/
> Signed-off-by: Eric Dumazet <edumazet@xxxxxxxxxx>
> Co-developed-by: Zhiyu Zhang <zhiyuzhang999@xxxxxxxxx>
> Signed-off-by: Zhiyu Zhang <zhiyuzhang999@xxxxxxxxx>
> ---
> Changes in v3:
> - Reject non-IPv4 skbs in tcp_v4_do_rcv() to cover packets queued
> across a native IPv6 to v4-mapped reconnect.
> - Drop Kuniyuki's Reviewed-by tag due to the code change.
>
> v2: https://lore.kernel.org/netdev/20261101182736.430733-1-zhiyuzhang999@xxxxxxxxx/
> v1: https://lore.kernel.org/netdev/20261030032314.1978575-1-zhiyuzhang999@xxxxxxxxx/
>
> net/ipv4/tcp_ipv4.c | 5 +++++
> net/ipv6/tcp_ipv6.c | 37 ++++++++++++++++++++++++++++---------
> net/mptcp/protocol.c | 4 ++--
> 3 files changed, 35 insertions(+), 11 deletions(-)
>
> diff --git a/net/ipv4/tcp_ipv4.c b/net/ipv4/tcp_ipv4.c
> index a7f9242f1e42..8fa6564d2b60 100644
> --- a/net/ipv4/tcp_ipv4.c
> +++ b/net/ipv4/tcp_ipv4.c
> @@ -1836,6 +1836,11 @@ int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
> {
> enum skb_drop_reason reason;
>
> + if (unlikely(skb->protocol != htons(ETH_P_IP))) {
> + reason = SKB_DROP_REASON_INVALID_PROTO;
> + goto err_discard;
> + }
> +

We do not want to add yet another check in TCP IPv4 fast path,
for a problem that only exists for IPv6 sockets.

tcp_v6_do_rcv() already dispatches IPv4 skbs to tcp_v4_do_rcv().

Simply stop changing sk->sk_backlog_rcv for v4-mapped sockets
(tcp_v6_connect() and tcp_v6_mapped_child_init()),
so that skbs found in the backlog are always handled by the correct path.

tcp_v6_disconnect() can then test
inet_csk(sk)->icsk_af_ops->net_header_len == sizeof(struct iphdr)
(this also covers subflow_v6m_specific).

Thanks.

Untested patch:

diff --git a/net/ipv6/tcp_ipv6.c b/net/ipv6/tcp_ipv6.c
index e006a30360eae1b43a4d53173b46b931ae668825..74358bed6ba210074f0534dff61247de34a56727
100644
--- a/net/ipv6/tcp_ipv6.c
+++ b/net/ipv6/tcp_ipv6.c
@@ -130,6 +130,18 @@ static int tcp_v6_pre_connect(struct sock *sk,
struct sockaddr_unsized *uaddr,
return BPF_CGROUP_RUN_PROG_INET6_CONNECT(sk, uaddr, &addr_len);
}

+/* Undo the v4-mapped setup done by tcp_v6_connect(). */
+static void tcp_v6_restore_af_ops(struct sock *sk)
+{
+ /* Paired with READ_ONCE() in tcp_(get|set)sockopt() */
+ WRITE_ONCE(inet_csk(sk)->icsk_af_ops, &ipv6_specific);
+ if (sk_is_mptcp(sk))
+ mptcpv6_handle_mapped(sk, false);
+#if defined(CONFIG_TCP_MD5SIG) || defined(CONFIG_TCP_AO)
+ tcp_sk(sk)->af_specific = &tcp_sock_ipv6_specific;
+#endif
+}
+
static int tcp_v6_connect(struct sock *sk, struct sockaddr_unsized *uaddr,
int addr_len)
{
@@ -234,7 +246,6 @@ static int tcp_v6_connect(struct sock *sk, struct
sockaddr_unsized *uaddr,
WRITE_ONCE(icsk->icsk_af_ops, &ipv6_mapped);
if (sk_is_mptcp(sk))
mptcpv6_handle_mapped(sk, true);
- sk->sk_backlog_rcv = tcp_v4_do_rcv;
#if defined(CONFIG_TCP_MD5SIG) || defined(CONFIG_TCP_AO)
tp->af_specific = &tcp_sock_ipv6_mapped_specific;
#endif
@@ -243,14 +254,7 @@ static int tcp_v6_connect(struct sock *sk, struct
sockaddr_unsized *uaddr,

if (err) {
icsk->icsk_ext_hdr_len = exthdrlen;
- /* Paired with READ_ONCE() in tcp_(get|set)sockopt() */
- WRITE_ONCE(icsk->icsk_af_ops, &ipv6_specific);
- if (sk_is_mptcp(sk))
- mptcpv6_handle_mapped(sk, false);
- sk->sk_backlog_rcv = tcp_v6_do_rcv;
-#if defined(CONFIG_TCP_MD5SIG) || defined(CONFIG_TCP_AO)
- tp->af_specific = &tcp_sock_ipv6_specific;
-#endif
+ tcp_v6_restore_af_ops(sk);
goto failure;
}
np->saddr = sk->sk_v6_rcv_saddr;
@@ -1363,7 +1367,6 @@ static void tcp_v6_mapped_child_init(struct sock
*newsk, const struct sock *sk)
inet_csk(newsk)->icsk_af_ops = &ipv6_mapped;
if (sk_is_mptcp(newsk))
mptcpv6_handle_mapped(newsk, true);
- newsk->sk_backlog_rcv = tcp_v4_do_rcv;
#if defined(CONFIG_TCP_MD5SIG) || defined(CONFIG_TCP_AO)
tcp_sk(newsk)->af_specific = &tcp_sock_ipv6_mapped_specific;
#endif
@@ -2093,6 +2096,21 @@ static int tcp_v6_init_sock(struct sock *sk)
return 0;
}

+static int tcp_v6_disconnect(struct sock *sk, int flags)
+{
+ int ret = tcp_disconnect(sk, flags);
+
+ /* A socket used for a v4-mapped connection must not keep IPv4
+ * handlers when it is reused for listen() or a native IPv6 connect().
+ * Note that ipv6_mapped and subflow_v6m_specific both use an IPv4
+ * network header.
+ */
+ if (inet_csk(sk)->icsk_af_ops->net_header_len == sizeof(struct iphdr))
+ tcp_v6_restore_af_ops(sk);
+
+ return ret;
+}
+
#ifdef CONFIG_PROC_FS
/* Proc filesystem TCPv6 sock list dumping. */
static void get_openreq6(struct seq_file *seq,
@@ -2283,7 +2301,7 @@ struct proto tcpv6_prot = {
.close = tcp_close,
.pre_connect = tcp_v6_pre_connect,
.connect = tcp_v6_connect,
- .disconnect = tcp_disconnect,
+ .disconnect = tcp_v6_disconnect,
.accept = inet_csk_accept,
.ioctl = tcp_ioctl,
.init = tcp_v6_init_sock,
diff --git a/net/mptcp/protocol.c b/net/mptcp/protocol.c
index 7a22e9388e0e9a8b1bd6fe27d04245a1db6950b1..fdfbc7de2ec03612bc49d0a337a80fe1089a7d81
100644
--- a/net/mptcp/protocol.c
+++ b/net/mptcp/protocol.c
@@ -2608,7 +2608,7 @@ static void __mptcp_subflow_disconnect(struct sock *ssk,
/* The MPTCP code never wait on the subflow sockets, TCP-level
* disconnect should never fail
*/
- WARN_ON_ONCE(tcp_disconnect(ssk, 0));
+ WARN_ON_ONCE(ssk->sk_prot->disconnect(ssk, 0));
mptcp_subflow_ctx_reset(subflow);
} else {
tcp_shutdown(ssk, SEND_SHUTDOWN);
@@ -3287,7 +3287,7 @@ void mptcp_subflow_shutdown(struct sock *sk,
struct sock *ssk, int how)
break;
fallthrough;
case TCP_SYN_SENT:
- WARN_ON_ONCE(tcp_disconnect(ssk, O_NONBLOCK));
+ WARN_ON_ONCE(ssk->sk_prot->disconnect(ssk, O_NONBLOCK));
break;
default:
if (__mptcp_check_fallback(mptcp_sk(sk))) {