sched discussion
[PATCH, Resend, v2] sched/debug: Reject invalid writes to numa_balancing scan_size_mb
LLM 分析
sched/debug:拒绝 numa_balancing scan_size_mb 的非法写入
系列概况
- 标题:
[PATCH, Resend, v2] sched/debug: Reject invalid writes to numa_balancing scan_size_mb - 作者:Li RongQing
<lirongqing@baidu.com> - 版本:v2(Resend,基于 v1 rebase 后重写 commit message 并调整 API 选择)
- 规模:1 file changed, 22 insertions(+), 1 deletion(-)
- 修改文件:
kernel/sched/debug.c - 代码统计:+22 / -1
- Message-ID:
20260822023313.1721-1-lirongqing@baidu.com - 完整性:单 patch,自带 commit message(含
Fixes:、Signed-off-by:)、diff、v1→v2 的 changelog,无邮件回复线程
补丁目的
NUMA balancing 的 scan_size_mb 参数以前是通过 debugfs_create_u32() 注册的,这条"原厂接口"对写入值是「来者不拒」的——任何 u32 都接受,包括 0,也包括超过 UINT_MAX 之后会被静默截断的"伪大数"。这两类输入都会让后续的 NUMA 扫描逻辑出问题:
- 写入
0会在task_scan_min()和task_nr_scan_windows()中触发除零。 - 写入超过
UINT_MAX的值,则在到达本 handler 之前就被 C 的隐式转换静默截断,可能写入与用户预期不符的u32。
本补丁要做的事是在 debugfs 那道闸门处加校验:自定义 file_operations,在 setter 里明确拒绝 0 和 val > UINT_MAX,统一返回 -ERANGE,让攻击面/误用面在边界就被挡掉。
旧流程的问题
user --write--> /sys/kernel/debug/sched/numa_balancing/scan_size_mb
|
v
debugfs_create_u32()
|
v
*(u32*) &sysctl_numa_balancing_scan_size = val
|
v
task_scan_min() | task_nr_scan_windows()
|
windows = MAX_SCAN_WINDOW / 0 --> division-by-zero
rss / nr_scan_pages (with nr_scan_pages from scan_size_mb = 0)
问题点:
- 没有任何上限/下限校验。
- 写入
0后真正调用除法时崩。 - 写入
> UINT_MAX时被截断,听不到任何反馈。 - 来自 commit
8a99b6833c88("sched: Move SCHED_DEBUG sysctl to debugfs"),把 sysctl 搬迁到 debugfs 时无意中放大了输入风险。
新流程
user --write val--> /sys/kernel/debug/sched/numa_balancing/scan_size_mb
|
v
debugfs_create_file_unsafe()
|
v
numa_scan_size_set(data, val)
|
+-----------+-----------+
| val == 0 ? val > UINT_MAX ?
| yes yes
+-------+-------+
|
v
return -ERANGE <-- 写入被拒绝
|
no
v
*(u32*)data = (u32)val; return 0;
关键点:用 DEFINE_DEBUGFS_ATTRIBUTE(numa_scan_size_fops, ...) 一次性把 getter/setter 包成 fops;注册时用 debugfs_create_file_unsafe() 而不是 debugfs_create_file(),避免再多套一层 full_proxy——因为这个 ops 内部已经借助 DEFINE_DEBUGFS_ATTRIBUTE 提供了基于 debugfs_file_get()/put() 的引用计数保护,无需重复。
关键实现
#ifdef CONFIG_NUMA_BALANCING
static int numa_scan_size_get(void *data, u64 *val)
{
*val = *(u32 *)data;
return 0;
}
static int numa_scan_size_set(void *data, u64 val)
{
if (val == 0 || val > UINT_MAX)
return -ERANGE;
*(u32 *)data = (u32)val;
return 0;
}
DEFINE_DEBUGFS_ATTRIBUTE(numa_scan_size_fops, numa_scan_size_get,
numa_scan_size_set, "%llu\n");
#endif /* CONFIG_NUMA_BALANCING */
注册处的替换:
- debugfs_create_u32("scan_size_mb", 0644, numa, &sysctl_numa_balancing_scan_size);
+ debugfs_create_file_unsafe("scan_size_mb", 0644, numa,
+ &sysctl_numa_balancing_scan_size, &numa_scan_size_fops);
函数路径里两处除法所在的上下文(这里只引用函数签名说明):
/* task_scan_min(): windows = MAX_SCAN_WINDOW / scan_size; */
/* task_nr_scan_windows(): return rss / nr_scan_pages; */
也就是说扫描窗口数与每次扫描的 page 数都来自 sysctl_numa_balancing_scan_size,对这个字段做 0 校验即可同时保护这两条除法分支。
类比
把它想成一个自助餐厅的取餐窗口:以前发餐员不验餐具,来人就把菜往盘里倒——有人端空盘子(写 0)走,结果打饭机 task_scan_min 直接卡死;有人端比出口还大的铁盘(写 > UINT_MAX),机器硬塞,出来一个被压变形的盘子(静默截断)。现在窗口前加了一道红外线扫描:set 就是闸机,「空盘」和「巨型盘」一律响警报、放行失败、扣回 -ERANGE;只有尺寸正常的盘子才让过、记录秤重。
或者更贴软件一点:原来的 debugfs_create_u32 像一个「无脑的快递柜」,用户塞多大的箱子它都吞,等到下游拆箱的工人(task_scan_min)才发现箱子是空的或者超规格,直接宕机。现在快递柜升级成「带 X 光扫描的智能柜」,扫描异常当场拒收,让 echo 0 > scan_size_mb 在源头就拿到 -ERANGE,而非等到除法时才崩。
Highlight:风险与注意点
- Fixes tag 必须保留:这个 bug 的本质是 commit
8a99b6833c88把 sysctl 迁移到 debugfs 时引入的,写Fixes:是为了让 stable 自动回溯到受影响的版本,合并时务必保留。 debugfs_create_file_unsafe的取舍:full_proxy层不仅管引用计数,还管 module unload 时的"安全移除"。本 ops 既然使用DEFINE_DEBUGFS_ATTRIBUTE,已经具备同等机制;但维护者仍会盯这个选择,需要解释为什么这里不需要full_proxy。- setter 校验放在边界即可,但下游别放松警惕:除零保护在
set上做了,但task_scan_min/task_nr_scan_windows自己仍可能因为别的修改把scan_size_mb拉成 0(比如某些 debug 接口绕过 debugfs 直接写 sysctl 变量)。后续 review 可以追问:要不要在函数入口也加一道WARN_ON_ONCE? val > UINT_MAX这一点其实早被 C 静默截断:因为 setter 收到的u64 val在到达本代码之前已经被截成u32了吗?答案是不——debugfs 走的是 string parsing,传进来的是u64,所以这次校验是有意义的。但这也意味着历史上"用户写大数但被静默截断"的旧行为,本质是别的路径,需要单独 review。- 重复发送 + v2:邮件标题带
[Resend]、v2,需要确认 v1 在 ml 上是否有人提过 review 意见,v2 的两个改动(rebase 与改用 unsafe)应对应回答 v1 的具体诘问。
版本变化
- v1 → v2:
- 基于最新基线 rebase。
- 把注册调用从
debugfs_create_file换成debugfs_create_file_unsafe——因为DEFINE_DEBUGFS_ATTRIBUTE已自带引用计数保护,避免重复的full_proxy。 - 重写 commit message,更明确地解释两类风险(除零 + 静默截断)和
-ERANGE的语义。
一句话总结
把 sched/debug 里 numa_balancing scan_size_mb 的 debugfs 写入路径换成自定义 setter:拒绝 0 与 val > UINT_MAX、返回 -ERANGE,把可能的除零与隐式截断都挡在闸门之外。