sched discussion
[PATCH v2 RESEND] sched/debug: Validate writes to the scan_size_mb debugfs knob
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-ID:
20260905085005.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:
- 写
0→task_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
新流程
- 把
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)。 - 新增
sched_numa_scan_size_write():用kstrtouint_from_user()解析写入串为unsigned int;
若为0或超过NUMA_SCAN_SIZE_MB_MAX,返回-EINVAL;否则更新
sysctl_numa_balancing_scan_size。 - 引入上限
NUMA_SCAN_SIZE_MB_MAX = UINT_MAX >> (20 - PAGE_SHIFT),保证
MB_TO_PAGES()的左移不会回绕到 0。 - 新增
sched_numa_scan_size_show(),通过single_open暴露当前值,保持
cat行为不变。 - 整套读写
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 已有的设计套路。