From: Tejun Heo <tj@kernel.org>
To: David Vernet <void@manifault.com>,
Andrea Righi <arighi@nvidia.com>,
Changwoo Min <changwoo@igalia.com>
Cc: sched-ext@lists.linux.dev, Emil Tsalapatis <emil@etsalapatis.com>,
Johannes Weiner <hannes@cmpxchg.org>,
Michal Koutny <mkoutny@suse.com>,
cgroups@vger.kernel.org, linux-kernel@vger.kernel.org,
Tejun Heo <tj@kernel.org>
Subject: [PATCH 4/8] sched_ext: Re-home tasks on cgroup migration
Date: Fri, 17 Jul 2026 22:17:23 -1000 [thread overview]
Message-ID: <20260718081727.582037-5-tj@kernel.org> (raw)
In-Reply-To: <20260718081727.582037-1-tj@kernel.org>
A task's sched (p->scx.sched) must match its cgroup's owner
(cgrp->scx_sched). cgroup migration breaks the invariant:
scx_cgroup_move_task() only fires root's ops.cgroup_move() and never
re-homes the task, leading to wrong-sched scheduling and, once the stale
sched is freed, a use-after-free.
Hook into the new cgroup task migration events and re-home each task whose
destination cgroup is owned by a different sched. The events map naturally
to the transfer: MIGRATING runs the fallible init for the destination sched,
letting it reject the migration the same way ops.cgroup_prep_move() can,
MIGRATED does the re-home, which can't fail, and CANCELED undoes the init
when the migration falls through.
Pre-commit, the task's task_group still reflects the source, so
__scx_init_task() grows an explicit cgroup argument for the migration path
to hand ops.init_task() the destination cgroup.
Signed-off-by: Tejun Heo <tj@kernel.org>
---
kernel/sched/ext/ext.c | 29 +++++---
kernel/sched/ext/internal.h | 3 +-
kernel/sched/ext/sub.c | 135 ++++++++++++++++++++++++++++++++++--
3 files changed, 152 insertions(+), 15 deletions(-)
diff --git a/kernel/sched/ext/ext.c b/kernel/sched/ext/ext.c
index cd99947b4229..6bac68758704 100644
--- a/kernel/sched/ext/ext.c
+++ b/kernel/sched/ext/ext.c
@@ -3485,15 +3485,28 @@ static struct cgroup *tg_cgrp(struct task_group *tg)
return &cgrp_dfl_root.cgrp;
}
-#define SCX_INIT_TASK_ARGS_CGROUP(tg) .cgroup = tg_cgrp(tg),
+#define SCX_INIT_TASK_ARGS_CGROUP(cgrp) .cgroup = (cgrp),
#else /* CONFIG_EXT_GROUP_SCHED */
-#define SCX_INIT_TASK_ARGS_CGROUP(tg)
+#define SCX_INIT_TASK_ARGS_CGROUP(cgrp)
#endif /* CONFIG_EXT_GROUP_SCHED */
-int __scx_init_task(struct scx_sched *sch, struct task_struct *p, bool fork)
+/**
+ * __scx_init_task - Initialize a task for a sched
+ * @sch: sched to initialize @p for
+ * @p: task of interest
+ * @cgrp: cgroup @p is joining, %NULL for @p's current task_group's cgroup
+ * @fork: %true if @p is being forked
+ *
+ * Pre-commit cgroup migration passes @cgrp explicitly as @p's task_group
+ * still reflects the source.
+ *
+ * Return 0 on success, -errno on failure.
+ */
+int __scx_init_task(struct scx_sched *sch, struct task_struct *p,
+ struct cgroup *cgrp, bool fork)
{
int ret;
@@ -3501,7 +3514,7 @@ int __scx_init_task(struct scx_sched *sch, struct task_struct *p, bool fork)
if (SCX_HAS_OP(sch, init_task)) {
struct scx_init_task_args args = {
- SCX_INIT_TASK_ARGS_CGROUP(task_group(p))
+ SCX_INIT_TASK_ARGS_CGROUP(cgrp ?: tg_cgrp(task_group(p)))
.fork = fork,
};
@@ -3747,7 +3760,7 @@ int scx_fork(struct task_struct *p, struct kernel_clone_args *kargs)
struct scx_sched *sch = scx_root;
#endif
scx_set_task_state(p, SCX_TASK_INIT_BEGIN);
- ret = __scx_init_task(sch, p, true);
+ ret = __scx_init_task(sch, p, NULL, true);
if (unlikely(ret)) {
scx_set_task_state(p, SCX_TASK_NONE);
return ret;
@@ -5969,8 +5982,8 @@ static void scx_root_disable(struct scx_sched *sch)
WRITE_ONCE(scx_switching_all, false);
/*
- * Shut down cgroup support before tasks so that the cgroup attach path
- * doesn't race against scx_disable_and_exit_task().
+ * Shut down cgroup support before tasks so that the cgroup attach and
+ * migration paths don't race against scx_disable_and_exit_task().
*/
scx_cgroup_lock();
scx_cgroup_enabled = false;
@@ -7263,7 +7276,7 @@ static void scx_root_enable_workfn(struct kthread_work *work)
scx_set_task_state(p, SCX_TASK_INIT_BEGIN);
scx_task_iter_unlock(&sti);
- ret = __scx_init_task(sch, p, false);
+ ret = __scx_init_task(sch, p, NULL, false);
scx_task_iter_relock(&sti, p);
diff --git a/kernel/sched/ext/internal.h b/kernel/sched/ext/internal.h
index d76ac22019af..23fc95502ea6 100644
--- a/kernel/sched/ext/internal.h
+++ b/kernel/sched/ext/internal.h
@@ -1920,7 +1920,8 @@ void scx_flush_dispatch_buf(struct scx_sched *sch, struct rq *rq);
void scx_kick_cpu(struct scx_sched *sch, s32 cpu, u64 flags);
void schedule_dsq_reenq(struct scx_sched *sch, struct scx_dispatch_q *dsq,
u64 reenq_flags, struct rq *locked_rq);
-int __scx_init_task(struct scx_sched *sch, struct task_struct *p, bool fork);
+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);
void __scx_disable_and_exit_task(struct scx_sched *sch, struct task_struct *p);
void scx_sub_init_cancel_task(struct scx_sched *sch, struct task_struct *p);
diff --git a/kernel/sched/ext/sub.c b/kernel/sched/ext/sub.c
index 5f7ac6696d17..393dbd00d2f7 100644
--- a/kernel/sched/ext/sub.c
+++ b/kernel/sched/ext/sub.c
@@ -906,7 +906,7 @@ void scx_sub_disable(struct scx_sched *sch)
* parent. A child can't directly affect the parent through its
* own failures.
*/
- ret = __scx_init_task(parent, p, false);
+ ret = __scx_init_task(parent, p, NULL, false);
if (ret) {
scx_fail_parent(sch, p, ret);
put_task_struct(p);
@@ -1212,7 +1212,7 @@ void scx_sub_enable_workfn(struct kthread_work *work)
* As $p is still on $parent, it can't be transitioned to INIT.
* Let's worry about task state later. Use __scx_init_task().
*/
- ret = __scx_init_task(sch, p, false);
+ ret = __scx_init_task(sch, p, NULL, false);
if (ret)
goto abort;
@@ -1336,6 +1336,99 @@ void scx_sub_enable_workfn(struct kthread_work *work)
cmd->ret = 0;
}
+/**
+ * scx_cgroup_task_migrating - Prepare a task for a cgroup migration
+ * @ctx: migration being prepared
+ *
+ * A task's sched must match its cgroup's owner, so a migration that crosses a
+ * sched boundary re-homes the task once committed. Run the fallible part here,
+ * before the migration commits: initialize the task for the destination sched.
+ * A rejection fails the cgroup.procs write.
+ */
+static s32 scx_cgroup_task_migrating(struct cgroup_task_migrate_ctx *ctx)
+{
+ struct task_struct *p = ctx->task;
+ struct scx_sched *to;
+ int ret;
+
+ /*
+ * Cleared under scx_cgroup_lock() before root disable starts tearing
+ * down tasks. As cgroup_mutex is held, a set flag guarantees that the
+ * teardown loop is not running concurrently.
+ */
+ if (!scx_cgroup_enabled)
+ return NOTIFY_OK;
+
+ to = ctx->dst_dcgrp->scx_sched;
+ if (scx_task_on_sched(to, p))
+ return NOTIFY_OK;
+
+ ret = __scx_init_task(to, p, ctx->dst_dcgrp, false);
+ if (ret)
+ return notifier_from_errno(ret);
+
+ return NOTIFY_OK;
+}
+
+/**
+ * scx_cgroup_task_migrated - Re-home a task that changed cgroups
+ * @ctx: committed migration
+ *
+ * Move the task to its new cgroup's sched, which scx_cgroup_task_migrating()
+ * already initialized it for. Can't fail.
+ *
+ * This is safe against all phases of the destination sched's destruction. A
+ * disable resets cgroup ownership to the parent and re-homes tasks in one
+ * scx_cgroup_lock() section. If that section already ran, the destination would
+ * be the parent. Otherwise, the re-home loop is still ahead and guaranteed to
+ * visit the task, now in the destination cgroup.
+ */
+static void scx_cgroup_task_migrated(struct cgroup_task_migrate_ctx *ctx)
+{
+ struct task_struct *p = ctx->task;
+ struct scx_sched *to;
+ struct rq *rq;
+ struct rq_flags rf;
+
+ if (!scx_cgroup_enabled)
+ return;
+
+ to = ctx->dst_dcgrp->scx_sched;
+ if (scx_task_on_sched(to, p))
+ return;
+
+ rq = task_rq_lock(p, &rf);
+ scx_rehome_task(to, p);
+ task_rq_unlock(rq, p, &rf);
+}
+
+/**
+ * scx_cgroup_task_migrate_canceled - Undo migration preparation
+ * @ctx: canceled migration
+ *
+ * The migration failed after scx_cgroup_task_migrating() initialized the task
+ * for the destination sched. The task stays on its current sched in the source
+ * cgroup. Undo the destination's init.
+ */
+static void scx_cgroup_task_migrate_canceled(struct cgroup_task_migrate_ctx *ctx)
+{
+ struct task_struct *p = ctx->task;
+ struct scx_sched *to;
+ struct rq *rq;
+ struct rq_flags rf;
+
+ if (!scx_cgroup_enabled)
+ return;
+
+ to = ctx->dst_dcgrp->scx_sched;
+ if (scx_task_on_sched(to, p))
+ return;
+
+ rq = task_rq_lock(p, &rf);
+ scx_sub_init_cancel_task(to, p);
+ task_rq_unlock(rq, p, &rf);
+}
+
static s32 scx_cgroup_lifetime_notify(struct notifier_block *nb,
unsigned long action, void *data)
{
@@ -1367,12 +1460,42 @@ static struct notifier_block scx_cgroup_lifetime_nb = {
.notifier_call = scx_cgroup_lifetime_notify,
};
-static s32 __init scx_cgroup_lifetime_notifier_init(void)
+static s32 scx_cgroup_task_notify(struct notifier_block *nb,
+ unsigned long action, void *data)
+{
+ struct cgroup_task_migrate_ctx *ctx = data;
+
+ switch (action) {
+ case CGROUP_TASK_MIGRATING:
+ return scx_cgroup_task_migrating(ctx);
+ case CGROUP_TASK_MIGRATED:
+ scx_cgroup_task_migrated(ctx);
+ break;
+ case CGROUP_TASK_MIGRATE_CANCELED:
+ scx_cgroup_task_migrate_canceled(ctx);
+ break;
+ }
+
+ return NOTIFY_OK;
+}
+
+static struct notifier_block scx_cgroup_task_nb = {
+ .notifier_call = scx_cgroup_task_notify,
+};
+
+static s32 __init scx_cgroup_notifier_init(void)
{
- return blocking_notifier_chain_register(&cgroup_lifetime_notifier,
- &scx_cgroup_lifetime_nb);
+ s32 ret;
+
+ ret = blocking_notifier_chain_register(&cgroup_lifetime_notifier,
+ &scx_cgroup_lifetime_nb);
+ if (ret)
+ return ret;
+
+ return blocking_notifier_chain_register(&cgroup_task_notifier,
+ &scx_cgroup_task_nb);
}
-core_initcall(scx_cgroup_lifetime_notifier_init);
+core_initcall(scx_cgroup_notifier_init);
static void scx_pstack_recursion(struct bpf_prog *prog, const char *op)
{
--
2.55.0
next prev parent reply other threads:[~2026-07-18 8:17 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-18 8:17 [PATCHSET sched_ext/for-7.3] sched_ext: Cgroup migration and op delivery for sub-schedulers Tejun Heo
2026-07-18 8:17 ` [PATCH 1/8] cgroup: Add cgroup_task_notifier and task migration events Tejun Heo
2026-07-18 8:17 ` [PATCH 2/8] sched_ext: Factor out scx_rehome_task() and scx_punt_task() Tejun Heo
2026-07-18 8:17 ` [PATCH 3/8] sched_ext: Relocate scx_cgroup_enabled Tejun Heo
2026-07-18 8:17 ` Tejun Heo [this message]
2026-07-18 8:17 ` [PATCH 5/8] sched_ext: Deliver cgroup ops to each task_group's sched Tejun Heo
2026-07-18 8:17 ` [PATCH 6/8] sched_ext: Hand over cgroups at sub-scheduler enable/disable Tejun Heo
2026-07-18 8:17 ` [PATCH 7/8] tools/sched_ext: scx_qmap - Consume cgroup weights through set_weight Tejun Heo
2026-07-18 8:17 ` [PATCH 8/8] tools/sched_ext: scx_qmap - Add init fault injection modes Tejun Heo
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=20260718081727.582037-5-tj@kernel.org \
--to=tj@kernel.org \
--cc=arighi@nvidia.com \
--cc=cgroups@vger.kernel.org \
--cc=changwoo@igalia.com \
--cc=emil@etsalapatis.com \
--cc=hannes@cmpxchg.org \
--cc=linux-kernel@vger.kernel.org \
--cc=mkoutny@suse.com \
--cc=sched-ext@lists.linux.dev \
--cc=void@manifault.com \
/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.