32. 软件保护扩展 (SGX)

32.1. 概述

软件保护扩展 (SGX) 硬件使用户空间应用程序能够预留私有代码和数据内存区域

  • 特权(环 0)ENCLS 函数协调区域的构造。

  • 非特权(环 3)ENCLU 函数允许应用程序进入并在区域内执行。

这些内存区域称为飞地。飞地只能在固定的入口点进入。每个入口点一次只能容纳一个硬件线程。虽然飞地通过使用 ENCLS 函数从常规二进制文件加载,但只有飞地内的线程才能访问其内存。该区域被 CPU 拒绝从外部访问,并在离开 LLC 之前进行加密。

可以通过以下方式确定支持:

grep sgx /proc/cpuinfo

SGX 必须同时在处理器中受支持,并且由 BIOS 启用。如果 SGX 在具有硬件支持的系统上显示为不受支持,请确保在 BIOS 中启用了支持。如果 BIOS 在 SGX 的“已启用”和“软件已启用”模式之间提供选择,请选择“已启用”。

32.2. 飞地页面缓存

SGX 使用飞地页面缓存 (EPC) 来存储与飞地关联的页面。它包含在物理内存的 BIOS 保留区域中。与用于常规内存的页面不同,页面只能在飞地构建期间通过特殊的、有限的 SGX 指令从飞地外部访问。

只有在飞地内执行的 CPU 才能直接访问飞地内存。但是,在飞地内执行的 CPU 可能会访问飞地外的正常内存。

内核管理飞地内存的方式与处理设备内存的方式类似。

32.2.1. 飞地页面类型

SGX 飞地控制结构 (SECS)

飞地的地址范围、属性和其他全局数据由此结构定义。

常规 (REG)

常规 EPC 页面包含飞地的代码和数据。

线程控制结构 (TCS)

线程控制结构页面定义了飞地的入口点并跟踪飞地线程的执行状态。

版本数组 (VA)

版本数组页面包含 512 个槽,每个槽都可以包含从 EPC 中逐出的页面的版本号。

32.2.2. 飞地页面缓存映射

处理器在称为飞地页面缓存映射 (EPCM) 的硬件元数据结构中跟踪 EPC 页面。EPCM 包含每个 EPC 页面的条目,其中描述了拥有飞地、访问权限和页面类型等。

EPCM 权限与正常页表分开。这可以防止内核例如允许写入飞地希望保持只读的数据。EPCM 权限可能只会对正常的 x86 页面权限施加额外的限制。

对于所有意图和目的,SGX 架构允许处理器随意使所有 EPCM 条目无效。这要求软件随时准备好处理 EPCM 故障。实际上,这可能会发生在电源转换等事件中,此时加密飞地内存的临时密钥会丢失。

32.3. 应用程序接口

32.3.1. 飞地构建函数

除了传统的编译器和链接器构建过程之外,SGX 还有一个单独的飞地“构建”过程。飞地必须先构建才能执行(进入)。构建飞地的第一步是打开 /dev/sgx_enclave 设备。由于飞地内存受到直接访问的保护,因此使用特殊的特权指令将数据复制到飞地页面并建立飞地页面权限。

long sgx_ioc_enclave_create(struct sgx_encl *encl, void __user *arg)

SGX_IOC_ENCLAVE_CREATE 的处理程序

参数

struct sgx_encl *encl

飞地指针。

void __user *arg

ioctl 参数。

描述

为飞地分配内核数据结构并调用 ECREATE。

返回

  • 0:成功。

  • -EIO:ECREATE 失败。

  • -errno:POSIX 错误。

long sgx_ioc_enclave_add_pages(struct sgx_encl *encl, void __user *arg)

SGX_IOC_ENCLAVE_ADD_PAGES 的处理程序

参数

struct sgx_encl *encl

飞地指针

void __user *arg

指向 struct sgx_enclave_add_pages 实例的用户指针

描述

将一个或多个页面添加到未初始化的飞地,并可选择使用页面的内容扩展度量。SECINFO 和度量掩码应用于所有页面。

TCS 的 SECINFO 必须始终包含零权限,因为 CPU 会以静默方式将其归零。允许其他任何权限都会导致度量不匹配。

mmap() 的保护位受页面权限的限制。对于每个页面地址,使用以下启发式方法计算最大保护位:

  1. 常规页面:PROT_R、PROT_W 和 PROT_X 匹配 SECINFO 权限。

  2. TCS 页面:PROT_R | PROT_W。

不允许 mmap() 超出给定地址范围内的最大保护位的最小值。

该函数取消初始化飞地的内核数据结构,并在以下任何情况下返回 -EIO:

  • 飞地页面缓存 (EPC),即保存飞地的物理内存,已被无效化。这将导致 EADD 和 EEXTEND 失败。

  • 如果执行 EADD 时源地址以某种方式损坏。

返回

  • 0:成功。

  • -EACCES:源页面位于 noexec 分区中。

  • -ENOMEM:EPC 页面不足。

  • -EINTR:在处理数据之前调用被中断。

  • -EIO:由于无效的源地址导致 EADD 或 EEXTEND 失败

    或断电。

  • -errno:POSIX 错误。

long sgx_ioc_enclave_init(struct sgx_encl *encl, void __user *arg)

SGX_IOC_ENCLAVE_INIT 的处理程序

参数

struct sgx_encl *encl

飞地指针

void __user *arg

指向 struct sgx_enclave_init 实例的用户空间指针

描述

刷新任何未完成的排队 EADD 操作并执行 EINIT。根据需要重写启动飞地公钥哈希 MSR,以匹配从提供的 sigstruct 计算出的飞地的 MRSIGNER。

返回

  • 0:成功。

  • -EPERM:无效的 SIGSTRUCT。

  • -EIO:由于断电导致 EINIT 失败。

  • -errno:POSIX 错误。

long sgx_ioc_enclave_provision(struct sgx_encl *encl, void __user *arg)

SGX_IOC_ENCLAVE_PROVISION 的处理程序

参数

struct sgx_encl *encl

飞地指针

void __user *arg

指向 struct sgx_enclave_provision 实例的用户空间指针

描述

通过为 /dev/sgx_provision 提供文件句柄,允许飞地的 ATTRIBUTE.PROVISION_KEY。

返回

  • 0:成功。

  • -errno:否则。

32.3.2. 飞地运行时管理

支持 SGX2 的系统还支持对已初始化飞地的更改:修改飞地页面权限和类型,以及动态添加和删除飞地页面。当飞地访问其地址范围内没有后备页面的地址时,将动态地向飞地添加新的常规页面。仍然需要飞地在新页面上运行 EACCEPT 才能使用它。

long sgx_ioc_enclave_restrict_permissions(struct sgx_encl *encl, void __user *arg)

SGX_IOC_ENCLAVE_RESTRICT_PERMISSIONS 的处理程序

参数

struct sgx_encl *encl

飞地指针

void __user *arg

指向 struct sgx_enclave_restrict_permissions 实例的用户空间指针

描述

SGX2 区分放宽和限制由已初始化飞地(在 SGX_IOC_ENCLAVE_INIT 之后)拥有的页面的硬件(EPCM 权限)维护的飞地页面权限。

无法从飞地内限制 EPCM 权限,飞地需要内核运行特权级别 0 指令 ENCLS[EMODPR] 和 ENCLS[ETRACK]。硬件将忽略尝试使用此调用来放宽 EPCM 权限。

返回

  • 0:成功

  • -errno:否则

long sgx_ioc_enclave_modify_types(struct sgx_encl *encl, void __user *arg)

SGX_IOC_ENCLAVE_MODIFY_TYPES 的处理程序

参数

struct sgx_encl *encl

飞地指针

void __user *arg

指向 struct sgx_enclave_modify_types 实例的用户空间指针

描述

更改飞地页面类型的功能支持以下用例:

  • 可以通过将常规页面 (SGX_PAGE_TYPE_REG) 的类型更改为 TCS (SGX_PAGE_TYPE_TCS) 页面来将 TCS 页面添加到飞地。通过此支持,可以动态增加已初始化飞地支持的线程数。

  • 可以通过将页面类型更改为 SGX_PAGE_TYPE_TRIM,从已初始化的飞地动态删除常规页面或 TCS 页面。将页面类型更改为 SGX_PAGE_TYPE_TRIM 会将页面标记为删除,实际删除由运行 ENCLU[EACCEPT] 后调用的 ioctl() 的 SGX_IOC_ENCLAVE_REMOVE_PAGES 处理程序完成,从飞地内的 SGX_PAGE_TYPE_TRIM 页面调用。

返回

  • 0:成功

  • -errno:否则

long sgx_ioc_enclave_remove_pages(struct sgx_encl *encl, void __user *arg)

SGX_IOC_ENCLAVE_REMOVE_PAGES 的处理程序

参数

struct sgx_encl *encl

飞地指针

void __user *arg

指向 struct sgx_enclave_remove_pages 实例的用户空间指针

描述

从已初始化的飞地中删除页面的流程的最后一步。完整流程为:

  1. 用户使用 SGX_IOC_ENCLAVE_MODIFY_TYPES ioctl() 将要删除的页面的类型更改为 SGX_PAGE_TYPE_TRIM

  2. 用户通过从飞地内运行 ENCLU[EACCEPT] 来批准页面删除。

  3. 用户使用此处处理的 SGX_IOC_ENCLAVE_REMOVE_PAGES ioctl() 启动实际页面删除。

首先删除指向该页面的任何页表条目,然后继续实际删除飞地页面和支持它的数据。

VA 页面不受此删除的影响。因此,飞地最终可能比支持其所有页面所需的 VA 页面更多。

返回

  • 0:成功

  • -errno:否则

32.3.3. 飞地 vDSO

只能通过特定于 SGX 的 EENTER 和 ERESUME 函数进入飞地,这是一个非常复杂的过程。由于转换到飞地和从飞地转换的复杂性,飞地通常使用库来处理实际转换。这大致类似于大多数应用程序如何使用 glibc 实现来包装系统调用。

飞地的另一个关键特性是,它们可以生成异常作为其正常操作的一部分,这些异常需要在飞地中处理或对于 SGX 是唯一的。

SGX 可以利用 vDSO 提供的特殊异常修复,而不是使用传统的信号机制来处理这些异常。内核提供的 vDSO 函数包装了到/从飞地的低级转换,如 EENTER 和 ERESUME。vDSO 函数拦截原本会生成信号的异常,并将故障信息直接返回给其调用者。这避免了处理信号处理程序的需要。

vdso_sgx_enter_enclave_t

Typedef:__vdso_sgx_enter_enclave() 的原型,一个用于进入 SGX 飞地的 vDSO 函数。

语法

int vdso_sgx_enter_enclave_t (unsigned long rdi, unsigned long rsi, unsigned long rdx, unsigned int function, unsigned long r8, unsigned long r9, struct sgx_enclave_run *run)

参数

unsigned long rdi

RDI 的直通值

unsigned long rsi

RSI 的直通值

unsigned long rdx

RDX 的直通值

unsigned int function

ENCLU 函数,必须是 EENTER 或 ERESUME

unsigned long r8

R8 的直通值

unsigned long r9

R9 的直通值

struct sgx_enclave_run *run

struct sgx_enclave_run,必须为非 NULL

注意

__vdso_sgx_enter_enclave() 不确保完全符合 x86-64 ABI,例如不处理 XSAVE 状态。除了非易失性通用寄存器、EFLAGS.DF 和 RSP 对齐之外,根据 x86-64 ABI 保存/设置状态是飞地及其运行时的责任,即,在飞地及其运行时都经过仔细考虑后,才能从 C 代码调用 __vdso_sgx_enter_enclave()。

描述

除了 RAX、RBX 和 RCX 之外的所有通用寄存器都按原样传递到飞地。RAX、RBX 和 RCX 由 EENTER 和 ERESUME 使用,并分别加载 function、异步退出指针和 run.tcs

RBP 和堆栈用于将 __vdso_sgx_enter_enclave() 锚定到飞地前状态,例如,在飞地退出后检索 run.exceptionrun.user_handler。所有其他寄存器都可供飞地及其运行时使用,例如,飞地可以将其他数据推送到堆栈上(并修改 RSP)以将信息传递到可选的用户处理程序(请参见下文)。

ENCLU 上报告的大多数异常,包括在飞地内发生的异常,都将被修复并同步报告,而不是通过标准信号传递。调试异常 (#DB) 和断点 (#BP) 永远不会被修复,并且始终通过标准信号传递。在同步报告的异常中,将返回 -EFAULT,并且有关异常的详细信息将记录在 run.exception(可选的 sgx_enclave_exception 结构)中。

返回

  • 0:ENCLU 函数已成功执行。

  • -EINVAL:无效的 ENCL 编号(既不是 EENTER 也不是 ERESUME)。

32.4. ksgxd

SGX 支持包括一个名为 ksgxd 的内核线程。

32.4.1. EPC 清理

ksgxd 在 SGX 初始化时启动。飞地内存通常在处理器启动或重置时可以使用。但是,如果自重置以来一直在使用 SGX,则飞地页面可能处于不一致的状态。例如,这可能会在崩溃和 kexec() 周期后发生。在启动时,ksgxd 重新初始化所有飞地页面,以便可以分配和重新使用它们。

清理是通过遍历 EPC 地址空间并将 EREMOVE 函数应用于每个物理页面来完成的。某些飞地页面(如 SECS 页面)在其他页面上具有硬件依赖关系,这会阻止 EREMOVE 起作用。执行两次 EREMOVE 传递会删除依赖关系。

32.4.2. 页面回收器

与核心 kswapd 类似,ksgxd 负责管理飞地内存的过度提交。如果系统耗尽飞地内存,则 ksgxd 会将飞地内存“交换”到普通内存。

32.5. 启动控制

SGX 提供了一种启动控制机制。复制完所有飞地页面后,内核执行 EINIT 函数,该函数初始化飞地。只有在此之后,CPU 才能在飞地内执行。

EINIT 函数采用飞地度量的 RSA-3072 签名。该函数检查度量是否正确,并且签名是否使用哈希到表示公钥 SHA256 的四个 IA32_SGXLEPUBKEYHASH{0, 1, 2, 3} MSR 的密钥签名。

这些 MSR 可以由 BIOS 配置为可读或可写。Linux 仅支持可写配置,以便内核完全控制启动控制策略。在调用 EINIT 函数之前,驱动程序设置 MSR 以匹配飞地的签名密钥。

32.6. 加密引擎

为了在飞地数据不在 CPU 封装中时隐藏飞地数据,内存控制器具有加密引擎来透明地加密和解密飞地内存。

在 Ice Lake 之前的 CPU 中,内存加密引擎 (MEE) 用于加密离开 CPU 缓存的页面。MEE 使用具有 SRAM 中根的 n 元 Merkle 树来维护加密数据的完整性。这提供了完整性和防重放保护,但无法扩展到大型内存大小,因为更新 Merkle 树所需的时间与内存大小呈对数增长。

从 Icelake 开始的 CPU 使用完全内存加密 (TME) 代替 MEE。基于 TME 的 SGX 实现没有完整性 Merkle 树,这意味着完整性和重放攻击不会得到缓解。但是,它包括额外的更改,以防止返回密文和创建 SW 内存别名。

通过 MEE 和 TME 系统上的范围寄存器阻止 DMA 到飞地内存 (SDM section 41.10)。

32.7. 使用模型

32.7.1. 共享库

敏感数据和作用于敏感数据的代码从应用程序中划分到一个单独的库中。然后,该库作为 DSO 链接,该 DSO 可以加载到飞地中。然后,应用程序可以通过特殊的 SGX 指令进行对飞地的单个函数调用。配置飞地内的运行时以将函数参数编组到飞地中和飞地外,并调用正确的库函数。

32.7.2. 应用程序容器

应用程序可以加载到容器飞地中,该容器飞地专门配置了库操作系统和允许应用程序运行的运行时。当线程进入飞地时,飞地运行时和库操作系统协同工作以执行应用程序。

32.8. 潜在内核 SGX 错误的影響

32.8.1. EPC 泄漏

当 EPC 页面泄漏发生时,dmesg 中会显示如下 WARNING:

“EREMOVE 返回 … 并且 EPC 页面泄漏。SGX 可能变得无法使用……”

这实际上是 EPC 页面的内核释放后使用,并且由于 SGX 的工作方式,在释放时检测到该错误。内核不会将页面添加回可用的 EPC 页面池,而是故意泄漏该页面,以避免将来出现其他错误。

发生这种情况时,内核可能会很快泄漏更多 EPC 页面,并且 SGX 可能会变得无法使用,因为 SGX 可用的内存有限。但是,虽然这对于 SGX 可能是致命的,但内核的其余部分不太可能受到影响,并且应该继续工作。

因此,发生这种情况时,用户应停止运行任何新的 SGX 工作负载(或只是任何新的工作负载),并迁移所有有价值的工作负载。虽然机器重启可以恢复所有 EPC 内存,但应将该错误报告给 Linux 开发人员。

32.9. 虚拟 EPC

该实现还有一个虚拟 EPC 驱动程序来支持访客中的 SGX 飞地。与 SGX 驱动程序不同,由虚拟 EPC 驱动程序分配的 EPC 页面没有与其关联的特定飞地。这是因为 KVM 不跟踪访客如何使用 EPC 页面。

因此,SGX 核心页面回收器不支持通过虚拟 EPC 驱动程序回收分配给 KVM 访客的 EPC 页面。如果用户想要在同一台机器上的主机和访客中部署 SGX 应用程序,则用户应为主机 SGX 应用程序保留足够的 EPC(通过从物理 EPC 大小中减去所有 SGX VM 的总虚拟 EPC 大小),以便它们可以以可接受的性能运行。

体系结构行为是在访客重启后也将所有 EPC 页面恢复为未初始化的状态。由于只能通过特权 ENCLS[EREMOVE] 指令达到此状态,因此 /dev/sgx_vepc 提供了 SGX_IOC_VEPC_REMOVE_ALL ioctl 以在虚拟 EPC 中的所有页面上执行该指令。

EREMOVE 可能因三个原因而失败。用户空间必须注意预期的失败并按以下方式处理它们:

  1. 当任何线程在页面所属的飞地中运行时,页面删除将始终失败。在这种情况下,ioctl 将返回 EBUSY,而不管它是否已成功删除某些页面;用户空间可以通过阻止映射虚拟 EPC 的任何 vcpu 的执行来避免这些失败。

  2. 如果同时为引用同一“SECS”元数据页面的页面调用两次 EREMOVE,则页面删除将导致一般保护错误。如果同时调用 SGX_IOC_VEPC_REMOVE_ALL,或者在关闭访客中的 /dev/sgx_vepc 文件描述符的同时调用 SGX_IOC_VEPC_REMOVE_ALL,则可能会发生这种情况;它也将报告为 EBUSY。可以通过序列化对 ioctl() 和 close() 的调用来避免用户空间中的这种情况,但通常来说,这不应该是一个问题。

  3. 最后,如果SECS元数据页面仍有子页面,则页面移除将失败。可以通过在所有映射到客户机的 /dev/sgx_vepc 文件描述符上执行 SGX_IOC_VEPC_REMOVE_ALL 来移除子页面。这意味着必须调用两次 ioctl():一组初始调用用于移除子页面,然后一组后续调用用于移除 SECS 页面。第二组调用仅对于那些从第一次调用返回非零值的映射是必需的。如果第二轮 SGX_IOC_VEPC_REMOVE_ALL 调用中任何一个的返回代码不是 0,则表示内核或用户空间客户端存在错误。