sgbj 在 Google 的 Paul Turner 撰写的 cmets 中提到的The article 更详细地解释了以下内容,但我会试一试:
据我目前有限的信息来看,retpoline 是一个 return trampoline,它使用永远不会执行的无限循环来防止 CPU 推测目标间接跳转。
基本方法可见Andi Kleen's kernel branch解决这个问题:
它引入了新的__x86.indirect_thunk 调用,该调用加载调用目标,其内存地址(我将称之为ADDR)存储在堆栈顶部,并使用RET 指令执行跳转。然后使用 NOSPEC_JMP/CALL 宏调用 thunk 本身,该宏用于替换许多(如果不是全部)间接调用和跳转。如果需要,宏只是将调用目标放在堆栈上并正确设置返回地址(注意非线性控制流):
.macro NOSPEC_CALL target
jmp 1221f /* jumps to the end of the macro */
1222:
push \target /* pushes ADDR to the stack */
jmp __x86.indirect_thunk /* executes the indirect jump */
1221:
call 1222b /* pushes the return address to the stack */
.endm
call 最后的位置是必要的,这样当间接调用完成时,控制流在使用 NOSPEC_CALL 宏之后继续,所以它可以用来代替常规的 call
thunk 本身如下所示:
call retpoline_call_target
2:
lfence /* stop speculation */
jmp 2b
retpoline_call_target:
lea 8(%rsp), %rsp
ret
这里的控制流可能有点混乱,所以让我澄清一下:
-
call 将当前指令指针(标签 2)压入堆栈。
-
lea 将 8 添加到 堆栈指针,有效地丢弃了最近推送的四字,这是最后一个返回地址(到标签 2)。之后,栈顶再次指向真正的返回地址 ADDR。
-
ret 跳转到 *ADDR 并将堆栈指针重置为调用堆栈的开头。
最后,整个行为实际上相当于直接跳转到*ADDR。我们得到的一个好处是用于返回语句的分支预测器(Return Stack Buffer,RSB),在执行call 指令时,假设相应的ret 语句将跳转到标签2。
标签 2 之后的部分实际上永远不会被执行,它只是一个无限循环,理论上会用JMP 指令填充指令管道。通过使用LFENCE、PAUSE 或更一般地,导致指令流水线停止的指令可以阻止 CPU 在这种推测性执行上浪费任何功率和时间。这是因为如果对 retpoline_call_target 的调用正常返回,LFENCE 将是下一条要执行的指令。这也是分支预测器会根据原始返回地址(标签 2)预测的内容
引用英特尔架构手册:
LFENCE 之后的指令可能会在 LFENCE 之前从内存中获取,但在 LFENCE 完成之前它们不会执行。
但是请注意,规范从未提到 LFENCE 和 PAUSE 会导致管道停止,所以我在这里读到了几行之间的内容。
现在回到你原来的问题:
内核内存信息泄露之所以成为可能,是因为结合了两种思路:
即使在推测错误时推测执行应该没有副作用,推测执行仍然会影响缓存层次结构。这意味着当推测性地执行内存加载时,它可能仍然导致缓存行被逐出。可以通过仔细测量映射到同一缓存集的内存的访问时间来识别缓存层次结构中的这种变化。
当内存读取的源地址本身是从内核内存中读取时,您甚至可以泄漏一些任意内存位。
Intel CPU 的间接分支预测器只使用源指令的最低 12 位,因此很容易用用户控制的内存地址毒化所有 2^12 可能的预测历史。然后,当在内核中预测到间接跳转时,可以使用内核权限推测性地执行这些操作。使用缓存定时侧通道,您可以因此泄漏任意内核内存。
更新:在kernel mailing list 上,正在进行的讨论使我相信 retpolines 不能完全缓解分支预测问题,例如当返回堆栈缓冲区 (RSB) 为空时,最近的英特尔架构 (Skylake+) 回退到易受攻击的分支目标缓冲区 (BTB):
Retpoline 作为一种缓解策略,将间接分支换成回报,
避免使用来自 BTB 的预测,因为它们可以
被袭击者毒死。
Skylake+ 的问题是 RSB 下溢回退到使用
BTB 预测,允许攻击者控制猜测。