sched discussion
[PATCH] sched/topology: Free NUMA masks on topology allocation failure
LLM 分析
sched/topology:sched_init_numa() 失败路径 NUMA masks 泄漏修复
系列概况
- 标题:
[PATCH] sched/topology: Free NUMA masks on topology allocation failure - 作者:Fengyu Wang wangfengyu@hygon.cn
- 版本:v1(无显式版本号),后续回复提及会有 v2
- 规模:单 patch,单文件,10 行新增 / 1 行删除
- 修改文件:
kernel/sched/topology.c - 代码统计:1 file changed, 10 insertions(+), 1 deletion(-)
- Message-ID:
20260731081413.5505-1-wangfengyu@hygon.cn - 完整性:完整;第 2、3 封邮件是 review 对话,作者已承诺出 v2
补丁目的
sched_init_numa() 在分配 sched_domain_topology_level 数组(即 tl)之前,先用 rcu_assign_pointer() 把 sched_domains_numa_masks 发布出去。
如果紧接着的 kzalloc() 失败,原代码只是 return 提前退出,结果是:
masks已经对外可见(通过 RCU 指针);- 但
sched_domains_numa_levels仍是 0,没有读者会去解引用; tl没分配成功,整套 NUMA 拓扑都不会被安装;masks既无人使用、也无人 free,构成一次性内存泄漏。
本 patch 把这条失败路径补完整,回收 masks 三层结构里所有 kzalloc 出来的内存。
旧流程的问题
sched_init_numa()
|
v
+----------------------------+
| rcu_assign_pointer( |
| sched_domains_numa_masks,|
| masks) | <-- publish masks early
+----------------------------+
|
v
+----------------------------+
| tl = kzalloc(...) |
+----------------------------+
|
+-- ok --> copy default topology
|
+-- fail --> return
(masks leaked: published but never freed)
问题核心是 publish-then-check 顺序:先宣布"我有数据在这里",再校验"我是否能建好"。
新流程
v1(本 patch 的做法)
tl = kzalloc(...)
|
+-- fail --> cleanup branch:
| rcu_assign_pointer(sched_domains_numa_masks, NULL);
| synchronize_rcu(); // wait for RCU readers
| for i in 0..nr_levels:
| for_each_node j:
| kfree(masks[i][j]);
| kfree(masks[i]);
| kfree(masks);
| return;
|
+-- ok --> continue topology copy/install
v2(Tim Chen 提议的更简洁方案)
把 rcu_assign_pointer(sched_domains_numa_masks, masks) 整体移到 tl = kzalloc(...) 之后。失败路径上 masks 从未对外发布,连 synchronize_rcu() 都不需要。
tl = kzalloc(...)
|
+-- fail --> return
| (never published, freed by process exit)
|
+-- ok --> rcu_assign_pointer(sched_domains_numa_masks, masks);
copy default topology ...
Patch 概览
v1 在 kernel/sched/topology.c 的 sched_init_numa() 中,把单行 if (!tl) return; 替换为带花括号的复合语句:
if (!tl) {
rcu_assign_pointer(sched_domains_numa_masks, NULL);
synchronize_rcu();
for (i = 0; i < nr_levels; i++) {
for_each_node(j)
kfree(masks[i][j]);
kfree(masks[i]);
}
kfree(masks);
return;
}
i 与 j 在前面已被循环用过,复用是安全的。释放顺序与原分配顺序(masks -> masks[i] -> masks[i][j])严格对称,避免悬挂指针。
关键实现
rcu_assign_pointer(..., NULL):让外部观察者看到指针已被清空,避免 RCU 读者读到刚要被释放的旧masks。synchronize_rcu():确保所有正在 RCU 临界区里走读的 CPU 都退出后,再去kfree。这是 v1 中"先 unpublish 再释放"安全性的核心。- 三层
kfree:分别回收[levels][nr_node_ids]的叶子指针、masks[i]行指针、以及最外层masks。 - 复用循环变量
i、j:原函数在前面已经使用它们,所以这里直接复用不会引入新的状态。
类比
把 sched_init_numa() 想成搬家时贴门牌号:
- 你先把门牌挂上去(
rcu_assign_pointer),可屋里家具还没搬完。 - 如果货车在半路抛锚(
kzalloc失败),原代码直接把钥匙丢了走人:门牌还挂着,屋里家具既没人用也没人搬走,全丢在空屋里。 - v1 的做法:发现货车抛锚,先摘门牌,再挨家挨户通知搬家公司别再按这个地址送货(
synchronize_rcu),然后把屋里家具全部清走。 - Tim Chen 的 v2 方案更聪明:门牌等家具真的全部到位再挂,货车抛锚直接走人,连通知都省了——把"发布顺序"挪到"全部检查通过"之后。
Highlight:风险与注意点
- 顺序敏感性:
synchronize_rcu()在早期启动路径上调用,开销非零;更重要的是,sched_init_numa早期阶段是否合法地允许等待 RCU grace period,需要确认。这是 v1 方案的可推敲点。 - publish-then-check 反模式:本次修复体现了"先发布、后校验"的隐患。Tim Chen 的 v2 通过"check-then-publish"彻底回避了 unpublish + 同步的复杂度,更安全也更易读。
- 失败路径测试:作者通过把
tl硬编码为 NULL 人工制造失败,验证释放后机器能正常启动;这是模拟kzalloc失败的常见做法,但硬编码 NULL 不能进正式 patch。 Fixestag:Fixes: cb83b629bae0 ("sched/numa: Rewrite the CONFIG_NUMA sched domain support")指向 NUMA 调度域大重构,意味着这个泄漏从那次重构就潜伏了。- 后续观察点:v2 落地后,需要确认
sched_init_numa()在tl成功之前的代码路径里,没有任何代码已经对sched_domains_numa_masks做了解引用;若有,需要在那些地方加上"指针非空"的判断。
版本变化
- v1 -> v2(计划中):移除失败路径里的
rcu_assign_pointer(..., NULL)+synchronize_rcu()清理块,改为把rcu_assign_pointer(sched_domains_numa_masks, masks)移到tl分配成功之后。逻辑行更少,也避免早期启动阶段做 RCU 同步的潜在风险。Fengyu Wang 在第 3 封邮件明确表示 "V2 will move the rcu_assign_pointer() below the tl check"。
一句话总结
sched_init_numa() 在分配 topology 数组前就先发布 NUMA masks,分配失败时既不卸下指针也不释放内存,造成泄漏;v1 在失败路径里 unpublish + RCU 同步 + 三层 kfree 回收,作者随后将按 Tim Chen 的建议在 v2 中改为"先校验、后发布"以彻底省掉清理逻辑。