[PATCH v2 2/2] selftests/sched_ext: Test that ops.dequeue() can iterate the consumed DSQ
From: Qiurong Fang
Date: Tue Sep 15 2026 - 09:28:21 EST
From: fangqiurong <fangqiurong@xxxxxxxxxx>
Add a scheduler whose ops.dequeue() iterates the user DSQ tasks are
dispatched from with bpf_iter_scx_dsq. The iteration takes the DSQ's raw
spinlock; on a kernel that runs ops.dequeue() while the consume path
still holds that lock, the first task consumed self-deadlocks the CPU
until the scheduler watchdog fires and the test fails with a UEI. On a
fixed kernel the scheduler runs clean and the test passes.
Signed-off-by: fangqiurong <fangqiurong@xxxxxxxxxx>
---
tools/testing/selftests/sched_ext/Makefile | 1 +
.../selftests/sched_ext/dequeue_iter.bpf.c | 65 +++++++++++++
.../selftests/sched_ext/dequeue_iter.c | 94 +++++++++++++++++++
3 files changed, 160 insertions(+)
create mode 100644 tools/testing/selftests/sched_ext/dequeue_iter.bpf.c
create mode 100644 tools/testing/selftests/sched_ext/dequeue_iter.c
diff --git a/tools/testing/selftests/sched_ext/Makefile b/tools/testing/selftests/sched_ext/Makefile
index 5f5dd9ab903a..08c2646cdd1a 100644
--- a/tools/testing/selftests/sched_ext/Makefile
+++ b/tools/testing/selftests/sched_ext/Makefile
@@ -164,6 +164,7 @@ all_test_bpfprogs := $(foreach prog,$(wildcard *.bpf.c),$(INCLUDE_DIR)/$(patsubs
auto-test-targets := \
create_dsq \
dequeue \
+ dequeue_iter \
enq_last_no_enq_fails \
ddsp_bogus_dsq_fail \
ddsp_vtimelocal_fail \
diff --git a/tools/testing/selftests/sched_ext/dequeue_iter.bpf.c b/tools/testing/selftests/sched_ext/dequeue_iter.bpf.c
new file mode 100644
index 000000000000..406a767c31b2
--- /dev/null
+++ b/tools/testing/selftests/sched_ext/dequeue_iter.bpf.c
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ops.dequeue() of this scheduler iterates the user DSQ tasks are
+ * consumed from with bpf_iter_scx_dsq, which takes the DSQ lock.
+ * On a kernel that still runs ops.dequeue() with that lock held, the
+ * iteration self-deadlocks the CPU - this test wedges the system on
+ * unfixed kernels instead of failing cleanly.
+ *
+ * Copyright (c) 2026 fangqiurong <fangqiurong@xxxxxxxxxx>
+ */
+
+#include <scx/common.bpf.h>
+
+char _license[] SEC("license") = "GPL";
+
+UEI_DEFINE(uei);
+
+#define TEST_DSQ_ID 1000
+
+u64 dq_count;
+
+s32 BPF_STRUCT_OPS_SLEEPABLE(dequeue_iter_init)
+{
+ return scx_bpf_create_dsq(TEST_DSQ_ID, -1);
+}
+
+void BPF_STRUCT_OPS(dequeue_iter_enqueue, struct task_struct *p, u64 enq_flags)
+{
+ scx_bpf_dsq_insert(p, TEST_DSQ_ID, SCX_SLICE_DFL, enq_flags);
+}
+
+void BPF_STRUCT_OPS(dequeue_iter_dispatch, s32 cpu, struct task_struct *task)
+{
+ scx_bpf_dsq_move_to_local(TEST_DSQ_ID, 0);
+}
+
+void BPF_STRUCT_OPS(dequeue_iter_dequeue, struct task_struct *p, u64 deq_flags)
+{
+ struct bpf_iter_scx_dsq it;
+ struct task_struct *t;
+
+ if (!bpf_iter_scx_dsq_new(&it, TEST_DSQ_ID, 0)) {
+ while ((t = bpf_iter_scx_dsq_next(&it)))
+ ;
+ }
+ bpf_iter_scx_dsq_destroy(&it);
+
+ __sync_fetch_and_add(&dq_count, 1);
+}
+
+void BPF_STRUCT_OPS(dequeue_iter_exit, struct scx_exit_info *ei)
+{
+ scx_bpf_destroy_dsq(TEST_DSQ_ID);
+}
+
+SEC(".struct_ops.link")
+struct sched_ext_ops dequeue_iter_ops = {
+ .init = (void *)dequeue_iter_init,
+ .enqueue = (void *)dequeue_iter_enqueue,
+ .dispatch = (void *)dequeue_iter_dispatch,
+ .dequeue = (void *)dequeue_iter_dequeue,
+ .exit = (void *)dequeue_iter_exit,
+ .timeout_ms = 5000,
+ .name = "dequeue_iter",
+};
diff --git a/tools/testing/selftests/sched_ext/dequeue_iter.c b/tools/testing/selftests/sched_ext/dequeue_iter.c
new file mode 100644
index 000000000000..35bc52cfc474
--- /dev/null
+++ b/tools/testing/selftests/sched_ext/dequeue_iter.c
@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (c) 2026 fangqiurong <fangqiurong@xxxxxxxxxx>
+ */
+#include <bpf/bpf.h>
+#include <pthread.h>
+#include <sched.h>
+#include <scx/common.h>
+#include <time.h>
+#include <unistd.h>
+#include "dequeue_iter.bpf.skel.h"
+#include "scx_test.h"
+
+#define DQ_TARGET 10
+#define DQ_DEADLINE_MS 3000
+
+static unsigned long long now_ms(void)
+{
+ struct timespec ts;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+
+ return ts.tv_sec * 1000ULL + ts.tv_nsec / 1000000;
+}
+
+static void *run_workload(void *arg)
+{
+ struct dequeue_iter *skel = arg;
+ unsigned long long end = now_ms() + DQ_DEADLINE_MS;
+
+ while (skel->bss->dq_count < DQ_TARGET && now_ms() < end)
+ usleep(100);
+
+ return NULL;
+}
+
+static enum scx_test_status setup(void **ctx)
+{
+ struct dequeue_iter *skel;
+
+ skel = dequeue_iter__open();
+ SCX_FAIL_IF(!skel, "Failed to open");
+ SCX_ENUM_INIT(skel);
+ SCX_FAIL_IF(dequeue_iter__load(skel), "Failed to load skel");
+
+ *ctx = skel;
+
+ return SCX_TEST_PASS;
+}
+
+static enum scx_test_status run(void *ctx)
+{
+ struct dequeue_iter *skel = ctx;
+ struct bpf_link *link;
+ pthread_t tid;
+
+ link = bpf_map__attach_struct_ops(skel->maps.dequeue_iter_ops);
+ SCX_FAIL_IF(!link, "Failed to attach scheduler");
+
+ SCX_FAIL_IF(pthread_create(&tid, NULL, run_workload, skel),
+ "Failed to create workload thread");
+ pthread_join(tid, NULL);
+ bpf_link__destroy(link);
+
+ if (UEI_EXITED(skel, uei)) {
+ UEI_REPORT(skel, uei);
+ SCX_ERR("Scheduler exited unexpectedly\n");
+ return SCX_TEST_FAIL;
+ }
+
+ if (skel->bss->dq_count < DQ_TARGET) {
+ SCX_ERR("ops.dequeue() fired only %llu times\n",
+ (unsigned long long)skel->bss->dq_count);
+ return SCX_TEST_FAIL;
+ }
+
+ return SCX_TEST_PASS;
+}
+
+static void cleanup(void *ctx)
+{
+ struct dequeue_iter *skel = ctx;
+
+ dequeue_iter__destroy(skel);
+}
+
+struct scx_test dequeue_iter = {
+ .name = "dequeue_iter",
+ .description = "Verify ops.dequeue() can iterate its source user DSQ",
+ .setup = setup,
+ .run = run,
+ .cleanup = cleanup,
+};
+REGISTER_SCX_TEST(&dequeue_iter)
--
2.43.0