sched discussion
[PATCH] sched/debug: Validate writes to the scan_size_mb debugfs knob
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 的写入:
- 写入
0→task_scan_min()把scan_size当除数,直接除零; - 写入 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 |
|---|---|---|---|
0 | windows = MAX_SCAN_WINDOW / scan_size | init_numa_balancing() 中的 task_scan_max() | task_scan_max+0x30/0x1a0 |
16777216 | MB_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_mb 由 debugfs_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_xsigned用kstrtoull(..., 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。