* [PATCH] sched/autogroup: Fix task_group UAF during thread creation
@ 2026-10-23 1:36 Daehyeon Ko
2026-10-26 11:57 ` Peter Zijlstra
0 siblings, 1 reply; 8+ messages in thread
From: Daehyeon Ko @ 2026-10-23 1:36 UTC (permalink / raw)
To: Ingo Molnar, Peter Zijlstra
Cc: Juri Lelli, Vincent Guittot, Dietmar Eggemann, linux-kernel
sched_cgroup_fork() assigns a CLONE_THREAD child in the root CPU cgroup to
p->signal->autogroup->tg before the child is visible on the thread list.
Autogroup ownership is per signal_struct: CLONE_THREAD reuses the existing
signal_struct and does not acquire an autogroup reference for the child.
ksys_setsid() releases tasklist_lock before calling
sched_autogroup_create_attach(), so tasklist_lock does not serialize the
autogroup move against this unpublished part of copy_process():
clone(CLONE_THREAD) caller sibling setsid() caller
------------------------- -----------------------
sched_cgroup_fork()
sched_autogroup_create_attach()
autogroup_move_group()
lock(siglock)
signal->autogroup = new
for_each_thread()
unlock(siglock)
autogroup_kref_put(old)
lock(siglock)
list_add_tail_rcu(thread_node)
unlock(siglock)
wake_up_new_task()
The iteration cannot see the unpublished child. The final put can therefore
drop the signal's last reference to the old autogroup. Two RCU callbacks
then free its per-CPU cfs_rq and task_group while p->sched_task_group,
p->se.cfs_rq, and p->se.parent still point into them.
At the last failure-free point in copy_process(), revalidate the child's
scheduler group while holding the shared siglock and keep the lock held
through thread-list publication. An autogroup move then either completes
before revalidation, or blocks until publication and includes the child.
If the group changed, update p->sched_task_group under p->pi_lock, clear
p->se.cfs_rq so set_task_rq_fair() cannot inspect the freed old runqueue,
and call set_task_rq() to rebuild the fair and RT group links. The TASK_NEW
child has no attached load to migrate.
An unprivileged clone()/setsid() reproducer running as uid/gid 65534 with
all capability sets empty triggered a KASAN task_group slab use-after-free
in wake_up_new_task() on an unmodified Linux v7.3 kernel. The allocation
came from sched_create_group(), and the free ran from
sched_free_group_rcu().
The fix completed 1,024 iterations of both the original race sweep and a
delayed-setsid stress schedule without a KASAN, lockdep, oops, or panic
report.
An AI coding assistant helped identify and validate the race, construct
the reproducer, and draft and test this fix.
Fixes: b1e8206582f9 ("sched: Fix yet more sched_fork() races")
Cc: stable@vger.kernel.org
Signed-off-by: Daehyeon Ko <4ncienth@gmail.com>
---
include/linux/sched/autogroup.h | 5 +++++
kernel/fork.c | 9 +++++++++
kernel/sched/autogroup.c | 30 ++++++++++++++++++++++++++++++
3 files changed, 44 insertions(+)
diff --git a/include/linux/sched/autogroup.h b/include/linux/sched/autogroup.h
index 704391cc1d206e..d61e4a83b12bac 100644
--- a/include/linux/sched/autogroup.h
+++ b/include/linux/sched/autogroup.h
@@ -6,11 +6,14 @@ struct signal_struct;
struct task_struct;
struct task_group;
struct seq_file;
+struct kernel_clone_args;
#ifdef CONFIG_SCHED_AUTOGROUP
extern void sched_autogroup_create_attach(struct task_struct *p);
extern void sched_autogroup_detach(struct task_struct *p);
extern void sched_autogroup_fork(struct signal_struct *sig);
+void sched_autogroup_fork_finalize(struct task_struct *p,
+ struct kernel_clone_args *kargs);
extern void sched_autogroup_exit(struct signal_struct *sig);
extern void sched_autogroup_exit_task(struct task_struct *p);
#ifdef CONFIG_PROC_FS
@@ -21,6 +24,8 @@ extern int proc_sched_autogroup_set_nice(struct task_struct *p, int nice);
static inline void sched_autogroup_create_attach(struct task_struct *p) { }
static inline void sched_autogroup_detach(struct task_struct *p) { }
static inline void sched_autogroup_fork(struct signal_struct *sig) { }
+static inline void sched_autogroup_fork_finalize(struct task_struct *p,
+ struct kernel_clone_args *kargs) { }
static inline void sched_autogroup_exit(struct signal_struct *sig) { }
static inline void sched_autogroup_exit_task(struct task_struct *p) { }
#endif
diff --git a/kernel/fork.c b/kernel/fork.c
index 10f2d05d816a5f..6df4ff21d1b780 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -2500,6 +2500,15 @@ __latent_entropy struct task_struct *copy_process(
/* No more failure paths after this point. */
+ /*
+ * A concurrent autogroup move can replace signal->autogroup after
+ * sched_cgroup_fork() but before this task is on the thread list.
+ * Recheck while holding siglock so the move either precedes this check or
+ * observes the new thread after publication below.
+ */
+ if (clone_flags & CLONE_THREAD)
+ sched_autogroup_fork_finalize(p, args);
+
/*
* Copy seccomp details explicitly here, in case they were changed
* before holding sighand lock.
diff --git a/kernel/sched/autogroup.c b/kernel/sched/autogroup.c
index e380cf9372bbd2..50ad43e7648fc5 100644
--- a/kernel/sched/autogroup.c
+++ b/kernel/sched/autogroup.c
@@ -215,6 +215,36 @@ void sched_autogroup_fork(struct signal_struct *sig)
sig->autogroup = autogroup_task_get(current);
}
+/**
+ * sched_autogroup_fork_finalize - recheck a new thread's scheduler group
+ * @p: new task
+ * @kargs: clone arguments holding the pinned cgroup set
+ *
+ * The caller must hold the shared sighand lock and keep it held through
+ * thread-list publication. The task is still TASK_NEW and has no attached
+ * scheduler load.
+ */
+void sched_autogroup_fork_finalize(struct task_struct *p,
+ struct kernel_clone_args *kargs)
+{
+ struct task_group *tg;
+ unsigned long flags;
+
+ lockdep_assert_held(&p->sighand->siglock);
+
+ raw_spin_lock_irqsave(&p->pi_lock, flags);
+ tg = container_of(kargs->cset->subsys[cpu_cgrp_id],
+ struct task_group, css);
+ tg = autogroup_task_group(p, tg);
+ if (tg != p->sched_task_group) {
+ p->sched_task_group = tg;
+ /* Do not let set_task_rq_fair() inspect the possibly freed cfs_rq. */
+ p->se.cfs_rq = NULL;
+ set_task_rq(p, task_cpu(p));
+ }
+ raw_spin_unlock_irqrestore(&p->pi_lock, flags);
+}
+
void sched_autogroup_exit(struct signal_struct *sig)
{
autogroup_kref_put(sig->autogroup);
--
2.55.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH] sched/autogroup: Fix task_group UAF during thread creation
2026-10-23 1:36 [PATCH] sched/autogroup: Fix task_group UAF during thread creation Daehyeon Ko
@ 2026-10-26 11:57 ` Peter Zijlstra
2026-10-30 3:45 ` Daehyeon Ko
0 siblings, 1 reply; 8+ messages in thread
From: Peter Zijlstra @ 2026-10-26 11:57 UTC (permalink / raw)
To: Daehyeon Ko
Cc: Ingo Molnar, Juri Lelli, Vincent Guittot, Dietmar Eggemann, linux-kernel
On Fri, Oct 23, 2026 at 10:36:31AM +0900, Daehyeon Ko wrote:
> sched_cgroup_fork() assigns a CLONE_THREAD child in the root CPU cgroup to
> p->signal->autogroup->tg before the child is visible on the thread list.
> Autogroup ownership is per signal_struct: CLONE_THREAD reuses the existing
> signal_struct and does not acquire an autogroup reference for the child.
>
> ksys_setsid() releases tasklist_lock before calling
> sched_autogroup_create_attach(), so tasklist_lock does not serialize the
> autogroup move against this unpublished part of copy_process():
>
> clone(CLONE_THREAD) caller sibling setsid() caller
> ------------------------- -----------------------
> sched_cgroup_fork()
> sched_autogroup_create_attach()
> autogroup_move_group()
> lock(siglock)
> signal->autogroup = new
> for_each_thread()
> unlock(siglock)
> autogroup_kref_put(old)
> lock(siglock)
> list_add_tail_rcu(thread_node)
> unlock(siglock)
> wake_up_new_task()
>
> The iteration cannot see the unpublished child. The final put can therefore
> drop the signal's last reference to the old autogroup. Two RCU callbacks
> then free its per-CPU cfs_rq and task_group while p->sched_task_group,
> p->se.cfs_rq, and p->se.parent still point into them.
>
> At the last failure-free point in copy_process(), revalidate the child's
> scheduler group while holding the shared siglock and keep the lock held
> through thread-list publication. An autogroup move then either completes
> before revalidation, or blocks until publication and includes the child.
>
> If the group changed, update p->sched_task_group under p->pi_lock, clear
> p->se.cfs_rq so set_task_rq_fair() cannot inspect the freed old runqueue,
> and call set_task_rq() to rebuild the fair and RT group links. The TASK_NEW
> child has no attached load to migrate.
>
> An unprivileged clone()/setsid() reproducer running as uid/gid 65534 with
> all capability sets empty triggered a KASAN task_group slab use-after-free
> in wake_up_new_task() on an unmodified Linux v7.3 kernel. The allocation
> came from sched_create_group(), and the free ran from
> sched_free_group_rcu().
> The fix completed 1,024 iterations of both the original race sweep and a
> delayed-setsid stress schedule without a KASAN, lockdep, oops, or panic
> report.
>
> An AI coding assistant helped identify and validate the race, construct
> the reproducer, and draft and test this fix.
>
> Fixes: b1e8206582f9 ("sched: Fix yet more sched_fork() races")
> Cc: stable@vger.kernel.org
> Signed-off-by: Daehyeon Ko <4ncienth@gmail.com>
> diff --git a/kernel/fork.c b/kernel/fork.c
> index 10f2d05d816a5f..6df4ff21d1b780 100644
> --- a/kernel/fork.c
> +++ b/kernel/fork.c
> @@ -2500,6 +2500,15 @@ __latent_entropy struct task_struct *copy_process(
>
> /* No more failure paths after this point. */
>
> + /*
> + * A concurrent autogroup move can replace signal->autogroup after
> + * sched_cgroup_fork() but before this task is on the thread list.
> + * Recheck while holding siglock so the move either precedes this check or
> + * observes the new thread after publication below.
> + */
> + if (clone_flags & CLONE_THREAD)
> + sched_autogroup_fork_finalize(p, args);
> +
> /*
> * Copy seccomp details explicitly here, in case they were changed
> * before holding sighand lock.
> diff --git a/kernel/sched/autogroup.c b/kernel/sched/autogroup.c
> index e380cf9372bbd2..50ad43e7648fc5 100644
> --- a/kernel/sched/autogroup.c
> +++ b/kernel/sched/autogroup.c
> @@ -215,6 +215,36 @@ void sched_autogroup_fork(struct signal_struct *sig)
> sig->autogroup = autogroup_task_get(current);
> }
>
> +/**
> + * sched_autogroup_fork_finalize - recheck a new thread's scheduler group
> + * @p: new task
> + * @kargs: clone arguments holding the pinned cgroup set
> + *
> + * The caller must hold the shared sighand lock and keep it held through
> + * thread-list publication. The task is still TASK_NEW and has no attached
> + * scheduler load.
> + */
> +void sched_autogroup_fork_finalize(struct task_struct *p,
> + struct kernel_clone_args *kargs)
> +{
> + struct task_group *tg;
> + unsigned long flags;
> +
> + lockdep_assert_held(&p->sighand->siglock);
> +
> + raw_spin_lock_irqsave(&p->pi_lock, flags);
> + tg = container_of(kargs->cset->subsys[cpu_cgrp_id],
> + struct task_group, css);
> + tg = autogroup_task_group(p, tg);
> + if (tg != p->sched_task_group) {
> + p->sched_task_group = tg;
> + /* Do not let set_task_rq_fair() inspect the possibly freed cfs_rq. */
> + p->se.cfs_rq = NULL;
I'm not sure this works; afaict this will result in a NULL deref
instead.
> + set_task_rq(p, task_cpu(p));
> + }
> + raw_spin_unlock_irqrestore(&p->pi_lock, flags);
> +}
> +
> void sched_autogroup_exit(struct signal_struct *sig)
> {
> autogroup_kref_put(sig->autogroup);
Can't we make ksys_setsid() more complicated instead? Have it create the
autogroup in advance, move under the tasklist_lock and destroy on
failure or something?
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH] sched/autogroup: Fix task_group UAF during thread creation
2026-10-26 11:57 ` Peter Zijlstra
@ 2026-10-30 3:45 ` Daehyeon Ko
2026-10-30 15:49 ` Peter Zijlstra
0 siblings, 1 reply; 8+ messages in thread
From: Daehyeon Ko @ 2026-10-30 3:45 UTC (permalink / raw)
To: Peter Zijlstra
Cc: Ingo Molnar, Juri Lelli, Vincent Guittot, Dietmar Eggemann, linux-kernel
On Mon, Oct 26, 2026 at 12:57:07PM +0100, Peter Zijlstra wrote:
> I'm not sure this works; afaict this will result in a NULL deref
> instead.
Thanks for taking a look.
The NULL previous cfs_rq should be handled by the existing set_task_rq()
contract. __sched_fork() initializes p->se.cfs_rq to NULL, and the normal
sched_cgroup_fork() path reaches set_task_rq() through __set_task_cpu().
set_task_rq_fair() returns when prev is NULL, before
cfs_rq_last_update_time(prev), and set_task_rq() then installs the new
cfs_rq, parent, and depth.
> Can't we make ksys_setsid() more complicated instead? Have it create the
> autogroup in advance, move under the tasklist_lock and destroy on
> failure or something?
I don't think moving the autogroup move under tasklist_lock alone closes
the window, because sched_cgroup_fork() runs before copy_process() takes
tasklist_lock:
clone(CLONE_THREAD) setsid()
------------------- --------
sched_cgroup_fork() -> old tg
create new autogroup
lock(tasklist_lock)
move listed threads; child is absent
unlock(tasklist_lock)
drop old autogroup reference
lock(tasklist_lock)
publish child with old tg
The ordering trace hit this pre-publication sequence seven times. The
final revalidation is intended to cover precisely this interval.
Am I missing another ordering which excludes this interleaving?
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH] sched/autogroup: Fix task_group UAF during thread creation
2026-10-30 3:45 ` Daehyeon Ko
@ 2026-10-30 15:49 ` Peter Zijlstra
2026-10-31 2:28 ` Tejun Heo
0 siblings, 1 reply; 8+ messages in thread
From: Peter Zijlstra @ 2026-10-30 15:49 UTC (permalink / raw)
To: Daehyeon Ko
Cc: Ingo Molnar, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
linux-kernel, Tejun Heo
On Fri, Oct 30, 2026 at 12:45:34PM +0900, Daehyeon Ko wrote:
> On Mon, Oct 26, 2026 at 12:57:07PM +0100, Peter Zijlstra wrote:
> > I'm not sure this works; afaict this will result in a NULL deref
> > instead.
>
> Thanks for taking a look.
>
> The NULL previous cfs_rq should be handled by the existing set_task_rq()
> contract. __sched_fork() initializes p->se.cfs_rq to NULL, and the normal
> sched_cgroup_fork() path reaches set_task_rq() through __set_task_cpu().
> set_task_rq_fair() returns when prev is NULL, before
> cfs_rq_last_update_time(prev), and set_task_rq() then installs the new
> cfs_rq, parent, and depth.
>
> > Can't we make ksys_setsid() more complicated instead? Have it create the
> > autogroup in advance, move under the tasklist_lock and destroy on
> > failure or something?
>
> I don't think moving the autogroup move under tasklist_lock alone closes
> the window, because sched_cgroup_fork() runs before copy_process() takes
> tasklist_lock:
>
> clone(CLONE_THREAD) setsid()
> ------------------- --------
> sched_cgroup_fork() -> old tg
> create new autogroup
> lock(tasklist_lock)
> move listed threads; child is absent
> unlock(tasklist_lock)
> drop old autogroup reference
> lock(tasklist_lock)
> publish child with old tg
>
> The ordering trace hit this pre-publication sequence seven times. The
> final revalidation is intended to cover precisely this interval.
>
> Am I missing another ordering which excludes this interleaving?
No, I think you're right. Also, its a mess :/
We have far too many sched hooks here:
free_signal_struct:
sched_autogroup_exit();
__put_task_struct:
sched_core_free();
copy_mm:
sched_cache_fork();
copy_signal:
sched_autogroup_fork();
copy_process:
ret = sched_fork()
scx_pre_fork()
LOCK scx_fork_rwsem
cgroup_can_fork()
LOCK cgroup_threadgroup_rwsem
ret = sched_cgroup_fork();
sched_class::task_fork()
return scx_fork() // requires scx_fork_rwsem;
// also requires p->sched_task_group to be correct;
// scx ignores auto-group
LOCK tasklist_lock
sched_core_fork() // requires tasklist_lock
LOCK siglock
+ sched_autogroup_fork_finish();
UNLOCK siglock
UNLOCK tasklist_lock
cgroup_post_fork()
UNLOCK cgroup_threadgroup_rwsem
sched_post_fork()
scx_post_fork()
UNLOCK scx_fork_rwsem
Now, AUTOGROUP depends on CGROUP, so we can make autogroup_move_group()
also use cgroup_threadgroup_rwsem, such that the autogroup and cgroup
logic stays the same?
I'd hate for this to get more complicated than it already is.
I mean, yes we can move sched_core_fork() into the siglock region, but
duplicating most of sched_cgroup_fork() in there just to fix an
autogroup thingy seems weird and inconsistent.
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH] sched/autogroup: Fix task_group UAF during thread creation
2026-10-30 15:49 ` Peter Zijlstra
@ 2026-10-31 2:28 ` Tejun Heo
2026-11-02 10:17 ` Peter Zijlstra
0 siblings, 1 reply; 8+ messages in thread
From: Tejun Heo @ 2026-10-31 2:28 UTC (permalink / raw)
To: Peter Zijlstra
Cc: Daehyeon Ko, Ingo Molnar, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, linux-kernel
Hello, Peter.
On Fri, Oct 30, 2026 at 04:49:40PM +0100, Peter Zijlstra wrote:
> Now, AUTOGROUP depends on CGROUP, so we can make autogroup_move_group()
> also use cgroup_threadgroup_rwsem, such that the autogroup and cgroup
> logic stays the same?
cgroup_threadgroup_rwsem is a global percpu rwsem and taking it for
write on a regular basis can be really expensive.
> I mean, yes we can move sched_core_fork() into the siglock region, but
> duplicating most of sched_cgroup_fork() in there just to fix an
> autogroup thingy seems weird and inconsistent.
Either way, AFAICS, autogroup doesn't need cgroup_threadgroup_rwsem. The
cgroup side has to be settled early in fork, but the autogroup side
doesn't. Can't we just move the autogroup assignment later in fork, to a
point where it can't race against setsid()?
Thanks.
--
tejun
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH] sched/autogroup: Fix task_group UAF during thread creation
2026-10-31 2:28 ` Tejun Heo
@ 2026-11-02 10:17 ` Peter Zijlstra
2026-11-02 19:21 ` Tejun Heo
0 siblings, 1 reply; 8+ messages in thread
From: Peter Zijlstra @ 2026-11-02 10:17 UTC (permalink / raw)
To: Tejun Heo
Cc: Daehyeon Ko, Ingo Molnar, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, linux-kernel
On Fri, Oct 30, 2026 at 04:28:54PM -1000, Tejun Heo wrote:
> Hello, Peter.
>
> On Fri, Oct 30, 2026 at 04:49:40PM +0100, Peter Zijlstra wrote:
> > Now, AUTOGROUP depends on CGROUP, so we can make autogroup_move_group()
> > also use cgroup_threadgroup_rwsem, such that the autogroup and cgroup
> > logic stays the same?
>
> cgroup_threadgroup_rwsem is a global percpu rwsem and taking it for
> write on a regular basis can be really expensive.
Yeah, but setsid() isn't more critical than moving tasks between
cgroups. Also, AFAIK nearly nobody is using autogroups these days,
because systemd does the cgroup thing.
> > I mean, yes we can move sched_core_fork() into the siglock region, but
> > duplicating most of sched_cgroup_fork() in there just to fix an
> > autogroup thingy seems weird and inconsistent.
>
> Either way, AFAICS, autogroup doesn't need cgroup_threadgroup_rwsem. The
> cgroup side has to be settled early in fork, but the autogroup side
> doesn't. Can't we just move the autogroup assignment later in fork, to a
> point where it can't race against setsid()?
That is more or less the proposal, but it rather creates more complexity
rather than reducing it.
Having two different rules for p->sched_task_group, depending on cgroup
or autogroup isn't at all appealing. Not to mention adding even more
fork callbacks.
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH] sched/autogroup: Fix task_group UAF during thread creation
2026-11-02 10:17 ` Peter Zijlstra
@ 2026-11-02 19:21 ` Tejun Heo
2026-11-03 11:45 ` Peter Zijlstra
0 siblings, 1 reply; 8+ messages in thread
From: Tejun Heo @ 2026-11-02 19:21 UTC (permalink / raw)
To: Peter Zijlstra
Cc: Daehyeon Ko, Ingo Molnar, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, linux-kernel
Hello, Peter.
On Mon, Nov 02, 2026 at 11:17:59AM +0100, Peter Zijlstra wrote:
> On Fri, Oct 30, 2026 at 04:28:54PM -1000, Tejun Heo wrote:
> > cgroup_threadgroup_rwsem is a global percpu rwsem and taking it for
> > write on a regular basis can be really expensive.
>
> Yeah, but setsid() isn't more critical than moving tasks between
> cgroups. Also, AFAIK nearly nobody is using autogroups these days,
> because systemd does the cgroup thing.
We don't want to take the write lock on every setsid(). It would have to be
limited to actual autogroup joins, which seems doable but likely adds a bit
of complication.
> > Either way, AFAICS, autogroup doesn't need cgroup_threadgroup_rwsem. The
> > cgroup side has to be settled early in fork, but the autogroup side
> > doesn't. Can't we just move the autogroup assignment later in fork, to a
> > point where it can't race against setsid()?
>
> That is more or less the proposal, but it rather creates more complexity
> rather than reducing it.
>
> Having two different rules for p->sched_task_group, depending on cgroup
> or autogroup isn't at all appealing. Not to mention adding even more
> fork callbacks.
I see your point. Moving the autogroup handling into the cpu controller
keeps it better contained though. sched_cgroup_fork() places the new task by
its cgroup only, and the cpu controller applies the autogroup once the task
is on the thread list. This isn't a hill I'm going to die on, but it looks
like the better direction to me. Can you please take a look at the patch
below?
diff --git a/kernel/sched/autogroup.h b/kernel/sched/autogroup.h
--- a/kernel/sched/autogroup.h
+++ b/kernel/sched/autogroup.h
@@ -27,12 +27,12 @@ static inline bool task_group_is_autogroup(struct task_group *tg)
return !!tg->autogroup;
}
+extern unsigned int sysctl_sched_autogroup_enabled;
extern bool task_wants_autogroup(struct task_struct *p, struct task_group *tg);
static inline struct task_group *
autogroup_task_group(struct task_struct *p, struct task_group *tg)
{
- extern unsigned int sysctl_sched_autogroup_enabled;
int enabled = READ_ONCE(sysctl_sched_autogroup_enabled);
if (enabled && task_wants_autogroup(p, tg))
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -4958,7 +4958,6 @@ int sched_cgroup_fork(struct task_struct *p, struct kernel_clone_args *kargs)
struct task_group *tg;
tg = container_of(kargs->cset->subsys[cpu_cgrp_id],
struct task_group, css);
- tg = autogroup_task_group(p, tg);
p->sched_task_group = tg;
}
#endif
@@ -9598,6 +9597,15 @@ static void cpu_cgroup_cancel_attach(struct cgroup_taskset *tset)
scx_cgroup_cancel_attach(tset);
}
+#ifdef CONFIG_SCHED_AUTOGROUP
+static void cpu_cgroup_fork(struct task_struct *p)
+{
+ if (READ_ONCE(p->sched_task_group) == &root_task_group &&
+ READ_ONCE(sysctl_sched_autogroup_enabled))
+ sched_move_task(p, true);
+}
+#endif
+
#ifdef CONFIG_UCLAMP_TASK_GROUP
static void cpu_util_update_eff(struct cgroup_subsys_state *css)
{
@@ -10562,6 +10570,9 @@ struct cgroup_subsys cpu_cgrp_subsys = {
.can_attach = cpu_cgroup_can_attach,
.attach = cpu_cgroup_attach,
.cancel_attach = cpu_cgroup_cancel_attach,
+#ifdef CONFIG_SCHED_AUTOGROUP
+ .fork = cpu_cgroup_fork,
+#endif
.legacy_cftypes = cpu_legacy_files,
.dfl_cftypes = cpu_files,
.early_init = true,
Thanks.
--
tejun
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH] sched/autogroup: Fix task_group UAF during thread creation
2026-11-02 19:21 ` Tejun Heo
@ 2026-11-03 11:45 ` Peter Zijlstra
0 siblings, 0 replies; 8+ messages in thread
From: Peter Zijlstra @ 2026-11-03 11:45 UTC (permalink / raw)
To: Tejun Heo
Cc: Daehyeon Ko, Ingo Molnar, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, linux-kernel
On Mon, Nov 02, 2026 at 09:21:14AM -1000, Tejun Heo wrote:
> I see your point. Moving the autogroup handling into the cpu controller
> keeps it better contained though. sched_cgroup_fork() places the new task by
> its cgroup only, and the cpu controller applies the autogroup once the task
> is on the thread list. This isn't a hill I'm going to die on, but it looks
> like the better direction to me. Can you please take a look at the patch
> below?
>
> diff --git a/kernel/sched/autogroup.h b/kernel/sched/autogroup.h
> --- a/kernel/sched/autogroup.h
> +++ b/kernel/sched/autogroup.h
> @@ -27,12 +27,12 @@ static inline bool task_group_is_autogroup(struct task_group *tg)
> return !!tg->autogroup;
> }
>
> +extern unsigned int sysctl_sched_autogroup_enabled;
> extern bool task_wants_autogroup(struct task_struct *p, struct task_group *tg);
>
> static inline struct task_group *
> autogroup_task_group(struct task_struct *p, struct task_group *tg)
> {
> - extern unsigned int sysctl_sched_autogroup_enabled;
> int enabled = READ_ONCE(sysctl_sched_autogroup_enabled);
>
> if (enabled && task_wants_autogroup(p, tg))
> diff --git a/kernel/sched/core.c b/kernel/sched/core.c
> --- a/kernel/sched/core.c
> +++ b/kernel/sched/core.c
> @@ -4958,7 +4958,6 @@ int sched_cgroup_fork(struct task_struct *p, struct kernel_clone_args *kargs)
> struct task_group *tg;
> tg = container_of(kargs->cset->subsys[cpu_cgrp_id],
> struct task_group, css);
> - tg = autogroup_task_group(p, tg);
> p->sched_task_group = tg;
> }
> #endif
> @@ -9598,6 +9597,15 @@ static void cpu_cgroup_cancel_attach(struct cgroup_taskset *tset)
> scx_cgroup_cancel_attach(tset);
> }
>
> +#ifdef CONFIG_SCHED_AUTOGROUP
> +static void cpu_cgroup_fork(struct task_struct *p)
> +{
> + if (READ_ONCE(p->sched_task_group) == &root_task_group &&
> + READ_ONCE(sysctl_sched_autogroup_enabled))
> + sched_move_task(p, true);
> +}
> +#endif
> +
> #ifdef CONFIG_UCLAMP_TASK_GROUP
> static void cpu_util_update_eff(struct cgroup_subsys_state *css)
> {
> @@ -10562,6 +10570,9 @@ struct cgroup_subsys cpu_cgrp_subsys = {
> .can_attach = cpu_cgroup_can_attach,
> .attach = cpu_cgroup_attach,
> .cancel_attach = cpu_cgroup_cancel_attach,
> +#ifdef CONFIG_SCHED_AUTOGROUP
> + .fork = cpu_cgroup_fork,
> +#endif
> .legacy_cftypes = cpu_legacy_files,
> .dfl_cftypes = cpu_files,
> .early_init = true,
>
Right; so by not doing the autogroup assignment in sched_cgroup_fork()
(which is before publishing the task) but moving it to
cgroup_post_fork() (which is post publishing), we solve the race where
setsid() misses the new task, since the task will update post publish.
Now, using cgroup_post_fork() seems weird (also, I'm not sure if this
calls cgroup_subsys::fork for *all* controllers, or only the enabled
ones).
But we can easily move this into sched_post_fork(); all we really need
to ensure is this happens before scx_post_fork(), right?
Something like so?
---
diff --git a/kernel/sched/autogroup.h b/kernel/sched/autogroup.h
index 06c82b2bdfb5..14cf3fde9767 100644
--- a/kernel/sched/autogroup.h
+++ b/kernel/sched/autogroup.h
@@ -19,6 +19,7 @@ struct autogroup {
int nice;
};
+extern unsigned int sysctl_sched_autogroup_enabled;
extern void autogroup_init(struct task_struct *init_task);
extern void autogroup_free(struct task_group *tg);
@@ -32,7 +33,6 @@ extern bool task_wants_autogroup(struct task_struct *p, struct task_group *tg);
static inline struct task_group *
autogroup_task_group(struct task_struct *p, struct task_group *tg)
{
- extern unsigned int sysctl_sched_autogroup_enabled;
int enabled = READ_ONCE(sysctl_sched_autogroup_enabled);
if (enabled && task_wants_autogroup(p, tg))
@@ -41,6 +41,16 @@ autogroup_task_group(struct task_struct *p, struct task_group *tg)
return tg;
}
+static inline void autogroup_post_fork(struct task_struct *p)
+{
+ /*
+ * If setsid() missed this new task; move it to the new autogroup now.
+ */
+ if (READ_ONCE(sysctl_sched_autogroup_enabled) &&
+ task_wants_autogroup(p, p->sched_task_group))
+ sched_move_task(p, true);
+}
+
extern int autogroup_path(struct task_group *tg, char *buf, int buflen);
#else /* !CONFIG_SCHED_AUTOGROUP: */
@@ -58,6 +68,8 @@ autogroup_task_group(struct task_struct *p, struct task_group *tg)
return tg;
}
+static inline void autogroup_post_fork(struct task_struct *p) { }
+
static inline int autogroup_path(struct task_group *tg, char *buf, int buflen)
{
return 0;
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 04b62c16fa8c..3e05f7c959e6 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -4958,7 +4958,13 @@ int sched_cgroup_fork(struct task_struct *p, struct kernel_clone_args *kargs)
struct task_group *tg;
tg = container_of(kargs->cset->subsys[cpu_cgrp_id],
struct task_group, css);
+ /*
+ * No autogroup here because this is unsafe vs setsid()
+ * which will not see this thread yet. Fixed up in
+ * sched_post_fork().
+ *
tg = autogroup_task_group(p, tg);
+ */
p->sched_task_group = tg;
}
#endif
@@ -4983,6 +4989,7 @@ static void sched_mm_cid_fork(struct task_struct *t);
void sched_post_fork(struct task_struct *p)
{
+ autogroup_post_fork(p);
sched_mm_cid_fork(p);
uclamp_post_fork(p);
scx_post_fork(p);
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-11-03 11:46 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-23 1:36 [PATCH] sched/autogroup: Fix task_group UAF during thread creation Daehyeon Ko
2026-10-26 11:57 ` Peter Zijlstra
2026-10-30 3:45 ` Daehyeon Ko
2026-10-30 15:49 ` Peter Zijlstra
2026-10-31 2:28 ` Tejun Heo
2026-11-02 10:17 ` Peter Zijlstra
2026-11-02 19:21 ` Tejun Heo
2026-11-03 11:45 ` Peter Zijlstra
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®