0/3 已展开

LLM 分析

sched/debug:拒绝 numa_balancing scan_size_mb 的 0 写入

系列概况

  • 标题:[PATCH] sched/debug: reject zero writes to numa_balancing scan_size_mb
  • 作者:Li RongQing lirongqing@baidu.com
  • 版本:单封 PATCH(作者在 thread 中已承诺会按评审意见发 v2)
  • 规模:1 个文件,+22/-1 行
  • 修改文件:kernel/sched/debug.c
  • Message-ID20260714053045.2177-1-lirongqing@baidu.com
  • 完整性:完整(diff、commit message、Fixes、Signed-off-by 齐全)
  • 关联回复:2 封(Xusheng 评审 + 作者确认)

补丁目的

sysctl_numa_balancing_scan_size 此前通过 debugfs_create_u32() 注册,对任何
u32 值都不做校验,包括 0。一旦 scan_size_mb == 0,下游 NUMA 扫描代码会
落入两处除零或未定义行为:

  1. task_scan_min()windows = MAX_SCAN_WINDOW / scan_size
  2. task_nr_scan_windows():先 round_up(rss, nr_scan_pages)(0 模长语义未定义),
    rss / nr_scan_pages(结果为 0 时再次除零)

修复目标:把 scan_size_mb 这一项从裸 debugfs_create_u32() 换成带自定义
get/setfile_operations,在写入阶段就拒绝 0 与越界值,绝不让0 进入底层变量。

旧流程的问题

  user: echo 0 > .../sched/numa/scan_size_mb
                |
                v
  debugfs_create_u32 -> direct store to sysctl_numa_balancing_scan_size
                |
                v
  task_scan_min(): windows = MAX_SCAN_WINDOW / 0    [divide by zero]
  task_nr_scan_windows(): rss / 0                   [oops / UB]

风险点:

  • debugfs_create_u32() 不做任何范围校验,等价于"开闸泄洪"。
  • scan_size_mb 既是 KB 数又是后续 nr_scan_pages = scan_size << PAGE_SHIFT
    的换算基数,0 会让 nr_scan_pages 也是 0,触发后续 / nr_scan_pages
  • 仅在 CONFIG_NUMA_BALANCING 启用时才生效,但一旦启用,写入路径就完全无防护。

新流程

  user: echo 0 > .../sched/numa/scan_size_mb
                |
                v
  debugfs_create_file(..., &numa_scan_size_fops)
                |
                v
  numa_scan_size_set():
    if (val == 0 || val > UINT_MAX) return -ERANGE   [rejected]
    *(u32 *)data = (u32)val;                         [stored]
                |
                v
  task_scan_min / task_nr_scan_windows always see positive divisor

关键实现

新增文件操作只在 CONFIG_NUMA_BALANCING 段内:

static int numa_scan_size_get(void *data, u64 *val)
{
    *val = *(u32 *)data;
    return 0;
}

static int numa_scan_size_set(void *data, u64 val)
{
    if (val == 0 || val > UINT_MAX)
        return -ERANGE;

    *(u32 *)data = (u32)val;
    return 0;
}

DEFINE_DEBUGFS_ATTRIBUTE(numa_scan_size_fops,
                         numa_scan_size_get,
                         numa_scan_size_set,
 "%llu\n");

注册侧把原先:

debugfs_create_u32("scan_size_mb", 0644, numa,
                   &sysctl_numa_balancing_scan_size);

替换为:

debugfs_create_file("scan_size_mb", 0644, numa,
                    &sysctl_numa_balancing_scan_size,
                    &numa_scan_size_fops);

Fixes 指到 commit 8a99b6833c88("sched: Move SCHED_DEBUG sysctl to debugfs"),
说明该回归是 sysctl → debugfs 迁移时丢掉了范围校验。

类比

scan_size_mb 想象成水压表:旧的 debugfs_create_u32 像只显示数值的水压表,
写 0 等于把表头拆掉、还把管子接到空压头,下游管道一冲就炸。新的 file_operations
相当于在水压表和主管道之间加了一个安全阀:读数能更新,但 0 或异常值会被自动拒绝,
主管道始终有最低安全水压。DEFINE_DEBUGFS_ATTRIBUTE 是阀门自带的锁
debugfs_file_get/put),再用 debugfs_create_file() 套一层 full_proxy
就重复了——Xusheng 指出应该换成 debugfs_create_file_unsafe(),跟同文件
verbose 旋钮的接法保持一致。

Highlight:风险与注意点

  • changelog 与代码不一致:commit message 写的是 -EINVAL,但实现返回的是
    -ERANGE。Xusheng 表示 -ERANGE 没问题,v2 需要把 commit body 改成 -ERANGE
  • debugfs_create_file() vs debugfs_create_file_unsafe()
    DEFINE_DEBUGFS_ATTRIBUTE 已经通过 debugfs_attr_read/writedebugfs_file_get/put 的移除保护,再叠 debugfs_create_file()full_proxy
    属于冗余代理。同文件的 verbose 已经在用 unsafe 写法,v2 应保持一致风格。
  • 范围边界:在 u64 → u32 路径里 val > UINT_MAX 实际上永远走不到
    (u32)val 会截断),真正有效拦截只有 val == 0。可考虑简化为只检查 0,
    或者把 > UINT_MAX 改成 > U32_MAX 让语义更准确。
  • 并发保护缺口numa_scan_size_set 只是裸写,没有锁。NUMA balancer读取路径
    通常不需要严格同步,但若后续让该值影响运行期策略,需要补读侧屏障或 RCU。
  • 回归源头8a99b6833c88 把 sysctl 迁到 debugfs 时把原本的范围校验降级成了
    debugfs_create_u32,这是引入除零的根因,Fixes tag 指对了方向。

版本变化

  • v1 → 待发 v2(作者在 thread 中承诺):
    1. 注册函数由 debugfs_create_file() 改为 debugfs_create_file_unsafe()
    2. commit message 中的错误码由 -EINVAL 改为 -ERANGE

一句话总结

scan_size_mb debugfs 项换上一个带 set 钩子的 file_operations,把 0 写入挡在门外,
从而消除 task_scan_min() / task_nr_scan_windows() 的除零;v2 还会按评审意见把
debugfs_create_file 换成 debugfs_create_file_unsafe,并修掉 changelog 与代码错误码
不一致的小毛病。