sched discussion
[PATCH] sched/numa: Prevent race on sysctl_numa_balancing static key
LLM 分析
sched/numa:防止 sysctl_numa_balancing 静态键上的竞态
系列概况
- 标题:[PATCH] sched/numa: Prevent race on sysctl_numa_balancing static key
- 作者:Chen Jinghuang chenjinghuang2@huawei.com
- 版本:v1(单 patch,非系列;经过 2 轮 review 修订)
- 规模:1 个文件,4 行新增
- 修改文件:
kernel/sched/core.c - 代码统计:4 insertions(+), 0 deletions(-)
- Message-ID:
20260803123046.297426-1-chenjinghuang2@huawei.com - 完整性:完整。共 5 封邮件(1 patch + 4 回复),作者与 reviewer Prateek Nayak(AMD)两轮交互,最终把锁位置与写法都修订了,作者在末封承诺将发布 v2。
补丁目的
/proc/sys/kernel/numa_balancing 允许用户态并发写 0/1 切换 NUMA balancing。该 sysctl 直接走 __set_numabalancing_state(),最终调用 static_key_enable_cpuslocked() / static_key_disable_cpuslocked()。
jump_label 子系统有一个内部约定:enable 路径在持有 jump_label_lock 期间先把 key->enabled 临时写为 -1,等 jump_label_update() 收尾后再恢复为 1;而 disable 路径在加 jump_label_lock 之前就读 enabled。两个写并发时,disable 可能读到 -1,触发 WARN_ON_ONCE(enabled != 0),正是 syzkaller 报告的告警。
补丁目标:在 sysctl 写路径上串行化 enable/disable 调用,使中间状态 -1 不再泄露给并发 disable。
旧流程的问题
write 1 (enable) write 0 (disable)
---------------------------------- ----------------------------------
static_key_enable_cpuslocked static_key_disable_cpuslocked
jump_label_lock()
atomic_set(enabled, -1) <----+
| atomic_read(enabled) <- reads -1
jump_label_update() |
WARN_ON_ONCE(enabled != 0) <--+ <-- actual WARN site
atomic_set_release(enabled, 1)
jump_label_unlock() (disable misfires before checking)
根本原因是:enable 内部需要把 key->enabled 暂时置 -1,但这个窗口必须对外部观察者不可见;只要 disable 与 enable 并发,disable 就会从 -1 推断出错。
新流程
把锁收到 sysctl 的 write 分支里(避开早期 init 无竞争路径),并用 scoped guard 让锁随作用域自动释放。
sysctl write 0/1 (userspace)
|
v
guard(mutex)(&numabalancing_mutex)
|
v
sysctl_numa_balancing_mode = state
|
v
__set_numabalancing_state(state)
|- static_key_enable_cpuslocked --+
|- static_key_disable_cpuslocked +-- mutually exclusive
--+
|
v
(guard exits -> mutex released)
下面给出静态键的内部状态机,以及新锁是如何把两个状态机串行化的:
static key state machine (single thread view)
(idle) enable_cpuslocked update done
| +-----> [transient: -1] ----+
| | |
| | v
[enabled:1] <--------+ [enabled:1]
| ^
| disable_cpuslocked |
+------> [disabled:0] |
--+
Without the mutex, a concurrent "disable" peeks at
"enabled" BEFORE taking jump_label_lock and sees -1.
关键实现
v1(作者首发)的最小形态:
static DEFINE_MUTEX(numabalancing_mutex);
void set_numabalancing_state(bool enabled)
{
mutex_lock(&numabalancing_mutex);
...
__set_numabalancing_state(enabled);
mutex_unlock(&numabalancing_mutex);
}
review 后(消息 3)的修订:锁从 set_numabalancing_state() 撤回 sysctl_numa_balancing() 的 if (write) 分支,因为早期 init 阶段 check_numabalancing_enable() 单独调用,没有并发,不需要加锁。
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)
{
...
if (write) {
mutex_lock(&numabalancing_mutex);
if (!(sysctl_numa_balancing_mode & NUMA_BALANCING_MEMORY_TIERING) &&
(state & NUMA_BALANCING_MEMORY_TIERING))
reset_memory_tiering();
sysctl_numa_balancing_mode = state;
__set_numabalancing_state(state);
mutex_unlock(&numabalancing_mutex);
}
return err;
}
review 进一步收敛(消息 4-5):用 guard(mutex)(&numabalancing_mutex); 替换显式 lock/unlock,依赖 cleanup.h 的作用域 guard,函数提前 return 也会自动解锁。
作者在 commit message 里点出了两条已有的同模式参考实现:kernel/time/timer.c 中的 timer_key_mutex 守护 timers_update_migration(),kernel/events/core.c 中的 perf_sched_mutex 守护 static_branch_enable() —— 都是“在静态键切换点加锁串行化”的成熟写法。
类比
把 key->enabled = -1 想象成电梯检修时挂出来的临时检修牌:检修工(enable)必须先把牌子挂出来(设为 -1)才能换线路(jump_label_update),换好再摘下(恢复 1)。如果另一位检修工(disable)不等牌子摘下就冲进电梯口检查状态,就会以为出了故障,按下警报铃。
numabalancing_mutex 就像电梯口的门禁:两个检修工必须排队刷门禁卡,轮到的那位挂好牌子、换好线路再交还通行权,外部观察者再也看不到那块中间状态的牌子。
guard(mutex) 像自动门:进门就锁上,出门自动开,不用再操心忘带钥匙(忘 unlock)。
Highlight:风险与注意点
- 锁位置决定正确性:把锁放在
set_numabalancing_state()会把早期 init 路径(无竞争)也加锁,浪费且可能引入 init 顺序问题;reviewer 的核心意见就是“锁只覆盖真正并发的 sysfs 写”。这是该 patch 最容易做错的地方。 - 不要换 spinlock:sysctl 写路径处于进程上下文,普通
mutex即可;错误升级为 spinlock 会带来不可睡眠等隐性约束。 - jump_label 协议本身未变:补丁只是把“看到中间状态 -1”的窗口挡住,并未修复 jump_label 子系统对 enable/disable 并发的根因;任何其他
static_key_enable/disable共存点仍需各自加锁。 guard(mutex)的作用域:使用guard()时要确认__set_numabalancing_state()不会触发可能跳出的回调;目前路径上reset_memory_tiering()是同步函数,安全。- 回归验证:建议用 syzkaller 重跑原 race 复现,或人为并发
echo 1 > numa_balancing & echo 0 > numa_balancing &触发旧告警,确认 WARN 消失;同时确认 NUMA balancing 正常 enable/disable 路径上的 perf 没有可观察的回归。
版本变化
- v1:在
set_numabalancing_state()内加DEFINE_MUTEX(numabalancing_mutex)与 lock/unlock,4 行。 - review-1(消息 3,inline diff):把锁从
set_numabalancing_state()撤回sysctl_numa_balancing()的if (write)分支,避开早期 init 路径。 - review-2(消息 4-5):把
mutex_lock/unlock替换为guard(mutex)(&numabalancing_mutex);,用 cleanup.h 的 scoped guard 自动释放。 - v1 之后没有新 patch 文件发到列表,作者在末封承诺“会相应更新 patch”,即下一版以 review-2 的形态发布。
一句话总结
通过在 sysctl 写路径上加 numabalancing_mutex(最终改用 guard(mutex))把 sched_numa_balancing 静态键的并发 enable/disable 串行化,遮住 jump_label 中间状态 -1 的观察窗口,消除 syzkaller 触发的 WARN_ON_ONCE。