sched discussion
FAILED: patch "[PATCH] sched/psi: Shut down rtpoll_timer in psi_cgroup_free()" failed to apply to 5.15-stable tree
LLM 分析
sched/psi:rtpoll_timer 生命周期修复的 5.15 stable 回移
系列概况
- 标题:
sched/psi: Shut down rtpoll_timer in psi_cgroup_free() - 作者:原始补丁 Tejun Heo;5.15.y 回移 Sasha Levin;FAILED 通知 Greg KH
- 版本:无 vN 标记;本线程是 stable 回移流程(失败通知 + 适配版补丁)
- 规模:2 封邮件,回移补丁 6 行新增
- 修改文件:
kernel/sched/psi.c - 代码统计:
1 file changed, 6 insertions(+) - Message-ID:
2026081706-verdict-hangout-e22e@gregkh、20260818193056.3302866-1-sashal@kernel.org - 完整性:完整。上游 commit
5457025fa8ca3c0d2732109513de839e3e797190,失败通知与回移一一对应
补丁目的
修复 PSI(Pressure Stall Information)里的一个 use-after-free:cgroup 被释放后,仍可能有一个已重新装填(re-armed)的 rtpoll_timer 挂在定时器轮上,触发时 poll_timer_fn() 访问已释放内存。
根因是竞态:psi_schedule_rtpoll_work() 在调度器热路径上无锁调用,而 psi_trigger_destroy() 正在销毁最后一个 rtpoll trigger。
本线程的具体事项:上游补丁无法 cherry-pick 到 5.15-stable(因为 5.15 里 psi 是内嵌结构而非指针,且成员名还叫 poll_*),Sasha Levin 提交了手工适配版。
旧流程的问题
历史上补丁在"销毁路径"里反复挪动 timer 删除点,两头都不对:
461daba06bdc:在synchronize_rcu()之后同步删除 timer——挡住了本次竞态,但与 创建 竞态:宽限期内新 trigger 装填的 timer 会被误删,而当时创建还会重新 init timer,导致 timer 结构损坏。8f91efd870ea:把 init 挪到group_init(),把删除挪进加锁段——换掉了创建竞态,却打开了本次的销毁窗口。
竞态窗口:
psi_schedule_rtpoll_work() psi_trigger_destroy()
rcu_read_lock()
task = rcu_dereference(rtpoll_task)
rcu_assign_pointer(rtpoll_task, NULL)
timer_delete(&rtpoll_timer)
mod_timer(&rtpoll_timer, ...) <-- re-armed
rcu_read_unlock()
synchronize_rcu()
kthread_stop(task)
... cgroup freed, timer pending -> UAF
结论:销毁路径里放哪都不对。
新流程
不再试图在 trigger 销毁时精确删除 timer,而是把 timer 的生命周期绑定到 group 本身,在 psi_cgroup_free() 里一次性关闭。
关键前提:group 还活着时 timer 误触发是无害的——poll_timer_fn() 只是唤醒 rtpoll 等待队列,不会自我重新装填。到 psi_cgroup_free() 时已无人能再 arm 它,所以用 timer_shutdown_sync()(永久关闭,之后不再允许 arm)。
Patch 概览
| 邮件 | 作者 | 作用 |
|---|---|---|
| 1 | Greg KH | 自动通知:上游 commit 无法应用到 5.15.y,附冲突复现步骤 |
| 2 | Sasha Levin | 5.15.y 适配版:把 cgroup->psi->rtpoll_timer 改成 cgroup->psi.poll_timer |
关键实现
上游(mainline,指针 + rtpoll_* 命名):
void psi_cgroup_free(struct cgroup *cgroup)
{
...
/*
* A psi_schedule_rtpoll_work() call racing the last trigger's
* destruction may have re-armed the timer after psi_trigger_destroy()
* deleted it. Spurious firing while the group is alive is harmless.
*/
timer_shutdown_sync(&cgroup->psi->rtpoll_timer);
cancel_delayed_work_sync(&cgroup->psi->avgs_work);
free_percpu(cgroup->psi->pcpu);
WARN_ONCE(cgroup->psi->rtpoll_states, "psi: trigger leak\n");
}
5.15.y 回移(内嵌结构 + poll_* 旧命名):
timer_shutdown_sync(&cgroup->psi.poll_timer);
cancel_delayed_work_sync(&cgroup->psi.avgs_work);
free_percpu(cgroup->psi.pcpu);
WARN_ONCE(cgroup->psi.poll_states, "psi: trigger leak\n");
差异只有两处机械改动,语义完全一致;Sasha 在 changelog 中明确标注了 [ adapted ... ]。
顺序也重要:timer_shutdown_sync() 必须排在 free_percpu() 之前,否则 timer 回调仍可能摸到已释放的 per-cpu 数据。
[OLD] destroy path [NEW] group lifetime
-------------------- --------------------
timer_delete in lock --X timer_shutdown_sync
<-- races both ways in psi_cgroup_free()
cannot re-arm -> safe
类比
像酒店房间里的自动叫醒电话。旧做法是:最后一位客人退房时前台手忙脚乱地取消叫醒——但客人可能刚好在退房那一刻又按了一次"半小时后叫我",于是房间已经清空重新分配了,电话还照响,吵到了不该吵的人(已释放内存)。
新做法是:退房时不管叫醒,反正房间还没拆,电话响一下也没事;等到整栋楼这间房被拆掉(psi_cgroup_free())时,直接把线路物理拔掉,之后谁也没法再设定叫醒。
Highlight:风险与注意点
- 顺序敏感:
timer_shutdown_sync()必须在free_percpu(pcpu)之前,回移时若插错位置就白修了;本回移位置正确。 timer_shutdown_sync()的可用性:这是较新的 API(v6.2 引入timer_shutdown_sync)。5.15.y 要能直接用,说明相关基础设施已被回移进 5.15.y——如果某些更老分支(如标注的 v5.10+)没有它,backport 需要退回del_timer_sync(),语义弱一些(不阻止再次 arm)。- 命名重构掩盖了 backport 风险:
rtpoll_*<-poll_*只是改名,但 review 时容易误以为是不同对象;psi从内嵌变指针也一样。 - "误触发无害"是本补丁成立的核心前提:依赖
poll_timer_fn()不自我 re-arm。若将来该函数改为会重新装填,这个修复的假设立即失效。 WARN_ONCE(rtpoll_states)仍保留:说明 trigger 泄漏检测未受影响,但也意味着如果真有 trigger 未清理,timer 关闭并不掩盖该问题。- 待跟进:Greg 的通知里
Possible dependencies:为空,Closes:也是空的——原始报告链接缺失,追溯 reproducer 时需要回到 lore 上的原线程。
版本变化
无多版本迭代。演进体现在历史 commit 链上:
461daba06bdc(删除放在synchronize_rcu()后)-> 与创建竞态8f91efd870ea(init 移入group_init(),删除移入加锁段)-> 与销毁竞态(本次Fixes:目标)5457025fa8ca(本补丁:绑定 group 生命周期)-> 两个竞态同时消失
一句话总结
与其在 trigger 销毁路径上和调度器热路径抢时间删 timer(怎么放都有竞态),不如把 rtpoll_timer 的生命周期绑到 cgroup 上、在 psi_cgroup_free() 里 timer_shutdown_sync() 一刀关闭;本线程是该修复在 5.15.y 上因结构体形态与命名差异 cherry-pick 失败后的手工回移。