[PATCH net v2 1/2] tcp: Fix the SO_REUSEPORT bind cache admitting a conflicting socket

From: Juan Agustin Niccolini

Date: Wed Nov 11 2026 - 13:31:06 EST


When the port is in the FASTREUSEPORT_STRICT state, inet_csk_get_port()
skips the bind conflict check if sk_reuseport_match() finds the new
socket's address covered by the socket cached in tb->fast_*.

This is only safe if both sockets match on everything
inet_bind_conflict() reads. The cache misses some of it:

- It does not record sk_bound_dev_if or sk_reuse.
- It records the family and IPV6_V6ONLY but never compares them with
the new socket, so a cached v6only [::] (or 0.0.0.0) socket covers
a dual-stack [::] socket.

As a result, an unprivileged user can bind and listen on a port that
another user is listening on, for example a dual-stack [::] socket on
top of a 0.0.0.0 listener:

victim: bind(0.0.0.0:P), listen()
attacker: SO_REUSEPORT + IPV6_V6ONLY, bind([::]:P) /* fills cache */
attacker: SO_REUSEPORT, bind([::]:P), listen() /* should fail */

Only take the fast path when the new socket matches the cached one in
family, IPV6_V6ONLY, bound device and SO_REUSEADDR. Identical
SO_REUSEPORT listeners, the workload this cache was added for, still
skip the scan (binding 16384 of them: 9.80 ms before, 9.86 ms after,
5874 ms with the full check every time).

Fixes: 637bc8bbe6c0 ("inet: reset tb->fastreuseport when adding a reuseport sk")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Juan Agustin Niccolini <juanagusnicco@xxxxxxxxx>
---
include/net/inet_hashtables.h | 2 ++
net/ipv4/inet_connection_sock.c | 15 ++++++++++++---
2 files changed, 14 insertions(+), 3 deletions(-)

diff --git a/include/net/inet_hashtables.h b/include/net/inet_hashtables.h
index 6e2fe186d0dc..44b7e39a1245 100644
--- a/include/net/inet_hashtables.h
+++ b/include/net/inet_hashtables.h
@@ -87,6 +87,8 @@ struct inet_bind_bucket {
__be32 fast_rcv_saddr;
unsigned short fast_sk_family;
bool fast_ipv6_only;
+ unsigned char fast_sk_reuse;
+ int fast_bound_dev_if;
struct hlist_node node;
struct hlist_head bhash2;
struct rcu_head rcu;
diff --git a/net/ipv4/inet_connection_sock.c b/net/ipv4/inet_connection_sock.c
index e56beec43877..70b8662b3004 100644
--- a/net/ipv4/inet_connection_sock.c
+++ b/net/ipv4/inet_connection_sock.c
@@ -420,13 +420,18 @@ static inline int sk_reuseport_match(struct inet_bind_bucket *tb,
return 0;
if (!uid_eq(tb->fastuid, sk_uid(sk)))
return 0;
- /* We only need to check the rcv_saddr if this tb was once marked
+ /* We only need to check the fast_* fields if this tb was once marked
* without fastreuseport and then was reset, as we can only know that
- * the fast_*rcv_saddr doesn't have any conflicts with the socks on the
- * owners list.
+ * the socket they describe doesn't have any conflicts with the socks
+ * on the owners list.
*/
if (tb->fastreuseport == FASTREUSEPORT_ANY)
return 1;
+ if (tb->fast_sk_family != sk->sk_family ||
+ tb->fast_ipv6_only != ipv6_only_sock(sk) ||
+ tb->fast_bound_dev_if != sk->sk_bound_dev_if ||
+ tb->fast_sk_reuse != sk->sk_reuse)
+ return 0;
#if IS_ENABLED(CONFIG_IPV6)
if (tb->fast_sk_family == AF_INET6)
return ipv6_rcv_saddr_equal(&tb->fast_v6_rcv_saddr,
@@ -455,6 +460,8 @@ void inet_csk_update_fastreuse(const struct sock *sk,
tb->fast_rcv_saddr = sk->sk_rcv_saddr;
tb->fast_ipv6_only = ipv6_only_sock(sk);
tb->fast_sk_family = sk->sk_family;
+ tb->fast_sk_reuse = sk->sk_reuse;
+ tb->fast_bound_dev_if = sk->sk_bound_dev_if;
#if IS_ENABLED(CONFIG_IPV6)
tb->fast_v6_rcv_saddr = sk->sk_v6_rcv_saddr;
#endif
@@ -482,6 +489,8 @@ void inet_csk_update_fastreuse(const struct sock *sk,
tb->fast_rcv_saddr = sk->sk_rcv_saddr;
tb->fast_ipv6_only = ipv6_only_sock(sk);
tb->fast_sk_family = sk->sk_family;
+ tb->fast_sk_reuse = sk->sk_reuse;
+ tb->fast_bound_dev_if = sk->sk_bound_dev_if;
#if IS_ENABLED(CONFIG_IPV6)
tb->fast_v6_rcv_saddr = sk->sk_v6_rcv_saddr;
#endif
--
2.43.0