Ber*_*ach 4 cpu x86 caching cpu-architecture cpu-cache
如果新的CPU有一个缓存缓冲区,如果提交的指令只提交给实际的CPU缓存,那么类似于Meltdown的攻击仍然可能吗?
建议是让推测性执行能够从内存加载,但在实际提交之前不要写入CPU缓存.
TL:DR:是的,我认为它会解决当前形式的Spectre(和Meltdown)(使用刷新+读取缓存 - 时序侧通道从物理寄存器复制秘密数据),但可能过于昂贵(电力成本) ,也许还有性能)是一个可能的实现.
但是对于超线程(或者更一般地说是任何SMT),如果你可以误推测使用秘密数据运行数据相关的ALU指令而不是将其用作数组索引,那么还有一个ALU /端口压力侧通道. Meltdown论文在关注flush + reload cache-timing side-channel之前讨论了这种可能性.(对于Meltdown而言,它比Spectre更可行,因为您可以更好地控制何时使用秘密数据).
因此,修改缓存行为不会阻止攻击.但是,它会夺走可靠的通道,以便将秘密数据带入攻击过程.(即ALU定时具有更高的噪声,因此具有更低的带宽以获得相同的可靠性; Shannon嘈杂的信道定理),并且您必须确保您的代码与受攻击的代码在同一物理内核上运行.
在没有SMT的CPU上(例如Intel的桌面i5芯片),ALU时序侧通道很难与Spectre一起使用,因为您无法在没有权限的代码上直接使用perf计数器.(但是,通过使用Linux计算自己的ALU指令,仍然可以利用Meltdown perf).
特别是Meltdown 更容易防御,微体系结构,更简单和更便宜的更改CPU的硬连线部分,微码更新无法重新连接.
您不需要阻止推测性加载影响缓存; 如果TLB命中的负载在达到退役时会出现故障,那么改变可能就像让推测执行继续一样简单,但是0由于对TLB条目的权限检查失败而推迟执行后续指令所使用的值.
因此,错误推测(在故障负载之后secret)touch array[secret*4096]负载总是会使相同的高速缓存行变热,而没有与秘密数据相关的行为.秘密数据本身将进入缓存,但不是物理寄存器.(这也会阻止ALU /端口压力侧通道.)
阻止故障负载甚至首先将"秘密"行放入缓存可能会使得更难以区分内核映射和未映射页面之间的区别,这可能有助于防止用户空间试图通过查找来击败KASLR内核映射的虚拟地址.但那并不是崩溃.
幽灵是一个很难的,因为错误推测的指令对微架构状态进行数据依赖性修改确实有权读取秘密数据. 是的,与存储队列类似的"加载队列"可以解决这个问题,但有效地实现它可能会很昂贵.(特别是考虑到我在编写第一部分时没有想到的缓存一致性问题.)
(还有其他方法可以实现你的基本想法;也许甚至有一种可行的方法.但L1D线上用于跟踪其状态的额外位有缺点,并且显然不容易.)
存储队列跟踪存储执行,直到它们提交到L1D缓存.(商店在退休之前不能承诺L1D,因为这是他们被认为是非推测性的点,因此可以使其他核心全局可见).
加载队列必须存储整个传入的缓存行,而不仅仅是已加载的字节.(但请注意,Skylake-X可以执行64字节的ZMM存储,因此它的存储缓冲区条目必须是缓存行的大小.但是如果它们可以互相借用空间,那么可能没有64 * entries字节可用的存储空间,也就是说,只有全部数量的条目可用于标量或窄矢量存储.我从来没有读过关于这样的限制的任何内容,所以我认为没有一个,但这似乎是合理的)
更严重的问题是英特尔目前的L1D设计有2个读端口+ 1个写端口.(也许还有另一个端口用于写入从L2到达并行提交存储的行?在英特尔Skylake上存储循环的意外差和奇怪的双峰性能上进行了一些讨论.)
如果加载的数据在负载退出之后才能进入L1D,那么它们可能会竞争存储使用的相同写端口.
但是,L1D中的负载仍然可以直接来自L1D,并且内存顺序缓冲区中的负载仍然可以在每个时钟2执行.(MOB现在将包括这个新的加载队列以及通常的存储队列+用于加载的标记来维护x86内存排序语义).您仍然需要两个L1D读取端口来维持不接触大量新内存的代码的性能,并且主要是重新加载L1D中一直很热的内容.
这将使MOB大约两倍(就数据存储而言),尽管它不再需要任何条目.据我了解,当前Intel CPU中的MOB由各个加载缓冲区和存储缓冲区条目组成.(Haswell分别有72和42).
嗯,更复杂的是MOB中的负载数据必须保持与其他核心的高速缓存一致性.这与存储数据非常不同,存储数据是私有的,并且在提交到L1D之前不会全局可见/不是全局存储器顺序和缓存一致性的一部分.
因此,如果没有调整,这个提议的"加载队列"实现机制可能是不可行的:它必须通过来自其他内核的无效请求来检查,因此这是MOB中需要的另一个读端口.
任何可能的实现都会遇到需要稍后像商店一样提交到L1D的问题.我认为,当它从非核心到达时,不能驱逐+分配新线路将是一个重大的负担.
(即使允许推测性驱逐而不是冲突中的推测性替换,也会导致可能的缓存定时攻击.你会对所有线路进行填充,然后进行一次从一组线路或另一条线路中驱逐一条线路的负载,并找出被驱逐的线路而不是使用类似的缓存时序侧信道获取哪一个.因此,使用L1D中的额外位来查找/逐出从错误推测中恢复期间加载的线路将不会消除此侧信道.)
脚注:所有说明都是推测性的.这个问题措辞得很好,但我认为很多人在阅读有关OoO执行官和思考Meltdown/Spectre的内容时陷入了混淆投机执行与错误推测混淆的陷阱.
请记住,所有指令在执行时都是推测性的.直到退休才知道这是正确的推测.Meltdown/Spectre依赖于访问秘密数据并在误推测期间使用它.但目前OoO CPU设计的基础是你不知道你是否正确推测; 一切都是投机直到退休.
任何加载或存储都可能发生故障,一些ALU指令也是如此(例如,如果异常未被屏蔽,则为浮点),因此任何"仅在推测性地执行时"应用的性能成本实际上始终适用.这就是为什么商店不能从存储队列到L1D提交,直到后店里微指令已经从乱序CPU核心(与存储队列存储数据)退休.
但是,我认为有条件和间接分支是专门处理的,因为它们预计会在某些时候误推测,并且优化它们的恢复非常重要.现在的CPU使用分支做得更好,而不仅仅是在检测到错误预测时回滚到当前的退出状态,我想使用某种检查点缓冲区.因此,在分支可以在恢复期间继续执行指令的无序执行.
但是循环和其他分支是非常常见的,因此大多数代码也在这种意义上"推测性地"执行,至少有一个分支回滚检查点尚未被验证为正确的推测.大多数情况下,这是正确的推测,因此不会发生回滚.
对错误推测内存排序或故障负载的恢复是一个完整的管道核心,回滚到退休架构状态.所以我认为只有分支使用分支检查点微体系结构资源.
无论如何,所有这些都是让斯佩特如此阴险的原因:在事实发生之后,CPU无法区分误推测和正确推测之间的区别.如果它知道它是错误的推测,它将启动回滚而不是执行无用的指令/ uops.间接分支也不罕见(在用户空间中); 每个DLL或共享库函数调用在Windows和Linux上使用普通可执行文件中的一个.