BPF List
 help / color / mirror / Atom feed
* [RFC PATCH bpf-next v1 0/6] Concurrency Testing
@ 2026-02-11 18:12 Kumar Kartikeya Dwivedi
  2026-02-11 18:12 ` [RFC PATCH bpf-next v1 1/6] bpf: Support repeat, duration fields for syscall prog runs Kumar Kartikeya Dwivedi
                   ` (5 more replies)
  0 siblings, 6 replies; 11+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-02-11 18:12 UTC (permalink / raw)
  To: bpf
  Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
	Martin KaFai Lau, Eduard Zingerman, Mykyta Yatsenko, kkd,
	kernel-team

This set introduces basic tooling to generate more exhaustive test cases
for task<->NMI reentrancy and parallel invocation of various operations
associated with rqspinlock, timer, wq, and task_work. For now, the
approach is simple: test cases are generated up to a maximum specified
level of parallelism for the operations to be invoked vertically against
each other (on the same CPU, in different contexts), and horizontally
(in parallel). To cut down test time, similar cases are pruned out after
generation (e.g. start vs cancel and cancel vs start in parallel on two
CPUs are similar, thus only one combination should be tested).

More details are in the commit log.

Kumar Kartikeya Dwivedi (6):
  bpf: Support repeat, duration fields for syscall prog runs
  bpf: Allow timing functions in lock critical sections
  bpf: Enable rqspinlock in tracing progs
  selftests/bpf: Introduce concurrency testing tool
  selftests/bpf: Generate various conctest permutations
  selftests/bpf: Extend conctest to wq and task_work

 kernel/bpf/verifier.c                        |    9 +-
 net/bpf/test_run.c                           |   92 +-
 tools/testing/selftests/bpf/Makefile         |    6 +
 tools/testing/selftests/bpf/conctest.c       | 1110 ++++++++++++++++++
 tools/testing/selftests/bpf/progs/conctest.c |  758 ++++++++++++
 5 files changed, 1955 insertions(+), 20 deletions(-)
 create mode 100644 tools/testing/selftests/bpf/conctest.c
 create mode 100644 tools/testing/selftests/bpf/progs/conctest.c


base-commit: dc855b77719fe452d670cae2cf64da1eb51f16cc
-- 
2.47.3


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

* [RFC PATCH bpf-next v1 1/6] bpf: Support repeat, duration fields for syscall prog runs
  2026-02-11 18:12 [RFC PATCH bpf-next v1 0/6] Concurrency Testing Kumar Kartikeya Dwivedi
@ 2026-02-11 18:12 ` Kumar Kartikeya Dwivedi
  2026-02-11 22:02   ` Alexei Starovoitov
  2026-02-11 18:12 ` [RFC PATCH bpf-next v1 2/6] bpf: Allow timing functions in lock critical sections Kumar Kartikeya Dwivedi
                   ` (4 subsequent siblings)
  5 siblings, 1 reply; 11+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-02-11 18:12 UTC (permalink / raw)
  To: bpf
  Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
	Martin KaFai Lau, Eduard Zingerman, Mykyta Yatsenko, kkd,
	kernel-team

Currently, BPF syscall programs do not support specifying the repeat
field to repeat the test inside the kernel multiple times. Use the
test_timer infra for other prog run tests and make it with RCU tasks
trace to allow usage for syscall programs. Also make the duration field
available for use.

Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
 net/bpf/test_run.c | 92 +++++++++++++++++++++++++++++++++++++---------
 1 file changed, 74 insertions(+), 18 deletions(-)

diff --git a/net/bpf/test_run.c b/net/bpf/test_run.c
index 178c4738e63b..4e06e516d9f2 100644
--- a/net/bpf/test_run.c
+++ b/net/bpf/test_run.c
@@ -33,23 +33,27 @@ struct bpf_test_timer {
 	u64 time_start, time_spent;
 };
 
-static void bpf_test_timer_enter(struct bpf_test_timer *t)
-	__acquires(rcu)
+static void __bpf_test_timer_enter(struct bpf_test_timer *t, bool trace)
 {
-	rcu_read_lock_dont_migrate();
+	if (trace)
+		rcu_read_lock_trace();
+	else
+		rcu_read_lock_dont_migrate();
 	t->time_start = ktime_get_ns();
 }
 
-static void bpf_test_timer_leave(struct bpf_test_timer *t)
-	__releases(rcu)
+static void __bpf_test_timer_leave(struct bpf_test_timer *t, bool trace)
 {
 	t->time_start = 0;
-	rcu_read_unlock_migrate();
+	if (trace)
+		rcu_read_unlock_trace();
+	else
+		rcu_read_unlock_migrate();
 }
 
-static bool bpf_test_timer_continue(struct bpf_test_timer *t, int iterations,
-				    u32 repeat, int *err, u32 *duration)
-	__must_hold(rcu)
+static bool __bpf_test_timer_continue(struct bpf_test_timer *t,
+				      int iterations, u32 repeat,
+				      int *err, u32 *duration, bool trace)
 {
 	t->i += iterations;
 	if (t->i >= repeat) {
@@ -70,9 +74,9 @@ static bool bpf_test_timer_continue(struct bpf_test_timer *t, int iterations,
 	if (need_resched()) {
 		/* During iteration: we need to reschedule between runs. */
 		t->time_spent += ktime_get_ns() - t->time_start;
-		bpf_test_timer_leave(t);
+		__bpf_test_timer_leave(t, trace);
 		cond_resched();
-		bpf_test_timer_enter(t);
+		__bpf_test_timer_enter(t, trace);
 	}
 
 	/* Do another round. */
@@ -83,6 +87,45 @@ static bool bpf_test_timer_continue(struct bpf_test_timer *t, int iterations,
 	return false;
 }
 
+static void bpf_test_timer_enter(struct bpf_test_timer *t)
+	__acquires(rcu)
+{
+	__bpf_test_timer_enter(t, false);
+}
+
+static void bpf_test_timer_leave(struct bpf_test_timer *t)
+	__releases(rcu)
+{
+	__bpf_test_timer_leave(t, false);
+}
+
+static bool bpf_test_timer_continue(struct bpf_test_timer *t, int iterations,
+				    u32 repeat, int *err, u32 *duration)
+	__must_hold(rcu)
+{
+	return __bpf_test_timer_continue(t, iterations, repeat, err, duration, false);
+}
+
+static void bpf_test_timer_enter_trace(struct bpf_test_timer *t)
+	__acquires(rcu_trace)
+{
+	__bpf_test_timer_enter(t, true);
+}
+
+static void bpf_test_timer_leave_trace(struct bpf_test_timer *t)
+	__releases(rcu_trace)
+{
+	__bpf_test_timer_leave(t, true);
+}
+
+static bool bpf_test_timer_continue_trace(struct bpf_test_timer *t,
+					  int iterations, u32 repeat,
+					  int *err, u32 *duration)
+	__must_hold(rcu_trace)
+{
+	return __bpf_test_timer_continue(t, iterations, repeat, err, duration, true);
+}
+
 /* We put this struct at the head of each page with a context and frame
  * initialised when the page is allocated, so we don't have to do this on each
  * repetition of the test run.
@@ -1615,14 +1658,14 @@ int bpf_prog_test_run_syscall(struct bpf_prog *prog,
 {
 	void __user *ctx_in = u64_to_user_ptr(kattr->test.ctx_in);
 	__u32 ctx_size_in = kattr->test.ctx_size_in;
+	struct bpf_test_timer t = {};
+	u32 retval, duration, repeat;
 	void *ctx = NULL;
-	u32 retval;
 	int err = 0;
 
-	/* doesn't support data_in/out, ctx_out, duration, or repeat or flags */
+	/* doesn't support data_in/out, ctx_out, or flags */
 	if (kattr->test.data_in || kattr->test.data_out ||
-	    kattr->test.ctx_out || kattr->test.duration ||
-	    kattr->test.repeat || kattr->test.flags ||
+	    kattr->test.ctx_out || kattr->test.flags ||
 	    kattr->test.batch_size)
 		return -EINVAL;
 
@@ -1636,14 +1679,27 @@ int bpf_prog_test_run_syscall(struct bpf_prog *prog,
 			return PTR_ERR(ctx);
 	}
 
-	rcu_read_lock_trace();
-	retval = bpf_prog_run_pin_on_cpu(prog, ctx);
-	rcu_read_unlock_trace();
+	repeat = kattr->test.repeat;
+	if (!repeat)
+		repeat = 1;
+
+	bpf_test_timer_enter_trace(&t);
+	do {
+		retval = bpf_prog_run_pin_on_cpu(prog, ctx);
+	} while (bpf_test_timer_continue_trace(&t, 1, repeat, &err, &duration));
+	bpf_test_timer_leave_trace(&t);
+
+	if (err)
+		goto out;
 
 	if (copy_to_user(&uattr->test.retval, &retval, sizeof(u32))) {
 		err = -EFAULT;
 		goto out;
 	}
+	if (copy_to_user(&uattr->test.duration, &duration, sizeof(duration))) {
+		err = -EFAULT;
+		goto out;
+	}
 	if (ctx_size_in)
 		if (copy_to_user(ctx_in, ctx, ctx_size_in))
 			err = -EFAULT;
-- 
2.47.3


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

* [RFC PATCH bpf-next v1 2/6] bpf: Allow timing functions in lock critical sections
  2026-02-11 18:12 [RFC PATCH bpf-next v1 0/6] Concurrency Testing Kumar Kartikeya Dwivedi
  2026-02-11 18:12 ` [RFC PATCH bpf-next v1 1/6] bpf: Support repeat, duration fields for syscall prog runs Kumar Kartikeya Dwivedi
@ 2026-02-11 18:12 ` Kumar Kartikeya Dwivedi
  2026-02-11 18:12 ` [RFC PATCH bpf-next v1 3/6] bpf: Enable rqspinlock in tracing progs Kumar Kartikeya Dwivedi
                   ` (3 subsequent siblings)
  5 siblings, 0 replies; 11+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-02-11 18:12 UTC (permalink / raw)
  To: bpf
  Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
	Martin KaFai Lau, Eduard Zingerman, Mykyta Yatsenko, kkd,
	kernel-team

Allow timing related functions in critical sections, which is useful to
measure the time spent acquiring a lock inside BPF programs.

Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
 kernel/bpf/verifier.c | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index edf5342b982f..4acd8e848c88 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -21044,7 +21044,11 @@ static int do_check_insn(struct bpf_verifier_env *env, bool *do_print_state)
 
 			if (env->cur_state->active_locks) {
 				if ((insn->src_reg == BPF_REG_0 &&
-				     insn->imm != BPF_FUNC_spin_unlock) ||
+				     insn->imm != BPF_FUNC_spin_unlock &&
+				     insn->imm != BPF_FUNC_ktime_get_ns &&
+				     insn->imm != BPF_FUNC_ktime_get_boot_ns &&
+				     insn->imm != BPF_FUNC_ktime_get_coarse_ns &&
+				     insn->imm != BPF_FUNC_ktime_get_tai_ns) ||
 				    (insn->src_reg == BPF_PSEUDO_KFUNC_CALL &&
 				     (insn->off != 0 || !kfunc_spin_allowed(insn->imm)))) {
 					verbose(env,
-- 
2.47.3


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

* [RFC PATCH bpf-next v1 3/6] bpf: Enable rqspinlock in tracing progs
  2026-02-11 18:12 [RFC PATCH bpf-next v1 0/6] Concurrency Testing Kumar Kartikeya Dwivedi
  2026-02-11 18:12 ` [RFC PATCH bpf-next v1 1/6] bpf: Support repeat, duration fields for syscall prog runs Kumar Kartikeya Dwivedi
  2026-02-11 18:12 ` [RFC PATCH bpf-next v1 2/6] bpf: Allow timing functions in lock critical sections Kumar Kartikeya Dwivedi
@ 2026-02-11 18:12 ` Kumar Kartikeya Dwivedi
  2026-02-11 22:04   ` Alexei Starovoitov
  2026-02-11 18:12 ` [RFC PATCH bpf-next v1 4/6] selftests/bpf: Introduce concurrency testing tool Kumar Kartikeya Dwivedi
                   ` (2 subsequent siblings)
  5 siblings, 1 reply; 11+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-02-11 18:12 UTC (permalink / raw)
  To: bpf
  Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
	Martin KaFai Lau, Eduard Zingerman, Mykyta Yatsenko, kkd,
	kernel-team

Allow using rqspinlock in tracing programs, since it is safe to acquire
from any context. This permits its stress testing from any context using
BPF programs.

Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
 kernel/bpf/verifier.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 4acd8e848c88..fee92ff348a9 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -21542,7 +21542,8 @@ static int check_map_prog_compatibility(struct bpf_verifier_env *env,
 			return -EINVAL;
 		}
 
-		if (is_tracing_prog_type(prog_type)) {
+		if (is_tracing_prog_type(prog_type) &&
+		    btf_record_has_field(map->record, BPF_SPIN_LOCK)) {
 			verbose(env, "tracing progs cannot use bpf_spin_lock yet\n");
 			return -EINVAL;
 		}
-- 
2.47.3


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

* [RFC PATCH bpf-next v1 4/6] selftests/bpf: Introduce concurrency testing tool
  2026-02-11 18:12 [RFC PATCH bpf-next v1 0/6] Concurrency Testing Kumar Kartikeya Dwivedi
                   ` (2 preceding siblings ...)
  2026-02-11 18:12 ` [RFC PATCH bpf-next v1 3/6] bpf: Enable rqspinlock in tracing progs Kumar Kartikeya Dwivedi
@ 2026-02-11 18:12 ` Kumar Kartikeya Dwivedi
  2026-02-11 22:08   ` Alexei Starovoitov
  2026-02-11 18:12 ` [RFC PATCH bpf-next v1 5/6] selftests/bpf: Generate various conctest permutations Kumar Kartikeya Dwivedi
  2026-02-11 18:12 ` [RFC PATCH bpf-next v1 6/6] selftests/bpf: Extend conctest to wq and task_work Kumar Kartikeya Dwivedi
  5 siblings, 1 reply; 11+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-02-11 18:12 UTC (permalink / raw)
  To: bpf
  Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
	Martin KaFai Lau, Eduard Zingerman, Mykyta Yatsenko, kkd,
	kernel-team

Introduce a scaffolding for concurrency testing tool with rqspinlock as
the first example. The main idea is to set up various permutations of
context reentrancy and parallel invocations to allow more exhaustive
testing of various concurrency-related primitives. Each operation also
supports specification of expected values (e.g. ABBA should always
-EDEADLK, never -ETIMEDOUT) and threshold values on maximum expected
delay. In case violations occur, the tests will fail and a histogram
with timing distribution will be printed. The timing information is
useful in general to observe various statistics of operation latency
from different contexts to notice potential anomalies.

Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
 tools/testing/selftests/bpf/Makefile         |   6 +
 tools/testing/selftests/bpf/conctest.c       | 889 +++++++++++++++++++
 tools/testing/selftests/bpf/progs/conctest.c | 289 ++++++
 3 files changed, 1184 insertions(+)
 create mode 100644 tools/testing/selftests/bpf/conctest.c
 create mode 100644 tools/testing/selftests/bpf/progs/conctest.c

diff --git a/tools/testing/selftests/bpf/Makefile b/tools/testing/selftests/bpf/Makefile
index c6bf4dfb1495..2e225d19f248 100644
--- a/tools/testing/selftests/bpf/Makefile
+++ b/tools/testing/selftests/bpf/Makefile
@@ -123,6 +123,7 @@ TEST_KMOD_TARGETS = $(addprefix $(OUTPUT)/,$(TEST_KMODS))
 # Compile but not part of 'make run_tests'
 TEST_GEN_PROGS_EXTENDED = \
 	bench \
+	conctest \
 	flow_dissector_load \
 	test_cpp \
 	test_lirc_mode2_user \
@@ -504,6 +505,7 @@ linked_maps.skel.h-deps := linked_maps1.bpf.o linked_maps2.bpf.o
 test_subskeleton.skel.h-deps := test_subskeleton_lib2.bpf.o test_subskeleton_lib.bpf.o test_subskeleton.bpf.o
 test_subskeleton_lib.skel.h-deps := test_subskeleton_lib2.bpf.o test_subskeleton_lib.bpf.o
 test_usdt.skel.h-deps := test_usdt.bpf.o test_usdt_multispec.bpf.o
+conctest.skel.h-deps := conctest.bpf.o
 xsk_xdp_progs.skel.h-deps := xsk_xdp_progs.bpf.o
 xdp_hw_metadata.skel.h-deps := xdp_hw_metadata.bpf.o
 xdp_features.skel.h-deps := xdp_features.bpf.o
@@ -823,6 +825,10 @@ $(OUTPUT)/xdp_features: xdp_features.c $(OUTPUT)/network_helpers.o $(OUTPUT)/xdp
 	$(call msg,BINARY,,$@)
 	$(Q)$(CC) $(CFLAGS) $(filter %.a %.o %.c,$^) $(LDLIBS) -o $@
 
+$(OUTPUT)/conctest: conctest.c $(OUTPUT)/conctest.skel.h $(BPFOBJ) | $(OUTPUT)
+	$(call msg,BINARY,,$@)
+	$(Q)$(CC) $(CFLAGS) $(filter %.a %.o %.c,$^) $(LDLIBS) -lm -o $@
+
 # Make sure we are able to include and link libbpf against c++.
 CXXFLAGS += $(CFLAGS)
 CXXFLAGS := $(subst -D_GNU_SOURCE=,,$(CXXFLAGS))
diff --git a/tools/testing/selftests/bpf/conctest.c b/tools/testing/selftests/bpf/conctest.c
new file mode 100644
index 000000000000..e4b333aff419
--- /dev/null
+++ b/tools/testing/selftests/bpf/conctest.c
@@ -0,0 +1,889 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <pthread.h>
+#include <sched.h>
+#include <getopt.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <math.h>
+#include <time.h>
+#include <linux/perf_event.h>
+#include <sys/syscall.h>
+
+#include <bpf/bpf.h>
+#include <bpf/libbpf.h>
+#include "conctest.skel.h"
+
+#define MAX_FILTERS 64
+#define MAX_ATTACH_LINKS 1024
+#define NMI_SAMPLE_PERIOD 1000
+#define DEFAULT_DURATION 5
+#define DEFAULT_NR_CPUS 1
+
+#define CONCTEST_HIST_BUCKETS 28
+#define EDEADLK 35
+#define ETIMEDOUT 110
+#define HIST_BAR_WIDTH 40
+
+static int nmi_sample_period = NMI_SAMPLE_PERIOD;
+static volatile int stop;
+static int verbose;
+static __u32 delay_seed;
+static __u32 delay_max_us = 10;
+static __u32 delay_us;
+
+enum conctest_stat_id {
+	CONCTEST_STAT_SYSCALL = 0,
+	CONCTEST_STAT_NMI,
+	CONCTEST_STAT_MAX,
+};
+
+static const char *stat_names[CONCTEST_STAT_MAX] = {
+	[CONCTEST_STAT_SYSCALL] = "syscall",
+	[CONCTEST_STAT_NMI] = "nmi",
+};
+
+struct conctest_op_stats {
+	__u64 count;
+	__u64 success;
+	__u64 failure;
+	__u64 unexpected;
+	__u64 total_ns;
+	__u64 min_ns;
+	__u64 max_ns;
+	__u64 hist[CONCTEST_HIST_BUCKETS];
+};
+
+struct conctest_op_state {
+	__s64 expect_ret;
+	__u64 delay_thresh_ns;
+	__u64 failed_print_once;
+	__u64 delay_print_once;
+};
+
+/* Upper boundary (exclusive) of each histogram bucket, in ns */
+static const __u64 hist_upper[CONCTEST_HIST_BUCKETS] = {
+	1000, /* [0, 1us) */
+	100000, /* [1us, 100us) */
+	200000,	    300000,	400000,	   500000,    600000,
+	700000,	    800000,	900000,	   1000000, /* [900us, 1ms) */
+	10000000, /* [1ms, 10ms) */
+	20000000,   30000000,	40000000,  50000000,  60000000,
+	70000000,   80000000,	90000000,  100000000, /* [90ms, 100ms) */
+	150000000,  200000000,	250000000, 500000000, 750000000,
+	1000000000, UINT64_MAX, /* [1s, inf) */
+};
+
+static void format_ns(char *buf, size_t sz, __u64 ns)
+{
+	if (ns < 1000)
+		snprintf(buf, sz, "%lluns", (unsigned long long)ns);
+	else if (ns < 1000000)
+		snprintf(buf, sz, "%.1fus", ns / 1000.0);
+	else if (ns < 1000000000)
+		snprintf(buf, sz, "%.1fms", ns / 1000000.0);
+	else
+		snprintf(buf, sz, "%.2fs", ns / 1000000000.0);
+}
+
+static void format_bucket_range(char *buf, size_t sz, int i)
+{
+	__u64 lo = i > 0 ? hist_upper[i - 1] : 0;
+	__u64 hi = hist_upper[i];
+	char lo_s[32], hi_s[32];
+
+	format_ns(lo_s, sizeof(lo_s), lo);
+	if (hi == UINT64_MAX)
+		snprintf(buf, sz, "[%7s, inf)", lo_s);
+	else {
+		format_ns(hi_s, sizeof(hi_s), hi);
+		snprintf(buf, sz, "[%7s, %7s)", lo_s, hi_s);
+	}
+}
+
+static __u64 estimate_median(struct conctest_op_stats *s)
+{
+	__u64 half = s->count / 2;
+	__u64 cumulative = 0;
+	int i;
+
+	for (i = 0; i < CONCTEST_HIST_BUCKETS; i++) {
+		cumulative += s->hist[i];
+		if (cumulative >= half) {
+			__u64 lo = i > 0 ? hist_upper[i - 1] : 0;
+			__u64 hi = hist_upper[i];
+
+			if (hi == UINT64_MAX)
+				return lo;
+			return (lo + hi) / 2;
+		}
+	}
+	return 0;
+}
+
+static __u64 estimate_stddev(struct conctest_op_stats *s)
+{
+	double mean, sum_sq = 0;
+	int i;
+
+	if (s->count < 2)
+		return 0;
+
+	mean = (double)s->total_ns / s->count;
+
+	for (i = 0; i < CONCTEST_HIST_BUCKETS; i++) {
+		double lo, hi, mid, diff;
+
+		if (!s->hist[i])
+			continue;
+
+		lo = i > 0 ? (double)hist_upper[i - 1] : 0;
+		hi = hist_upper[i] == UINT64_MAX ? lo : (double)hist_upper[i];
+		mid = (lo + hi) / 2;
+		diff = mid - mean;
+		sum_sq += s->hist[i] * diff * diff;
+	}
+
+	return (__u64)sqrt(sum_sq / s->count);
+}
+
+static void print_histogram(struct conctest_op_stats *s)
+{
+	int i, j, len, last_printed = -1;
+	__u64 max_count = 0;
+
+	for (i = 0; i < CONCTEST_HIST_BUCKETS; i++)
+		if (s->hist[i] > max_count)
+			max_count = s->hist[i];
+
+	if (!max_count)
+		return;
+
+	printf("  Histogram:\n");
+	for (i = 0; i < CONCTEST_HIST_BUCKETS; i++) {
+		char range[48];
+
+		if (!s->hist[i])
+			continue;
+
+		if (last_printed >= 0 && i > last_printed + 1)
+			printf("           ...\n");
+		last_printed = i;
+
+		format_bucket_range(range, sizeof(range), i);
+		printf("    %s %8llu |", range, (unsigned long long)s->hist[i]);
+
+		len = max_count ? (int)(s->hist[i] * HIST_BAR_WIDTH / max_count) : 0;
+		for (j = 0; j < len; j++)
+			putchar('#');
+		printf("\n");
+	}
+}
+
+static int dump_test_stats(struct conctest *skel)
+{
+	char avg_str[32], min_str[32], max_str[32], med_str[32], stddev_str[32], thresh_str[32];
+	int ncpus = libbpf_num_possible_cpus();
+	struct conctest_op_stats *per_cpu;
+	struct conctest_op_state state;
+	struct conctest_op_stats agg;
+	int has_error = 0;
+	int id, cpu, b;
+
+	per_cpu = calloc(ncpus, sizeof(*per_cpu));
+	if (!per_cpu)
+		return -ENOMEM;
+
+	for (id = 0; id < CONCTEST_STAT_MAX; id++) {
+		int has_min = 0;
+		int key = id;
+
+		if (bpf_map__lookup_elem(skel->maps.perf_map, &key, sizeof(key),
+					 per_cpu, ncpus * sizeof(*per_cpu), 0))
+			continue;
+
+		memset(&agg, 0, sizeof(agg));
+		for (cpu = 0; cpu < ncpus; cpu++) {
+			struct conctest_op_stats *p = &per_cpu[cpu];
+
+			agg.count += p->count;
+			agg.success += p->success;
+			agg.failure += p->failure;
+			agg.unexpected += p->unexpected;
+			agg.total_ns += p->total_ns;
+
+			if (p->count > 0) {
+				if (!has_min || p->min_ns < agg.min_ns) {
+					agg.min_ns = p->min_ns;
+					has_min = 1;
+				}
+			}
+			if (p->max_ns > agg.max_ns)
+				agg.max_ns = p->max_ns;
+
+			for (b = 0; b < CONCTEST_HIST_BUCKETS; b++)
+				agg.hist[b] += p->hist[b];
+		}
+
+		if (!agg.count)
+			continue;
+
+		if (bpf_map__lookup_elem(skel->maps.state_map, &key,
+					 sizeof(key), &state, sizeof(state), 0))
+			memset(&state, 0, sizeof(state));
+
+		format_ns(avg_str, sizeof(avg_str), agg.total_ns / agg.count);
+		format_ns(min_str, sizeof(min_str), agg.min_ns);
+		format_ns(max_str, sizeof(max_str), agg.max_ns);
+		format_ns(med_str, sizeof(med_str), estimate_median(&agg));
+		format_ns(stddev_str, sizeof(stddev_str), estimate_stddev(&agg));
+
+		if (agg.unexpected || state.delay_print_once)
+			has_error = 1;
+
+		if (!verbose && !has_error)
+			continue;
+		printf("=== Stats: %s ===\n",
+		       stat_names[id] ? stat_names[id] : "unknown");
+		printf("  Count: %lu (success: %lu, failure: %lu, unexpected: %lu)\n",
+		       (unsigned long)agg.count,
+		       (unsigned long)agg.success,
+		       (unsigned long)agg.failure,
+		       (unsigned long)agg.unexpected);
+		printf("  Timing: avg=%s min=%s max=%s median=~%s stddev=~%s\n",
+		       avg_str, min_str, max_str, med_str, stddev_str);
+		if (state.delay_thresh_ns) {
+			format_ns(thresh_str, sizeof(thresh_str), state.delay_thresh_ns);
+			printf("  Delay threshold: %s (exceeded: %s)\n", thresh_str,
+			       state.delay_print_once ? "YES" : "no");
+		}
+		print_histogram(&agg);
+	}
+
+	free(per_cpu);
+	return has_error ? -1 : 0;
+}
+
+enum conctest_cfg_type {
+	CT_INVALID = 0,
+	CT_TASK_PROG,
+	CT_NMI_PROG,
+	CT_ATTACH_PROG,
+};
+
+struct conctest_cfg {
+	enum conctest_cfg_type type;
+	union {
+		struct {
+			const char *prog_name;
+		} task;
+		struct {
+			const char *prog_name;
+		} nmi;
+		struct {
+			struct bpf_program *(*get_prog)(struct conctest *skel,
+							struct conctest_cfg *cfg,
+							bool loaded);
+			const char *tp_category;
+			const char *tp_name;
+		} attach;
+	};
+};
+
+#define for_each_conctest_cfg(cfg, arr) \
+	for (struct conctest_cfg *cfg = (arr); cfg->type != CT_INVALID; cfg++)
+
+struct conctest_test {
+	const char *name;
+	struct conctest_cfg *cfgs;
+	void (*init)(struct conctest *skel);
+	int nr_cpus;
+};
+
+struct conctest_cfg rqspinlock[] = {
+	{
+		.type = CT_TASK_PROG,
+		.task = { "conctest_rqspinlock_task" },
+	},
+	{
+		.type = CT_NMI_PROG,
+		.nmi = { "conctest_rqspinlock_nmi" },
+	},
+	{}
+};
+
+struct conctest_cfg rqspinlock_shift[] = {
+	{
+		.type = CT_TASK_PROG,
+		.task = { "conctest_rqspinlock_task" },
+	},
+	{
+		.type = CT_NMI_PROG,
+		.nmi = { "conctest_rqspinlock_nmi_shift" },
+	},
+	{}
+};
+
+static void __rqspinlock_init(struct conctest *skel, int err, int delay)
+{
+	struct conctest_op_state state;
+	int key;
+
+	key = CONCTEST_STAT_SYSCALL;
+	memset(&state, 0, sizeof(state));
+	state.expect_ret = 0;
+	state.delay_thresh_ns = 1000000 * delay;
+	bpf_map__update_elem(skel->maps.state_map, &key, sizeof(key), &state,
+			     sizeof(state), 0);
+
+	key = CONCTEST_STAT_NMI;
+	memset(&state, 0, sizeof(state));
+	state.expect_ret = err;
+	state.delay_thresh_ns = 1000000 * delay;
+	bpf_map__update_elem(skel->maps.state_map, &key, sizeof(key), &state,
+			     sizeof(state), 0);
+}
+
+static void rqspinlock_init(struct conctest *skel)
+{
+	return __rqspinlock_init(skel, -EDEADLK, 2);
+}
+
+static void rqspinlock_init_delay(struct conctest *skel)
+{
+	return __rqspinlock_init(skel, -EDEADLK, 10);
+}
+
+static void rqspinlock_init_timeout(struct conctest *skel)
+{
+	return __rqspinlock_init(skel, -ETIMEDOUT, 300);
+}
+
+static struct conctest_test all_tests[] = {
+	{
+		.name = "rqspinlock:AA",
+		.cfgs = rqspinlock,
+		.init = rqspinlock_init,
+		.nr_cpus = 1
+	},
+	{
+		.name = "rqspinlock:ABBA",
+		.cfgs = rqspinlock_shift,
+		.init = rqspinlock_init_delay,
+		.nr_cpus = 2
+	},
+	{
+		.name = "rqspinlock:ABBCBA",
+		.cfgs = rqspinlock_shift,
+		.init = rqspinlock_init_timeout,
+		.nr_cpus = 3
+	},
+	{},
+};
+
+struct task_ctx {
+	int prog_fd;
+	int cpu;
+};
+
+static int pin_to_cpu(int cpu)
+{
+	cpu_set_t cpuset;
+
+	CPU_ZERO(&cpuset);
+	CPU_SET(cpu, &cpuset);
+	return pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset);
+}
+
+static void *worker(void *arg)
+{
+	LIBBPF_OPTS(bpf_test_run_opts, opts, .repeat = 1000);
+	struct task_ctx *ctx = arg;
+
+	if (pin_to_cpu(ctx->cpu)) {
+		fprintf(stderr, "Failed to pin to CPU %d\n", ctx->cpu);
+		return NULL;
+	}
+
+	while (!stop)
+		bpf_prog_test_run_opts(ctx->prog_fd, &opts);
+
+	return NULL;
+}
+
+static int open_perf_event(int cpu)
+{
+	struct perf_event_attr attr = {
+		.type = PERF_TYPE_HARDWARE,
+		.config = PERF_COUNT_HW_CPU_CYCLES,
+		.size = sizeof(attr),
+		.sample_period = nmi_sample_period,
+		.pinned = 1,
+		.disabled = 0,
+	};
+
+	return syscall(__NR_perf_event_open, &attr, -1, cpu, -1, 0);
+}
+
+struct conctest_ctx {
+	pthread_t *threads;
+	struct task_ctx *task_ctxs;
+	int nr_threads;
+
+	int *nmi_link_fds;
+	int *nmi_pmu_fds;
+	int nr_nmis;
+
+	struct bpf_link **attach_links;
+	int nr_attach_links;
+	int max_attach_links;
+};
+
+static struct conctest_ctx *alloc_ctx(int nr_cpus, struct conctest_cfg *cfgs)
+{
+	int nr_task = 0, nr_nmi = 0, nr_attach = 0;
+	struct conctest_ctx *ctx;
+
+	for_each_conctest_cfg(cfg, cfgs) {
+		switch (cfg->type) {
+		case CT_TASK_PROG:
+			nr_task++;
+			break;
+		case CT_NMI_PROG:
+			nr_nmi++;
+			break;
+		case CT_ATTACH_PROG:
+			nr_attach++;
+			break;
+		default:
+			break;
+		}
+	}
+
+	if (nr_task > 1) {
+		fprintf(stderr, "Only one CT_TASK_PROG entry allowed\n");
+		return NULL;
+	}
+
+	if (nr_nmi > 1) {
+		fprintf(stderr, "Only one CT_NMI_PROG entry allowed\n");
+		return NULL;
+	}
+
+	if (nr_attach > MAX_ATTACH_LINKS) {
+		fprintf(stderr, "Too many attach links (%d > %d)\n", nr_attach,
+			MAX_ATTACH_LINKS);
+		return NULL;
+	}
+
+	ctx = calloc(1, sizeof(*ctx));
+	if (!ctx)
+		return NULL;
+
+	if (nr_task) {
+		ctx->threads = calloc(nr_cpus, sizeof(*ctx->threads));
+		ctx->task_ctxs = calloc(nr_cpus, sizeof(*ctx->task_ctxs));
+		if (!ctx->threads || !ctx->task_ctxs)
+			goto err;
+	}
+
+	if (nr_nmi) {
+		ctx->nmi_link_fds = malloc(nr_cpus * sizeof(*ctx->nmi_link_fds));
+		ctx->nmi_pmu_fds = malloc(nr_cpus * sizeof(*ctx->nmi_pmu_fds));
+		if (!ctx->nmi_link_fds || !ctx->nmi_pmu_fds)
+			goto err;
+		memset(ctx->nmi_link_fds, -1, nr_cpus * sizeof(*ctx->nmi_link_fds));
+		memset(ctx->nmi_pmu_fds, -1, nr_cpus * sizeof(*ctx->nmi_pmu_fds));
+	}
+
+	if (nr_attach) {
+		ctx->attach_links = calloc(nr_attach, sizeof(*ctx->attach_links));
+		if (!ctx->attach_links)
+			goto err;
+		ctx->max_attach_links = nr_attach;
+	}
+
+	return ctx;
+
+err:
+	free(ctx->threads);
+	free(ctx->task_ctxs);
+	free(ctx->nmi_link_fds);
+	free(ctx->nmi_pmu_fds);
+	free(ctx->attach_links);
+	free(ctx);
+	return NULL;
+}
+
+static void free_ctx(struct conctest_ctx *ctx)
+{
+	int i;
+
+	if (!ctx)
+		return;
+
+	for (i = 0; i < ctx->nr_nmis; i++) {
+		if (ctx->nmi_link_fds[i] >= 0)
+			close(ctx->nmi_link_fds[i]);
+		if (ctx->nmi_pmu_fds[i] >= 0)
+			close(ctx->nmi_pmu_fds[i]);
+	}
+
+	for (i = 0; i < ctx->nr_attach_links; i++)
+		bpf_link__destroy(ctx->attach_links[i]);
+
+	for (i = 0; i < ctx->nr_threads; i++)
+		pthread_join(ctx->threads[i], NULL);
+
+	free(ctx->threads);
+	free(ctx->task_ctxs);
+	free(ctx->nmi_link_fds);
+	free(ctx->nmi_pmu_fds);
+	free(ctx->attach_links);
+	free(ctx);
+}
+
+static void dump_prog_stdout(struct conctest *skel)
+{
+	LIBBPF_OPTS(bpf_prog_stream_read_opts, ropts);
+	struct bpf_program *prog;
+	char buf[4096];
+	int fd, ret;
+
+	bpf_object__for_each_program(prog, skel->obj) {
+		int printed_header = 0;
+
+		fd = bpf_program__fd(prog);
+		if (fd < 0)
+			continue;
+
+		for (;;) {
+			ret = bpf_prog_stream_read(fd, BPF_STREAM_STDOUT, buf,
+						   sizeof(buf) - 1, &ropts);
+			if (ret <= 0)
+				break;
+
+			if (!printed_header) {
+				printf("\n=== %s stdout: ===\n", bpf_program__name(prog));
+				printed_header = 1;
+			}
+
+			buf[ret] = '\0';
+			printf("%s", buf);
+		}
+	}
+}
+
+static int find_prog_fd(struct conctest *skel, const char *prog_name)
+{
+	struct bpf_program *prog;
+
+	prog = bpf_object__find_program_by_name(skel->obj, prog_name);
+	if (!prog)
+		return -1;
+	return bpf_program__fd(prog);
+}
+
+static int run_test(struct conctest_test *test, int nr_cpus, int duration)
+{
+	struct conctest_ctx *ctx;
+	struct conctest *skel;
+	int err = -1;
+
+	if (test->nr_cpus > 0)
+		nr_cpus = test->nr_cpus;
+
+	ctx = alloc_ctx(nr_cpus, test->cfgs);
+	if (!ctx)
+		return -ENOMEM;
+
+	printf("Running test '%s' on %d CPU(s) for %d seconds\n", test->name, nr_cpus, duration);
+
+	skel = conctest__open();
+	if (!skel) {
+		fprintf(stderr, "Failed to open BPF skeleton\n");
+		goto free;
+	}
+
+	for_each_conctest_cfg(cfg, test->cfgs) {
+		struct bpf_program *prog;
+		const char *name = NULL;
+
+		switch (cfg->type) {
+		case CT_TASK_PROG:
+			name = cfg->task.prog_name;
+			break;
+		case CT_NMI_PROG:
+			name = cfg->nmi.prog_name;
+			break;
+		case CT_ATTACH_PROG:
+			cfg->attach.get_prog(skel, cfg, false);
+			break;
+		default:
+			break;
+		}
+
+		if (name) {
+			prog = bpf_object__find_program_by_name(skel->obj, name);
+			if (!prog) {
+				fprintf(stderr, "Program '%s' not found\n", name);
+				goto out;
+			}
+			bpf_program__set_autoload(prog, true);
+		}
+	}
+
+	err = conctest__load(skel);
+	if (err) {
+		fprintf(stderr, "Failed to load BPF skeleton: %d\n", err);
+		goto out;
+	}
+
+	stop = 0;
+
+	if (test->init)
+		test->init(skel);
+	/* Pass the test nr_cpus to rotate between objects, but use passed in nr_cpus otherwise. */
+	skel->bss->nr_cpus = test->nr_cpus;
+	skel->bss->delay_seed = delay_us;
+
+	for_each_conctest_cfg(cfg, test->cfgs) {
+		int cpu;
+
+		switch (cfg->type) {
+		case CT_TASK_PROG:
+			for (cpu = 0; cpu < nr_cpus; cpu++) {
+				struct task_ctx *tc;
+				int prog_fd;
+
+				prog_fd = find_prog_fd(skel, cfg->task.prog_name);
+				if (prog_fd < 0) {
+					fprintf(stderr,
+						"Failed to get task prog fd for CPU %d\n", cpu);
+					goto out;
+				}
+
+				tc = &ctx->task_ctxs[ctx->nr_threads];
+				tc->prog_fd = prog_fd;
+				tc->cpu = cpu;
+				err = pthread_create(&ctx->threads[ctx->nr_threads], NULL,
+						     worker, tc);
+				if (err) {
+					fprintf(stderr,
+						"Failed to create thread for CPU %d: %s\n",
+						cpu, strerror(err));
+					goto out;
+				}
+				ctx->nr_threads++;
+			}
+			break;
+		case CT_NMI_PROG:
+			for (cpu = 0; cpu < nr_cpus; cpu++) {
+				int prog_fd, pmu_fd, link_fd;
+				int idx = ctx->nr_nmis;
+
+				prog_fd = find_prog_fd(skel, cfg->nmi.prog_name);
+				if (prog_fd < 0) {
+					fprintf(stderr,
+						"Failed to get NMI prog fd for CPU %d\n", cpu);
+					goto out;
+				}
+
+				pmu_fd = open_perf_event(cpu);
+				if (pmu_fd < 0) {
+					fprintf(stderr,
+						"Failed to open perf event on CPU %d: %s\n",
+						cpu, strerror(errno));
+					goto out;
+				}
+				ctx->nmi_pmu_fds[idx] = pmu_fd;
+
+				link_fd = bpf_link_create(prog_fd, pmu_fd, BPF_PERF_EVENT, NULL);
+				if (link_fd < 0) {
+					fprintf(stderr,
+						"Failed to attach NMI prog on CPU %d: %s\n",
+						cpu, strerror(errno));
+					ctx->nr_nmis++;
+					goto out;
+				}
+				ctx->nmi_link_fds[idx] = link_fd;
+				ctx->nr_nmis++;
+			}
+			break;
+		case CT_ATTACH_PROG: {
+			struct bpf_program *prog;
+			struct bpf_link *link;
+
+			if (ctx->nr_attach_links >= ctx->max_attach_links) {
+				fprintf(stderr, "Too many attach links\n");
+				goto out;
+			}
+
+			prog = cfg->attach.get_prog(skel, cfg, true);
+			if (!prog) {
+				fprintf(stderr, "Failed to get attach prog\n");
+				goto out;
+			}
+
+			link = bpf_program__attach_tracepoint(prog, cfg->attach.tp_category,
+							      cfg->attach.tp_name);
+			if (!link) {
+				fprintf(stderr, "Failed to attach prog: %s\n",
+					strerror(errno));
+				goto out;
+			}
+			ctx->attach_links[ctx->nr_attach_links++] = link;
+			break;
+		}
+		default:
+			break;
+		}
+	}
+
+	sleep(duration);
+	printf("Test '%s' completed\n", test->name);
+	err = 0;
+out:
+	stop = 1;
+	if (dump_test_stats(skel))
+		err = -1;
+	dump_prog_stdout(skel);
+	printf("Test '%s': %s\n", test->name, err ? "FAIL" : "PASS");
+	conctest__destroy(skel);
+free:
+	free_ctx(ctx);
+	return err;
+}
+
+static const char *test_filter[MAX_FILTERS];
+static int nr_test_filters;
+
+static int parse_test_filter(char *arg)
+{
+	char *token;
+
+	token = strtok(arg, ",");
+	while (token) {
+		if (nr_test_filters >= MAX_FILTERS) {
+			fprintf(stderr, "Too many test filters (max %d)\n", MAX_FILTERS);
+			return -1;
+		}
+		test_filter[nr_test_filters++] = token;
+		token = strtok(NULL, ",");
+	}
+	return 0;
+}
+
+static int test_selected(const char *name)
+{
+	int i;
+
+	if (!nr_test_filters)
+		return 1;
+
+	for (i = 0; i < nr_test_filters; i++) {
+		if (strncmp(test_filter[i], name, strlen(test_filter[i])) == 0)
+			return 1;
+	}
+	return 0;
+}
+
+static void usage(const char *prog)
+{
+	fprintf(stderr,
+		"Usage: %s [OPTIONS]\n"
+		"  -h         Show this help message\n"
+		"  -t TESTS   Comma-separated list of test names to run\n"
+		"  -c CPUS    Number of CPUs/threads (default: %d)\n"
+		"  -d SECS    Duration per test in seconds (default: %d)\n"
+		"  -s PERIOD  NMI perf sample period (default: %d)\n"
+		"  -S SEED    Delay seed for reproducibility (default: random)\n"
+		"  -D USECS   Max critical section delay in us (default: 10)\n"
+		"  -v         Verbose output\n",
+		prog, DEFAULT_NR_CPUS, DEFAULT_DURATION, NMI_SAMPLE_PERIOD);
+}
+
+int main(int argc, char **argv)
+{
+	int max_cpus = libbpf_num_possible_cpus();
+	int duration = DEFAULT_DURATION;
+	int nr_cpus = DEFAULT_NR_CPUS;
+	int opt, ran = 0, failed = 0;
+	struct conctest_test *test;
+
+	if (max_cpus < 0) {
+		fprintf(stderr, "Failed to get number of possible CPUs\n");
+		return 1;
+	}
+
+	while ((opt = getopt(argc, argv, "ht:c:d:s:S:D:v")) != -1) {
+		switch (opt) {
+		case 't':
+			if (parse_test_filter(optarg))
+				return 1;
+			break;
+		case 'c':
+			nr_cpus = atoi(optarg);
+			if (nr_cpus < 1 || nr_cpus > max_cpus) {
+				fprintf(stderr, "Invalid CPU count (1-%d)\n", max_cpus);
+				return 1;
+			}
+			break;
+		case 'd':
+			duration = atoi(optarg);
+			if (duration < 1) {
+				fprintf(stderr, "Invalid duration\n");
+				return 1;
+			}
+			break;
+		case 's':
+			nmi_sample_period = atoi(optarg);
+			if (nmi_sample_period < 1) {
+				fprintf(stderr, "Invalid sample period\n");
+				return 1;
+			}
+			break;
+		case 'v':
+			verbose = 1;
+			break;
+		case 'D':
+			delay_max_us = (__u32)atoi(optarg);
+			break;
+		case 'S':
+			delay_seed = (__u32)strtoul(optarg, NULL, 0);
+			break;
+		case 'h':
+		default:
+			usage(argv[0]);
+			return opt == 'h' ? 0 : 1;
+		}
+	}
+
+	if (!delay_seed)
+		delay_seed = (__u32)time(NULL);
+	delay_us = delay_seed % (delay_max_us + 1);
+	printf("Seed: %u, delay: %u us (max: %u us)\n", delay_seed, delay_us, delay_max_us);
+
+	for (test = all_tests; test->name; test++) {
+		if (!test_selected(test->name))
+			continue;
+
+		ran++;
+		if (run_test(test, nr_cpus, duration))
+			failed++;
+		fprintf(stderr, "\n\n\n");
+	}
+
+	if (!ran)
+		printf("No tests matched\n");
+	else
+		printf("\n%d/%d tests passed\n", ran - failed, ran);
+
+	return failed > 0 ? 1 : 0;
+}
diff --git a/tools/testing/selftests/bpf/progs/conctest.c b/tools/testing/selftests/bpf/progs/conctest.c
new file mode 100644
index 000000000000..aca36320caf9
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/conctest.c
@@ -0,0 +1,289 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <vmlinux.h>
+#include <bpf_atomic.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf_experimental.h>
+
+#define EDEADLK		35
+#define ETIMEDOUT	110
+#define CONCTEST_HIST_BUCKETS 28
+
+int nr_cpus;
+__u32 delay_seed;
+
+enum conctest_stat_id {
+	CONCTEST_STAT_SYSCALL = 0,
+	CONCTEST_STAT_NMI,
+	CONCTEST_STAT_MAX,
+};
+
+struct conctest_op_stats {
+	__u64 count;
+	__u64 success;
+	__u64 failure;
+	__u64 unexpected;
+	__u64 total_ns;
+	__u64 min_ns;
+	__u64 max_ns;
+	__u64 hist[CONCTEST_HIST_BUCKETS];
+};
+
+struct conctest_op_state {
+	__s64 expect_ret;
+	__u64 delay_thresh_ns;
+	__u64 failed_print_once;
+	__u64 delay_print_once;
+};
+
+struct {
+	__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
+	__uint(max_entries, CONCTEST_STAT_MAX);
+	__type(key, int);
+	__type(value, struct conctest_op_stats);
+} perf_map SEC(".maps");
+
+struct {
+	__uint(type, BPF_MAP_TYPE_ARRAY);
+	__uint(max_entries, CONCTEST_STAT_MAX);
+	__type(key, int);
+	__type(value, struct conctest_op_state);
+} state_map SEC(".maps");
+
+enum ctx_level {
+	CTX_TASK = 0,
+	CTX_SOFTIRQ,
+	CTX_HARDIRQ,
+	CTX_NMI,
+	CTX_MAX,
+};
+
+struct ctx_time {
+	__u64 accumulated_ns[CTX_MAX];
+	__u64 start_ns[CTX_MAX];
+	__u64 snapshot[CTX_MAX];
+};
+
+struct {
+	__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
+	__uint(max_entries, 1);
+	__type(key, int);
+	__type(value, struct ctx_time);
+} ctx_time_map SEC(".maps");
+
+static __always_inline enum ctx_level get_ctx_level(void)
+{
+	int pcnt = get_preempt_count();
+
+	if (pcnt & NMI_MASK)
+		return CTX_NMI;
+	if (pcnt & HARDIRQ_MASK)
+		return CTX_HARDIRQ;
+	if (pcnt & SOFTIRQ_MASK)
+		return CTX_SOFTIRQ;
+	return CTX_TASK;
+}
+
+static __always_inline __u64 ctx_higher_sum(struct ctx_time *ct, enum ctx_level level)
+{
+	__u64 sum = 0;
+	int i;
+
+	for (i = level + 1; i < CTX_MAX; i++)
+		sum += ct->accumulated_ns[i];
+	return sum;
+}
+
+static __always_inline void conctest_begin(void)
+{
+	struct ctx_time *ct;
+	enum ctx_level level;
+	int key = 0;
+
+	ct = bpf_map_lookup_elem(&ctx_time_map, &key);
+	if (!ct)
+		return;
+
+	level = get_ctx_level();
+	ct->snapshot[level] = ctx_higher_sum(ct, level);
+	ct->start_ns[level] = bpf_ktime_get_ns();
+}
+
+/*
+ *   0: [0, 1µs)           1: [1µs, 100µs)
+ *   2: [100µs, 200µs) ... 10: [900µs, 1ms)
+ *  11: [1ms, 10ms)        12: [10ms, 20ms) ... 20: [90ms, 100ms)
+ *  21: [100ms, 150ms)     22: [150ms, 200ms)   23: [200ms, 250ms)
+ *  24: [250ms, 500ms)     25: [500ms, 750ms)   26: [750ms, 1s)
+ *  27: [1s, inf)
+ */
+static __always_inline __u32 hist_bucket(__u64 ns)
+{
+	if (ns < 1000)
+		return 0;
+	if (ns < 100000)
+		return 1;
+	if (ns < 1000000)
+		return 2 + (ns - 100000) / 100000;
+	if (ns < 10000000)
+		return 11;
+	if (ns < 100000000)
+		return 12 + (ns - 10000000) / 10000000;
+	if (ns < 250000000)
+		return 21 + (ns - 100000000) / 50000000;
+	if (ns < 1000000000)
+		return 24 + (ns - 250000000) / 250000000;
+	return 27;
+}
+
+static __always_inline void conctest_record(int stat_id, __s64 ret, bool ctx_adjust)
+{
+	__u64 raw, higher_ns, duration_ns;
+	struct conctest_op_state *state;
+	struct conctest_op_stats *stats;
+	struct ctx_time *ct;
+	enum ctx_level level;
+	__u32 bucket;
+	int key = 0;
+
+	ct = bpf_map_lookup_elem(&ctx_time_map, &key);
+	if (!ct)
+		return;
+
+	level = get_ctx_level();
+	raw = bpf_ktime_get_ns() - ct->start_ns[level];
+
+	if (ctx_adjust) {
+		higher_ns = ctx_higher_sum(ct, level) - ct->snapshot[level];
+		duration_ns = raw > higher_ns ? raw - higher_ns : 0;
+
+		ct->accumulated_ns[level] += raw;
+	} else {
+		duration_ns = raw;
+	}
+
+	stats = bpf_map_lookup_elem(&perf_map, &stat_id);
+	if (!stats)
+		return;
+
+	state = bpf_map_lookup_elem(&state_map, &stat_id);
+	if (!state)
+		return;
+
+	if (stats->count == 0 || duration_ns < stats->min_ns)
+		stats->min_ns = duration_ns;
+	if (duration_ns > stats->max_ns)
+		stats->max_ns = duration_ns;
+
+	stats->count++;
+	stats->total_ns += duration_ns;
+
+	bucket = hist_bucket(duration_ns);
+	if (bucket < CONCTEST_HIST_BUCKETS)
+		stats->hist[bucket]++;
+
+	if (ret == 0) {
+		stats->success++;
+	} else if (ret == state->expect_ret) {
+		stats->failure++;
+	} else {
+		stats->failure++;
+		stats->unexpected++;
+		if (!cmpxchg(&state->failed_print_once, 0, 1)) {
+			bpf_stream_printk(BPF_STDOUT,
+					  "UNEXPECTED ERROR: stat %d expected %lld got %lld\n",
+					  stat_id, state->expect_ret, ret);
+			bpf_stream_print_stack(BPF_STDOUT);
+		}
+	}
+
+	u64 delay_thresh_ns = state->delay_thresh_ns;
+	if (!delay_thresh_ns)
+		delay_thresh_ns = 10000000; /* 10ms */
+	if (delay_thresh_ns && duration_ns > delay_thresh_ns && !cmpxchg(&state->delay_print_once, 0, 1)) {
+		const char *unit = duration_ns >= 1000000 ? "ms" : "ns";
+		bpf_stream_printk(BPF_STDOUT,
+				  "DELAY EXCEEDED: stat %d duration %llu %s > threshold %llu %s\n",
+				  stat_id, duration_ns / 1000000, unit, delay_thresh_ns / 1000000,
+				  unit);
+		bpf_stream_print_stack(BPF_STDOUT);
+	}
+}
+
+static __always_inline void bpf_udelay(__u32 us)
+{
+	__u64 target = bpf_ktime_get_ns() + (__u64)us * 1000;
+
+	while (bpf_ktime_get_ns() < target && can_loop)
+		;
+}
+
+struct lock_val {
+	struct bpf_res_spin_lock lock;
+};
+
+struct {
+	__uint(type, BPF_MAP_TYPE_ARRAY);
+	__uint(max_entries, 3);
+	__type(key, int);
+	__type(value, struct lock_val);
+} lock_map SEC(".maps");
+
+SEC("?syscall")
+int conctest_rqspinlock_task(void *ctx)
+{
+	struct lock_val *e;
+	__u32 key;
+	int ret;
+
+	key = bpf_get_smp_processor_id();
+	key %= (__u32)nr_cpus;
+	e = bpf_map_lookup_elem(&lock_map, &key);
+	if (!e)
+		return 0;
+
+	conctest_begin();
+	ret = bpf_res_spin_lock(&e->lock);
+	if (ret == 0) {
+		bpf_udelay(delay_seed);
+		bpf_res_spin_unlock(&e->lock);
+	}
+	conctest_record(CONCTEST_STAT_SYSCALL, ret, true);
+
+	return 0;
+}
+
+static int __conctest_rqspinlock_nmi(int start)
+{
+	struct lock_val *e;
+	__u32 key = start;
+	int ret;
+
+	key += bpf_get_smp_processor_id();
+	key %= (__u32)nr_cpus;
+	e = bpf_map_lookup_elem(&lock_map, &key);
+	if (!e)
+		return 0;
+
+	conctest_begin();
+	ret = bpf_res_spin_lock(&e->lock);
+	if (ret == 0)
+		bpf_res_spin_unlock(&e->lock);
+	conctest_record(CONCTEST_STAT_NMI, ret, true);
+
+	return 0;
+}
+
+SEC("?perf_event")
+int conctest_rqspinlock_nmi(void *ctx)
+{
+	return __conctest_rqspinlock_nmi(0);
+}
+
+SEC("?perf_event")
+int conctest_rqspinlock_nmi_shift(void *ctx)
+{
+	return __conctest_rqspinlock_nmi(1);
+}
+
+char _license[] SEC("license") = "GPL";
-- 
2.47.3


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

* [RFC PATCH bpf-next v1 5/6] selftests/bpf: Generate various conctest permutations
  2026-02-11 18:12 [RFC PATCH bpf-next v1 0/6] Concurrency Testing Kumar Kartikeya Dwivedi
                   ` (3 preceding siblings ...)
  2026-02-11 18:12 ` [RFC PATCH bpf-next v1 4/6] selftests/bpf: Introduce concurrency testing tool Kumar Kartikeya Dwivedi
@ 2026-02-11 18:12 ` Kumar Kartikeya Dwivedi
  2026-02-11 22:09   ` Alexei Starovoitov
  2026-02-11 18:12 ` [RFC PATCH bpf-next v1 6/6] selftests/bpf: Extend conctest to wq and task_work Kumar Kartikeya Dwivedi
  5 siblings, 1 reply; 11+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-02-11 18:12 UTC (permalink / raw)
  To: bpf
  Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
	Martin KaFai Lau, Eduard Zingerman, Mykyta Yatsenko, kkd,
	kernel-team

Introduce support for testing various permutations given a list of a
operations for an async primitive. This unburdens the user from setting
up various cases and thinking through all possible interleavings. The
allowed context for each operation and the number of operations are
specified and the various permutations in both directions (vertical,
i.e. on the same CPU, and horizontal, i.e. across CPUs in a flat context)
are generated automatically. Further, symmetric operations that won't
improve coverage (e.g. flat AvsB and BvsA cases) are pruned to reduce
test time.

Add support for timer operations testing in this commit to show its
usefulness. Unlike rqspinlock, the number of operations extend to 6
(start, cancel, cancel_async, init, delete, set_callback) which must
be tested in various combinations to approach completeness. To bound
the number of generated cases bound the maximum parallelism to 4 for
timer and 3 for rqspinlock.

Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
 tools/testing/selftests/bpf/conctest.c       | 319 ++++++++++++++-----
 tools/testing/selftests/bpf/progs/conctest.c | 230 ++++++++++++-
 2 files changed, 476 insertions(+), 73 deletions(-)

diff --git a/tools/testing/selftests/bpf/conctest.c b/tools/testing/selftests/bpf/conctest.c
index e4b333aff419..be6d3864d455 100644
--- a/tools/testing/selftests/bpf/conctest.c
+++ b/tools/testing/selftests/bpf/conctest.c
@@ -300,92 +300,237 @@ struct conctest_cfg {
 	for (struct conctest_cfg *cfg = (arr); cfg->type != CT_INVALID; cfg++)
 
 struct conctest_test {
-	const char *name;
-	struct conctest_cfg *cfgs;
-	void (*init)(struct conctest *skel);
+	char name[128];
+	struct conctest_cfg cfgs[4];
+	void (*init)(struct conctest *skel, struct conctest_test *test);
+	const char **extra_progs;
 	int nr_cpus;
+	bool vertical;
 };
 
-struct conctest_cfg rqspinlock[] = {
-	{
-		.type = CT_TASK_PROG,
-		.task = { "conctest_rqspinlock_task" },
-	},
-	{
-		.type = CT_NMI_PROG,
-		.nmi = { "conctest_rqspinlock_nmi" },
-	},
-	{}
+static int find_prog_fd(struct conctest *skel, const char *prog_name);
+
+#define CTX_TASK_OK	(1 << 0)
+#define CTX_NMI_OK	(1 << 1)
+
+struct conctest_op {
+	const char *name;
+	const char *task_prog;
+	const char *nmi_prog;
+	unsigned int ctx_mask;
 };
 
-struct conctest_cfg rqspinlock_shift[] = {
-	{
-		.type = CT_TASK_PROG,
-		.task = { "conctest_rqspinlock_task" },
-	},
-	{
-		.type = CT_NMI_PROG,
-		.nmi = { "conctest_rqspinlock_nmi_shift" },
-	},
-	{}
+struct conctest_suite {
+	const char *name;
+	struct conctest_op *ops;
+	int nr_ops;
+	void (*init)(struct conctest *skel, struct conctest_test *test);
+	const char **extra_progs;
+	int max_cpus;
 };
 
-static void __rqspinlock_init(struct conctest *skel, int err, int delay)
+static struct conctest_op rqspinlock_ops[] = {
+	{ "lock",       "conctest_rqspinlock_task", "conctest_rqspinlock_nmi",       CTX_TASK_OK | CTX_NMI_OK },
+	{ "lock_shift", "conctest_rqspinlock_task", "conctest_rqspinlock_nmi_shift", CTX_TASK_OK | CTX_NMI_OK },
+	{},
+};
+
+static void rqspinlock_init(struct conctest *skel, struct conctest_test *test)
 {
 	struct conctest_op_state state;
-	int key;
+	bool same_lock, is_vert;
+	int key, nmi_err;
+
+	is_vert = test->vertical;
+
+	/*
+	 * Horizontal: don't set expectations.
+	 * Vertical, same lock: always -EDEADLK.
+	 * Vertical, different locks: -EDEADLK for <3 CPUs, -ETIMEDOUT for >=3.
+	 */
+	if (!is_vert) {
+		key = CONCTEST_STAT_SYSCALL;
+		memset(&state, 0, sizeof(state));
+		state.delay_thresh_ns = 10000000;
+		bpf_map__update_elem(skel->maps.state_map, &key, sizeof(key),
+				     &state, sizeof(state), 0);
+		return;
+	}
+
+	same_lock = (test->cfgs[0].task.prog_name == test->cfgs[1].nmi.prog_name) ||
+		    (strcmp(test->cfgs[0].task.prog_name, "conctest_rqspinlock_task") == 0 &&
+		     strcmp(test->cfgs[1].nmi.prog_name, "conctest_rqspinlock_nmi") == 0);
+
+	if (same_lock || test->nr_cpus < 3)
+		nmi_err = -EDEADLK;
+	else
+		nmi_err = -ETIMEDOUT;
 
 	key = CONCTEST_STAT_SYSCALL;
 	memset(&state, 0, sizeof(state));
 	state.expect_ret = 0;
-	state.delay_thresh_ns = 1000000 * delay;
-	bpf_map__update_elem(skel->maps.state_map, &key, sizeof(key), &state,
-			     sizeof(state), 0);
+	state.delay_thresh_ns = (test->nr_cpus >= 3) ? 300000000ULL : 10000000ULL;
+	bpf_map__update_elem(skel->maps.state_map, &key, sizeof(key),
+			     &state, sizeof(state), 0);
 
 	key = CONCTEST_STAT_NMI;
 	memset(&state, 0, sizeof(state));
-	state.expect_ret = err;
-	state.delay_thresh_ns = 1000000 * delay;
-	bpf_map__update_elem(skel->maps.state_map, &key, sizeof(key), &state,
-			     sizeof(state), 0);
+	state.expect_ret = nmi_err;
+	state.delay_thresh_ns = (test->nr_cpus >= 3) ? 300000000ULL : 10000000ULL;
+	bpf_map__update_elem(skel->maps.state_map, &key, sizeof(key),
+			     &state, sizeof(state), 0);
 }
 
-static void rqspinlock_init(struct conctest *skel)
+static struct conctest_suite rqspinlock_suite = {
+	.name = "rqspinlock",
+	.ops = rqspinlock_ops,
+	.nr_ops = 2,
+	.init = rqspinlock_init,
+	.max_cpus = 3,
+};
+
+static void timer_init(struct conctest *skel, struct conctest_test *test)
 {
-	return __rqspinlock_init(skel, -EDEADLK, 2);
+	LIBBPF_OPTS(bpf_test_run_opts, opts);
+	int fd;
+
+	fd = find_prog_fd(skel, "conctest_timer_init");
+	if (fd < 0)
+		return;
+	bpf_prog_test_run_opts(fd, &opts);
 }
 
-static void rqspinlock_init_delay(struct conctest *skel)
+static struct conctest_op timer_ops[] = {
+	{ "init",         "conctest_timer_task_reinit",     "conctest_timer_nmi_reinit",     CTX_TASK_OK | CTX_NMI_OK },
+	{ "start",        "conctest_timer_task_start",      "conctest_timer_nmi_start",      CTX_TASK_OK | CTX_NMI_OK },
+	{ "cancel",       "conctest_timer_task_cancel",      NULL,                            CTX_TASK_OK },
+	{ "cancel_async", "conctest_timer_task_cancel_async","conctest_timer_nmi_cancel_async",CTX_TASK_OK | CTX_NMI_OK },
+	{ "set_cb",       "conctest_timer_task_set_cb",      "conctest_timer_nmi_set_cb",     CTX_TASK_OK | CTX_NMI_OK },
+	{ "delete",       "conctest_timer_task_delete",      "conctest_timer_nmi_delete",     CTX_TASK_OK | CTX_NMI_OK },
+	{},
+};
+
+static const char *timer_extra_progs[] = { "conctest_timer_init", NULL };
+
+static struct conctest_suite timer_suite = {
+	.name = "timer",
+	.ops = timer_ops,
+	.nr_ops = 6,
+	.init = timer_init,
+	.extra_progs = timer_extra_progs,
+	.max_cpus = 4,
+};
+
+static struct conctest_suite *suites[] = {
+	&rqspinlock_suite,
+	&timer_suite,
+	NULL,
+};
+
+#define MAX_GENERATED_TESTS 1024
+
+static struct conctest_test generated_tests[MAX_GENERATED_TESTS];
+static int nr_generated;
+
+static void add_test(const char *suite_name, const char *a_name, const char *b_name,
+		     const char *task_prog, const char *nmi_prog,
+		     const char *task_prog_b,
+		     void (*init)(struct conctest *skel, struct conctest_test *test),
+		     const char **extra_progs, int nr_cpus)
 {
-	return __rqspinlock_init(skel, -EDEADLK, 10);
+	struct conctest_test *t;
+
+	if (nr_generated >= MAX_GENERATED_TESTS)
+		return;
+
+	t = &generated_tests[nr_generated++];
+	if (nmi_prog)
+		snprintf(t->name, sizeof(t->name), "%s:%d:vert:%s_vs_%s",
+			 suite_name, nr_cpus, a_name, b_name);
+	else
+		snprintf(t->name, sizeof(t->name), "%s:%d:flat:%s_vs_%s",
+			 suite_name, nr_cpus, a_name, b_name);
+
+	t->init = init;
+	t->extra_progs = extra_progs;
+	t->nr_cpus = nr_cpus;
+	t->vertical = (nmi_prog != NULL);
+
+	t->cfgs[0].type = CT_TASK_PROG;
+	t->cfgs[0].task.prog_name = task_prog;
+
+	if (nmi_prog) {
+		t->cfgs[1].type = CT_NMI_PROG;
+		t->cfgs[1].nmi.prog_name = nmi_prog;
+		t->cfgs[2].type = CT_INVALID;
+	} else if (task_prog_b) {
+		t->cfgs[1].type = CT_TASK_PROG;
+		t->cfgs[1].task.prog_name = task_prog_b;
+		t->cfgs[2].type = CT_INVALID;
+	} else {
+		t->cfgs[1].type = CT_INVALID;
+	}
 }
 
-static void rqspinlock_init_timeout(struct conctest *skel)
+static void generate_suite_tests(struct conctest_suite *suite, int max_cpus)
 {
-	return __rqspinlock_init(skel, -ETIMEDOUT, 300);
+	int i, j, cpus;
+
+	if (suite->max_cpus > max_cpus)
+		suite->max_cpus = max_cpus;
+
+	for (cpus = 1; cpus <= suite->max_cpus; cpus++) {
+		for (i = 0; i < suite->nr_ops; i++) {
+			struct conctest_op *a = &suite->ops[i];
+
+			if (!(a->ctx_mask & CTX_TASK_OK))
+				continue;
+
+			for (j = 0; j < suite->nr_ops; j++) {
+				struct conctest_op *b = &suite->ops[j];
+
+				if (!(b->ctx_mask & CTX_NMI_OK) || !b->nmi_prog)
+					continue;
+
+				add_test(suite->name, a->name, b->name,
+					 a->task_prog, b->nmi_prog, NULL,
+					 suite->init, suite->extra_progs, cpus);
+			}
+		}
+
+		/* Horizontal: task(a) vs task(b), needs at least 2 CPUs */
+		if (cpus < 2)
+			continue;
+
+		for (i = 0; i < suite->nr_ops; i++) {
+			struct conctest_op *a = &suite->ops[i];
+
+			if (!(a->ctx_mask & CTX_TASK_OK))
+				continue;
+
+			/* j = i to skip symmetric pairs (A vs B == B vs A) */
+			for (j = i; j < suite->nr_ops; j++) {
+				struct conctest_op *b = &suite->ops[j];
+
+				if (!(b->ctx_mask & CTX_TASK_OK) || !b->task_prog)
+					continue;
+
+				add_test(suite->name, a->name, b->name,
+					 a->task_prog, NULL, b->task_prog,
+					 suite->init, suite->extra_progs, cpus);
+			}
+		}
+	}
 }
 
-static struct conctest_test all_tests[] = {
-	{
-		.name = "rqspinlock:AA",
-		.cfgs = rqspinlock,
-		.init = rqspinlock_init,
-		.nr_cpus = 1
-	},
-	{
-		.name = "rqspinlock:ABBA",
-		.cfgs = rqspinlock_shift,
-		.init = rqspinlock_init_delay,
-		.nr_cpus = 2
-	},
-	{
-		.name = "rqspinlock:ABBCBA",
-		.cfgs = rqspinlock_shift,
-		.init = rqspinlock_init_timeout,
-		.nr_cpus = 3
-	},
-	{},
-};
+static void generate_all_tests(int max_cpus)
+{
+	int i;
+
+	nr_generated = 0;
+	for (i = 0; suites[i]; i++)
+		generate_suite_tests(suites[i], max_cpus);
+}
 
 struct task_ctx {
 	int prog_fd;
@@ -466,11 +611,6 @@ static struct conctest_ctx *alloc_ctx(int nr_cpus, struct conctest_cfg *cfgs)
 		}
 	}
 
-	if (nr_task > 1) {
-		fprintf(stderr, "Only one CT_TASK_PROG entry allowed\n");
-		return NULL;
-	}
-
 	if (nr_nmi > 1) {
 		fprintf(stderr, "Only one CT_NMI_PROG entry allowed\n");
 		return NULL;
@@ -603,7 +743,9 @@ static int run_test(struct conctest_test *test, int nr_cpus, int duration)
 	if (!ctx)
 		return -ENOMEM;
 
-	printf("Running test '%s' on %d CPU(s) for %d seconds\n", test->name, nr_cpus, duration);
+	if (verbose)
+		printf("Running test '%s' on %d CPU(s) for %d seconds\n", test->name, nr_cpus,
+		       duration);
 
 	skel = conctest__open();
 	if (!skel) {
@@ -639,6 +781,20 @@ static int run_test(struct conctest_test *test, int nr_cpus, int duration)
 		}
 	}
 
+	if (test->extra_progs) {
+		struct bpf_program *ep_prog;
+		const char **ep;
+
+		for (ep = test->extra_progs; *ep; ep++) {
+			ep_prog = bpf_object__find_program_by_name(skel->obj, *ep);
+			if (!ep_prog) {
+				fprintf(stderr, "Extra program '%s' not found\n", *ep);
+				goto out;
+			}
+			bpf_program__set_autoload(ep_prog, true);
+		}
+	}
+
 	err = conctest__load(skel);
 	if (err) {
 		fprintf(stderr, "Failed to load BPF skeleton: %d\n", err);
@@ -648,17 +804,30 @@ static int run_test(struct conctest_test *test, int nr_cpus, int duration)
 	stop = 0;
 
 	if (test->init)
-		test->init(skel);
+		test->init(skel, test);
 	/* Pass the test nr_cpus to rotate between objects, but use passed in nr_cpus otherwise. */
 	skel->bss->nr_cpus = test->nr_cpus;
 	skel->bss->delay_seed = delay_us;
 
 	for_each_conctest_cfg(cfg, test->cfgs) {
-		int cpu;
+		int cpu, nr_task_cfgs = 0, task_idx = 0;
+		int cpu_start, cpu_end;
+
+		for_each_conctest_cfg(c, test->cfgs)
+			if (c->type == CT_TASK_PROG)
+				nr_task_cfgs++;
 
 		switch (cfg->type) {
 		case CT_TASK_PROG:
-			for (cpu = 0; cpu < nr_cpus; cpu++) {
+			for_each_conctest_cfg(c, test->cfgs) {
+				if (c == cfg)
+					break;
+				if (c->type == CT_TASK_PROG)
+					task_idx++;
+			}
+			cpu_start = task_idx * nr_cpus / nr_task_cfgs;
+			cpu_end = (task_idx + 1) * nr_cpus / nr_task_cfgs;
+			for (cpu = cpu_start; cpu < cpu_end; cpu++) {
 				struct task_ctx *tc;
 				int prog_fd;
 
@@ -747,7 +916,8 @@ static int run_test(struct conctest_test *test, int nr_cpus, int duration)
 	}
 
 	sleep(duration);
-	printf("Test '%s' completed\n", test->name);
+	if (verbose)
+		printf("Test '%s' completed\n", test->name);
 	err = 0;
 out:
 	stop = 1;
@@ -814,7 +984,7 @@ int main(int argc, char **argv)
 	int max_cpus = libbpf_num_possible_cpus();
 	int duration = DEFAULT_DURATION;
 	int nr_cpus = DEFAULT_NR_CPUS;
-	int opt, ran = 0, failed = 0;
+	int opt, ran = 0, failed = 0, i;
 	struct conctest_test *test;
 
 	if (max_cpus < 0) {
@@ -870,14 +1040,19 @@ int main(int argc, char **argv)
 	delay_us = delay_seed % (delay_max_us + 1);
 	printf("Seed: %u, delay: %u us (max: %u us)\n", delay_seed, delay_us, delay_max_us);
 
-	for (test = all_tests; test->name; test++) {
+	generate_all_tests(max_cpus);
+	printf("Generated %d tests\n\n", nr_generated);
+
+	for (i = 0; i < nr_generated; i++) {
+		test = &generated_tests[i];
 		if (!test_selected(test->name))
 			continue;
 
 		ran++;
 		if (run_test(test, nr_cpus, duration))
 			failed++;
-		fprintf(stderr, "\n\n\n");
+		if (verbose)
+			printf("\n");
 	}
 
 	if (!ran)
diff --git a/tools/testing/selftests/bpf/progs/conctest.c b/tools/testing/selftests/bpf/progs/conctest.c
index aca36320caf9..703fd65c6ce3 100644
--- a/tools/testing/selftests/bpf/progs/conctest.c
+++ b/tools/testing/selftests/bpf/progs/conctest.c
@@ -6,6 +6,7 @@
 #include <bpf_experimental.h>
 
 #define EDEADLK		35
+#define EBUSY		16
 #define ETIMEDOUT	110
 #define CONCTEST_HIST_BUCKETS 28
 
@@ -184,7 +185,7 @@ static __always_inline void conctest_record(int stat_id, __s64 ret, bool ctx_adj
 
 	if (ret == 0) {
 		stats->success++;
-	} else if (ret == state->expect_ret) {
+	} else if (!state->expect_ret || ret == state->expect_ret) {
 		stats->failure++;
 	} else {
 		stats->failure++;
@@ -286,4 +287,231 @@ int conctest_rqspinlock_nmi_shift(void *ctx)
 	return __conctest_rqspinlock_nmi(1);
 }
 
+#define CLOCK_MONOTONIC	    1
+#define BPF_F_TIMER_CPU_PIN (1ULL << 1)
+
+struct timer_elem {
+	struct bpf_timer timer;
+};
+
+struct {
+	__uint(type, BPF_MAP_TYPE_ARRAY);
+	__uint(max_entries, 1);
+	__type(key, int);
+	__type(value, struct timer_elem);
+} timer_map SEC(".maps");
+
+static int timer_callback(void *map, int *key, struct timer_elem *val)
+{
+	bpf_timer_start(&val->timer, 0, BPF_F_TIMER_CPU_PIN);
+	return 0;
+}
+
+SEC("?syscall")
+int conctest_timer_init(void *ctx)
+{
+	struct timer_elem *elem;
+	int key = 0, ret;
+
+	elem = bpf_map_lookup_elem(&timer_map, &key);
+	if (!elem)
+		return -1;
+
+	ret = bpf_timer_init(&elem->timer, &timer_map, CLOCK_MONOTONIC);
+	if (ret && ret != -EBUSY)
+		return ret;
+
+	ret = bpf_timer_set_callback(&elem->timer, timer_callback);
+	if (ret)
+		return ret;
+
+	return bpf_timer_start(&elem->timer, 0, BPF_F_TIMER_CPU_PIN);
+}
+
+SEC("?syscall")
+int conctest_timer_task_reinit(void *ctx)
+{
+	struct timer_elem *elem;
+	int key = 0, ret;
+
+	elem = bpf_map_lookup_elem(&timer_map, &key);
+	if (!elem)
+		return 0;
+
+	conctest_begin();
+	ret = bpf_timer_init(&elem->timer, &timer_map, CLOCK_MONOTONIC);
+	conctest_record(CONCTEST_STAT_SYSCALL, ret, true);
+	return 0;
+}
+
+SEC("?perf_event")
+int conctest_timer_nmi_reinit(void *ctx)
+{
+	struct timer_elem *elem;
+	int key = 0, ret;
+
+	elem = bpf_map_lookup_elem(&timer_map, &key);
+	if (!elem)
+		return 0;
+
+	conctest_begin();
+	ret = bpf_timer_init(&elem->timer, &timer_map, CLOCK_MONOTONIC);
+	conctest_record(CONCTEST_STAT_NMI, ret, true);
+	return 0;
+}
+
+SEC("?syscall")
+int conctest_timer_task_start(void *ctx)
+{
+	struct timer_elem *elem;
+	int key = 0, ret;
+
+	elem = bpf_map_lookup_elem(&timer_map, &key);
+	if (!elem)
+		return 0;
+
+	conctest_begin();
+	ret = bpf_timer_start(&elem->timer, 0, BPF_F_TIMER_CPU_PIN);
+	conctest_record(CONCTEST_STAT_SYSCALL, ret, true);
+	return 0;
+}
+
+SEC("?syscall")
+int conctest_timer_task_cancel(void *ctx)
+{
+	struct timer_elem *elem;
+	int key = 0, ret;
+
+	elem = bpf_map_lookup_elem(&timer_map, &key);
+	if (!elem)
+		return 0;
+
+	conctest_begin();
+	ret = bpf_timer_cancel(&elem->timer);
+	conctest_record(CONCTEST_STAT_SYSCALL, ret, true);
+	return 0;
+}
+
+SEC("?syscall")
+int conctest_timer_task_cancel_async(void *ctx)
+{
+	struct timer_elem *elem;
+	int key = 0, ret;
+
+	elem = bpf_map_lookup_elem(&timer_map, &key);
+	if (!elem)
+		return 0;
+
+	conctest_begin();
+	ret = bpf_timer_cancel_async(&elem->timer);
+	conctest_record(CONCTEST_STAT_SYSCALL, ret, true);
+	return 0;
+}
+
+SEC("?syscall")
+int conctest_timer_task_set_cb(void *ctx)
+{
+	struct timer_elem *elem;
+	int key = 0, ret;
+
+	elem = bpf_map_lookup_elem(&timer_map, &key);
+	if (!elem)
+		return 0;
+
+	conctest_begin();
+	ret = bpf_timer_set_callback(&elem->timer, timer_callback);
+	conctest_record(CONCTEST_STAT_SYSCALL, ret, true);
+	return 0;
+}
+
+SEC("?syscall")
+int conctest_timer_task_delete(void *ctx)
+{
+	struct timer_elem *elem;
+	int key = 0, ret;
+
+	/* Delete the element (cancels + frees timer) */
+	conctest_begin();
+	ret = bpf_map_delete_elem(&timer_map, &key);
+	if (ret == 0) {
+		/* Re-init so subsequent ops still work */
+		elem = bpf_map_lookup_elem(&timer_map, &key);
+		if (elem) {
+			bpf_timer_init(&elem->timer, &timer_map, CLOCK_MONOTONIC);
+			bpf_timer_set_callback(&elem->timer, timer_callback);
+			bpf_timer_start(&elem->timer, 0, BPF_F_TIMER_CPU_PIN);
+		}
+	}
+	conctest_record(CONCTEST_STAT_SYSCALL, ret, true);
+	return 0;
+}
+
+SEC("?perf_event")
+int conctest_timer_nmi_start(void *ctx)
+{
+	struct timer_elem *elem;
+	int key = 0, ret;
+
+	elem = bpf_map_lookup_elem(&timer_map, &key);
+	if (!elem)
+		return 0;
+
+	conctest_begin();
+	ret = bpf_timer_start(&elem->timer, 0, BPF_F_TIMER_CPU_PIN);
+	conctest_record(CONCTEST_STAT_NMI, ret, true);
+	return 0;
+}
+
+SEC("?perf_event")
+int conctest_timer_nmi_cancel_async(void *ctx)
+{
+	struct timer_elem *elem;
+	int key = 0, ret;
+
+	elem = bpf_map_lookup_elem(&timer_map, &key);
+	if (!elem)
+		return 0;
+
+	conctest_begin();
+	ret = bpf_timer_cancel_async(&elem->timer);
+	conctest_record(CONCTEST_STAT_NMI, ret, true);
+	return 0;
+}
+
+SEC("?perf_event")
+int conctest_timer_nmi_set_cb(void *ctx)
+{
+	struct timer_elem *elem;
+	int key = 0, ret;
+
+	elem = bpf_map_lookup_elem(&timer_map, &key);
+	if (!elem)
+		return 0;
+
+	conctest_begin();
+	ret = bpf_timer_set_callback(&elem->timer, timer_callback);
+	conctest_record(CONCTEST_STAT_NMI, ret, true);
+	return 0;
+}
+
+SEC("?perf_event")
+int conctest_timer_nmi_delete(void *ctx)
+{
+	struct timer_elem *elem;
+	int key = 0, ret;
+
+	conctest_begin();
+	ret = bpf_map_delete_elem(&timer_map, &key);
+	if (ret == 0) {
+		elem = bpf_map_lookup_elem(&timer_map, &key);
+		if (elem) {
+			bpf_timer_init(&elem->timer, &timer_map, CLOCK_MONOTONIC);
+			bpf_timer_set_callback(&elem->timer, timer_callback);
+			bpf_timer_start(&elem->timer, 0, BPF_F_TIMER_CPU_PIN);
+		}
+	}
+	conctest_record(CONCTEST_STAT_NMI, ret, true);
+	return 0;
+}
+
 char _license[] SEC("license") = "GPL";
-- 
2.47.3


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

* [RFC PATCH bpf-next v1 6/6] selftests/bpf: Extend conctest to wq and task_work
  2026-02-11 18:12 [RFC PATCH bpf-next v1 0/6] Concurrency Testing Kumar Kartikeya Dwivedi
                   ` (4 preceding siblings ...)
  2026-02-11 18:12 ` [RFC PATCH bpf-next v1 5/6] selftests/bpf: Generate various conctest permutations Kumar Kartikeya Dwivedi
@ 2026-02-11 18:12 ` Kumar Kartikeya Dwivedi
  5 siblings, 0 replies; 11+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-02-11 18:12 UTC (permalink / raw)
  To: bpf
  Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
	Martin KaFai Lau, Eduard Zingerman, Mykyta Yatsenko, kkd,
	kernel-team

Extend coverage to various operations for wq and task_work.

Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
 tools/testing/selftests/bpf/conctest.c       |  46 ++++
 tools/testing/selftests/bpf/progs/conctest.c | 241 +++++++++++++++++++
 2 files changed, 287 insertions(+)

diff --git a/tools/testing/selftests/bpf/conctest.c b/tools/testing/selftests/bpf/conctest.c
index be6d3864d455..f1f9b0e9a78a 100644
--- a/tools/testing/selftests/bpf/conctest.c
+++ b/tools/testing/selftests/bpf/conctest.c
@@ -421,9 +421,55 @@ static struct conctest_suite timer_suite = {
 	.max_cpus = 4,
 };
 
+static void wq_init(struct conctest *skel, struct conctest_test *test)
+{
+	LIBBPF_OPTS(bpf_test_run_opts, opts);
+	int fd;
+
+	fd = find_prog_fd(skel, "conctest_wq_init");
+	if (fd < 0)
+		return;
+	bpf_prog_test_run_opts(fd, &opts);
+}
+
+static struct conctest_op wq_ops[] = {
+	{ "start",  "conctest_wq_task_start",  "conctest_wq_nmi_start",  CTX_TASK_OK | CTX_NMI_OK },
+	{ "set_cb", "conctest_wq_task_set_cb", "conctest_wq_nmi_set_cb", CTX_TASK_OK | CTX_NMI_OK },
+	{ "delete", "conctest_wq_task_delete", "conctest_wq_nmi_delete", CTX_TASK_OK | CTX_NMI_OK },
+	{},
+};
+
+static const char *wq_extra_progs[] = { "conctest_wq_init", NULL };
+
+static struct conctest_suite wq_suite = {
+	.name = "wq",
+	.ops = wq_ops,
+	.nr_ops = 3,
+	.init = wq_init,
+	.extra_progs = wq_extra_progs,
+	.max_cpus = 4,
+};
+
+static struct conctest_op tw_ops[] = {
+	{ "signal", "conctest_tw_task_signal", "conctest_tw_nmi_signal", CTX_TASK_OK | CTX_NMI_OK },
+	{ "resume", "conctest_tw_task_resume", "conctest_tw_nmi_resume", CTX_TASK_OK | CTX_NMI_OK },
+	{ "delete", "conctest_tw_task_delete", "conctest_tw_nmi_delete", CTX_TASK_OK | CTX_NMI_OK },
+	{},
+};
+
+static struct conctest_suite tw_suite = {
+	.name = "task_work",
+	.ops = tw_ops,
+	.nr_ops = 3,
+	.init = NULL,
+	.max_cpus = 4,
+};
+
 static struct conctest_suite *suites[] = {
 	&rqspinlock_suite,
 	&timer_suite,
+	&wq_suite,
+	&tw_suite,
 	NULL,
 };
 
diff --git a/tools/testing/selftests/bpf/progs/conctest.c b/tools/testing/selftests/bpf/progs/conctest.c
index 703fd65c6ce3..4995846731ac 100644
--- a/tools/testing/selftests/bpf/progs/conctest.c
+++ b/tools/testing/selftests/bpf/progs/conctest.c
@@ -514,4 +514,245 @@ int conctest_timer_nmi_delete(void *ctx)
 	return 0;
 }
 
+struct wq_elem {
+	struct bpf_wq work;
+};
+
+struct {
+	__uint(type, BPF_MAP_TYPE_ARRAY);
+	__uint(max_entries, 1);
+	__type(key, int);
+	__type(value, struct wq_elem);
+} wq_map SEC(".maps");
+
+static int wq_callback(void *map, int *key, void *val)
+{
+	return 0;
+}
+
+SEC("?syscall")
+int conctest_wq_init(void *ctx)
+{
+	struct wq_elem *elem;
+	int key = 0, ret;
+
+	elem = bpf_map_lookup_elem(&wq_map, &key);
+	if (!elem)
+		return -1;
+
+	ret = bpf_wq_init(&elem->work, &wq_map, 0);
+	if (ret)
+		return ret;
+
+	return bpf_wq_set_callback(&elem->work, wq_callback, 0);
+}
+
+SEC("?syscall")
+int conctest_wq_task_start(void *ctx)
+{
+	struct wq_elem *elem;
+	int key = 0, ret;
+
+	elem = bpf_map_lookup_elem(&wq_map, &key);
+	if (!elem)
+		return 0;
+
+	conctest_begin();
+	ret = bpf_wq_start(&elem->work, 0);
+	conctest_record(CONCTEST_STAT_SYSCALL, ret, true);
+	return 0;
+}
+
+SEC("?syscall")
+int conctest_wq_task_set_cb(void *ctx)
+{
+	struct wq_elem *elem;
+	int key = 0, ret;
+
+	elem = bpf_map_lookup_elem(&wq_map, &key);
+	if (!elem)
+		return 0;
+
+	conctest_begin();
+	ret = bpf_wq_set_callback(&elem->work, wq_callback, 0);
+	conctest_record(CONCTEST_STAT_SYSCALL, ret, true);
+	return 0;
+}
+
+SEC("?perf_event")
+int conctest_wq_nmi_start(void *ctx)
+{
+	struct wq_elem *elem;
+	int key = 0, ret;
+
+	elem = bpf_map_lookup_elem(&wq_map, &key);
+	if (!elem)
+		return 0;
+
+	conctest_begin();
+	ret = bpf_wq_start(&elem->work, 0);
+	conctest_record(CONCTEST_STAT_NMI, ret, true);
+	return 0;
+}
+
+SEC("?perf_event")
+int conctest_wq_nmi_set_cb(void *ctx)
+{
+	struct wq_elem *elem;
+	int key = 0, ret;
+
+	elem = bpf_map_lookup_elem(&wq_map, &key);
+	if (!elem)
+		return 0;
+
+	conctest_begin();
+	ret = bpf_wq_set_callback(&elem->work, wq_callback, 0);
+	conctest_record(CONCTEST_STAT_NMI, ret, true);
+	return 0;
+}
+
+SEC("?syscall")
+int conctest_wq_task_delete(void *ctx)
+{
+	struct wq_elem *elem;
+	int key = 0, ret;
+
+	conctest_begin();
+	ret = bpf_map_delete_elem(&wq_map, &key);
+	if (ret == 0) {
+		elem = bpf_map_lookup_elem(&wq_map, &key);
+		if (elem) {
+			bpf_wq_init(&elem->work, &wq_map, 0);
+			bpf_wq_set_callback(&elem->work, wq_callback, 0);
+		}
+	}
+	conctest_record(CONCTEST_STAT_SYSCALL, ret, true);
+	return 0;
+}
+
+SEC("?perf_event")
+int conctest_wq_nmi_delete(void *ctx)
+{
+	struct wq_elem *elem;
+	int key = 0, ret;
+
+	conctest_begin();
+	ret = bpf_map_delete_elem(&wq_map, &key);
+	if (ret == 0) {
+		elem = bpf_map_lookup_elem(&wq_map, &key);
+		if (elem) {
+			bpf_wq_init(&elem->work, &wq_map, 0);
+			bpf_wq_set_callback(&elem->work, wq_callback, 0);
+		}
+	}
+	conctest_record(CONCTEST_STAT_NMI, ret, true);
+	return 0;
+}
+
+struct tw_elem {
+	struct bpf_task_work tw;
+};
+
+struct {
+	__uint(type, BPF_MAP_TYPE_ARRAY);
+	__uint(max_entries, 1);
+	__type(key, int);
+	__type(value, struct tw_elem);
+} tw_map SEC(".maps");
+
+static int tw_callback(struct bpf_map *map, void *key, void *value)
+{
+	return 0;
+}
+
+SEC("?syscall")
+int conctest_tw_task_signal(void *ctx)
+{
+	struct tw_elem *elem;
+	int key = 0, ret;
+
+	elem = bpf_map_lookup_elem(&tw_map, &key);
+	if (!elem)
+		return 0;
+
+	conctest_begin();
+	ret = bpf_task_work_schedule_signal(bpf_get_current_task_btf(),
+					    &elem->tw, &tw_map, tw_callback);
+	conctest_record(CONCTEST_STAT_SYSCALL, ret, true);
+	return 0;
+}
+
+SEC("?syscall")
+int conctest_tw_task_resume(void *ctx)
+{
+	struct tw_elem *elem;
+	int key = 0, ret;
+
+	elem = bpf_map_lookup_elem(&tw_map, &key);
+	if (!elem)
+		return 0;
+
+	conctest_begin();
+	ret = bpf_task_work_schedule_resume(bpf_get_current_task_btf(),
+					    &elem->tw, &tw_map, tw_callback);
+	conctest_record(CONCTEST_STAT_SYSCALL, ret, true);
+	return 0;
+}
+
+SEC("?perf_event")
+int conctest_tw_nmi_signal(void *ctx)
+{
+	struct tw_elem *elem;
+	int key = 0, ret;
+
+	elem = bpf_map_lookup_elem(&tw_map, &key);
+	if (!elem)
+		return 0;
+
+	conctest_begin();
+	ret = bpf_task_work_schedule_signal(bpf_get_current_task_btf(),
+					    &elem->tw, &tw_map, tw_callback);
+	conctest_record(CONCTEST_STAT_NMI, ret, true);
+	return 0;
+}
+
+SEC("?perf_event")
+int conctest_tw_nmi_resume(void *ctx)
+{
+	struct tw_elem *elem;
+	int key = 0, ret;
+
+	elem = bpf_map_lookup_elem(&tw_map, &key);
+	if (!elem)
+		return 0;
+
+	conctest_begin();
+	ret = bpf_task_work_schedule_resume(bpf_get_current_task_btf(),
+					    &elem->tw, &tw_map, tw_callback);
+	conctest_record(CONCTEST_STAT_NMI, ret, true);
+	return 0;
+}
+
+SEC("?syscall")
+int conctest_tw_task_delete(void *ctx)
+{
+	int key = 0, ret;
+
+	conctest_begin();
+	ret = bpf_map_delete_elem(&tw_map, &key);
+	conctest_record(CONCTEST_STAT_SYSCALL, ret, true);
+	return 0;
+}
+
+SEC("?perf_event")
+int conctest_tw_nmi_delete(void *ctx)
+{
+	int key = 0, ret;
+
+	conctest_begin();
+	ret = bpf_map_delete_elem(&tw_map, &key);
+	conctest_record(CONCTEST_STAT_NMI, ret, true);
+	return 0;
+}
+
 char _license[] SEC("license") = "GPL";
-- 
2.47.3


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

* Re: [RFC PATCH bpf-next v1 1/6] bpf: Support repeat, duration fields for syscall prog runs
  2026-02-11 18:12 ` [RFC PATCH bpf-next v1 1/6] bpf: Support repeat, duration fields for syscall prog runs Kumar Kartikeya Dwivedi
@ 2026-02-11 22:02   ` Alexei Starovoitov
  0 siblings, 0 replies; 11+ messages in thread
From: Alexei Starovoitov @ 2026-02-11 22:02 UTC (permalink / raw)
  To: Kumar Kartikeya Dwivedi
  Cc: bpf, Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
	Martin KaFai Lau, Eduard Zingerman, Mykyta Yatsenko, kkd,
	Kernel Team

On Wed, Feb 11, 2026 at 10:12 AM Kumar Kartikeya Dwivedi
<memxor@gmail.com> wrote:
>
> Currently, BPF syscall programs do not support specifying the repeat
> field to repeat the test inside the kernel multiple times. Use the
> test_timer infra for other prog run tests and make it with RCU tasks
> trace to allow usage for syscall programs. Also make the duration field
> available for use.
>
> Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
> ---
>  net/bpf/test_run.c | 92 +++++++++++++++++++++++++++++++++++++---------
>  1 file changed, 74 insertions(+), 18 deletions(-)
>
> diff --git a/net/bpf/test_run.c b/net/bpf/test_run.c
> index 178c4738e63b..4e06e516d9f2 100644
> --- a/net/bpf/test_run.c
> +++ b/net/bpf/test_run.c
> @@ -33,23 +33,27 @@ struct bpf_test_timer {
>         u64 time_start, time_spent;
>  };
>
> -static void bpf_test_timer_enter(struct bpf_test_timer *t)
> -       __acquires(rcu)
> +static void __bpf_test_timer_enter(struct bpf_test_timer *t, bool trace)
>  {
> -       rcu_read_lock_dont_migrate();
> +       if (trace)
> +               rcu_read_lock_trace();
> +       else
> +               rcu_read_lock_dont_migrate();
>         t->time_start = ktime_get_ns();
>  }
>
> -static void bpf_test_timer_leave(struct bpf_test_timer *t)
> -       __releases(rcu)
> +static void __bpf_test_timer_leave(struct bpf_test_timer *t, bool trace)
>  {
>         t->time_start = 0;
> -       rcu_read_unlock_migrate();
> +       if (trace)
> +               rcu_read_unlock_trace();
> +       else
> +               rcu_read_unlock_migrate();
>  }
>
> -static bool bpf_test_timer_continue(struct bpf_test_timer *t, int iterations,
> -                                   u32 repeat, int *err, u32 *duration)
> -       __must_hold(rcu)
> +static bool __bpf_test_timer_continue(struct bpf_test_timer *t,
> +                                     int iterations, u32 repeat,
> +                                     int *err, u32 *duration, bool trace)
>  {
>         t->i += iterations;
>         if (t->i >= repeat) {
> @@ -70,9 +74,9 @@ static bool bpf_test_timer_continue(struct bpf_test_timer *t, int iterations,
>         if (need_resched()) {
>                 /* During iteration: we need to reschedule between runs. */
>                 t->time_spent += ktime_get_ns() - t->time_start;
> -               bpf_test_timer_leave(t);
> +               __bpf_test_timer_leave(t, trace);
>                 cond_resched();
> -               bpf_test_timer_enter(t);
> +               __bpf_test_timer_enter(t, trace);
>         }
>
>         /* Do another round. */
> @@ -83,6 +87,45 @@ static bool bpf_test_timer_continue(struct bpf_test_timer *t, int iterations,
>         return false;
>  }
>
> +static void bpf_test_timer_enter(struct bpf_test_timer *t)
> +       __acquires(rcu)
> +{
> +       __bpf_test_timer_enter(t, false);
> +}
> +
> +static void bpf_test_timer_leave(struct bpf_test_timer *t)
> +       __releases(rcu)
> +{
> +       __bpf_test_timer_leave(t, false);
> +}
> +
> +static bool bpf_test_timer_continue(struct bpf_test_timer *t, int iterations,
> +                                   u32 repeat, int *err, u32 *duration)
> +       __must_hold(rcu)
> +{
> +       return __bpf_test_timer_continue(t, iterations, repeat, err, duration, false);
> +}
> +
> +static void bpf_test_timer_enter_trace(struct bpf_test_timer *t)
> +       __acquires(rcu_trace)
> +{
> +       __bpf_test_timer_enter(t, true);
> +}

rcu_task_trace doesn't exist any more. We should move away
from that name as well.
Especially here '_trace' is misleading. What you meant is task_trace
and that it's sleepable.
So let's use either "_sleepable" suffix and replace 'bool trace'
to 'bool sleepable'?
or use "_srcu" if you want to describe the logic inside rcu_read_lock_trace().
At least "s" in srcu also means sleepable.

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

* Re: [RFC PATCH bpf-next v1 3/6] bpf: Enable rqspinlock in tracing progs
  2026-02-11 18:12 ` [RFC PATCH bpf-next v1 3/6] bpf: Enable rqspinlock in tracing progs Kumar Kartikeya Dwivedi
@ 2026-02-11 22:04   ` Alexei Starovoitov
  0 siblings, 0 replies; 11+ messages in thread
From: Alexei Starovoitov @ 2026-02-11 22:04 UTC (permalink / raw)
  To: Kumar Kartikeya Dwivedi
  Cc: bpf, Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
	Martin KaFai Lau, Eduard Zingerman, Mykyta Yatsenko, kkd,
	Kernel Team

On Wed, Feb 11, 2026 at 10:12 AM Kumar Kartikeya Dwivedi
<memxor@gmail.com> wrote:
>
> Allow using rqspinlock in tracing programs, since it is safe to acquire

...

> -               if (is_tracing_prog_type(prog_type)) {
> +               if (is_tracing_prog_type(prog_type) &&
> +                   btf_record_has_field(map->record, BPF_SPIN_LOCK)) {

hmm. Did you mean BPF_RES_SPIN_LOCK ?

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

* Re: [RFC PATCH bpf-next v1 4/6] selftests/bpf: Introduce concurrency testing tool
  2026-02-11 18:12 ` [RFC PATCH bpf-next v1 4/6] selftests/bpf: Introduce concurrency testing tool Kumar Kartikeya Dwivedi
@ 2026-02-11 22:08   ` Alexei Starovoitov
  0 siblings, 0 replies; 11+ messages in thread
From: Alexei Starovoitov @ 2026-02-11 22:08 UTC (permalink / raw)
  To: Kumar Kartikeya Dwivedi
  Cc: bpf, Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
	Martin KaFai Lau, Eduard Zingerman, Mykyta Yatsenko, kkd,
	Kernel Team

On Wed, Feb 11, 2026 at 10:13 AM Kumar Kartikeya Dwivedi
<memxor@gmail.com> wrote:
>
> +
> +struct lock_val {
> +       struct bpf_res_spin_lock lock;
> +};
> +
> +struct {
> +       __uint(type, BPF_MAP_TYPE_ARRAY);
> +       __uint(max_entries, 3);
> +       __type(key, int);
> +       __type(value, struct lock_val);
> +} lock_map SEC(".maps");
> +
> +SEC("?syscall")
> +int conctest_rqspinlock_task(void *ctx)
> +{
> +       struct lock_val *e;
> +       __u32 key;
> +       int ret;
> +
> +       key = bpf_get_smp_processor_id();
> +       key %= (__u32)nr_cpus;
> +       e = bpf_map_lookup_elem(&lock_map, &key);
> +       if (!e)
> +               return 0;

Pls remove all of the 'if (!e)' checks when doing lookup from an array
with a constant.
I'm not sure in the above the verifier is smart enough,
but I think we improved bpf_mod logic recently, so should be.
If not, improve the verifier :) and let's get rid of this legacy 'if' checks.

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

* Re: [RFC PATCH bpf-next v1 5/6] selftests/bpf: Generate various conctest permutations
  2026-02-11 18:12 ` [RFC PATCH bpf-next v1 5/6] selftests/bpf: Generate various conctest permutations Kumar Kartikeya Dwivedi
@ 2026-02-11 22:09   ` Alexei Starovoitov
  0 siblings, 0 replies; 11+ messages in thread
From: Alexei Starovoitov @ 2026-02-11 22:09 UTC (permalink / raw)
  To: Kumar Kartikeya Dwivedi
  Cc: bpf, Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
	Martin KaFai Lau, Eduard Zingerman, Mykyta Yatsenko, kkd,
	Kernel Team

On Wed, Feb 11, 2026 at 10:13 AM Kumar Kartikeya Dwivedi
<memxor@gmail.com> wrote:
>
> +SEC("?syscall")
> +int conctest_timer_task_reinit(void *ctx)
> +{
> +       struct timer_elem *elem;
> +       int key = 0, ret;
> +
> +       elem = bpf_map_lookup_elem(&timer_map, &key);
> +       if (!elem)
> +               return 0;

here it definitely can be removed.

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

end of thread, other threads:[~2026-02-11 22:09 UTC | newest]

Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-02-11 18:12 [RFC PATCH bpf-next v1 0/6] Concurrency Testing Kumar Kartikeya Dwivedi
2026-02-11 18:12 ` [RFC PATCH bpf-next v1 1/6] bpf: Support repeat, duration fields for syscall prog runs Kumar Kartikeya Dwivedi
2026-02-11 22:02   ` Alexei Starovoitov
2026-02-11 18:12 ` [RFC PATCH bpf-next v1 2/6] bpf: Allow timing functions in lock critical sections Kumar Kartikeya Dwivedi
2026-02-11 18:12 ` [RFC PATCH bpf-next v1 3/6] bpf: Enable rqspinlock in tracing progs Kumar Kartikeya Dwivedi
2026-02-11 22:04   ` Alexei Starovoitov
2026-02-11 18:12 ` [RFC PATCH bpf-next v1 4/6] selftests/bpf: Introduce concurrency testing tool Kumar Kartikeya Dwivedi
2026-02-11 22:08   ` Alexei Starovoitov
2026-02-11 18:12 ` [RFC PATCH bpf-next v1 5/6] selftests/bpf: Generate various conctest permutations Kumar Kartikeya Dwivedi
2026-02-11 22:09   ` Alexei Starovoitov
2026-02-11 18:12 ` [RFC PATCH bpf-next v1 6/6] selftests/bpf: Extend conctest to wq and task_work Kumar Kartikeya Dwivedi

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox