sched/cache: Introduce task_struct->sched_cache_grp to fix UAF

Add a sched_cache_grp pointer to task_struct so that scheduler code
can access the cache group directly via the task, without going
through mm->sched_cache_grp.  This decouples the scheduler's hot-path
accesses from the mm_struct.

Each task holds its own refcount on the sched_cache_group, separate
from the reference held by its mm_struct.  The reference is acquired
in copy_mm() (fork) and exec_mmap() (exec), and released in exit_mm().
This fixes the use-after-free when account_mm_sched() reaches the group
through a task whose mm is being switched, as reported by Hyunwoo:

  https://lore.kernel.org/lkml/apPb-Dr4nPYuHQOK@v4bel/

Convert all scheduler code in fair.c and exit.c to use
p->sched_cache_grp instead of p->mm->sched_cache_grp.

Keep the fork/exec/exit reference management out of the generic mm
paths: add sched_cache_fork(), sched_cache_fork_cleanup(),
sched_cache_exec_mmap() and sched_cache_exit_mm() in
kernel/sched/cache_sched.c (with empty stubs for !CONFIG_SCHED_CACHE),
so fs/exec.c, kernel/fork.c and kernel/exit.c each call one helper
instead of open-coding the refcounting under #ifdef.  Also add
sched_cache_group_get() and task_cache_group_get().

Fixes: df0d984759 ("sched/cache: Introduce infrastructure for cache-aware load balancing")
Closes: https://lore.kernel.org/lkml/apPb-Dr4nPYuHQOK@v4bel/
Closes: https://lore.kernel.org/all/343a7e07-7fad-4979-9c9b-82ec038c293c@linux.dev/
Reported-by: Hyunwoo Kim <imv4bel@gmail.com>
Reported-by: Zenghui Yu (Huawei) <zenghui.yu@linux.dev>
Co-developed-by: Chen Yu <yu.c.chen@intel.com>
Signed-off-by: Chen Yu <yu.c.chen@intel.com>
Signed-off-by: Tim Chen <tim.c.chen@linux.intel.com>
Signed-off-by: Peter Zijlstra (Intel) <peterz@infradead.org>
Signed-off-by: Ingo Molnar <mingo@kernel.org>
Cc: <stable@kernel.org> #7.2.x
Link: https://patch.msgid.link/ae7081dc54736bf115215f9867abb2711a7403fb.1790035273.git.tim.c.chen@linux.intel.com
This commit is contained in:
Tim Chen 2026-09-21 17:37:25 -07:00 committed by Ingo Molnar
parent 28f9c0e0a0
commit b636fef85b
5 changed files with 154 additions and 92 deletions

View File

@ -882,6 +882,7 @@ static int exec_mmap(struct linux_binprm *bprm)
active_mm = tsk->active_mm; active_mm = tsk->active_mm;
tsk->active_mm = mm; tsk->active_mm = mm;
tsk->mm = mm; tsk->mm = mm;
sched_cache_exec_mmap(tsk, mm);
mm_init_cid(mm, tsk); mm_init_cid(mm, tsk);
exec_state = task_exec_state_replace(tsk, exec_state); exec_state = task_exec_state_replace(tsk, exec_state);
/* /*

View File

@ -1433,6 +1433,7 @@ struct task_struct {
#ifdef CONFIG_SCHED_CACHE #ifdef CONFIG_SCHED_CACHE
struct callback_head cache_work; struct callback_head cache_work;
struct sched_cache_group __rcu *sched_cache_grp;
int preferred_llc; int preferred_llc;
/* 1: task was enqueued to its preferred LLC, 0 otherwise */ /* 1: task was enqueued to its preferred LLC, 0 otherwise */
int pref_llc_queued; int pref_llc_queued;
@ -2417,10 +2418,23 @@ struct sched_cache_group {
struct rcu_head rcu; struct rcu_head rcu;
} ____cacheline_aligned_in_smp; } ____cacheline_aligned_in_smp;
struct sched_cache_group *sched_cache_group_get(struct sched_cache_group *grp);
struct sched_cache_group *task_cache_group_get(struct task_struct *p);
void sched_cache_fork(struct task_struct *p);
void sched_cache_fork_cleanup(struct task_struct *p);
void sched_cache_exec_mmap(struct task_struct *p, struct mm_struct *mm);
void sched_cache_exit_mm(struct task_struct *p);
#else #else
struct sched_cache_group { }; struct sched_cache_group { };
static inline void sched_cache_fork(struct task_struct *p) { }
static inline void sched_cache_fork_cleanup(struct task_struct *p) { }
static inline void sched_cache_exec_mmap(struct task_struct *p, struct mm_struct *mm) { }
static inline void sched_cache_exit_mm(struct task_struct *p) { }
#endif #endif
#ifndef MODULE #ifndef MODULE

View File

@ -551,37 +551,6 @@ void mm_update_next_owner(struct mm_struct *mm)
} }
#endif /* CONFIG_MEMCG */ #endif /* CONFIG_MEMCG */
#if defined(CONFIG_SCHED_CACHE) && defined(CONFIG_NUMA_BALANCING)
/*
* Subtract the memory footprint of the current task from
* mm.
*/
static void exit_mm_sched_cache(struct mm_struct *mm)
{
struct sched_cache_group *grp;
unsigned long fp, sub;
if (!current->total_numa_faults)
return;
/*
* No lock protection due to performance considerations.
* Make sure the group footprint does not become
* negative.
*/
grp = READ_ONCE(mm->sched_cache_grp);
if (!grp)
return;
fp = READ_ONCE(grp->footprint);
sub = min(fp, current->total_numa_faults);
WRITE_ONCE(grp->footprint, fp - sub);
}
#else
static inline void exit_mm_sched_cache(struct mm_struct *mm)
{
}
#endif /* CONFIG_SCHED_CACHE CONFIG_NUMA_BALANCING */
/* /*
* Turn us into a lazy TLB process if we * Turn us into a lazy TLB process if we
* aren't already.. * aren't already..
@ -594,7 +563,7 @@ static void exit_mm(void)
if (!mm) if (!mm)
return; return;
exit_mm_sched_cache(mm); sched_cache_exit_mm(current);
mmap_read_lock(mm); mmap_read_lock(mm);
mmgrab_lazy_tlb(mm); mmgrab_lazy_tlb(mm);

View File

@ -1599,6 +1599,7 @@ static int copy_mm(u64 clone_flags, struct task_struct *tsk)
tsk->mm = mm; tsk->mm = mm;
tsk->active_mm = mm; tsk->active_mm = mm;
sched_cache_fork(tsk);
return 0; return 0;
} }
@ -2602,6 +2603,7 @@ __latent_entropy struct task_struct *copy_process(
bad_fork_cleanup_namespaces: bad_fork_cleanup_namespaces:
exit_nsproxy_namespaces(p); exit_nsproxy_namespaces(p);
bad_fork_cleanup_mm: bad_fork_cleanup_mm:
sched_cache_fork_cleanup(p);
if (p->mm) { if (p->mm) {
mm_clear_owner(p->mm, p); mm_clear_owner(p->mm, p);
mmput(p->mm); mmput(p->mm);

View File

@ -1478,7 +1478,7 @@ static inline int get_sched_cache_scale(int mul)
return (1 + (tol - 1) * mul); return (1 + (tol - 1) * mul);
} }
static bool exceed_llc_capacity(struct mm_struct *mm, int cpu) static bool exceed_llc_capacity(struct sched_cache_group *grp, int cpu)
{ {
#ifdef CONFIG_NUMA_BALANCING #ifdef CONFIG_NUMA_BALANCING
unsigned long llc, footprint; unsigned long llc, footprint;
@ -1492,11 +1492,6 @@ static bool exceed_llc_capacity(struct mm_struct *mm, int cpu)
return true; return true;
if (static_branch_likely(&sched_numa_balancing)) { if (static_branch_likely(&sched_numa_balancing)) {
struct sched_cache_group *grp = READ_ONCE(mm->sched_cache_grp);
if (!grp)
return true;
/* /*
* TBD: RDT exclusive LLC ways reserved should be * TBD: RDT exclusive LLC ways reserved should be
* excluded. * excluded.
@ -1531,10 +1526,9 @@ static bool exceed_llc_capacity(struct mm_struct *mm, int cpu)
return false; return false;
} }
static bool invalid_llc_nr(struct mm_struct *mm, struct task_struct *p, static bool invalid_llc_nr(struct sched_cache_group *grp, struct task_struct *p,
int cpu) int cpu)
{ {
struct sched_cache_group *grp;
int scale; int scale;
if (get_nr_threads(p) <= 1) if (get_nr_threads(p) <= 1)
@ -1548,10 +1542,6 @@ static bool invalid_llc_nr(struct mm_struct *mm, struct task_struct *p,
if (scale == INT_MAX) if (scale == INT_MAX)
return false; return false;
grp = READ_ONCE(mm->sched_cache_grp);
if (!grp)
return true;
return !fits_capacity((READ_ONCE(grp->nr_running_avg) * cpu_smt_num_threads), return !fits_capacity((READ_ONCE(grp->nr_running_avg) * cpu_smt_num_threads),
(scale * per_cpu(sd_llc_size, cpu))); (scale * per_cpu(sd_llc_size, cpu)));
} }
@ -1723,6 +1713,96 @@ static void sched_cache_group_put(struct sched_cache_group *grp)
call_rcu(&grp->rcu, sched_cache_group_free_rcu); call_rcu(&grp->rcu, sched_cache_group_free_rcu);
} }
DEFINE_FREE(sched_cache_group_put, struct sched_cache_group *,
sched_cache_group_put(_T));
#define rcu_deref_sched_cache_grp(tsk) \
rcu_dereference_check((tsk)->sched_cache_grp, (tsk) == current)
static struct sched_cache_group *sched_cache_replace_grp(struct task_struct *p,
struct sched_cache_group *new)
{
struct sched_cache_group *old;
old = rcu_deref_sched_cache_grp(p);
rcu_assign_pointer(p->sched_cache_grp, new);
return old;
}
struct sched_cache_group *sched_cache_group_get(struct sched_cache_group *grp)
{
/*
* refcount_inc_not_zero() is the acquire primitive for lockless
* (RCU) lookups; plain refcount_inc() would scribble the count if
* it already reached zero. Return NULL in that case.
*/
if (grp && !refcount_inc_not_zero(&grp->refcnt))
grp = NULL;
return grp;
}
struct sched_cache_group *task_cache_group_get(struct task_struct *p)
{
guard(rcu)();
return sched_cache_group_get(rcu_dereference(p->sched_cache_grp));
}
void sched_cache_fork(struct task_struct *p)
{
/*
* The child takes its own reference on the mm's cache group, separate
* from the reference held by the mm. @p is not yet visible to readers,
* so a plain initializing store is enough.
*/
RCU_INIT_POINTER(p->sched_cache_grp,
sched_cache_group_get(p->mm->sched_cache_grp));
}
void sched_cache_fork_cleanup(struct task_struct *p)
{
/*
* A fork that fails after sched_cache_fork() never reaches exit_mm(),
* so drop the reference here. @p never became visible, so there are no
* concurrent readers and the reference we hold keeps the group alive.
*/
sched_cache_group_put(rcu_access_pointer(p->sched_cache_grp));
RCU_INIT_POINTER(p->sched_cache_grp, NULL);
}
void sched_cache_exec_mmap(struct task_struct *p, struct mm_struct *mm)
{
struct sched_cache_group *old;
/*
* Acquire the new reference before publishing the pointer, then drop
* the old one. @p is current and the only writer of its own pointer.
*/
old = sched_cache_replace_grp(p, sched_cache_group_get(mm->sched_cache_grp));
sched_cache_group_put(old);
}
void sched_cache_exit_mm(struct task_struct *p)
{
struct sched_cache_group *grp = sched_cache_replace_grp(p, NULL);
#ifdef CONFIG_NUMA_BALANCING
/*
* Subtract this task's footprint from the group before dropping the
* reference, so the group footprint converges as its threads exit.
* Unlocked for performance; clamp to avoid underflow.
*/
if (grp && p->total_numa_faults) {
unsigned long fp = READ_ONCE(grp->footprint);
unsigned long sub = min(fp, p->total_numa_faults);
WRITE_ONCE(grp->footprint, fp - sub);
}
#endif
sched_cache_group_put(grp);
}
void mm_destroy_sched(struct mm_struct *mm) void mm_destroy_sched(struct mm_struct *mm)
{ {
sched_cache_group_put(mm->sched_cache_grp); sched_cache_group_put(mm->sched_cache_grp);
@ -1777,15 +1857,10 @@ static unsigned long fraction_mm_sched(struct rq *rq,
return div64_u64(NICE_0_LOAD * pcpu_sched->runtime, rq->cpu_runtime + 1); return div64_u64(NICE_0_LOAD * pcpu_sched->runtime, rq->cpu_runtime + 1);
} }
static int get_pref_llc(struct task_struct *p, struct mm_struct *mm) static int get_pref_llc(struct task_struct *p, struct sched_cache_group *grp)
{ {
int mm_sched_llc = -1, mm_sched_cpu; int mm_sched_llc = -1, mm_sched_cpu;
struct sched_cache_group *grp;
if (!mm)
return -1;
grp = READ_ONCE(mm->sched_cache_grp);
if (!grp) if (!grp)
return -1; return -1;
@ -1819,9 +1894,8 @@ static unsigned int task_running_on_cpu(int cpu, struct task_struct *p);
static inline static inline
void account_mm_sched(struct rq *rq, struct task_struct *p, s64 delta_exec) void account_mm_sched(struct rq *rq, struct task_struct *p, s64 delta_exec)
{ {
struct sched_cache_group *grp = rcu_dereference_all(p->sched_cache_grp);
struct sched_cache_time *pcpu_sched; struct sched_cache_time *pcpu_sched;
struct sched_cache_group *grp;
struct mm_struct *mm = p->mm;
int mm_sched_llc = -1; int mm_sched_llc = -1;
unsigned long epoch; unsigned long epoch;
@ -1832,12 +1906,8 @@ void account_mm_sched(struct rq *rq, struct task_struct *p, s64 delta_exec)
return; return;
/* /*
* init_task, kthreads and user thread created * init_task, kthreads and user thread created
* by user_mode_thread() don't have mm. * by user_mode_thread() don't have a cache group.
*/ */
if (!mm)
return;
grp = READ_ONCE(mm->sched_cache_grp);
if (!grp || !grp->pcpu_sched) if (!grp || !grp->pcpu_sched)
return; return;
@ -1855,13 +1925,13 @@ void account_mm_sched(struct rq *rq, struct task_struct *p, s64 delta_exec)
* its preferred state. * its preferred state.
*/ */
if ((long)(epoch - READ_ONCE(grp->epoch)) > llc_epoch_affinity_timeout || if ((long)(epoch - READ_ONCE(grp->epoch)) > llc_epoch_affinity_timeout ||
invalid_llc_nr(mm, p, cpu_of(rq)) || invalid_llc_nr(grp, p, cpu_of(rq)) ||
exceed_llc_capacity(mm, cpu_of(rq))) { exceed_llc_capacity(grp, cpu_of(rq))) {
if (READ_ONCE(grp->cpu) != -1) if (READ_ONCE(grp->cpu) != -1)
WRITE_ONCE(grp->cpu, -1); WRITE_ONCE(grp->cpu, -1);
} }
mm_sched_llc = get_pref_llc(p, mm); mm_sched_llc = get_pref_llc(p, grp);
/* task not on rq accounted later in account_entity_enqueue() */ /* task not on rq accounted later in account_entity_enqueue() */
if (task_running_on_cpu(rq->cpu, p) && if (task_running_on_cpu(rq->cpu, p) &&
@ -1874,19 +1944,15 @@ void account_mm_sched(struct rq *rq, struct task_struct *p, s64 delta_exec)
static void task_tick_cache(struct rq *rq, struct task_struct *p) static void task_tick_cache(struct rq *rq, struct task_struct *p)
{ {
struct sched_cache_group *grp = rcu_dereference_all(p->sched_cache_grp);
struct callback_head *work = &p->cache_work; struct callback_head *work = &p->cache_work;
struct sched_cache_group *grp;
struct mm_struct *mm = p->mm;
unsigned long epoch; unsigned long epoch;
if (!sched_cache_enabled()) if (!sched_cache_enabled())
return; return;
if (!mm || p->flags & PF_KTHREAD) if (!grp || p->flags & PF_KTHREAD ||
return; !grp->pcpu_sched)
grp = READ_ONCE(mm->sched_cache_grp);
if (!grp || !grp->pcpu_sched)
return; return;
epoch = rq->cpu_epoch; epoch = rq->cpu_epoch;
@ -1968,14 +2034,13 @@ static inline void update_avg_scale(u64 *avg, u64 sample)
static void task_cache_work(struct callback_head *work) static void task_cache_work(struct callback_head *work)
{ {
struct sched_cache_group *grp __free(sched_cache_group_put) = NULL;
cpumask_var_t cpus __free(free_cpumask_var) = CPUMASK_VAR_NULL;
int cpu, m_a_cpu = -1, nr_running = 0, curr_cpu; int cpu, m_a_cpu = -1, nr_running = 0, curr_cpu;
unsigned long next_scan, now = jiffies; unsigned long next_scan, now = jiffies;
struct task_struct *p = current, *cur; struct task_struct *p = current, *cur;
unsigned long curr_m_a_occ = 0; unsigned long curr_m_a_occ = 0;
struct sched_cache_group *grp;
struct mm_struct *mm = p->mm;
unsigned long m_a_occ = 0; unsigned long m_a_occ = 0;
cpumask_var_t cpus;
WARN_ON_ONCE(work != &p->cache_work); WARN_ON_ONCE(work != &p->cache_work);
@ -1984,7 +2049,12 @@ static void task_cache_work(struct callback_head *work)
if (p->flags & PF_EXITING) if (p->flags & PF_EXITING)
return; return;
grp = READ_ONCE(mm->sched_cache_grp); /*
* A reference makes sure grp is not released by others. The rcu
* lock can not be held till after zalloc_cpumask_var() below,
* because the latter might sleep.
*/
grp = task_cache_group_get(p);
if (!grp) if (!grp)
return; return;
@ -1999,8 +2069,8 @@ static void task_cache_work(struct callback_head *work)
return; return;
curr_cpu = task_cpu(p); curr_cpu = task_cpu(p);
if (invalid_llc_nr(mm, p, curr_cpu) || if (invalid_llc_nr(grp, p, curr_cpu) ||
exceed_llc_capacity(mm, curr_cpu)) { exceed_llc_capacity(grp, curr_cpu)) {
if (READ_ONCE(grp->cpu) != -1) if (READ_ONCE(grp->cpu) != -1)
WRITE_ONCE(grp->cpu, -1); WRITE_ONCE(grp->cpu, -1);
@ -2033,9 +2103,13 @@ static void task_cache_work(struct callback_head *work)
m_cpu = i; m_cpu = i;
} }
/*
* rcu_access_pointer() is used because the
* pointer is only compared, never dereferenced.
*/
cur = rcu_dereference_all(cpu_rq(i)->curr); cur = rcu_dereference_all(cpu_rq(i)->curr);
if (cur && !(cur->flags & (PF_EXITING | PF_KTHREAD)) && if (cur && !(cur->flags & (PF_EXITING | PF_KTHREAD)) &&
cur->mm == mm) rcu_access_pointer(cur->sched_cache_grp) == grp)
nr_running++; nr_running++;
} }
@ -2081,7 +2155,6 @@ static void task_cache_work(struct callback_head *work)
} }
update_avg_scale(&grp->nr_running_avg, nr_running); update_avg_scale(&grp->nr_running_avg, nr_running);
free_cpumask_var(cpus);
} }
void init_sched_mm(struct task_struct *p) void init_sched_mm(struct task_struct *p)
@ -2090,6 +2163,13 @@ void init_sched_mm(struct task_struct *p)
init_task_work(work, task_cache_work); init_task_work(work, task_cache_work);
work->next = work; work->next = work;
/*
* dup_task_struct() copies the parent's task_struct, including its
* sched_cache_grp, for which the child holds no reference. Clear it
* here - before copy_mm() runs - so the child never carries a
* borrowed pointer that the fork error path would put.
*/
RCU_INIT_POINTER(p->sched_cache_grp, NULL);
/* /*
* Reset new task's preference to avoid * Reset new task's preference to avoid
* polluting account_llc_enqueue(). * polluting account_llc_enqueue().
@ -3890,10 +3970,9 @@ static void task_numa_placement(struct task_struct *p)
* heuristic and occasional lost updates are tolerable. * heuristic and occasional lost updates are tolerable.
* *
* If a task exits, its corresponding footprint must * If a task exits, its corresponding footprint must
* be subtracted from the mm->sched_cache_grp->footprint, * be subtracted from p->sched_cache_grp->footprint,
* otherwise the mm->sched_cache_grp->footprint will not * otherwise the footprint will not converge: the
* converge: the exiting thread's footprint remains * exiting thread's footprint remains unchanged/undecayed.
* unchanged/undecayed in mm->sched_cache_grp->footprint.
* See exit_mm(). * See exit_mm().
* *
* Lost updates and unsynchronized subtraction * Lost updates and unsynchronized subtraction
@ -3901,12 +3980,14 @@ static void task_numa_placement(struct task_struct *p)
* go negative. Clamp to zero to prevent the * go negative. Clamp to zero to prevent the
* unsigned footprint from wrapping. * unsigned footprint from wrapping.
*/ */
grp = READ_ONCE(p->mm->sched_cache_grp); scoped_guard(rcu) {
if (!grp) grp = rcu_dereference(p->sched_cache_grp);
continue;
new_fp = (long)READ_ONCE(grp->footprint) + diff; if (grp) {
WRITE_ONCE(grp->footprint, max(new_fp, 0L)); new_fp = (long)READ_ONCE(grp->footprint) + diff;
WRITE_ONCE(grp->footprint, max(new_fp, 0L));
}
}
#endif #endif
} }
@ -10855,7 +10936,6 @@ static enum llc_mig can_migrate_llc_task(struct lb_env *env,
struct task_struct *p) struct task_struct *p)
{ {
struct sched_cache_group *grp; struct sched_cache_group *grp;
struct mm_struct *mm;
bool to_pref; bool to_pref;
int cpu, src_cpu, dst_cpu; int cpu, src_cpu, dst_cpu;
@ -10864,11 +10944,7 @@ static enum llc_mig can_migrate_llc_task(struct lb_env *env,
src_cpu = env->src_cpu; src_cpu = env->src_cpu;
dst_cpu = env->dst_cpu; dst_cpu = env->dst_cpu;
mm = p->mm; grp = rcu_dereference_all(p->sched_cache_grp);
if (!mm)
return mig_unrestricted;
grp = READ_ONCE(mm->sched_cache_grp);
if (!grp) if (!grp)
return mig_unrestricted; return mig_unrestricted;
@ -10877,8 +10953,8 @@ static enum llc_mig can_migrate_llc_task(struct lb_env *env,
return mig_unrestricted; return mig_unrestricted;
/* skip cache aware load balance for too many threads */ /* skip cache aware load balance for too many threads */
if (invalid_llc_nr(mm, p, dst_cpu) || if (invalid_llc_nr(grp, p, dst_cpu) ||
exceed_llc_capacity(mm, dst_cpu)) { exceed_llc_capacity(grp, dst_cpu)) {
if (READ_ONCE(grp->cpu) != -1) if (READ_ONCE(grp->cpu) != -1)
WRITE_ONCE(grp->cpu, -1); WRITE_ONCE(grp->cpu, -1);
return mig_unrestricted; return mig_unrestricted;