0/3 已展开

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 永远不会被安装
  • v1 试图用 rcu_assign_pointer(NULL) + synchronize_rcu() 撤回发布再释放,
    代价是要等一个 RCU grace period,路径复杂且不必要

新流程

  • 仍先分配 masks 数组
  • kzalloc() 分配 topology level 数组 tl
  • 若失败:
    • 遍历 nr_levels x 每个 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 撤回动作。