All of lore.kernel.org
 help / color / mirror / Atom feed
From: netdev-bot+sashiko@kernel.org
To: zihanx@nebusec.ai
Cc: netdev@vger.kernel.org, linux-kernel@vger.kernel.org,
	mptcp@lists.linux.dev, davem@davemloft.net, edumazet@google.com,
	kuba@kernel.org, pabeni@redhat.com, horms@kernel.org,
	ncardwell@google.com, kuniyu@google.com, matttbe@kernel.org,
	martineau@kernel.org, geliang@kernel.org, gnault@redhat.com,
	fw@strlen.de, stable@vger.kernel.org, vega@nebusec.ai
Subject: Re: [PATCH net v2 1/2] tcp: diag: bound bucket lock hold in tcp_diag_dump()
Date: Mon, 07 Sep 2026 04:22:34 +0000	[thread overview]
Message-ID: <178875495405.219967.5594821749566554418@kernel.org> (raw)
In-Reply-To: <cdc628415cf10721480a51ce14837f846ec778c2.1788248601.git.zihanx@nebusec.ai>

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

Critical: 2 · High: 1 · Medium: 2 · Low: 1

- [Critical] Use-after-free / wild-pointer traversal: the new bind-hash
  dump cursor revalidation in tcp_diag_dump() cannot detect that the…
- [Critical] Refcount initialization race: tcp_diag_bind_collect_sock()
  calls sock_hold() unconditionally on every node of tb2->owners,…
- [High] Loss of dump forward progress and duplicate output: the patch
  deletes every positional (`s_num`) resume path in tcp_diag_dump()…
- [Medium] Lockless, unordered reads of cursor state: the listen and
  ehash cursor validations establish "this socket is in the bucket I…
- [Medium] The commit message mischaracterizes the pre-existing defect.
- [Low] Concern that the sibling MPTCP listener dump
  (mptcp_diag_dump_listeners()) keeps evaluating request-supplied…

--

tcp: diag: bound bucket lock hold in tcp_diag_dump()

This reworks the three tcp_diag_dump() hash walks so that only referenced
sockets are collected under each bucket lock, and moves the family/port
filters, inet_diag_bc_sk() bytecode evaluation and the fill work outside the
critical section.  A referenced dump cursor is added to
struct inet_diag_dump_data so a following batch resumes after the previously
reported socket, with per-table validation of the cursor.

v1 Link: https://lore.kernel.org/all/cover.1785307984.git.zihanx@nebusec.ai/

>     inet_diag dumps run request-supplied bytecode through inet_diag_bc_sk().
>     tcp_diag_dump() currently evaluates socket filters and fills replies while
>     holding the listener, bind, and ehash bucket locks.

[Severity: Medium]
Is this description accurate for all three tables?

At the baseline (544d85de4dc2) only the listener walk fills replies under
ilb->lock.  The bind walk already collected at most SKARR_SZ = 16 referenced
sockets under spin_lock_bh(&ibb->lock) and only called inet_sk_diag_fill()
after the lock was dropped:

	pause_bind_walk:
			spin_unlock_bh(&ibb->lock);

			res = 0;
			for (idx = 0; idx < accum; idx++) {
				if (res >= 0) {
					res = inet_sk_diag_fill(sk_arr[idx],
								NULL, skb, cb,
								r, NLM_F_MULTI,
								net_admin);

The ehash walk did the same with sk_diag_fill() after spin_unlock_bh(lock).
What remained under the bind and ehash locks was inet_diag_bc_sk() plus the
cheap family/port comparisons, bounded to 16 sockets per lock hold.

Could the changelog be narrowed to the listener walk, or state explicitly
what part of the bind/ehash work it is moving out?

> diff --git a/include/linux/inet_diag.h b/include/linux/inet_diag.h
> index 704fd415c2b49..6ccd32bc48f91 100644
> --- a/include/linux/inet_diag.h
> +++ b/include/linux/inet_diag.h

[ ... ]

> @@ -32,12 +33,23 @@ struct inet_diag_handler {
>  };
>  
>  struct bpf_sk_storage_diag;
> +
> +enum inet_diag_dump_cursor_type {
> +	INET_DIAG_DUMP_CURSOR_NONE,
> +	INET_DIAG_DUMP_CURSOR_TCP_LISTEN,
> +	INET_DIAG_DUMP_CURSOR_TCP_BIND,
> +	INET_DIAG_DUMP_CURSOR_TCP_EHASH,
> +};
> +

[Severity: Low]
This isn't a bug, but a note for backporters.  The sibling MPTCP listener
dump mptcp_diag_dump_listeners() is converted by the follow-up patch in this
series (INET_DIAG_DUMP_CURSOR_MPTCP_LISTEN, which is why the enum slot and
the exported inet_diag_dump_clear_cursor() appear here), so at the tip of the
series the MPTCP listener walk also drops ilb->lock before calling
sk_diag_dump()/inet_diag_bc_sk().

This patch is Cc: stable, and the commit message body does not mention that
the MPTCP listener walk keeps running request-supplied bytecode under the
same hashinfo->lhash2 bucket lock until the follow-up patch is applied.  Only
the v2 changelog, which is stripped on apply, mentions "Left
INET_DIAG_DUMP_CURSOR_MPTCP_LISTEN to the MPTCP patch".  Could that
dependency be stated in the commit message?

[ ... ]

> diff --git a/include/net/inet_hashtables.h b/include/net/inet_hashtables.h
> index 6e2fe186d0dcb..d95639ac70c67 100644
> --- a/include/net/inet_hashtables.h
> +++ b/include/net/inet_hashtables.h
> @@ -188,6 +188,24 @@ inet_lhash2_bucket(struct inet_hashinfo *h, u32 hash)
>  	return &h->lhash2[hash & h->lhash2_mask];
>  }
>  
> +static inline struct inet_listen_hashbucket *
> +inet_lhash2_bucket_sk(struct inet_hashinfo *h, struct sock *sk)
> +{
> +	u32 hash;
> +
> +#if IS_ENABLED(CONFIG_IPV6)
> +	if (sk->sk_family == AF_INET6)
> +		hash = ipv6_portaddr_hash(sock_net(sk),
> +					  &sk->sk_v6_rcv_saddr,
> +					  inet_sk(sk)->inet_num);
> +	else
> +#endif
> +		hash = ipv4_portaddr_hash(sock_net(sk),
> +					  inet_sk(sk)->inet_rcv_saddr,
> +					  inet_sk(sk)->inet_num);
> +	return inet_lhash2_bucket(h, hash);
> +}
> +

[ ... ]

> diff --git a/net/ipv4/tcp_diag.c b/net/ipv4/tcp_diag.c
> index ba1fdbe9807fd..842e13ee79e5b 100644
> --- a/net/ipv4/tcp_diag.c
> +++ b/net/ipv4/tcp_diag.c
> @@ -285,6 +285,73 @@ static int sk_diag_fill(struct sock *sk, struct sk_buff *skb,
>  				 net_admin);
>  }
>  

[ ... ]

> +static struct inet_bind2_bucket *tcp_diag_sk_bind2(const struct sock *sk)
> +{
> +	if (sk->sk_state == TCP_TIME_WAIT)
> +		return inet_twsk(sk)->tw_tb2;
> +
> +	return inet_csk(sk)->icsk_bind2_hash;
> +}
> +
> +static bool tcp_diag_bind_collect_sock(struct sock *sk, struct sock **sk_arr,
> +				       int *num_arr, int *accum, int num)
> +{
> +	sock_hold(sk);
> +	num_arr[*accum] = num;
> +	sk_arr[*accum] = sk;
> +
> +	return ++*accum == SKARR_SZ;
> +}
> +
> +static bool tcp_diag_bind_collect_owners(struct hlist_head *owners,
> +					 struct sock **sk_arr, int *num_arr,
> +					 int *accum, int *num, int s_num)
> +{
> +	struct sock *sk;
> +
> +	sk_for_each_bound(sk, owners) {
> +		if (*num < s_num) {
> +			(*num)++;
> +			continue;
> +		}
> +
> +		if (tcp_diag_bind_collect_sock(sk, sk_arr, num_arr, accum, *num))
> +			return true;
> +		(*num)++;
> +	}
> +
> +	return false;
> +}

[Severity: Critical]
Can the unconditional sock_hold() in tcp_diag_bind_collect_sock() hit a
TIME_WAIT socket whose tw_refcnt is still 0?

The changelog says "Bind collection counts TIME_WAIT nodes toward the batch
limit", and tcp_diag_bind_collect_owners() no longer filters on
sk->sk_state != TCP_CLOSE before taking the reference, so every node of
tb2->owners now gets a refcount_inc().

inet_twsk_alloc() starts the object at zero on purpose:

	/*
	 * Because we use RCU lookups, we should not set tw_refcnt
	 * to a non null value before everything is setup for this
	 * timewait socket.
	 */
	refcount_set(&tw->tw_refcnt, 0);

and inet_twsk_hashdance_schedule() publishes it into the bind owners list and
drops both bind locks before initializing the count:

	sk_add_bind_node((struct sock *)tw, &tw->tw_tb2->owners);

	spin_unlock(&bhead2->lock);
	spin_unlock(&bhead->lock);

	spin_lock(lock);

	refcount_set(&tw->tw_refcnt, 3);

A dumper blocked on bhead2->lock acquires it right after that unlock and
immediately walks tb2->owners, so it can observe the published tw with
tw_refcnt == 0.  sock_hold() then calls refcount_inc() on zero, which trips
refcount_warn_saturate() and leaves the counter saturated, after which
refcount_set(&tw->tw_refcnt, 3) discards the dump's reference.  The later
sock_gen_put() from the fill loop then consumes one of the three structural
references (bhash, ehash, timer).

Would using refcount_inc_not_zero() here, or keeping the TCP_CLOSE check
under the lock, be safer?

> @@ -335,8 +402,15 @@ static void tcp_diag_dump(struct sk_buff *skb, struct netlink_callback *cb,
>  		for (i = s_i; i <= hashinfo->lhash2_mask; i++) {
>  			struct inet_listen_hashbucket *ilb;
>  			struct hlist_nulls_node *node;
> +			struct sock *sk_arr[SKARR_SZ];
> +			int num_arr[SKARR_SZ];
> +			struct sock *cursor;
> +			int idx, accum, res;
> +			bool use_cursor;
>  
> +resume_listen_walk:
>  			num = 0;
> +			accum = 0;
>  			ilb = &hashinfo->lhash2[i];
>  
>  			if (hlist_nulls_empty(&ilb->nulls_head)) {
> @@ -344,52 +418,81 @@ static void tcp_diag_dump(struct sk_buff *skb, struct netlink_callback *cb,
>  				continue;
>  			}
>  			spin_lock(&ilb->lock);
> -			sk_nulls_for_each(sk, node, &ilb->nulls_head) {
> -				struct inet_sock *inet = inet_sk(sk);
> +			cursor = cb_data->dump_cursor;
> +			use_cursor = cursor &&
> +				     cb_data->dump_cursor_type ==
> +				     INET_DIAG_DUMP_CURSOR_TCP_LISTEN &&
> +				     cb_data->dump_cursor_slot == i &&
> +				     inet_sk_state_load(cursor) == TCP_LISTEN &&
> +				     !hlist_nulls_unhashed(&cursor->sk_nulls_node) &&
> +				     cursor->sk_nulls_node.pprev != LIST_POISON2 &&
> +				     inet_lhash2_bucket_sk(hashinfo, cursor) == ilb;
> +			node = use_cursor ? cursor->sk_nulls_node.next :
> +					    ilb->nulls_head.first;

[Severity: Medium]
Is the "cursor is still in this bucket" conclusion sound when every input to
it is read without the lock that publishes it?

In the listener path the recomputation uses sock_net(cursor),
sk_v6_rcv_saddr (a 16-byte non-atomic read) and inet_sk(cursor)->inet_num via
the newly exported inet_lhash2_bucket_sk(), none of which is protected by
ilb->lock, and the pprev test and the bucket computation are separate plain
loads with no barrier between them.

The ehash variant later in this function has the same shape with
cursor->sk_nulls_node.pprev followed by cursor->sk_hash.  The insertion side
sets sk->sk_hash before taking the target bucket lock and linking with
__sk_nulls_add_node_rcu(), and inet_csk_listen_start() stores TCP_LISTEN
before hashing into lhash2, so a reader that observes the new linkage is not
guaranteed to observe the matching sk_hash/sk_state store.  A socket that
left ehash bucket i and called listen() keeps its old sk_hash, so
inet_sk_state_load(cursor) != TCP_LISTEN can still be true while the lhash2
linkage is already visible.

If validation passes on a stale snapshot, the walk then follows
cursor->sk_nulls_node.next into a chain whose spinlock is not held and
sock_hold()s the entries it finds.  On x86 the store order makes the
inconsistent snapshot hard to observe, but is bucket membership something
that can be established from lockless per-socket fields at all?

> +			if (!use_cursor)
> +				s_num = 0;
> +			hlist_nulls_for_each_entry_from(sk, node, sk_nulls_node) {
>  
> -				if (!net_eq(sock_net(sk), net))
> -					continue;
> +				sock_hold(sk);
> +				num_arr[accum] = num;
> +				sk_arr[accum] = sk;
> +				if (++accum == SKARR_SZ)
> +					break;
>  
> -				if (num < s_num) {
> -					num++;
> -					continue;
> -				}
> +				++num;
> +			}
> +			spin_unlock(&ilb->lock);

[Severity: High]
With the num < s_num skip removed from this loop and s_num forced to 0 when
the cursor is rejected, what stops the bucket from being re-dumped from its
head?

All three walks now do:

	if (!use_cursor)
		s_num = 0;

and the batch continuations replace s_num = num + 1 with s_num = 0:

	if (accum == SKARR_SZ) {
		s_num = 0;
		goto resume_listen_walk;
	}

The bind walk and the ehash walk have the same two changes, and the s_num
parameter of tcp_diag_bind_collect_owners() is dead because its only caller
sits in the !use_cursor branch where s_num has just been zeroed.

So whenever cursor validation fails, which the changelog describes as the
"safe restart on mismatch", the walk restarts at ilb->nulls_head.first, at
the head of ibb->chain, or at head->chain.first, and re-emits sockets from
that bucket that were already sent to user space earlier in the same dump.
Before this change s_num = num + 1 guaranteed the restart advanced.

There is a second effect: if all of the re-collected leading SKARR_SZ sockets
are discarded after the lock is dropped (foreign netns, since lhash2 and
ehash are shared across netns, or sdiag_family/port mismatch, or bytecode
rejection), nothing is emitted, res stays 0 and the goto resume_*_walk loop
repeats over the same head with only cond_resched() yielding.

tcp_diag_dump() still loads s_num = num = cb->args[2] on entry and still
stores cb->args[2] = num at the done: label, but num is now a chunk-relative
counter that no resume path consumes.  Should that state be dropped, or
should a positional fallback be kept for the mismatch case?  Also, since the
walk can now repeat objects within a dump, should NLM_F_DUMP_INTR be set via
a generation counter in netlink_callback.seq?

[ ... ]

> @@ -412,34 +517,46 @@ static void tcp_diag_dump(struct sk_buff *skb, struct netlink_callback *cb,
>  				continue;
>  			}
>  			spin_lock_bh(&ibb->lock);
> -			inet_bind_bucket_for_each(tb2, &ibb->chain) {
> -				if (!net_eq(ib2_net(tb2), net))
> -					continue;
> -
> -				sk_for_each_bound(sk, &tb2->owners) {
> -					struct inet_sock *inet = inet_sk(sk);
> -
> -					if (num < s_num)
> -						goto next_bind;
> -
> -					if (sk->sk_state != TCP_CLOSE ||
> -					    !inet->inet_num)
> -						goto next_bind;
> -
> -					if (r->sdiag_family != AF_UNSPEC &&
> -					    r->sdiag_family != sk->sk_family)
> -						goto next_bind;
> -
> -					if (!inet_diag_bc_sk(cb_data, sk))
> -						goto next_bind;
> -
> -					sock_hold(sk);
> -					num_arr[accum] = num;
> -					sk_arr[accum] = sk;
> -					if (++accum == SKARR_SZ)
> +			cursor = cb_data->dump_cursor;
> +			use_cursor = cursor &&
> +				     cb_data->dump_cursor_type ==
> +				     INET_DIAG_DUMP_CURSOR_TCP_BIND &&
> +				     cb_data->dump_cursor_slot == i &&
> +				     !hlist_unhashed(&cursor->sk_bind_node) &&
> +				     cursor->sk_bind_node.pprev != LIST_POISON2;
> +			if (use_cursor) {
> +				tb2 = tcp_diag_sk_bind2(cursor);
> +				use_cursor = tb2 &&
> +					     inet_bhashfn_portaddr(hashinfo, cursor,
> +								   sock_net(cursor),
> +								   inet_sk(cursor)->inet_num) ==
> +					     ibb;
> +			}
> +			if (!use_cursor)
> +				s_num = 0;
> +			if (use_cursor) {
> +				sk = cursor;
> +				if (tcp_diag_bind_collect_owners_continue(sk, sk_arr,
> +									  num_arr,
> +									  &accum,
> +									  &num))
> +					goto pause_bind_walk;
> +				hlist_for_each_entry_continue(tb2, node) {
> +					if (tcp_diag_bind_collect_owners(&tb2->owners,
> +									 sk_arr,
> +									 num_arr,
> +									 &accum,
> +									 &num, 0))
> +						goto pause_bind_walk;
> +				}

[Severity: Critical]
Can these two liveness checks ever detect that the cursor left
tb2->owners, and can tb2 here already be freed?

Removal from a bind chain goes through __sk_del_bind_node():

include/net/sock.h:
static inline void __sk_del_bind_node(struct sock *sk)
{
	__hlist_del(&sk->sk_bind_node);
}

__hlist_del() only writes *pprev and next->pprev; it leaves the removed
node's own pprev and next untouched.  So for a socket that was unlinked,
hlist_unhashed(&cursor->sk_bind_node) is false and
cursor->sk_bind_node.pprev != LIST_POISON2 holds, and both tests pass.

The freed-bucket part comes from __inet_bhash2_update_saddr():

net/ipv4/inet_hashtables.c:
	spin_lock(&head2->lock);
	__sk_del_bind_node(sk);
	inet_bind2_bucket_destroy(hinfo->bind2_bucket_cachep, inet_csk(sk)->icsk_bind2_hash);
	spin_unlock(&head2->lock);

	if (reset)
		inet_reset_saddr(sk);
	else
		inet_update_saddr(sk, saddr, family);

icsk_bind2_hash is never cleared, and inet_bind2_bucket_destroy() frees the
bucket immediately with no grace period when it was the sole owner:

	if (hlist_empty(&tb->owners)) {
		__hlist_del(&tb->node);
		__hlist_del(&tb->bhash_node);
		kmem_cache_free(cachep, tb);
		return;
	}

In the window between that spin_unlock() and the address update, the address
is unchanged, so inet_bhashfn_portaddr() still resolves to ibb, and
tcp_diag_sk_bind2(cursor) returns the dangling icsk_bind2_hash.  use_cursor
therefore becomes true.

tcp_diag_bind_collect_owners_continue() then iterates the cursor's stale
sk_bind_node.next, and hlist_for_each_entry_continue(tb2, node) reads
tb2->node.next out of the freed slab object, producing an arbitrary
struct inet_bind2_bucket * whose owners list is walked with sock_hold()
applied to each apparent entry.

Note the dumper holds only hashinfo->bhash2[i].lock and never the primary
bhash head lock, and a cursor is saved for every collected owner including
the ones filtered out afterwards, so a process that dumps with
TCPF_BOUND_INACTIVE while calling connect() on a bound socket in that bucket
can reach this.  Could the cursor instead record a validated tb2 identity
that is re-looked-up under ibb->lock, rather than being derived from
icsk_bind2_hash?

> +			} else {
> +				inet_bind_bucket_for_each(tb2, &ibb->chain) {
> +					if (tcp_diag_bind_collect_owners(&tb2->owners,
> +									 sk_arr,
> +									 num_arr,
> +									 &accum,
> +									 &num, s_num))
>  						goto pause_bind_walk;
> -next_bind:
> -					num++;
>  				}
>  			}
>  pause_bind_walk:

[ ... ]

> @@ -463,13 +598,15 @@ static void tcp_diag_dump(struct sk_buff *skb, struct netlink_callback *cb,
>  			cond_resched();
>  
>  			if (accum == SKARR_SZ) {
> -				s_num = num + 1;
> +				s_num = 0;
>  				goto resume_bind_walk;
>  			}
>  
> +			inet_diag_dump_clear_cursor(cb_data);
>  			s_num = 0;
>  		}

[ ... ]

>  		if (accum == SKARR_SZ) {
> -			s_num = num + 1;
> +			s_num = 0;
>  			goto next_chunk;
>  		}

[ ... ]

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/cover.1788248601.git.zihanx%40nebusec.ai

  parent reply	other threads:[~2026-09-07  4:22 UTC|newest]

Thread overview: 16+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-01 12:53 [PATCH net v2 0/2] tcp: diag: bound bucket lock hold in diag dump paths Zihan Xi
2026-09-01 12:53 ` [PATCH net v2 1/2] tcp: diag: bound bucket lock hold in tcp_diag_dump() Zihan Xi
2026-09-02 12:54   ` sashiko-bot
2026-09-07  4:22   ` netdev-bot+sashiko [this message]
2026-09-07  4:48     ` zihan xi
2026-09-07  7:05       ` Eric Dumazet
2026-09-07  7:15         ` zihan xi
2026-09-07  7:41           ` zihan xi
2026-09-01 12:53 ` [PATCH net v2 2/2] mptcp: diag: bound listener bucket lock hold Zihan Xi
2026-09-02 12:54   ` sashiko-bot
2026-09-07  4:22   ` netdev-bot+sashiko
2026-09-07  4:49     ` zihan xi
2026-09-07  7:09   ` Eric Dumazet
2026-09-01 13:58 ` [PATCH net v2 0/2] tcp: diag: bound bucket lock hold in diag dump paths MPTCP CI
2026-09-03  2:09 ` Kuniyuki Iwashima
2026-09-03  2:35   ` zihan xi

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=178875495405.219967.5594821749566554418@kernel.org \
    --to=netdev-bot+sashiko@kernel.org \
    --cc=davem@davemloft.net \
    --cc=edumazet@google.com \
    --cc=fw@strlen.de \
    --cc=geliang@kernel.org \
    --cc=gnault@redhat.com \
    --cc=horms@kernel.org \
    --cc=kuba@kernel.org \
    --cc=kuniyu@google.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=martineau@kernel.org \
    --cc=matttbe@kernel.org \
    --cc=mptcp@lists.linux.dev \
    --cc=ncardwell@google.com \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=stable@vger.kernel.org \
    --cc=vega@nebusec.ai \
    --cc=zihanx@nebusec.ai \
    /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.