0/3 已展开

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->mmsc_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_sched
  • can_migrate_llc_task()invalid_llc_nr() / exceed_llc_capacity()mm->sc_stat.footprintnr_running_avg
  • task_cache_work()p->mm->sc_stat.next_scancpu

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_structstruct sched_cache_group *sched_cache_grp 替换 sched_cache_stat sc_stat
  • sched_cache_stat 改名 sched_cache_group,新增 refcount_t refcntstruct 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.cexceed_llc_capacity() / invalid_llc_nr() / get_pref_llc() / get_scan_cpumasks() 入参从 struct mm_struct *mm 换成 struct sched_cache_group *grpaccount_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 = grpcan_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() 之后单独 put grp,否则一次失败的 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 窗口。