From: sashiko-bot@kernel.org
To: "Radu Toader" <radu.m.toader@gmail.com>
Cc: sched-ext@lists.linux.dev, bpf@vger.kernel.org
Subject: Re: [PATCH 1/2] sched_ext: Add scx_tickbug, a scheduler that holds a task on an idle CPU
Date: Fri, 06 Nov 2026 10:15:36 +0000 [thread overview]
Message-ID: <sashiko-outbox-201192@kernel.org> (raw)
In-Reply-To: <20261106094343.174905-2-radu.m.toader@gmail.com>
Thank you for your contribution! Sashiko AI review found 4 potential
issues to consider:
- [Medium] sched_ext: stale hold_q entry consumed in release_one() after
dequeue
- [Medium] sched_ext: skipped hold and local DSQ trap in handle_tagged()
- [Medium] sched_ext: untagged SCX_ENQ_LAST tasks kept on target_cpu in
tickbug_enqueue()
- [Medium] sched_ext: untagged wakeups disturb target_cpu in
tickbug_select_cpu()
--
commit 36aa4109ffa11a9bc7eddd270c091950516a7eb6
Author: Radu Toader <radu.m.toader@gmail.com>
sched_ext: Add scx_tickbug, a scheduler that holds a task on an idle CPU
This commit adds the scx_tickbug test scheduler, which parks tasks named
"spin" in a BPF queue on a target CPU's run queue without kicking the CPU,
releasing them periodically via a timer pinned to another CPU.
> diff --git a/tools/sched_ext/scx_tickbug.bpf.c b/tools/sched_ext/scx_tickbug.bpf.c
> new file mode 100644
> index 0000000000000..3d7ce74236289
> --- /dev/null
> +++ b/tools/sched_ext/scx_tickbug.bpf.c
[ ... ]
> +s32 BPF_STRUCT_OPS(tickbug_select_cpu, struct task_struct *p,
> + s32 prev_cpu, u64 wake_flags)
> +{
> + if (is_tagged(p))
> + return target_cpu;
> + return prev_cpu;
> +}
[Severity: Medium]
When an untagged task with prev_cpu == target_cpu wakes up while target_cpu
is idling with a held task, doesn't returning prev_cpu here break
target_cpu out of the idle loop under test?
ttwu_do_activate() activates the task on target_cpu's run queue and
wakeup_preempt() calls resched_curr() because ext_sched_class is above
idle_sched_class. That forces target_cpu out of do_idle() through
tick_nohz_idle_exit() and back into tick_nohz_idle_enter(), resetting
TS_FLAG_IDLE_ACTIVE mid-hold, while tickbug_enqueue() places the task into
SHARED_DSQ without kicking an idle non-target CPU to run it.
[ ... ]
> +static bool handle_tagged(struct task_struct *p, struct task_ctx *tctx,
> + u64 enq_flags)
> +{
[ ... ]
> + if (!(enq_flags & SCX_ENQ_LAST) && scx_bpf_task_running(p)) {
> + /*
> + * Re-enqueued while it is the current scheduling context, e.g.
> + * after a kthread preempted it. Holding it here would take CPU
> + * away from work already in progress, which is not the state
> + * under test: the hold belongs at the start of a run, not in
> + * the middle of one. Put it straight back so it carries on.
> + *
> + * %SCX_ENQ_LAST is excluded because it is not that case. It
> + * arrives from put_prev_task_scx(), where rq->donor is still
> + * @p - put_prev_set_next_task() asserts as much - so
> + * scx_bpf_task_running() is true for every enqueue on that
> + * path, while the CPU is in fact about to go idle with @p as
> + * the only runnable task. That is what has to be held.
> + */
> + __sync_fetch_and_add(&nr_requeue, 1);
> + scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, slice_ns, enq_flags);
> + return true;
> + }
[Severity: Medium]
Can a tagged task exhausting its slice in put_prev_task_scx() hit this
branch and skip being held when next is not the idle task?
When a running tagged task on target_cpu runs out of slice,
do_pick_task_scx() calls tickbug_dispatch() before put_prev_task_scx()
enqueues the outgoing task. Because nr_held_now is 0 during
tickbug_dispatch(), target_cpu pulls from SHARED_DSQ; if an untagged task
is moved to the local DSQ, put_prev_task_scx() sees going_idle == false and
enqueues the slice-expired tagged task without SCX_ENQ_LAST while rq->donor
is still that task, so handle_tagged() inserts it into SCX_DSQ_LOCAL
instead of holding it.
Similarly, if two tagged tasks are held in hold_q, the timer firing for the
second task while the first is running preempts the first task and
dispatches the second into SCX_DSQ_LOCAL. put_prev_task_scx() then
re-enqueues the first task without SCX_ENQ_LAST into SCX_DSQ_LOCAL, after
which both tasks stay in SCX_DSQ_LOCAL trading slices forever without
calling tickbug_dispatch() or being held again.
[ ... ]
> +void BPF_STRUCT_OPS(tickbug_enqueue, struct task_struct *p, u64 enq_flags)
> +{
[ ... ]
> + /*
> + * A %SCX_ENQ_LAST task stays on its own CPU. put_prev_task_scx() sets
> + * need_resched on the incoming idle task only if the enqueue landed on
> + * the local DSQ, so a task left in a shared DSQ here would sit there
> + * until some other CPU happened to drain it.
> + */
> + if (last)
> + scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, slice_ns, enq_flags);
> + else
> + scx_bpf_dsq_insert(p, SHARED_DSQ, slice_ns, enq_flags);
> +}
[Severity: Medium]
If an untagged task is running on target_cpu when a tagged task wakes up
and is parked in hold_q, won't inserting the untagged task into
SCX_DSQ_LOCAL on SCX_ENQ_LAST keep target_cpu busy throughout the hold?
When the untagged task's slice expires on target_cpu, tickbug_dispatch()
sees nr_held_now > 0 and returns early so target_cpu will enter the idle
loop. That makes pick_next_task() select rq->idle, so put_prev_task_scx()
calls tickbug_enqueue() with SCX_ENQ_LAST for the untagged task; putting it
back into SCX_DSQ_LOCAL causes put_prev_task_scx() to set need_resched on
rq->idle and immediately run the untagged task again, inflating
total_held_ns and max_held_ns with time target_cpu spent running.
> +
> +void BPF_STRUCT_OPS(tickbug_dequeue, struct task_struct *p, u64 deq_flags)
> +{
> + struct task_ctx *tctx;
> +
> + tctx = bpf_task_storage_get(&task_ctx_stor, p, 0, 0);
> + if (!tctx || !tctx->held)
> + return;
> +
> + /*
> + * A parked task is leaving BPF custody without going through
> + * release_one(): an affinity or property change, a class change, or
> + * exit. Give up the hold now. Its entry stays in @hold_q, where
> + * release_one() recognises it by this flag and drops it.
> + */
> + tctx->held = false;
> + __sync_fetch_and_add(&nr_undone, 1);
> + __sync_fetch_and_sub(&nr_held_now, 1);
> +}
> +
> +/* Let one held task run on the CPU we are dispatching for. */
> +static bool release_one(void)
> +{
> + struct hold_entry ent;
> + struct task_ctx *tctx;
> + struct task_struct *p;
> +
> + bpf_repeat(BPF_MAX_LOOPS) {
[ ... ]
> + tctx = bpf_task_storage_get(&task_ctx_stor, p, 0, 0);
> + if (!tctx || !tctx->held) {
> + __sync_fetch_and_add(&nr_stale, 1);
> + bpf_task_release(p);
> + continue;
> + }
> +
> + tctx->held = false;
> + __sync_fetch_and_sub(&nr_held_now, 1);
[Severity: Medium]
What happens in release_one() if a parked task goes through
tickbug_dequeue() and is re-enqueued into hold_q before its undone entry is
popped?
A property or affinity change on a parked task (such as setpriority() or
sched_setaffinity()) runs tickbug_dequeue(), which clears tctx->held while
leaving the older hold_entry in hold_q, and then immediately calls
tickbug_enqueue(), which sees tctx->held == false, pushes a second
hold_entry for the same pid, and sets tctx->held = true. When
release_one() pops the older undone entry first, it sees tctx->held == true
and consumes the stale entry as live, leaving hold_q off by one for that
task and corrupting total_held_ns and max_held_ns with the older timestamp
on every subsequent release. Storing the active hold timestamp (or a
sequence number) in task_ctx and matching it against ent.at in
release_one() would distinguish stale entries from the current hold.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261106094343.174905-1-radu.m.toader@gmail.com?part=1
next prev parent reply other threads:[~2026-11-06 10:15 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-06 9:43 [PATCH 0/2] softirq: fix idle accounting for an interrupted idle task Radu Toader
2026-11-06 9:43 ` [PATCH 1/2] sched_ext: Add scx_tickbug, a scheduler that holds a task on an idle CPU Radu Toader
2026-11-06 10:15 ` sashiko-bot [this message]
2026-11-06 16:59 ` Tejun Heo
2026-11-07 2:15 ` Radu Toader
2026-11-06 20:10 ` bot+bpf-ci
2026-11-07 2:15 ` Radu Toader
2026-11-06 9:43 ` [PATCH 2/2] softirq: Treat an interrupted idle task as idle in tick_irq_exit() Radu Toader
2026-11-06 10:23 ` Peter Zijlstra
2026-11-07 2:15 ` Radu Toader
2026-11-06 19:55 ` bot+bpf-ci
2026-11-07 2:15 ` Radu Toader
-- strict thread matches above, loose matches on Subject: below --
2026-11-05 20:02 [PATCH 0/2] softirq: fix idle accounting for an interrupted idle task Radu Toader
2026-11-05 20:02 ` [PATCH 1/2] sched_ext: Add scx_tickbug, a scheduler that holds a task on an idle CPU Radu Toader
2026-11-05 20:33 ` sashiko-bot
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=sashiko-outbox-201192@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=radu.m.toader@gmail.com \
--cc=sashiko-reviews@lists.linux.dev \
--cc=sched-ext@lists.linux.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.