sched discussion
[PATCH] sched/cache: Fix use-after-free of the mm replaced by exec
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.c、include/linux/sched.h、kernel/events/core.c、kernel/sched/fair.c - 代码统计:+23 / -0
- Message-ID(首封):
apPb-Dr4nPYuHQOK@v4bel - 完整性:含 KASAN Call Trace、Freed-by 调用栈、
Fixes:tag、Cc: stable、Signed-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.h | 在 struct sched_cache_stat 后声明 sched_cache_exec_done();!CONFIG_SCHED_CACHE 时提供空 stub |
fs/exec.c | exec_mm_put_old() 内追加 sched_cache_exec_done() 调用点 |
kernel/events/core.c | attach_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(¤t->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 即可保证:
- 任何已经在旧 mm 上的 reader 会先完成(被 mutex 等待);
- 之后任何取锁的 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_stat与sched_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 解耦的系列所替代。