sched discussion
[PATCH 0/2] sched/cache: Fix use after free mm access in account_mm_sched()
LLM 分析
sched/cache:修复 account_mm_sched() 中的 use-after-free
系列概况
- 标题: [PATCH 0/2] sched/cache: Fix use after free mm access in account_mm_sched()
- 作者: Tim Chen tim.c.chen@linux.intel.com
- 版本: 单次发送,未标 vN;前两 patch 拆自 cache-aware prctl RFC 系列
- 规模: 2 patches
- 修改文件: 9 个 — fs/exec.c, include/linux/mm_types.h, include/linux/sched.h, kernel/exit.c, kernel/fork.c, kernel/sched/build_utility.c, kernel/sched/cache_sched.c (新增), kernel/sched/fair.c, kernel/sched/sched.h
- 代码统计: +239 / -86
- Message-ID: cover.1788305725.git.tim.c.chen@linux.intel.com
- 完整性: 完整(cover letter + 2 patches);Hyunwoo Kim 报告 bug 并提供 Tested-by,KASAN splat 已消失
补丁目的
修复 cache-aware scheduling 在 account_mm_sched() 中触发的 KASAN use-after-free。统计对象 sched_cache_stat 当前嵌入在 mm_struct 里,mm 被释放时同步销毁;但调度器在 update_curr()(仅持 rq lock)、load-balance 谓词 can_migrate_llc_task()、task_cache_work() 的 LLC 占用扫描里读 rq->curr->mm 或远端 p->mm 的 sc_stat,路径上对 mm 没有引用。
并发 exit 或 exec_mmap() 装入新 mm 时,可能在另一个 CPU 脚下 free_percpu(pcpu_sched),造成 UAF。给 mm 拆除路径加 rq lock 会把调度器锁塞进 __mmdrop(),耦合过大;不如让对象拥有独立生命周期。
旧流程的问题
+----------+ rq->curr->mm +--------------------+
| task A | -----------------> | mm_struct |
| (rq lock)| | + sc_stat { |
+----------+ | pcpu_sched } |
+--------------------+
|
__mmdrop() -> free_percpu
v
+------------------+
| reader deref UAF |
+------------------+
读者路径只看到 mm 指针,无 refcount:
update_curr()→account_mm_sched()→rq->curr->mm->sc_stat.pcpu_schedcan_migrate_llc_task()→invalid_llc_nr()/exceed_llc_capacity()读mm->sc_stat.footprint、nr_running_avgtask_cache_work()读p->mm->sc_stat.next_scan、cpu
mm 在 __mmdrop() 中调用 free_percpu,但 mm 拆除路径与读者路径没有同步关系。
新流程
+-----------+ +----------------------+ +--------------+
| task_struct| -----> | sched_cache_group | <----- | mm_struct |
| grp (rcu)| | refcount_t refcnt | | grp 指针 |
| | | rcu_head | +--------------+
+-----------+ | pcpu_sched |
^ +----------------------+
| ^
| refcnt==0 -> call_rcu free
|
copy_mm/exec_mmap get exit_mm / fork-fail put
读路径改为通过 p->sched_cache_grp 取得 grp,先 acquire 引用再访问;mm 销毁只 put 一个引用,最后一个引用消失才 call_rcu() 把 percpu buffer 与 grp 一起释放。
Patch 概览
Patch 1/2 sched/cache: Decouple sched_cache_group from mm
mm_struct用struct sched_cache_group *sched_cache_grp替换sched_cache_stat sc_stat。sched_cache_stat改名sched_cache_group,新增refcount_t refcnt、struct rcu_head rcu,导出sched_cache_group_put()。mm_init_sched()改为返回int:分配 grp、初始化、用smp_store_release(&grp->pcpu_sched, …)发布,再写mm->sched_cache_grp = grp。- 新增
mm_destroy_sched(),内部调用sched_cache_group_put()。 - 新增
kernel/sched/cache_sched.c,提供sched_cache_group_free_rcu()(在call_rcu回调里free_percpu+kfree)。 fair.c中所有mm->sc_stat.*访问改为mm->sched_cache_grp->*,但函数入参暂未改为 grp(由 patch 2 完成)。
Patch 2/2 sched/cache: Introduce task_struct->sched_cache_grp
task_struct新增struct sched_cache_group __rcu *sched_cache_grp。copy_mm()/exec_mmap()通过sched_cache_group_get()acquire 引用,再用rcu_assign_pointer发布;旧 grp 在exec_mmap()中释放。exit_mm()把指针置空并 put;copy_process()失败路径单独 put 防 leak。- 新增
sched_cache_group_get()用refcount_inc_not_zero()做 RCU 下安全 acquire;新增task_cache_group_get()在guard(rcu)下包装。 fair.c把exceed_llc_capacity()/invalid_llc_nr()/get_pref_llc()/get_scan_cpumasks()入参从struct mm_struct *mm换成struct sched_cache_group *grp;account_mm_sched()/task_tick_cache()/task_cache_work()改读rcu_dereference_all(p->sched_cache_grp)。
关键实现
/* kernel/sched/cache_sched.c */
static void sched_cache_group_free_rcu(struct rcu_head *rcu)
{
struct sched_cache_group *grp =
container_of(rcu, struct sched_cache_group, rcu);
free_percpu(grp->pcpu_sched); /* atomic context safe */
kfree(grp);
}
void sched_cache_group_put(struct sched_cache_group *grp)
{
if (!grp || !refcount_dec_and_test(&grp->refcnt))
return;
call_rcu(&grp->rcu, sched_cache_group_free_rcu);
}
struct sched_cache_group *sched_cache_group_get(struct sched_cache_group *grp)
{
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));
}
mm_init_sched() 的发布顺序很关键:先 smp_store_release(&grp->pcpu_sched, _pcpu_sched),再写 mm->sched_cache_grp = grp。can_migrate_llc_task() 只判 grp 指针是否非 NULL 就读 footprint / nr_running_avg,因此 grp 必须先完整初始化,再把指针暴露给读者。
account_mm_sched() 早返条件改为 if (!grp || p->flags & PF_KTHREAD || !grp->pcpu_sched),kthread 通过 kthread_use_mm() 临时借来的 mm 不会被当成正常用户态任务去记账。
类比
把社区活动中心的"健身卡"想象成 sched_cache_group。以前这张卡是"会员卡"(mm_struct)的副卡,会员资格一过期卡就作废。问题是器材室里别的访客手里还捏着这张卡在读"本周剩余次数",卡突然作废就等于别人还在刷一张已销毁的卡——这就是 UAF。
改成独立卡:办卡就 refcount++,每多一个借卡人 ++,退卡 --;最后一个用户退卡才把卡送进"回收站"(call_rcu)真正销毁。每位访客自己持卡,不再挂在会员卡后面,临时换组也不会让手里的卡失效。
Highlight:风险与注意点
- 两个 patch 必须成对 cherry-pick:patch 1 单看只挪位,并没有解决 UAF;单独 backport 会让
account_mm_sched()读到一个由 patch 1 创建、由 patch 2 管理的 grp,但读者还没法稳定持引用,仍然 race。 refcount_inc_not_zero()是 RCU 下 lockless acquire 的正确原语;普通refcount_inc()会在已归零的 grp 上"自欺欺人",导致读者拿到一个马上被释放的对象。mm->sched_cache_grp的发布必须晚于 grp 内部字段初始化(特别是smp_store_release(pcpu_sched)之后),否则can_migrate_llc_task()会读到 partial-init grp。copy_process()失败路径必须在copy_mm()之后单独putgrp,否则一次失败的 fork 会 leak grp 和它的 percpu buffer。- kthread 通过
kthread_use_mm()临时借 mm,PF_KTHREAD早返不能去掉,否则调度器会把"借用 mm 的 kthread"误当成正常用户态任务去记账。 - 后续要把 grp key 从 mm 切到 cgroup / core-scheduling cookie / numa_group,reviewer 要评估接口是否能承载这些 key,而不是只解决眼前 UAF。
一句话总结
把 cache-aware scheduling 的 per-mm 统计对象从 mm_struct 抽出,做成 refcount + RCU 管理的 sched_cache_group,再让每个 task_struct 自己持有引用,从根上消除 account_mm_sched() 在 rq lock 下读 p->mm->sc_stat 的 UAF 窗口。