Re: [scx_nest] bpf_timer_cancel() unusable from select_cpu()

Zhan Xusheng <[email protected]>
Newsgroups dev.linux.lists.sched-ext
Message-ID <[email protected]>
On Fri, 21 Aug 2026 01:39:23 +0000, yaoyiqi (A) wrote:
> Testing showed that on this kernel bpf_timer_cancel_async(&timer) also
> returns an error from the same context, so this appears to be a broader
> restriction than the sync/async split: cancelling a CPU-pinned timer is
> not usable from a sched_ext hot-path callback at all.

Which value did it return? From .select_cpu() the expected one is
-ECANCELED, and that is not a refusal: kernel/bpf/helpers.c:4737 lists it
as "-ECANCELED when the timer will be cancelled asynchronously". The path
is helpers.c:4762-4770, where bpf_timer_cancel_async() hands the work to
bpf_async_schedule_op(cb, BPF_ASYNC_CANCEL, 0, 0) and reports the
deferral rather than declining it. A plain "< 0" test reads that as a
failure. The other two possibilities there are -ENOMEM from
kmalloc_nolock() and -ENOENT from a racing deletion, both real errors.

If it was -ECANCELED then the sync/async split is the whole story, and
the gate is neither sleepability nor sched_ext:

  static bool defer_timer_wq_op(void)
  {
	return in_hardirq() || irqs_disabled();
  }

helpers.c:1515. .select_cpu() qualifies through irqs_disabled():
try_to_wake_up() holds p->pi_lock across the wakeup via
scoped_guard(raw_spinlock_irqsave) at kernel/sched/core.c:4302 and calls
select_task_rq() inside it at 4403, which dispatches through
sched_class->select_task_rq (core.c:3629) into select_task_rq_scx() and
SCX_CALL_OP_TASK_RET(sch, select_cpu, ...) at kernel/sched/ext/ext.c:3530
and 3558. So a sched_ext callback that runs with IRQs enabled is
unaffected, and this is not specific to CPU-pinned timers.

That also answers whether it is intentional: bpf_timer_cancel() refuses
at helpers.c:1571 because it would otherwise reach hrtimer_cancel() at
1615 and wait there for a running callback, while the async variant was
given a deferral path for exactly this context.

One difference if you switch to it: the async variant leaves the callback
installed. bpf_async_update_prog_callback(&t->cb, NULL, NULL) appears
only in the sync path (helpers.c:1611) and in cancel_and_free (1694), so
the bpf_timer_set_callback() re-arm your original code needed after
bpf_timer_cancel() is not needed after bpf_timer_cancel_async().

Your flag-based workaround is what the Rust schedulers do and avoids the
question entirely, so none of this asks you to change it back.

On documentation, the async return codes are all listed at
helpers.c:4731-4740. The gap is on the sync side, where
include/uapi/linux/bpf.h:5456-5461 gives 0, 1, -EINVAL and -EDEADLK for
bpf_timer_cancel() and not the -EOPNOTSUPP you hit.

Thanks,
Zhan Xusheng
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.