From: sashiko-bot@kernel.org
To: "Tejun Heo" <tj@kernel.org>, sched-ext@lists.linux.dev
Subject: Re: [PATCH 13/18] sched_ext: Fix bypassed tasks stalling when the host lacks SCX_CAP_BASE
Date: Tue, 27 Oct 2026 00:44:18 +0000 [thread overview]
Message-ID: <sashiko-outbox-185966@kernel.org> (raw)
In-Reply-To: <20261027001749.1821379-14-tj@kernel.org>
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] sched_ext: missing slice end for NONE tasks in __scx_disable_and_exit_task()
- [High] sched_ext: misattributed rescue usage and stalls for bypassed tasks
--
commit 22b95f16249b92e8730c0989de8ef32cf7909110
Author: Tejun Heo <tj@kernel.org>
sched_ext: Fix bypassed tasks stalling when the host lacks SCX_CAP_BASE
This commit fixes bypassed tasks stalling on a CPU where their non-bypassing
host ancestor lacks SCX_CAP_BASE by resolving them against the host with
SCX_ENQ_RESCUE at enqueue time and keeping preempted rescuees on the local
DSQ during bypass.
> diff --git a/kernel/sched/ext/ext.c b/kernel/sched/ext/ext.c
> index 75f863de565a2..985a9cc24db56 100644
> --- a/kernel/sched/ext/ext.c
> +++ b/kernel/sched/ext/ext.c
> @@ -388,8 +388,12 @@ static const struct sched_class *scx_setscheduler_class(struct task_struct *p)
> return __setscheduler_class(p->policy, p->prio);
> }
>
> -static struct scx_dispatch_q *bypass_enq_target_dsq(struct scx_sched *sch, s32 cpu)
> +static struct scx_dispatch_q *bypass_enq_target_dsq(struct scx_sched *sch, struct rq *rq,
> + struct task_struct *p, u64 *enq_flags)
> {
> + struct scx_sched *host = scx_exec_sched(sch, cpu_of(rq));
> + struct scx_dispatch_q *dsq;
> +
> /*
> * If @sch is a sub-sched which is bypassing, its tasks should go into
> * the bypass DSQs of the nearest ancestor which is not bypassing. The
> @@ -400,8 +404,29 @@ static struct scx_dispatch_q *bypass_enq_target_dsq(struct scx_sched *sch, s32 c
> * Whenever a sched starts bypassing, all runnable tasks in its subtree
> * are re-enqueued after scx_bypassing() is turned on, guaranteeing that
> * all tasks are transferred to the right DSQs.
> - */
> - return scx_bypass_dsq(scx_exec_sched(sch, cpu), cpu);
> + *
> + * A cpu's dispatch only descends into a host holding %SCX_CAP_BASE on
> + * it, so the bypass DSQ of a host without BASE here is never consumed.
> + * Send @p to rescue on this cpu instead, resolving against @host.
> + * scx_dispatch_enqueue() resolves a local DSQ again, against the
> + * bypassing @sch. %SCX_ENQ_IGNORE_CAPS makes that second resolution
> + * admit @p.
> + *
> + * A migration-disabled task can't leave this cpu, and a task with a
> + * migration pending must run here until the migration moves it. Either
> + * would wait on the unconsumed bypass DSQ until the host regains BASE.
> + * Resolve them too when rescue is disabled. The resolution admits both
> + * to the local DSQ, as it does any insert of such a task.
> + */
> + if (unlikely(scx_missing_caps(host, cpu_of(rq), SCX_CAP_BASE)) &&
> + (scx_rescue_enabled() || is_migration_disabled(p) || p->migration_pending)) {
> + *enq_flags |= SCX_ENQ_RESCUE;
> + dsq = scx_resolve_local_dsq(host, rq, p, enq_flags);
> + *enq_flags |= SCX_ENQ_IGNORE_CAPS;
> + } else {
> + dsq = scx_bypass_dsq(host, cpu_of(rq));
> + }
> + return dsq;
> }
[Severity: High]
When bypass_enq_target_dsq() resolves p against host via
scx_resolve_local_dsq(), p is placed on rq->scx.rescue.dsq or admitted to
rq->scx.local_dsq, whereas scx_task_sched(p) remains the bypassing child sch
(for instance during scx_sub_disable() where sch->exit_kind is already
SCX_EXIT_DONE, or after scx_fail_parent()).
In kernel/sched/ext/sub.c, scx_rescue_charge() charges the rescue runtime to
scx_task_sched(rq->scx.rescue.curr) rather than the executing host:
kernel/sched/ext/sub.c:scx_rescue_charge() {
...
/* per-cpu usage average feeds the overload victim pick */
pcpu = per_cpu_ptr(scx_task_sched(rq->scx.rescue.curr)->pcpu,
cpu_of(rq));
pcpu->rescue_avg = scx_rescue_decay_avg(pcpu) + delta_exec;
}
When rescue becomes overloaded, scx_rescue_check_overload() skips schedulers
whose exit_kind is not SCX_EXIT_NONE:
kernel/sched/ext/sub.c:scx_rescue_check_overload() {
...
list_for_each_entry_rcu(pos, &scx_sched_all, all) {
u64 avg = scx_rescue_decay_avg(per_cpu_ptr(pos->pcpu, cpu));
/* skip an already-exiting sub, else the ejection is wasted */
if (pos->level && avg > max_avg &&
atomic_read(&pos->exit_kind) == SCX_EXIT_NONE) {
max_avg = avg;
victim = pos;
}
}
}
Because the runtime was credited to the already-exiting sch instead of host,
host has avg == 0 and is not selected, which can cause
scx_rescue_check_overload() to either eject an unrelated active sub-scheduler
on that CPU with a small non-zero rescue_avg or find no victim at all.
Similarly, while p waits on rq->scx.rescue.dsq or rq->scx.local_dsq,
dsq->sched is NULL (unlike scx_bypass_dsq(host, cpu), where dsq->sched is
host). In kernel/sched/ext/ext.c, check_rq_for_timeouts() only overrides
sch = scx_task_sched(p) when dsq->sched is non-NULL:
kernel/sched/ext/ext.c:check_rq_for_timeouts() {
...
if (dsq && dsq->sched && dsq->id != SCX_DSQ_LOCAL)
sch = dsq->sched;
__scx_exit(sch, SCX_EXIT_ERROR_STALL, 0, cpu_of(rq),
"%s[%d] failed to run for %u.%03us",
p->comm, p->pid, dur_ms / 1000,
dur_ms % 1000);
timed_out = true;
break;
}
If p stalls there while sch is already exiting, __scx_exit(sch, ...) becomes
a no-op because scx_claim_exit() fails on sch, yet check_rq_for_timeouts()
sets timed_out = true and stops scanning the rest of rq->scx.runnable_list
and the remaining CPUs in scx_watchdog_workfn(). If the stalled task holds a
lock that scx_sub_disable(sch) waits on (such as cgroup_mutex or
scx_fork_rwsem), could this prevent host from being ejected and cause
scx_sub_disable() to hang?
Should scx_rescue_charge() and check_rq_for_timeouts() attribute a bypassed
task's rescue usage and stall timeouts to scx_exec_sched(sch, cpu_of(rq))?
[ ... ]
> @@ -4984,11 +5014,14 @@ static void switched_from_scx(struct rq *rq, struct task_struct *p)
> * parent's own teardown). There is nothing to disable; calling
> * scx_disable_task() would WARN on the non-%ENABLED state and trigger a
> * NONE -> READY validation failure.
> + *
> + * @p can still be a rescuee under the parent. End its slice as
> + * scx_disable_task() does for an enabled task.
> */
> if (scx_get_task_state(p) == SCX_TASK_NONE)
> - return;
> -
> - scx_disable_task(scx_task_sched(p), p);
> + scx_task_slice_ended(rq, p);
> + else
> + scx_disable_task(scx_task_sched(p), p);
> }
>
> static void switched_to_scx(struct rq *rq, struct task_struct *p) {}
[Severity: High]
What happens if a running SCX_TASK_NONE rescuee leaves its bypassing parent
without changing scheduler class, such as via scx_sub_disable() ->
scx_rehome_task(), scx_cgroup_task_migrated() -> scx_rehome_task(), or
another scx_fail_parent() -> scx_punt_task()?
When scx_fail_parent() punts p to parent at SCX_TASK_NONE, it has already
called scx_bypass(parent, true). When scx_sub_disable(parent) later calls
scx_bypass(parent, true), inc_bypass_depth() sees parent->bypass_depth > 1
and does not walk the CPUs to call scx_task_slice_ended(rq, rq->donor).
Then scx_rehome_task() and scx_punt_task() run under scoped_guard(sched_change,
p, DEQUEUE_SAVE | DEQUEUE_MOVE | DEQUEUE_NOCLOCK) without DEQUEUE_CLASS, so
switched_from_scx() is not invoked. For a running donor with p->scx.slice > 0,
dequeue_task_scx() skips scx_task_slice_ended() due to DEQUEUE_SAVE:
kernel/sched/ext/ext.c:dequeue_task_scx() {
...
/* see scx_task_slice_ended() for the save/restore exception */
if (!((deq_flags & DEQUEUE_SAVE) && task_current_donor(rq, p)))
scx_task_slice_ended(rq, p);
}
put_prev_task_scx() also skips scx_task_slice_ended() because p->scx.slice is
non-zero, and __scx_disable_and_exit_task() returns early for SCX_TASK_NONE:
kernel/sched/ext/ext.c:__scx_disable_and_exit_task() {
...
switch (scx_get_task_state(p)) {
case SCX_TASK_NONE:
return;
case SCX_TASK_INIT:
args.cancelled = true;
break;
case SCX_TASK_READY:
break;
case SCX_TASK_ENABLED:
scx_disable_task(sch, p);
break;
}
When sched_change_end() restores p via enqueue_task_scx() with
ENQUEUE_RESTORE, it takes the ENQ_ACTION_LOCAL_NOREFILL path, leaving
rq->scx.rescue.curr == p and SCX_TASK_KERNEL_SLICE (and SCX_TASK_PROTECTED if
set) active under the new scheduler.
If p is re-homed to scx_root and all sub-schedulers finish disabling (turning
off __scx_has_subs via scx_dec_has_subs()), scx_rescuee(rq) begins returning
NULL:
kernel/sched/ext/sub.h:scx_rescuee() {
lockdep_assert_rq_held(rq);
if (!scx_has_subs())
return NULL;
return rq->scx.rescue.curr;
}
At that point scx_task_slice_ended() can no longer call scx_rescue_end(rq) to
clear rq->scx.rescue.curr, leaving a dangling task_struct pointer in
rq->scx.rescue.curr after p exits that scx_rescue_timerfn() continues to
dereference.
Should __scx_disable_and_exit_task() also call scx_task_slice_ended() and
scx_set_task_slice(p, 0) when p is in SCX_TASK_NONE?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261027001749.1821379-1-tj@kernel.org?part=13
next prev parent reply other threads:[~2026-10-27 0:44 UTC|newest]
Thread overview: 27+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-27 0:17 [PATCHSET v4 sched_ext/for-7.4-fixes] sched_ext: Fix bypassed sub-scheduler tasks stalling when the host lacks caps Tejun Heo
2026-10-27 0:17 ` [PATCH 01/18] sched_ext: Pack scx_ent_flags' multi-bit fields at the top Tejun Heo
2026-10-27 0:17 ` [PATCH 02/18] sched_ext: Clear holding_cpu where the claim is confirmed Tejun Heo
2026-10-27 0:17 ` [PATCH 03/18] sched_ext: Fix bypassed tasks stranded by the reenqueue limit Tejun Heo
2026-10-27 0:17 ` [PATCH 04/18] sched_ext: Fix bypassed tasks looping on a host without SCX_CAP_ENQ Tejun Heo
2026-10-27 0:17 ` [PATCH 05/18] sched_ext: Move refill_task_slice_dfl() below apply_slice_vtime() Tejun Heo
2026-10-27 0:17 ` [PATCH 06/18] sched_ext: Fix rescue served time disagreeing with the slice under proxy execution Tejun Heo
2026-10-27 0:17 ` [PATCH 07/18] sched_ext: Restore a rescuee's slice after ops.stopping() Tejun Heo
2026-10-27 0:17 ` [PATCH 08/18] sched_ext: Fix a cpu idling with a runnable rescuee Tejun Heo
2026-10-27 0:34 ` sashiko-bot
2026-10-27 14:08 ` Tejun Heo
2026-10-27 0:17 ` [PATCH 09/18] sched_ext: Mark the rescue grant as a kernel slice with SCX_TASK_KERNEL_SLICE Tejun Heo
2026-10-27 0:17 ` [PATCH 10/18] sched_ext: Refuse extending a kernel slice Tejun Heo
2026-10-27 0:17 ` [PATCH 11/18] sched_ext: Fix a cpu idling after put_prev_task_scx() queues its task on the local DSQ Tejun Heo
2026-10-27 0:17 ` [PATCH 12/18] sched_ext: Expire prev's slice in dispatch_one() under bypass Tejun Heo
2026-10-27 0:17 ` [PATCH 13/18] sched_ext: Fix bypassed tasks stalling when the host lacks SCX_CAP_BASE Tejun Heo
2026-10-27 0:44 ` sashiko-bot [this message]
2026-10-27 14:08 ` Tejun Heo
2026-10-27 0:17 ` [PATCH 14/18] sched_ext: Fix the bypass load balancer stranding tasks on cpus where " Tejun Heo
2026-10-27 0:17 ` [PATCH 15/18] sched_ext: Factor out bypass_lb_move() and insert with the donor DSQ unlocked Tejun Heo
2026-10-27 0:17 ` [PATCH 16/18] sched_ext: Let the bypass load balancer move tasks homed on other cpus Tejun Heo
2026-10-27 0:17 ` [PATCH 17/18] sched_ext: Fix queued bypassed tasks stalling when the host loses SCX_CAP_BASE Tejun Heo
2026-10-27 0:17 ` [PATCH 18/18] sched_ext: Fix a lone IMMED task waiting behind a kept prev Tejun Heo
2026-10-27 14:08 ` [PATCH 19/18] sched_ext: Fix use-after-free of a NONE rescuee leaving its sched while running Tejun Heo
2026-10-27 14:08 ` [PATCH 20/18] sched_ext: Fix the rescue overload pick ejecting the wrong sub under bypass Tejun Heo
2026-10-27 19:11 ` [PATCHSET v4 sched_ext/for-7.4-fixes] sched_ext: Fix bypassed sub-scheduler tasks stalling when the host lacks caps Tejun Heo
2026-10-27 19:28 ` Andrea Righi
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-185966@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=sashiko-reviews@lists.linux.dev \
--cc=sched-ext@lists.linux.dev \
--cc=tj@kernel.org \
/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.