0/1 已展开

LLM 分析

sched/debug:给 scan_size_mb debugfs 节点补写入校验

系列概况

  • 标题:[PATCH v2 RESEND] sched/debug: Validate writes to the scan_size_mb debugfs knob
  • 作者:Zhan Xusheng zhanxusheng@xiaomi.com
  • 版本:v2 RESEND,无功能变更,仅重发
  • 规模:1 个文件改动,+50 / -1
  • 修改文件kernel/sched/debug.c
  • 代码统计:插入 50 行,删除 1 行
  • Message-ID20260905085005.1290752-1-zhanxusheng@xiaomi.com
  • 完整性:单封邮件,含完整 diff、Fixes、签名行与复现步骤;未观察到 maintainer 回复

补丁目的

debugfs 节点 scan_size_mb(位于
/sys/kernel/debug/sched/numa_balancing/scan_size_mb)原本用
debugfs_create_u32() 注册,任何 u32 值都会被原样写入底层变量
sysctl_numa_balancing_scan_size,但这个值最终在 NUMA balancing 的两个计算
路径里被当作除数使用:

  • task_scan_min() 通过 MAX_SCAN_WINDOW / scan_size 计算窗口下限;
  • task_nr_scan_windows() 把 MB 转页数后用作除数。

当用户写入 0 或某些极大值(4K 页下任何 2^24 的倍数)时,除数变成 0,触发内核 oops:

  • 0task_scan_min()MAX_SCAN_WINDOW / scan_size 除零;
  • 16777216(= 2^24)→ MB_TO_PAGES()unsigned int 左移后回绕为 0,task_nr_scan_windows() 除零。

补丁把这些异常值挡在 debugfs 写入路径之外,避免根因。

旧流程的问题

scan_size_mb 之前是 sysctl,通过 .extra1 = SYSCTL_ONE 强制 ≥ 1。2018 年 commit
8a99b6833c88 ("sched: Move SCHED_DEBUG sysctl to debugfs") 把它迁到 debugfs,
改用 debugfs_create_u32() 注册,丢失了下界校验;上界也从未被强制过。
同一时期 tunable_scaling 改用 sched_scaling_fops,自带范围检查。

复现命令:

# echo 0       > /sys/kernel/debug/sched/numa_balancing/scan_size_mb
  Oops: divide error: 0000 [#1] SMP PTI
  RIP: 0010:task_scan_max+0x30/0x1a0
  Call Trace:
   init_numa_balancing+0xe0/0x200
   __sched_fork+0x13b/0x180
   sched_fork+0x12/0x1d0
   copy_process+0xdea/0x2370
   kernel_clone+0xd6/0x4a0

task_scan_min() 被内联进 task_scan_max();触发的是 CLONE_VM 子进程,因为
init_numa_balancing() 在新地址空间这次调用前已经返回。

# echo 16777216 > /sys/kernel/debug/sched/numa_balancing/scan_size_mb
  RIP: 0010:task_nr_scan_windows.isra.0+0x5c/0x70

新流程

  1. debugfs_create_u32("scan_size_mb", 0644, numa, &sysctl_numa_balancing_scan_size)
    替换为 debugfs_create_file("scan_size_mb", 0644, numa, NULL, &sched_numa_scan_size_fops)
  2. 新增 sched_numa_scan_size_write():用 kstrtouint_from_user() 解析写入串为 unsigned int
    若为 0 或超过 NUMA_SCAN_SIZE_MB_MAX,返回 -EINVAL;否则更新
    sysctl_numa_balancing_scan_size
  3. 引入上限 NUMA_SCAN_SIZE_MB_MAX = UINT_MAX >> (20 - PAGE_SHIFT),保证
    MB_TO_PAGES() 的左移不会回绕到 0。
  4. 新增 sched_numa_scan_size_show(),通过 single_open 暴露当前值,保持
    cat 行为不变。
  5. 整套读写 file_operations 仿照 sched_scaling_fops,并限定在 CONFIG_NUMA_BALANCING 下。

关键实现

#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, 0, &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;
}

注册处改为:

debugfs_create_file("scan_size_mb", 0644, numa, NULL,
                    &sched_numa_scan_size_fops);

上界推导:MB_TO_PAGES(mb) = mb << (20 - PAGE_SHIFT),对 unsigned int 左移
(20 - PAGE_SHIFT) 位若 ≥ 32 会回绕到 0,所以允许的最大 mb 就是
UINT_MAX >> (20 - PAGE_SHIFT)。4K 页下 (20 - 12) = 8,最大 ≈ 16777215,
正好让 16777216 在 MB_TO_PAGES() 中回绕为 0。

+-----------+      +----------------------+      +----------------------+
| user echo | ---> | kstrtouint_from_user | ---> | range check          |
| "0" or    |      | +cnt, 0, &mb         |      | 0 < mb <= MB_MAX     |
| "16777216"|      +----------------------+      +----------------------+
                                                       |
                          +----------------------------+----------------------+
                          |                                                   |
                          v                                                   v
                  +----------------+                                   +-------------+
                  | return -EINVAL |                                   | store mb    |
                  | (no panic)     |                                   | *ppos +=cnt |
                  +----------------+                                   +-------------+
                          ^
                          |
                  +-----------------+
                  | task_scan_min/  |
                  | task_nr_scan... |
                  | never see bad   |
                  | divisor         |
                  +-----------------+

类比

scan_size_mb 想成一个 公共水龙头:以前任何人都可以随手把它拧到 0
(停水)或拧到把管子撑爆的位置(爆管),后果是楼上楼下的住户(其它任务)一起
遭殃。补丁相当于在水龙头前装一个 带最小最大流量限制的阀门:低于最小
流量或高于最大流量的请求一律拒绝并报警(-EINVAL),中间正常区间放行。
sched_scaling_fops 就是这栋楼里已经装好阀门的样板间,本补丁把同一套思路
复制到了 scan_size_mb 这个水龙头上。

Highlight:风险与注意点

  • 校验只挡 0 与上界:仍然接受大但合法的值(例如 16777215),需要确认这
    类值在后续 task_scan_* 算法里不会让 NUMA 迁移窗口过大、造成不必要的
    page migration 抖动。
  • debugfs 一般仅 root 可写,但仍可作为本地 DoS 的入口;保留 -EINVAL
    路径很重要,既保护内核,也让上层脚本能据此判断失败原因。
  • show / open 与 write 之间没有锁sysctl_numa_balancing_scan_size
    是单字 unsigned int,并发读写依赖现有内存模型,不算新增竞争,但若后续
    改成 u64 需要重新评估。
  • v2 RESEND 期间未见 maintainer 回复,作者也提到 v7.3-rc 仍能复现。后续
    跟进点:是否已经合入 tip/sched/core 或 mainline。
  • commit 8a99b6833c88 把多个 sysctl 迁到 debugfs,除 scan_size_mb 外还
    scan_delay_ms / scan_period_min_ms / scan_period_max_ms /
    hot_threshold_ms。这些节点是否同样需要校验,可以一并审计(虽然它们未必
    当作除数,但仍然值得按 sysctl 时代 .extra1/.extra2 的语义过一遍)。

版本变化

  • v1 → v2:仅重发(RESEND),正文与 diff 完全一致,未引入新的 changelog。
  • 当前 mainline 与 tip/sched/core 上的 scan_size_mb 仍是普通
    debugfs_create_u32(),未合入本次校验。

一句话总结

scan_size_mb debugfs 节点从裸 u32 升级为带范围校验的 file_operations
NUMA_SCAN_SIZE_MB_MAX 拒绝 0 与会让 MB_TO_PAGES() 回绕为 0 的超大值,
从源头堵掉两个内核除零 oops,复用 sched_scaling_fops 已有的设计套路。