sched/cache: Fix use-after-free of the mm replaced by exec
Subsystems (5)
PATCH 1/1 sched/cache: Fix use-after-free of the mm replaced by exec
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-freeKASAN 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.
--- fs/exec.c | 1 + include/linux/sched.h | 4 ++++ kernel/events/core.c | 2 ++ kernel/sched/fair.c | 16 ++++++++++++++++ 4 files changed, 23 insertions(+)
fs/exec.c +1 -0
@@ -916,6 +916,7 @@ static void exec_mm_put_old(struct mm_struct *old_mm) {setmax_mm_hiwater_rss (¤t->signal->maxrss, old_mm);mm_update_next_owner (old_mm); +sched_cache_exec_done ();mmput (old_mm); }
include/linux/sched.h +4 -0
@@ -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
kernel/events/core.c +2 -0
@@ -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)
kernel/sched/fair.c +16 -0
@@ -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,
Signature · 1 line
2.43.0
Hi Hyunwoo,
Quoted 39 lines · Hyunwoo Kim
When the waker cannot use the wakelist,
ttwu_queue()takes the target rq lock and goes down intoupdate_curr(). If the target rq belongs to another CPU, the task handed toaccount_mm_sched()is the one running on that CPU, not the task being woken.
account_mm_sched()readsp->mmand updatesmm->sc_stat. Nothing keeps that mm alive. The rq lock andrq->cpu_epoch_lockit holds have nothing to do with the lifetime of the mm.If that task happens to be in
execve(),exec_mmap()pointstsk->mmandtsk->active_mmat the new mm, andexec_mm_put_old()fromsetup_new_exec()drops the old one.free_bprm()does the same when exec fails. On the way frommmput()down to__mmdrop(),mm_destroy_sched()callsfree_percpu()on sc_stat.pcpu_sched andfree_mm()returns themm_struct.Whoever already read the old pointer keeps using it. It adds to runtime in the freed per-cpu area and reads
sc_statin the freedmm_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 remainingp->mmdereference to the local variable, and said theactive_mmreference keeps the structure allocated. That holds for the other paths that detach an mm, since they take anmmgrab_lazy_tlb()reference. exec reassignsactive_mmto the new mm as well, so that reference is gone. What is left is themm_usersreference inbprm->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()
Quoted 9 lines
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-freeAh, thanks for catching this.
Quoted 43 lines
--- fs/exec.c | 1 + include/linux/sched.h | 4 ++++ kernel/events/core.c | 2 ++ kernel/sched/fair.c | 16 ++++++++++++++++ 4 files changed, 23 insertions(+)fs/exec.c +1
-0@@ -916,6 +916,7 @@ static void exec_mm_put_old(struct mm_struct *old_mm) {setmax_mm_hiwater_rss (¤t->signal->maxrss, old_mm);mm_update_next_owner (old_mm); +sched_cache_exec_done ();mmput (old_mm); }include/linux/sched.h +4
-0@@ -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 kernel/events/core.c +0
-0
kernel/events/core.c attach_task_ctx_data(struct task_struct *task, struct kmem_cache *ctx_cache, · line 5431
Quoted 5 lines
kernel/events/core.c +2
-0@@ -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?
kernel/events/core.c attach_task_ctx_data(struct task_struct *task, struct kmem_cache *ctx_cache,
Quoted 13 lines
kernel/events/core.c +13
-0+/* 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
Quoted 90 lines · Yu C
Hi Hyunwoo,
Quoted 39 lines · Hyunwoo Kim
When the waker cannot use the wakelist,
ttwu_queue()takes the target rq lock and goes down intoupdate_curr(). If the target rq belongs to another CPU, the task handed toaccount_mm_sched()is the one running on that CPU, not the task being woken.
account_mm_sched()readsp->mmand updatesmm->sc_stat. Nothing keeps that mm alive. The rq lock andrq->cpu_epoch_lockit holds have nothing to do with the lifetime of the mm.If that task happens to be in
execve(),exec_mmap()pointstsk->mmandtsk->active_mmat the new mm, andexec_mm_put_old()fromsetup_new_exec()drops the old one.free_bprm()does the same when exec fails. On the way frommmput()down to__mmdrop(),mm_destroy_sched()callsfree_percpu()on sc_stat.pcpu_sched andfree_mm()returns themm_struct.Whoever already read the old pointer keeps using it. It adds to runtime in the freed per-cpu area and reads
sc_statin the freedmm_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 remainingp->mmdereference to the local variable, and said theactive_mmreference keeps the structure allocated. That holds for the other paths that detach an mm, since they take anmmgrab_lazy_tlb()reference. exec reassignsactive_mmto the new mm as well, so that reference is gone. What is left is themm_usersreference inbprm->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()Quoted 9 lines
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-freeAh, thanks for catching this.
Quoted 36 lines
--- fs/exec.c | 1 + include/linux/sched.h | 4 ++++ kernel/events/core.c | 2 ++ kernel/sched/fair.c | 16 ++++++++++++++++ 4 files changed, 23 insertions(+)fs/exec.c +1
-0@@ -916,6 +916,7 @@ static void exec_mm_put_old(struct mm_struct *old_mm) {setmax_mm_hiwater_rss (¤t->signal->maxrss, old_mm);mm_update_next_owner (old_mm); +sched_cache_exec_done ();mmput (old_mm); }include/linux/sched.h +4
-0@@ -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 kernel/events/core.c +0
-1--- a/kernel/events/core.c
Quoted 8 lines
kernel/events/core.c attach_task_ctx_data(struct task_struct *task, struct kmem_cache *ctx_cache, · line 5430
Quoted 6 lines
+++
b/kernel/events/core.ckernel/events/core.c +2
-0@@ -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.
Quoted 12 lines
Quoted 12 lines
kernel/events/core.c +11
-0+/* 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.
Quoted 8 lines
kernel/events/core.c
Quoted 2 lines
kernel/events/core.c +2
-0+*/ + rq =this_rq_lock_irq (&rf);A smart fix, learnt! It behaves like a
synchronize_rcu()to protect against the read inaccount_mm_sched(). Small open: since the context of invokingaccount_mm_sched()is preemption-disabled, I wonder if we can simply usesynchronize_rcu()directly instead ofthis_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
Quoted 100 lines · Hyunwoo Kim
Quoted 90 lines · Yu C
Hi Hyunwoo,
Quoted 39 lines · Hyunwoo Kim
When the waker cannot use the wakelist,
ttwu_queue()takes the target rq lock and goes down intoupdate_curr(). If the target rq belongs to another CPU, the task handed toaccount_mm_sched()is the one running on that CPU, not the task being woken.
account_mm_sched()readsp->mmand updatesmm->sc_stat. Nothing keeps that mm alive. The rq lock andrq->cpu_epoch_lockit holds have nothing to do with the lifetime of the mm.If that task happens to be in
execve(),exec_mmap()pointstsk->mmandtsk->active_mmat the new mm, andexec_mm_put_old()fromsetup_new_exec()drops the old one.free_bprm()does the same when exec fails. On the way frommmput()down to__mmdrop(),mm_destroy_sched()callsfree_percpu()on sc_stat.pcpu_sched andfree_mm()returns themm_struct.Whoever already read the old pointer keeps using it. It adds to runtime in the freed per-cpu area and reads
sc_statin the freedmm_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 remainingp->mmdereference to the local variable, and said theactive_mmreference keeps the structure allocated. That holds for the other paths that detach an mm, since they take anmmgrab_lazy_tlb()reference. exec reassignsactive_mmto the new mm as well, so that reference is gone. What is left is themm_usersreference inbprm->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()Quoted 9 lines
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-freeAh, thanks for catching this.
Quoted 36 lines
--- fs/exec.c | 1 + include/linux/sched.h | 4 ++++ kernel/events/core.c | 2 ++ kernel/sched/fair.c | 16 ++++++++++++++++ 4 files changed, 23 insertions(+)fs/exec.c +1
-0@@ -916,6 +916,7 @@ static void exec_mm_put_old(struct mm_struct *old_mm) {setmax_mm_hiwater_rss (¤t->signal->maxrss, old_mm);mm_update_next_owner (old_mm); +sched_cache_exec_done ();mmput (old_mm); }include/linux/sched.h +4
-0@@ -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 kernel/events/core.c +0
-1--- a/kernel/events/core.cQuoted 8 lines
kernel/events/core.c attach_task_ctx_data(struct task_struct *task, struct kmem_cache *ctx_cache, · line 5430
Quoted 6 lines
+++
b/kernel/events/core.ckernel/events/core.c +2
-0@@ -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?
Quoted 22 lines
Duh.. that one is unrelated. My mistake.
Quoted 20 lines
Quoted 4 lines
kernel/events/core.c +3
-0+/* exec() has switched to the new mm and is about to drop the old one. */ +void sched_cache_exec_done (void ) +{kernel/events/core.c attach_task_ctx_data(struct task_struct *task, struct kmem_cache *ctx_cache,
Quoted 10 lines
kernel/events/core.c +10
-0+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 inaccount_mm_sched(). Small open: since the context of invokingaccount_mm_sched()is preemption-disabled, I wonder if we can simply usesynchronize_rcu()directly instead ofthis_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/[email protected]/
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
Quoted 124 lines · Tim Chen
Quoted 100 lines · Hyunwoo Kim
Quoted 90 lines · Yu C
Hi Hyunwoo,
Quoted 39 lines · Hyunwoo Kim
When the waker cannot use the wakelist,
ttwu_queue()takes the target rq lock and goes down intoupdate_curr(). If the target rq belongs to another CPU, the task handed toaccount_mm_sched()is the one running on that CPU, not the task being woken.
account_mm_sched()readsp->mmand updatesmm->sc_stat. Nothing keeps that mm alive. The rq lock andrq->cpu_epoch_lockit holds have nothing to do with the lifetime of the mm.If that task happens to be in
execve(),exec_mmap()pointstsk->mmandtsk->active_mmat the new mm, andexec_mm_put_old()fromsetup_new_exec()drops the old one.free_bprm()does the same when exec fails. On the way frommmput()down to__mmdrop(),mm_destroy_sched()callsfree_percpu()on sc_stat.pcpu_sched andfree_mm()returns themm_struct.Whoever already read the old pointer keeps using it. It adds to runtime in the freed per-cpu area and reads
sc_statin the freedmm_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 remainingp->mmdereference to the local variable, and said theactive_mmreference keeps the structure allocated. That holds for the other paths that detach an mm, since they take anmmgrab_lazy_tlb()reference. exec reassignsactive_mmto the new mm as well, so that reference is gone. What is left is themm_usersreference inbprm->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()Quoted 9 lines
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-freeAh, thanks for catching this.
Quoted 36 lines
--- fs/exec.c | 1 + include/linux/sched.h | 4 ++++ kernel/events/core.c | 2 ++ kernel/sched/fair.c | 16 ++++++++++++++++ 4 files changed, 23 insertions(+)fs/exec.c +1
-0@@ -916,6 +916,7 @@ static void exec_mm_put_old(struct mm_struct *old_mm) {setmax_mm_hiwater_rss (¤t->signal->maxrss, old_mm);mm_update_next_owner (old_mm); +sched_cache_exec_done ();mmput (old_mm); }include/linux/sched.h +4
-0@@ -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 kernel/events/core.c +0
-1--- a/kernel/events/core.cQuoted 8 lines
kernel/events/core.c attach_task_ctx_data(struct task_struct *task, struct kmem_cache *ctx_cache, · line 5430
Quoted 6 lines
+++
b/kernel/events/core.ckernel/events/core.c +2
-0@@ -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?
Quoted 22 lines
Duh.. that one is unrelated. My mistake.
Quoted 20 lines
Quoted 4 lines
kernel/events/core.c +3
-0+/* exec() has switched to the new mm and is about to drop the old one. */ +void sched_cache_exec_done (void ) +{kernel/events/core.c attach_task_ctx_data(struct task_struct *task, struct kmem_cache *ctx_cache,
Quoted 10 lines
kernel/events/core.c +10
-0+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 inaccount_mm_sched(). Small open: since the context of invokingaccount_mm_sched()is preemption-disabled, I wonder if we can simply usesynchronize_rcu()directly instead ofthis_rq_lock_irq()- just to avoid contention for rq-lock in heavy system?
Quoted 1 line
Thanks to Kyunwoo to show this potential use-after-free problem.
It is a real, triggerable issue.
Quoted 14 lines
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_statfrom mm and make it its own structure. Then we can do proper refcounting and rcu management onsc_statitself. We will let task points tosc_statwith proper ref count and change the access astask->sc_statdirectly nstead of viatask->mm->sc_stat.Then we will know
sc_statexists inaccount_mm_sched()execution as long as a task points to it and do not have to worry thatsc_statis released due to the mm life cycle.
Quoted 8 lines
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/[email protected]/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
Quoted 125 lines · Hyunwoo Kim
Quoted 124 lines · Tim Chen
Quoted 122 lines · Hyunwoo Kim
Quoted 90 lines · Yu C
Hi Hyunwoo,
Quoted 39 lines · Hyunwoo Kim
When the waker cannot use the wakelist,
ttwu_queue()takes the target rq lock and goes down intoupdate_curr(). If the target rq belongs to another CPU, the task handed toaccount_mm_sched()is the one running on that CPU, not the task being woken.
account_mm_sched()readsp->mmand updatesmm->sc_stat. Nothing keeps that mm alive. The rq lock andrq->cpu_epoch_lockit holds have nothing to do with the lifetime of the mm.If that task happens to be in
execve(),exec_mmap()pointstsk->mmandtsk->active_mmat the new mm, andexec_mm_put_old()fromsetup_new_exec()drops the old one.free_bprm()does the same when exec fails. On the way frommmput()down to__mmdrop(),mm_destroy_sched()callsfree_percpu()on sc_stat.pcpu_sched andfree_mm()returns themm_struct.Whoever already read the old pointer keeps using it. It adds to runtime in the freed per-cpu area and reads
sc_statin the freedmm_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 remainingp->mmdereference to the local variable, and said theactive_mmreference keeps the structure allocated. That holds for the other paths that detach an mm, since they take anmmgrab_lazy_tlb()reference. exec reassignsactive_mmto the new mm as well, so that reference is gone. What is left is themm_usersreference inbprm->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()Quoted 9 lines
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-freeAh, thanks for catching this.
Quoted 36 lines
--- fs/exec.c | 1 + include/linux/sched.h | 4 ++++ kernel/events/core.c | 2 ++ kernel/sched/fair.c | 16 ++++++++++++++++ 4 files changed, 23 insertions(+)fs/exec.c +1
-0@@ -916,6 +916,7 @@ static void exec_mm_put_old(struct mm_struct *old_mm) {setmax_mm_hiwater_rss (¤t->signal->maxrss, old_mm);mm_update_next_owner (old_mm); +sched_cache_exec_done ();mmput (old_mm); }include/linux/sched.h +4
-0@@ -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 kernel/events/core.c +0
-1--- a/kernel/events/core.cQuoted 8 lines
kernel/events/core.c attach_task_ctx_data(struct task_struct *task, struct kmem_cache *ctx_cache, · line 5430
Quoted 6 lines
+++
b/kernel/events/core.ckernel/events/core.c +2
-0@@ -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.
Quoted 20 lines
Quoted 4 lines
kernel/events/core.c +3
-0+/* exec() has switched to the new mm and is about to drop the old one. */ +void sched_cache_exec_done (void ) +{kernel/events/core.c attach_task_ctx_data(struct task_struct *task, struct kmem_cache *ctx_cache,
Quoted 10 lines
kernel/events/core.c +10
-0+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 inaccount_mm_sched(). Small open: since the context of invokingaccount_mm_sched()is preemption-disabled, I wonder if we can simply usesynchronize_rcu()directly instead ofthis_rq_lock_irq()- just to avoid contention for rq-lock in heavy system?
Quoted 4 lines
Quoted 1 line
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/[email protected]/
fixes this issue.
Quoted 19 lines
Quoted 19 lines
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_statfrom mm and make it its own structure. Then we can do proper refcounting and rcu management onsc_statitself. We will let task points tosc_statwith proper ref count and change the access astask->sc_statdirectly nstead of viatask->mm->sc_stat.Then we will know
sc_statexists inaccount_mm_sched()execution as long as a task points to it and do not have to worry thatsc_statis 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/[email protected]/
Quoted 5 lines
Quoted 3 lines
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?
Quoted 3 lines
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
Quoted 4 lines
Best regards,
Hyunwoo Kim
On Mon, Aug 31, 2026 at 03:10:40PM -0700, Tim Chen wrote:
[ ... ]
Quoted 11 lines
Quoted 5 lines
Quoted 3 lines
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_statis 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
Quoted 165 lines · Tim Chen
Quoted 125 lines · Hyunwoo Kim
Quoted 124 lines · Tim Chen
Quoted 122 lines · Hyunwoo Kim
Quoted 90 lines · Yu C
Hi Hyunwoo,
Quoted 39 lines · Hyunwoo Kim
When the waker cannot use the wakelist,
ttwu_queue()takes the target rq lock and goes down intoupdate_curr(). If the target rq belongs to another CPU, the task handed toaccount_mm_sched()is the one running on that CPU, not the task being woken.
account_mm_sched()readsp->mmand updatesmm->sc_stat. Nothing keeps that mm alive. The rq lock andrq->cpu_epoch_lockit holds have nothing to do with the lifetime of the mm.If that task happens to be in
execve(),exec_mmap()pointstsk->mmandtsk->active_mmat the new mm, andexec_mm_put_old()fromsetup_new_exec()drops the old one.free_bprm()does the same when exec fails. On the way frommmput()down to__mmdrop(),mm_destroy_sched()callsfree_percpu()on sc_stat.pcpu_sched andfree_mm()returns themm_struct.Whoever already read the old pointer keeps using it. It adds to runtime in the freed per-cpu area and reads
sc_statin the freedmm_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 remainingp->mmdereference to the local variable, and said theactive_mmreference keeps the structure allocated. That holds for the other paths that detach an mm, since they take anmmgrab_lazy_tlb()reference. exec reassignsactive_mmto the new mm as well, so that reference is gone. What is left is themm_usersreference inbprm->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()Quoted 9 lines
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-freeAh, thanks for catching this.
Quoted 36 lines
--- fs/exec.c | 1 + include/linux/sched.h | 4 ++++ kernel/events/core.c | 2 ++ kernel/sched/fair.c | 16 ++++++++++++++++ 4 files changed, 23 insertions(+)fs/exec.c +1
-0@@ -916,6 +916,7 @@ static void exec_mm_put_old(struct mm_struct *old_mm) {setmax_mm_hiwater_rss (¤t->signal->maxrss, old_mm);mm_update_next_owner (old_mm); +sched_cache_exec_done ();mmput (old_mm); }include/linux/sched.h +4
-0@@ -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 kernel/events/core.c +0
-1--- a/kernel/events/core.cQuoted 8 lines
kernel/events/core.c attach_task_ctx_data(struct task_struct *task, struct kmem_cache *ctx_cache, · line 5430
Quoted 6 lines
+++
b/kernel/events/core.ckernel/events/core.c +2
-0@@ -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.
Quoted 20 lines
Quoted 4 lines
kernel/events/core.c +3
-0+/* exec() has switched to the new mm and is about to drop the old one. */ +void sched_cache_exec_done (void ) +{kernel/events/core.c attach_task_ctx_data(struct task_struct *task, struct kmem_cache *ctx_cache,
Quoted 10 lines
kernel/events/core.c +10
-0+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 inaccount_mm_sched(). Small open: since the context of invokingaccount_mm_sched()is preemption-disabled, I wonder if we can simply usesynchronize_rcu()directly instead ofthis_rq_lock_irq()- just to avoid contention for rq-lock in heavy system?Quoted 4 lines
Quoted 1 line
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/[email protected]/
fixes this issue.Quoted 19 lines
Quoted 19 lines
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_statfrom mm and make it its own structure. Then we can do proper refcounting and rcu management onsc_statitself. We will let task points tosc_statwith proper ref count and change the access astask->sc_statdirectly nstead of viatask->mm->sc_stat.Then we will know
sc_statexists inaccount_mm_sched()execution as long as a task points to it and do not have to worry thatsc_statis 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/[email protected]/Quoted 5 lines
Quoted 3 lines
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_statis the best solution without adding latency in the exec path.
Quoted 8 lines
Wonder what is Peter's opinion?
Quoted 3 lines
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
Quoted 169 lines · Hyunwoo Kim
Quoted 168 lines · Tim Chen
Quoted 125 lines · Hyunwoo Kim
Quoted 124 lines · Tim Chen
Quoted 122 lines · Hyunwoo Kim
Quoted 90 lines · Yu C
Hi Hyunwoo,
Quoted 39 lines · Hyunwoo Kim
When the waker cannot use the wakelist,
ttwu_queue()takes the target rq lock and goes down intoupdate_curr(). If the target rq belongs to another CPU, the task handed toaccount_mm_sched()is the one running on that CPU, not the task being woken.
account_mm_sched()readsp->mmand updatesmm->sc_stat. Nothing keeps that mm alive. The rq lock andrq->cpu_epoch_lockit holds have nothing to do with the lifetime of the mm.If that task happens to be in
execve(),exec_mmap()pointstsk->mmandtsk->active_mmat the new mm, andexec_mm_put_old()fromsetup_new_exec()drops the old one.free_bprm()does the same when exec fails. On the way frommmput()down to__mmdrop(),mm_destroy_sched()callsfree_percpu()on sc_stat.pcpu_sched andfree_mm()returns themm_struct.Whoever already read the old pointer keeps using it. It adds to runtime in the freed per-cpu area and reads
sc_statin the freedmm_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 remainingp->mmdereference to the local variable, and said theactive_mmreference keeps the structure allocated. That holds for the other paths that detach an mm, since they take anmmgrab_lazy_tlb()reference. exec reassignsactive_mmto the new mm as well, so that reference is gone. What is left is themm_usersreference inbprm->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()Quoted 9 lines
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-freeAh, thanks for catching this.
Quoted 36 lines
--- fs/exec.c | 1 + include/linux/sched.h | 4 ++++ kernel/events/core.c | 2 ++ kernel/sched/fair.c | 16 ++++++++++++++++ 4 files changed, 23 insertions(+)fs/exec.c +1
-0@@ -916,6 +916,7 @@ static void exec_mm_put_old(struct mm_struct *old_mm) {setmax_mm_hiwater_rss (¤t->signal->maxrss, old_mm);mm_update_next_owner (old_mm); +sched_cache_exec_done ();mmput (old_mm); }include/linux/sched.h +4
-0@@ -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 kernel/events/core.c +0
-1--- a/kernel/events/core.cQuoted 8 lines
kernel/events/core.c attach_task_ctx_data(struct task_struct *task, struct kmem_cache *ctx_cache, · line 5430
Quoted 6 lines
+++
b/kernel/events/core.ckernel/events/core.c +2
-0@@ -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.
Quoted 20 lines
Quoted 4 lines
kernel/events/core.c +3
-0+/* exec() has switched to the new mm and is about to drop the old one. */ +void sched_cache_exec_done (void ) +{kernel/events/core.c attach_task_ctx_data(struct task_struct *task, struct kmem_cache *ctx_cache,
Quoted 10 lines
kernel/events/core.c +10
-0+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 inaccount_mm_sched(). Small open: since the context of invokingaccount_mm_sched()is preemption-disabled, I wonder if we can simply usesynchronize_rcu()directly instead ofthis_rq_lock_irq()- just to avoid contention for rq-lock in heavy system?Quoted 4 lines
Quoted 1 line
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/[email protected]/
fixes this issue.Quoted 19 lines
Quoted 19 lines
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_statfrom mm and make it its own structure. Then we can do proper refcounting and rcu management onsc_statitself. We will let task points tosc_statwith proper ref count and change the access astask->sc_statdirectly nstead of viatask->mm->sc_stat.Then we will know
sc_statexists inaccount_mm_sched()execution as long as a task points to it and do not have to worry thatsc_statis 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/[email protected]/Quoted 5 lines
Quoted 3 lines
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_statis the best solution without adding latency in the exec path.Wonder what is Peter's opinion?
Quoted 1 line
Quoted 8 lines
Quoted 5 lines
Quoted 2 lines
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
Quoted 3 lines
Best regards,
Hyunwoo Kim