推测执行

本文档解释了推测执行的潜在影响,以及如何使用通用 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() 辅助函数可用于防止通过侧信道泄漏信息。

即使在 cpu 推测条件下,调用 array_index_nospec(index, size) 也会返回一个限定在 [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];
        }
}