0/5 已展开

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-ID20260803123046.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