0/2 已展开

LLM 分析

sched/debug:为 scan_size_mb debugfs 写操作补边界校验

系列概况

  • 标题: [PATCH] sched/debug: Validate writes to the scan_size_mb debugfs knob
  • 作者: Zhan Xusheng zhanxusheng@xiaomi.com
  • 版本: v1,单补丁
  • 规模: 1 file changed, 50 insertions(+), 1 deletion(-)
  • 修改文件: kernel/sched/debug.c
  • 代码统计: 新增 50 行 / 删除 1 行;新增一组 file_operations 和 write handler
  • Message-ID: 20260810081829.3149958-1-zhanxusheng@xiaomi.com
  • 完整性: 含完整 diff、Fixes tag、Signed-off-by 与 2 节点 qemu 复现脚本;附 Chen Yu 的 Reviewed-by 回复

补丁目的

为 debugfs 节点 scan_size_mb 的写入口加边界校验,堵住两类会让内核 Oops 的写入:

  1. 写入 0task_scan_min()scan_size 当除数,直接除零;
  2. 写入 2^24 倍数(4K 页下即 16777216)→ MB_TO_PAGES()unsigned int 移位回绕到 0,task_nr_scan_windows() 再次除零。

回归根因是 commit 8a99b6833c88 把 SCHED_DEBUG sysctl 迁到 debugfs 时,原 sysctl 的 .extra1 = SYSCTL_ONE 约束被丢弃,而新路径换成 debugfs_create_u32(),相当于在 debugfs 入口放了一个没有闸门的开关。

旧流程的问题

旧 sysctl 入口受 sysctl 框架统一保护,写 0 时 .extra1 = SYSCTL_ONE 会返回 -EINVAL。debugfs 化后这套钩子全部失效,scan_size_mb 改用 debugfs_create_u32("scan_size_mb", 0644, numa, &sysctl_numa_balancing_scan_size),写入是直通赋值。

两类致命写入:

输入值计算路径触发点崩溃 RIP
0windows = MAX_SCAN_WINDOW / scan_sizeinit_numa_balancing() 中的 task_scan_max()task_scan_max+0x30/0x1a0
16777216MB_TO_PAGES(mb) << PAGE_SHIFT 回绕到 0后续扫描窗口计算task_nr_scan_windows.isra.0+0x5c/0x70

触发路径并不相同:零值是在 CLONE_VM 子线程的 __sched_fork -> init_numa_balancing() 阶段;回绕值是在稍后的扫描窗口重建阶段。

新流程

参照同文件已有的 sched_scaling_fops 模板,把 scan_size_mbdebugfs_create_u32() 替换成 debugfs_create_file(),新增 write handler 在赋值前做合法性校验:

   user: echo N > /sys/kernel/debug/sched/numa_balancing/scan_size_mb
                                |
                                v
                sched_numa_scan_size_write()
                                |
                                v
                kstrtouint_from_user(ubuf, cnt, 10, &mb)
                                |
                +---------------+---------------+
                |                               |
                v                               v
        mb == 0 or mb > MAX          valid range (1 <= mb <= MAX)
                |                               |
                v                               v
         return -EINVAL             sysctl_numa_balancing_scan_size = mb
                                            |
                                            v
                                  *ppos += cnt; return cnt;

校验逻辑:

#define NUMA_SCAN_SIZE_MB_MAX (UINT_MAX >> (20 - PAGE_SHIFT))

static ssize_t sched_numa_scan_size_write(struct file *filp,
                const char __user *ubuf, size_t cnt, loff_t *ppos)
{
    unsigned int mb;
    int ret;

    ret = kstrtouint_from_user(ubuf, cnt, 10, &mb);
    if (ret)
        return ret;

    if (!mb || mb > NUMA_SCAN_SIZE_MB_MAX)
        return -EINVAL;

    sysctl_numa_balancing_scan_size = mb;

    *ppos += cnt;
    return cnt;
}

NUMA_SCAN_SIZE_MB_MAX 选取的是 MB_TO_PAGES() 不会回绕到 0 的最大 MB 数:在 4K 页下等于 UINT_MAX >> (20 - 12) = 2^24 - 1,刚好把 16777216、3*16777216 这些值挡在外面。

Patch 概览

  • 新增 sched_numa_scan_size_open / _show / _write 一组回调;
  • 新增 sched_numa_scan_size_fops,复用 seq_read / seq_lseek / single_release
  • debugfs_create_u32("scan_size_mb", ...) 替换为 debugfs_create_file(..., &sched_numa_scan_size_fops)
  • 全部新增代码块用 #ifdef CONFIG_NUMA_BALANCING 包住,与原节点创建位置一致;
  • read 路径沿用 single_open + seq_printf("%u\n", ...),与原 u32 的 show 行为对齐。

关键实现

write handler 是补丁的核心,三件事:解析、校验、写入。read handler 只回显当前值,因此简单沿用 seq_file 模板。整块结构与文件上方已存在的 sched_scaling_fops 几乎一一对应:

   +----------------------------------------------------------+
   |   sysctl_numa_balancing_scan_size (global variable)      |
   +----------------------------------------------------------+
                  ^                                 ^
                  | write: assign after validation   | read: print current
                  |                                 |
   +--------------+----------------+   +-------------+-------------+
   | sched_numa_scan_size_write()   |   | sched_numa_scan_size_show()|
   | - kstrtouint_from_user()       |   | - seq_printf("%u\n", ...)  |
   | - reject 0 / out-of-range      |   |                            |
   | - advance *ppos on success     |   |                            |
   +--------------------------------+   +----------------------------+
                  ^                                 ^
                  +------- sched_numa_scan_size_fops --------+
                                       |
                                       v
                /sys/kernel/debug/sched/numa_balancing/scan_size_mb

类比

scan_size_mb 想象成电饭煲的煮饭量刻度旋钮。原来的 sysctl 相当于面板上有一根物理卡位"最少一杯米",旋钮转不到零;后来搬到数字面板时这条卡位被一起拆掉了,于是你可以拨到 0 杯(电饭煲立即报错),也可以拨到远超锅容量的刻度——内部把"杯数"换算成"页数"时发生整数溢出,结果还是报错。本补丁相当于把"至少 1 杯"和"最多 2^24-1 杯"两条数字卡位重新焊回面板,违规直接弹错、不替换默认值。

Highlight:风险与注意点

  • 基数与原行为不一致:Chen Yu 在 review 中指出 kstrtouint_from_user(..., 10, &mb) 应用 base 0,否则像 echo 0x100 > scan_size_mb 这种十六进制写法会被错误地返回 -EINVAL,而原 debugfs_create_u32() 是支持十六进制的(simple_attr_write_xsignedkstrtoull(..., 0, &val))。
  • 同类节点未审计:同区域里 scan_delay_ms / scan_period_min_ms / scan_period_max_ms / hot_threshold_ms 仍是裸 debugfs_create_u32(),理论上存在同样的回归面,需后续逐一排查。
  • 并发写未上锁:debugfs 默认没有锁,多线程并发写理论上存在竞态;但每次都是完整赋一个 unsigned int,对除零风险没有额外影响。
  • 修复范围依赖 Kconfig:整段新增代码包在 #ifdef CONFIG_NUMA_BALANCING 里,缺该选项的内核本来就不创建节点,因此补丁在 CONFIG_NUMA_BALANCING=n 时是空操作。
  • 触发条件已被官方文档化:该 knob 在 Documentation/scheduler/sched-debug.rst 中被列为控制扫描速率的文件之一,所以返回 -EINVAL 比静默截断到 1 更符合用户预期。

版本变化

  • v1(2026-08-10 上午):首次提交,单补丁,含完整 diff、Fixes tag、qemu 复现脚本。
  • 2026-08-10 晚(Chen Yu):回复建议把 base 10 改为 base 0 后给出 Reviewed-by: Chen Yu <yu.c.chen@intel.com>。尚未见到 v2。

一句话总结

为 debugfs 节点 scan_size_mb 的写入口补上 [1, NUMA_SCAN_SIZE_MB_MAX] 范围校验,避免 0 与 MB_TO_PAGES() 移位回绕两类值在 task_scan_min / task_nr_scan_windows 中触发除零内核 Oops。