sched discussion
[PATCH] sched/topology: fix memory leaks in allocation failure paths
LLM 分析
sched/topology:修复分配失败路径中的内存泄漏
系列概况
- 标题:[PATCH] sched/topology: fix memory leaks in allocation failure paths
- 作者:monios114514 monios114514@outlook.com(Signed-off-by cpumsconfig)
- 版本:单 patch,v1(无版本号、无系列 index)
- 规模:1 file changed, 53 insertions(+), 46 deletions(-)
- 修改文件:kernel/sched/topology.c
- 代码统计:净 +7 行
- Message-ID(首封):SL2P216MB261965EB5BCCF15723D13A76C1FE2@SL2P216MB2619.KORP216.PROD.OUTLOOK.COM
- 完整性:首发 patch + 1 封 0day/llvm 警告回执;尚无 maintainer 评审或 re-spin
补丁目的
修复 kernel/sched/topology.c 中三个分配失败路径(alloc-failure paths)的内存泄漏:
sched_init_numa():masks[i]/ 内层mask/tl任一 kzalloc 失败时,外层masks[0..i-1]不会被释放,masks自身以及从arch_sched_node_distance拷贝出来的domain_distances也都会泄漏。__sdt_alloc():内层for_each_cpu循环里sd/sg/sgc任一 kzalloc_node 失败时,已经成功alloc_percpu并完成部分 CPU 的*per_cpu_ptr写入会泄漏。__sds_alloc():d->sds的 percpu 分配成功,但内部sdskzalloc_node 失败时,d->sds自身清理路径缺失。
旧流程的问题
原代码在失败分支只是 return; 或 return -ENOMEM;,让已经分配出来的资源听天由命:
// 旧代码:失败即裸退
if (!masks[i]) return;
if (!mask) return;
if (!tl) return;
return -ENOMEM; // 同样的故事 在 __sdt_alloc 出现 5 次
sched_init_numa() 中甚至还有一段注释,提示"临时把 sched_domains_numa_levels 重置成 0,避免后续函数遍历 sched_domains_numa_masks[][] 时越界" —也就是说作者意识到失败路径会留下不完整状态,但选择让泄漏换取"最终一致性"。这是典型的"先解决能用,再解决内存"问题。
新流程
统一采用 goto fail; / goto free_distance; 集中收尾:
sched_init_numa()失败时:先把已分配好的masks[0..i-1](或masks[0..nr_levels-1])按层回溯kfree,再处理sched_domains_numa_masks的 RCU 指针,再kfree(masks),最后释放domain_distances。__sdt_alloc()失败时:goto fail;→__sdt_free(cpu_map);一把扫掉前面所有alloc_percpu已经填好的指针。__sds_alloc()失败时:__sds_free(d, cpu_map);兜底d->sds。
Patch 概览
整条线索只有 1 个 patch,详见下一节关键实现。
关键实现
1. sched_init_numa():三层 alloc 必须都回滚
if (!masks[i]) {
for (i = i - 1; i >= 0; i--) {
if (!masks[i]) continue;
for_each_cpu_node_but(j, offline_node)
kfree(masks[i][j]);
kfree(masks[i]);
}
kfree(masks);
goto free_distance;
}
注意:第二处补丁原本写成 for (i = i; i >= 0; i--)(保留 i 自身),被 0day 用 clang -Wself-assign 抓出来 — i = i 是显式自赋值,clang 视为可疑代码。最终意图是"清理当前层加上已经成功的层",但展开后其实是冗余的(masks[i][j] 此时为 NULL,kfree(NULL) 是 no-op),属于可读性 + 编译警告两个维度的瑕疵。
2. __sdt_alloc():从五处 return -ENOMEM; 收敛到单点
fail:
__sdt_free(cpu_map);
return -ENOMEM;
把分散的 5 个 -ENOMEM 收口到单个 fail 标签,是 kernel 惯用的错误处理惯例 — 把"清理 + 报错"合并成一处,方便新增 alloc 时不会漏掉。
3. __sds_alloc():补齐 d->sds 的清理
if (!sds) {
__sds_free(d, cpu_map);
}
d->sds 是 alloc_percpu 出来的,只有 __sds_free() 知道 free_percpu 的细节,所以这里必须显式调用。让 __sds_alloc() 内部自己处理"半成功状态",调用方就不再感知。
类比
把这三个函数想成餐厅的三道菜上桌流程:
sched_init_numa()是"点一桌酒席":先准备餐桌(domain_distances)、再摆碗(masks)、再给每个碗分配筷子(mask)。原本任何一个环节缺货,厨房就直接把客人赶走,桌上的碗筷全留在那儿没人收。__sdt_alloc()像"给每张桌子发菜单":菜单(sd/sg/sgc)按桌号填写,填到一半发现缺货,回来时把已经印好的菜单全部撕掉,避免"客人拿着半张菜单退钱"。__sds_alloc()像"账单打印机":打印机准备好(d->sds),但缺纸(sds),需要把打印机也撤走,不然下一桌客人会拿到乱跳纸的账单。
goto fail / goto free_distance 就是餐厅的"收桌阿姨" — 她只认一个标签,桌上有啥她都按清单收一遍。
+--------------------------------------+
| sched_init_numa() entry point |
+--------------------------------------+
|
+-----------+-----------+-----------+-----------+
| | | | |
v v v v v
kzalloc kzalloc kzalloc kzalloc(tl)
(masks) (masks[i]) (mask)
| | | |
fail fail fail fail
| | | |
+-----------+-----------+-----------+
|
v
success: return
|
v free_distance:
kfree(domain_distances)
__sdt_alloc():
alloc_percpu(sd) -- fail --> goto fail
alloc_percpu(sg) -- fail --> goto fail
alloc_percpu(sgc) -- fail --> goto fail
for_each_cpu:
kzalloc(sd) -- fail --> goto fail
kzalloc(sg) -- fail --> goto fail
kzalloc(sgc) -- fail --> goto fail
return 0 fail:
__sdt_free(cpu_map); free all alloc_percpu
return -ENOMEM;
Highlight:风险与注意点
- clang
-Wself-assign警告:for (i = i; i >= 0; i--)触发-Wself-assign。这是 0day 在 riscv-randconfig-002-20260814 / clang 24 上抓到的。需要把i改成不同的循环变量(比如int k;然后for (k = i; k >= 0; k--)),或者写成for (i = i - 1; i >= 0; i--)复用masks[i]失败那一段的写法。 - RCU 释放顺序:
rcu_assign_pointer(sched_domains_numa_masks, NULL);必须在kfree(masks)之前完成,否则并发读端可能还在引用,RCU grace 还没过期;不过当前补丁在tl失败前就先 disarm 指针,顺序是对的。 sched_domains_numa_levels状态:失败路径里没有显式恢复,可能再留 0 或者保持原值,需要阅读者弄清楚最终默认是哪种状态。- 测试覆盖:riscv-randconfig 这种稀疏配置才能暴露 "alloc 失败" 路径,常规 x86 build 几乎不会触发;建议加一条
CONFIG_DEBUG_KMEMLEAK=y的 kmemleak 扫描用例。 - base tree:0day 建议用
--base指明 base commit;目前邮件里没有 base 信息,CI 只能猜。
版本变化
目前只有 v1,无版本演进。
一句话总结
这个 patch 一次性补上 sched/topology.c 三个分配失败路径的内存泄漏,但其中 for (i = i; ...) 的自赋值写法被 clang 抓到,下一版只需要换个循环变量名即可再次提测。