21. Intel Trust Domain Extensions (TDX)

Intel 的 Trust Domain Extensions (TDX) 通过隔离 guest 寄存器状态和加密 guest 内存来保护机密 guest VM 免受主机和物理攻击。在 TDX 中,运行在特殊模式下的特殊模块位于主机和 guest 之间,并管理 guest/主机分离。

21.1. TDX 主机内核支持

TDX 引入了一种新的 CPU 模式,称为安全仲裁模式 (SEAM) 和一个新的隔离范围,由 SEAM Ranger Register (SEAMRR) 指向。一个 CPU 认证的软件模块,称为 “TDX 模块”,运行在新的隔离范围内,以提供管理和运行受保护 VM 的功能。

TDX 还利用 Intel Multi-Key Total Memory Encryption (MKTME) 为 VM 提供加密保护。TDX 保留部分 MKTME KeyID 作为 TDX 私有 KeyID,这些 KeyID 只能在 SEAM 模式下访问。BIOS 负责对传统 MKTME KeyID 和 TDX KeyID 进行分区。

在 TDX 模块可用于创建和运行受保护的 VM 之前,必须将其加载到隔离范围并正确初始化。TDX 架构不要求 BIOS 加载 TDX 模块,但内核假定它由 BIOS 加载。

21.1.1. TDX 启动时检测

内核通过在内核启动期间检测 TDX 私有 KeyID 来检测 TDX。下面的 dmesg 显示了 BIOS 何时启用了 TDX

[..] virt/tdx: BIOS enabled: private KeyID range: [16, 64)

21.1.2. TDX 模块初始化

内核通过新的 SEAMCALL 指令与 TDX 模块通信。TDX 模块实现 SEAMCALL 叶函数,以允许内核初始化它。

如果未加载 TDX 模块,则 SEAMCALL 指令会失败并出现特殊错误。在这种情况下,内核会使模块初始化失败,并报告该模块未加载

[..] virt/tdx: module not loaded

初始化 TDX 模块会消耗大约 ~1/256 的系统 RAM 大小,将其用作 TDX 内存的 “元数据”。它还需要额外的 CPU 时间来初始化这些元数据以及 TDX 模块本身。这两者都不是微不足道的。内核在运行时按需初始化 TDX 模块。

除了初始化 TDX 模块之外,在任何其他 SEAMCALL 可以在该 CPU 上进行之前,必须在一个 CPU 上完成每个 CPU 的初始化 SEAMCALL。

内核提供两个函数 tdx_enable() 和 tdx_cpu_enable(),以允许 TDX 的用户启用 TDX 模块并分别在本地 CPU 上启用 TDX。

进行 SEAMCALL 需要在该 CPU 上完成 VMXON。目前只有 KVM 实现了 VMXON。目前,tdx_enable() 和 tdx_cpu_enable() 都不会在内部执行 VMXON(并非易事),而是依赖于调用者来保证这一点。

要启用 TDX,TDX 的调用者应:1) 暂时禁用 CPU 热插拔;2) 在所有在线 CPU 上执行 VMXON 和 tdx_enable_cpu();3) 调用 tdx_enable()。例如

cpus_read_lock();
on_each_cpu(vmxon_and_tdx_cpu_enable());
ret = tdx_enable();
cpus_read_unlock();
if (ret)
        goto no_tdx;
// TDX is ready to use

TDX 的调用者必须保证在希望运行任何其他 SEAMCALL 之前,已在任何 CPU 上成功完成 tdx_cpu_enable()。一种典型的用法是在 CPU 热插拔在线回调中同时执行 VMXON 和 tdx_cpu_enable(),如果 tdx_cpu_enable() 失败,则拒绝上线。

用户可以查阅 dmesg,以查看是否已初始化 TDX 模块。

如果成功初始化了 TDX 模块,则 dmesg 会显示如下内容

[..] virt/tdx: 262668 KBs allocated for PAMT
[..] virt/tdx: module initialized

如果 TDX 模块未能初始化,dmesg 也会显示未能初始化

[..] virt/tdx: module initialization failed ...

21.1.3. TDX 与其他内核组件的交互

21.1.3.1. TDX 内存策略

TDX 报告一个 “可转换内存区域” (CMR) 列表,以告诉内核哪些内存与 TDX 兼容。内核需要构建一个内存区域列表(从 CMR 中提取),作为 “TDX 可用” 内存,并将这些区域传递给 TDX 模块。完成此操作后,这些 “TDX 可用” 内存区域将在模块的生命周期内固定。

为了简化操作,目前内核仅保证页面分配器中的所有页面都是 TDX 内存。具体来说,内核使用核心 mm 中的所有系统内存(在 TDX 模块初始化时)作为 TDX 内存,与此同时,拒绝在内存热插拔中上线任何非 TDX 内存。

21.1.3.2. 物理内存热插拔

注意,TDX 假定可转换内存在机器运行时始终物理存在。非错误的 BIOS 绝不应支持热移除任何可转换内存。此实现不处理 ACPI 内存移除,而是依赖 BIOS 来正确运行。

21.1.3.3. CPU 热插拔

TDX 模块要求必须在一个 CPU 上完成每个 CPU 的初始化 SEAMCALL,然后才能在该 CPU 上进行任何其他 SEAMCALL。内核提供 tdx_cpu_enable(),以便 TDX 的用户在想要使用新的 CPU 进行 TDX 任务时执行此操作。

TDX 不支持物理 (ACPI) CPU 热插拔。在机器启动期间,TDX 会在启用 TDX 之前验证所有启动时存在的逻辑 CPU 是否与 TDX 兼容。非错误的 BIOS 绝不应支持物理 CPU 的热添加/移除。目前,内核不处理物理 CPU 热插拔,而是依赖 BIOS 来正确运行。

注意,TDX 适用于 CPU 逻辑在线/离线,因此内核仍然允许离线逻辑 CPU 并再次上线。

21.1.3.4. Kexec()

TDX 主机支持目前缺乏处理 kexec 的能力。为了简单起见,只能在 Kconfig 中启用其中一个。这将在未来得到修复。

21.1.3.5. 勘误表

最初几代的 TDX 硬件存在勘误。对 TDX 私有内存缓存行的部分写入会静默地 “污染” 该行。随后的读取将消耗该污染并生成机器检查。

部分写入是指小于缓存行的写入事务到达内存控制器的内存写入。CPU 通过非临时写入指令(如 MOVNTI)或通过 UC/WC 内存映射执行这些操作。设备也可以通过 DMA 执行部分写入。

理论上,内核错误可能会对 TDX 私有内存执行部分写入并触发意外的机器检查。更重要的是,机器检查代码会将这些显示为 “硬件错误”,而实际上它们是由软件触发的问题。但最终,这个问题很难触发。

如果平台存在此类勘误,内核会在机器检查处理程序中打印附加消息,以告知用户机器检查可能是由 TDX 私有内存上的内核错误引起的。

21.1.3.6. 与 S3 和更深状态的交互

TDX 无法从 S3 和更深的状态中恢复。当平台进入 S3 和更深的状态时,硬件会重置并完全禁用 TDX。TDX guest 和 TDX 模块都会被永久销毁。

内核使用 S3 进行挂起到 RAM,并使用 S4 和更深的状态进行休眠。目前,为了简单起见,内核选择使 TDX 与 S3 和休眠互斥。

当休眠支持可用时,内核会在早期启动期间禁用 TDX

[..] virt/tdx: initialization failed: Hibernation support is enabled

添加 “nohibernate” 内核命令行以禁用休眠,以便使用 TDX。

如果启用了 TDX,则在内核早期启动期间禁用 ACPI S3。用户需要在 BIOS 中关闭 TDX 才能使用 S3。

21.2. TDX Guest 支持

由于主机无法直接访问 guest 寄存器或内存,因此必须将 hypervisor 的许多正常功能移动到 guest 中。这是通过 guest 内核处理的虚拟化异常 (#VE) 实现的。#VE 完全在 guest 内核内部处理,但有些需要咨询 hypervisor。

TDX 包括用于从 guest 向 hypervisor 或 TDX 模块进行通信的新的类似 hypercall 的机制。

21.2.1. 新的 TDX 异常

TDX guest 的行为与裸机和传统 VMX guest 不同。在 TDX guest 中,其他正常的指令或内存访问可能会导致 #VE 或 #GP 异常。

标有 “*” 的指令有条件地导致异常。下面讨论这些指令的详细信息。

21.2.1.1. 基于指令的 #VE

  • 端口 I/O (INS, OUTS, IN, OUT)

  • HLT

  • MONITOR, MWAIT

  • WBINVD, INVD

  • VMCALL

  • RDMSR*,WRMSR*

  • CPUID*

21.2.1.2. 基于指令的 #GP

  • 所有 VMX 指令:INVEPT, INVVPID, VMCLEAR, VMFUNC, VMLAUNCH, VMPTRLD, VMPTRST, VMREAD, VMRESUME, VMWRITE, VMXOFF, VMXON

  • ENCLS, ENCLU

  • GETSEC

  • RSM

  • ENQCMD

  • RDMSR*,WRMSR*

21.2.1.3. RDMSR/WRMSR 行为

MSR 访问行为分为三类

  • 生成 #GP

  • 生成 #VE

  • “Just works”

通常,不应在 guest 中使用 #GP MSR。它们的使用可能表明 guest 中存在错误。guest 可能会尝试使用 hypercall 处理 #GP,但不太可能成功。

#VE MSR 通常可以由 hypervisor 处理。Guest 可以调用 hypervisor 来处理 #VE。

“Just works” MSR 不需要任何特殊的 guest 处理。它们可以通过直接将 MSR 传递到硬件或通过在 TDX 模块中捕获和处理来实现。除了可能很慢之外,这些 MSR 看起来与在裸机上运行一样。

21.2.1.4. CPUID 行为

对于某些 CPUID 叶和子叶,CPUID 返回值(在 guest EAX/EBX/ECX/EDX 中)的虚拟化位字段可以由 hypervisor 配置。对于这种情况,Intel TDX 模块架构定义了两种虚拟化类型

  • hypervisor 控制 guest TD 看到的值的位字段。

  • hypervisor 配置值的位字段,以便 guest TD 可以看到其本机值或值为 0。对于这些位字段,hypervisor 可以屏蔽本机值,但不能打开值。

对于 TDX 模块不知道如何处理的 CPUID 叶和子叶,会生成 #VE。Guest 内核可以使用 hypercall 向 hypervisor 请求该值。

21.2.2. 内存访问上的 #VE

本质上有两类 TDX 内存:私有和共享。私有内存接收完整的 TDX 保护。其内容受到保护,无法从 hypervisor 访问。共享内存应在 guest 和 hypervisor 之间共享,并且不会接收完整的 TDX 保护。

TD guest 可以控制其内存访问是被视为私有还是共享。它使用页面表条目中的一个位来选择行为。这有助于确保 guest 不会将敏感信息放在共享内存中,从而将其暴露给不受信任的 hypervisor。

21.2.2.1. 共享内存上的 #VE

访问共享映射可能会导致 #VE。hypervisor 最终控制共享内存访问是否会导致 #VE,因此 guest 必须小心,仅引用它可以安全处理 #VE 的共享页面。例如,guest 应小心,不要在读取 #VE 信息结构 (TDG.VP.VEINFO.GET) 之前访问 #VE 处理程序中的共享内存。

共享映射内容完全由 hypervisor 控制。Guest 应该仅将共享映射用于与 hypervisor 通信。共享映射绝不能用于敏感内存内容,如内核堆栈。一个好的经验法则是,应将 hypervisor 共享内存视为与映射到用户空间的内存相同。hypervisor 和用户空间都完全不受信任。

虚拟设备的 MMIO 作为共享内存实现。Guest 必须小心,不要访问设备 MMIO 区域,除非它也准备好处理 #VE。

21.2.2.2. 私有页面上的 #VE

访问私有映射也可能导致 #VE。由于所有内核内存也是私有内存,因此理论上内核可能需要在任意内核内存访问时处理 #VE。这是不可行的,因此 TDX guest 确保在使用内核内存之前,所有 guest 内存都已 “接受”。

在内核运行之前,固件会预先接受少量的内存(通常为 512M),以确保内核可以启动而不会受到 #VE 的影响。

允许 hypervisor 单方面将接受的页面移动到 “阻止” 状态。但是,如果它这样做,页面访问将不会生成 #VE。相反,它会导致 “TD Exit”,在这种情况下,需要 hypervisor 处理异常。

21.2.3. Linux #VE 处理程序

就像页面错误或 #GP 一样,#VE 异常可以被处理或致命。通常,未处理的用户空间 #VE 会导致 SIGSEGV。未处理的内核 #VE 会导致 oops。

在 x86 上处理嵌套异常通常是一件令人讨厌的事情。#VE 可能会被 NMI 中断,NMI 触发另一个 #VE,从而导致混乱。TDX #VE 架构预料到了这种情况,并包含一个功能,使其稍微不那么令人讨厌。

在 #VE 处理期间,TDX 模块确保所有中断(包括 NMI)都被阻止。该阻止一直有效,直到 guest 执行 TDG.VP.VEINFO.GET TDCALL。这允许 guest 控制何时可以传递中断或新的 #VE。

但是,guest 内核仍然必须小心,以避免在此阻止生效时发生潜在的 #VE 触发操作(如上所述)。当阻止生效时,任何 #VE 都会被提升为双重错误 (#DF),这是不可恢复的。

21.2.4. MMIO 处理

在非 TDX VM 中,MMIO 通常通过允许 guest 访问一个映射来实现,该映射将在访问时导致 VMEXIT,然后 hypervisor 模拟该访问。这在 TDX guest 中是不可能的,因为 VMEXIT 会将寄存器状态暴露给主机。TDX guest 不信任主机,并且不能将其状态暴露给主机。

在 TDX 中,MMIO 区域通常会在 guest 中触发 #VE 异常。然后,guest #VE 处理程序会在 guest 内部模拟 MMIO 指令,并将其转换为对主机的受控 TDCALL,而不是将 guest 状态暴露给主机。

x86 上的 MMIO 地址只是特殊的物理地址。理论上,可以使用访问内存的任何指令来访问它们。但是,内核指令解码方法受到限制。它仅设计用于解码由 io.h 宏生成的指令。

通过其他方式(如结构覆盖)访问 MMIO 可能会导致 oops。

21.2.5. 共享内存转换

所有 TDX guest 内存在启动时都以私有方式启动。hypervisor 无法访问此内存。但是,某些内核用户(如设备驱动程序)可能需要与 hypervisor 共享数据。为此,必须在共享和私有之间转换内存。这可以使用一些现有的内存加密助手来完成

  • set_memory_decrypted() 将一系列页面转换为共享。

  • set_memory_encrypted() 将内存转换回私有。

设备驱动程序是共享内存的主要用户,但无需触及每个驱动程序。DMA 缓冲区和 ioremap() 会自动执行转换。

TDX 对大多数 DMA 分配使用 SWIOTLB。SWIOTLB 缓冲区在启动时转换为共享。

对于一致的 DMA 分配,DMA 缓冲区在分配时进行转换。有关详细信息,请查看 force_dma_unencrypted()。

21.3. 证明

证明用于在向 guest 提供机密之前,向其他实体验证 TDX guest 的可信度。例如,密钥服务器可能希望使用证明来验证 guest 是否是所需的 guest,然后才释放加密密钥以挂载加密的 rootfs 或辅助驱动器。

TDX 模块使用构建时度量寄存器 (MRTD) 和运行时度量寄存器 (RTMR) 记录 guest 启动过程各个阶段的 TDX guest 状态。与 guest 初始配置和固件映像相关的度量记录在 MRTD 寄存器中。与初始状态、内核映像、固件映像、命令行选项、initrd、ACPI 表等相关的度量记录在 RTMR 寄存器中。有关更多详细信息,例如,请参阅 TDX 虚拟固件设计规范,标题为 “TD 度量” 的部分。在 TDX guest 运行时,证明过程用于证明这些度量。

证明过程包括两个步骤:TDREPORT 生成和 Quote 生成。

TDX guest 使用 TDCALL[TDG.MR.REPORT] 从 TDX 模块获取 TDREPORT (TDREPORT_STRUCT)。TDREPORT 是由 TDX 模块生成的固定大小的数据结构,其中包含 guest 特定信息(如构建和启动度量)、平台安全版本以及用于保护 TDREPORT 完整性的 MAC。用户提供的 64 字节 REPORTDATA 用作输入并包含在 TDREPORT 中。通常,它可以是证明服务提供的一些 nonce,以便可以唯一地验证 TDREPORT。有关 TDREPORT 的更多详细信息,请参见 Intel TDX 模块规范,标题为 “TDG.MR.REPORT Leaf” 的部分。

获得 TDREPORT 后,证明过程的第二步是将其发送到 Quoting Enclave (QE) 以生成 Quote。根据设计,TDREPORT 只能在本地平台上验证,因为 MAC 密钥绑定到平台。为了支持 TDREPORT 的远程验证,TDX 利用 Intel SGX Quoting Enclave 在本地验证 TDREPORT 并将其转换为可远程验证的 Quote。将 TDREPORT 发送到 QE 的方法是特定于实现的。证明软件可以选择任何可用的通信通道(即 vsock 或 TCP/IP)将 TDREPORT 发送到 QE 并接收 Quote。

21.4. 参考

TDX 参考资料在此处收集

https://www.intel.com/content/www/us/en/developer/articles/technical/intel-trust-domain-extensions.html