sched discussion
[PATCH v2] sched/topology: Free NUMA masks on topology allocation failure
LLM 分析
sched/topology:topology 分配失败时释放 NUMA masks
系列概况
- 标题:
[PATCH v2] sched/topology: Free NUMA masks on topology allocation failure - 作者:Fengyu Wang
<wangfengyu@hygon.cn> - 版本:v2(系列共一封 patch,无 v3/v4)
- 规模:1 file changed, 10 insertions(+), 2 deletions(-)
- 修改文件:
kernel/sched/topology.c - 代码统计:净增加约 8 行,hunk 范围
@@ -2392,15 +2392,23 @@ - Message-ID:
20260812062206.82410-1-wangfengyu@hygon.cn - 完整性:完整,含 commit message、Fixes tag、Signed-off-by 与 v1 -> v2 changelog
补丁目的
sched_init_numa() 在分配 sched_domain_topology_level 数组之前,就通过
rcu_assign_pointer() 把 sched_domains_numa_masks 发布出去。一旦随后的
kzalloc() 失败,函数直接 return,masks 既不会被解引用,也不会被释放 -- 它们
变成了"孤儿内存"。补丁的目标是:在错误路径上正确释放 masks,并把发布时机
推迟到 topology 数组分配成功之后。
旧流程的问题
- 先
rcu_assign_pointer(sched_domains_numa_masks, masks)把 masks 暴露给 RCU 读者 - 再
kzalloc()分配 topology level 数组tl - 若
kzalloc失败 -> 直接return:- masks 已发布,但
sched_domains_numa_levels仍为 0,无人解引用、无人释放 - 与这些 masks 对应的 topology 永远不会被安装
- masks 已发布,但
- v1 试图用
rcu_assign_pointer(NULL)+synchronize_rcu()撤回发布再释放,
代价是要等一个 RCU grace period,路径复杂且不必要
新流程
- 仍先分配 masks 数组
- 再
kzalloc()分配 topology level 数组tl - 若失败:
- 遍历
nr_levelsx 每个 node,依次kfree(masks[i][j])->kfree(masks[i])->kfree(masks) - 直接 return,不再触碰 RCU(因为从未发布过)
- 遍历
- 若成功,再
rcu_assign_pointer()把sched_domains_numa_masks发布给 RCU 读者 - 错误路径不再有
synchronize_rcu(),整体更简单、更快
整体发布/释放顺序对比:
OLD (v1)
+-------------------+
| alloc masks[][] |
+---------+---------+
|
v
+----------------------+
| rcu_assign_pointer | <-- publish too early
| (masks) |
+-----------+----------+
|
v
+-----------+----------+
| kzalloc(tl) |
+-----+--------+-------+
| |
fail ok
| |
v v
+-------------+ +--------------+
| rcu_assign_ | | build |
| pointer | | topology |
| (NULL) | +--------------+
| synchronize_|
| rcu() |
| kfree masks |
| [i][j] |
| kfree masks |
| [i] |
| kfree masks |
| return |
+-------------+
NEW (v2)
+-------------------+
| alloc masks[][] |
+---------+---------+
|
v
+---------+----------+
| kzalloc(tl) |
+-----+--------+-----+
| |
fail ok
| |
v v
+--------------+ +------------------+
| kfree masks | | rcu_assign_ptr |
| [i][j] | | (masks) |
| kfree masks | | publish AFTER |
| [i] | | topology ok |
| kfree masks | +------------------+
| return |
+--------------+
关键实现
补丁 hunk 集中在 sched_init_numa() 的错误分支与发布动作:
if (!tl) {
for (i = 0; i < nr_levels; i++) {
for_each_node(j)
kfree(masks[i][j]);
kfree(masks[i]);
}
kfree(masks);
}
rcu_assign_pointer(sched_domains_numa_masks, masks);
- 释放顺序:先叶子
masks[i][j],再内层数组masks[i],最后外层masks rcu_assign_pointer()被无条件移到if (!tl)之后,意味着失败路径根本不会触碰 RCU 发布
发布时机的状态变化可以再看一张紧致的对照图:
sched_init_numa()
|
v
+----------------+
| alloc masks[][] |
+-------+--------+
|
v
+----------------+
| kzalloc(tl) |
+-------+--------+
|
+------+------+
| |
tl==NULL tl!=NULL
| |
v v
+-----------+ +------------------+
| kfree all | | rcu_assign_ptr |
| masks[][] | | (masks) |
| return | | topology built |
+-----------+ +------------------+
类比
把它想成餐厅厨房做菜的顺序:
- 旧流程:服务员先把"今日菜单"贴到门口(对外公布),厨师再开始炒菜。中途发现没
食材时,菜单已经贴出去了,客人不会进来取,菜单也就永远挂在那里 -- 典型的
"孤儿菜单"。 - 新流程:厨师先把菜做完,确认能上桌后,再把菜单连同菜品一起端出去。中途如果没
食材,半成品(菜 + 临时菜单)一并处理掉,不会留下任何半吊子状态。
Highlight:风险与注意点
- Valentin Schneider 在 v2 邮件里指出:
sched_init_numa()失败时仍是void返回,
调度拓扑分配失败基本意味着整台机器启动都会"大火";这个补丁只是"让你走得稍微远
一点",属于防御性修复,实际触发概率极低。 - 双重
for释放循环假设masks的维度是nr_levels x nr_nodes;如果上游改变
masks的形状(例如未来 NR_NODES 变成动态),这里必须同步修改,否则可能
use-after-free。 - v2 借助"never publish, no need to retire"的思路,把
synchronize_rcu()从错误
路径彻底拿掉,比 v1 更干净 -- 这是典型的"把失败语义想在分配之前"的例子。 - Fixes tag 指向
cb83b629bae0("sched/numa: Rewrite the CONFIG_NUMA sched domain
support"),说明该漏洞自当年大重写之后就存在,stable backport 时需要顺带捎上。 - 后续可观察点:若
build_this_cpu_maps()等调用方在看到sched_domains_numa_levels == 0
时还有别的隐式依赖,本补丁只是把 leak 关上,不会改变它们的语义。
版本变化
- v1:在错误路径上
rcu_assign_pointer(NULL)+synchronize_rcu()撤回发布,再释放 masks - v2:彻底移除错误路径中的 RCU 操作,把
rcu_assign_pointer()放到tl分配成功之后;
错误路径只剩纯kfree(依据 Tim Chen 的反馈调整)
一句话总结
sched/topology 修复 sched_init_numa() 在 topology array 分配失败时未释放 NUMA
masks 的内存泄漏,v2 通过"先分配再发布"的顺序避免了 v1 中不必要的 RCU 撤回动作。