0/2 已展开

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)的内存泄漏:

  1. sched_init_numa()masks[i] / 内层 mask / tl 任一 kzalloc 失败时,外层 masks[0..i-1] 不会被释放,masks 自身以及从 arch_sched_node_distance 拷贝出来的 domain_distances 也都会泄漏。
  2. __sdt_alloc():内层 for_each_cpu 循环里 sd / sg / sgc 任一 kzalloc_node 失败时,已经成功 alloc_percpu 并完成部分 CPU 的 *per_cpu_ptr 写入会泄漏。
  3. __sds_alloc()d->sds 的 percpu 分配成功,但内部 sds kzalloc_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->sdsalloc_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 抓到,下一版只需要换个循环变量名即可再次提测。