0/9 已展开

LLM 分析

sched/cache:修复 exec 替换 mm 后的 use-after-free

系列概况

  • 标题:[PATCH] sched/cache: Fix use-after-free of the mm replaced by exec
  • 作者:Hyunwoo Kim imv4bel@gmail.com
  • 版本:单封 PATCH,无 v2/v3
  • 规模:1 个 patch,4 个文件改动,23 行新增 / 0 行删除
  • 修改文件:fs/exec.cinclude/linux/sched.hkernel/events/core.ckernel/sched/fair.c
  • 代码统计:+23 / -0
  • Message-ID(首封):apPb-Dr4nPYuHQOK@v4bel
  • 完整性:含 KASAN Call Trace、Freed-by 调用栈、Fixes: tag、Cc: stableSigned-off-by,背景与重现信息齐全

补丁目的

修复 CONFIG_SCHED_CACHE(cache-aware load balancing)基础设施中的 use-after-free:当某个 task 在某个 CPU 上执行 execve()tsk->mm 切到新 mm 并紧接着释放旧 mm 时,另一个 CPU 上处于目标 rq 锁内的 reader(account_mm_sched())仍然持有旧 mm 指针去读写已被 free_percpu()free_mm() 回收的 sc_stat,触发 KASAN 报告的 slab use-after-free。

旧流程的问题

CPU0 (waker)                          CPU1 (execve)
=============                         =============
try_to_wake_up()
  ttwu_queue()
    lock rq1
      update_curr()
        update_se()
          account_mm_sched(rq1, curr)
            mm = rq1->curr->mm   // reads OLD mm pointer
                                       exec_mmap()  tsk->mm = new_mm
                                       exec_mm_put_old()
                                         mmput() -> mmdrop()
                                           mm_destroy_sched()
                                             free_percpu(sc_stat.pcpu_sched)
                                           free_mm(mm_struct)
                                       // old mm is now DEAD
            mm->sc_stat.epoch  // UAF: read freed memory
            mm->sc_stat.cpu    // UAF: write freed memory

KASAN 报告关键摘录:

BUG: KASAN: slab-use-after-free in update_se+0xe6e/0xf70
Read of size 8 at addr ffff8880093cad10
 task sc-direct-set/80
...
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

新流程

execve path                                 reader path (any CPU)
============                                 ====================
exec_mmap()  tsk->mm = new_mm
setup_new_exec() -> exec_mm_put_old()
  setmax_mm_hiwater_rss(...)
  mm_update_next_owner(old_mm)
+ sched_cache_exec_done()   // NEW HOOK
+   rq = this_rq_lock_irq(&rf)   -- take self rq lock
+   rq_unlock_irq(rq, &rf)       -- release self rq lock
  mmput(old_mm)                         // then drop old mm
   |
   v
account_mm_sched() that wanted the old mm:
  -> either was mutex-waited out by the lock cycle above
  -> or re-acquires the lock and observes the NEW mm
  -> never touches the freed old_mm->sc_stat again

Patch 概览

文件改动
kernel/sched/fair.c新增 sched_cache_exec_done():在旧 mm 被 drop 之前,对 exec'ing task 自身的 rq 锁做一次 acquire/release,配 RCU 释放语义保证 reader 之后看到 new mm
include/linux/sched.hstruct sched_cache_stat 后声明 sched_cache_exec_done()!CONFIG_SCHED_CACHE 时提供空 stub
fs/exec.cexec_mm_put_old() 内追加 sched_cache_exec_done() 调用点
kernel/events/core.cattach_task_ctx_data() 中在 try_cmpxchg() 周围用 guard(rcu) 包住 old 的使用

关键实现

/* kernel/sched/fair.c */
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);
}

include/linux/sched.h 中的对称声明:

#ifdef CONFIG_SCHED_CACHE
void sched_cache_exec_done(void);
#else
static inline void sched_cache_exec_done(void) { }
#endif

fs/exec.c 中的挂钩点:

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();   /* lock cycle BEFORE mmput */
    mmput(old_mm);
}

作者的核心论证:

  • account_mm_sched() 在目标 rq 锁内读 rq->curr->mm,而 rq->curr->mm 在 exec 路径上正是被换掉的那个 mm。
  • rq 锁不保证 mm 不会被释放;只有 mmgrab_*() 系列的引用计数保证。
  • 9f23469401b0 依赖的 active_mm 引用在 exec 中也会被切到 new mm,因此 bprm->old_mm 上的 mm_users 引用就成为唯一最后持有者——一旦 mmput(),就触发 free。
  • 在 exec'ing task 自身的 rq 上做一次 lock/unlock 即可保证:
    1. 任何已经在旧 mm 上的 reader 会先完成(被 mutex 等待);
    2. 之后任何取锁的 reader 看到的是 new mm,因为 tsk->mm = new_mm 不能越过 rq 锁 release 被重排到外层。

类比

把这想象成办公室的「工位交接」。员工 A 离职,HR 要把 A 的工位清理掉;但就在 HR 动手的同一秒,另一位同事正按 A 的名牌去找 A 借资料。原先的修复只保证「会议室申请表」上 A 的名字还在(active_mm 引用),可 exec 把这张表也改成了 B,于是 A 的工位立刻被清走,借资料的同事扑了个空——这正是 UAF。

补丁的做法是:在清理 A 的工位之前,先让 A 所在部门的主管把会议室钥匙(rq 锁)取一次再放回去——这把钥匙是「同一把」,借资料的同事一定也在等这把钥匙;等到钥匙被放回、主管再次锁门时,A 的工位已经被清掉,借资料的人也已经在等待队列里被排空。等到下一位来借资料的同事进门时,名牌上已经是 B,再也不会去碰 A 的遗物。

Highlight:风险与注意点

  • rq 锁依赖隐式成立:补丁的成立完全依赖 account_mm_sched() 在 rq 锁内取 mm。一旦后续有人把这条路径脱锁取 mm 或改成 RCU-only,UAF 仍会回来——建议把"在 rq 锁内取 mm"作为 invariant 写进注释或 review checklist。
  • 迁移路径:exec'ing task 在 setup_new_exec()exec_mm_put_old() 之间可能迁移到别的 CPU;补丁锁的是"当前所在 rq"。作者论证:"若已经迁走,离开旧 rq 时必经过同一把锁,因此那里的 reader 必已走完"——逻辑正确但偏隐式,建议补丁补一句顺序保证说明。
  • 替代方案:Tim Chen 主张应尽快合并他那一系列把 sc_statsched_group 引用计数解耦、用 call_rcu() 释放的 patch;Chen Yu 认同"decouple sc_stat 是延迟最低"的方案。本补丁是 最小止血,长期看应作为过渡。
  • stable 回退:补丁标了 Cc: stable,需要回退到启用 CONFIG_SCHED_CACHE 的稳定版本,移植者需核对 this_rq_lock_irq / rq_unlock_irq 在目标 stable 上的签名。
  • RCU 注释:kernel/events/core.c 里加的 @old, loaded by the try_cmpxchg() below, is only stable under RCU. 这句注释虽小,但说明 perf ctx_data 的 attach 路径也踩过同类 RCU-vs-pointer 坑,提醒 reviewer 关注"用本地变量缓存原子读出的指针"这种写法的生命周期。

版本变化

本线程只有一封 PATCH,没有 v2/v3。后续讨论集中在"是否合并 Tim Chen 更大的解耦系列"而非本补丁本身的迭代。

一句话总结

exec_mm_put_old() 释放旧 mm 之前,对当前 task 自身的 rq 锁做一次 acquire/release,把所有还在用旧 mm 的 reader 用同一把锁排空,从而修掉 sched/cache 在跨 CPU exec 场景下的 slab use-after-free;这是一次最小止血,长期看会被 Tim Chen 把 sc_stat 从 mm/sched_group 解耦的系列所替代。