推测

本文档介绍了推测的潜在影响,以及如何使用通用 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];
        }
}