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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox