sgbj在Google的Paul Turner撰写的评论中提到的文章更详细地解释了以下内容,但我会试一试:
就目前为止我可以从有限的信息中总结出来的是,retpoline是一个返回的蹦床,它使用了一个无限循环,该循环从未执行过,可以防止CPU在间接跳转的目标上进行推测。
基本方法可以在Andi Kleen的内核分支中解决此问题:
它引入了新的__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
重击本身看起来如下:
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
。我们得到的一个好处是,在执行call
指令时,用于返回语句的分支预测变量(Return Stack Buffer,RSB)假定相应的ret
语句将跳转到标签2。
标签2之后的部分实际上从未执行过,它只是一个无限循环,理论上将用JMP
指令填充指令流水线。通过使用LFENCE
,PAUSE
或更一般而言,使用导致指令流水线停止的指令,可以阻止CPU在这种推测性执行上浪费任何功率和时间。这是因为如果对retpoline_call_target的调用正常返回,LFENCE
则将是下一条要执行的指令。这也是分支预测程序根据原始返回地址(标签2)预测的结果
引用英特尔架构手册:
可以在LFENCE之前从内存中提取LFENCE之后的指令,但是直到LFENCE完成后它们才执行。
但是请注意,该规范从未提及LFENCE和PAUSE会导致管道停顿,因此我在这里在两行之间读了一点。
现在回到您的原始问题:由于以下两种思想的结合,可以公开内核内存信息:
即使在推测错误时推测执行应该没有副作用,但推测执行仍会影响缓存层次结构。这意味着当以推测方式执行内存加载时,它仍然可能导致缓存行被驱逐。可以通过仔细测量对映射到同一高速缓存集的内存的访问时间来确定高速缓存层次结构中的此更改。
当读取的内存源地址本身是从内核内存中读取的时,您甚至可能泄漏任意内存的某些位。
英特尔CPU的间接分支预测变量仅使用源指令的最低12位,因此很容易使用用户控制的存储器地址来破坏所有2 ^ 12个可能的预测历史。然后,当在内核中预测到间接跳转时,就可以使用内核特权进行推测性执行。因此,使用缓存定时辅助通道可以泄漏任意内核内存。
更新:在内核邮件列表上,正在进行的讨论使我相信,回传不能充分缓解分支预测问题,因为当返回堆栈缓冲区(RSB)空着时,较新的Intel体系结构(Skylake +)会退缩到易受攻击的分支目标缓冲区(BTB):
Retpoline作为缓解策略,将间接分支机构换成回报,以避免使用来自BTB的预测,因为这些预测可能会受到攻击者的毒害。Skylake +的问题在于RSB下溢回落到使用BTB预测,这使攻击者可以控制推测。