Sched_ext development
 help / color / mirror / Atom feed
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

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