0/1 已展开

LLM 分析

sched/debug:给 scan_size_mb debugfs 旋钮加上写入校验

系列概况

  • 标题[PATCH v2] sched/debug: Validate writes to the scan_size_mb debugfs knob
  • 作者:Zhan Xusheng <zhanxusheng@xiaomi.com>
  • 版本:v2(单 patch,v1 → v2 仅改解析进制与 changelog 用语)
  • 规模:1 个 patch,1 个文件,50 行新增 / 1 行删除
  • 修改文件kernel/sched/debug.c
  • 代码统计+50 / -1
  • Message-ID20260810142050.3828587-1-zhanxusheng@xiaomi.com
  • 完整性:含 Fixes tag、Signed-off-by、Reviewed-by、v2 changelog、Link to v1,无缺项

补丁目的

scan_size_mb 是 NUMA balancing 在 debugfs 下暴露的调节旋钮,写入值会作为除数被 task_scan_min()task_nr_scan_windows() 使用。两个特定数值会让内核在普通 clone() / fork() 路径上 Oops:

  1. 写入 0MAX_SCAN_WINDOW / 0 直接除零。
  2. 写入 2^24(4K 页):MB_TO_PAGES()unsigned int 左移 24 位回绕成 0,再次除零。

把旋钮从原先 sysctl 迁到 debugfs 时丢了 .extra1 = SYSCTL_ONE 下界校验,上界也从未存在。该 patch 用自定义 write handler 替换裸的 debugfs_create_u32(),写入时校验 0 < mb <= NUMA_SCAN_SIZE_MB_MAX,越界返回 -EINVAL

旧流程的问题

旧实现一行 debugfs_create_u32() 直接把 sysctl_numa_balancing_scan_size 暴露给用户态,无任何边界检查:

echo 0 > scan_size_mb
        |
        v
sysctl_numa_balancing_scan_size = 0
        |
        v
fork/clone -> task_scan_min()
        |
        v
MAX_SCAN_WINDOW / 0
        |
        v
divide error -> kernel Oops

echo 16777216 > scan_size_mb   (4K page)
        |
        v
MB_TO_PAGES(mb) : mb << (20 - PAGE_SHIFT)
        |
        v
unsigned int shift wraps to 0
        |
        v
task_nr_scan_windows() / 0
        |
        v
divide error -> kernel Oops

两条路径都从 init_numa_balancing()__sched_fork() / copy_process() 进入,普通 clone() 即可触发。

新流程

echo <value> > scan_size_mb
        |
        v
sched_numa_scan_size_write()
        |
        v
kstrtouint_from_user(buf, cnt, 0, &mb)
        |     (base=0 -> decimal or hex)
        v
+--------------------------------------------+
|  mb == 0                  -> return -EINVAL  (lower bound)
|  mb > NUMA_SCAN_SIZE_MB_MAX -> return -EINVAL (upper bound)
|  else: store to global and return cnt
+--------------------------------------------+
        |
        v
sysctl_numa_balancing_scan_size = mb   (safe value)

NUMA_SCAN_SIZE_MB_MAX = UINT_MAX >> (20 - PAGE_SHIFT) 取「左移后仍非零」的最大 MB 数,从源头封住 MB_TO_PAGES() 的回绕窗口。

Patch 概览

  • #ifdef CONFIG_NUMA_BALANCING 内新增 sched_numa_scan_size_write/_show/_opensched_numa_scan_size_fops
  • 写 handler 使用 kstrtouint_from_user(ubuf, cnt, 0, &mb) 解析;base=0 保留 simple_attr_write_xsigned() 原先允许的十六进制写入(v2 由 Chen Yu 指出后调整)。
  • 校验通过后写回 sysctl_numa_balancing_scan_size*ppos += cnt; return cnt; 与 simple_attr 约定一致。
  • 注册节点从 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),仅此一行。
  • 旋钮已在 Documentation/scheduler/sched-debug.rst 中有文档,注释强调「-EINVAL 比悄悄存入下次 clone 即 panic 的值更安全」。

关键实现

#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;
}

static int sched_numa_scan_size_show(struct seq_file *m, void *v)
{
    seq_printf(m, "%u\n", sysctl_numa_balancing_scan_size);
    return 0;
}

static int sched_numa_scan_size_open(struct inode *inode, struct file *filp)
{
    return single_open(filp, sched_numa_scan_size_show, NULL);
}

static const struct file_operations sched_numa_scan_size_fops = {
    .open    = sched_numa_scan_size_open,
    .write   = sched_numa_scan_size_write,
    .read    = seq_read,
    .llseek  = seq_lseek,
    .release = single_release,
};

要点:

  • !mb 拦截下界,对应 task_scan_min() 的除零。
  • mb > NUMA_SCAN_SIZE_MB_MAX 拦截上界,对应 MB_TO_PAGES() 的 32 位回绕。
  • base=0kstrtouint_from_user 同时识别 "0x10""16",与旧 simple_attr 行为兼容。
  • *ppos += cnt; return cnt; 是 simple_attr 的标准约定,避免 llseek 后短写 / 重写状态错乱。

类比

scan_size_mb 想象成自助餐厅入口的「每人取餐份数」告示牌。旧版把粉笔直接交给顾客,写 0(「谁也不许取」)或「天文数字」(超过厨房容量)时,后厨阿姨(task_scan_min)要算「总量 ÷ 份数」,分母为 0 或绕回 0 都会当场崩溃。新版则在牌子前装了电子闸机:写 0、超大数都直接红灯拒绝放行,只有合理区间才更新到总规则里,下一位顾客来打饭才安全。

Highlight:风险与注意点

  • debugfs 节点权限偏宽scan_size_mb 默认 0644,具备挂载点写权限的用户即可让系统 Oops;发行版应评估是否收紧为 root-only 或要求 CAP_SYS_ADMIN
  • 上界依赖编译时常量NUMA_SCAN_SIZE_MB_MAX 假设 MB_TO_PAGES() 仍是 32 位位移;若未来改为 64 位或换公式,上界需重新推导。
  • 同文件其它旋钮未加固scan_delay_ms / scan_period_min_ms / scan_period_max_ms / hot_threshold_ms 仍是裸 debugfs_create_u32,其中含除法或饱和语义的字段理论上存在同类风险,建议按相同模式补 write handler。
  • stable backport 一致性Fixes 指向 8a99b6833c88,回 stable 时需与整个「SCHED_DEBUG sysctl 迁 debugfs」系列同步评估,避免在 stable 分支上重新暴露旧 sysctl 节点。
  • 回归覆盖有限:作者仅在 2 节点 qemu 客机上验证 0、2^243*2^24512;社区宜追加 LTP debugfs 模糊测试,覆盖 UINT_MAX2^24-12^24+1 等边界。

版本变化

  • v1 → v2kstrtouint_from_userbase10 改为 0,让十六进制写入继续生效,匹配 simple_attr_write_xsigned()kstrtoull(attr->set_buf, 0, &val) 的行为(Chen Yu 指出);changelog 中精确点出了对应 helper 名称。

一句话总结

把 NUMA balancing 的 debugfs 旋钮 scan_size_mb 从裸 debugfs_create_u32 换成带 write handler 的 file,并在写入时校验 0 < mb <= UINT_MAX >> (20 - PAGE_SHIFT),封住 task_scan_mintask_nr_scan_windows 两处除零 panic。