sched discussion
[PATCH v2] sched/numa: Prevent race on sysctl_numa_balancing static key
LLM 分析
sched/numa: 防止 sysctl_numa_balancing 静态键的并发竞争
系列概况
- 标题: [PATCH v2] sched/numa: Prevent race on sysctl_numa_balancing static key
- 作者: Chen Jinghuang chenjinghuang2@huawei.com
- 版本: v2(单 patch)
- 规模: 1 个文件,+3 行
- 修改文件: kernel/sched/core.c
- 代码统计: 3 insertions(+), 0 deletions(-)
- Message-ID: 20260804085320.570021-1-chenjinghuang2@huawei.com
- 完整性: 仅 1 封邮件(v2 patch),无后续回复
补丁目的
在 syzkaller 模糊测试中发现:两个线程同时向 /proc/sys/kernel/numa_balancing 写 1 和 0,会让 static_key 内部的 enabled 字段短暂变成 -1(enable 路径正在切换的哨兵值),而 disable 路径在拿锁之前提前做 atomic_read(&key->enabled) 检查,撞上 -1 后触发 WARN_ON_ONCE(enabled != 0)。
本 patch 在 sysctl 写路径上增加一把 numabalancing_mutex,把 enable/disable 这一对切换串行化,让 disable 路径在锁外永远不会读到 -1,从而消除误报。
旧流程的问题
sysctl_numa_balancing() 在 write 分支直接调用 static_key_enable_cpuslocked() 或 static_key_disable_cpuslocked()。两者各自只通过 jump_label_lock 串行化对静态键的最终修改,但 disable 路径会在拿锁之前先做 atomic_read(&key->enabled) 快读,用作"如果已经禁用就跳过整个流程"的优化。这给并发写留下窗口:
Thread A (write 1) Thread B (write 0)
----------------- -----------------
static_key_enable_cpuslocked() static_key_disable_cpuslocked()
jump_label_lock() atomic_read(&key->enabled) -> -1
atomic_set(&key->enabled, -1) jump_label_lock() -> blocked
jump_label_update() -> unblocks
atomic_set_release(...enabled,1) jump_label_lock() acquired
jump_label_unlock() WARN_ON_ONCE(enabled != 0) -> FIRES
atomic_set(&key->enabled, 0)
jump_label_unlock()
enabled == -1 不是 bug,而是 static_key 内部"正在切换"的合法暂态;但它不应被调用方直接观察到。
新流程
在 sysctl_numa_balancing() 的 write 分支前面用 guard(mutex)(&numabalancing_mutex) 把 enable/disable 串行化,DEFINE_MUTEX 静态定义在 reset_memory_tiering() 上方,初始化期间不参与。
Thread A (write 1) Thread B (write 0)
----------------- -----------------
guard(mutex) acquired
static_key_enable_cpuslocked()
jump_label_lock()
atomic_set(&key->enabled, -1)
jump_label_update()
atomic_set_release(...enabled, 1)
jump_label_unlock()
guard(mutex) released guard(mutex) blocks --+
| wait
guard(mutex) acquired ---+
static_key_disable_cpuslocked()
atomic_read(&key->enabled) -> 1
jump_label_lock()
atomic_set(&key->enabled, 0)
jump_label_unlock()
guard(mutex) released
disable 路径下锁外的 atomic_read 永远只能读到 1 或 0,WARN_ON_ONCE 不再触发。
关键实现
static DEFINE_MUTEX(numabalancing_mutex);
static int sysctl_numa_balancing(const struct ctl_table *table, int write,
void *buffer, size_t *lenp, loff_t *ppos)
{
int err;
if (err < 0)
return err;
if (write) {
guard(mutex)(&numabalancing_mutex);
...
if (!(sysctl_numa_balancing_mode & NUMA_BALANCING_MEMORY_TIERING) &&
(state & NUMA_BALANCING_MEMORY_TIERING))
reset_memory_tiering();
...
}
...
}
要点:
DEFINE_MUTEX紧贴reset_memory_tiering(),因为两者本就协同处理NUMA_BALANCING_MEMORY_TIERING状态切换,放一起更易读。guard(mutex)是 scoped guard,异常路径与正常返回路径都会自动释放锁,比显式mutex_lock/unlock更安全也更短。- mutex 只在
write路径生效;读取/proc/sys/kernel/numa_balancing走的是static_key自身的 fast path,不会触碰enabled,无需保护。
类比
把 static_key 想成办公楼一楼大堂的旋转门。物业原本允许两个员工同时去推门:
- 员工 A 要把门调成"通行":进入旋转区 -> 标记"正在调整"(
enabled = -1)-> 调整 -> 标记"通行"(enabled = 1)。 - 员工 B 要把门调成"禁止":在 A 还在调整时远远看一眼门,看到"正在调整"(
-1),以为系统出错,拉响警报(WARN_ON_ONCE)。
修复方法:在控制室加一把钥匙(numabalancing_mutex)。任何一次状态切换都要先取钥匙;员工 B 拿不到钥匙就老老实实在门外等,等 A 把"通行"标识挂好才动手,于是他看到的状态总是 1 或 0,警报不会响。
Highlight:风险与注意点
- 角落处的同类隐患:其他走
static_key_*_cpuslocked的 sysctl 切换(例如部分 cgroup 控制器、timer migration 的 sysctl)若在锁外读enabled,理论上有同款竞态,值得 grep 一遍static_key_disable_cpuslocked的调用点。 -1哨兵不是错误:这是static_key内部"正切换中"的合法状态;调用方应只信任锁内的最终值,锁外的 fast read 应当只观察到 0/1。Fixestag 指向 1dbb6704de91("jump_label: Fix concurrent static_key_enable/disable()"),说明同样的设计意图已经在底层被强化过。- 为什么不抽通用 helper:因为每次调用语义略有差别(有的同时改 sysctl 状态、有的还配套动作),内核里类似 case 选择小型本地 mutex 而非全局 helper,
timer_key_mutex(kernel/time/timer.c)和perf_sched_mutex(kernel/events/core.c)都是同款模式。 - 是否覆盖读路径:本 patch 只锁写路径,理由充分——读路径走
static_key自身的 fast path,不写enabled。 - 测试覆盖:仅靠 syzkaller 触发说明该 bug 实际可达;维护者应在合入后跑一下高频
echo 1 > numa_balancing; echo 0 > numa_balancing来验证告警消失。
版本变化
- v1 -> v2:
- 把
numabalancing_mutex作用范围从函数整体收窄到write分支,初始化阶段不再无谓取锁。 mutex_lock()/mutex_unlock()改为guard(mutex),更安全也更短。
- 把
一句话总结
/proc/sys/kernel/numa_balancing 的并发 1/0 写会让 static_key 内部 enabled 短暂变成 -1,被 disable 路径锁外读取触发 WARN_ON_ONCE;本 patch 在 sysctl 写路径加 numabalancing_mutex 串行化 enable/disable,消除告警。