sched-ext discussion
[PATCH] tools/sched_ext: scx_flatcg: Fix uninitialized stats on allocation failure
LLM 分析
Linux Scheduler 讨论分析:scx_flatcg 未初始化统计修复
系列基线信息
- 标题: [PATCH] tools/sched_ext: scx_flatcg: Fix uninitialized stats on allocation failure
- 作者: Liang Luo luoliang@kylinos.cn
- 版本: 单 patch,无版本号(v1 直接提交)
- 规模: 1 file changed, 2 insertions(+), 2 deletions(-)
- Message-ID: 20260713071808.89847-1-luoliang@kylinos.cn
- 来源频道: sched-ext
- 关联 commit: Fixes: cabd76bbc036 ("tools/sched_ext: scx_flatcg: fix potential stack overflow from VLA in fcg_read_stats")
- 流向: 提交 → Andrea Righi 评审 → Tejun Heo 应用到 sched_ext/for-7.3
明确目的
这个 patch 修复了 scx_flatcg 用户态工具里 fcg_read_stats() 函数的一个回归 bug。
具体要解决的问题:
- 未初始化内存读取:当
calloc()分配cnts失败时,fcg_read_stats()会提前return,导致调用方传入的stats数组永远不会被清零。 - UB(未定义行为):调用方
main()中acc_stats数组是栈上声明但未初始化,调用fcg_read_stats()后又无条件读取,于是读到了栈上残留的垃圾数据。 - 基线污染:垃圾数据通过
memcpy()被拷贝进last_stats,让下一个采样周期的差分基准被污染,统计结果从此一直错下去。 - 回归来源:之前的 commit
cabd76bbc036把 VLA 换成calloc()时,把memset()放到了失败检查之后,造成了这个新 bug。
遍历代码
修复前(有 bug 的代码)
static void fcg_read_stats(struct scx_flatcg *skel, __u64 *stats)
{
__u64 *cnts;
__u32 idx;
cnts = calloc(skel->rodata->nr_cpus, sizeof(__u64));
if (!cnts)
return; // ← 提前 return,stats 没被清零
for (idx = 0; idx < FCG_NR_STATS; idx++) {
...
}
memset(stats, 0, sizeof(stats[0]) * FCG_NR_STATS); // ← 这句永远到不了(失败路径下)
}
### 修复后
```c
static void fcg_read_stats(struct scx_flatcg *skel, __u64 *stats)
{
__u64 *cnts;
__u32 idx;
memset(stats, 0, sizeof(stats[0]) * FCG_NR_STATS); // ← 先清零
cnts = calloc(skel->rodata->nr_cpus, sizeof(__u64));
if (!cnts)
return; // ← 失败时 stats 已经是 0
for (idx = 0; idx < FCG_NR_STATS; idx++) {
...
}
}
调用方(main 中相关片段)
__u64 acc_stats[FCG_NR_STATS]; // 栈上未初始化
fcg_read_stats(skel, acc_stats);
stats[i] = acc_stats[i] - last_stats[i]; // 失败时读 garbage → UB
// 接着 memcpy 到 last_stats,下一轮 baseline 被污染
关键改动
- 把
memset(stats, 0, sizeof(stats[0]) * FCG_NR_STATS)从失败检查之后移到之前。 calloc失败路径依然能保证stats被填为 0,调用方减出来的差分会是0 - last_stats[i],至少是一个可预测的合法值,不再是未定义行为。- 行为对称:
fcg_read_stats()无论走哪条路径,都至少把stats写一次。
ASCII 流程图
1. 修复前后的控制流对比
修复前(cabd76bbc036 之后) 修复后(本 patch)
=========================== ===========================
fcg_read_stats() fcg_read_stats()
│ │
├─ calloc(cnts) ├─ memset(stats, 0, …) ← 先清零
│ │ │ │
│ ├─ 成功 ─→ loop … ├─ calloc(cnts)
│ │ │ │ │
│ │ └─ memset(stats,0) │ ├─ 成功 ─→ loop …
│ │ │ │
│ └─ 失败 ─→ return ← stats 未写! │ └─ 失败 ─→ return
│ ↑ stats=0, 可预测
▼ ▼
调用方读 stats[i] 调用方读 stats[i] = 0 - last_stats[i]
→ UB(garbage) → 合法、可预测
2. 数据污染链路
栈分配 acc_stats[FCG_NR_STATS] [ 未初始化(栈残留) ]
│
▼ fcg_read_stats() 失败路径跳过 memset
acc_stats [ 仍是栈 garbage ]
│
▼ stats[i] = acc_stats[i] - last_stats[i]
stats[i] [ 错误差分 ]
│
▼ memcpy 到 last_stats
last_stats [ baseline 被污染 ]
│
▼ 下一周期继续累加
后续所有差分 [ 全部带噪声/雪崩 ]
3. 评审 → 合并链路
Liang Luo (luoliang@kylinos.cn)
│ 2026-07-13 07:18 提交 patch
▼
Andrea Righi (arighi@nvidia)
│ 2026-07-13 07:53 回复(review/Ack)
▼
Tejun Heo (tj@kernel.org, sched_ext maintainer)
│ 2026-07-13 18:35 Applied to sched_ext/for-7.3
▼
sched_ext/for-7.3 分支(目标 7.3 合并窗口)
概念类比
餐厅点餐场景:
想象一个服务员流程:
fcg_read_stats()是「厨房」,它的责任是把一桌菜(stats数组)端给顾客。- 厨房需要临时借一个盘子(
calloc出来的cnts)来摆菜。 - 旧流程:厨房先去借盘子;如果借不到就直接回厨房不出来,饭桌上空空如也;然后顾客以为菜已经端上来,伸筷子去夹,结果夹到的全是上一桌客人吃剩的渣(栈 garbage)。
- 新流程:厨房在借盘子之前先把桌面铺一层干净的桌布(
memset(0));哪怕盘子借不到,桌面也是干净的,顾客看到的差值至少是「这桌没菜 - 上一桌的菜」,是一个合理、稳定的数字。
再换一种角度:这个 bug 像是「变量先使用、后赋值」的典型错误——memset 原本是「赋值」动作,但被错误地放到了「使用」之后;只要走到失败分支,stats 就从来没被赋值过,而调用方却当成已经赋值来读,触发 C 语言的经典 UB。
Highlight 突出问题
- UB 触发条件可能很罕见:
calloc在普通压力下很难失败,但在内存紧张、容器 OOM、cgroup 限额触发时就会浮上来;上线前最好做一个「人为压低 memory+ 跑scx_flatcg` 的烟雾测试,确认差分曲线没有突刺。 void返回类型让错误无法传播:fcg_read_stats()返回void,调用方没有任何途径感知失败,这是一个接口设计层面的小问题。本 patch 没改 API,但后续可以讨论:要么改成bool/int返回值让调用方决定 fallback,要么在文档里明确「失败时 stats 会被清零」的契约。memset(sizeof(stats[0]) * FCG_NR_STATS)的字节数计算是否每次都正确:本 patch 维持原写法,意味着stats必须是指针而不是数组名——这一假设成立,但容易被未来重构者破坏;如果有人把__u64 *stats改成__u64 stats[],sizeof(stats)就会变成指针大小,bug 静默复发。建议加一行注释或BUILD_BUG_ON之类。- 回归来源未被更早发现:commit
cabd76bbc036是为修 VLA 栈溢出引入的,本身是个正确的改动;但任何「在 if(!ptr) return; 之前/之后插入语句」的改动都应该 review 一下所有调用方对 out 参数的依赖。可考虑引入__attribute__((nonnull))或-Wmaybe-uninitialized更激进地开启。 - 跟进观察:合入
sched_ext/for-7.3后,需要关注 7.3 合并窗口期间是否有人基于这个修复做进一步清理(例如把fcg_read_stats改成bool返回,或在main()主动把acc_stats初始化为 0 防御)。
版本演进
本 patch 只有 v1,无 v2/v3:
- v1:直接把
memset上移到calloc失败检查之前。 - Andrea Righi 在评审中未要求修改(从其简短回复推断为 Ack/Reviewed-by 类型)。
- Tejun Heo 当天直接 pick 进了
sched_ext/for-7.3,说明补丁被认为足够干净。
与其他相关 patch 系列的关联
- 直接前置:commit
cabd76bbc036("tools/sched_ext: scx_flatcg: fix potential stack overflow from VLA in fcg_read_stats")——本 patch 是它的直接回归修复。 - 同文件 (scx_flatcg.c) 的演进:scx_flatcg 作为 sched_ext 示例 BPF 程序,长期处于小修小补状态,类似「out 参数未初始化」类问题在其他
*_read_stats函数中也可能存在,值得一并审计。 - sched_ext 仓库节奏:Tejun 在该 patch 当天 pick,说明
for-7.3分支正处在 7.3 合并窗口前的快速收纳期,类似小修复都会快速入树。
一句话总结
这是一个典型的「把 memset 放错了位置」导致的未初始化内存读取 UB 修复:通过把 memset 上移到 calloc 失败检查之前,保证 stats 数组无论分配成败都被清零,堵住了回归 commit cabd76bbc036 引入的隐患,并被 Tejun Heo 当天 pick 进 sched_ext/for-7.3。