Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH] sched/cache: Fix use-after-free of the mm replaced by exec
@ 2026-08-30  7:30 Hyunwoo Kim
  2026-08-31  4:22 ` Chen, Yu C
  0 siblings, 1 reply; 9+ messages in thread
From: Hyunwoo Kim @ 2026-08-30  7:30 UTC (permalink / raw)
  To: Peter Zijlstra, Ingo Molnar
  Cc: Chen Yu, Tim Chen, Kees Cook, Christian Brauner, Alexander Viro,
	Jan Kara, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
	Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider,
	K Prateek Nayak, Shrikanth Hegde, Qais Yousef, Aaron Lu,
	Srikar Dronamraju, Vineeth Remanan Pillai, linux-kernel, linux-mm,
	linux-fsdevel, imv4bel

When the waker cannot use the wakelist, ttwu_queue() takes the target rq
lock and goes down into update_curr(). If the target rq belongs to another
CPU, the task handed to account_mm_sched() is the one running on that CPU,
not the task being woken.

account_mm_sched() reads p->mm and updates mm->sc_stat. Nothing keeps that
mm alive. The rq lock and rq->cpu_epoch_lock it holds have nothing to do
with the lifetime of the mm.

If that task happens to be in execve(), exec_mmap() points tsk->mm and
tsk->active_mm at the new mm, and exec_mm_put_old() from setup_new_exec()
drops the old one. free_bprm() does the same when exec fails. On the way
from mmput() down to __mmdrop(), mm_destroy_sched() calls free_percpu() on
sc_stat.pcpu_sched and free_mm() returns the mm_struct.

Whoever already read the old pointer keeps using it. It adds to runtime in
the freed per-cpu area and reads sc_stat in the freed mm_struct. Depending
on the condition it also writes sc_stat.cpu. That is a use-after-free.

Commit 9f23469401b0 ("sched/cache: Fix potential NULL mm pointer access")
changed the remaining p->mm dereference to the local variable, and said the
active_mm reference keeps the structure allocated. That holds for the other
paths that detach an mm, since they take an mmgrab_lazy_tlb() reference.
exec reassigns active_mm to the new mm as well, so that reference is gone.
What is left is the mm_users reference in bprm->old_mm, and dropping it is
the free.

             CPU0                                CPU1

                                      write(pipe)
                                      try_to_wake_up()
                                        ttwu_queue()      // takes rq0 lock
                                          enqueue_task_fair()
                                            update_curr()
                                              update_se()
                                                account_mm_sched()
                                                  mm = rq0->curr->mm
                                                        // old mm
  execve()
    exec_mmap()                       // tsk->mm = new mm
    setup_new_exec()
      exec_mm_put_old()
        mmput() -> ... -> __mmdrop()
          mm_destroy_sched()          // free_percpu()
          free_mm()
                                                  read mm->sc_stat.epoch
                                                        // use-after-free

KASAN log:

  BUG: KASAN: slab-use-after-free in update_se+0xe6e/0xf70
  Read of size 8 at addr ffff8880093cad10 by task sc-direct-set/80
  ...
  Call Trace:
   update_se+0xe6e/0xf70
   update_curr.isra.0+0x28/0x380
   enqueue_task_fair+0xb58/0x3560
   enqueue_task+0x70/0x170
   ttwu_do_activate+0xeb/0x590
   try_to_wake_up+0x79b/0x14f0
   autoremove_wake_function+0x16/0x150
   __wake_up_common+0xed/0x160
   __wake_up_sync_key+0x36/0x50
   anon_pipe_write+0xa62/0x1830
   vfs_write+0xa4f/0xcf0
   ksys_write+0x17c/0x1c0
   do_syscall_64+0xdd/0x4a0
   entry_SYSCALL_64_after_hwframe+0x77/0x7f
  ...
  Freed by task 1:
   kmem_cache_free+0xba/0x3b0
   setup_new_exec+0x2c4/0x3d0
   load_elf_binary+0x435/0x4740
   bprm_execve+0x6d2/0x1290
   do_execveat_common.isra.0+0x3a3/0x580
   __x64_sys_execve+0x8e/0xc0
  ...
  The buggy address belongs to the object at ffff8880093cab80
   which belongs to the cache mm_struct of size 1688
  The buggy address is located 400 bytes inside of
   freed 1688-byte region [ffff8880093cab80, ffff8880093cb218)

Take and release the exec'ing task's own rq lock once before the old mm is
dropped. The task account_mm_sched() looks at is that rq's curr, so it is
the same lock a reader holding the old pointer is on. If the task migrated
to another CPU in between, leaving that rq required the same lock, so a
reader there has already finished.

That reader only uses the mm inside the rq lock and never hands the
pointer out. Getting the lock means it is done, and whoever takes the lock
after that sees the new tsk->mm.

Fixes: df0d98475954 ("sched/cache: Introduce infrastructure for cache-aware load balancing")
Cc: stable@vger.kernel.org
Signed-off-by: Hyunwoo Kim <imv4bel@gmail.com>
---
 fs/exec.c             |  1 +
 include/linux/sched.h |  4 ++++
 kernel/events/core.c  |  2 ++
 kernel/sched/fair.c   | 16 ++++++++++++++++
 4 files changed, 23 insertions(+)

diff --git a/fs/exec.c b/fs/exec.c
index 745f6eb5279e6..6194c38807980 100644
--- a/fs/exec.c
+++ b/fs/exec.c
@@ -916,6 +916,7 @@ static void exec_mm_put_old(struct mm_struct *old_mm)
 {
 	setmax_mm_hiwater_rss(&current->signal->maxrss, old_mm);
 	mm_update_next_owner(old_mm);
+	sched_cache_exec_done();
 	mmput(old_mm);
 }
 
diff --git a/include/linux/sched.h b/include/linux/sched.h
index 8b3d47a325cca..6ae31bffe049e 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -2415,10 +2415,14 @@ struct sched_cache_stat {
 	int cpu;
 } ____cacheline_aligned_in_smp;
 
+void sched_cache_exec_done(void);
+
 #else
 
 struct sched_cache_stat { };
 
+static inline void sched_cache_exec_done(void) { }
+
 #endif
 
 #ifndef MODULE
diff --git a/kernel/events/core.c b/kernel/events/core.c
index a6c8e38a31104..2f29cbccf03f1 100644
--- a/kernel/events/core.c
+++ b/kernel/events/core.c
@@ -5427,6 +5427,8 @@ attach_task_ctx_data(struct task_struct *task, struct kmem_cache *ctx_cache,
 	if (!cd)
 		return -ENOMEM;
 
+	/* @old, loaded by the try_cmpxchg() below, is only stable under RCU. */
+	guard(rcu)();
 	for (;;) {
 		if (try_cmpxchg(&task->perf_ctx_data, &old, cd)) {
 			if (old)
diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
index 6d881e530f891..fc63bfcbccc5b 100644
--- a/kernel/sched/fair.c
+++ b/kernel/sched/fair.c
@@ -1989,6 +1989,22 @@ void init_sched_mm(struct task_struct *p)
 	p->preferred_llc = -1;
 }
 
+/* exec() has switched to the new mm and is about to drop the old one. */
+void sched_cache_exec_done(void)
+{
+	struct rq_flags rf;
+	struct rq *rq;
+
+	/*
+	 * account_mm_sched() dereferences rq->curr->mm under this rq's lock,
+	 * so a remote CPU can still be using the old mm. The lock cycle waits
+	 * for it, and the store to tsk->mm cannot be reordered past the
+	 * release, so later acquirers see the new mm.
+	 */
+	rq = this_rq_lock_irq(&rf);
+	rq_unlock_irq(rq, &rf);
+}
+
 #else /* CONFIG_SCHED_CACHE */
 
 static inline void account_mm_sched(struct rq *rq, struct task_struct *p,
-- 
2.43.0



^ permalink raw reply related	[flat|nested] 9+ messages in thread

* Re: [PATCH] sched/cache: Fix use-after-free of the mm replaced by exec
  2026-08-30  7:30 [PATCH] sched/cache: Fix use-after-free of the mm replaced by exec Hyunwoo Kim
@ 2026-08-31  4:22 ` Chen, Yu C
  2026-08-31 19:28   ` Hyunwoo Kim
  0 siblings, 1 reply; 9+ messages in thread
From: Chen, Yu C @ 2026-08-31  4:22 UTC (permalink / raw)
  To: Hyunwoo Kim
  Cc: Tim Chen, Kees Cook, Christian Brauner, Alexander Viro, Jan Kara,
	Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt,
	Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak,
	Shrikanth Hegde, Qais Yousef, Aaron Lu, Srikar Dronamraju,
	Vineeth Remanan Pillai, linux-kernel, linux-mm, linux-fsdevel,
	Ingo Molnar, Peter Zijlstra, chen.yu@linux.dev

Hi Hyunwoo,

On 8/30/2026 3:30 PM, Hyunwoo Kim wrote:
> When the waker cannot use the wakelist, ttwu_queue() takes the target rq
> lock and goes down into update_curr(). If the target rq belongs to another
> CPU, the task handed to account_mm_sched() is the one running on that CPU,
> not the task being woken.
> 
> account_mm_sched() reads p->mm and updates mm->sc_stat. Nothing keeps that
> mm alive. The rq lock and rq->cpu_epoch_lock it holds have nothing to do
> with the lifetime of the mm.
> 
> If that task happens to be in execve(), exec_mmap() points tsk->mm and
> tsk->active_mm at the new mm, and exec_mm_put_old() from setup_new_exec()
> drops the old one. free_bprm() does the same when exec fails. On the way
> from mmput() down to __mmdrop(), mm_destroy_sched() calls free_percpu() on
> sc_stat.pcpu_sched and free_mm() returns the mm_struct.
> 
> Whoever already read the old pointer keeps using it. It adds to runtime in
> the freed per-cpu area and reads sc_stat in the freed mm_struct. Depending
> on the condition it also writes sc_stat.cpu. That is a use-after-free.
> 
> Commit 9f23469401b0 ("sched/cache: Fix potential NULL mm pointer access")
> changed the remaining p->mm dereference to the local variable, and said the
> active_mm reference keeps the structure allocated. That holds for the other
> paths that detach an mm, since they take an mmgrab_lazy_tlb() reference.
> exec reassigns active_mm to the new mm as well, so that reference is gone.
> What is left is the mm_users reference in bprm->old_mm, and dropping it is
> the free.
> 
>               CPU0                                CPU1
> 
>                                        write(pipe)
>                                        try_to_wake_up()
>                                          ttwu_queue()      // takes rq0 lock
>                                            enqueue_task_fair()
>                                              update_curr()
>                                                update_se()
>                                                  account_mm_sched()
>                                                    mm = rq0->curr->mm
>                                                          // old mm
>    execve()
>      exec_mmap()                       // tsk->mm = new mm
>      setup_new_exec()
>        exec_mm_put_old()
>          mmput() -> ... -> __mmdrop()
>            mm_destroy_sched()          // free_percpu()
>            free_mm()
>                                                    read mm->sc_stat.epoch
>                                                          // use-after-free
> 

Ah, thanks for catching this.

> ---
>   fs/exec.c             |  1 +
>   include/linux/sched.h |  4 ++++
>   kernel/events/core.c  |  2 ++
>   kernel/sched/fair.c   | 16 ++++++++++++++++
>   4 files changed, 23 insertions(+)
> 
> diff --git a/fs/exec.c b/fs/exec.c
> index 745f6eb5279e6..6194c38807980 100644
> --- a/fs/exec.c
> +++ b/fs/exec.c
> @@ -916,6 +916,7 @@ static void exec_mm_put_old(struct mm_struct *old_mm)
>   {
>   	setmax_mm_hiwater_rss(&current->signal->maxrss, old_mm);
>   	mm_update_next_owner(old_mm);
> +	sched_cache_exec_done();
>   	mmput(old_mm);
>   }
>   
> diff --git a/include/linux/sched.h b/include/linux/sched.h
> index 8b3d47a325cca..6ae31bffe049e 100644
> --- a/include/linux/sched.h
> +++ b/include/linux/sched.h
> @@ -2415,10 +2415,14 @@ struct sched_cache_stat {
>   	int cpu;
>   } ____cacheline_aligned_in_smp;
>   
> +void sched_cache_exec_done(void);
> +
>   #else
>   
>   struct sched_cache_stat { };
>   
> +static inline void sched_cache_exec_done(void) { }
> +
>   #endif
>   
>   #ifndef MODULE
> diff --git a/kernel/events/core.c b/kernel/events/core.c
> index a6c8e38a31104..2f29cbccf03f1 100644
> --- a/kernel/events/core.c
> +++ b/kernel/events/core.c
> @@ -5427,6 +5427,8 @@ attach_task_ctx_data(struct task_struct *task, struct kmem_cache *ctx_cache,
>   	if (!cd)
>   		return -ENOMEM;
>   
> +	/* @old, loaded by the try_cmpxchg() below, is only stable under RCU. */
> +	guard(rcu)();

Is this change related to this UAF issue?

> +/* exec() has switched to the new mm and is about to drop the old one. */
> +void sched_cache_exec_done(void)
> +{
> +	struct rq_flags rf;
> +	struct rq *rq;
> +
> +	/*
> +	 * account_mm_sched() dereferences rq->curr->mm under this rq's lock,
> +	 * so a remote CPU can still be using the old mm. The lock cycle waits
> +	 * for it, and the store to tsk->mm cannot be reordered past the
> +	 * release, so later acquirers see the new mm.
> +	 */
> +	rq = this_rq_lock_irq(&rf);

A smart fix, learnt! It behaves like a synchronize_rcu() to protect against
the read in account_mm_sched(). Small open: since the context of invoking
account_mm_sched() is preemption-disabled, I wonder if we can simply use
synchronize_rcu() directly instead of this_rq_lock_irq() - just to avoid
contention for rq-lock in heavy system?

thanks,
Chenyu


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH] sched/cache: Fix use-after-free of the mm replaced by exec
  2026-08-31  4:22 ` Chen, Yu C
@ 2026-08-31 19:28   ` Hyunwoo Kim
  2026-08-31 20:25     ` Tim Chen
  0 siblings, 1 reply; 9+ messages in thread
From: Hyunwoo Kim @ 2026-08-31 19:28 UTC (permalink / raw)
  To: Chen, Yu C
  Cc: Tim Chen, Kees Cook, Christian Brauner, Alexander Viro, Jan Kara,
	Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt,
	Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak,
	Shrikanth Hegde, Qais Yousef, Aaron Lu, Srikar Dronamraju,
	Vineeth Remanan Pillai, linux-kernel, linux-mm, linux-fsdevel,
	Ingo Molnar, Peter Zijlstra, chen.yu@linux.dev, imv4bel

On Mon, Aug 31, 2026 at 12:22:45PM +0800, Chen, Yu C wrote:
> Hi Hyunwoo,
> 
> On 8/30/2026 3:30 PM, Hyunwoo Kim wrote:
> > When the waker cannot use the wakelist, ttwu_queue() takes the target rq
> > lock and goes down into update_curr(). If the target rq belongs to another
> > CPU, the task handed to account_mm_sched() is the one running on that CPU,
> > not the task being woken.
> > 
> > account_mm_sched() reads p->mm and updates mm->sc_stat. Nothing keeps that
> > mm alive. The rq lock and rq->cpu_epoch_lock it holds have nothing to do
> > with the lifetime of the mm.
> > 
> > If that task happens to be in execve(), exec_mmap() points tsk->mm and
> > tsk->active_mm at the new mm, and exec_mm_put_old() from setup_new_exec()
> > drops the old one. free_bprm() does the same when exec fails. On the way
> > from mmput() down to __mmdrop(), mm_destroy_sched() calls free_percpu() on
> > sc_stat.pcpu_sched and free_mm() returns the mm_struct.
> > 
> > Whoever already read the old pointer keeps using it. It adds to runtime in
> > the freed per-cpu area and reads sc_stat in the freed mm_struct. Depending
> > on the condition it also writes sc_stat.cpu. That is a use-after-free.
> > 
> > Commit 9f23469401b0 ("sched/cache: Fix potential NULL mm pointer access")
> > changed the remaining p->mm dereference to the local variable, and said the
> > active_mm reference keeps the structure allocated. That holds for the other
> > paths that detach an mm, since they take an mmgrab_lazy_tlb() reference.
> > exec reassigns active_mm to the new mm as well, so that reference is gone.
> > What is left is the mm_users reference in bprm->old_mm, and dropping it is
> > the free.
> > 
> >               CPU0                                CPU1
> > 
> >                                        write(pipe)
> >                                        try_to_wake_up()
> >                                          ttwu_queue()      // takes rq0 lock
> >                                            enqueue_task_fair()
> >                                              update_curr()
> >                                                update_se()
> >                                                  account_mm_sched()
> >                                                    mm = rq0->curr->mm
> >                                                          // old mm
> >    execve()
> >      exec_mmap()                       // tsk->mm = new mm
> >      setup_new_exec()
> >        exec_mm_put_old()
> >          mmput() -> ... -> __mmdrop()
> >            mm_destroy_sched()          // free_percpu()
> >            free_mm()
> >                                                    read mm->sc_stat.epoch
> >                                                          // use-after-free
> > 
> 
> Ah, thanks for catching this.
> 
> > ---
> >   fs/exec.c             |  1 +
> >   include/linux/sched.h |  4 ++++
> >   kernel/events/core.c  |  2 ++
> >   kernel/sched/fair.c   | 16 ++++++++++++++++
> >   4 files changed, 23 insertions(+)
> > 
> > diff --git a/fs/exec.c b/fs/exec.c
> > index 745f6eb5279e6..6194c38807980 100644
> > --- a/fs/exec.c
> > +++ b/fs/exec.c
> > @@ -916,6 +916,7 @@ static void exec_mm_put_old(struct mm_struct *old_mm)
> >   {
> >   	setmax_mm_hiwater_rss(&current->signal->maxrss, old_mm);
> >   	mm_update_next_owner(old_mm);
> > +	sched_cache_exec_done();
> >   	mmput(old_mm);
> >   }
> > diff --git a/include/linux/sched.h b/include/linux/sched.h
> > index 8b3d47a325cca..6ae31bffe049e 100644
> > --- a/include/linux/sched.h
> > +++ b/include/linux/sched.h
> > @@ -2415,10 +2415,14 @@ struct sched_cache_stat {
> >   	int cpu;
> >   } ____cacheline_aligned_in_smp;
> > +void sched_cache_exec_done(void);
> > +
> >   #else
> >   struct sched_cache_stat { };
> > +static inline void sched_cache_exec_done(void) { }
> > +
> >   #endif
> >   #ifndef MODULE
> > diff --git a/kernel/events/core.c b/kernel/events/core.c
> > index a6c8e38a31104..2f29cbccf03f1 100644
> > --- a/kernel/events/core.c
> > +++ b/kernel/events/core.c
> > @@ -5427,6 +5427,8 @@ attach_task_ctx_data(struct task_struct *task, struct kmem_cache *ctx_cache,
> >   	if (!cd)
> >   		return -ENOMEM;
> > +	/* @old, loaded by the try_cmpxchg() below, is only stable under RCU. */
> > +	guard(rcu)();
> 
> Is this change related to this UAF issue?

Duh.. that one is unrelated. My mistake.

> 
> > +/* exec() has switched to the new mm and is about to drop the old one. */
> > +void sched_cache_exec_done(void)
> > +{
> > +	struct rq_flags rf;
> > +	struct rq *rq;
> > +
> > +	/*
> > +	 * account_mm_sched() dereferences rq->curr->mm under this rq's lock,
> > +	 * so a remote CPU can still be using the old mm. The lock cycle waits
> > +	 * for it, and the store to tsk->mm cannot be reordered past the
> > +	 * release, so later acquirers see the new mm.
> > +	 */
> > +	rq = this_rq_lock_irq(&rf);
> 
> A smart fix, learnt! It behaves like a synchronize_rcu() to protect against
> the read in account_mm_sched(). Small open: since the context of invoking
> account_mm_sched() is preemption-disabled, I wonder if we can simply use
> synchronize_rcu() directly instead of this_rq_lock_irq() - just to avoid
> contention for rq-lock in heavy system?

Yeah, that works too. I think rcu is nicer here as well. I'll do some
testing and send a v2.


Best regards,
Hyunwoo Kim


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH] sched/cache: Fix use-after-free of the mm replaced by exec
  2026-08-31 19:28   ` Hyunwoo Kim
@ 2026-08-31 20:25     ` Tim Chen
  2026-08-31 21:44       ` Hyunwoo Kim
  0 siblings, 1 reply; 9+ messages in thread
From: Tim Chen @ 2026-08-31 20:25 UTC (permalink / raw)
  To: Hyunwoo Kim, Chen, Yu C
  Cc: Kees Cook, Christian Brauner, Alexander Viro, Jan Kara,
	Juri Lelli, Vincent Guittot, Dietmar Eggemann, Steven Rostedt,
	Ben Segall, Mel Gorman, Valentin Schneider, K Prateek Nayak,
	Shrikanth Hegde, Qais Yousef, Aaron Lu, Srikar Dronamraju,
	Vineeth Remanan Pillai, linux-kernel, linux-mm, linux-fsdevel,
	Ingo Molnar, Peter Zijlstra, chen.yu@linux.dev

On Tue, 2026-09-01 at 04:28 +0900, Hyunwoo Kim wrote:
> On Mon, Aug 31, 2026 at 12:22:45PM +0800, Chen, Yu C wrote:
> > Hi Hyunwoo,
> > 
> > On 8/30/2026 3:30 PM, Hyunwoo Kim wrote:
> > > When the waker cannot use the wakelist, ttwu_queue() takes the target rq
> > > lock and goes down into update_curr(). If the target rq belongs to another
> > > CPU, the task handed to account_mm_sched() is the one running on that CPU,
> > > not the task being woken.
> > > 
> > > account_mm_sched() reads p->mm and updates mm->sc_stat. Nothing keeps that
> > > mm alive. The rq lock and rq->cpu_epoch_lock it holds have nothing to do
> > > with the lifetime of the mm.
> > > 
> > > If that task happens to be in execve(), exec_mmap() points tsk->mm and
> > > tsk->active_mm at the new mm, and exec_mm_put_old() from setup_new_exec()
> > > drops the old one. free_bprm() does the same when exec fails. On the way
> > > from mmput() down to __mmdrop(), mm_destroy_sched() calls free_percpu() on
> > > sc_stat.pcpu_sched and free_mm() returns the mm_struct.
> > > 
> > > Whoever already read the old pointer keeps using it. It adds to runtime in
> > > the freed per-cpu area and reads sc_stat in the freed mm_struct. Depending
> > > on the condition it also writes sc_stat.cpu. That is a use-after-free.
> > > 
> > > Commit 9f23469401b0 ("sched/cache: Fix potential NULL mm pointer access")
> > > changed the remaining p->mm dereference to the local variable, and said the
> > > active_mm reference keeps the structure allocated. That holds for the other
> > > paths that detach an mm, since they take an mmgrab_lazy_tlb() reference.
> > > exec reassigns active_mm to the new mm as well, so that reference is gone.
> > > What is left is the mm_users reference in bprm->old_mm, and dropping it is
> > > the free.
> > > 
> > >               CPU0                                CPU1
> > > 
> > >                                        write(pipe)
> > >                                        try_to_wake_up()
> > >                                          ttwu_queue()      // takes rq0 lock
> > >                                            enqueue_task_fair()
> > >                                              update_curr()
> > >                                                update_se()
> > >                                                  account_mm_sched()
> > >                                                    mm = rq0->curr->mm
> > >                                                          // old mm
> > >    execve()
> > >      exec_mmap()                       // tsk->mm = new mm
> > >      setup_new_exec()
> > >        exec_mm_put_old()
> > >          mmput() -> ... -> __mmdrop()
> > >            mm_destroy_sched()          // free_percpu()
> > >            free_mm()
> > >                                                    read mm->sc_stat.epoch
> > >                                                          // use-after-free
> > > 
> > 
> > Ah, thanks for catching this.
> > 
> > > ---
> > >   fs/exec.c             |  1 +
> > >   include/linux/sched.h |  4 ++++
> > >   kernel/events/core.c  |  2 ++
> > >   kernel/sched/fair.c   | 16 ++++++++++++++++
> > >   4 files changed, 23 insertions(+)
> > > 
> > > diff --git a/fs/exec.c b/fs/exec.c
> > > index 745f6eb5279e6..6194c38807980 100644
> > > --- a/fs/exec.c
> > > +++ b/fs/exec.c
> > > @@ -916,6 +916,7 @@ static void exec_mm_put_old(struct mm_struct *old_mm)
> > >   {
> > >   	setmax_mm_hiwater_rss(&current->signal->maxrss, old_mm);
> > >   	mm_update_next_owner(old_mm);
> > > +	sched_cache_exec_done();
> > >   	mmput(old_mm);
> > >   }
> > > diff --git a/include/linux/sched.h b/include/linux/sched.h
> > > index 8b3d47a325cca..6ae31bffe049e 100644
> > > --- a/include/linux/sched.h
> > > +++ b/include/linux/sched.h
> > > @@ -2415,10 +2415,14 @@ struct sched_cache_stat {
> > >   	int cpu;
> > >   } ____cacheline_aligned_in_smp;
> > > +void sched_cache_exec_done(void);
> > > +
> > >   #else
> > >   struct sched_cache_stat { };
> > > +static inline void sched_cache_exec_done(void) { }
> > > +
> > >   #endif
> > >   #ifndef MODULE
> > > diff --git a/kernel/events/core.c b/kernel/events/core.c
> > > index a6c8e38a31104..2f29cbccf03f1 100644
> > > --- a/kernel/events/core.c
> > > +++ b/kernel/events/core.c
> > > @@ -5427,6 +5427,8 @@ attach_task_ctx_data(struct task_struct *task, struct kmem_cache *ctx_cache,
> > >   	if (!cd)
> > >   		return -ENOMEM;
> > > +	/* @old, loaded by the try_cmpxchg() below, is only stable under RCU. */
> > > +	guard(rcu)();
> > 
> > Is this change related to this UAF issue?
> 
> Duh.. that one is unrelated. My mistake.
> 
> > 
> > > +/* exec() has switched to the new mm and is about to drop the old one. */
> > > +void sched_cache_exec_done(void)
> > > +{
> > > +	struct rq_flags rf;
> > > +	struct rq *rq;
> > > +
> > > +	/*
> > > +	 * account_mm_sched() dereferences rq->curr->mm under this rq's lock,
> > > +	 * so a remote CPU can still be using the old mm. The lock cycle waits
> > > +	 * for it, and the store to tsk->mm cannot be reordered past the
> > > +	 * release, so later acquirers see the new mm.
> > > +	 */
> > > +	rq = this_rq_lock_irq(&rf);
> > 
> > A smart fix, learnt! It behaves like a synchronize_rcu() to protect against
> > the read in account_mm_sched(). Small open: since the context of invoking
> > account_mm_sched() is preemption-disabled, I wonder if we can simply use
> > synchronize_rcu() directly instead of this_rq_lock_irq() - just to avoid
> > contention for rq-lock in heavy system?

Thanks to Kyunwoo to show this potential use-after-free problem.

While synchronize_rcu() avoids rq lock, it could introduce a long delay
of a full grace period that will be undesirable.


I think a better fix is to decouple sc_stat from mm and make it its own structure.
Then we can do proper refcounting and rcu management on sc_stat itself.
We will let task points to sc_stat with proper ref count and change the access
as task->sc_stat directly nstead of via task->mm->sc_stat.

Then we will know sc_stat exists in account_mm_sched() execution
as long as a task points to it and do not have to worry that sc_stat is
released due to the mm life cycle.

In the first 2 patches of the prctl control series for cache aware scheduling
we posted, we introduced the change I described above and should 
fix this issue without additional rq locking or synchronize_rcu() overhead.
 https://lore.kernel.org/lkml/cover.1787955777.git.tim.c.chen@linux.intel.com/

We should probably prioritize to merge the first couple of patches from
that series now in light of this issue.  It will make maintenance and future
modification of the cache aware scheduling code easier.

Tim


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH] sched/cache: Fix use-after-free of the mm replaced by exec
  2026-08-31 20:25     ` Tim Chen
@ 2026-08-31 21:44       ` Hyunwoo Kim
  2026-08-31 22:10         ` Tim Chen
  0 siblings, 1 reply; 9+ messages in thread
From: Hyunwoo Kim @ 2026-08-31 21:44 UTC (permalink / raw)
  To: Tim Chen
  Cc: Chen, Yu C, Kees Cook, Christian Brauner, Alexander Viro,
	Jan Kara, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
	Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider,
	K Prateek Nayak, Shrikanth Hegde, Qais Yousef, Aaron Lu,
	Srikar Dronamraju, Vineeth Remanan Pillai, linux-kernel, linux-mm,
	linux-fsdevel, Ingo Molnar, Peter Zijlstra, chen.yu@linux.dev,
	imv4bel

On Mon, Aug 31, 2026 at 01:25:06PM -0700, Tim Chen wrote:
> On Tue, 2026-09-01 at 04:28 +0900, Hyunwoo Kim wrote:
> > On Mon, Aug 31, 2026 at 12:22:45PM +0800, Chen, Yu C wrote:
> > > Hi Hyunwoo,
> > > 
> > > On 8/30/2026 3:30 PM, Hyunwoo Kim wrote:
> > > > When the waker cannot use the wakelist, ttwu_queue() takes the target rq
> > > > lock and goes down into update_curr(). If the target rq belongs to another
> > > > CPU, the task handed to account_mm_sched() is the one running on that CPU,
> > > > not the task being woken.
> > > > 
> > > > account_mm_sched() reads p->mm and updates mm->sc_stat. Nothing keeps that
> > > > mm alive. The rq lock and rq->cpu_epoch_lock it holds have nothing to do
> > > > with the lifetime of the mm.
> > > > 
> > > > If that task happens to be in execve(), exec_mmap() points tsk->mm and
> > > > tsk->active_mm at the new mm, and exec_mm_put_old() from setup_new_exec()
> > > > drops the old one. free_bprm() does the same when exec fails. On the way
> > > > from mmput() down to __mmdrop(), mm_destroy_sched() calls free_percpu() on
> > > > sc_stat.pcpu_sched and free_mm() returns the mm_struct.
> > > > 
> > > > Whoever already read the old pointer keeps using it. It adds to runtime in
> > > > the freed per-cpu area and reads sc_stat in the freed mm_struct. Depending
> > > > on the condition it also writes sc_stat.cpu. That is a use-after-free.
> > > > 
> > > > Commit 9f23469401b0 ("sched/cache: Fix potential NULL mm pointer access")
> > > > changed the remaining p->mm dereference to the local variable, and said the
> > > > active_mm reference keeps the structure allocated. That holds for the other
> > > > paths that detach an mm, since they take an mmgrab_lazy_tlb() reference.
> > > > exec reassigns active_mm to the new mm as well, so that reference is gone.
> > > > What is left is the mm_users reference in bprm->old_mm, and dropping it is
> > > > the free.
> > > > 
> > > >               CPU0                                CPU1
> > > > 
> > > >                                        write(pipe)
> > > >                                        try_to_wake_up()
> > > >                                          ttwu_queue()      // takes rq0 lock
> > > >                                            enqueue_task_fair()
> > > >                                              update_curr()
> > > >                                                update_se()
> > > >                                                  account_mm_sched()
> > > >                                                    mm = rq0->curr->mm
> > > >                                                          // old mm
> > > >    execve()
> > > >      exec_mmap()                       // tsk->mm = new mm
> > > >      setup_new_exec()
> > > >        exec_mm_put_old()
> > > >          mmput() -> ... -> __mmdrop()
> > > >            mm_destroy_sched()          // free_percpu()
> > > >            free_mm()
> > > >                                                    read mm->sc_stat.epoch
> > > >                                                          // use-after-free
> > > > 
> > > 
> > > Ah, thanks for catching this.
> > > 
> > > > ---
> > > >   fs/exec.c             |  1 +
> > > >   include/linux/sched.h |  4 ++++
> > > >   kernel/events/core.c  |  2 ++
> > > >   kernel/sched/fair.c   | 16 ++++++++++++++++
> > > >   4 files changed, 23 insertions(+)
> > > > 
> > > > diff --git a/fs/exec.c b/fs/exec.c
> > > > index 745f6eb5279e6..6194c38807980 100644
> > > > --- a/fs/exec.c
> > > > +++ b/fs/exec.c
> > > > @@ -916,6 +916,7 @@ static void exec_mm_put_old(struct mm_struct *old_mm)
> > > >   {
> > > >   	setmax_mm_hiwater_rss(&current->signal->maxrss, old_mm);
> > > >   	mm_update_next_owner(old_mm);
> > > > +	sched_cache_exec_done();
> > > >   	mmput(old_mm);
> > > >   }
> > > > diff --git a/include/linux/sched.h b/include/linux/sched.h
> > > > index 8b3d47a325cca..6ae31bffe049e 100644
> > > > --- a/include/linux/sched.h
> > > > +++ b/include/linux/sched.h
> > > > @@ -2415,10 +2415,14 @@ struct sched_cache_stat {
> > > >   	int cpu;
> > > >   } ____cacheline_aligned_in_smp;
> > > > +void sched_cache_exec_done(void);
> > > > +
> > > >   #else
> > > >   struct sched_cache_stat { };
> > > > +static inline void sched_cache_exec_done(void) { }
> > > > +
> > > >   #endif
> > > >   #ifndef MODULE
> > > > diff --git a/kernel/events/core.c b/kernel/events/core.c
> > > > index a6c8e38a31104..2f29cbccf03f1 100644
> > > > --- a/kernel/events/core.c
> > > > +++ b/kernel/events/core.c
> > > > @@ -5427,6 +5427,8 @@ attach_task_ctx_data(struct task_struct *task, struct kmem_cache *ctx_cache,
> > > >   	if (!cd)
> > > >   		return -ENOMEM;
> > > > +	/* @old, loaded by the try_cmpxchg() below, is only stable under RCU. */
> > > > +	guard(rcu)();
> > > 
> > > Is this change related to this UAF issue?
> > 
> > Duh.. that one is unrelated. My mistake.
> > 
> > > 
> > > > +/* exec() has switched to the new mm and is about to drop the old one. */
> > > > +void sched_cache_exec_done(void)
> > > > +{
> > > > +	struct rq_flags rf;
> > > > +	struct rq *rq;
> > > > +
> > > > +	/*
> > > > +	 * account_mm_sched() dereferences rq->curr->mm under this rq's lock,
> > > > +	 * so a remote CPU can still be using the old mm. The lock cycle waits
> > > > +	 * for it, and the store to tsk->mm cannot be reordered past the
> > > > +	 * release, so later acquirers see the new mm.
> > > > +	 */
> > > > +	rq = this_rq_lock_irq(&rf);
> > > 
> > > A smart fix, learnt! It behaves like a synchronize_rcu() to protect against
> > > the read in account_mm_sched(). Small open: since the context of invoking
> > > account_mm_sched() is preemption-disabled, I wonder if we can simply use
> > > synchronize_rcu() directly instead of this_rq_lock_irq() - just to avoid
> > > contention for rq-lock in heavy system?
> 
> Thanks to Kyunwoo to show this potential use-after-free problem.

It is a real, triggerable issue.

> 
> While synchronize_rcu() avoids rq lock, it could introduce a long delay
> of a full grace period that will be undesirable.
> 
> 
> I think a better fix is to decouple sc_stat from mm and make it its own structure.
> Then we can do proper refcounting and rcu management on sc_stat itself.
> We will let task points to sc_stat with proper ref count and change the access
> as task->sc_stat directly nstead of via task->mm->sc_stat.
> 
> Then we will know sc_stat exists in account_mm_sched() execution
> as long as a task points to it and do not have to worry that sc_stat is
> released due to the mm life cycle.
> 
> In the first 2 patches of the prctl control series for cache aware scheduling
> we posted, we introduced the change I described above and should 
> fix this issue without additional rq locking or synchronize_rcu() overhead.
>  https://lore.kernel.org/lkml/cover.1787955777.git.tim.c.chen@linux.intel.com/
> 
> We should probably prioritize to merge the first couple of patches from
> that series now in light of this issue.  It will make maintenance and future
> modification of the cache aware scheduling code easier.

That is a fairly large change. 

If you add a Reported-by: tag for this UAF to the patch, I am fine with 
handling it that way.


Best regards,
Hyunwoo Kim


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH] sched/cache: Fix use-after-free of the mm replaced by exec
  2026-08-31 21:44       ` Hyunwoo Kim
@ 2026-08-31 22:10         ` Tim Chen
  2026-09-01  3:00           ` Chen Yu
  2026-09-01 10:44           ` Hyunwoo Kim
  0 siblings, 2 replies; 9+ messages in thread
From: Tim Chen @ 2026-08-31 22:10 UTC (permalink / raw)
  To: Hyunwoo Kim
  Cc: Chen, Yu C, Kees Cook, Christian Brauner, Alexander Viro,
	Jan Kara, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
	Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider,
	K Prateek Nayak, Shrikanth Hegde, Qais Yousef, Aaron Lu,
	Srikar Dronamraju, Vineeth Remanan Pillai, linux-kernel, linux-mm,
	linux-fsdevel, Ingo Molnar, Peter Zijlstra, chen.yu@linux.dev

On Tue, 2026-09-01 at 06:44 +0900, Hyunwoo Kim wrote:
> On Mon, Aug 31, 2026 at 01:25:06PM -0700, Tim Chen wrote:
> > On Tue, 2026-09-01 at 04:28 +0900, Hyunwoo Kim wrote:
> > > On Mon, Aug 31, 2026 at 12:22:45PM +0800, Chen, Yu C wrote:
> > > > Hi Hyunwoo,
> > > > 
> > > > On 8/30/2026 3:30 PM, Hyunwoo Kim wrote:
> > > > > When the waker cannot use the wakelist, ttwu_queue() takes the target rq
> > > > > lock and goes down into update_curr(). If the target rq belongs to another
> > > > > CPU, the task handed to account_mm_sched() is the one running on that CPU,
> > > > > not the task being woken.
> > > > > 
> > > > > account_mm_sched() reads p->mm and updates mm->sc_stat. Nothing keeps that
> > > > > mm alive. The rq lock and rq->cpu_epoch_lock it holds have nothing to do
> > > > > with the lifetime of the mm.
> > > > > 
> > > > > If that task happens to be in execve(), exec_mmap() points tsk->mm and
> > > > > tsk->active_mm at the new mm, and exec_mm_put_old() from setup_new_exec()
> > > > > drops the old one. free_bprm() does the same when exec fails. On the way
> > > > > from mmput() down to __mmdrop(), mm_destroy_sched() calls free_percpu() on
> > > > > sc_stat.pcpu_sched and free_mm() returns the mm_struct.
> > > > > 
> > > > > Whoever already read the old pointer keeps using it. It adds to runtime in
> > > > > the freed per-cpu area and reads sc_stat in the freed mm_struct. Depending
> > > > > on the condition it also writes sc_stat.cpu. That is a use-after-free.
> > > > > 
> > > > > Commit 9f23469401b0 ("sched/cache: Fix potential NULL mm pointer access")
> > > > > changed the remaining p->mm dereference to the local variable, and said the
> > > > > active_mm reference keeps the structure allocated. That holds for the other
> > > > > paths that detach an mm, since they take an mmgrab_lazy_tlb() reference.
> > > > > exec reassigns active_mm to the new mm as well, so that reference is gone.
> > > > > What is left is the mm_users reference in bprm->old_mm, and dropping it is
> > > > > the free.
> > > > > 
> > > > >               CPU0                                CPU1
> > > > > 
> > > > >                                        write(pipe)
> > > > >                                        try_to_wake_up()
> > > > >                                          ttwu_queue()      // takes rq0 lock
> > > > >                                            enqueue_task_fair()
> > > > >                                              update_curr()
> > > > >                                                update_se()
> > > > >                                                  account_mm_sched()
> > > > >                                                    mm = rq0->curr->mm
> > > > >                                                          // old mm
> > > > >    execve()
> > > > >      exec_mmap()                       // tsk->mm = new mm
> > > > >      setup_new_exec()
> > > > >        exec_mm_put_old()
> > > > >          mmput() -> ... -> __mmdrop()
> > > > >            mm_destroy_sched()          // free_percpu()
> > > > >            free_mm()
> > > > >                                                    read mm->sc_stat.epoch
> > > > >                                                          // use-after-free
> > > > > 
> > > > 
> > > > Ah, thanks for catching this.
> > > > 
> > > > > ---
> > > > >   fs/exec.c             |  1 +
> > > > >   include/linux/sched.h |  4 ++++
> > > > >   kernel/events/core.c  |  2 ++
> > > > >   kernel/sched/fair.c   | 16 ++++++++++++++++
> > > > >   4 files changed, 23 insertions(+)
> > > > > 
> > > > > diff --git a/fs/exec.c b/fs/exec.c
> > > > > index 745f6eb5279e6..6194c38807980 100644
> > > > > --- a/fs/exec.c
> > > > > +++ b/fs/exec.c
> > > > > @@ -916,6 +916,7 @@ static void exec_mm_put_old(struct mm_struct *old_mm)
> > > > >   {
> > > > >   	setmax_mm_hiwater_rss(&current->signal->maxrss, old_mm);
> > > > >   	mm_update_next_owner(old_mm);
> > > > > +	sched_cache_exec_done();
> > > > >   	mmput(old_mm);
> > > > >   }
> > > > > diff --git a/include/linux/sched.h b/include/linux/sched.h
> > > > > index 8b3d47a325cca..6ae31bffe049e 100644
> > > > > --- a/include/linux/sched.h
> > > > > +++ b/include/linux/sched.h
> > > > > @@ -2415,10 +2415,14 @@ struct sched_cache_stat {
> > > > >   	int cpu;
> > > > >   } ____cacheline_aligned_in_smp;
> > > > > +void sched_cache_exec_done(void);
> > > > > +
> > > > >   #else
> > > > >   struct sched_cache_stat { };
> > > > > +static inline void sched_cache_exec_done(void) { }
> > > > > +
> > > > >   #endif
> > > > >   #ifndef MODULE
> > > > > diff --git a/kernel/events/core.c b/kernel/events/core.c
> > > > > index a6c8e38a31104..2f29cbccf03f1 100644
> > > > > --- a/kernel/events/core.c
> > > > > +++ b/kernel/events/core.c
> > > > > @@ -5427,6 +5427,8 @@ attach_task_ctx_data(struct task_struct *task, struct kmem_cache *ctx_cache,
> > > > >   	if (!cd)
> > > > >   		return -ENOMEM;
> > > > > +	/* @old, loaded by the try_cmpxchg() below, is only stable under RCU. */
> > > > > +	guard(rcu)();
> > > > 
> > > > Is this change related to this UAF issue?
> > > 
> > > Duh.. that one is unrelated. My mistake.
> > > 
> > > > 
> > > > > +/* exec() has switched to the new mm and is about to drop the old one. */
> > > > > +void sched_cache_exec_done(void)
> > > > > +{
> > > > > +	struct rq_flags rf;
> > > > > +	struct rq *rq;
> > > > > +
> > > > > +	/*
> > > > > +	 * account_mm_sched() dereferences rq->curr->mm under this rq's lock,
> > > > > +	 * so a remote CPU can still be using the old mm. The lock cycle waits
> > > > > +	 * for it, and the store to tsk->mm cannot be reordered past the
> > > > > +	 * release, so later acquirers see the new mm.
> > > > > +	 */
> > > > > +	rq = this_rq_lock_irq(&rf);
> > > > 
> > > > A smart fix, learnt! It behaves like a synchronize_rcu() to protect against
> > > > the read in account_mm_sched(). Small open: since the context of invoking
> > > > account_mm_sched() is preemption-disabled, I wonder if we can simply use
> > > > synchronize_rcu() directly instead of this_rq_lock_irq() - just to avoid
> > > > contention for rq-lock in heavy system?
> > 
> > Thanks to Kyunwoo to show this potential use-after-free problem.
> 
> It is a real, triggerable issue.
> 

Appreciate if you can check whether the first 2 patches in 
https://lore.kernel.org/lkml/cover.1787955777.git.tim.c.chen@linux.intel.com/
fixes this issue.

> > 
> > While synchronize_rcu() avoids rq lock, it could introduce a long delay
> > of a full grace period that will be undesirable.
> > 
> > 
> > I think a better fix is to decouple sc_stat from mm and make it its own structure.
> > Then we can do proper refcounting and rcu management on sc_stat itself.
> > We will let task points to sc_stat with proper ref count and change the access
> > as task->sc_stat directly nstead of via task->mm->sc_stat.
> > 
> > Then we will know sc_stat exists in account_mm_sched() execution
> > as long as a task points to it and do not have to worry that sc_stat is
> > released due to the mm life cycle.
> > 
> > In the first 2 patches of the prctl control series for cache aware scheduling
> > we posted, we introduced the change I described above and should 
> > fix this issue without additional rq locking or synchronize_rcu() overhead.
> >  https://lore.kernel.org/lkml/cover.1787955777.git.tim.c.chen@linux.intel.com/
> > 
> > We should probably prioritize to merge the first couple of patches from
> > that series now in light of this issue.  It will make maintenance and future
> > modification of the cache aware scheduling code easier.
> 
> That is a fairly large change. 

However adding rcu synchronization will slow down the launch of a new process
too much.  The rq lock solution has less latency in my opinion.  But I
think decoupling sc_stat is the best solution without adding latency in the
exec path.

Wonder what is Peter's opinion?

> 
> If you add a Reported-by: tag for this UAF to the patch, I am fine with 
> handling it that way.

I'll be happy to do that. If you let us know if those two patches from prctl series fixes
the bug you found in KASAN log, I can also add a Tested-by tag in addition to Reported-by.

Thanks.

Tim

> 
> 
> Best regards,
> Hyunwoo Kim


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH] sched/cache: Fix use-after-free of the mm replaced by exec
  2026-08-31 22:10         ` Tim Chen
@ 2026-09-01  3:00           ` Chen Yu
  2026-09-01 10:44           ` Hyunwoo Kim
  1 sibling, 0 replies; 9+ messages in thread
From: Chen Yu @ 2026-09-01  3:00 UTC (permalink / raw)
  To: Tim Chen
  Cc: Hyunwoo Kim, Chen, Yu C, Kees Cook, Christian Brauner,
	Alexander Viro, Jan Kara, Juri Lelli, Vincent Guittot,
	Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
	Valentin Schneider, K Prateek Nayak, Shrikanth Hegde, Qais Yousef,
	Aaron Lu, Srikar Dronamraju, Vineeth Remanan Pillai, linux-kernel,
	linux-mm, linux-fsdevel, Ingo Molnar, Peter Zijlstra

On Mon, Aug 31, 2026 at 03:10:40PM -0700, Tim Chen wrote:


[ ... ]

> > > We should probably prioritize to merge the first couple of patches from
> > > that series now in light of this issue.  It will make maintenance and future
> > > modification of the cache aware scheduling code easier.
> > 
> > That is a fairly large change. 
> 
> However adding rcu synchronization will slow down the launch of a new process
> too much.  The rq lock solution has less latency in my opinion.  But I
> think decoupling sc_stat is the best solution without adding latency in the
> exec path.
>

OK, I see. In decoupling patches 1 and 2, the lifetime of the sched_group is managed via
a reference count, and it is freed using the non-blocking call_rcu() instead of
synchronize_rcu().

thanks,
Chenyu


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH] sched/cache: Fix use-after-free of the mm replaced by exec
  2026-08-31 22:10         ` Tim Chen
  2026-09-01  3:00           ` Chen Yu
@ 2026-09-01 10:44           ` Hyunwoo Kim
  2026-09-01 20:49             ` Tim Chen
  1 sibling, 1 reply; 9+ messages in thread
From: Hyunwoo Kim @ 2026-09-01 10:44 UTC (permalink / raw)
  To: Tim Chen
  Cc: Chen, Yu C, Kees Cook, Christian Brauner, Alexander Viro,
	Jan Kara, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
	Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider,
	K Prateek Nayak, Shrikanth Hegde, Qais Yousef, Aaron Lu,
	Srikar Dronamraju, Vineeth Remanan Pillai, linux-kernel, linux-mm,
	linux-fsdevel, Ingo Molnar, Peter Zijlstra, chen.yu@linux.dev,
	imv4bel

On Mon, Aug 31, 2026 at 03:10:40PM -0700, Tim Chen wrote:
> On Tue, 2026-09-01 at 06:44 +0900, Hyunwoo Kim wrote:
> > On Mon, Aug 31, 2026 at 01:25:06PM -0700, Tim Chen wrote:
> > > On Tue, 2026-09-01 at 04:28 +0900, Hyunwoo Kim wrote:
> > > > On Mon, Aug 31, 2026 at 12:22:45PM +0800, Chen, Yu C wrote:
> > > > > Hi Hyunwoo,
> > > > > 
> > > > > On 8/30/2026 3:30 PM, Hyunwoo Kim wrote:
> > > > > > When the waker cannot use the wakelist, ttwu_queue() takes the target rq
> > > > > > lock and goes down into update_curr(). If the target rq belongs to another
> > > > > > CPU, the task handed to account_mm_sched() is the one running on that CPU,
> > > > > > not the task being woken.
> > > > > > 
> > > > > > account_mm_sched() reads p->mm and updates mm->sc_stat. Nothing keeps that
> > > > > > mm alive. The rq lock and rq->cpu_epoch_lock it holds have nothing to do
> > > > > > with the lifetime of the mm.
> > > > > > 
> > > > > > If that task happens to be in execve(), exec_mmap() points tsk->mm and
> > > > > > tsk->active_mm at the new mm, and exec_mm_put_old() from setup_new_exec()
> > > > > > drops the old one. free_bprm() does the same when exec fails. On the way
> > > > > > from mmput() down to __mmdrop(), mm_destroy_sched() calls free_percpu() on
> > > > > > sc_stat.pcpu_sched and free_mm() returns the mm_struct.
> > > > > > 
> > > > > > Whoever already read the old pointer keeps using it. It adds to runtime in
> > > > > > the freed per-cpu area and reads sc_stat in the freed mm_struct. Depending
> > > > > > on the condition it also writes sc_stat.cpu. That is a use-after-free.
> > > > > > 
> > > > > > Commit 9f23469401b0 ("sched/cache: Fix potential NULL mm pointer access")
> > > > > > changed the remaining p->mm dereference to the local variable, and said the
> > > > > > active_mm reference keeps the structure allocated. That holds for the other
> > > > > > paths that detach an mm, since they take an mmgrab_lazy_tlb() reference.
> > > > > > exec reassigns active_mm to the new mm as well, so that reference is gone.
> > > > > > What is left is the mm_users reference in bprm->old_mm, and dropping it is
> > > > > > the free.
> > > > > > 
> > > > > >               CPU0                                CPU1
> > > > > > 
> > > > > >                                        write(pipe)
> > > > > >                                        try_to_wake_up()
> > > > > >                                          ttwu_queue()      // takes rq0 lock
> > > > > >                                            enqueue_task_fair()
> > > > > >                                              update_curr()
> > > > > >                                                update_se()
> > > > > >                                                  account_mm_sched()
> > > > > >                                                    mm = rq0->curr->mm
> > > > > >                                                          // old mm
> > > > > >    execve()
> > > > > >      exec_mmap()                       // tsk->mm = new mm
> > > > > >      setup_new_exec()
> > > > > >        exec_mm_put_old()
> > > > > >          mmput() -> ... -> __mmdrop()
> > > > > >            mm_destroy_sched()          // free_percpu()
> > > > > >            free_mm()
> > > > > >                                                    read mm->sc_stat.epoch
> > > > > >                                                          // use-after-free
> > > > > > 
> > > > > 
> > > > > Ah, thanks for catching this.
> > > > > 
> > > > > > ---
> > > > > >   fs/exec.c             |  1 +
> > > > > >   include/linux/sched.h |  4 ++++
> > > > > >   kernel/events/core.c  |  2 ++
> > > > > >   kernel/sched/fair.c   | 16 ++++++++++++++++
> > > > > >   4 files changed, 23 insertions(+)
> > > > > > 
> > > > > > diff --git a/fs/exec.c b/fs/exec.c
> > > > > > index 745f6eb5279e6..6194c38807980 100644
> > > > > > --- a/fs/exec.c
> > > > > > +++ b/fs/exec.c
> > > > > > @@ -916,6 +916,7 @@ static void exec_mm_put_old(struct mm_struct *old_mm)
> > > > > >   {
> > > > > >   	setmax_mm_hiwater_rss(&current->signal->maxrss, old_mm);
> > > > > >   	mm_update_next_owner(old_mm);
> > > > > > +	sched_cache_exec_done();
> > > > > >   	mmput(old_mm);
> > > > > >   }
> > > > > > diff --git a/include/linux/sched.h b/include/linux/sched.h
> > > > > > index 8b3d47a325cca..6ae31bffe049e 100644
> > > > > > --- a/include/linux/sched.h
> > > > > > +++ b/include/linux/sched.h
> > > > > > @@ -2415,10 +2415,14 @@ struct sched_cache_stat {
> > > > > >   	int cpu;
> > > > > >   } ____cacheline_aligned_in_smp;
> > > > > > +void sched_cache_exec_done(void);
> > > > > > +
> > > > > >   #else
> > > > > >   struct sched_cache_stat { };
> > > > > > +static inline void sched_cache_exec_done(void) { }
> > > > > > +
> > > > > >   #endif
> > > > > >   #ifndef MODULE
> > > > > > diff --git a/kernel/events/core.c b/kernel/events/core.c
> > > > > > index a6c8e38a31104..2f29cbccf03f1 100644
> > > > > > --- a/kernel/events/core.c
> > > > > > +++ b/kernel/events/core.c
> > > > > > @@ -5427,6 +5427,8 @@ attach_task_ctx_data(struct task_struct *task, struct kmem_cache *ctx_cache,
> > > > > >   	if (!cd)
> > > > > >   		return -ENOMEM;
> > > > > > +	/* @old, loaded by the try_cmpxchg() below, is only stable under RCU. */
> > > > > > +	guard(rcu)();
> > > > > 
> > > > > Is this change related to this UAF issue?
> > > > 
> > > > Duh.. that one is unrelated. My mistake.
> > > > 
> > > > > 
> > > > > > +/* exec() has switched to the new mm and is about to drop the old one. */
> > > > > > +void sched_cache_exec_done(void)
> > > > > > +{
> > > > > > +	struct rq_flags rf;
> > > > > > +	struct rq *rq;
> > > > > > +
> > > > > > +	/*
> > > > > > +	 * account_mm_sched() dereferences rq->curr->mm under this rq's lock,
> > > > > > +	 * so a remote CPU can still be using the old mm. The lock cycle waits
> > > > > > +	 * for it, and the store to tsk->mm cannot be reordered past the
> > > > > > +	 * release, so later acquirers see the new mm.
> > > > > > +	 */
> > > > > > +	rq = this_rq_lock_irq(&rf);
> > > > > 
> > > > > A smart fix, learnt! It behaves like a synchronize_rcu() to protect against
> > > > > the read in account_mm_sched(). Small open: since the context of invoking
> > > > > account_mm_sched() is preemption-disabled, I wonder if we can simply use
> > > > > synchronize_rcu() directly instead of this_rq_lock_irq() - just to avoid
> > > > > contention for rq-lock in heavy system?
> > > 
> > > Thanks to Kyunwoo to show this potential use-after-free problem.
> > 
> > It is a real, triggerable issue.
> > 
> 
> Appreciate if you can check whether the first 2 patches in 
> https://lore.kernel.org/lkml/cover.1787955777.git.tim.c.chen@linux.intel.com/
> fixes this issue.
> 
> > > 
> > > While synchronize_rcu() avoids rq lock, it could introduce a long delay
> > > of a full grace period that will be undesirable.
> > > 
> > > 
> > > I think a better fix is to decouple sc_stat from mm and make it its own structure.
> > > Then we can do proper refcounting and rcu management on sc_stat itself.
> > > We will let task points to sc_stat with proper ref count and change the access
> > > as task->sc_stat directly nstead of via task->mm->sc_stat.
> > > 
> > > Then we will know sc_stat exists in account_mm_sched() execution
> > > as long as a task points to it and do not have to worry that sc_stat is
> > > released due to the mm life cycle.
> > > 
> > > In the first 2 patches of the prctl control series for cache aware scheduling
> > > we posted, we introduced the change I described above and should 
> > > fix this issue without additional rq locking or synchronize_rcu() overhead.
> > >  https://lore.kernel.org/lkml/cover.1787955777.git.tim.c.chen@linux.intel.com/
> > > 
> > > We should probably prioritize to merge the first couple of patches from
> > > that series now in light of this issue.  It will make maintenance and future
> > > modification of the cache aware scheduling code easier.
> > 
> > That is a fairly large change. 
> 
> However adding rcu synchronization will slow down the launch of a new process
> too much.  The rq lock solution has less latency in my opinion.  But I
> think decoupling sc_stat is the best solution without adding latency in the
> exec path.
> 
> Wonder what is Peter's opinion?
> 
> > 
> > If you add a Reported-by: tag for this UAF to the patch, I am fine with 
> > handling it that way.
> 
> I'll be happy to do that. If you let us know if those two patches from prctl series fixes
> the bug you found in KASAN log, I can also add a Tested-by tag in addition to Reported-by.

I confirmed that patches 1 and 2 of your series prevent this UAF.


Best regards,
Hyunwoo Kim


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH] sched/cache: Fix use-after-free of the mm replaced by exec
  2026-09-01 10:44           ` Hyunwoo Kim
@ 2026-09-01 20:49             ` Tim Chen
  0 siblings, 0 replies; 9+ messages in thread
From: Tim Chen @ 2026-09-01 20:49 UTC (permalink / raw)
  To: Hyunwoo Kim
  Cc: Chen, Yu C, Kees Cook, Christian Brauner, Alexander Viro,
	Jan Kara, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
	Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider,
	K Prateek Nayak, Shrikanth Hegde, Qais Yousef, Aaron Lu,
	Srikar Dronamraju, Vineeth Remanan Pillai, linux-kernel, linux-mm,
	linux-fsdevel, Ingo Molnar, Peter Zijlstra, chen.yu@linux.dev

On Tue, 2026-09-01 at 19:44 +0900, Hyunwoo Kim wrote:
> On Mon, Aug 31, 2026 at 03:10:40PM -0700, Tim Chen wrote:
> > On Tue, 2026-09-01 at 06:44 +0900, Hyunwoo Kim wrote:
> > > On Mon, Aug 31, 2026 at 01:25:06PM -0700, Tim Chen wrote:
> > > > On Tue, 2026-09-01 at 04:28 +0900, Hyunwoo Kim wrote:
> > > > > On Mon, Aug 31, 2026 at 12:22:45PM +0800, Chen, Yu C wrote:
> > > > > > Hi Hyunwoo,
> > > > > > 
> > > > > > On 8/30/2026 3:30 PM, Hyunwoo Kim wrote:
> > > > > > > When the waker cannot use the wakelist, ttwu_queue() takes the target rq
> > > > > > > lock and goes down into update_curr(). If the target rq belongs to another
> > > > > > > CPU, the task handed to account_mm_sched() is the one running on that CPU,
> > > > > > > not the task being woken.
> > > > > > > 
> > > > > > > account_mm_sched() reads p->mm and updates mm->sc_stat. Nothing keeps that
> > > > > > > mm alive. The rq lock and rq->cpu_epoch_lock it holds have nothing to do
> > > > > > > with the lifetime of the mm.
> > > > > > > 
> > > > > > > If that task happens to be in execve(), exec_mmap() points tsk->mm and
> > > > > > > tsk->active_mm at the new mm, and exec_mm_put_old() from setup_new_exec()
> > > > > > > drops the old one. free_bprm() does the same when exec fails. On the way
> > > > > > > from mmput() down to __mmdrop(), mm_destroy_sched() calls free_percpu() on
> > > > > > > sc_stat.pcpu_sched and free_mm() returns the mm_struct.
> > > > > > > 
> > > > > > > Whoever already read the old pointer keeps using it. It adds to runtime in
> > > > > > > the freed per-cpu area and reads sc_stat in the freed mm_struct. Depending
> > > > > > > on the condition it also writes sc_stat.cpu. That is a use-after-free.
> > > > > > > 
> > > > > > > Commit 9f23469401b0 ("sched/cache: Fix potential NULL mm pointer access")
> > > > > > > changed the remaining p->mm dereference to the local variable, and said the
> > > > > > > active_mm reference keeps the structure allocated. That holds for the other
> > > > > > > paths that detach an mm, since they take an mmgrab_lazy_tlb() reference.
> > > > > > > exec reassigns active_mm to the new mm as well, so that reference is gone.
> > > > > > > What is left is the mm_users reference in bprm->old_mm, and dropping it is
> > > > > > > the free.
> > > > > > > 
> > > > > > >               CPU0                                CPU1
> > > > > > > 
> > > > > > >                                        write(pipe)
> > > > > > >                                        try_to_wake_up()
> > > > > > >                                          ttwu_queue()      // takes rq0 lock
> > > > > > >                                            enqueue_task_fair()
> > > > > > >                                              update_curr()
> > > > > > >                                                update_se()
> > > > > > >                                                  account_mm_sched()
> > > > > > >                                                    mm = rq0->curr->mm
> > > > > > >                                                          // old mm
> > > > > > >    execve()
> > > > > > >      exec_mmap()                       // tsk->mm = new mm
> > > > > > >      setup_new_exec()
> > > > > > >        exec_mm_put_old()
> > > > > > >          mmput() -> ... -> __mmdrop()
> > > > > > >            mm_destroy_sched()          // free_percpu()
> > > > > > >            free_mm()
> > > > > > >                                                    read mm->sc_stat.epoch
> > > > > > >                                                          // use-after-free
> > > > > > > 
> > > > > > 
> > > > > > Ah, thanks for catching this.
> > > > > > 
> > > > > > > ---
> > > > > > >   fs/exec.c             |  1 +
> > > > > > >   include/linux/sched.h |  4 ++++
> > > > > > >   kernel/events/core.c  |  2 ++
> > > > > > >   kernel/sched/fair.c   | 16 ++++++++++++++++
> > > > > > >   4 files changed, 23 insertions(+)
> > > > > > > 
> > > > > > > diff --git a/fs/exec.c b/fs/exec.c
> > > > > > > index 745f6eb5279e6..6194c38807980 100644
> > > > > > > --- a/fs/exec.c
> > > > > > > +++ b/fs/exec.c
> > > > > > > @@ -916,6 +916,7 @@ static void exec_mm_put_old(struct mm_struct *old_mm)
> > > > > > >   {
> > > > > > >   	setmax_mm_hiwater_rss(&current->signal->maxrss, old_mm);
> > > > > > >   	mm_update_next_owner(old_mm);
> > > > > > > +	sched_cache_exec_done();
> > > > > > >   	mmput(old_mm);
> > > > > > >   }
> > > > > > > diff --git a/include/linux/sched.h b/include/linux/sched.h
> > > > > > > index 8b3d47a325cca..6ae31bffe049e 100644
> > > > > > > --- a/include/linux/sched.h
> > > > > > > +++ b/include/linux/sched.h
> > > > > > > @@ -2415,10 +2415,14 @@ struct sched_cache_stat {
> > > > > > >   	int cpu;
> > > > > > >   } ____cacheline_aligned_in_smp;
> > > > > > > +void sched_cache_exec_done(void);
> > > > > > > +
> > > > > > >   #else
> > > > > > >   struct sched_cache_stat { };
> > > > > > > +static inline void sched_cache_exec_done(void) { }
> > > > > > > +
> > > > > > >   #endif
> > > > > > >   #ifndef MODULE
> > > > > > > diff --git a/kernel/events/core.c b/kernel/events/core.c
> > > > > > > index a6c8e38a31104..2f29cbccf03f1 100644
> > > > > > > --- a/kernel/events/core.c
> > > > > > > +++ b/kernel/events/core.c
> > > > > > > @@ -5427,6 +5427,8 @@ attach_task_ctx_data(struct task_struct *task, struct kmem_cache *ctx_cache,
> > > > > > >   	if (!cd)
> > > > > > >   		return -ENOMEM;
> > > > > > > +	/* @old, loaded by the try_cmpxchg() below, is only stable under RCU. */
> > > > > > > +	guard(rcu)();
> > > > > > 
> > > > > > Is this change related to this UAF issue?
> > > > > 
> > > > > Duh.. that one is unrelated. My mistake.
> > > > > 
> > > > > > 
> > > > > > > +/* exec() has switched to the new mm and is about to drop the old one. */
> > > > > > > +void sched_cache_exec_done(void)
> > > > > > > +{
> > > > > > > +	struct rq_flags rf;
> > > > > > > +	struct rq *rq;
> > > > > > > +
> > > > > > > +	/*
> > > > > > > +	 * account_mm_sched() dereferences rq->curr->mm under this rq's lock,
> > > > > > > +	 * so a remote CPU can still be using the old mm. The lock cycle waits
> > > > > > > +	 * for it, and the store to tsk->mm cannot be reordered past the
> > > > > > > +	 * release, so later acquirers see the new mm.
> > > > > > > +	 */
> > > > > > > +	rq = this_rq_lock_irq(&rf);
> > > > > > 
> > > > > > A smart fix, learnt! It behaves like a synchronize_rcu() to protect against
> > > > > > the read in account_mm_sched(). Small open: since the context of invoking
> > > > > > account_mm_sched() is preemption-disabled, I wonder if we can simply use
> > > > > > synchronize_rcu() directly instead of this_rq_lock_irq() - just to avoid
> > > > > > contention for rq-lock in heavy system?
> > > > 
> > > > Thanks to Kyunwoo to show this potential use-after-free problem.
> > > 
> > > It is a real, triggerable issue.
> > > 
> > 
> > Appreciate if you can check whether the first 2 patches in 
> > https://lore.kernel.org/lkml/cover.1787955777.git.tim.c.chen@linux.intel.com/
> > fixes this issue.
> > 
> > > > 
> > > > While synchronize_rcu() avoids rq lock, it could introduce a long delay
> > > > of a full grace period that will be undesirable.
> > > > 
> > > > 
> > > > I think a better fix is to decouple sc_stat from mm and make it its own structure.
> > > > Then we can do proper refcounting and rcu management on sc_stat itself.
> > > > We will let task points to sc_stat with proper ref count and change the access
> > > > as task->sc_stat directly nstead of via task->mm->sc_stat.
> > > > 
> > > > Then we will know sc_stat exists in account_mm_sched() execution
> > > > as long as a task points to it and do not have to worry that sc_stat is
> > > > released due to the mm life cycle.
> > > > 
> > > > In the first 2 patches of the prctl control series for cache aware scheduling
> > > > we posted, we introduced the change I described above and should 
> > > > fix this issue without additional rq locking or synchronize_rcu() overhead.
> > > >  https://lore.kernel.org/lkml/cover.1787955777.git.tim.c.chen@linux.intel.com/
> > > > 
> > > > We should probably prioritize to merge the first couple of patches from
> > > > that series now in light of this issue.  It will make maintenance and future
> > > > modification of the cache aware scheduling code easier.
> > > 
> > > That is a fairly large change. 
> > 
> > However adding rcu synchronization will slow down the launch of a new process
> > too much.  The rq lock solution has less latency in my opinion.  But I
> > think decoupling sc_stat is the best solution without adding latency in the
> > exec path.
> > 
> > Wonder what is Peter's opinion?
> > 
> > > 
> > > If you add a Reported-by: tag for this UAF to the patch, I am fine with 
> > > handling it that way.
> > 
> > I'll be happy to do that. If you let us know if those two patches from prctl series fixes
> > the bug you found in KASAN log, I can also add a Tested-by tag in addition to Reported-by.
> 
> I confirmed that patches 1 and 2 of your series prevent this UAF.
> 

Thanks for confirming that patches 1 and 2 in prctl series fix this use after free issue.
I will post those two patches separately and update the commit log
with reference to this UAF problem. We will try to expedite getting those two patches merged.

Tim

> 
> Best regards,
> Hyunwoo Kim


^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2026-09-01 20:49 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-30  7:30 [PATCH] sched/cache: Fix use-after-free of the mm replaced by exec Hyunwoo Kim
2026-08-31  4:22 ` Chen, Yu C
2026-08-31 19:28   ` Hyunwoo Kim
2026-08-31 20:25     ` Tim Chen
2026-08-31 21:44       ` Hyunwoo Kim
2026-08-31 22:10         ` Tim Chen
2026-09-01  3:00           ` Chen Yu
2026-09-01 10:44           ` Hyunwoo Kim
2026-09-01 20:49             ` Tim Chen

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox