0/1 已展开

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。Fixes tag 指向 1dbb6704de91("jump_label: Fix concurrent static_key_enable/disable()"),说明同样的设计意图已经在底层被强化过。
  • 为什么不抽通用 helper:因为每次调用语义略有差别(有的同时改 sysctl 状态、有的还配套动作),内核里类似 case 选择小型本地 mutex 而非全局 helper,timer_key_mutexkernel/time/timer.c)和 perf_sched_mutexkernel/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,消除告警。