summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2026-10-14 15:58:16 -0700
committerLinus Torvalds <torvalds@linux-foundation.org>2026-10-14 15:58:16 -0700
commitdb7afcbf9eef65aa8bfe6edb5ff4b4a92d1a4792 (patch)
treeafef6085d6fef58cdc2605a8e8d4d666c6f37718
parente177c722b3dd9ad432195b5c2cbbfe58c909a821 (diff)
parent2935062be77113c1c4565ca9d0c72180449aafee (diff)
downloadlinux-next-db7afcbf9eef65aa8bfe6edb5ff4b4a92d1a4792.tar.gz
linux-next-db7afcbf9eef65aa8bfe6edb5ff4b4a92d1a4792.zip
Merge tag 'sched_ext-for-7.3-rc7-fixes' of git://git.kernel.org/pub/scm/linux/kernel/git/tj/sched_ext
Pull sched_ext fixes from Tejun Heo: - A task that skipped the BPF scheduler on its way back to its CPU's local queue could stall there until the watchdog fired, as the CPU had already decided to go idle. - Use-after-free of a destroyed user dispatch queue by a deferred re-enqueue request. - A task could be moved off a dispatch queue while the dispatching CPU was still running ops.dequeue() on it. - Bypass mode mishandled a sub-scheduler bypass overlapping an ancestor's, as across a system suspend. Descendants could leave bypass mid-teardown and tasks could be stranded on bypass queues. - The idle CPU selection helper could return a CPU outside the allowed CPUs of a pinned or migration-disabled task. * tag 'sched_ext-for-7.3-rc7-fixes' of git://git.kernel.org/pub/scm/linux/kernel/git/tj/sched_ext: sched_ext: Wait for SCX_OPSS_DISPATCHING before moving tasks off DSQs sched_ext: Rename scx_reenq_wait_dispatching() to scx_wait_dispatching() sched_ext: idle: Respect task affinity in restricted CPU selection sched_ext: Fix the bypass dispatch claim for schedulers bypassed by an ancestor sched_ext: Propagate every bypass depth change to the descendants sched_ext: Fix use-after-free of a destroyed user DSQ by a deferred reenqueue sched_ext: Fix stall when a task enqueued with SCX_ENQ_LAST lands back on its CPU's local DSQ
-rw-r--r--kernel/sched/ext/ext.c123
-rw-r--r--kernel/sched/ext/idle.c50
-rw-r--r--kernel/sched/ext/internal.h7
-rw-r--r--kernel/sched/ext/sub.c2
4 files changed, 132 insertions, 50 deletions
diff --git a/kernel/sched/ext/ext.c b/kernel/sched/ext/ext.c
index 5fe980da545d..f1b9d52f624a 100644
--- a/kernel/sched/ext/ext.c
+++ b/kernel/sched/ext/ext.c
@@ -1149,6 +1149,10 @@ void schedule_dsq_reenq(struct scx_sched *sch, struct scx_dispatch_q *dsq,
guard(raw_spinlock_irqsave)(&rq->scx.deferred_reenq_lock);
+ /* @dsq is being destroyed, see free_dsq_irq_workfn() */
+ if (unlikely(READ_ONCE(dsq->id) == SCX_DSQ_INVALID))
+ return;
+
if (list_empty(&dru->node))
list_move_tail(&dru->node, &rq->scx.deferred_reenq_users);
WRITE_ONCE(dru->flags, dru->flags | reenq_flags);
@@ -2403,11 +2407,14 @@ static void wakeup_preempt_scx(struct rq *rq, struct task_struct *p, int wake_fl
void scx_move_local_task_to_local_dsq(struct scx_sched *sch, struct task_struct *p,
u64 enq_flags, struct rq *dst_rq)
{
- struct scx_dispatch_q *dst_dsq = scx_resolve_local_dsq(sch, dst_rq, p, &enq_flags);
+ struct scx_dispatch_q *dst_dsq;
lockdep_assert_rq_held(dst_rq);
WARN_ON_ONCE(p->scx.holding_cpu >= 0);
+ scx_wait_dispatching(p);
+
+ dst_dsq = scx_resolve_local_dsq(sch, dst_rq, p, &enq_flags);
if (enq_flags & (SCX_ENQ_HEAD | SCX_ENQ_PREEMPT))
dsq_insert_head(dst_dsq, p);
@@ -2587,8 +2594,12 @@ static bool unlink_dsq_and_switch_rq_lock(struct task_struct *p,
switch_rq_lock(locked_rq, src_rq);
/* task_rq couldn't have changed if we're still the holding cpu */
- return likely(p->scx.holding_cpu == cpu) &&
- !WARN_ON_ONCE(src_rq != task_rq(p));
+ if (likely(p->scx.holding_cpu == cpu) && !WARN_ON_ONCE(src_rq != task_rq(p))) {
+ scx_wait_dispatching(p);
+ return true;
+ }
+
+ return false;
}
static bool consume_remote_task(struct scx_sched *sch, struct rq *this_rq,
@@ -2631,6 +2642,8 @@ static struct rq *move_task_between_dsqs(struct scx_sched *sch,
lockdep_assert_held(&src_dsq->lock);
lockdep_assert_rq_held(src_rq);
+ scx_wait_dispatching(p);
+
if (dst_dsq->id == SCX_DSQ_LOCAL) {
dst_rq = container_of(dst_dsq, struct rq, scx.local_dsq);
if (src_rq != dst_rq &&
@@ -3213,8 +3226,7 @@ static void put_prev_task_scx(struct rq *rq, struct task_struct *p,
/*
* If @p is runnable but we're about to enter a lower
* sched_class, %SCX_OPS_ENQ_LAST must be set. Tell
- * ops.enqueue() that @p is the only one available for this cpu,
- * which should trigger an explicit follow-up scheduling event.
+ * ops.enqueue() that @p is the only one available for this cpu.
* This doesn't apply if the baseline access on the CPU is lost.
*
* Under core scheduling, a pick dispatches only when nothing is
@@ -3226,6 +3238,16 @@ static void put_prev_task_scx(struct rq *rq, struct task_struct *p,
WARN_ON_ONCE(!sched_core_enabled(rq) &&
!(sch->ops.flags & SCX_OPS_ENQ_LAST));
scx_do_enqueue_task(rq, p, SCX_ENQ_LAST, -1);
+
+ /*
+ * A task put on the local DSQ runs, as anywhere else.
+ * Here the insert can't reschedule on its own: the pick
+ * has settled on @next and @p is still curr, so
+ * resched_curr() would flag @p and __schedule() clears
+ * that right after the pick. Flag @next instead.
+ */
+ if (p->scx.dsq == &rq->scx.local_dsq)
+ set_tsk_need_resched(next);
} else {
scx_do_enqueue_task(rq, p, 0, -1);
}
@@ -4470,11 +4492,12 @@ static bool local_task_should_reenq(struct rq *rq, struct task_struct *p,
}
/*
- * The dispatcher stores the final ops_state after dropping the DSQ lock, so @p
- * can be found on a DSQ while still %SCX_OPSS_DISPATCHING. Reenqueueing @p
- * before that store lands would have it clobber the new %SCX_OPSS_QUEUED.
+ * The dispatcher runs ops.dequeue() and stores the final ops_state after
+ * dropping the DSQ lock, so @p can be found on a DSQ while still
+ * %SCX_OPSS_DISPATCHING. Wait for the dispatch to finish before operating on
+ * @p.
*/
-void scx_reenq_wait_dispatching(struct task_struct *p)
+void scx_wait_dispatching(struct task_struct *p)
{
if (unlikely(atomic_long_read_acquire(&p->scx.ops_state) == SCX_OPSS_DISPATCHING))
wait_ops_state(p, SCX_OPSS_DISPATCHING);
@@ -4523,7 +4546,7 @@ static u32 reenq_local(struct scx_sched *sch, struct rq *rq, u64 reenq_flags)
if (!local_task_should_reenq(rq, p, &reenq_flags, &reason))
continue;
- scx_reenq_wait_dispatching(p);
+ scx_wait_dispatching(p);
scx_dispatch_dequeue(rq, p);
if (WARN_ON_ONCE(p->scx.flags & SCX_TASK_REENQ_REASON_MASK))
@@ -4647,7 +4670,7 @@ static void reenq_user(struct rq *rq, struct scx_dispatch_q *dsq, u64 reenq_flag
}
/* @p is on @dsq, its rq and @dsq are locked */
- scx_reenq_wait_dispatching(p);
+ scx_wait_dispatching(p);
dispatch_dequeue_locked(p, dsq);
raw_spin_unlock(&dsq->lock);
@@ -5171,8 +5194,8 @@ static void exit_dsq(struct scx_dispatch_q *dsq)
struct rq *rq = cpu_rq(cpu);
/*
- * There must have been a RCU grace period since the last
- * insertion and @dsq should be off the deferred list by now.
+ * free_dsq_irq_workfn() cancelled the reenqs before the grace
+ * period and schedule_dsq_reenq() queues nothing after that.
*/
if (WARN_ON_ONCE(!list_empty(&dru->node))) {
guard(raw_spinlock_irqsave)(&rq->scx.deferred_reenq_lock);
@@ -5196,8 +5219,26 @@ static void free_dsq_irq_workfn(struct irq_work *irq_work)
struct llist_node *to_free = llist_del_all(&dsqs_to_free);
struct scx_dispatch_q *dsq, *tmp_dsq;
- llist_for_each_entry_safe(dsq, tmp_dsq, to_free, free_node)
+ llist_for_each_entry_safe(dsq, tmp_dsq, to_free, free_node) {
+ s32 cpu;
+
+ /*
+ * Cancel pending reenqs. Only a run_deferred() that started
+ * before the grace period can hold one, and it keeps IRQs off,
+ * so the grace period waits for it. After this sweep,
+ * schedule_dsq_reenq() sees the invalid id under the same lock
+ * and queues nothing.
+ */
+ for_each_possible_cpu(cpu) {
+ struct scx_dsq_pcpu *pcpu = per_cpu_ptr(dsq->pcpu, cpu);
+ struct rq *rq = cpu_rq(cpu);
+
+ guard(raw_spinlock_irqsave)(&rq->scx.deferred_reenq_lock);
+ list_del_init(&pcpu->deferred_reenq_user.node);
+ }
+
call_rcu(&dsq->rcu, free_dsq_rcufn);
+ }
}
static DEFINE_IRQ_WORK(free_dsq_irq_work, free_dsq_irq_workfn);
@@ -6068,15 +6109,12 @@ static void enable_bypass_dsp(struct scx_sched *sch)
/*
* When a sub-sched bypasses, its tasks are queued on the bypass DSQs of
- * the nearest non-bypassing ancestor or root. As enable_bypass_dsp() is
- * called iff @sch is not already bypassed due to an ancestor bypassing,
- * we can assume that the parent is not bypassing and thus will be the
- * host of the bypass DSQs.
- *
- * While the situation may change in the future, the following
- * guarantees that the nearest non-bypassing ancestor or root has bypass
- * dispatch enabled while a descendant is bypassing, which is all that's
- * required.
+ * the nearest non-bypassing ancestor or root. The parent hosts them
+ * unless it is bypassing too. Then they sit further up and the parent
+ * consumes only its own bypass DSQs, empty until it leaves bypass and
+ * becomes the host. Either way the nearest non-bypassing ancestor or
+ * root has bypass dispatch enabled while a descendant is bypassing,
+ * which is all that's required.
*
* scx_bypass_dsp_enabled() test is used to determine whether to enter
* the bypass dispatch handling path from both bypassing and hosting
@@ -6185,35 +6223,46 @@ void scx_bypass(struct scx_sched *sch, bool bypass)
struct scx_sched *pos;
unsigned long flags;
int cpu;
+ bool changed;
raw_spin_lock_irqsave(&scx_bypass_lock, flags);
if (bypass) {
- if (!inc_bypass_depth(sch))
- goto unlock;
-
- enable_bypass_dsp(sch);
+ changed = inc_bypass_depth(sch);
+ if (changed)
+ enable_bypass_dsp(sch);
} else {
- if (!dec_bypass_depth(sch))
- goto unlock;
+ changed = dec_bypass_depth(sch);
}
/*
* Bypass state is propagated to all descendants - an scx_sched bypasses
- * if itself or any of its ancestors are in bypass mode.
+ * if itself or any of its ancestors are in bypass mode. A descendant's
+ * depth is the sum of its own bypasses and its ancestors', so every
+ * call propagates whether or not @sch changed state. The descendants'
+ * claims are released below with @sch's, after their tasks have moved.
*/
raw_spin_lock(&scx_sched_lock);
scx_for_each_descendant_pre(pos, sch) {
if (pos == sch)
continue;
- if (bypass)
- inc_bypass_depth(pos);
- else
+ if (bypass) {
+ if (inc_bypass_depth(pos))
+ enable_bypass_dsp(pos);
+ } else {
dec_bypass_depth(pos);
+ }
}
raw_spin_unlock(&scx_sched_lock);
/*
+ * A descendant's depth is at least @sch's, so no bypass state changed
+ * unless @sch's did.
+ */
+ if (!changed)
+ goto unlock;
+
+ /*
* No task property is changing. We just need to make sure all currently
* queued tasks are re-queued according to the new scx_bypassing()
* state. As an optimization, walk each rq's runnable_list instead of
@@ -6291,8 +6340,12 @@ void scx_bypass(struct scx_sched *sch, bool bypass)
}
/* disarming must come after moving all tasks out of the bypass DSQs */
- if (!bypass)
- scx_disable_bypass_dsp(sch);
+ if (!bypass) {
+ guard(raw_spinlock)(&scx_sched_lock);
+ scx_for_each_descendant_pre(pos, sch)
+ if (!pos->bypass_depth)
+ scx_disable_bypass_dsp(pos);
+ }
unlock:
raw_spin_unlock_irqrestore(&scx_bypass_lock, flags);
}
diff --git a/kernel/sched/ext/idle.c b/kernel/sched/ext/idle.c
index aa9fb6de0ad6..28ed9e327085 100644
--- a/kernel/sched/ext/idle.c
+++ b/kernel/sched/ext/idle.c
@@ -952,12 +952,43 @@ static bool is_bpf_migration_disabled(const struct task_struct *p)
return p->migration_disabled;
}
+static s32 select_cpu_restricted(struct task_struct *p, s32 prev_cpu,
+ const struct cpumask *allowed, u64 flags,
+ bool migration_disabled)
+{
+ s32 cpu;
+
+ if (migration_disabled)
+ cpu = task_cpu(p);
+ else
+ cpu = cpumask_first(p->cpus_ptr);
+
+ if (!cpumask_test_cpu(cpu, p->cpus_ptr))
+ return -EBUSY;
+
+ if (allowed && !cpumask_test_cpu(cpu, allowed))
+ return -EBUSY;
+
+ if (flags & SCX_PICK_IDLE_IN_NODE) {
+ int node = scx_cpu_node_if_enabled(prev_cpu);
+
+ if (scx_cpu_node_if_enabled(cpu) != node)
+ return -EBUSY;
+ }
+
+ if (!scx_idle_test_and_clear_cpu(cpu))
+ return -EBUSY;
+
+ return cpu;
+}
+
static s32 select_cpu_from_kfunc(struct scx_sched *sch, struct task_struct *p,
s32 prev_cpu, u64 wake_flags,
const struct cpumask *allowed, u64 flags)
{
unsigned long irq_flags;
bool we_locked = false;
+ bool migration_disabled;
s32 cpu;
if (!scx_cpu_valid(sch, prev_cpu, NULL))
@@ -994,20 +1025,17 @@ static s32 select_cpu_from_kfunc(struct scx_sched *sch, struct task_struct *p,
/*
* This may also be called from ops.enqueue(), so we need to handle
- * per-CPU tasks as well. For these tasks, we can skip all idle CPU
- * selection optimizations and simply check whether the previously
- * used CPU is idle and within the allowed cpumask.
+ * per-CPU tasks as well. For these tasks, skip general idle CPU
+ * selection and validate the task's only usable CPU directly.
*/
- if (p->nr_cpus_allowed == 1 || is_bpf_migration_disabled(p)) {
- if (cpumask_test_cpu(prev_cpu, allowed ?: p->cpus_ptr) &&
- scx_idle_test_and_clear_cpu(prev_cpu))
- cpu = prev_cpu;
- else
- cpu = -EBUSY;
- } else {
+ migration_disabled = is_bpf_migration_disabled(p);
+
+ if (p->nr_cpus_allowed == 1 || migration_disabled)
+ cpu = select_cpu_restricted(p, prev_cpu, allowed, flags,
+ migration_disabled);
+ else
cpu = scx_select_cpu_dfl(p, prev_cpu, wake_flags,
allowed ?: p->cpus_ptr, flags);
- }
if (we_locked)
raw_spin_unlock_irqrestore(&p->pi_lock, irq_flags);
diff --git a/kernel/sched/ext/internal.h b/kernel/sched/ext/internal.h
index 5b37faa532d6..352547aedbb7 100644
--- a/kernel/sched/ext/internal.h
+++ b/kernel/sched/ext/internal.h
@@ -1774,8 +1774,9 @@ enum scx_enq_flags {
* %SCX_OPS_ENQ_LAST is specified, they're ops.enqueue()'d with the
* %SCX_ENQ_LAST flag set.
*
- * The BPF scheduler is responsible for triggering a follow-up
- * scheduling event. Otherwise, Execution may stall.
+ * If the task is queued on the local DSQ of the CPU it was running on,
+ * it continues to run. Otherwise, the CPU goes idle. A scheduler that
+ * wants a full dispatch cycle on the CPU should kick it.
*/
SCX_ENQ_LAST = 1LLU << 41,
@@ -2122,7 +2123,7 @@ void scx_kick_cpu(struct scx_sched *sch, s32 cpu, u64 flags);
u64 __scx_bpf_now(struct rq *rq);
void schedule_dsq_reenq(struct scx_sched *sch, struct scx_dispatch_q *dsq,
u64 reenq_flags, struct rq *locked_rq);
-void scx_reenq_wait_dispatching(struct task_struct *p);
+void scx_wait_dispatching(struct task_struct *p);
int __scx_init_task(struct scx_sched *sch, struct task_struct *p,
struct cgroup *cgrp, bool fork);
void scx_enable_task(struct scx_sched *sch, struct task_struct *p);
diff --git a/kernel/sched/ext/sub.c b/kernel/sched/ext/sub.c
index 90441206d3b9..212de1cb8502 100644
--- a/kernel/sched/ext/sub.c
+++ b/kernel/sched/ext/sub.c
@@ -797,7 +797,7 @@ void scx_reenq_reject(struct rq *rq)
if (WARN_ON_ONCE(p->migration_pending))
continue;
- scx_reenq_wait_dispatching(p);
+ scx_wait_dispatching(p);
scx_dispatch_dequeue(rq, p);
if (WARN_ON_ONCE(p->scx.flags & SCX_TASK_REENQ_REASON_MASK))