Linux 内核 SRSO(Speculative Return Stack Overflow / CVE-2023-20569)缓解机制深度指南
导读
本文基于 Linux 内核源码树中的官方文档 Documentation/admin-guide/hw-vuln/srso.rst 展开,系统讲解针对 AMD 处理器"推测式返回栈溢出"(Speculative Return Stack Overflow,SRSO)漏洞(CVE-2023-20569)的内核缓解机制:如何解读 sysfs 中的缓解状态、如何通过 spec_rstack_overflow 内核命令行参数选择与切换缓解策略、Safe RET 缓解的底层工作原理,以及如何用性能计数器和官方 selftest 验证缓解确实生效。读完本文,你将能独立完成 AMD Zen1~Zen4 平台上 SRSO 漏洞的检测、缓解配置与生效性验证。
SRSO 漏洞是什么
SRSO 是 2023 年披露的、影响 AMD 处理器的一类推测执行(speculative execution)侧信道漏洞,在 Linux 内核中由 arch/x86/kernel/cpu/bugs.c 的相应逻辑负责检测与缓解,官方追踪编号为 CVE-2023-20569。
其攻击形态与业界熟知的 Spectre/Meltdown/Retbleed 同属一类:攻击者先污染 CPU 的功能单元——此处是分支目标缓冲(Branch Target Buffer,BTB)与返回地址预测器(Return Address Predictor,RAP)——再诱导高特权域(内核)把敏感数据泄漏出来。
AMD 处理器使用 RAP(也称 Return Address Stack / Return Stack Buffer,RSB)来预测 RET 指令的目标。问题在于:某些情况下,一条非架构性 CALL 指令(即 CPU 预测它是 CALL、但实际并非 CALL 的指令)也会在 RAP 中创建条目,而这个条目随后可能被用来预测后续某条 RET 指令的目标。
触发这种状态的具体微架构条件随代际而不同,但核心风险是一致的:攻击者可以"错训"(mis-train)内核空间的 BTB,使其把非架构性 CALL 预测到攻击者控制的地址上,从而劫持某条内核 RET 指令的推测目标,进而通过推测式侧信道实现信息泄露。从源码结构看,内核把这套攻击向量抽象为 User→Kernel、Guest→Host、User→User、Guest→Guest 四类,缓解策略的选择正是围绕这些权限域穿越场景展开的。
受影响处理器
| 处理器世代 | Family | 备注 |
|---|---|---|
| AMD Zen 1 | 0x17 | 受影响 |
| AMD Zen 2 | 0x17 | 受影响 |
| AMD Zen 3 | 0x19 | 受影响 |
| AMD Zen 4 | 0x19 | 受影响 |
即所有 Family 0x17 与 0x19(Zen 1 至 Zen 4 全部代际)均受影响,更老型号的处理器尚未被调查确认。内核代码中以 boot_cpu_has_bug(X86_BUG_SRSO) 判定当前 CPU 是否带该漏洞(参见 arch/x86/kernel/cpu/bugs.c),而 selftest 则通过 CPUID(1).EAX 是否落在 0x00800f00~0x00afffff 区间来判断运行环境是否属于 Zen[1-4],见 tools/testing/selftests/x86/srso.c。
检测系统信息与读取缓解状态
要让缓解真正生效,首先必须为系统加载包含 SRSO 修复的最新微码(microcode)。这是所有后续缓解措施生效的前提条件。
内核启动后,可通过以下 sysfs 文件查看 SRSO 缓解状态:
/sys/devices/system/cpu/vulnerabilities/spec_rstack_overflow
该文件的实际内容由内核在 arch/x86/kernel/cpu/bugs.c 的 srso_show_state() 中写入:若 CPU 不含 X86_BUG_SRSO 漏洞,则直接输出 Not affected(同一文件第 L3610 行)。展示函数 cpu_show_spec_rstack_overflow() 定义在 arch/x86/kernel/cpu/bugs.c,字符串表 srso_strings[] 位于 arch/x86/kernel/cpu/bugs.c。
sysfs 文件中的可能取值
以下取值与文档描述对应(个别取值在本仓库内核版本中的实际字符串略有差异,已用"实际为"标注):
| sysfs 输出 | 含义 |
|---|---|
Not affected |
处理器不受该漏洞影响(不携带 X86_BUG_SRSO)。 |
Vulnerable |
处理器存在漏洞,且未应用任何缓解。 |
Vulnerable: No microcode |
处理器存在漏洞,但尚未加载可扩展 IBPB 功能以解决该漏洞的微码。 |
Vulnerable: Safe RET, no microcode |
内核已应用 "Safe RET" 缓解,但未加载扩展 IBPB 的微码;用户态任务可能仍处于易受攻击状态。 |
Vulnerable: Microcode, no safe RET |
已应用扩展 IBPB 功能的微码补丁,但未解决 User→Kernel 与 Guest→Host 穿越保护;它只能覆盖 User→User 与 VM→VM 攻击向量。(对应 spec_rstack_overflow=microcode) |
Mitigation: Safe RET |
结合微码与软件的完整缓解,补齐了 User→Kernel 与 Guest→Host 穿越保护;默认选择,也可通过 spec_rstack_overflow=safe-ret 显式指定。 |
Mitigation: IBPB |
与 Safe RET 保护范围类似,但在权限域穿越(User→Kernel、Guest→Host)时执行一次 IBPB 屏障。(对应 spec_rstack_overflow=ibpb) |
Mitigation: IBPB on VMEXIT(本仓库实际为 Mitigation: IBPB on VMEXIT only) |
仅针对云服务商场景的 Guest→Host 穿越。(对应 spec_rstack_overflow=ibpb-vmexit) |
Mitigation: Reduced Speculation |
见下文 BpSpecReduce 说明。 |
说明:本仓库中 arch/x86/kernel/cpu/bugs.c 的
srso_strings[]还额外定义了一个文档中未单列的取值Mitigation: SMT disabled(对应SRSO_MITIGATION_NOSMT)——当处理器为 Zen1/Zen2 且系统不可能开启 SMT(cpu_smt_possible()为假)时自动选用,因为此类配置下不再受 SRSO 影响。
Reduced Speculation(BpSpecReduce)的自动启用逻辑
"Reduced Speculation" 属于一种自动叠加生效的缓解,分两种情况:
- 当选中上述 "IBPB on VMEXIT" 且 CPU 支持 BpSpecReduce 位时,会进一步自动升级为 "Reduced Speculation"。BpSpecReduce 相比 IBPB on VMEXIT 性能开销小得多,同时仍能覆盖 guest→host 攻击向量。
- 当 CPU 具备
SRSO_USER_KERNEL_NO=1的 CPUID 位时也会自动启用。此时说明 user/kernel 边界已不再受影响,Safe RET 不再必要,代码逻辑会自动切到=ibpb-vmexit一类缓解。
内核中的这段自动决策逻辑实现在 arch/x86/kernel/cpu/bugs.c 的 switch 分支中:一旦选中 SRSO_MITIGATION_IBPB_ON_VMEXIT 且 boot_cpu_has(X86_FEATURE_SRSO_BP_SPEC_REDUCE) 为真,就打印 "Reducing speculation to address VM/HV SRSO attack vector." 并切到 SRSO_MITIGATION_BP_SPEC_REDUCE。该 feature 位同时控制 KVM 中 BpSpecReduce MSR 位的切换(见 arch/x86/kernel/cpu/bugs.c 的注释与 srso_apply_mitigation())。
与 Spectre v2 用户态策略的联动
需要注意,User→User 缓解强度由 Spectre v2 缓解中的 IBPB 环节如何配置决定:
- conditional IBPB(条件式):每个进程可通过
PR_SPEC_DISABLE/PR_SPEC_ENABLE等 prctl 自行决定是否需要在其周边执行 IBPB,参见 Spectre v2 文档; - strict(严格模式,总是开启):在内核命令行提供
spectre_v2_user=on。
缓解选项与内核命令行配置
SRSO 的缓解策略通过早参数(early param)spec_rstack_overflow 配置。解析代码位于 arch/x86/kernel/cpu/bugs.c 的 srso_parse_cmdline(),所有取值在启动早期即被记录到静态变量 srso_mitigation 中:
| 内核命令行参数 | 对应缓解 | 说明 |
|---|---|---|
spec_rstack_overflow=off |
无(Vulnerable) |
显式关闭缓解。 |
spec_rstack_overflow=microcode |
仅微码 | 依赖扩展 IBPB 功能的微码,覆盖 User→User、VM→VM 向量。 |
spec_rstack_overflow=safe-ret |
Safe RET | 软件 + 微码的完整缓解,默认值。 |
spec_rstack_overflow=ibpb |
IBPB | 在权限域穿越点执行 IBPB 的完整缓解。 |
spec_rstack_overflow=ibpb-vmexit |
IBPB on VMEXIT | 面向云服务商,只覆盖 Guest→Host。 |
传入未知参数时内核会打印 Ignoring unknown SRSO option 并忽略之(见 arch/x86/kernel/cpu/bugs.c)。
默认策略的自动选择(源码视角)
若用户未显式指定(状态为 SRSO_MITIGATION_AUTO),内核在 srso_select_mitigation()(arch/x86/kernel/cpu/bugs.c)中按下述逻辑裁决:
- CPU 无
X86_BUG_SRSO→NONE,直接返回。 - 若需要保护 Guest→Host,或需要保护 User→Kernel 且 CPU 不具备
SRSO_USER_KERNEL_NO位 → 选 Safe RET; - 否则若只需保护 User→User 或 Guest→Guest → 选 microcode 缓解;
- 都不需要 →
NONE。 - 若 CPU 是 Zen1/Zen2 且 SMT 不可能开启 → 直接返回
NOSMT("SMT disabled"),因为这些处理器在 SMT 关闭时本就不受 SRSO 影响。 - 检查扩展 IBPB 功能的微码是否就位:缺少
X86_FEATURE_IBPB_BRTYPE时打印IBPB-extending microcode not applied!并降级为UCODE_NEEDED/SAFE_RET_UCODE_NEEDED(Safe RET 无需微码也能提供部分缓解)。 - 依据编译配置兜底检查:Safe RET 要求内核编译时开启
CONFIG_MITIGATION_SRSO;IBPB/IBPB on VMEXIT 要求开启CONFIG_MITIGATION_IBPB_ENTRY,否则强制NONE并打印对应告警。
此外,srso_update_mitigation()(arch/x86/kernel/cpu/bugs.c)还处理一个联动场景:若 Retbleed 缓解已选用 IBPB,且系统具备 X86_FEATURE_IBPB_BRTYPE 微码,则 Retbleed 的 IBPB 对 SRSO 同样有效,SRSO 会自动升级为 Mitigation: IBPB。
性能权衡与默认选择建议
考虑到各类缓解的性能影响,默认选择是 Safe RET(Mitigation: Safe RET),它应能覆盖绝大多数攻击向量,包括本地的 User→Kernel 场景。默认设置在出现新攻击向量时会重新评估。
必须坦率指出:Safe RET 会依据工作负载带来一定的性能开销。如果系统管理员充分信任自己的用户态程序、且不愿承受该性能损失,可以随时用 spec_rstack_overflow=off 关闭缓解。同理,Mitigation: IBPB 是另一种完整缓解,但在穿越点执行间接分支预测屏障同样带来性能代价。
成功利用所需的前提条件
作为运维/安全评估参考,文档明确给出成功利用该漏洞需要满足的条件,这也解释了为何 SRSO 属于"本地、且需要较强前置条件"的漏洞:
- 获得机器的本地访问权限;
- 攻破 kASLR(内核地址空间布局随机化);
- 在运行中的内核里找到可用于利用的 gadget;
- 视微架构而定,可能需要在兄弟线程上创建并绑定额外工作负载(Family 0x19 上则不需要);
- 最后运行利用程序。
深入 Safe RET 缓解的实现原理
Safe RET 是整个缓解体系中最核心的部分。其总体思路是:让所有 RET 指令都推测到一个受控位置——这与 retpoline 序列控制推测的方式类似。具体做法是让 __x86_return_thunk 强制 CPU 对每一个函数返回都"猜错",从而落入一条"安全返回"(safe return)序列。
在内核构建中,编译器通过 -mfunction-return=thunk-extern 把所有函数返回处的 RET 替换为跳转到 __x86_return_thunk(该符号在 arch/x86/lib/retpoline.S 中定义,其注释明确说明该函数名是"magical"的)。真正生效的返回 thunk 由 srso_apply_mitigation() 在启动后期通过 set_return_thunk() 安装(arch/x86/kernel/cpu/bugs.c)。
为了让该缓解本身安全,内核必须保证 safe return 序列自身不受攻击者干扰。由于不同代际的 BTB 结构不同,实现分两种:
Zen3 / Zen4:BTB 别名(aliasing)方案
在 Zen3 与 Zen4 上(Family 0x19,内核判定为 boot_cpu_data.x86 == 0x19),通过构造 BTB 别名来完成"冲刷 + 复用":
srso_alias_untrain_ret()(未训化函数)被放置到 2M 对齐的特殊地址;srso_alias_safe_ret()(安全返回函数)与前者处于同一个 2M 页面内,但其虚拟地址的 第 2、8、14、20 位被置位(而 untrain 函数对应位为清零)。
这两个地址因此在 Zen3/Zen4 的 BTB 中必然互为别名:执行 untrain 会驱逐该 BTB 槽位上任何可能被污染的条目,于是随后所有函数返回都命中这一个干净的 safe ret 槽位。相关汇编与地址约束注释见 arch/x86/lib/retpoline.S,实际安装的是 srso_alias_return_thunk。
Zen1 / Zen2:重解释(reinterpretation)方案
在更老的 Zen1 与 Zen2(Family 0x17)上,采用与 Retbleed 缓解类似的"重解释"手法,对应符号为 srso_untrain_ret() 与 srso_safe_ret()。其关键技巧是(见 arch/x86/lib/retpoline.S):
srso_untrain_ret()本质上是一条被"重新解释"的指令——一条movabs $0xccccc30824648d48,%rax的字节序列被精心摆放:当返回 thunk 稍后执行到内部标签srso_safe_ret()时,它表现为栈指针调整 + 一条 RET;- 这条 RET 会被 CPU 预测错(mispredict)而撞进陷阱指令(
UD2),随后实际执行流沿真实栈顶的返回地址继续——这正是"安全返回"的含义(retpoline.S 第 L255-L260 行注释)。
无论哪种方案,srso_return_thunk / srso_alias_return_thunk 的公共形态都是 call <safe_ret>; ud2:正常路径不会真正执行到该返回点,推测执行则被引导到陷阱。
若内核启动时相关编译配置(
CONFIG_MITIGATION_SRSO)缺失,Safe RET 无法应用,内核会打印WARNING: kernel not compiled with MITIGATION_SRSO.并把状态置为Vulnerable。
验证 Safe RET 缓解确实生效
由于 Safe RET 的设计目标是"让内核态每条 RET 都推测错误",这天然提供了一个可观测的验证方法:用两个性能计数器对比内核态"已退休的 RET 数"与"已退休且预测错误的 RET 数"。
- PMC 0xc8:退休的 RET/RET lw(near return)计数;
- PMC 0xc9:退休且预测错误的 RET/RET lw 计数。
在 Intel/AMD 平台上也可用 perf list 查看预定义事件名:
# perf list ex_ret_near_ret
其输出应包含:
List of pre-defined events (to be used in -e or -M):
core:
ex_ret_near_ret
[Retired Near Returns]
ex_ret_near_ret_mispred
[Retired Near Returns Mispredicted]
随后可任选以下两种方式之一采样内核态(:k 修饰符)的返回指令行为:
# 方式一:使用事件助记符
perf stat -e ex_ret_near_ret:k -e ex_ret_near_ret_mispred:k sleep 10s
# 方式二:使用原始 PMC 编号
perf stat -e cpu/event=0xc8,umask=0/k -e cpu/event=0xc9,umask=0/k sleep 10s
缓解生效时:两者几乎相等
当 Safe RET 正常工作时,每一条退休的 RET 都应是被预测错误的,即 0xc8 ≈ 0xc9。文档给出的真实采样结果如下:
Performance counter stats for 'sleep 10s':
137,167 cpu/event=0xc8,umask=0/k
137,173 cpu/event=0xc9,umask=0/k
10.004110303 seconds time elapsed
0.000000000 seconds user
0.004462000 seconds sys
可以看到两个计数几乎完全一致(137,167 vs 137,173)。
缓解未生效 / 被关闭时:两者差距悬殊
对比当缓解被禁用(spec_rstack_overflow=off)或未能正常工作时,同样的 10 秒采样窗口内,预测错误的退休 RET 数量只占极小比例:
Performance counter stats for 'sleep 10s':
201,627 cpu/event=0xc8,umask=0/k
4,074 cpu/event=0xc9,umask=0/k
10.003267252 seconds time elapsed
0.002729000 seconds user
0.000000000 seconds sys
201,627 条退休 RET 中仅有 4,074 条预测错误,说明绝大多数 RET 都被 BTB/RAP 正常预测——此时 Safe RET 未在起作用。
官方 selftest:一键验证
内核还附带了一个完成上述验证的 selftest。进入 tools/testing/selftests/x86/ 目录执行:
make srso
./srso
该程序 tools/testing/selftests/x86/srso.c 的内部逻辑与上文手工流程完全一致:
- 先通过
__cpuid(1, ...)检查 CPUID,仅在 Zen[1-4](CPUID(1).EAX处于0x00800f00~0x00afffff)上继续运行,否则直接退出(第 L19-L25 行); - 用
syscall(SYS_perf_event_open, ...)以PERF_TYPE_RAW打开0xc8(RET 退休)与0xc9(RET 预测错误)两个计数器,并设置exclude_user = exclude_hv = 1,即只统计内核态(第 L27-L48 行); - 通过 ioctl 复位并启用计数器,
sleep(10)后停表读数(第 L50-L63 行); - 打印两个计数值,并给出结论:当两个计数几乎相等时,SRSO Safe-RET 缓解工作正常(第 L65-L68 行)。
Sleeping for 10 seconds
RETs: (137167 retired <-> 137173 mispredicted)
SRSO Safe-RET mitigation works correctly if both counts are almost equal.
实战排查速查表
| 目标 | 手段 |
|---|---|
| 查看当前 SRSO 缓解状态 | cat /sys/devices/system/cpu/vulnerabilities/spec_rstack_overflow |
| 内核态 RET 计数采样 | perf stat -e cpu/event=0xc8,umask=0/k -e cpu/event=0xc9,umask=0/k sleep 10s |
| 一键验证 Safe RET | 进入 tools/testing/selftests/x86/ 执行 make srso && ./srso |
| 显式指定缓解策略 | 内核命令行追加 spec_rstack_overflow=safe-ret / ibpb / ibpb-vmexit / microcode / off |
| 彻底关闭缓解(不推荐) | spec_rstack_overflow=off |
关键源码索引
| 主题 | 位置 |
|---|---|
| SRSO 缓解状态字符串表 | arch/x86/kernel/cpu/bugs.c |
| 命令行参数解析 | arch/x86/kernel/cpu/bugs.c |
| 缓解自动选择逻辑 | arch/x86/kernel/cpu/bugs.c |
| 缓解应用与 thunk 安装 | arch/x86/kernel/cpu/bugs.c |
| sysfs 展示入口 | arch/x86/kernel/cpu/bugs.c、arch/x86/kernel/cpu/bugs.c |
| Zen3/4 alias 与 Zen1/2 重解释汇编序列 | arch/x86/lib/retpoline.S |
编译器替换目标 __x86_return_thunk |
arch/x86/lib/retpoline.S |
| 官方验证 selftest | tools/testing/selftests/x86/srso.c |
| 相关参考:Spectre v2(IBPB/PR_SPEC 联动) | Documentation/admin-guide/hw-vuln/spectre.rst |
最后仍需强调文档末尾的建议:像对待所有推测执行类漏洞一样,及时且持续地应用系统软件更新是维持有效防护的基础;SRSO 缓解策略的默认选择(Safe RET)也会随新攻击向量的出现而被内核开发者重新评估与调整,因此保持内核与微码同步更新至关重要。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0630
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00