关于 RIDL 漏洞和负载 "replaying"

About the RIDL vulnerabilities and the "replaying" of loads

我正在尝试了解 RIDL class 的漏洞。

这是一个 class 漏洞,能够从各种微架构缓冲区读取陈旧数据。
今天已知的漏洞利用:LFB、加载端口、eMC 和存储缓冲区。

链接的论文主要关注 LFB。

我不明白为什么 CPU 会满足 LFB 中陈旧数据的负载。
我可以想象,如果负载在 L1d 中命中,它在内部 "replayed" 直到 L1d 将数据带入 LFB,向 OoO 核心发出信号以停止 "replaying" 它(因为数据读取现在有效)。

不过我不确定 "replay" 到底是什么意思。
我认为负载被分派到一个负载能力端口,然后记录在负载缓冲区(在 MOB 中),并最终根据需要保留,直到它们的数据可用(由 L1 发出信号)。
所以我不确定 "replaying" 是如何发挥作用的,此外,为了让 RIDL 工作,每次尝试 "play" 加载也应该解除阻塞相关指令。
这对我来说似乎很奇怪,因为 CPU 需要跟踪加载正确完成后要重播的指令。

关于 RIDL 的论文使用此代码作为示例(不幸的是我不得不将其粘贴为图像,因为 PDF 布局不允许我复制它):

它可以工作的唯一原因是 CPU 将首先用陈旧的数据满足第 6 行的负载,然后重放它。
这似乎证实了以下几行:

Specifically, we may expect two accesses to be fast, not just the one corresponding to the leaked information. After all, when the processor discovers its mistake and restarts at Line 6 with the right value, the program will also access the buffer with this index.

但我希望 CPU 在转发 LFB(或任何其他内部缓冲区)中的数据之前检查加载地址。
除非 CPU 实际上重复执行加载,直到它检测到加载的数据现在有效(即重播)。
但是,为什么每次尝试都会解锁相关指令?

重放机制究竟是如何工作的(如果它存在的话),以及它如何与 RIDL 漏洞交互?

replay = 再次从 RS(调度程序)调度。 (这不是你整个问题的完整答案,只是关于什么是重播的部分。虽然我认为这涵盖了大部分内容,包括解除阻塞相关的 uops。)

此答案的部分内容对加载重播存在误解。

参见discussion in chat - 重播依赖于拆分或cache-miss 负载的微指令,而不是负载本身。 (除非负载在循环中依赖于自身,就像我一直在做的测试>.<)。 TODO:修复此答案的其余部分和其他答案。


事实证明,cache-miss 加载 不会 只是坐在加载缓冲区中,并在数据到达时唤醒相关的 uops。调度程序必须 re-dispatch 加载 uop 才能实际读取数据,并 write-back 到物理寄存器。 (并放在转发网络上,依赖uops可以在下一个周期读取它。)

因此 L1 未命中/L2 命中将导致分派 2 倍的负载微指令。 (调度程序是乐观的,L2 是 on-core,因此 L2 命中的预期延迟是固定的,这与 off-core 响应的时间不同。IDK 如果调度程序继续对到达特定时间的数据持乐观态度L3的时间。)


RIDL 论文提供了一些有趣的证据,证明加载 uops 实际上直接与 LFB 交互,而不是等待传入数据放入 L1d 并从那里读取它。


我们可以在实践中最容易地观察 cache-line-split 负载的重放,因为重复造成这种情况比缓存未命中更微不足道,需要更少的代码。 uops_dispatched_port.port_2port_3 的计数大约是只拆分负载的循环的两倍。 (我已经在 Skylake 上使用与 中基本相同的循环和测试程序在实践中验证了这一点)

检测到拆分(仅在 address-calculation 之后可能)的负载将加载数据的第一部分,而不是向 RS 发送成功完成信号,将此结果放入拆分缓冲区1 将在 uop 第二次调度时与来自第二缓存行的数据相结合。 (假设这两个时间都不是缓存未命中,否则它也会为此进行重播。)


当负载 uop 调度时,调度程序预计它将在 L1d 中命中并调度相关的 uops,以便它们可以在负载将它们放在该总线上的周期中从转发网络读取结果。

如果 没有 发生(因为加载数据没有准备好),依赖的 uops 也必须重放。同样,IIRC 这可以通过 dispatch 到端口的性能计数器观察到。


现有问答与 uop 在英特尔上重播的证据 CPUs:

  • 指出,重播的可能性意味着 RS 需要保留一个 uop,直到执行单元向 RS 发回成功完成的信号。它不能在第一次发送时放弃一个 uop(就像我第一次写那个答案时猜到的那样)。

脚注 1:

我们知道拆分缓冲区的数量有限;有一个 ld_blocks.no_sr 计数器用于计算因缺少负载而停止的负载。我推断它们在装载端口,因为这是有道理的。 Re-dispatching同一个load uop会发送到同一个load port,因为在issue/rename时uops被分配到端口。尽管可能有一个共享的拆分缓冲区池。


里德尔:

乐观调度是产生问题的机制的一部分。更明显的问题是让后面的 uops 的执行看到来自 LFB 的“垃圾”内部值,就像在 Meltdown 中一样。

http://blog.stuffedcow.net/2018/05/meltdown-microarchitecture/ 甚至表明 PPro 中的崩溃负载暴露了微架构状态的各种位,就像最新处理器中仍然存在的这个漏洞一样。

The Pentium Pro takes the “load value is a don’t-care” quite literally. For all of the forbidden loads, the load unit completes and produces a value, and that value appears to be various values taken from various parts of the processor. The value varies and can be non-deterministic. None of the returned values appear to be the memory data, so the Pentium Pro does not appear to be vulnerable to Meltdown.

The recognizable values include the PTE for the load (which, at least in recent years, is itself considered privileged information), the 12th-most-recent stored value (the store queue has 12 entries), and rarely, a segment descriptor from somewhere.

(稍后 CPUs,从 Core 2 开始,暴露 L1d 缓存中的值;这就是 Meltdown 漏洞本身。但是 PPro / PII / PIII 不受 Meltdown 的影响。显然 [=在这种情况下,62=] 容易受到 RIDL 攻击。)

因此,将微体系结构状态的位暴露给推测执行的英特尔设计理念是相同的。

在硬件中将其压缩为 0 应该很容易解决;加载端口已经知道它不成功,因此根据 success/fail 屏蔽加载数据应该希望只增加几个额外的门延迟,并且可以在不限制时钟速度的情况下实现。 (除非加载端口中的最后一个流水线阶段已经是 CPU 频率的关键路径。)

因此可能是未来 CPU 的一种简单且廉价的硬件修复,但很难通过现有 CPU 的微代码和软件来缓解。

我不认为来自 RS 的加载重放与 RIDL 攻击有关。因此,我不会解释什么是负载重播(@Peter 的回答是一个很好的起点),我将根据我对 RIDL 论文中提供的信息的理解来讨论我认为正在发生的事情,英特尔的 analysis这些漏洞,以及相关专利。

行填充缓冲区是 L1D 缓存中的硬件结构,用于保存缓存中未命中的内存请求和 I/O 请求,直到它们得到服务。当所需的缓存行填充到 L1D 数据数组中时,将服务可缓存请求。当驱逐 write-combining 缓冲区的任何条件发生时(如手册中所述),将为 write-combining 写入提供服务。 UC 或 I/O 请求在发送到 L2 缓存时得到服务(尽快发生)。

参考RIDL的图4paper。用于产生这些结果的实验​​如下:

  • 受害线程将一个已知值写入单个内存位置。内存位置的内存类型为WB、WT、WC或UC。
  • 受害线程在循环中读取相同的内存位置。每个加载操作后跟 MFENCE,还有一个可选的 CLFLUSH。从论文中我不清楚 CLFLUSH 相对于其他两条指令的顺序,但这可能无关紧要。 MFENCE 序列化缓存行刷新操作,以查看缓存中每次加载未命中时会发生什么。此外,MFENCE 减少了 L1D 端口上两个逻辑核心之间的争用,从而提高了攻击者的吞吐量。
  • 同级逻辑核心上的攻击者线程 运行 循环执行清单 1 中所示的代码。第 6 行使用的地址可以是任何内容。唯一重要的是第 6 行的加载出错或导致需要微代码辅助的页面遍历(以设置页面 table 条目中的访问位)。页面遍历也需要使用 LFB,并且大部分 LFB 在逻辑核心之间共享。

我不清楚图 4 中的 Y-axis 代表什么。我的理解是,它表示每秒从隐蔽通道中提取到缓存层次结构(第 10 行)中的行数,其中行在数组中的索引等于受害者写入的值。

如果内存位置是WB类型,当victim线程将已知值写入内存位置时,该行会被填充到L1D缓存中。如果内存位置是WT类型,当victim线程将已知值写入内存位置时,该行不会被填充到L1D缓存中。但是,在第一次读取该行时,它将被填充。因此,在这两种情况下,如果没有 CLFLUSH,受害线程的大部分负载都会命中缓存。

当加载请求的缓存行到达 L1D 缓存时,它首先写入为该请求分配的 LFB。缓存行的请求部分可以直接从 LFB 提供给加载缓冲区,而无需等待缓存行被填充。根据 MFBDS 漏洞的描述,在某些情况下,来自先前请求的陈旧数据可能会被转发到加载缓冲区以满足加载 uop。在 WB 和 WT 情况下(没有刷新),受害者的数据最多写入 2 个不同的 LFB。从攻击者线程走出来的页面可以很容易地覆盖受害者在 LFB 中的数据,之后攻击者线程将永远无法在其中找到数据。所有命中 L1D 缓存的加载请求都不会通过 LFB;它们有一条单独的路径,与 LFB 的路径复用。尽管如此,在某些情况下,来自 LFB 的陈旧数据(噪声)被推测性地转发到攻击者的逻辑核心,这可能来自页面遍历(也可能是中断处理程序和硬件预取器)。

有趣的是,WB 和 WT 案例中过时数据转发的频率远低于所有其他案例。这可以解释为受害者的吞吐量在这些情况下要高得多,实验可能会提前终止。

在所有其他情况下(WC、UC 和所有带刷新的类型),缓存中的每个加载都未命中,数据必须通过 LFB 从主内存提取到加载缓冲区。发生以下事件序列:

  1. 受害者的访问命中了 TLB,因为它们访问的是同一个有效虚拟页面。物理地址从 TLB 获得并提供给 L1D,L1D 为请求分配一个 LFB(由于未命中),物理地址与描述加载请求的其他信息一起写入 L​​FB。此时,来自受害者的请求在 LFB 中处于待处理状态。由于受害者在每次加载后都会执行 MFENCE,因此可以在LFB 在任何给定周期中来自受害者的最多一个未完成的负载。
  2. 攻击者 运行ning 在兄弟逻辑核心上,向 L1D 和 TLB 发出加载请求。每次加载到一个未映射的用户页面,所以会导致错误。当它在 TLB 中未命中时,MMU 告诉加载缓冲区应该阻止加载,直到地址转换完成。根据该专利的第 26 段和其他英特尔专利,这就是处理 TLB 未命中的方式。地址转换仍在进行中,加载被阻止。
  3. 来自受害者的加载请求接收其缓存行,该缓存行被写入 LFB allcoated 以供加载。负载请求的那部分行被转发给MOB,同时,该行被写入L1D缓存。之后,LFB 可以被deallcoated,但是none 的字段被清除(除了表明它是空闲的字段)。特别是,数据仍在 LFB 中。受害者然后发送另一个加载请求,该请求也未命中缓存,因为它不可缓存或缓存行已被刷新。
  4. 攻击者负载的地址转换过程完成。 MMU 确定需要引发故障,因为物理页面不存在。但是,直到负载即将退出时(当它到达 ROB 的顶部时)才会引发故障。无效的翻译不会缓存在 Intel 处理器上的 MMU 中。 MMU 仍然必须告诉 MOB 翻译已经完成,在这种情况下,在 ROB 的相应条目中设置错误代码。似乎当 ROB 发现其中一个 uops 具有有效的 fault/assist 代码时,它会禁用与该 uops 的大小和地址相关的所有检查(可能还有 ROB 中所有后来的 uops)。这些检查不再重要了。据推测,禁用这些检查可以节省动态能耗。退役逻辑知道当负载即将退役时,无论如何都会引发故障。同时,当 MOB 被告知翻译完成时,它会像往常一样重放攻击者的负载。然而,这一次,一些无效的物理地址被提供给 L1D 缓存。通常,物理地址需要与来自同一逻辑内核的 LFB 中的所有未决请求进行比较,以确保逻辑内核看到最新的值。这是在查找 L1D 缓存之前或同时完成的。物理地址并不重要,因为比较逻辑被禁用。但是,所有比较的结果都表现得好像结果表明成功。如果至少有一个分配的 LFB,物理地址将匹配某个分配的 LFB。由于受害者有一个未完成的请求,并且由于受害者的秘密可能已经写入与先前请求相同的 LFB,所以缓存行的同一部分在技术上包含陈旧数据,在这种情况下(陈旧数据是秘密),将被转发给攻击者。请注意,攻击者可以控制缓存行内的偏移量和要获取的字节数,但无法控制哪个 LFB。高速缓存行的大小为 64 字节,因此只有攻击者加载的虚拟地址的 6 个最低有效位与加载的大小有关。然后,攻击者使用数据对其数组进行索引,以使用缓存侧通道攻击来揭示秘密。这种行为也可以解释 MSBDS,显然数据大小和 STD uop 检查被禁用(即,检查平凡通过)。
  5. 稍后,faulting/assisting 负载到达 ROB 的顶部。负载没有退役,管道被冲洗。如果负载出现故障,则会引发故障。在辅助加载的情况下,从相同的加载指令重新开始执行,但需要协助在分页结构中设置所需的标志。
  6. 重复这些步骤。但是攻击者可能并不总是能够从受害者那里泄露秘密。如您所见,来自攻击者的加载请求必然会命中包含秘密的已分配 LFB 条目。为页面遍历和硬件预取器分配的 LFB 可能会使成功攻击变得更加困难。

如果攻击者的负载没有 fault/assist,LFB 将从 MMU 收到一个 有效 物理地址,并执行正确性所需的所有检查。这就是为什么负载必须 fault/assist.

论文中的以下引述讨论了如何在同一线程中执行 RIDL 攻击:

we perform the RIDL attack without SMT by writing values in our own thread and observing the values that we leak from the same thread. Figure3 shows that if we do not write the values (“no victim”), we leak only zeros, but with victim and attacker running in the same hardware thread (e.g., in a sandbox), we leak the secret value in almost all cases.

我认为在这个实验中没有权限级别的变化。受害者和攻击者 运行 在同一硬件线程上的同一 OS 线程中。当从受害者返回给攻击者时,来自(尤其是商店)的 LFB 中可能仍有一些未完成的请求。请注意,在 RIDL 论文中,所有实验都启用了 KPTI(与 Fallout 论文相反)。

此外从 LFB 泄漏数据,MLPDS 显示数据也可以从加载端口缓冲区泄漏。这些包括 line-split 缓冲区和用于大小大于 8 字节的加载的缓冲区(我认为当加载 uop 的大小大于加载端口的大小时需要它们,例如 [AVX 256b on SnB/IvB占用端口2个周期)。

图 5 中的 WB 情况(无冲洗)也很有趣。在这个实验中,受害者线程将 4 个不同的值写入 4 个不同的缓存行,而不是从同一个缓存行读取。该图显示,在 WB 情况下,只有写入最后一个缓存行的数据会泄露给攻击者。解释可能取决于缓存行在循环的不同迭代中是否不同,不幸的是,这在论文中并不清楚。论文说:

For WB without flushing, there is a signal only for the last cache line, which suggests that the CPU performs write combining in a single entry of the LFB before storing the data in the cache.

在将数据存储到缓存中之前,如何将对不同缓存行的写入合并到同一个 LFB 中?那是零意义。 LFB 可以包含单个缓存行和单个物理地址。不可能像那样组合写入。可能发生的情况是,WB 写入正在写入为其 RFO 请求分配的 LFB。当无效的物理地址被传输到 LFB 进行比较时,数据可能总是从最后分配的 LFB 提供。这可以解释为什么只有第四个存储写入的值被泄漏。

有关 MDS 缓解措施的信息,请参阅:What are the new MDS attacks, and how can they be mitigated?。我在那里的回答只讨论了基于英特尔微码更新的缓解措施(不是很有趣 "software sequences")。


下图显示了使用数据推测的易受攻击的结构。