在 LoongArch 上,内核约定使用 $r21(也称 $u0)保存当前 CPU 的 percpu 基址;内核态运行期间,该寄存器应始终指向当前核心的 percpu 区域。
缺陷成因
LoongArch 的 rethook 跳板在进入时把 $r21 保存到栈上,退出时再从栈中恢复。如果任务在 handler 执行期间迁移到其他 CPU,跳板退出时会把旧 CPU 的基址写回寄存器;此时新 CPU 上的任务,$r21 就变成旧 CPU 的 percpu 基地址。
故障表现
故障表现包括:调度器数据错乱,出现 scheduling-while-atomic 崩溃报错;RCU 报出“跑在错误 CPU 上”的警告;空闲 CPU 负载平衡校验失败(WARN_ON_ONCE(rq != this_rq()));CPU 进入空闲循环后定时器不再重新启动,最终硬锁死。
触发条件
该缺陷需要三个条件同时满足:系统运行在 LoongArch 上;有模块在内核高频调用的函数上挂载探针;内核启用了完全抢占(preempt=full)。
该缺陷从 Linux 6.3 版本的老 kretprobe 实现起就已存在;代码迁移到 rethook 后,这套错误逻辑被直接沿用。
修复与临时规避
修复方案是删除 rethook 跳板中对 $r21 的保存与恢复,因为迁移后该寄存器本身已持有正确的当前 CPU 基址。补丁修改 arch/loongarch/kernel/rethook_trampoline.S,来源记录为新增 0 行、删除 2 行。
临时规避方式是在内核启动参数中加入 preempt=none 或 preempt=voluntary,绕过会触发调度的让出点。该方式要求内核编译时启用了 PREEMPT_DYNAMIC,否则启动参数不生效。
Linux 5.12 引入的 PREEMPT_DYNAMIC 允许内核在编译时声明一个默认抢占模式,并在启用该选项后,于启动时通过 preempt= 参数覆盖它,无需重新编译内核。
本文事实依据:deepin 实践文章。


