0/2 已展开

LLM 分析

sched/psi:把 rtpoll_timer 的关闭下沉到 cgroup 释放流程

系列概况

  • 标题:FAILED: patch "[PATCH] sched/psi: Shut down rtpoll_timer in psi_cgroup_free()" failed to apply to 5.10-stable tree(自动转发),以及后续 [PATCH 5.10.y] sched/psi: Shut down rtpoll_timer in psi_cgroup_free()
  • 作者:Greg KH(stable 转发)、Tejun Heo(upstream)、Sasha Levin(5.10.y backport)
  • 版本:mainline 5457025fa8ca -> 5.10.y backport
  • 规模:单文件、6 行新增
  • 修改文件:kernel/sched/psi.c
  • 代码统计:1 file changed, 6 insertions(+)
  • Message-ID:2026081707-cozy-flier-448d@gregkh、20260819111459.3552196-1-sashal@kernel.org
  • 完整性:upstream commit 完整转发,5.10.y 适配版块由 Sasha Levin 重发,主体一致

补丁目的

PSI(Pressure Stall Information)在 cgroup 销毁路径上存在 use-after-free:

  • psi_schedule_rtpoll_work() 由调度热路径 lockless 调用,可能在 psi_trigger_destroy() 已经 timer_delete(&rtpoll_timer) 之后,再次 mod_timer() 重新装弹。
  • synchronize_rcu() + kthread_stop() 完成后 cgroup 被释放,pending 的 timer 仍然存在,poll_timer_fn() 会在已释放内存上运行。

补丁把 timer 的关闭绑定到 cgroup 的生命周期:在 psi_cgroup_free() 中调用 timer_shutdown_sync()(5.10.y 适配为 timer_delete_sync())显式关闭 rtpoll_timer(5.10 中名为 poll_timer)。

旧流程的问题

历史上两个修复都没能封死 race:

  1. 461daba06bdc("psi: eliminate kthread_worker ..."):在 synchronize_rcu() 之后同步删除 timer -- 解决 destroy 侧的 race,但反过来和 create 路径打架:同一 grace period 内新 trigger 的 mod_timer 仍可重新装弹一个刚被 cancel 的 timer,触发 timer corruption。
  2. 8f91efd870ea("psi: Fix race between psi_trigger_create/destroy"):把 init 挪到 group_init(),delete 挪到 locked 段。把 create 侧的 race 换成 destroy 侧的窗口 -- destroy 路径上 timer_delete 之后仍有 hot path mod_timer 的窗口。

两条路径都缺乏 "timer 与 group 共生死" 的语义。

新流程

  • 不再依赖 destroy 路径上的精细时序。
  • psi_cgroup_free() 中调用 timer_shutdown_sync(&cgroup->psi->rtpoll_timer),此时已没有任何东西可以再 arm 它。
  • 因 timer 之后再也不会被 arm,timer_shutdown_sync() 的 "关掉且禁止再 arm" 语义刚好对上。
cgroup unbind / rmdir
        |
        v
psi_trigger_destroy()                      (last trigger)
 +-- rcu_assign_pointer(rtpoll_task, NULL)
   +-- timer_delete(&rtpoll_timer)
   +-- synchronize_rcu() + kthread_stop()
                 |
                 v
psi_cgroup_free()                          (NEW: timer bound to group)
 +-- timer_shutdown_sync(&cgroup->psi->rtpoll_timer)   <-- key fix
   +-- cancel_delayed_work_sync(&cgroup->psi->avgs_work)
   +-- free_percpu(cgroup->psi->pcpu)
   +-- WARN_ONCE(rtpoll_states, "psi: trigger leak\n")
psi_schedule_rtpoll_work()        psi_trigger_destroy()
------------------------------    ------------------------------
rcu_read_lock()                   timer_delete(&rtpoll_timer)
task = rcu_dereference(...)       synchronize_rcu()
mod_timer(&rtpoll_timer, ...)     kthread_stop(task_to_destroy)
rcu_read_unlock()                          |
                                          v
 race window -> group freed
                                          |
                                          v
                                   poll_timer_fn() on freed mem |
                                          v
                                   timer_shutdown_sync()   (new fix)

Patch 概览

两封邮件是同一 commit 5457025fa8ca 的不同分发:

邮件版本主要差异
#1mainline -> stable 自动转发提示 5.10.y 上 apply 失败,需人工 backport
#25.10.y backportcgroup->psi-> -> cgroup->psi.rtpoll_timer -> poll_timertimer_shutdown_sync() -> timer_delete_sync(),commit log 同步改写

关键实现

upstream hunk(mainline):

+ /*
+  * 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);

5.10.y hunk:

+ /*
+  * A psi_schedule_poll_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_delete_sync(&cgroup->psi.poll_timer);

差异:

  • cgroup->psi 在新内核是指针,在 5.10 是嵌入字段,所以 -> vs .
  • 5.10 还没有 timer_shutdown_sync(),旧 API 是 timer_delete_sync()
  • 5.10 仍叫 poll_timer,命名演化后才改 rtpoll_timer("rt-poll")。

类比

把 cgroup 想成一家公司:

  • psi_trigger_destroy() 是最后一位股东退场。
  • psi_schedule_rtpoll_work() 是不断路过的快递员,他会在股东走时把新包裹塞进传达室的"定时取件"按钮(mod_timer)。
  • 老规则下,公司关门(cgroup 释放)之后下一次"取件铃"响时,快递员按的其实是废弃的传达室门铃 -- UAF。
  • 新做法是物业在合同里写死:取件铃一响直接断电、永远不再装回去(timer_shutdown_sync / timer_delete_sync)。之后不管快递员怎么按,铃都不会再被装上,公司大门关上之后也不会再有任何"取件"事件。

Highlight:风险与注意点

  1. psi_schedule_rtpoll_work 仍在 hot path 上 lockless 调用,新代码并不能阻止"多余触发",它接受"group 还活着时多余的 timer 触发是无害的"这一前提(poll_timer_fn() 只 wake waitqueue,不会 re-arm)。这个不变式一旦被打破(例如有人让 poll_timer_fn 自装载),旧 race 立即回归。
  2. 5.10.y 用 timer_delete_sync 而非 timer_shutdown_synctimer_delete_sync 之后 timer 理论上仍可被重新 arm;目前靠"之后没人会 arm"这一不变量兜底。若后续有人在 5.10 上加新的 arm 路径,需要重新审视是否会引入新的 UAF。
  3. WARN_ONCE(cgroup->psi->rtpoll_states, ...) 仍保留,意味着 trigger 计数泄漏依然告警;本 patch 只解决 timer 那一段,不处理 trigger 计数本身的泄漏。
  4. Greg KH 的失败邮件是脚本自动转发,5.10.y 的 backport 必须人工核对字段命名差异。这次 Sasha 同步把 commit log 中的 psi_schedule_rtpoll_work() 改为 psi_schedule_poll_work(),与 5.10 的命名保持一致,避免文档与代码对不上。
  5. Reported-by: Sashiko AI -- 这是 AI 扫描发现的 UAF,值得复盘:是否还有别的 cgroup_free / timer_shutdown 类遗漏,例如其他 cgroup 子系统或 work_struct 的同类 race。

版本变化

本 thread 内只有 mainline -> 5.10.y 一次 backport 演进:

  • mainline 5457025fa8ca:使用 cgroup->psi->rtpoll_timer + timer_shutdown_sync()
  • 5.10.y backport:字段嵌入化(-> -> .)、旧 timer API(timer_delete_sync())、变量名(rtpoll_timer -> poll_timer)、commit log 同步改名为 psi_schedule_poll_work()

一句话总结

把 PSI rtpoll timer 的关闭从 destroy 路径下沉到 psi_cgroup_free(),让 timer 与 cgroup 同生死以封堵 UAF race;5.10.y 通过字段嵌入、旧 timer API 与命名差异做适配 backport。