[PATCH sched_ext/for-7.2-fixes] selftests/sched_ext: Make allowed_cpus idle validation race-free
Andrea Righi <[email protected]> Sun, 26 Jul 2026 08:47:54 +0200
| Newsgroups | dev.linux.lists.sched-ext,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <[email protected]> |
A remotely selected CPU can be re-advertised as idle by an idle-to-idle re-pick before the BPF program validates the selection. Checking that the selected CPU remains absent from the idle mask is therefore inherently racy. Validate the stable local invariant instead: a CPU running a non-idle scheduling context in ops.select_cpu() must not be advertised as idle. Also validate both the requested domain and task affinity for selected CPUs. Moreover, bootstrap the test by running a task on every active CPU while ops.running() refreshes the initial idle state. This ensures that the idle masks are properly initialized before strict validation begins. Signed-off-by: Andrea Righi <[email protected]> --- .../selftests/sched_ext/allowed_cpus.bpf.c | 51 ++++++++++++++++--- .../selftests/sched_ext/allowed_cpus.c | 38 ++++++++++++++ 2 files changed, 82 insertions(+), 7 deletions(-) diff --git a/tools/testing/selftests/sched_ext/allowed_cpus.bpf.c b/tools/testing/selftests/sched_ext/allowed_cpus.bpf.c index 35923e74a2ec3..4a14b05065453 100644 --- a/tools/testing/selftests/sched_ext/allowed_cpus.bpf.c +++ b/tools/testing/selftests/sched_ext/allowed_cpus.bpf.c @@ -13,17 +13,46 @@ char _license[] SEC("license") = "GPL"; UEI_DEFINE(uei); private(PREF_CPUS) struct bpf_cpumask __kptr * allowed_cpumask; +volatile bool refresh_idle_masks; static void -validate_idle_cpu(const struct task_struct *p, const struct cpumask *allowed, s32 cpu) +validate_local_idle_state(void) { - if (scx_bpf_test_and_clear_cpu_idle(cpu)) - scx_bpf_error("CPU %d should be marked as busy", cpu); + struct task_struct *curr; + s32 cpu = bpf_get_smp_processor_id(); + bool curr_is_idle; - if (bpf_cpumask_subset(allowed, p->cpus_ptr) && - !bpf_cpumask_test_cpu(cpu, allowed)) + bpf_rcu_read_lock(); + curr = scx_bpf_cpu_curr(cpu); + curr_is_idle = curr && (curr->flags & PF_IDLE); + bpf_rcu_read_unlock(); + + /* + * Unlike a remote selected CPU, the local CPU cannot go through an + * idle re-pick while this callback is running. If it is running a + * non-idle scheduling context, it must not be advertised as idle. + */ + if (!curr_is_idle && scx_bpf_test_and_clear_cpu_idle(cpu) && !refresh_idle_masks) + scx_bpf_error("running CPU %d should be marked as busy", cpu); +} + +static void +validate_selected_cpu(const struct task_struct *p, s32 cpu) +{ + const struct cpumask *allowed = cast_mask(allowed_cpumask); + + if (!allowed) { + scx_bpf_error("allowed domain not initialized"); + return; + } + + if (!bpf_cpumask_test_cpu(cpu, allowed)) scx_bpf_error("CPU %d not in the allowed domain for %d (%s)", cpu, p->pid, p->comm); + + if (!bpf_cpumask_test_cpu(cpu, p->cpus_ptr)) + scx_bpf_error("CPU %d not in the affinity mask for %d (%s)", + cpu, p->pid, p->comm); } s32 BPF_STRUCT_OPS(allowed_cpus_select_cpu, @@ -42,8 +71,9 @@ s32 BPF_STRUCT_OPS(allowed_cpus_select_cpu, * Select an idle CPU strictly within the allowed domain. */ cpu = scx_bpf_select_cpu_and(p, prev_cpu, wake_flags, allowed, 0); + validate_local_idle_state(); if (cpu >= 0) { - validate_idle_cpu(p, allowed, cpu); + validate_selected_cpu(p, cpu); scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, SCX_SLICE_DFL, 0); return cpu; @@ -71,11 +101,17 @@ void BPF_STRUCT_OPS(allowed_cpus_enqueue, struct task_struct *p, u64 enq_flags) */ cpu = scx_bpf_select_cpu_and(p, prev_cpu, 0, allowed, 0); if (cpu >= 0) { - validate_idle_cpu(p, allowed, cpu); + validate_selected_cpu(p, cpu); scx_bpf_kick_cpu(cpu, SCX_KICK_IDLE); } } +void BPF_STRUCT_OPS(allowed_cpus_running, struct task_struct *p) +{ + if (refresh_idle_masks) + scx_bpf_test_and_clear_cpu_idle(bpf_get_smp_processor_id()); +} + s32 BPF_STRUCT_OPS_SLEEPABLE(allowed_cpus_init) { struct bpf_cpumask *mask; @@ -138,6 +174,7 @@ SEC(".struct_ops.link") struct sched_ext_ops allowed_cpus_ops = { .select_cpu = (void *)allowed_cpus_select_cpu, .enqueue = (void *)allowed_cpus_enqueue, + .running = (void *)allowed_cpus_running, .init = (void *)allowed_cpus_init, .exit = (void *)allowed_cpus_exit, .name = "allowed_cpus", diff --git a/tools/testing/selftests/sched_ext/allowed_cpus.c b/tools/testing/selftests/sched_ext/allowed_cpus.c index 093f285ab4bae..eb1708e55982b 100644 --- a/tools/testing/selftests/sched_ext/allowed_cpus.c +++ b/tools/testing/selftests/sched_ext/allowed_cpus.c @@ -3,6 +3,7 @@ * Copyright (c) 2025 Andrea Righi <[email protected]> */ #include <bpf/bpf.h> +#include <sched.h> #include <scx/common.h> #include <sys/wait.h> #include <unistd.h> @@ -47,14 +48,51 @@ static int test_select_cpu_from_user(const struct allowed_cpus *skel) return 0; } +/* + * Run this task once on every CPU while ops.running() repairs the bootstrap + * idle state. Once a CPU has been refreshed, subsequent idle transitions keep + * its state up to date. + */ +static int refresh_idle_masks(void) +{ + cpu_set_t original, one; + int cpu, ret = 0; + + if (sched_getaffinity(0, sizeof(original), &original)) + return -errno; + + for (cpu = 0; cpu < CPU_SETSIZE; cpu++) { + if (!CPU_ISSET(cpu, &original)) + continue; + + CPU_ZERO(&one); + CPU_SET(cpu, &one); + if (sched_setaffinity(0, sizeof(one), &one)) { + ret = -errno; + break; + } + + sched_yield(); + } + + if (sched_setaffinity(0, sizeof(original), &original) && !ret) + ret = -errno; + + return ret; +} + static enum scx_test_status run(void *ctx) { struct allowed_cpus *skel = ctx; struct bpf_link *link; + skel->bss->refresh_idle_masks = true; link = bpf_map__attach_struct_ops(skel->maps.allowed_cpus_ops); SCX_FAIL_IF(!link, "Failed to attach scheduler"); + SCX_FAIL_IF(refresh_idle_masks(), "Failed to refresh idle CPU state"); + __atomic_store_n(&skel->bss->refresh_idle_masks, false, __ATOMIC_RELEASE); + /* Pick an idle CPU from user-space */ SCX_FAIL_IF(test_select_cpu_from_user(skel), "Failed to pick idle CPU"); -- 2.55.0