netdev.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [PATCH v1 bpf-next 00/10] bpf: tcp: Exactly-once socket iteration
@ 2025-05-20 14:50 Jordan Rife
  2025-05-20 14:50 ` [PATCH v1 bpf-next 01/10] bpf: tcp: Make mem flags configurable through bpf_iter_tcp_realloc_batch Jordan Rife
                   ` (9 more replies)
  0 siblings, 10 replies; 25+ messages in thread
From: Jordan Rife @ 2025-05-20 14:50 UTC (permalink / raw)
  To: netdev, bpf
  Cc: Jordan Rife, Daniel Borkmann, Martin KaFai Lau, Willem de Bruijn,
	Kuniyuki Iwashima, Alexei Starovoitov

TCP socket iterators use iter->offset to track progress through a
bucket, which is a measure of the number of matching sockets from the
current bucket that have been seen or processed by the iterator. On
subsequent iterations, if the current bucket has unprocessed items, we
skip at least iter->offset matching items in the bucket before adding
any remaining items to the next batch. However, iter->offset isn't
always an accurate measure of "things already seen" when the underlying
bucket changes between reads which can lead to repeated or skipped
sockets. Instead, this series remembers the cookies of the sockets we
haven't seen yet in the current bucket and resumes from the first cookie
in that list that we can find on the next iteration.

This is a continuation of the work started in [1]. This series largely
replicates the patterns applied to UDP socket iterators, applying them
instead to TCP socket iterators.

Note: This series depends on [1] to apply cleanly, which is currently
      available only in bpf-next/net. As such, CI may not pass if it
      tries to test on top of bpf-next/master, but manual CI executions
      on my branch that included commits from [1] were green.

[1]: https://lore.kernel.org/bpf/20250502161528.264630-1-jordan@jrife.io/

Jordan Rife (10):
  bpf: tcp: Make mem flags configurable through
    bpf_iter_tcp_realloc_batch
  bpf: tcp: Make sure iter->batch always contains a full bucket snapshot
  bpf: tcp: Get rid of st_bucket_done
  bpf: tcp: Use bpf_tcp_iter_batch_item for bpf_tcp_iter_state batch
    items
  bpf: tcp: Avoid socket skips and repeats during iteration
  selftests/bpf: Add tests for bucket resume logic in listening sockets
  selftests/bpf: Allow for iteration over multiple ports
  selftests/bpf: Make ehash buckets configurable in socket iterator
    tests
  selftests/bpf: Create established sockets in socket iterator tests
  selftests/bpf: Add tests for bucket resume logic in established
    sockets

 net/ipv4/tcp_ipv4.c                           | 262 ++++++++---
 .../bpf/prog_tests/sock_iter_batch.c          | 442 +++++++++++++++++-
 .../selftests/bpf/progs/sock_iter_batch.c     |   4 +
 3 files changed, 631 insertions(+), 77 deletions(-)

-- 
2.43.0


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

* [PATCH v1 bpf-next 01/10] bpf: tcp: Make mem flags configurable through bpf_iter_tcp_realloc_batch
  2025-05-20 14:50 [PATCH v1 bpf-next 00/10] bpf: tcp: Exactly-once socket iteration Jordan Rife
@ 2025-05-20 14:50 ` Jordan Rife
  2025-05-21 18:54   ` Kuniyuki Iwashima
  2025-05-20 14:50 ` [PATCH v1 bpf-next 02/10] bpf: tcp: Make sure iter->batch always contains a full bucket snapshot Jordan Rife
                   ` (8 subsequent siblings)
  9 siblings, 1 reply; 25+ messages in thread
From: Jordan Rife @ 2025-05-20 14:50 UTC (permalink / raw)
  To: netdev, bpf
  Cc: Jordan Rife, Daniel Borkmann, Martin KaFai Lau, Willem de Bruijn,
	Kuniyuki Iwashima, Alexei Starovoitov

Prepare for the next patch which needs to be able to choose either
GFP_USER or GFP_NOWAIT for calls to bpf_iter_tcp_realloc_batch.

Signed-off-by: Jordan Rife <jordan@jrife.io>
---
 net/ipv4/tcp_ipv4.c | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/net/ipv4/tcp_ipv4.c b/net/ipv4/tcp_ipv4.c
index d5b5c32115d2..158c025a82ff 100644
--- a/net/ipv4/tcp_ipv4.c
+++ b/net/ipv4/tcp_ipv4.c
@@ -3048,12 +3048,12 @@ static void bpf_iter_tcp_put_batch(struct bpf_tcp_iter_state *iter)
 }
 
 static int bpf_iter_tcp_realloc_batch(struct bpf_tcp_iter_state *iter,
-				      unsigned int new_batch_sz)
+				      unsigned int new_batch_sz, gfp_t flags)
 {
 	struct sock **new_batch;
 
 	new_batch = kvmalloc(sizeof(*new_batch) * new_batch_sz,
-			     GFP_USER | __GFP_NOWARN);
+			     flags | __GFP_NOWARN);
 	if (!new_batch)
 		return -ENOMEM;
 
@@ -3165,7 +3165,8 @@ static struct sock *bpf_iter_tcp_batch(struct seq_file *seq)
 		return sk;
 	}
 
-	if (!resized && !bpf_iter_tcp_realloc_batch(iter, expected * 3 / 2)) {
+	if (!resized && !bpf_iter_tcp_realloc_batch(iter, expected * 3 / 2,
+						    GFP_USER)) {
 		resized = true;
 		goto again;
 	}
@@ -3596,7 +3597,7 @@ static int bpf_iter_init_tcp(void *priv_data, struct bpf_iter_aux_info *aux)
 	if (err)
 		return err;
 
-	err = bpf_iter_tcp_realloc_batch(iter, INIT_BATCH_SZ);
+	err = bpf_iter_tcp_realloc_batch(iter, INIT_BATCH_SZ, GFP_USER);
 	if (err) {
 		bpf_iter_fini_seq_net(priv_data);
 		return err;
-- 
2.43.0


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

* [PATCH v1 bpf-next 02/10] bpf: tcp: Make sure iter->batch always contains a full bucket snapshot
  2025-05-20 14:50 [PATCH v1 bpf-next 00/10] bpf: tcp: Exactly-once socket iteration Jordan Rife
  2025-05-20 14:50 ` [PATCH v1 bpf-next 01/10] bpf: tcp: Make mem flags configurable through bpf_iter_tcp_realloc_batch Jordan Rife
@ 2025-05-20 14:50 ` Jordan Rife
  2025-05-21 22:20   ` Kuniyuki Iwashima
  2025-05-20 14:50 ` [PATCH v1 bpf-next 03/10] bpf: tcp: Get rid of st_bucket_done Jordan Rife
                   ` (7 subsequent siblings)
  9 siblings, 1 reply; 25+ messages in thread
From: Jordan Rife @ 2025-05-20 14:50 UTC (permalink / raw)
  To: netdev, bpf
  Cc: Jordan Rife, Daniel Borkmann, Martin KaFai Lau, Willem de Bruijn,
	Kuniyuki Iwashima, Alexei Starovoitov

Require that iter->batch always contains a full bucket snapshot. This
invariant is important to avoid skipping or repeating sockets during
iteration when combined with the next few patches. Before, there were
two cases where a call to bpf_iter_tcp_batch may only capture part of a
bucket:

1. When bpf_iter_tcp_realloc_batch() returns -ENOMEM.
2. When more sockets are added to the bucket while calling
   bpf_iter_tcp_realloc_batch(), making the updated batch size
   insufficient.

In cases where the batch size only covers part of a bucket, it is
possible to forget which sockets were already visited, especially if we
have to process a bucket in more than two batches. This forces us to
choose between repeating or skipping sockets, so don't allow this:

1. Stop iteration and propagate -ENOMEM up to userspace if reallocation
   fails instead of continuing with a partial batch.
2. Try bpf_iter_tcp_realloc_batch() with GFP_USER just as before, but if
   we still aren't able to capture the full bucket, call
   bpf_iter_tcp_realloc_batch() again while holding the bucket lock to
   guarantee the bucket does not change. On the second attempt use
   GFP_NOWAIT since we hold onto the spin lock.

I did some manual testing to exercise the code paths where GFP_NOWAIT is
used and where ERR_PTR(err) is returned. I used the realloc test cases
included later in this series to trigger a scenario where a realloc
happens inside bpf_iter_tcp_batch and made a small code tweak to force
the first realloc attempt to allocate a too-small batch, thus requiring
another attempt with GFP_NOWAIT. Some printks showed both reallocs with
the tests passing:

May 09 18:18:55 crow kernel: resize batch TCP_SEQ_STATE_LISTENING
May 09 18:18:55 crow kernel: again GFP_USER
May 09 18:18:55 crow kernel: resize batch TCP_SEQ_STATE_LISTENING
May 09 18:18:55 crow kernel: again GFP_NOWAIT
May 09 18:18:57 crow kernel: resize batch TCP_SEQ_STATE_ESTABLISHED
May 09 18:18:57 crow kernel: again GFP_USER
May 09 18:18:57 crow kernel: resize batch TCP_SEQ_STATE_ESTABLISHED
May 09 18:18:57 crow kernel: again GFP_NOWAIT

With this setup, I also forced each of the bpf_iter_tcp_realloc_batch
calls to return -ENOMEM to ensure that iteration ends and that the
read() in userspace fails.

Signed-off-by: Jordan Rife <jordan@jrife.io>
---
 net/ipv4/tcp_ipv4.c | 96 ++++++++++++++++++++++++++++++++-------------
 1 file changed, 68 insertions(+), 28 deletions(-)

diff --git a/net/ipv4/tcp_ipv4.c b/net/ipv4/tcp_ipv4.c
index 158c025a82ff..27022018194a 100644
--- a/net/ipv4/tcp_ipv4.c
+++ b/net/ipv4/tcp_ipv4.c
@@ -3057,7 +3057,10 @@ static int bpf_iter_tcp_realloc_batch(struct bpf_tcp_iter_state *iter,
 	if (!new_batch)
 		return -ENOMEM;
 
-	bpf_iter_tcp_put_batch(iter);
+	if (flags != GFP_NOWAIT)
+		bpf_iter_tcp_put_batch(iter);
+
+	memcpy(new_batch, iter->batch, sizeof(*iter->batch) * iter->end_sk);
 	kvfree(iter->batch);
 	iter->batch = new_batch;
 	iter->max_sk = new_batch_sz;
@@ -3066,69 +3069,85 @@ static int bpf_iter_tcp_realloc_batch(struct bpf_tcp_iter_state *iter,
 }
 
 static unsigned int bpf_iter_tcp_listening_batch(struct seq_file *seq,
-						 struct sock *start_sk)
+						 struct sock **start_sk)
 {
-	struct inet_hashinfo *hinfo = seq_file_net(seq)->ipv4.tcp_death_row.hashinfo;
 	struct bpf_tcp_iter_state *iter = seq->private;
-	struct tcp_iter_state *st = &iter->state;
 	struct hlist_nulls_node *node;
 	unsigned int expected = 1;
 	struct sock *sk;
 
-	sock_hold(start_sk);
-	iter->batch[iter->end_sk++] = start_sk;
+	sock_hold(*start_sk);
+	iter->batch[iter->end_sk++] = *start_sk;
 
-	sk = sk_nulls_next(start_sk);
+	sk = sk_nulls_next(*start_sk);
+	*start_sk = NULL;
 	sk_nulls_for_each_from(sk, node) {
 		if (seq_sk_match(seq, sk)) {
 			if (iter->end_sk < iter->max_sk) {
 				sock_hold(sk);
 				iter->batch[iter->end_sk++] = sk;
+			} else if (!*start_sk) {
+				/* Remember where we left off. */
+				*start_sk = sk;
 			}
 			expected++;
 		}
 	}
-	spin_unlock(&hinfo->lhash2[st->bucket].lock);
 
 	return expected;
 }
 
 static unsigned int bpf_iter_tcp_established_batch(struct seq_file *seq,
-						   struct sock *start_sk)
+						   struct sock **start_sk)
 {
-	struct inet_hashinfo *hinfo = seq_file_net(seq)->ipv4.tcp_death_row.hashinfo;
 	struct bpf_tcp_iter_state *iter = seq->private;
-	struct tcp_iter_state *st = &iter->state;
 	struct hlist_nulls_node *node;
 	unsigned int expected = 1;
 	struct sock *sk;
 
-	sock_hold(start_sk);
-	iter->batch[iter->end_sk++] = start_sk;
+	sock_hold(*start_sk);
+	iter->batch[iter->end_sk++] = *start_sk;
 
-	sk = sk_nulls_next(start_sk);
+	sk = sk_nulls_next(*start_sk);
+	*start_sk = NULL;
 	sk_nulls_for_each_from(sk, node) {
 		if (seq_sk_match(seq, sk)) {
 			if (iter->end_sk < iter->max_sk) {
 				sock_hold(sk);
 				iter->batch[iter->end_sk++] = sk;
+			} else if (!*start_sk) {
+				/* Remember where we left off. */
+				*start_sk = sk;
 			}
 			expected++;
 		}
 	}
-	spin_unlock_bh(inet_ehash_lockp(hinfo, st->bucket));
 
 	return expected;
 }
 
+static void bpf_iter_tcp_unlock_bucket(struct seq_file *seq)
+{
+	struct inet_hashinfo *hinfo = seq_file_net(seq)->ipv4.tcp_death_row.hashinfo;
+	struct bpf_tcp_iter_state *iter = seq->private;
+	struct tcp_iter_state *st = &iter->state;
+
+	if (st->state == TCP_SEQ_STATE_LISTENING)
+		spin_unlock(&hinfo->lhash2[st->bucket].lock);
+	else
+		spin_unlock_bh(inet_ehash_lockp(hinfo, st->bucket));
+}
+
 static struct sock *bpf_iter_tcp_batch(struct seq_file *seq)
 {
 	struct inet_hashinfo *hinfo = seq_file_net(seq)->ipv4.tcp_death_row.hashinfo;
 	struct bpf_tcp_iter_state *iter = seq->private;
 	struct tcp_iter_state *st = &iter->state;
+	int prev_bucket, prev_state;
 	unsigned int expected;
-	bool resized = false;
+	int resizes = 0;
 	struct sock *sk;
+	int err;
 
 	/* The st->bucket is done.  Directly advance to the next
 	 * bucket instead of having the tcp_seek_last_pos() to skip
@@ -3149,29 +3168,50 @@ static struct sock *bpf_iter_tcp_batch(struct seq_file *seq)
 	/* Get a new batch */
 	iter->cur_sk = 0;
 	iter->end_sk = 0;
-	iter->st_bucket_done = false;
+	iter->st_bucket_done = true;
 
+	prev_bucket = st->bucket;
+	prev_state = st->state;
 	sk = tcp_seek_last_pos(seq);
 	if (!sk)
 		return NULL; /* Done */
+	if (st->bucket != prev_bucket || st->state != prev_state)
+		resizes = 0;
+	expected = 0;
 
+fill_batch:
 	if (st->state == TCP_SEQ_STATE_LISTENING)
-		expected = bpf_iter_tcp_listening_batch(seq, sk);
+		expected += bpf_iter_tcp_listening_batch(seq, &sk);
 	else
-		expected = bpf_iter_tcp_established_batch(seq, sk);
+		expected += bpf_iter_tcp_established_batch(seq, &sk);
 
-	if (iter->end_sk == expected) {
-		iter->st_bucket_done = true;
-		return sk;
-	}
+	if (unlikely(resizes <= 1 && iter->end_sk != expected)) {
+		resizes++;
+
+		if (resizes == 1) {
+			bpf_iter_tcp_unlock_bucket(seq);
 
-	if (!resized && !bpf_iter_tcp_realloc_batch(iter, expected * 3 / 2,
-						    GFP_USER)) {
-		resized = true;
-		goto again;
+			err = bpf_iter_tcp_realloc_batch(iter, expected * 3 / 2,
+							 GFP_USER);
+			if (err)
+				return ERR_PTR(err);
+			goto again;
+		}
+
+		err = bpf_iter_tcp_realloc_batch(iter, expected, GFP_NOWAIT);
+		if (err) {
+			bpf_iter_tcp_unlock_bucket(seq);
+			return ERR_PTR(err);
+		}
+
+		expected = iter->end_sk;
+		goto fill_batch;
 	}
 
-	return sk;
+	bpf_iter_tcp_unlock_bucket(seq);
+
+	WARN_ON_ONCE(iter->end_sk != expected);
+	return iter->batch[0];
 }
 
 static void *bpf_iter_tcp_seq_start(struct seq_file *seq, loff_t *pos)
-- 
2.43.0


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

* [PATCH v1 bpf-next 03/10] bpf: tcp: Get rid of st_bucket_done
  2025-05-20 14:50 [PATCH v1 bpf-next 00/10] bpf: tcp: Exactly-once socket iteration Jordan Rife
  2025-05-20 14:50 ` [PATCH v1 bpf-next 01/10] bpf: tcp: Make mem flags configurable through bpf_iter_tcp_realloc_batch Jordan Rife
  2025-05-20 14:50 ` [PATCH v1 bpf-next 02/10] bpf: tcp: Make sure iter->batch always contains a full bucket snapshot Jordan Rife
@ 2025-05-20 14:50 ` Jordan Rife
  2025-05-21 22:57   ` Kuniyuki Iwashima
  2025-05-20 14:50 ` [PATCH v1 bpf-next 04/10] bpf: tcp: Use bpf_tcp_iter_batch_item for bpf_tcp_iter_state batch items Jordan Rife
                   ` (6 subsequent siblings)
  9 siblings, 1 reply; 25+ messages in thread
From: Jordan Rife @ 2025-05-20 14:50 UTC (permalink / raw)
  To: netdev, bpf
  Cc: Jordan Rife, Daniel Borkmann, Martin KaFai Lau, Willem de Bruijn,
	Kuniyuki Iwashima, Alexei Starovoitov

Get rid of the st_bucket_done field to simplify TCP iterator state and
logic. Before, st_bucket_done could be false if bpf_iter_tcp_batch
returned a partial batch; however, with the last patch ("bpf: tcp: Make
sure iter->batch always contains a full bucket snapshot"),
st_bucket_done == true is equivalent to iter->cur_sk == iter->end_sk.

Signed-off-by: Jordan Rife <jordan@jrife.io>
---
 net/ipv4/tcp_ipv4.c | 14 ++++++--------
 1 file changed, 6 insertions(+), 8 deletions(-)

diff --git a/net/ipv4/tcp_ipv4.c b/net/ipv4/tcp_ipv4.c
index 27022018194a..20730723a02c 100644
--- a/net/ipv4/tcp_ipv4.c
+++ b/net/ipv4/tcp_ipv4.c
@@ -3020,7 +3020,6 @@ struct bpf_tcp_iter_state {
 	unsigned int end_sk;
 	unsigned int max_sk;
 	struct sock **batch;
-	bool st_bucket_done;
 };
 
 struct bpf_iter__tcp {
@@ -3043,8 +3042,10 @@ static int tcp_prog_seq_show(struct bpf_prog *prog, struct bpf_iter_meta *meta,
 
 static void bpf_iter_tcp_put_batch(struct bpf_tcp_iter_state *iter)
 {
-	while (iter->cur_sk < iter->end_sk)
-		sock_gen_put(iter->batch[iter->cur_sk++]);
+	unsigned int cur_sk = iter->cur_sk;
+
+	while (cur_sk < iter->end_sk)
+		sock_gen_put(iter->batch[cur_sk++]);
 }
 
 static int bpf_iter_tcp_realloc_batch(struct bpf_tcp_iter_state *iter,
@@ -3154,7 +3155,7 @@ static struct sock *bpf_iter_tcp_batch(struct seq_file *seq)
 	 * one by one in the current bucket and eventually find out
 	 * it has to advance to the next bucket.
 	 */
-	if (iter->st_bucket_done) {
+	if (iter->end_sk && iter->cur_sk == iter->end_sk) {
 		st->offset = 0;
 		st->bucket++;
 		if (st->state == TCP_SEQ_STATE_LISTENING &&
@@ -3168,7 +3169,6 @@ static struct sock *bpf_iter_tcp_batch(struct seq_file *seq)
 	/* Get a new batch */
 	iter->cur_sk = 0;
 	iter->end_sk = 0;
-	iter->st_bucket_done = true;
 
 	prev_bucket = st->bucket;
 	prev_state = st->state;
@@ -3316,10 +3316,8 @@ static void bpf_iter_tcp_seq_stop(struct seq_file *seq, void *v)
 			(void)tcp_prog_seq_show(prog, &meta, v, 0);
 	}
 
-	if (iter->cur_sk < iter->end_sk) {
+	if (iter->cur_sk < iter->end_sk)
 		bpf_iter_tcp_put_batch(iter);
-		iter->st_bucket_done = false;
-	}
 }
 
 static const struct seq_operations bpf_iter_tcp_seq_ops = {
-- 
2.43.0


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

* [PATCH v1 bpf-next 04/10] bpf: tcp: Use bpf_tcp_iter_batch_item for bpf_tcp_iter_state batch items
  2025-05-20 14:50 [PATCH v1 bpf-next 00/10] bpf: tcp: Exactly-once socket iteration Jordan Rife
                   ` (2 preceding siblings ...)
  2025-05-20 14:50 ` [PATCH v1 bpf-next 03/10] bpf: tcp: Get rid of st_bucket_done Jordan Rife
@ 2025-05-20 14:50 ` Jordan Rife
  2025-05-21 22:59   ` Kuniyuki Iwashima
  2025-05-20 14:50 ` [PATCH v1 bpf-next 05/10] bpf: tcp: Avoid socket skips and repeats during iteration Jordan Rife
                   ` (5 subsequent siblings)
  9 siblings, 1 reply; 25+ messages in thread
From: Jordan Rife @ 2025-05-20 14:50 UTC (permalink / raw)
  To: netdev, bpf
  Cc: Jordan Rife, Daniel Borkmann, Martin KaFai Lau, Willem de Bruijn,
	Kuniyuki Iwashima, Alexei Starovoitov

Prepare for the next patch that tracks cookies between iterations by
converting struct sock **batch to union bpf_tcp_iter_batch_item *batch
inside struct bpf_tcp_iter_state.

Signed-off-by: Jordan Rife <jordan@jrife.io>
---
 net/ipv4/tcp_ipv4.c | 24 ++++++++++++++----------
 1 file changed, 14 insertions(+), 10 deletions(-)

diff --git a/net/ipv4/tcp_ipv4.c b/net/ipv4/tcp_ipv4.c
index 20730723a02c..65569d67d8bf 100644
--- a/net/ipv4/tcp_ipv4.c
+++ b/net/ipv4/tcp_ipv4.c
@@ -3014,12 +3014,16 @@ static int tcp4_seq_show(struct seq_file *seq, void *v)
 }
 
 #ifdef CONFIG_BPF_SYSCALL
+union bpf_tcp_iter_batch_item {
+	struct sock *sk;
+};
+
 struct bpf_tcp_iter_state {
 	struct tcp_iter_state state;
 	unsigned int cur_sk;
 	unsigned int end_sk;
 	unsigned int max_sk;
-	struct sock **batch;
+	union bpf_tcp_iter_batch_item *batch;
 };
 
 struct bpf_iter__tcp {
@@ -3045,13 +3049,13 @@ static void bpf_iter_tcp_put_batch(struct bpf_tcp_iter_state *iter)
 	unsigned int cur_sk = iter->cur_sk;
 
 	while (cur_sk < iter->end_sk)
-		sock_gen_put(iter->batch[cur_sk++]);
+		sock_gen_put(iter->batch[cur_sk++].sk);
 }
 
 static int bpf_iter_tcp_realloc_batch(struct bpf_tcp_iter_state *iter,
 				      unsigned int new_batch_sz, gfp_t flags)
 {
-	struct sock **new_batch;
+	union bpf_tcp_iter_batch_item *new_batch;
 
 	new_batch = kvmalloc(sizeof(*new_batch) * new_batch_sz,
 			     flags | __GFP_NOWARN);
@@ -3078,7 +3082,7 @@ static unsigned int bpf_iter_tcp_listening_batch(struct seq_file *seq,
 	struct sock *sk;
 
 	sock_hold(*start_sk);
-	iter->batch[iter->end_sk++] = *start_sk;
+	iter->batch[iter->end_sk++].sk = *start_sk;
 
 	sk = sk_nulls_next(*start_sk);
 	*start_sk = NULL;
@@ -3086,7 +3090,7 @@ static unsigned int bpf_iter_tcp_listening_batch(struct seq_file *seq,
 		if (seq_sk_match(seq, sk)) {
 			if (iter->end_sk < iter->max_sk) {
 				sock_hold(sk);
-				iter->batch[iter->end_sk++] = sk;
+				iter->batch[iter->end_sk++].sk = sk;
 			} else if (!*start_sk) {
 				/* Remember where we left off. */
 				*start_sk = sk;
@@ -3107,7 +3111,7 @@ static unsigned int bpf_iter_tcp_established_batch(struct seq_file *seq,
 	struct sock *sk;
 
 	sock_hold(*start_sk);
-	iter->batch[iter->end_sk++] = *start_sk;
+	iter->batch[iter->end_sk++].sk = *start_sk;
 
 	sk = sk_nulls_next(*start_sk);
 	*start_sk = NULL;
@@ -3115,7 +3119,7 @@ static unsigned int bpf_iter_tcp_established_batch(struct seq_file *seq,
 		if (seq_sk_match(seq, sk)) {
 			if (iter->end_sk < iter->max_sk) {
 				sock_hold(sk);
-				iter->batch[iter->end_sk++] = sk;
+				iter->batch[iter->end_sk++].sk = sk;
 			} else if (!*start_sk) {
 				/* Remember where we left off. */
 				*start_sk = sk;
@@ -3211,7 +3215,7 @@ static struct sock *bpf_iter_tcp_batch(struct seq_file *seq)
 	bpf_iter_tcp_unlock_bucket(seq);
 
 	WARN_ON_ONCE(iter->end_sk != expected);
-	return iter->batch[0];
+	return iter->batch[0].sk;
 }
 
 static void *bpf_iter_tcp_seq_start(struct seq_file *seq, loff_t *pos)
@@ -3246,11 +3250,11 @@ static void *bpf_iter_tcp_seq_next(struct seq_file *seq, void *v, loff_t *pos)
 		 * st->bucket.  See tcp_seek_last_pos().
 		 */
 		st->offset++;
-		sock_gen_put(iter->batch[iter->cur_sk++]);
+		sock_gen_put(iter->batch[iter->cur_sk++].sk);
 	}
 
 	if (iter->cur_sk < iter->end_sk)
-		sk = iter->batch[iter->cur_sk];
+		sk = iter->batch[iter->cur_sk].sk;
 	else
 		sk = bpf_iter_tcp_batch(seq);
 
-- 
2.43.0


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

* [PATCH v1 bpf-next 05/10] bpf: tcp: Avoid socket skips and repeats during iteration
  2025-05-20 14:50 [PATCH v1 bpf-next 00/10] bpf: tcp: Exactly-once socket iteration Jordan Rife
                   ` (3 preceding siblings ...)
  2025-05-20 14:50 ` [PATCH v1 bpf-next 04/10] bpf: tcp: Use bpf_tcp_iter_batch_item for bpf_tcp_iter_state batch items Jordan Rife
@ 2025-05-20 14:50 ` Jordan Rife
  2025-05-23 23:05   ` Martin KaFai Lau
  2025-05-20 14:50 ` [PATCH v1 bpf-next 06/10] selftests/bpf: Add tests for bucket resume logic in listening sockets Jordan Rife
                   ` (4 subsequent siblings)
  9 siblings, 1 reply; 25+ messages in thread
From: Jordan Rife @ 2025-05-20 14:50 UTC (permalink / raw)
  To: netdev, bpf
  Cc: Jordan Rife, Daniel Borkmann, Martin KaFai Lau, Willem de Bruijn,
	Kuniyuki Iwashima, Alexei Starovoitov

Replace the offset-based approach for tracking progress through a bucket
in the TCP table with one based on socket cookies. Remember the cookies
of unprocessed sockets from the last batch and use this list to
pick up where we left off or, in the case that the next socket
disappears between reads, find the first socket after that point that
still exists in the bucket and resume from there.

This approach guarantees that all sockets that existed when iteration
began and continue to exist throughout will be visited exactly once.
Sockets that are added to the table during iteration may or may not be
seen, but if they are they will be seen exactly once.

Remove the conditional that advances the bucket at the top of
bpf_iter_tcp_batch, since if iter->cur_sk == iter->end_sk
bpf_iter_tcp_resume_listening or bpf_iter_tcp_resume_established will
naturally advance to the next bucket without wasting any time scanning
through the current bucket.

Signed-off-by: Jordan Rife <jordan@jrife.io>
---
 net/ipv4/tcp_ipv4.c | 141 +++++++++++++++++++++++++++++++++++---------
 1 file changed, 113 insertions(+), 28 deletions(-)

diff --git a/net/ipv4/tcp_ipv4.c b/net/ipv4/tcp_ipv4.c
index 65569d67d8bf..11531ed4ef3c 100644
--- a/net/ipv4/tcp_ipv4.c
+++ b/net/ipv4/tcp_ipv4.c
@@ -58,6 +58,7 @@
 #include <linux/times.h>
 #include <linux/slab.h>
 #include <linux/sched.h>
+#include <linux/sock_diag.h>
 
 #include <net/net_namespace.h>
 #include <net/icmp.h>
@@ -3016,6 +3017,7 @@ static int tcp4_seq_show(struct seq_file *seq, void *v)
 #ifdef CONFIG_BPF_SYSCALL
 union bpf_tcp_iter_batch_item {
 	struct sock *sk;
+	__u64 cookie;
 };
 
 struct bpf_tcp_iter_state {
@@ -3046,10 +3048,19 @@ static int tcp_prog_seq_show(struct bpf_prog *prog, struct bpf_iter_meta *meta,
 
 static void bpf_iter_tcp_put_batch(struct bpf_tcp_iter_state *iter)
 {
+	union bpf_tcp_iter_batch_item *item;
 	unsigned int cur_sk = iter->cur_sk;
+	__u64 cookie;
 
-	while (cur_sk < iter->end_sk)
-		sock_gen_put(iter->batch[cur_sk++].sk);
+	/* Remember the cookies of the sockets we haven't seen yet, so we can
+	 * pick up where we left off next time around.
+	 */
+	while (cur_sk < iter->end_sk) {
+		item = &iter->batch[cur_sk++];
+		cookie = sock_gen_cookie(item->sk);
+		sock_gen_put(item->sk);
+		item->cookie = cookie;
+	}
 }
 
 static int bpf_iter_tcp_realloc_batch(struct bpf_tcp_iter_state *iter,
@@ -3073,6 +3084,105 @@ static int bpf_iter_tcp_realloc_batch(struct bpf_tcp_iter_state *iter,
 	return 0;
 }
 
+static struct sock *bpf_iter_tcp_resume_bucket(struct sock *first_sk,
+					       union bpf_tcp_iter_batch_item *cookies,
+					       int n_cookies)
+{
+	struct hlist_nulls_node *node;
+	struct sock *sk;
+	int i;
+
+	for (i = 0; i < n_cookies; i++) {
+		sk = first_sk;
+		sk_nulls_for_each_from(sk, node) {
+			if (cookies[i].cookie == atomic64_read(&sk->sk_cookie))
+				return sk;
+		}
+	}
+
+	return NULL;
+}
+
+static struct sock *bpf_iter_tcp_resume_listening(struct seq_file *seq)
+{
+	struct inet_hashinfo *hinfo = seq_file_net(seq)->ipv4.tcp_death_row.hashinfo;
+	struct bpf_tcp_iter_state *iter = seq->private;
+	struct tcp_iter_state *st = &iter->state;
+	unsigned int find_cookie = iter->cur_sk;
+	unsigned int end_cookie = iter->end_sk;
+	int resume_bucket = st->bucket;
+	struct sock *sk;
+
+	sk = listening_get_first(seq);
+	iter->cur_sk = 0;
+	iter->end_sk = 0;
+
+	if (sk && st->bucket == resume_bucket && end_cookie) {
+		sk = bpf_iter_tcp_resume_bucket(sk, &iter->batch[find_cookie],
+						end_cookie - find_cookie);
+		if (!sk) {
+			spin_unlock(&hinfo->lhash2[st->bucket].lock);
+			++st->bucket;
+			sk = listening_get_first(seq);
+		}
+	}
+
+	return sk;
+}
+
+static struct sock *bpf_iter_tcp_resume_established(struct seq_file *seq)
+{
+	struct inet_hashinfo *hinfo = seq_file_net(seq)->ipv4.tcp_death_row.hashinfo;
+	struct bpf_tcp_iter_state *iter = seq->private;
+	struct tcp_iter_state *st = &iter->state;
+	unsigned int find_cookie = iter->cur_sk;
+	unsigned int end_cookie = iter->end_sk;
+	int resume_bucket = st->bucket;
+	struct sock *sk;
+
+	sk = established_get_first(seq);
+	iter->cur_sk = 0;
+	iter->end_sk = 0;
+
+	if (sk && st->bucket == resume_bucket && end_cookie) {
+		sk = bpf_iter_tcp_resume_bucket(sk, &iter->batch[find_cookie],
+						end_cookie - find_cookie);
+		if (!sk) {
+			spin_unlock_bh(inet_ehash_lockp(hinfo, st->bucket));
+			++st->bucket;
+			sk = established_get_first(seq);
+		}
+	}
+
+	return sk;
+}
+
+static struct sock *bpf_iter_tcp_resume(struct seq_file *seq)
+{
+	struct inet_hashinfo *hinfo = seq_file_net(seq)->ipv4.tcp_death_row.hashinfo;
+	struct bpf_tcp_iter_state *iter = seq->private;
+	struct tcp_iter_state *st = &iter->state;
+	struct sock *sk = NULL;
+
+	switch (st->state) {
+	case TCP_SEQ_STATE_LISTENING:
+		if (st->bucket > hinfo->lhash2_mask)
+			break;
+		sk = bpf_iter_tcp_resume_listening(seq);
+		if (sk)
+			break;
+		st->bucket = 0;
+		st->state = TCP_SEQ_STATE_ESTABLISHED;
+		fallthrough;
+	case TCP_SEQ_STATE_ESTABLISHED:
+		if (st->bucket > hinfo->ehash_mask)
+			break;
+		sk = bpf_iter_tcp_resume_established(seq);
+	}
+
+	return sk;
+}
+
 static unsigned int bpf_iter_tcp_listening_batch(struct seq_file *seq,
 						 struct sock **start_sk)
 {
@@ -3145,7 +3255,6 @@ static void bpf_iter_tcp_unlock_bucket(struct seq_file *seq)
 
 static struct sock *bpf_iter_tcp_batch(struct seq_file *seq)
 {
-	struct inet_hashinfo *hinfo = seq_file_net(seq)->ipv4.tcp_death_row.hashinfo;
 	struct bpf_tcp_iter_state *iter = seq->private;
 	struct tcp_iter_state *st = &iter->state;
 	int prev_bucket, prev_state;
@@ -3154,29 +3263,10 @@ static struct sock *bpf_iter_tcp_batch(struct seq_file *seq)
 	struct sock *sk;
 	int err;
 
-	/* The st->bucket is done.  Directly advance to the next
-	 * bucket instead of having the tcp_seek_last_pos() to skip
-	 * one by one in the current bucket and eventually find out
-	 * it has to advance to the next bucket.
-	 */
-	if (iter->end_sk && iter->cur_sk == iter->end_sk) {
-		st->offset = 0;
-		st->bucket++;
-		if (st->state == TCP_SEQ_STATE_LISTENING &&
-		    st->bucket > hinfo->lhash2_mask) {
-			st->state = TCP_SEQ_STATE_ESTABLISHED;
-			st->bucket = 0;
-		}
-	}
-
 again:
-	/* Get a new batch */
-	iter->cur_sk = 0;
-	iter->end_sk = 0;
-
 	prev_bucket = st->bucket;
 	prev_state = st->state;
-	sk = tcp_seek_last_pos(seq);
+	sk = bpf_iter_tcp_resume(seq);
 	if (!sk)
 		return NULL; /* Done */
 	if (st->bucket != prev_bucket || st->state != prev_state)
@@ -3245,11 +3335,6 @@ static void *bpf_iter_tcp_seq_next(struct seq_file *seq, void *v, loff_t *pos)
 		 * meta.seq_num is used instead.
 		 */
 		st->num++;
-		/* Move st->offset to the next sk in the bucket such that
-		 * the future start() will resume at st->offset in
-		 * st->bucket.  See tcp_seek_last_pos().
-		 */
-		st->offset++;
 		sock_gen_put(iter->batch[iter->cur_sk++].sk);
 	}
 
-- 
2.43.0


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

* [PATCH v1 bpf-next 06/10] selftests/bpf: Add tests for bucket resume logic in listening sockets
  2025-05-20 14:50 [PATCH v1 bpf-next 00/10] bpf: tcp: Exactly-once socket iteration Jordan Rife
                   ` (4 preceding siblings ...)
  2025-05-20 14:50 ` [PATCH v1 bpf-next 05/10] bpf: tcp: Avoid socket skips and repeats during iteration Jordan Rife
@ 2025-05-20 14:50 ` Jordan Rife
  2025-05-20 14:50 ` [PATCH v1 bpf-next 07/10] selftests/bpf: Allow for iteration over multiple ports Jordan Rife
                   ` (3 subsequent siblings)
  9 siblings, 0 replies; 25+ messages in thread
From: Jordan Rife @ 2025-05-20 14:50 UTC (permalink / raw)
  To: netdev, bpf
  Cc: Jordan Rife, Daniel Borkmann, Martin KaFai Lau, Willem de Bruijn,
	Kuniyuki Iwashima, Alexei Starovoitov

Replicate the set of test cases used for UDP socket iterators to test
similar scenarios for TCP listening sockets.

Signed-off-by: Jordan Rife <jordan@jrife.io>
---
 .../bpf/prog_tests/sock_iter_batch.c          | 47 +++++++++++++++++++
 1 file changed, 47 insertions(+)

diff --git a/tools/testing/selftests/bpf/prog_tests/sock_iter_batch.c b/tools/testing/selftests/bpf/prog_tests/sock_iter_batch.c
index a4517bee34d5..2adacd91fdf8 100644
--- a/tools/testing/selftests/bpf/prog_tests/sock_iter_batch.c
+++ b/tools/testing/selftests/bpf/prog_tests/sock_iter_batch.c
@@ -358,6 +358,53 @@ static struct test_case resume_tests[] = {
 		.family = AF_INET6,
 		.test = force_realloc,
 	},
+	{
+		.description = "tcp: resume after removing a seen socket (listening)",
+		.init_socks = nr_soreuse,
+		.max_socks = nr_soreuse,
+		.sock_type = SOCK_STREAM,
+		.family = AF_INET6,
+		.test = remove_seen,
+	},
+	{
+		.description = "tcp: resume after removing one unseen socket (listening)",
+		.init_socks = nr_soreuse,
+		.max_socks = nr_soreuse,
+		.sock_type = SOCK_STREAM,
+		.family = AF_INET6,
+		.test = remove_unseen,
+	},
+	{
+		.description = "tcp: resume after removing all unseen sockets (listening)",
+		.init_socks = nr_soreuse,
+		.max_socks = nr_soreuse,
+		.sock_type = SOCK_STREAM,
+		.family = AF_INET6,
+		.test = remove_all,
+	},
+	{
+		.description = "tcp: resume after adding a few sockets (listening)",
+		.init_socks = nr_soreuse,
+		.max_socks = nr_soreuse,
+		.sock_type = SOCK_STREAM,
+		/* Use AF_INET so that new sockets are added to the head of the
+		 * bucket's list.
+		 */
+		.family = AF_INET,
+		.test = add_some,
+	},
+	{
+		.description = "tcp: force a realloc to occur (listening)",
+		.init_socks = init_batch_size,
+		.max_socks = init_batch_size * 2,
+		.sock_type = SOCK_STREAM,
+		/* Use AF_INET6 so that new sockets are added to the tail of the
+		 * bucket's list, needing to be added to the next batch to force
+		 * a realloc.
+		 */
+		.family = AF_INET6,
+		.test = force_realloc,
+	},
 };
 
 static void do_resume_test(struct test_case *tc)
-- 
2.43.0


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

* [PATCH v1 bpf-next 07/10] selftests/bpf: Allow for iteration over multiple ports
  2025-05-20 14:50 [PATCH v1 bpf-next 00/10] bpf: tcp: Exactly-once socket iteration Jordan Rife
                   ` (5 preceding siblings ...)
  2025-05-20 14:50 ` [PATCH v1 bpf-next 06/10] selftests/bpf: Add tests for bucket resume logic in listening sockets Jordan Rife
@ 2025-05-20 14:50 ` Jordan Rife
  2025-05-20 14:50 ` [PATCH v1 bpf-next 08/10] selftests/bpf: Make ehash buckets configurable in socket iterator tests Jordan Rife
                   ` (2 subsequent siblings)
  9 siblings, 0 replies; 25+ messages in thread
From: Jordan Rife @ 2025-05-20 14:50 UTC (permalink / raw)
  To: netdev, bpf
  Cc: Jordan Rife, Daniel Borkmann, Martin KaFai Lau, Willem de Bruijn,
	Kuniyuki Iwashima, Alexei Starovoitov

Prepare to test TCP socket iteration over both listening and established
sockets by allowing the BPF iterator programs to skip the port check.

Signed-off-by: Jordan Rife <jordan@jrife.io>
---
 tools/testing/selftests/bpf/prog_tests/sock_iter_batch.c | 7 ++-----
 tools/testing/selftests/bpf/progs/sock_iter_batch.c      | 4 ++++
 2 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/tools/testing/selftests/bpf/prog_tests/sock_iter_batch.c b/tools/testing/selftests/bpf/prog_tests/sock_iter_batch.c
index 2adacd91fdf8..0d0f1b4debff 100644
--- a/tools/testing/selftests/bpf/prog_tests/sock_iter_batch.c
+++ b/tools/testing/selftests/bpf/prog_tests/sock_iter_batch.c
@@ -416,7 +416,6 @@ static void do_resume_test(struct test_case *tc)
 	int err, iter_fd = -1;
 	const char *addr;
 	int *fds = NULL;
-	int local_port;
 
 	counts = calloc(tc->max_socks, sizeof(*counts));
 	if (!ASSERT_OK_PTR(counts, "counts"))
@@ -431,10 +430,8 @@ static void do_resume_test(struct test_case *tc)
 				     tc->init_socks);
 	if (!ASSERT_OK_PTR(fds, "start_reuseport_server"))
 		goto done;
-	local_port = get_socket_local_port(*fds);
-	if (!ASSERT_GE(local_port, 0, "get_socket_local_port"))
-		goto done;
-	skel->rodata->ports[0] = ntohs(local_port);
+	skel->rodata->ports[0] = 0;
+	skel->rodata->ports[1] = 0;
 	skel->rodata->sf = tc->family;
 
 	err = sock_iter_batch__load(skel);
diff --git a/tools/testing/selftests/bpf/progs/sock_iter_batch.c b/tools/testing/selftests/bpf/progs/sock_iter_batch.c
index 8f483337e103..40dce6a38c30 100644
--- a/tools/testing/selftests/bpf/progs/sock_iter_batch.c
+++ b/tools/testing/selftests/bpf/progs/sock_iter_batch.c
@@ -52,6 +52,8 @@ int iter_tcp_soreuse(struct bpf_iter__tcp *ctx)
 		idx = 0;
 	else if (sk->sk_num == ports[1])
 		idx = 1;
+	else if (!ports[0] && !ports[1])
+		idx = 0;
 	else
 		return 0;
 
@@ -92,6 +94,8 @@ int iter_udp_soreuse(struct bpf_iter__udp *ctx)
 		idx = 0;
 	else if (sk->sk_num == ports[1])
 		idx = 1;
+	else if (!ports[0] && !ports[1])
+		idx = 0;
 	else
 		return 0;
 
-- 
2.43.0


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

* [PATCH v1 bpf-next 08/10] selftests/bpf: Make ehash buckets configurable in socket iterator tests
  2025-05-20 14:50 [PATCH v1 bpf-next 00/10] bpf: tcp: Exactly-once socket iteration Jordan Rife
                   ` (6 preceding siblings ...)
  2025-05-20 14:50 ` [PATCH v1 bpf-next 07/10] selftests/bpf: Allow for iteration over multiple ports Jordan Rife
@ 2025-05-20 14:50 ` Jordan Rife
  2025-05-20 14:50 ` [PATCH v1 bpf-next 09/10] selftests/bpf: Create established sockets " Jordan Rife
  2025-05-20 14:50 ` [PATCH v1 bpf-next 10/10] selftests/bpf: Add tests for bucket resume logic in established sockets Jordan Rife
  9 siblings, 0 replies; 25+ messages in thread
From: Jordan Rife @ 2025-05-20 14:50 UTC (permalink / raw)
  To: netdev, bpf
  Cc: Jordan Rife, Daniel Borkmann, Martin KaFai Lau, Willem de Bruijn,
	Kuniyuki Iwashima, Alexei Starovoitov

Prepare for bucket resume tests for established TCP sockets by making
the number of ehash buckets configurable. Subsequent patches force all
established sockets into the same bucket by setting ehash_buckets to
one.

Signed-off-by: Jordan Rife <jordan@jrife.io>
---
 .../bpf/prog_tests/sock_iter_batch.c          | 19 ++++++++++++++++++-
 1 file changed, 18 insertions(+), 1 deletion(-)

diff --git a/tools/testing/selftests/bpf/prog_tests/sock_iter_batch.c b/tools/testing/selftests/bpf/prog_tests/sock_iter_batch.c
index 0d0f1b4debff..847e4b87ab92 100644
--- a/tools/testing/selftests/bpf/prog_tests/sock_iter_batch.c
+++ b/tools/testing/selftests/bpf/prog_tests/sock_iter_batch.c
@@ -6,6 +6,7 @@
 #include "sock_iter_batch.skel.h"
 
 #define TEST_NS "sock_iter_batch_netns"
+#define TEST_CHILD_NS "sock_iter_batch_child_netns"
 
 static const int init_batch_size = 16;
 static const int nr_soreuse = 4;
@@ -304,6 +305,7 @@ struct test_case {
 		     int *socks, int socks_len, struct sock_count *counts,
 		     int counts_len, struct bpf_link *link, int iter_fd);
 	const char *description;
+	int ehash_buckets;
 	int init_socks;
 	int max_socks;
 	int sock_type;
@@ -410,13 +412,25 @@ static struct test_case resume_tests[] = {
 static void do_resume_test(struct test_case *tc)
 {
 	struct sock_iter_batch *skel = NULL;
+	struct sock_count *counts = NULL;
 	static const __u16 port = 10001;
+	struct nstoken *nstoken = NULL;
 	struct bpf_link *link = NULL;
-	struct sock_count *counts;
 	int err, iter_fd = -1;
 	const char *addr;
 	int *fds = NULL;
 
+	if (tc->ehash_buckets) {
+		SYS_NOFAIL("ip netns del " TEST_CHILD_NS);
+		SYS(done, "sysctl -w net.ipv4.tcp_child_ehash_entries=%d",
+		    tc->ehash_buckets);
+		SYS(done, "ip netns add %s", TEST_CHILD_NS);
+		SYS(done, "ip -net %s link set dev lo up", TEST_CHILD_NS);
+		nstoken = open_netns(TEST_CHILD_NS);
+		if (!ASSERT_OK_PTR(nstoken, "open_child_netns"))
+			goto done;
+	}
+
 	counts = calloc(tc->max_socks, sizeof(*counts));
 	if (!ASSERT_OK_PTR(counts, "counts"))
 		goto done;
@@ -452,6 +466,9 @@ static void do_resume_test(struct test_case *tc)
 	tc->test(tc->family, tc->sock_type, addr, port, fds, tc->init_socks,
 		 counts, tc->max_socks, link, iter_fd);
 done:
+	close_netns(nstoken);
+	SYS_NOFAIL("ip netns del " TEST_CHILD_NS);
+	SYS_NOFAIL("sysctl -w net.ipv4.tcp_child_ehash_entries=0");
 	free(counts);
 	free_fds(fds, tc->init_socks);
 	if (iter_fd >= 0)
-- 
2.43.0


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

* [PATCH v1 bpf-next 09/10] selftests/bpf: Create established sockets in socket iterator tests
  2025-05-20 14:50 [PATCH v1 bpf-next 00/10] bpf: tcp: Exactly-once socket iteration Jordan Rife
                   ` (7 preceding siblings ...)
  2025-05-20 14:50 ` [PATCH v1 bpf-next 08/10] selftests/bpf: Make ehash buckets configurable in socket iterator tests Jordan Rife
@ 2025-05-20 14:50 ` Jordan Rife
  2025-05-20 14:50 ` [PATCH v1 bpf-next 10/10] selftests/bpf: Add tests for bucket resume logic in established sockets Jordan Rife
  9 siblings, 0 replies; 25+ messages in thread
From: Jordan Rife @ 2025-05-20 14:50 UTC (permalink / raw)
  To: netdev, bpf
  Cc: Jordan Rife, Daniel Borkmann, Martin KaFai Lau, Willem de Bruijn,
	Kuniyuki Iwashima, Alexei Starovoitov

Prepare for bucket resume tests for established TCP sockets by creating
established sockets. Collect socket fds from connect() and accept()
sides and pass them to test cases.

Signed-off-by: Jordan Rife <jordan@jrife.io>
---
 .../bpf/prog_tests/sock_iter_batch.c          | 83 ++++++++++++++++++-
 1 file changed, 79 insertions(+), 4 deletions(-)

diff --git a/tools/testing/selftests/bpf/prog_tests/sock_iter_batch.c b/tools/testing/selftests/bpf/prog_tests/sock_iter_batch.c
index 847e4b87ab92..f14adda52f53 100644
--- a/tools/testing/selftests/bpf/prog_tests/sock_iter_batch.c
+++ b/tools/testing/selftests/bpf/prog_tests/sock_iter_batch.c
@@ -153,8 +153,66 @@ static void check_n_were_seen_once(int *fds, int fds_len, int n,
 	ASSERT_EQ(seen_once, n, "seen_once");
 }
 
+static int accept_from_one(int *server_fds, int server_fds_len)
+{
+	int i = 0;
+	int fd;
+
+	for (; i < server_fds_len; i++) {
+		fd = accept(server_fds[i], NULL, NULL);
+		if (fd >= 0)
+			return fd;
+		if (!ASSERT_EQ(errno, EWOULDBLOCK, "EWOULDBLOCK"))
+			return -1;
+	}
+
+	return -1;
+}
+
+static int *connect_to_server(int family, int sock_type, const char *addr,
+			      __u16 port, int nr_connects, int *server_fds,
+			      int server_fds_len)
+{
+	struct network_helper_opts opts = {
+		.timeout_ms = 0,
+	};
+	int *established_socks;
+	int i;
+
+	/* Make sure accept() doesn't block. */
+	for (i = 0; i < server_fds_len; i++)
+		if (!ASSERT_OK(fcntl(server_fds[i], F_SETFL, O_NONBLOCK),
+			       "fcntl(O_NONBLOCK)"))
+			return NULL;
+
+	established_socks = malloc(sizeof(int) * nr_connects*2);
+	if (!ASSERT_OK_PTR(established_socks, "established_socks"))
+		return NULL;
+
+	i = 0;
+
+	while (nr_connects--) {
+		established_socks[i] = connect_to_addr_str(family, sock_type,
+							   addr, port, &opts);
+		if (!ASSERT_OK_FD(established_socks[i], "connect_to_addr_str"))
+			goto error;
+		i++;
+		established_socks[i] = accept_from_one(server_fds,
+						       server_fds_len);
+		if (!ASSERT_OK_FD(established_socks[i], "accept_from_one"))
+			goto error;
+		i++;
+	}
+
+	return established_socks;
+error:
+	free_fds(established_socks, i);
+	return NULL;
+}
+
 static void remove_seen(int family, int sock_type, const char *addr, __u16 port,
-			int *socks, int socks_len, struct sock_count *counts,
+			int *socks, int socks_len, int *established_socks,
+			int established_socks_len, struct sock_count *counts,
 			int counts_len, struct bpf_link *link, int iter_fd)
 {
 	int close_idx;
@@ -185,6 +243,7 @@ static void remove_seen(int family, int sock_type, const char *addr, __u16 port,
 
 static void remove_unseen(int family, int sock_type, const char *addr,
 			  __u16 port, int *socks, int socks_len,
+			  int *established_socks, int established_socks_len,
 			  struct sock_count *counts, int counts_len,
 			  struct bpf_link *link, int iter_fd)
 {
@@ -217,6 +276,7 @@ static void remove_unseen(int family, int sock_type, const char *addr,
 
 static void remove_all(int family, int sock_type, const char *addr,
 		       __u16 port, int *socks, int socks_len,
+		       int *established_socks, int established_socks_len,
 		       struct sock_count *counts, int counts_len,
 		       struct bpf_link *link, int iter_fd)
 {
@@ -244,7 +304,8 @@ static void remove_all(int family, int sock_type, const char *addr,
 }
 
 static void add_some(int family, int sock_type, const char *addr, __u16 port,
-		     int *socks, int socks_len, struct sock_count *counts,
+		     int *socks, int socks_len, int *established_socks,
+		     int established_socks_len, struct sock_count *counts,
 		     int counts_len, struct bpf_link *link, int iter_fd)
 {
 	int *new_socks = NULL;
@@ -274,6 +335,7 @@ static void add_some(int family, int sock_type, const char *addr, __u16 port,
 
 static void force_realloc(int family, int sock_type, const char *addr,
 			  __u16 port, int *socks, int socks_len,
+			  int *established_socks, int established_socks_len,
 			  struct sock_count *counts, int counts_len,
 			  struct bpf_link *link, int iter_fd)
 {
@@ -302,10 +364,12 @@ static void force_realloc(int family, int sock_type, const char *addr,
 
 struct test_case {
 	void (*test)(int family, int sock_type, const char *addr, __u16 port,
-		     int *socks, int socks_len, struct sock_count *counts,
+		     int *socks, int socks_len, int *established_socks,
+		     int established_socks_len, struct sock_count *counts,
 		     int counts_len, struct bpf_link *link, int iter_fd);
 	const char *description;
 	int ehash_buckets;
+	int connections;
 	int init_socks;
 	int max_socks;
 	int sock_type;
@@ -416,6 +480,7 @@ static void do_resume_test(struct test_case *tc)
 	static const __u16 port = 10001;
 	struct nstoken *nstoken = NULL;
 	struct bpf_link *link = NULL;
+	int *established_fds = NULL;
 	int err, iter_fd = -1;
 	const char *addr;
 	int *fds = NULL;
@@ -444,6 +509,14 @@ static void do_resume_test(struct test_case *tc)
 				     tc->init_socks);
 	if (!ASSERT_OK_PTR(fds, "start_reuseport_server"))
 		goto done;
+	if (tc->connections) {
+		established_fds = connect_to_server(tc->family, tc->sock_type,
+						    addr, port,
+						    tc->connections, fds,
+						    tc->init_socks);
+		if (!ASSERT_OK_PTR(established_fds, "connect_to_server"))
+			goto done;
+	}
 	skel->rodata->ports[0] = 0;
 	skel->rodata->ports[1] = 0;
 	skel->rodata->sf = tc->family;
@@ -464,13 +537,15 @@ static void do_resume_test(struct test_case *tc)
 		goto done;
 
 	tc->test(tc->family, tc->sock_type, addr, port, fds, tc->init_socks,
-		 counts, tc->max_socks, link, iter_fd);
+		 established_fds, tc->connections*2, counts, tc->max_socks,
+		 link, iter_fd);
 done:
 	close_netns(nstoken);
 	SYS_NOFAIL("ip netns del " TEST_CHILD_NS);
 	SYS_NOFAIL("sysctl -w net.ipv4.tcp_child_ehash_entries=0");
 	free(counts);
 	free_fds(fds, tc->init_socks);
+	free_fds(established_fds, tc->connections*2);
 	if (iter_fd >= 0)
 		close(iter_fd);
 	bpf_link__destroy(link);
-- 
2.43.0


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

* [PATCH v1 bpf-next 10/10] selftests/bpf: Add tests for bucket resume logic in established sockets
  2025-05-20 14:50 [PATCH v1 bpf-next 00/10] bpf: tcp: Exactly-once socket iteration Jordan Rife
                   ` (8 preceding siblings ...)
  2025-05-20 14:50 ` [PATCH v1 bpf-next 09/10] selftests/bpf: Create established sockets " Jordan Rife
@ 2025-05-20 14:50 ` Jordan Rife
  2025-05-28  0:51   ` Martin KaFai Lau
  9 siblings, 1 reply; 25+ messages in thread
From: Jordan Rife @ 2025-05-20 14:50 UTC (permalink / raw)
  To: netdev, bpf
  Cc: Jordan Rife, Daniel Borkmann, Martin KaFai Lau, Willem de Bruijn,
	Kuniyuki Iwashima, Alexei Starovoitov

Replicate the set of test cases used for UDP socket iterators to test
similar scenarios for TCP established sockets.

Signed-off-by: Jordan Rife <jordan@jrife.io>
---
 .../bpf/prog_tests/sock_iter_batch.c          | 286 ++++++++++++++++++
 1 file changed, 286 insertions(+)

diff --git a/tools/testing/selftests/bpf/prog_tests/sock_iter_batch.c b/tools/testing/selftests/bpf/prog_tests/sock_iter_batch.c
index f14adda52f53..44fbb527594d 100644
--- a/tools/testing/selftests/bpf/prog_tests/sock_iter_batch.c
+++ b/tools/testing/selftests/bpf/prog_tests/sock_iter_batch.c
@@ -119,6 +119,44 @@ static int get_nth_socket(int *fds, int fds_len, struct bpf_link *link, int n)
 	return nth_sock_idx;
 }
 
+static bool close_and_wait(int fd, struct bpf_link *link)
+{
+	static const int us_per_ms = 1000;
+	__u64 cookie = socket_cookie(fd);
+	struct iter_out out;
+	bool exists = true;
+	int iter_fd, nread;
+	int waits = 20; /* 2 seconds */
+
+	close(fd);
+
+	/* Wait for socket to disappear from the ehash table. */
+	while (waits--) {
+		iter_fd = bpf_iter_create(bpf_link__fd(link));
+		if (!ASSERT_OK_FD(iter_fd, "bpf_iter_create"))
+			return false;
+
+		/* Is it still there? */
+		do {
+			nread = read(iter_fd, &out, sizeof(out));
+			if (!ASSERT_GE(nread, 0, "nread")) {
+				close(iter_fd);
+				return false;
+			}
+			exists = nread && cookie == out.cookie;
+		} while (!exists && nread);
+
+		close(iter_fd);
+
+		if (!exists)
+			break;
+
+		usleep(100 * us_per_ms);
+	}
+
+	return !exists;
+}
+
 static int get_seen_count(int fd, struct sock_count counts[], int n)
 {
 	__u64 cookie = socket_cookie(fd);
@@ -241,6 +279,43 @@ static void remove_seen(int family, int sock_type, const char *addr, __u16 port,
 			       counts_len);
 }
 
+static void remove_seen_established(int family, int sock_type, const char *addr,
+				    __u16 port, int *listen_socks,
+				    int listen_socks_len, int *established_socks,
+				    int established_socks_len,
+				    struct sock_count *counts, int counts_len,
+				    struct bpf_link *link, int iter_fd)
+{
+	int close_idx;
+
+	/* Iterate through all listening sockets. */
+	read_n(iter_fd, listen_socks_len, counts, counts_len);
+
+	/* Make sure we saw all listening sockets exactly once. */
+	check_n_were_seen_once(listen_socks, listen_socks_len, listen_socks_len,
+			       counts, counts_len);
+
+	/* Leave one established socket. */
+	read_n(iter_fd, established_socks_len - 1, counts, counts_len);
+
+	/* Close a socket we've already seen to remove it from the bucket. */
+	close_idx = get_seen_socket(established_socks, counts, counts_len);
+	if (!ASSERT_GE(close_idx, 0, "close_idx"))
+		return;
+	ASSERT_TRUE(close_and_wait(established_socks[close_idx], link),
+		    "close_and_wait");
+	established_socks[close_idx] = -1;
+
+	/* Iterate through the rest of the sockets. */
+	read_n(iter_fd, -1, counts, counts_len);
+
+	/* Make sure the last socket wasn't skipped and that there were no
+	 * repeats.
+	 */
+	check_n_were_seen_once(established_socks, established_socks_len,
+			       established_socks_len - 1, counts, counts_len);
+}
+
 static void remove_unseen(int family, int sock_type, const char *addr,
 			  __u16 port, int *socks, int socks_len,
 			  int *established_socks, int established_socks_len,
@@ -274,6 +349,52 @@ static void remove_unseen(int family, int sock_type, const char *addr,
 			       counts_len);
 }
 
+static void remove_unseen_established(int family, int sock_type,
+				      const char *addr, __u16 port,
+				      int *listen_socks, int listen_socks_len,
+				      int *established_socks,
+				      int established_socks_len,
+				      struct sock_count *counts, int counts_len,
+				      struct bpf_link *link, int iter_fd)
+{
+	int close_idx;
+
+	/* Iterate through all listening sockets. */
+	read_n(iter_fd, listen_socks_len, counts, counts_len);
+
+	/* Make sure we saw all listening sockets exactly once. */
+	check_n_were_seen_once(listen_socks, listen_socks_len, listen_socks_len,
+			       counts, counts_len);
+
+	/* Iterate through the first established socket. */
+	read_n(iter_fd, 1, counts, counts_len);
+
+	/* Make sure we saw one established socks. */
+	check_n_were_seen_once(established_socks, established_socks_len, 1,
+			       counts, counts_len);
+
+	/* Close what would be the next socket in the bucket to exercise the
+	 * condition where we need to skip past the first cookie we remembered.
+	 */
+	close_idx = get_nth_socket(established_socks, established_socks_len,
+				   link, listen_socks_len + 1);
+	if (!ASSERT_GE(close_idx, 0, "close_idx"))
+		return;
+
+	ASSERT_TRUE(close_and_wait(established_socks[close_idx], link),
+		    "close_and_wait");
+	established_socks[close_idx] = -1;
+
+	/* Iterate through the rest of the sockets. */
+	read_n(iter_fd, -1, counts, counts_len);
+
+	/* Make sure the remaining sockets were seen exactly once and that we
+	 * didn't repeat the socket that was already seen.
+	 */
+	check_n_were_seen_once(established_socks, established_socks_len,
+			       established_socks_len - 1, counts, counts_len);
+}
+
 static void remove_all(int family, int sock_type, const char *addr,
 		       __u16 port, int *socks, int socks_len,
 		       int *established_socks, int established_socks_len,
@@ -303,6 +424,47 @@ static void remove_all(int family, int sock_type, const char *addr,
 	ASSERT_EQ(read_n(iter_fd, -1, counts, counts_len), 0, "read_n");
 }
 
+static void remove_all_established(int family, int sock_type, const char *addr,
+				   __u16 port, int *listen_socks,
+				   int listen_socks_len, int *established_socks,
+				   int established_socks_len,
+				   struct sock_count *counts, int counts_len,
+				   struct bpf_link *link, int iter_fd)
+{
+	int close_idx, i;
+
+	/* Iterate through all listening sockets. */
+	read_n(iter_fd, listen_socks_len, counts, counts_len);
+
+	/* Make sure we saw all listening sockets exactly once. */
+	check_n_were_seen_once(listen_socks, listen_socks_len, listen_socks_len,
+			       counts, counts_len);
+
+	/* Iterate through the first established socket. */
+	read_n(iter_fd, 1, counts, counts_len);
+
+	/* Make sure we saw one established socks. */
+	check_n_were_seen_once(established_socks, established_socks_len, 1,
+			       counts, counts_len);
+
+	/* Close all remaining sockets to exhaust the list of saved cookies and
+	 * exit without putting any sockets into the batch on the next read.
+	 */
+	for (i = 0; i < established_socks_len - 1; i++) {
+		close_idx = get_nth_socket(established_socks,
+					   established_socks_len, link,
+					   listen_socks_len + 1);
+		if (!ASSERT_GE(close_idx, 0, "close_idx"))
+			return;
+		ASSERT_TRUE(close_and_wait(established_socks[close_idx], link),
+			    "close_and_wait");
+		established_socks[close_idx] = -1;
+	}
+
+	/* Make sure there are no more sockets returned */
+	ASSERT_EQ(read_n(iter_fd, -1, counts, counts_len), 0, "read_n");
+}
+
 static void add_some(int family, int sock_type, const char *addr, __u16 port,
 		     int *socks, int socks_len, int *established_socks,
 		     int established_socks_len, struct sock_count *counts,
@@ -333,6 +495,49 @@ static void add_some(int family, int sock_type, const char *addr, __u16 port,
 	free_fds(new_socks, socks_len);
 }
 
+static void add_some_established(int family, int sock_type, const char *addr,
+				 __u16 port, int *listen_socks,
+				 int listen_socks_len, int *established_socks,
+				 int established_socks_len,
+				 struct sock_count *counts,
+				 int counts_len, struct bpf_link *link,
+				 int iter_fd)
+{
+	int *new_socks = NULL;
+
+	/* Iterate through all listening sockets. */
+	read_n(iter_fd, listen_socks_len, counts, counts_len);
+
+	/* Make sure we saw all listening sockets exactly once. */
+	check_n_were_seen_once(listen_socks, listen_socks_len, listen_socks_len,
+			       counts, counts_len);
+
+	/* Iterate through the first established_socks_len - 1 sockets. */
+	read_n(iter_fd, established_socks_len - 1, counts, counts_len);
+
+	/* Make sure we saw established_socks_len - 1 sockets exactly once. */
+	check_n_were_seen_once(established_socks, established_socks_len,
+			       established_socks_len - 1, counts, counts_len);
+
+	/* Double the number of established sockets in the bucket. */
+	new_socks = connect_to_server(family, sock_type, addr, port,
+				      established_socks_len / 2, listen_socks,
+				      listen_socks_len);
+	if (!ASSERT_OK_PTR(new_socks, "connect_to_server"))
+		goto done;
+
+	/* Iterate through the rest of the sockets. */
+	read_n(iter_fd, -1, counts, counts_len);
+
+	/* Make sure each of the original sockets was seen exactly once. */
+	check_n_were_seen_once(listen_socks, listen_socks_len, listen_socks_len,
+			       counts, counts_len);
+	check_n_were_seen_once(established_socks, established_socks_len,
+			       established_socks_len, counts, counts_len);
+done:
+	free_fds(new_socks, established_socks_len);
+}
+
 static void force_realloc(int family, int sock_type, const char *addr,
 			  __u16 port, int *socks, int socks_len,
 			  int *established_socks, int established_socks_len,
@@ -362,6 +567,24 @@ static void force_realloc(int family, int sock_type, const char *addr,
 	free_fds(new_socks, socks_len);
 }
 
+static void force_realloc_established(int family, int sock_type,
+				      const char *addr, __u16 port,
+				      int *listen_socks, int listen_socks_len,
+				      int *established_socks,
+				      int established_socks_len,
+				      struct sock_count *counts, int counts_len,
+				      struct bpf_link *link, int iter_fd)
+{
+	/* Iterate through all sockets to trigger a realloc. */
+	read_n(iter_fd, -1, counts, counts_len);
+
+	/* Make sure each socket was seen exactly once. */
+	check_n_were_seen_once(listen_socks, listen_socks_len, listen_socks_len,
+			       counts, counts_len);
+	check_n_were_seen_once(established_socks, established_socks_len,
+			       established_socks_len, counts, counts_len);
+}
+
 struct test_case {
 	void (*test)(int family, int sock_type, const char *addr, __u16 port,
 		     int *socks, int socks_len, int *established_socks,
@@ -471,6 +694,69 @@ static struct test_case resume_tests[] = {
 		.family = AF_INET6,
 		.test = force_realloc,
 	},
+	{
+		.description = "tcp: resume after removing a seen socket (established)",
+		/* Force all established sockets into one bucket */
+		.ehash_buckets = 1,
+		.connections = nr_soreuse,
+		.init_socks = nr_soreuse,
+		/* Room for connect()ed and accept()ed sockets */
+		.max_socks = nr_soreuse * 3,
+		.sock_type = SOCK_STREAM,
+		.family = AF_INET6,
+		.test = remove_seen_established,
+	},
+	{
+		.description = "tcp: resume after removing one unseen socket (established)",
+		/* Force all established sockets into one bucket */
+		.ehash_buckets = 1,
+		.connections = nr_soreuse,
+		.init_socks = nr_soreuse,
+		/* Room for connect()ed and accept()ed sockets */
+		.max_socks = nr_soreuse * 3,
+		.sock_type = SOCK_STREAM,
+		.family = AF_INET6,
+		.test = remove_unseen_established,
+	},
+	{
+		.description = "tcp: resume after removing all unseen sockets (established)",
+		/* Force all established sockets into one bucket */
+		.ehash_buckets = 1,
+		.connections = nr_soreuse,
+		.init_socks = nr_soreuse,
+		/* Room for connect()ed and accept()ed sockets */
+		.max_socks = nr_soreuse * 3,
+		.sock_type = SOCK_STREAM,
+		.family = AF_INET6,
+		.test = remove_all_established,
+	},
+	{
+		.description = "tcp: resume after adding a few sockets (established)",
+		/* Force all established sockets into one bucket */
+		.ehash_buckets = 1,
+		.connections = nr_soreuse,
+		.init_socks = nr_soreuse,
+		/* Room for connect()ed and accept()ed sockets */
+		.max_socks = nr_soreuse * 3,
+		.sock_type = SOCK_STREAM,
+		.family = AF_INET6,
+		.test = add_some_established,
+	},
+	{
+		.description = "tcp: force a realloc to occur (established)",
+		/* Force all established sockets into one bucket */
+		.ehash_buckets = 1,
+		/* Bucket size will need to double when going from listening to
+		 * established sockets.
+		 */
+		.connections = init_batch_size,
+		.init_socks = nr_soreuse,
+		/* Room for connect()ed and accept()ed sockets */
+		.max_socks = nr_soreuse + (init_batch_size * 2),
+		.sock_type = SOCK_STREAM,
+		.family = AF_INET6,
+		.test = force_realloc_established,
+	},
 };
 
 static void do_resume_test(struct test_case *tc)
-- 
2.43.0


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

* Re: [PATCH v1 bpf-next 01/10] bpf: tcp: Make mem flags configurable through bpf_iter_tcp_realloc_batch
  2025-05-20 14:50 ` [PATCH v1 bpf-next 01/10] bpf: tcp: Make mem flags configurable through bpf_iter_tcp_realloc_batch Jordan Rife
@ 2025-05-21 18:54   ` Kuniyuki Iwashima
  0 siblings, 0 replies; 25+ messages in thread
From: Kuniyuki Iwashima @ 2025-05-21 18:54 UTC (permalink / raw)
  To: jordan
  Cc: alexei.starovoitov, bpf, daniel, kuniyu, martin.lau, netdev,
	willemdebruijn.kernel

From: Jordan Rife <jordan@jrife.io>
Date: Tue, 20 May 2025 07:50:48 -0700
> Prepare for the next patch which needs to be able to choose either
> GFP_USER or GFP_NOWAIT for calls to bpf_iter_tcp_realloc_batch.
> 
> Signed-off-by: Jordan Rife <jordan@jrife.io>

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

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

* Re: [PATCH v1 bpf-next 02/10] bpf: tcp: Make sure iter->batch always contains a full bucket snapshot
  2025-05-20 14:50 ` [PATCH v1 bpf-next 02/10] bpf: tcp: Make sure iter->batch always contains a full bucket snapshot Jordan Rife
@ 2025-05-21 22:20   ` Kuniyuki Iwashima
  0 siblings, 0 replies; 25+ messages in thread
From: Kuniyuki Iwashima @ 2025-05-21 22:20 UTC (permalink / raw)
  To: jordan
  Cc: alexei.starovoitov, bpf, daniel, kuniyu, martin.lau, netdev,
	willemdebruijn.kernel

From: Jordan Rife <jordan@jrife.io>
Date: Tue, 20 May 2025 07:50:49 -0700
> Require that iter->batch always contains a full bucket snapshot. This
> invariant is important to avoid skipping or repeating sockets during
> iteration when combined with the next few patches. Before, there were
> two cases where a call to bpf_iter_tcp_batch may only capture part of a
> bucket:
> 
> 1. When bpf_iter_tcp_realloc_batch() returns -ENOMEM.
> 2. When more sockets are added to the bucket while calling
>    bpf_iter_tcp_realloc_batch(), making the updated batch size
>    insufficient.
> 
> In cases where the batch size only covers part of a bucket, it is
> possible to forget which sockets were already visited, especially if we
> have to process a bucket in more than two batches. This forces us to
> choose between repeating or skipping sockets, so don't allow this:
> 
> 1. Stop iteration and propagate -ENOMEM up to userspace if reallocation
>    fails instead of continuing with a partial batch.
> 2. Try bpf_iter_tcp_realloc_batch() with GFP_USER just as before, but if
>    we still aren't able to capture the full bucket, call
>    bpf_iter_tcp_realloc_batch() again while holding the bucket lock to
>    guarantee the bucket does not change. On the second attempt use
>    GFP_NOWAIT since we hold onto the spin lock.
> 
> I did some manual testing to exercise the code paths where GFP_NOWAIT is
> used and where ERR_PTR(err) is returned. I used the realloc test cases
> included later in this series to trigger a scenario where a realloc
> happens inside bpf_iter_tcp_batch and made a small code tweak to force
> the first realloc attempt to allocate a too-small batch, thus requiring
> another attempt with GFP_NOWAIT. Some printks showed both reallocs with
> the tests passing:
> 
> May 09 18:18:55 crow kernel: resize batch TCP_SEQ_STATE_LISTENING
> May 09 18:18:55 crow kernel: again GFP_USER
> May 09 18:18:55 crow kernel: resize batch TCP_SEQ_STATE_LISTENING
> May 09 18:18:55 crow kernel: again GFP_NOWAIT
> May 09 18:18:57 crow kernel: resize batch TCP_SEQ_STATE_ESTABLISHED
> May 09 18:18:57 crow kernel: again GFP_USER
> May 09 18:18:57 crow kernel: resize batch TCP_SEQ_STATE_ESTABLISHED
> May 09 18:18:57 crow kernel: again GFP_NOWAIT
> 
> With this setup, I also forced each of the bpf_iter_tcp_realloc_batch
> calls to return -ENOMEM to ensure that iteration ends and that the
> read() in userspace fails.
> 
> Signed-off-by: Jordan Rife <jordan@jrife.io>

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

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

* Re: [PATCH v1 bpf-next 03/10] bpf: tcp: Get rid of st_bucket_done
  2025-05-20 14:50 ` [PATCH v1 bpf-next 03/10] bpf: tcp: Get rid of st_bucket_done Jordan Rife
@ 2025-05-21 22:57   ` Kuniyuki Iwashima
  2025-05-21 23:17     ` Kuniyuki Iwashima
  0 siblings, 1 reply; 25+ messages in thread
From: Kuniyuki Iwashima @ 2025-05-21 22:57 UTC (permalink / raw)
  To: jordan
  Cc: alexei.starovoitov, bpf, daniel, kuniyu, martin.lau, netdev,
	willemdebruijn.kernel

From: Jordan Rife <jordan@jrife.io>
Date: Tue, 20 May 2025 07:50:50 -0700
> Get rid of the st_bucket_done field to simplify TCP iterator state and
> logic. Before, st_bucket_done could be false if bpf_iter_tcp_batch
> returned a partial batch; however, with the last patch ("bpf: tcp: Make
> sure iter->batch always contains a full bucket snapshot"),
> st_bucket_done == true is equivalent to iter->cur_sk == iter->end_sk.
> 
> Signed-off-by: Jordan Rife <jordan@jrife.io>
> ---
>  net/ipv4/tcp_ipv4.c | 14 ++++++--------
>  1 file changed, 6 insertions(+), 8 deletions(-)
> 
> diff --git a/net/ipv4/tcp_ipv4.c b/net/ipv4/tcp_ipv4.c
> index 27022018194a..20730723a02c 100644
> --- a/net/ipv4/tcp_ipv4.c
> +++ b/net/ipv4/tcp_ipv4.c
> @@ -3020,7 +3020,6 @@ struct bpf_tcp_iter_state {
>  	unsigned int end_sk;
>  	unsigned int max_sk;
>  	struct sock **batch;
> -	bool st_bucket_done;
>  };
>  
>  struct bpf_iter__tcp {
> @@ -3043,8 +3042,10 @@ static int tcp_prog_seq_show(struct bpf_prog *prog, struct bpf_iter_meta *meta,
>  
>  static void bpf_iter_tcp_put_batch(struct bpf_tcp_iter_state *iter)
>  {
> -	while (iter->cur_sk < iter->end_sk)
> -		sock_gen_put(iter->batch[iter->cur_sk++]);
> +	unsigned int cur_sk = iter->cur_sk;
> +
> +	while (cur_sk < iter->end_sk)
> +		sock_gen_put(iter->batch[cur_sk++]);

Why is this chunk included in this patch ?

This is a bit confusing given bpf_iter_tcp_seq_next() proceeds
iter->cur_sk during sock_gen_put().

Otherwise looks good.

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

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

* Re: [PATCH v1 bpf-next 04/10] bpf: tcp: Use bpf_tcp_iter_batch_item for bpf_tcp_iter_state batch items
  2025-05-20 14:50 ` [PATCH v1 bpf-next 04/10] bpf: tcp: Use bpf_tcp_iter_batch_item for bpf_tcp_iter_state batch items Jordan Rife
@ 2025-05-21 22:59   ` Kuniyuki Iwashima
  0 siblings, 0 replies; 25+ messages in thread
From: Kuniyuki Iwashima @ 2025-05-21 22:59 UTC (permalink / raw)
  To: jordan
  Cc: alexei.starovoitov, bpf, daniel, kuniyu, martin.lau, netdev,
	willemdebruijn.kernel

From: Jordan Rife <jordan@jrife.io>
Date: Tue, 20 May 2025 07:50:51 -0700
> Prepare for the next patch that tracks cookies between iterations by
> converting struct sock **batch to union bpf_tcp_iter_batch_item *batch
> inside struct bpf_tcp_iter_state.
> 
> Signed-off-by: Jordan Rife <jordan@jrife.io>

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

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

* Re: [PATCH v1 bpf-next 03/10] bpf: tcp: Get rid of st_bucket_done
  2025-05-21 22:57   ` Kuniyuki Iwashima
@ 2025-05-21 23:17     ` Kuniyuki Iwashima
  2025-05-22 18:16       ` Jordan Rife
  0 siblings, 1 reply; 25+ messages in thread
From: Kuniyuki Iwashima @ 2025-05-21 23:17 UTC (permalink / raw)
  To: kuniyu
  Cc: alexei.starovoitov, bpf, daniel, jordan, martin.lau, netdev,
	willemdebruijn.kernel

From: Kuniyuki Iwashima <kuniyu@amazon.com>
Date: Wed, 21 May 2025 15:57:59 -0700
> From: Jordan Rife <jordan@jrife.io>
> Date: Tue, 20 May 2025 07:50:50 -0700
> > Get rid of the st_bucket_done field to simplify TCP iterator state and
> > logic. Before, st_bucket_done could be false if bpf_iter_tcp_batch
> > returned a partial batch; however, with the last patch ("bpf: tcp: Make
> > sure iter->batch always contains a full bucket snapshot"),
> > st_bucket_done == true is equivalent to iter->cur_sk == iter->end_sk.
> > 
> > Signed-off-by: Jordan Rife <jordan@jrife.io>
> > ---
> >  net/ipv4/tcp_ipv4.c | 14 ++++++--------
> >  1 file changed, 6 insertions(+), 8 deletions(-)
> > 
> > diff --git a/net/ipv4/tcp_ipv4.c b/net/ipv4/tcp_ipv4.c
> > index 27022018194a..20730723a02c 100644
> > --- a/net/ipv4/tcp_ipv4.c
> > +++ b/net/ipv4/tcp_ipv4.c
> > @@ -3020,7 +3020,6 @@ struct bpf_tcp_iter_state {
> >  	unsigned int end_sk;
> >  	unsigned int max_sk;
> >  	struct sock **batch;
> > -	bool st_bucket_done;
> >  };
> >  
> >  struct bpf_iter__tcp {
> > @@ -3043,8 +3042,10 @@ static int tcp_prog_seq_show(struct bpf_prog *prog, struct bpf_iter_meta *meta,
> >  
> >  static void bpf_iter_tcp_put_batch(struct bpf_tcp_iter_state *iter)
> >  {
> > -	while (iter->cur_sk < iter->end_sk)
> > -		sock_gen_put(iter->batch[iter->cur_sk++]);
> > +	unsigned int cur_sk = iter->cur_sk;
> > +
> > +	while (cur_sk < iter->end_sk)
> > +		sock_gen_put(iter->batch[cur_sk++]);
> 
> Why is this chunk included in this patch ?

This should be in patch 5 to keep cur_sk for find_cookie

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

* Re: [PATCH v1 bpf-next 03/10] bpf: tcp: Get rid of st_bucket_done
  2025-05-21 23:17     ` Kuniyuki Iwashima
@ 2025-05-22 18:16       ` Jordan Rife
  2025-05-22 20:42         ` Kuniyuki Iwashima
  0 siblings, 1 reply; 25+ messages in thread
From: Jordan Rife @ 2025-05-22 18:16 UTC (permalink / raw)
  To: Kuniyuki Iwashima
  Cc: alexei.starovoitov, bpf, daniel, martin.lau, netdev,
	willemdebruijn.kernel

> > >  static void bpf_iter_tcp_put_batch(struct bpf_tcp_iter_state *iter)
> > >  {
> > > -	while (iter->cur_sk < iter->end_sk)
> > > -		sock_gen_put(iter->batch[iter->cur_sk++]);
> > > +	unsigned int cur_sk = iter->cur_sk;
> > > +
> > > +	while (cur_sk < iter->end_sk)
> > > +		sock_gen_put(iter->batch[cur_sk++]);
> > 
> > Why is this chunk included in this patch ?
> 
> This should be in patch 5 to keep cur_sk for find_cookie

Without this, iter->cur_sk is mutated when iteration stops, and we lose
our place. When iteration resumes and we call bpf_iter_tcp_batch the
iter->cur_sk == iter->end_sk condition will always be true, so we will
skip to the next bucket without seeking to the offset.

Before, we relied on st_bucket_done to tell us if we had remaining items
in the current bucket to process but now need to preserve iter->cur_sk
through iterations to make the behavior equivalent to what we had before.

Jordan

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

* Re: [PATCH v1 bpf-next 03/10] bpf: tcp: Get rid of st_bucket_done
  2025-05-22 18:16       ` Jordan Rife
@ 2025-05-22 20:42         ` Kuniyuki Iwashima
  2025-05-23 22:07           ` Martin KaFai Lau
  0 siblings, 1 reply; 25+ messages in thread
From: Kuniyuki Iwashima @ 2025-05-22 20:42 UTC (permalink / raw)
  To: jordan
  Cc: alexei.starovoitov, bpf, daniel, kuniyu, martin.lau, netdev,
	willemdebruijn.kernel

From: Jordan Rife <jordan@jrife.io>
Date: Thu, 22 May 2025 11:16:13 -0700
> > > >  static void bpf_iter_tcp_put_batch(struct bpf_tcp_iter_state *iter)
> > > >  {
> > > > -	while (iter->cur_sk < iter->end_sk)
> > > > -		sock_gen_put(iter->batch[iter->cur_sk++]);
> > > > +	unsigned int cur_sk = iter->cur_sk;
> > > > +
> > > > +	while (cur_sk < iter->end_sk)
> > > > +		sock_gen_put(iter->batch[cur_sk++]);
> > > 
> > > Why is this chunk included in this patch ?
> > 
> > This should be in patch 5 to keep cur_sk for find_cookie
> 
> Without this, iter->cur_sk is mutated when iteration stops, and we lose
> our place. When iteration resumes and we call bpf_iter_tcp_batch the
> iter->cur_sk == iter->end_sk condition will always be true, so we will
> skip to the next bucket without seeking to the offset.
> 
> Before, we relied on st_bucket_done to tell us if we had remaining items
> in the current bucket to process but now need to preserve iter->cur_sk
> through iterations to make the behavior equivalent to what we had before.

Thanks for explanation, I was confused by calling tcp_seek_last_pos()
multiple times, and I think we need to preserve/restore st->offset too
in patch 2 and need this change.

diff --git a/net/ipv4/tcp_ipv4.c b/net/ipv4/tcp_ipv4.c
index ac00015d5e7a..0816f20bfdff 100644
--- a/net/ipv4/tcp_ipv4.c
+++ b/net/ipv4/tcp_ipv4.c
@@ -2791,6 +2791,7 @@ static void *tcp_seek_last_pos(struct seq_file *seq)
 			break;
 		st->bucket = 0;
 		st->state = TCP_SEQ_STATE_ESTABLISHED;
+		offset = 0;
 		fallthrough;
 	case TCP_SEQ_STATE_ESTABLISHED:
 		if (st->bucket > hinfo->ehash_mask)


Let's say we are resuming at an offset (10) in the last lhash bucket
but a few sockets (3) disappeared, then we go to the ehash part with
a non-zero offset (3), which will overwrite st->offset (3).

If the ehash does not fit into the batch size, we need to allocate
a new batch and retry, but the offset (3) is different from the
first try (10).

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

* Re: [PATCH v1 bpf-next 03/10] bpf: tcp: Get rid of st_bucket_done
  2025-05-22 20:42         ` Kuniyuki Iwashima
@ 2025-05-23 22:07           ` Martin KaFai Lau
  2025-05-24 21:09             ` Jordan Rife
  0 siblings, 1 reply; 25+ messages in thread
From: Martin KaFai Lau @ 2025-05-23 22:07 UTC (permalink / raw)
  To: Kuniyuki Iwashima, jordan
  Cc: alexei.starovoitov, bpf, daniel, netdev, willemdebruijn.kernel

On 5/22/25 1:42 PM, Kuniyuki Iwashima wrote:
> From: Jordan Rife <jordan@jrife.io>
> Date: Thu, 22 May 2025 11:16:13 -0700
>>>>>   static void bpf_iter_tcp_put_batch(struct bpf_tcp_iter_state *iter)
>>>>>   {
>>>>> -	while (iter->cur_sk < iter->end_sk)
>>>>> -		sock_gen_put(iter->batch[iter->cur_sk++]);
>>>>> +	unsigned int cur_sk = iter->cur_sk;
>>>>> +
>>>>> +	while (cur_sk < iter->end_sk)
>>>>> +		sock_gen_put(iter->batch[cur_sk++]);
>>>>
>>>> Why is this chunk included in this patch ?
>>>
>>> This should be in patch 5 to keep cur_sk for find_cookie
>>
>> Without this, iter->cur_sk is mutated when iteration stops, and we lose
>> our place. When iteration resumes and we call bpf_iter_tcp_batch the
>> iter->cur_sk == iter->end_sk condition will always be true, so we will
>> skip to the next bucket without seeking to the offset.
>>
>> Before, we relied on st_bucket_done to tell us if we had remaining items
>> in the current bucket to process but now need to preserve iter->cur_sk
>> through iterations to make the behavior equivalent to what we had before.
> 
> Thanks for explanation, I was confused by calling tcp_seek_last_pos()
> multiple times, and I think we need to preserve/restore st->offset too
> in patch 2 and need this change.
> 
> diff --git a/net/ipv4/tcp_ipv4.c b/net/ipv4/tcp_ipv4.c
> index ac00015d5e7a..0816f20bfdff 100644
> --- a/net/ipv4/tcp_ipv4.c
> +++ b/net/ipv4/tcp_ipv4.c
> @@ -2791,6 +2791,7 @@ static void *tcp_seek_last_pos(struct seq_file *seq)
>   			break;
>   		st->bucket = 0;
>   		st->state = TCP_SEQ_STATE_ESTABLISHED;
> +		offset = 0;

This seems like an existing bug not necessarily related to this set.

The patch 5 has also removed the tcp_seek_last_pos() dependency, so I think it 
can be a standalone fix on its own.


>   		fallthrough;
>   	case TCP_SEQ_STATE_ESTABLISHED:
>   		if (st->bucket > hinfo->ehash_mask)> 
> 
> Let's say we are resuming at an offset (10) in the last lhash bucket
> but a few sockets (3) disappeared, then we go to the ehash part with
> a non-zero offset (3), which will overwrite st->offset (3).
> 
> If the ehash does not fit into the batch size, we need to allocate
> a new batch and retry, but the offset (3) is different from the
> first try (10).


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

* Re: [PATCH v1 bpf-next 05/10] bpf: tcp: Avoid socket skips and repeats during iteration
  2025-05-20 14:50 ` [PATCH v1 bpf-next 05/10] bpf: tcp: Avoid socket skips and repeats during iteration Jordan Rife
@ 2025-05-23 23:05   ` Martin KaFai Lau
  2025-05-24  1:24     ` Jordan Rife
  0 siblings, 1 reply; 25+ messages in thread
From: Martin KaFai Lau @ 2025-05-23 23:05 UTC (permalink / raw)
  To: Jordan Rife
  Cc: Daniel Borkmann, Willem de Bruijn, Kuniyuki Iwashima,
	Alexei Starovoitov, netdev, bpf

On 5/20/25 7:50 AM, Jordan Rife wrote:
> Replace the offset-based approach for tracking progress through a bucket
> in the TCP table with one based on socket cookies. Remember the cookies
> of unprocessed sockets from the last batch and use this list to
> pick up where we left off or, in the case that the next socket
> disappears between reads, find the first socket after that point that
> still exists in the bucket and resume from there.
> 
> This approach guarantees that all sockets that existed when iteration
> began and continue to exist throughout will be visited exactly once.
> Sockets that are added to the table during iteration may or may not be
> seen, but if they are they will be seen exactly once.
> 
> Remove the conditional that advances the bucket at the top of
> bpf_iter_tcp_batch, since if iter->cur_sk == iter->end_sk
> bpf_iter_tcp_resume_listening or bpf_iter_tcp_resume_established will
> naturally advance to the next bucket without wasting any time scanning
> through the current bucket.

> 
> Signed-off-by: Jordan Rife <jordan@jrife.io>
> ---
>   net/ipv4/tcp_ipv4.c | 141 +++++++++++++++++++++++++++++++++++---------
>   1 file changed, 113 insertions(+), 28 deletions(-)
> 
> diff --git a/net/ipv4/tcp_ipv4.c b/net/ipv4/tcp_ipv4.c
> index 65569d67d8bf..11531ed4ef3c 100644
> --- a/net/ipv4/tcp_ipv4.c
> +++ b/net/ipv4/tcp_ipv4.c
> @@ -58,6 +58,7 @@
>   #include <linux/times.h>
>   #include <linux/slab.h>
>   #include <linux/sched.h>
> +#include <linux/sock_diag.h>
>   
>   #include <net/net_namespace.h>
>   #include <net/icmp.h>
> @@ -3016,6 +3017,7 @@ static int tcp4_seq_show(struct seq_file *seq, void *v)
>   #ifdef CONFIG_BPF_SYSCALL
>   union bpf_tcp_iter_batch_item {
>   	struct sock *sk;
> +	__u64 cookie;
>   };
>   
>   struct bpf_tcp_iter_state {
> @@ -3046,10 +3048,19 @@ static int tcp_prog_seq_show(struct bpf_prog *prog, struct bpf_iter_meta *meta,
>   
>   static void bpf_iter_tcp_put_batch(struct bpf_tcp_iter_state *iter)
>   {
> +	union bpf_tcp_iter_batch_item *item;
>   	unsigned int cur_sk = iter->cur_sk;
> +	__u64 cookie;
>   
> -	while (cur_sk < iter->end_sk)
> -		sock_gen_put(iter->batch[cur_sk++].sk);
> +	/* Remember the cookies of the sockets we haven't seen yet, so we can
> +	 * pick up where we left off next time around.
> +	 */
> +	while (cur_sk < iter->end_sk) {
> +		item = &iter->batch[cur_sk++];
> +		cookie = sock_gen_cookie(item->sk);
> +		sock_gen_put(item->sk);
> +		item->cookie = cookie;
> +	}
>   }
>   
>   static int bpf_iter_tcp_realloc_batch(struct bpf_tcp_iter_state *iter,
> @@ -3073,6 +3084,105 @@ static int bpf_iter_tcp_realloc_batch(struct bpf_tcp_iter_state *iter,
>   	return 0;
>   }
>   
> +static struct sock *bpf_iter_tcp_resume_bucket(struct sock *first_sk,
> +					       union bpf_tcp_iter_batch_item *cookies,
> +					       int n_cookies)
> +{
> +	struct hlist_nulls_node *node;
> +	struct sock *sk;
> +	int i;
> +
> +	for (i = 0; i < n_cookies; i++) {
> +		sk = first_sk;
> +		sk_nulls_for_each_from(sk, node) {
> +			if (cookies[i].cookie == atomic64_read(&sk->sk_cookie))
> +				return sk;
> +		}
> +	}
> +
> +	return NULL;
> +}
> +
> +static struct sock *bpf_iter_tcp_resume_listening(struct seq_file *seq)
> +{
> +	struct inet_hashinfo *hinfo = seq_file_net(seq)->ipv4.tcp_death_row.hashinfo;
> +	struct bpf_tcp_iter_state *iter = seq->private;
> +	struct tcp_iter_state *st = &iter->state;
> +	unsigned int find_cookie = iter->cur_sk;
> +	unsigned int end_cookie = iter->end_sk;
> +	int resume_bucket = st->bucket;
> +	struct sock *sk;
> +
> +	sk = listening_get_first(seq);

Since it does not advance the sk->bucket++ now, it will still scan until the 
first seq_sk_match()?

Does it make sense to advance the st->bucket++ in the bpf_iter_tcp_seq_next and 
bpf_iter_tcp_seq_stop?

> +	iter->cur_sk = 0;
> +	iter->end_sk = 0;
> +
> +	if (sk && st->bucket == resume_bucket && end_cookie) {

If doing st->bucket++ in the next and stop, this probably needs the "&& 
end_cookie - find_cookie" check.

> +		sk = bpf_iter_tcp_resume_bucket(sk, &iter->batch[find_cookie],
> +						end_cookie - find_cookie);
> +		if (!sk) {
> +			spin_unlock(&hinfo->lhash2[st->bucket].lock);
> +			++st->bucket;
> +			sk = listening_get_first(seq);
> +		}
> +	}
> +
> +	return sk;
> +}
> +
> +static struct sock *bpf_iter_tcp_resume_established(struct seq_file *seq)
> +{
> +	struct inet_hashinfo *hinfo = seq_file_net(seq)->ipv4.tcp_death_row.hashinfo;
> +	struct bpf_tcp_iter_state *iter = seq->private;
> +	struct tcp_iter_state *st = &iter->state;
> +	unsigned int find_cookie = iter->cur_sk;
> +	unsigned int end_cookie = iter->end_sk;
> +	int resume_bucket = st->bucket;
> +	struct sock *sk;
> +
> +	sk = established_get_first(seq);
> +	iter->cur_sk = 0;
> +	iter->end_sk = 0;
> +
> +	if (sk && st->bucket == resume_bucket && end_cookie) {
> +		sk = bpf_iter_tcp_resume_bucket(sk, &iter->batch[find_cookie],
> +						end_cookie - find_cookie);
> +		if (!sk) {
> +			spin_unlock_bh(inet_ehash_lockp(hinfo, st->bucket));
> +			++st->bucket;
> +			sk = established_get_first(seq);
> +		}
> +	}
> +
> +	return sk;
> +}
> +
> +static struct sock *bpf_iter_tcp_resume(struct seq_file *seq)
> +{
> +	struct inet_hashinfo *hinfo = seq_file_net(seq)->ipv4.tcp_death_row.hashinfo;
> +	struct bpf_tcp_iter_state *iter = seq->private;
> +	struct tcp_iter_state *st = &iter->state;
> +	struct sock *sk = NULL;
> +
> +	switch (st->state) {
> +	case TCP_SEQ_STATE_LISTENING:
> +		if (st->bucket > hinfo->lhash2_mask)

Understood that this is borrowed from the existing tcp_seek_last_pos().

I wonder if this case would ever be hit. If it is not, may be avoid adding it to 
this new resume function?

> +			break;
> +		sk = bpf_iter_tcp_resume_listening(seq);
> +		if (sk)
> +			break;
> +		st->bucket = 0;
> +		st->state = TCP_SEQ_STATE_ESTABLISHED;
> +		fallthrough;
> +	case TCP_SEQ_STATE_ESTABLISHED:
> +		if (st->bucket > hinfo->ehash_mask)

Same here, and the following established_get_first() should have taken care of 
this case also.

Overall the set makes sense to me. Thanks for making a similar improvement in 
the tcp iter.

I often find the seq_start/next/stop logic a bit tricky. I will need to do 
another look early next week. I also haven't had a chance to look at the tests.


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

* Re: [PATCH v1 bpf-next 05/10] bpf: tcp: Avoid socket skips and repeats during iteration
  2025-05-23 23:05   ` Martin KaFai Lau
@ 2025-05-24  1:24     ` Jordan Rife
  0 siblings, 0 replies; 25+ messages in thread
From: Jordan Rife @ 2025-05-24  1:24 UTC (permalink / raw)
  To: Martin KaFai Lau
  Cc: Daniel Borkmann, Willem de Bruijn, Kuniyuki Iwashima,
	Alexei Starovoitov, netdev, bpf

> > +static struct sock *bpf_iter_tcp_resume_listening(struct seq_file *seq)
> > +{
> > +	struct inet_hashinfo *hinfo = seq_file_net(seq)->ipv4.tcp_death_row.hashinfo;
> > +	struct bpf_tcp_iter_state *iter = seq->private;
> > +	struct tcp_iter_state *st = &iter->state;
> > +	unsigned int find_cookie = iter->cur_sk;
> > +	unsigned int end_cookie = iter->end_sk;
> > +	int resume_bucket = st->bucket;
> > +	struct sock *sk;
> > +
> > +	sk = listening_get_first(seq);
> 
> Since it does not advance the sk->bucket++ now, it will still scan until the
> first seq_sk_match()?

Yeah, true. It will waste some time in the current bucket to find the
first match even if iter->cur_sk == iter->end_sk.

> Does it make sense to advance the st->bucket++ in the bpf_iter_tcp_seq_next
> and bpf_iter_tcp_seq_stop?

It seems like this should work. If you're on the last listening bucket
and do st->bucket++ on stop/next, the next call to listening_get_first()
will just return NULL then fall through to the established buckets in
bpf_iter_tcp_resume(). Will need to think through the edge cases a bit
more...

> > +static struct sock *bpf_iter_tcp_resume(struct seq_file *seq)
> > +{
> > +	struct inet_hashinfo *hinfo = seq_file_net(seq)->ipv4.tcp_death_row.hashinfo;
> > +	struct bpf_tcp_iter_state *iter = seq->private;
> > +	struct tcp_iter_state *st = &iter->state;
> > +	struct sock *sk = NULL;
> > +
> > +	switch (st->state) {
> > +	case TCP_SEQ_STATE_LISTENING:
> > +		if (st->bucket > hinfo->lhash2_mask)
> 
> Understood that this is borrowed from the existing tcp_seek_last_pos().
> 
> I wonder if this case would ever be hit. If it is not, may be avoid adding
> it to this new resume function?

Yeah, I think we should be able to get rid of this.
bpf_iter_tcp_resume_listening and bpf_iter_tcp_resume_established should
just fall through and return NULL anyway in these cases.

Jordan

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

* Re: [PATCH v1 bpf-next 03/10] bpf: tcp: Get rid of st_bucket_done
  2025-05-23 22:07           ` Martin KaFai Lau
@ 2025-05-24 21:09             ` Jordan Rife
  2025-05-27 18:19               ` Martin KaFai Lau
  0 siblings, 1 reply; 25+ messages in thread
From: Jordan Rife @ 2025-05-24 21:09 UTC (permalink / raw)
  To: Martin KaFai Lau
  Cc: Kuniyuki Iwashima, alexei.starovoitov, bpf, daniel, netdev,
	willemdebruijn.kernel

On Fri, May 23, 2025 at 03:07:32PM -0700, Martin KaFai Lau wrote:
> On 5/22/25 1:42 PM, Kuniyuki Iwashima wrote:
> > From: Jordan Rife <jordan@jrife.io>
> > Date: Thu, 22 May 2025 11:16:13 -0700
> > > > > >   static void bpf_iter_tcp_put_batch(struct bpf_tcp_iter_state *iter)
> > > > > >   {
> > > > > > -	while (iter->cur_sk < iter->end_sk)
> > > > > > -		sock_gen_put(iter->batch[iter->cur_sk++]);
> > > > > > +	unsigned int cur_sk = iter->cur_sk;
> > > > > > +
> > > > > > +	while (cur_sk < iter->end_sk)
> > > > > > +		sock_gen_put(iter->batch[cur_sk++]);
> > > > > 
> > > > > Why is this chunk included in this patch ?
> > > > 
> > > > This should be in patch 5 to keep cur_sk for find_cookie
> > > 
> > > Without this, iter->cur_sk is mutated when iteration stops, and we lose
> > > our place. When iteration resumes and we call bpf_iter_tcp_batch the
> > > iter->cur_sk == iter->end_sk condition will always be true, so we will
> > > skip to the next bucket without seeking to the offset.
> > > 
> > > Before, we relied on st_bucket_done to tell us if we had remaining items
> > > in the current bucket to process but now need to preserve iter->cur_sk
> > > through iterations to make the behavior equivalent to what we had before.
> > 
> > Thanks for explanation, I was confused by calling tcp_seek_last_pos()
> > multiple times, and I think we need to preserve/restore st->offset too
> > in patch 2 and need this change.
> > 
> > diff --git a/net/ipv4/tcp_ipv4.c b/net/ipv4/tcp_ipv4.c
> > index ac00015d5e7a..0816f20bfdff 100644
> > --- a/net/ipv4/tcp_ipv4.c
> > +++ b/net/ipv4/tcp_ipv4.c
> > @@ -2791,6 +2791,7 @@ static void *tcp_seek_last_pos(struct seq_file *seq)
> >   			break;
> >   		st->bucket = 0;
> >   		st->state = TCP_SEQ_STATE_ESTABLISHED;
> > +		offset = 0;
> 
> This seems like an existing bug not necessarily related to this set.

Agree that this is more of an existing bug.

> The patch 5 has also removed the tcp_seek_last_pos() dependency, so I think
> it can be a standalone fix on its own.

With the tcp_seq_* ops there are also other corner cases that can lead
to skips, since they rely on st->offset to seek to the last position.

In the scenario described above, sockets disappearing from the last lhash
bucket leads to skipped sockets in the first ehash bucket, but you could
also have a scenario where, for example, the current lhash bucket has 6
sockets, iter->offset is currently 3, 3 sockets disappear from the start
of the current lhash bucket then tcp_seek_last_pos skips the remaining 3
sockets and goes to the next bucket.

I'm not sure it's worth fixing just this one case without also
overhauling the tcp_seq_* logic to prevent these other cases. Otherwise,
it seems more like a Band-aid fix. Perhaps a later series could explore
a more comprehensive solution there.

Jordan

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

* Re: [PATCH v1 bpf-next 03/10] bpf: tcp: Get rid of st_bucket_done
  2025-05-24 21:09             ` Jordan Rife
@ 2025-05-27 18:19               ` Martin KaFai Lau
  0 siblings, 0 replies; 25+ messages in thread
From: Martin KaFai Lau @ 2025-05-27 18:19 UTC (permalink / raw)
  To: Jordan Rife
  Cc: Kuniyuki Iwashima, alexei.starovoitov, bpf, daniel, netdev,
	willemdebruijn.kernel

On 5/24/25 2:09 PM, Jordan Rife wrote:
> On Fri, May 23, 2025 at 03:07:32PM -0700, Martin KaFai Lau wrote:
>> On 5/22/25 1:42 PM, Kuniyuki Iwashima wrote:
>>> From: Jordan Rife <jordan@jrife.io>
>>> Date: Thu, 22 May 2025 11:16:13 -0700
>>>>>>>    static void bpf_iter_tcp_put_batch(struct bpf_tcp_iter_state *iter)
>>>>>>>    {
>>>>>>> -	while (iter->cur_sk < iter->end_sk)
>>>>>>> -		sock_gen_put(iter->batch[iter->cur_sk++]);
>>>>>>> +	unsigned int cur_sk = iter->cur_sk;
>>>>>>> +
>>>>>>> +	while (cur_sk < iter->end_sk)
>>>>>>> +		sock_gen_put(iter->batch[cur_sk++]);
>>>>>>
>>>>>> Why is this chunk included in this patch ?
>>>>>
>>>>> This should be in patch 5 to keep cur_sk for find_cookie
>>>>
>>>> Without this, iter->cur_sk is mutated when iteration stops, and we lose
>>>> our place. When iteration resumes and we call bpf_iter_tcp_batch the
>>>> iter->cur_sk == iter->end_sk condition will always be true, so we will
>>>> skip to the next bucket without seeking to the offset.
>>>>
>>>> Before, we relied on st_bucket_done to tell us if we had remaining items
>>>> in the current bucket to process but now need to preserve iter->cur_sk
>>>> through iterations to make the behavior equivalent to what we had before.
>>>
>>> Thanks for explanation, I was confused by calling tcp_seek_last_pos()
>>> multiple times, and I think we need to preserve/restore st->offset too
>>> in patch 2 and need this change.
>>>
>>> diff --git a/net/ipv4/tcp_ipv4.c b/net/ipv4/tcp_ipv4.c
>>> index ac00015d5e7a..0816f20bfdff 100644
>>> --- a/net/ipv4/tcp_ipv4.c
>>> +++ b/net/ipv4/tcp_ipv4.c
>>> @@ -2791,6 +2791,7 @@ static void *tcp_seek_last_pos(struct seq_file *seq)
>>>    			break;
>>>    		st->bucket = 0;
>>>    		st->state = TCP_SEQ_STATE_ESTABLISHED;
>>> +		offset = 0;
>>
>> This seems like an existing bug not necessarily related to this set.
> 
> Agree that this is more of an existing bug.
> 
>> The patch 5 has also removed the tcp_seek_last_pos() dependency, so I think
>> it can be a standalone fix on its own.
> 
> With the tcp_seq_* ops there are also other corner cases that can lead
> to skips, since they rely on st->offset to seek to the last position.
> 
> In the scenario described above, sockets disappearing from the last lhash
> bucket leads to skipped sockets in the first ehash bucket, but you could
> also have a scenario where, for example, the current lhash bucket has 6
> sockets, iter->offset is currently 3, 3 sockets disappear from the start
> of the current lhash bucket then tcp_seek_last_pos skips the remaining 3
> sockets and goes to the next bucket.
> 
> I'm not sure it's worth fixing just this one case without also
> overhauling the tcp_seq_* logic to prevent these other cases. Otherwise,
> it seems more like a Band-aid fix. Perhaps a later series could explore
> a more comprehensive solution there.

It is arguable that the missing "offset = 0;" here is a programmer’s error 
rather than the limitation of the offset approach itself. Adding it could be a 
quick fix for this corner case.

That said, it is a very rare case, given there is a "while (... && bucket == 
st->bucket)" condition, and the bug has probably existed since 2010 in commit 
a8b690f98baf. If there is a plan for a long-term fix in /proc/net/tcp[6], I 
think it is reasonable to wait also. I do not have a strong opinion either way. 
I am just unsure if any users care about the skip improvement in /proc/net/tcp[6].


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

* Re: [PATCH v1 bpf-next 10/10] selftests/bpf: Add tests for bucket resume logic in established sockets
  2025-05-20 14:50 ` [PATCH v1 bpf-next 10/10] selftests/bpf: Add tests for bucket resume logic in established sockets Jordan Rife
@ 2025-05-28  0:51   ` Martin KaFai Lau
  2025-05-28 18:32     ` Jordan Rife
  0 siblings, 1 reply; 25+ messages in thread
From: Martin KaFai Lau @ 2025-05-28  0:51 UTC (permalink / raw)
  To: Jordan Rife
  Cc: Daniel Borkmann, Willem de Bruijn, Kuniyuki Iwashima,
	Alexei Starovoitov, netdev, bpf

On 5/20/25 7:50 AM, Jordan Rife wrote:
> +static bool close_and_wait(int fd, struct bpf_link *link)
> +{
> +	static const int us_per_ms = 1000;
> +	__u64 cookie = socket_cookie(fd);
> +	struct iter_out out;
> +	bool exists = true;
> +	int iter_fd, nread;
> +	int waits = 20; /* 2 seconds */
> +
> +	close(fd);
> +
> +	/* Wait for socket to disappear from the ehash table. */
> +	while (waits--) {
> +		iter_fd = bpf_iter_create(bpf_link__fd(link));
> +		if (!ASSERT_OK_FD(iter_fd, "bpf_iter_create"))
> +			return false;
> +
> +		/* Is it still there? */
> +		do {
> +			nread = read(iter_fd, &out, sizeof(out));
> +			if (!ASSERT_GE(nread, 0, "nread")) {
> +				close(iter_fd);
> +				return false;
> +			}
> +			exists = nread && cookie == out.cookie;
> +		} while (!exists && nread);
> +
> +		close(iter_fd);
> +
> +		if (!exists)
> +			break;
> +
> +		usleep(100 * us_per_ms);

Instead of retrying with the bpf_iter_tcp to confirm the sk is gone from the 
ehash table, I think the bpf_sock_destroy() can help here.

> +	}
> +
> +	return !exists;
> +}
> +
>   static int get_seen_count(int fd, struct sock_count counts[], int n)
>   {
>   	__u64 cookie = socket_cookie(fd);
> @@ -241,6 +279,43 @@ static void remove_seen(int family, int sock_type, const char *addr, __u16 port,
>   			       counts_len);
>   }
>   
> +static void remove_seen_established(int family, int sock_type, const char *addr,
> +				    __u16 port, int *listen_socks,
> +				    int listen_socks_len, int *established_socks,
> +				    int established_socks_len,
> +				    struct sock_count *counts, int counts_len,
> +				    struct bpf_link *link, int iter_fd)
> +{
> +	int close_idx;
> +
> +	/* Iterate through all listening sockets. */
> +	read_n(iter_fd, listen_socks_len, counts, counts_len);
> +
> +	/* Make sure we saw all listening sockets exactly once. */
> +	check_n_were_seen_once(listen_socks, listen_socks_len, listen_socks_len,
> +			       counts, counts_len);
> +
> +	/* Leave one established socket. */
> +	read_n(iter_fd, established_socks_len - 1, counts, counts_len);

hmm... In the "SEC("iter/tcp") int iter_tcp_soreuse(...)" bpf prog, there is a 
"sk->sk_state != TCP_LISTEN" check and the established sk should have been 
skipped. Does it have an existing bug? I suspect it is missing a "()" around
"sk->sk_family == AF_INET6 ? !ipv6_addr_loopback(...) : ...".


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

* Re: [PATCH v1 bpf-next 10/10] selftests/bpf: Add tests for bucket resume logic in established sockets
  2025-05-28  0:51   ` Martin KaFai Lau
@ 2025-05-28 18:32     ` Jordan Rife
  0 siblings, 0 replies; 25+ messages in thread
From: Jordan Rife @ 2025-05-28 18:32 UTC (permalink / raw)
  To: Martin KaFai Lau
  Cc: Daniel Borkmann, Willem de Bruijn, Kuniyuki Iwashima,
	Alexei Starovoitov, netdev, bpf

> > +
> > +		close(iter_fd);
> > +
> > +		if (!exists)
> > +			break;
> > +
> > +		usleep(100 * us_per_ms);
> 
> Instead of retrying with the bpf_iter_tcp to confirm the sk is gone from the
> ehash table, I think the bpf_sock_destroy() can help here.

Sure, I will explore this. I was a little worried about having a sleep
here, since it may introduce some flakiness into CI at some point, so
it would be good to have something more deterministic.

> 
> > +	}
> > +
> > +	return !exists;
> > +}
> > +
> >   static int get_seen_count(int fd, struct sock_count counts[], int n)
> >   {
> >   	__u64 cookie = socket_cookie(fd);
> > @@ -241,6 +279,43 @@ static void remove_seen(int family, int sock_type, const char *addr, __u16 port,
> >   			       counts_len);
> >   }
> > +static void remove_seen_established(int family, int sock_type, const char *addr,
> > +				    __u16 port, int *listen_socks,
> > +				    int listen_socks_len, int *established_socks,
> > +				    int established_socks_len,
> > +				    struct sock_count *counts, int counts_len,
> > +				    struct bpf_link *link, int iter_fd)
> > +{
> > +	int close_idx;
> > +
> > +	/* Iterate through all listening sockets. */
> > +	read_n(iter_fd, listen_socks_len, counts, counts_len);
> > +
> > +	/* Make sure we saw all listening sockets exactly once. */
> > +	check_n_were_seen_once(listen_socks, listen_socks_len, listen_socks_len,
> > +			       counts, counts_len);
> > +
> > +	/* Leave one established socket. */
> > +	read_n(iter_fd, established_socks_len - 1, counts, counts_len);
> 
> hmm... In the "SEC("iter/tcp") int iter_tcp_soreuse(...)" bpf prog, there is
> a "sk->sk_state != TCP_LISTEN" check and the established sk should have been
> skipped. Does it have an existing bug? I suspect it is missing a "()" around
> "sk->sk_family == AF_INET6 ? !ipv6_addr_loopback(...) : ...".

Agh, yeah it looks like it's the "()". Adding these around the ternary
operation leads to the expected result with all established sockets
being skipped. I will fix that in the next spin.

Jordan

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

end of thread, other threads:[~2025-05-28 18:32 UTC | newest]

Thread overview: 25+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-05-20 14:50 [PATCH v1 bpf-next 00/10] bpf: tcp: Exactly-once socket iteration Jordan Rife
2025-05-20 14:50 ` [PATCH v1 bpf-next 01/10] bpf: tcp: Make mem flags configurable through bpf_iter_tcp_realloc_batch Jordan Rife
2025-05-21 18:54   ` Kuniyuki Iwashima
2025-05-20 14:50 ` [PATCH v1 bpf-next 02/10] bpf: tcp: Make sure iter->batch always contains a full bucket snapshot Jordan Rife
2025-05-21 22:20   ` Kuniyuki Iwashima
2025-05-20 14:50 ` [PATCH v1 bpf-next 03/10] bpf: tcp: Get rid of st_bucket_done Jordan Rife
2025-05-21 22:57   ` Kuniyuki Iwashima
2025-05-21 23:17     ` Kuniyuki Iwashima
2025-05-22 18:16       ` Jordan Rife
2025-05-22 20:42         ` Kuniyuki Iwashima
2025-05-23 22:07           ` Martin KaFai Lau
2025-05-24 21:09             ` Jordan Rife
2025-05-27 18:19               ` Martin KaFai Lau
2025-05-20 14:50 ` [PATCH v1 bpf-next 04/10] bpf: tcp: Use bpf_tcp_iter_batch_item for bpf_tcp_iter_state batch items Jordan Rife
2025-05-21 22:59   ` Kuniyuki Iwashima
2025-05-20 14:50 ` [PATCH v1 bpf-next 05/10] bpf: tcp: Avoid socket skips and repeats during iteration Jordan Rife
2025-05-23 23:05   ` Martin KaFai Lau
2025-05-24  1:24     ` Jordan Rife
2025-05-20 14:50 ` [PATCH v1 bpf-next 06/10] selftests/bpf: Add tests for bucket resume logic in listening sockets Jordan Rife
2025-05-20 14:50 ` [PATCH v1 bpf-next 07/10] selftests/bpf: Allow for iteration over multiple ports Jordan Rife
2025-05-20 14:50 ` [PATCH v1 bpf-next 08/10] selftests/bpf: Make ehash buckets configurable in socket iterator tests Jordan Rife
2025-05-20 14:50 ` [PATCH v1 bpf-next 09/10] selftests/bpf: Create established sockets " Jordan Rife
2025-05-20 14:50 ` [PATCH v1 bpf-next 10/10] selftests/bpf: Add tests for bucket resume logic in established sockets Jordan Rife
2025-05-28  0:51   ` Martin KaFai Lau
2025-05-28 18:32     ` Jordan Rife

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).