From: David Lee <david.lee@trailofbits.com>
To: edumazet@google.com, ncardwell@google.com, davem@davemloft.net,
kuba@kernel.org, pabeni@redhat.com
Cc: David Lee <david.lee@trailofbits.com>,
Kyle Zeng <kylebot@openai.com>,
Dominik 'Disconnect3d' Czarnota
<dominik.czarnota@trailofbits.com>,
kuniyu@google.com, horms@kernel.org, netdev@vger.kernel.org,
linux-kernel@vger.kernel.org
Subject: [PATCH] net: inet: prevent lhash2 lookups from escaping into ehash
Date: Fri, 31 Jul 2026 14:05:12 +0000 [thread overview]
Message-ID: <20260731140512.566464-1-david.lee@trailofbits.com> (raw)
The lhash2 and ehash tables share sk_nulls_node. When a listening
socket is unhashed and rehashed as a connected socket while an RCU
listener lookup is walking it, the reader can follow the node's new
next pointer into ehash. inet_lhash2_lookup() and
inet6_lhash2_lookup() do not validate the terminal nulls marker, so
they can return an ehash entry under the listener lookup's
unreferenced ownership contract.
In particular, returning a TIME_WAIT socket makes the TCP receive
path consume a reference that the lookup never acquired. Repeated
races can free the object while it remains linked in the hash tables.
Skip non-listening sockets before scoring them. Also restart the walk
when its terminal nulls marker does not match the expected lhash2 slot,
as the established lookup already does.
Fixes: cae3873c5b3a ("net: inet: Retire port only listening_hash")
Bug found and triaged by OpenAI Security Research and
validated by Trail of Bits.
Assisted-by: Codex:gpt-5.6-sol gpt-5.5-cyber
Signed-off-by: Kyle Zeng <kylebot@openai.com>
---
Trail of Bits has a reproducer for this bug that triggers a
KASAN use-after-free and can share if needed.
net/ipv4/inet_hashtables.c | 20 ++++++++++++++++----
net/ipv6/inet6_hashtables.c | 20 ++++++++++++++++----
2 files changed, 32 insertions(+), 8 deletions(-)
diff --git a/net/ipv4/inet_hashtables.c b/net/ipv4/inet_hashtables.c
index ba0faa9ae..563c8af44 100644
--- a/net/ipv4/inet_hashtables.c
+++ b/net/ipv4/inet_hashtables.c
@@ -415,16 +415,23 @@ EXPORT_SYMBOL_GPL(inet_lookup_reuseport);
/* called with rcu_read_lock() : No refcount taken on the socket */
static struct sock *inet_lhash2_lookup(const struct net *net,
struct inet_listen_hashbucket *ilb2,
+ unsigned int slot,
struct sk_buff *skb, int doff,
const __be32 saddr, __be16 sport,
const __be32 daddr, const unsigned short hnum,
const int dif, const int sdif)
{
- struct sock *sk, *result = NULL;
+ struct sock *sk, *result;
struct hlist_nulls_node *node;
- int score, hiscore = 0;
+ int score, hiscore;
+begin:
+ result = NULL;
+ hiscore = 0;
sk_nulls_for_each_rcu(sk, node, &ilb2->nulls_head) {
+ if (READ_ONCE(sk->sk_state) != TCP_LISTEN)
+ continue;
+
score = compute_score(sk, net, hnum, daddr, dif, sdif);
if (score > hiscore) {
result = inet_lookup_reuseport(net, sk, skb, doff,
@@ -437,6 +444,9 @@ static struct sock *inet_lhash2_lookup(const struct net *net,
}
}
+ if (get_nulls_value(node) != slot + LISTENING_NULLS_BASE)
+ goto begin;
+
return result;
}
@@ -486,7 +496,8 @@ struct sock *__inet_lookup_listener(const struct net *net,
hash2 = ipv4_portaddr_hash(net, daddr, hnum);
ilb2 = inet_lhash2_bucket(hashinfo, hash2);
- result = inet_lhash2_lookup(net, ilb2, skb, doff,
+ result = inet_lhash2_lookup(net, ilb2,
+ hash2 & hashinfo->lhash2_mask, skb, doff,
saddr, sport, daddr, hnum,
dif, sdif);
if (result)
@@ -496,7 +507,8 @@ struct sock *__inet_lookup_listener(const struct net *net,
hash2 = ipv4_portaddr_hash(net, htonl(INADDR_ANY), hnum);
ilb2 = inet_lhash2_bucket(hashinfo, hash2);
- result = inet_lhash2_lookup(net, ilb2, skb, doff,
+ result = inet_lhash2_lookup(net, ilb2,
+ hash2 & hashinfo->lhash2_mask, skb, doff,
saddr, sport, htonl(INADDR_ANY), hnum,
dif, sdif);
done:
diff --git a/net/ipv6/inet6_hashtables.c b/net/ipv6/inet6_hashtables.c
index b111b51d6..a9dfd47dd 100644
--- a/net/ipv6/inet6_hashtables.c
+++ b/net/ipv6/inet6_hashtables.c
@@ -184,16 +184,23 @@ EXPORT_SYMBOL_GPL(inet6_lookup_reuseport);
/* called with rcu_read_lock() */
static struct sock *inet6_lhash2_lookup(const struct net *net,
struct inet_listen_hashbucket *ilb2,
+ unsigned int slot,
struct sk_buff *skb, int doff,
const struct in6_addr *saddr,
const __be16 sport, const struct in6_addr *daddr,
const unsigned short hnum, const int dif, const int sdif)
{
- struct sock *sk, *result = NULL;
+ struct sock *sk, *result;
struct hlist_nulls_node *node;
- int score, hiscore = 0;
+ int score, hiscore;
+begin:
+ result = NULL;
+ hiscore = 0;
sk_nulls_for_each_rcu(sk, node, &ilb2->nulls_head) {
+ if (READ_ONCE(sk->sk_state) != TCP_LISTEN)
+ continue;
+
score = compute_score(sk, net, hnum, daddr, dif, sdif);
if (score > hiscore) {
result = inet6_lookup_reuseport(net, sk, skb, doff,
@@ -206,6 +213,9 @@ static struct sock *inet6_lhash2_lookup(const struct net *net,
}
}
+ if (get_nulls_value(node) != slot + LISTENING_NULLS_BASE)
+ goto begin;
+
return result;
}
@@ -260,7 +270,8 @@ struct sock *inet6_lookup_listener(const struct net *net,
hash2 = ipv6_portaddr_hash(net, daddr, hnum);
ilb2 = inet_lhash2_bucket(hashinfo, hash2);
- result = inet6_lhash2_lookup(net, ilb2, skb, doff,
+ result = inet6_lhash2_lookup(net, ilb2,
+ hash2 & hashinfo->lhash2_mask, skb, doff,
saddr, sport, daddr, hnum,
dif, sdif);
if (result)
@@ -270,7 +281,8 @@ struct sock *inet6_lookup_listener(const struct net *net,
hash2 = ipv6_portaddr_hash(net, &in6addr_any, hnum);
ilb2 = inet_lhash2_bucket(hashinfo, hash2);
- result = inet6_lhash2_lookup(net, ilb2, skb, doff,
+ result = inet6_lhash2_lookup(net, ilb2,
+ hash2 & hashinfo->lhash2_mask, skb, doff,
saddr, sport, &in6addr_any, hnum,
dif, sdif);
done:
next reply other threads:[~2026-07-31 14:05 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-31 14:05 David Lee [this message]
2026-08-02 8:40 ` [PATCH] net: inet: prevent lhash2 lookups from escaping into ehash Sven Eckelmann
2026-08-04 1:53 ` Kuniyuki Iwashima
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260731140512.566464-1-david.lee@trailofbits.com \
--to=david.lee@trailofbits.com \
--cc=davem@davemloft.net \
--cc=dominik.czarnota@trailofbits.com \
--cc=edumazet@google.com \
--cc=horms@kernel.org \
--cc=kuba@kernel.org \
--cc=kuniyu@google.com \
--cc=kylebot@openai.com \
--cc=linux-kernel@vger.kernel.org \
--cc=ncardwell@google.com \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.