* [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; 6+ 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] 6+ 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; 6+ 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] 6+ 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; 6+ 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] 6+ 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; 6+ 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] 6+ 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; 6+ 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] 6+ 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
0 siblings, 0 replies; 6+ 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] 6+ messages in thread
end of thread, other threads:[~2026-11-02 10:18 UTC | newest]
Thread overview: 6+ 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
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®