32. 软件保护扩展 (SGX)

32.1. 概述

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

  • 特权 (ring-0) ENCLS 函数编排区域的构建。

  • 非特权 (ring-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

指向 sgx_enclave_add_pages 实例的 user 指针

描述

向未初始化的飞地添加一个或多个页面,并可选择使用页面内容扩展测量。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

指向 sgx_enclave_init 实例的 userspace 指针

描述

刷新所有未完成的排队 EADD 操作并执行 EINIT。根据需要重写 Launch Enclave 公钥哈希 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 的文件句柄,允许 enclave 使用 ATTRIBUTE.PROVISION_KEY。

返回

  • 0: 成功。

  • -errno: 否则。

32.3.2. Enclave 运行时管理

支持 SGX2 的系统还支持对已初始化 enclave 进行更改:修改 enclave 页面权限和类型,以及动态添加和删除 enclave 页面。当 enclave 访问其地址范围内没有后备页面的地址时,将动态地向 enclave 添加新的常规页面。在可以使用新页面之前,enclave 仍然需要在新页面上运行 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 区分了放宽和限制由硬件 (EPCM 权限) 维护的、属于已初始化 enclave 的页面权限(在 SGX_IOC_ENCLAVE_INIT 之后)。

不能从 enclave 内部限制 EPCM 权限,enclave 需要内核运行特权级别 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 实例的用户空间指针

描述

更改 enclave 页面类型的功能支持以下用例

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

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

返回

  • 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 实例的用户空间指针

描述

从已初始化 enclave 中删除页面的流程的最后一步。完整流程是

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

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

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

首先删除指向该页面的所有页表条目,然后继续实际删除 enclave 页面和支持它的数据。

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

返回

  • 0: 成功

  • -errno: 否则

32.3.3. Enclave vDSO

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

enclave 的另一个关键特性是,它们可以作为其正常操作的一部分生成需要在 enclave 中处理或 SGX 特有的异常。

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

vdso_sgx_enter_enclave_t

Typedef: __vdso_sgx_enter_enclave() 的原型,用于进入 SGX enclave 的 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 保存/设置状态是 enclave 及其运行时的责任,即,__vdso_sgx_enter_enclave() 不能从 C 代码调用,而无需 enclave 及其运行时仔细考虑。

描述

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

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

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

返回

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

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

32.4. ksgxd

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

32.4.1. EPC 清理

当 SGX 初始化时,ksgxd 启动。通常情况下,当处理器上电或重置时,飞地内存已准备好使用。但是,如果自重置以来 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 第 41.10 节)。

32.7. 使用模型

32.7.1. 共享库

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

32.7.2. 应用程序容器

可以将应用程序加载到容器飞地中,该容器飞地经过特殊配置,其中包含允许应用程序运行的库操作系统和运行时。当线程进入飞地时,飞地运行时和库操作系统协同工作来执行应用程序。

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

32.8.1. EPC 泄漏

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

“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,则表示内核或用户空间客户端中存在错误。