0/3 已展开

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.csched_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;
}

ij 在前面已被循环用过,复用是安全的。释放顺序与原分配顺序(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
  • 复用循环变量 ij:原函数在前面已经使用它们,所以这里直接复用不会引入新的状态。

类比

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。
  • Fixes tagFixes: 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 中改为"先校验、后发布"以彻底省掉清理逻辑。