推测执行

本文档解释了推测执行的潜在影响,以及如何使用通用API以可移植的方式缓解不良影响。


为了提高性能并最大限度地减少平均延迟,许多现代CPU采用推测执行技术,例如分支预测,执行可能会在稍后阶段丢弃的工作。

通常,推测执行无法从架构状态(例如寄存器的内容)中观察到。但是,在某些情况下,可以观察到它对微架构状态的影响,例如缓存中是否存在数据。这种状态可能形成侧信道,可以通过观察来提取秘密信息。

例如,在存在分支预测的情况下,代码中进行推测执行时可能会忽略边界检查。 考虑以下代码

int load_array(int *array, unsigned int index)
{
        if (index >= MAX_ARRAY_ELEMS)
                return 0;
        else
                return array[index];
}

在 arm64 上,可能被编译成如下的汇编序列

      CMP     <index>, #MAX_ARRAY_ELEMS
      B.LT    less
      MOV     <returnval>, #0
      RET
less:
      LDR     <returnval>, [<array>, <index>]
      RET

CPU 可能错误地预测条件分支,并推测性地加载 array[index],即使 index >= MAX_ARRAY_ELEMS。 此值随后将被丢弃,但推测性加载可能会影响微架构状态,这些状态可以随后测量。

涉及多个依赖内存访问的更复杂的序列可能导致敏感信息泄露。 考虑以下代码,在之前的示例的基础上构建

int load_dependent_arrays(int *arr1, int *arr2, int index)
{
        int val1, val2,

        val1 = load_array(arr1, index);
        val2 = load_array(arr2, val1);

        return val2;
}

在推测执行下,对 load_array() 的第一次调用可能会返回越界地址的值,而第二次调用将影响依赖于该值的微架构状态。 这可能提供任意读取原语。

缓解推测执行侧信道

内核提供了一个通用API,以确保即使在推测执行下也能遵守边界检查。 受基于推测的侧信道影响的架构应实现这些原语。

<linux/nospec.h> 中的 array_index_nospec() 辅助函数可用于防止信息通过侧信道泄露。

调用 array_index_nospec(index, size) 将返回一个经过清理的索引值,即使在 CPU 推测条件下,该值也被限制在 [0, size) 范围内。

这可以用来保护之前的 load_array() 示例

int load_array(int *array, unsigned int index)
{
        if (index >= MAX_ARRAY_ELEMS)
                return 0;
        else {
                index = array_index_nospec(index, MAX_ARRAY_ELEMS);
                return array[index];
        }
}