22. 英特尔信任域扩展 (TDX)

英特尔信任域扩展 (TDX) 通过隔离客户机寄存器状态和加密客户机内存,来保护机密客户机虚拟机免受来自宿主机和物理攻击的威胁。在 TDX 中,运行在特殊模式下的一个特殊模块位于宿主机和客户机之间,并负责管理客户机与宿主机之间的隔离。

22.1. TDX 宿主机内核支持

TDX 引入了一种名为安全仲裁模式 (Secure Arbitration Mode, SEAM) 的新 CPU 模式,以及一个由 SEAM 范围寄存器 (SEAMRR) 指向的新隔离范围。一个经过 CPU 认证的、被称为“TDX 模块”的软件模块运行在这个新的隔离范围内,以提供用于管理和运行受保护虚拟机的各项功能。

TDX 还利用英特尔多密钥全内存加密 (MKTME) 来为虚拟机提供加密保护。TDX 将一部分 MKTME 密钥 ID (KeyID) 保留为 TDX 私有 KeyID,这些 KeyID 只能在 SEAM 模式下访问。BIOS 负责对传统 MKTME KeyID 和 TDX KeyID 进行分区。

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

22.1.1. TDX 启动时检测

内核在启动期间通过检测 TDX 私有 KeyID 来检测 TDX。以下 dmesg 展示了当 TDX 被 BIOS 启用时的情况

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

22.1.2. TDX 模块初始化

内核通过新的 SEAMCALL 指令与 TDX 模块进行通信。TDX 模块实现了 SEAMCALL 叶子函数,以允许内核对其进行初始化。

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

[..] virt/tdx: module not loaded

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

除了初始化 TDX 模块之外,在对某个 CPU 执行任何其他 SEAMCALL 之前,必须在该 CPU 上执行一次每 CPU (per-cpu) 初始化 SEAMCALL。

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

如果 TDX 模块初始化成功,dmesg 会显示类似以下内容

[..] virt/tdx: 262668 KBs allocated for PAMT
[..] virt/tdx: TDX-Module initialized

如果 TDX 模块初始化失败,dmesg 也会显示其初始化失败的信息

[..] virt/tdx: TDX-Module initialization failed ...

22.1.3. TDX 模块运行时更新

与微代码类似,BIOS 通常在闪存 (flash) 中保存有一份 TDX 模块的副本。它会在启动时将此模块镜像加载到 RAM 中。然而,就像微代码一样,BIOS 加载的 TDX 模块可能会过时,原因可能是 BIOS 版本较旧,或者系统已经运行了很长时间。内核可以替换 RAM 中的 BIOS 版本并加载不同的 TDX 模块。内核加载的 TDX 模块不会影响 BIOS 闪存,并且在重启后不会保留。

通常情况下,TDX 模块是内核与之交互的唯一运行在 SEAM 模式下的软件。但还有第二个软件用于加载或更新 TDX 模块:持久性 SEAM 加载器 (P-SEAMLDR)。它与 TDX 模块分开运行在 SEAM 模式下。内核与 P-SEAMLDR 通信以执行 TDX 模块运行时更新。

22.1.3.1. 如何更新 TDX 模块

更新 TDX 模块是一个复杂的过程。大部分逻辑和策略都留给用户空间处理。最终用户应使用其发行版提供的现有更新基础设施。英特尔 TDX 模块二进制文件 (TDX Module Binaries) 仓库中包含了此逻辑的参考实现

本节将大致介绍实现用户空间驱动的 TDX 模块更新所需的内容。关于 tdx_host ABI 的详细文档可在以下位置找到

Documentation/ABI/testing/sysfs-devices-faux-tdx-host

本文档中不再重复说明。

  1. 检查是否完全支持运行时更新

    验证 TDX 模块固件上传接口是否可用

    /sys/class/firmware/tdx_module
    

    请注意,这是通用的内核固件更新 ABI。它与“tdx_host”设备 ABI 本身是分开的。

  2. 检查是否可以进行其他更新。验证

    /sys/devices/faux/tdx_host/num_remaining_updates
    

    的值大于 0。如果为 0,则 TDX 更新日志可能已满。请重启以将其重置为一个非零值。

  3. 选择兼容的 TDX 模块镜像

    选择兼容的 TDX 模块镜像并非易事。其中既有硬性的兼容性要求,也有需要做出的策略选择。

    硬性兼容性要求

    • 更新必须与内核兼容。

      更新绝不能以任何不兼容旧版本的方式更改任何 TDX ABI。它可以引入新功能,但绝不能要求内核为现有功能使用新的 ABI。必须确保系统的其余部分不受任何影响。系统上的软件绝不能察觉到任何行为变化。除了版本变更外,证明 (attestation) 结果应该完全相同。

    • 更新必须与 CPU 兼容。

      支持的 CPU FMS 值(系列、型号、步进)集合已编码在模块镜像本身中。实际上,模块版本系列是特定于平台的。例如,1.5.x 系列运行在 Sapphire Rapids 上,但不能运行在需要 2.0.x 的 Granite Rapids 上。

    • 更新必须与 P-SEAMLDR 兼容。

      此信息在随模块镜像一起发布的元数据文件(通常为 mapping_file.json)中提供。每个模块镜像都指定了所需的最低 P-SEAMLDR 版本,只有当运行中的 P-SEAMLDR 满足该要求时,更新才兼容。

      P-SEAMLDR 的当前版本可以在此处读取

      /sys/devices/faux/tdx_host/seamldr_version
      
    • 更新必须与正在运行的 TDX 模块兼容。

      与 P-SEAMLDR 一样,每个模块镜像还指定了所需的最低 TDX 模块版本。运行中的模块必须满足该要求。

      更新软件可以在此处读取当前的 TDX 模块版本

      /sys/devices/faux/tdx_host/version
      

    策略选择

    • 更新软件会选择如何优化其更新过程。例如,它可以针对更少的更新次数进行优化,也可以针对更小的版本跨度进行优化,例如 1.2.3 => 1.2.5 对比 1.2.3 => 1.2.4 => 1.2.5。

  4. 执行更新

    运行

    echo 1 > /sys/class/firmware/tdx_module/loading
    cat <path_to_module_image> > /sys/class/firmware/tdx_module/data
    echo 0 > /sys/class/firmware/tdx_module/loading
    

    文件 /sys/class/firmware/tdx_module/status 和 /sys/class/firmware/tdx_module/error 会报告更新进度和错误信息。

    更新完成后,新模块版本可在 /sys/devices/faux/tdx_host/version 中查看。

22.1.3.2. 对运行中的 TD 的影响

TDX 模块运行时更新对运行中的 TD 应当几乎没有可见的影响。任何对 TD 可见的影响都是 TDX 模块的 bug。

主要的例外是 TD 报价 (quote) 中的 TEE_TCB_SVN_2 字段,它反映了当前运行的 TDX 模块的 TCB,因此会在更新后发生变化。相比之下,TEE_TCB_SVN 反映的是 TD 启动时的 TCB,不受影响。

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

22.1.4.1. TDX 内存策略

TDX 会报告一个“可转换内存区域”(CMR) 列表,以告知内核哪些内存是兼容 TDX 的。内核需要从 CMR 中构建一个内存区域列表作为“TDX 可用”内存,并将这些区域传递给 TDX 模块。一旦完成此操作,这些“TDX 可用”内存区域在模块的生命周期内将保持不变。

为简单起见,目前内核直接保证页面分配器中的所有页面都是 TDX 内存。具体而言,内核在“TDX 模块初始化时”将核心内存管理 (core-mm) 中的所有系统内存用作 TDX 内存,同时在内存热插拔过程中拒绝将任何非 TDX 内存上线 (online)。

22.1.4.2. 物理内存热插拔

请注意,TDX 假设可转换内存始终在机器运行时物理存在。一个没有 bug 的 BIOS 绝不应支持热移除任何可转换内存。本实现不处理 ACPI 内存移除,而是依赖 BIOS 的正确行为。

22.1.4.3. CPU 热插拔

TDX 模块要求:在对某个 CPU 执行任何其他 SEAMCALL 之前,必须在该 CPU 上执行每 CPU 初始化 SEAMCALL。内核通过 CPU 热插拔框架,在首次将 CPU 上线 (online) 时执行必要的初始化。

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

请注意,TDX 配合 CPU 逻辑上线/下线工作,因此内核仍然允许将逻辑 CPU 下线并再次上线。

22.1.4.4. 勘误 (Erratum)

前几代 TDX 硬件存在一个勘误。对 TDX 私有内存缓存行 (cacheline) 的部分写入会静默地“污染”该行。随后的读取将会消耗该污染并产生机器检查 (machine check)。

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

理论上,内核 bug 可能会对 TDX 私有内存进行部分写入并触发意外的机器检查。此外,机器检查代码会将这些错误呈现为“硬件错误 (Hardware error)”,而实际上它们是由软件触发的问题。但总的来说,这个问题很难被触发。

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

22.1.4.5. 与 S3 及更深状态的交互

TDX 无法在 S3 及更深状态下存活。当平台进入 S3 及更深状态时,硬件会完全重置并禁用 TDX。TDX 客户机和 TDX 模块都会被永久销毁。

内核使用 S3 进行挂起到内存 (suspend-to-ram),并使用 S4 及更深状态进行休眠 (hibernation)。目前,为简单起见,内核选择让 TDX 与 S3 和休眠互斥。

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

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

添加 ‘nohibernate’ 内核命令行参数以禁用休眠,从而能够使用 TDX。

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

22.2. TDX 客户机支持

由于宿主机无法直接访问客户机的寄存器或内存,虚拟机监控程序 (hypervisor) 的许多正常功能必须移至客户机内部。这是通过由客户机内核处理的虚拟化异常 (#VE) 来实现的。#VE 完全在客户机内核内部处理,但有些情况需要咨询虚拟机监控程序。

TDX 包含新的类似 hypercall 的机制,用于从客户机向虚拟机监控程序或 TDX 模块进行通信。

22.2.1. 新的 TDX 异常

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

标记有 ‘*’ 的指令会有条件地引发异常。这些指令的详细信息将在下文讨论。

22.2.1.1. 基于指令的 #VE

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

  • HLT

  • MONITOR, MWAIT

  • WBINVD, INVD

  • VMCALL

  • RDMSR*,WRMSR*

  • CPUID*

22.2.1.2. 基于指令的 #GP

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

  • ENCLS, ENCLU

  • GETSEC

  • RSM

  • ENQCMD

  • RDMSR*,WRMSR*

22.2.1.3. RDMSR/WRMSR 行为

MSR 访问行为分为三类

  • 生成 #GP

  • 生成 #VE

  • “直接工作”(Just works)

通常,不应在客户机中使用会产生 #GP 的 MSR。使用它们可能表明客户机中存在 bug。客户机可能会尝试通过 hypercall 来处理 #GP,但这不太可能成功。

产生 #VE 的 MSR 通常可以由虚拟机监控程序处理。客户机可以向虚拟机监控程序发出 hypercall 以处理 #VE。

“直接工作”的 MSR 不需要任何特殊的客户机处理。它们可以通过直接将 MSR 透传给硬件来实现,也可以通过在 TDX 模块中捕获并处理来实现。除了可能速度较慢之外,这些 MSR 的功能看起来与在裸机上完全一样。

22.2.1.4. CPUID 行为

对于某些 CPUID 叶子 (leaves) 和子叶,CPUID 返回值的虚拟化位字段(在客户机的 EAX/EBX/ECX/EDX 中)可由虚拟机监控程序配置。对于这些情况,英特尔 TDX 模块架构定义了两种虚拟化类型

  • 虚拟机监控程序控制客户机 TD 所看到的位字段的值。

  • 虚拟机监控程序对其进行配置的位字段,使得客户机 TD 要么看到它们的本机值,要么看到 0。对于这些位字段,虚拟机监控程序可以屏蔽本机值,但不能将值置为 开启 (on)

对于 TDX 模块不知道如何处理的 CPUID 叶子和子叶,会生成一个 #VE。客户机内核可以通过 hypercall 向虚拟机监控程序请求该值。

22.2.2. 内存访问时的 #VE

TDX 内存本质上有两类:私有内存和共享内存。私有内存享有完整的 TDX 保护。其内容受保护,可防止来自虚拟机监控程序的访问。共享内存预计在客户机和虚拟机监控程序之间共享,不享受完整的 TDX 保护。

TD 客户机可以控制其内存访问是被视为私有还是共享。它通过其页表项中的某一位来选择这种行为。这有助于确保客户机不会将敏感信息放入共享内存中,从而将其暴露给不受信任的虚拟机监控程序。

22.2.2.1. 共享内存上的 #VE

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

共享映射的内容完全由虚拟机监控程序控制。客户机应仅将共享映射用于与虚拟机监控程序通信。绝不能将共享映射用于内核栈等敏感内存内容。一个很好的经验法则是:应将虚拟机监控程序共享内存视为与映射到用户空间的内存相同。虚拟机监控程序和用户空间都是完全不受信任的。

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

22.2.2.2. 私有页面上的 #VE

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

在内核运行之前,固件会预先接受适量的内存(通常为 512MB),以确保内核能够在不受 #VE 干扰的情况下启动。

允许虚拟机监控程序单方面将已接受的页面移动到“阻塞”(blocked) 状态。但是,如果它这样做,页面访问将不会生成 #VE。相反,它会引发一个“TD 退出”(TD Exit),此时虚拟机监控程序必须处理该异常。

22.2.3. Linux #VE 处理程序

就像页错误 (page faults) 或 #GP 一样,#VE 异常要么被处理,要么是致命的。通常,未处理的用户空间 #VE 会导致 SIGSEGV。未处理的内核 #VE 会导致 oops。

在 x86 上处理嵌套异常通常是一件棘手的事情。#VE 可能会被 NMI 中断,从而触发另一个 #VE,然后就会引发各种混乱。TDX #VE 架构预见到了这种场景,并包含了一项功能,使其不那么棘手。

在 #VE 处理期间,TDX 模块确保所有中断(包括 NMI)都被阻塞。该阻塞会一直持续,直到客户机发起 TDG.VP.VEINFO.GET TDCALL。这允许客户机控制何时可以传递中断或新的 #VE。

但是,在实施此阻塞期间,客户机内核仍必须小心避免潜在的会触发 #VE 的操作(如上所述)。在阻塞期间,任何 #VE 都会升级为双重错误 (#DF),这是不可恢复的。

22.2.4. MMIO 处理

在非 TDX 虚拟机中,MMIO 通常通过为客户机提供一个在访问时会导致 VMEXIT 的映射来实现,然后由虚拟机监控程序模拟该访问。这在 TDX 客户机中是不可能的,因为 VMEXIT 会将寄存器状态暴露给宿主机。TDX 客户机不信任宿主机,不能让其状态暴露给宿主机。

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

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

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

22.2.5. 共享内存转换

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

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

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

设备驱动程序是共享内存的主要用户,但没有必要修改每个驱动程序。DMA 缓冲区和 ioremap() 会自动进行转换。

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

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

22.3. 证明 (Attestation)

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

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

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

TDX 客户机使用 TDCALL[TDG.MR.REPORT] 从 TDX 模块获取 TDREPORT (TDREPORT_STRUCT)。TDREPORT 是由 TDX 模块生成的一个固定大小的数据结构,其中包含客户机特定信息(如构建和引导测量值)、平台安全版本以及用于保护 TDREPORT 完整性的 MAC。用户提供的 64 字节 REPORTDATA 用作输入并包含在 TDREPORT 中。通常,它可以是由证明服务提供的一些随机数 (nonce),以便可以唯一地验证 TDREPORT。有关 TDREPORT 的更多详细信息,请参阅《英特尔 TDX 模块规范》中题为“TDG.MR.REPORT 叶子”的章节。

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

22.4. 参考资料

TDX 参考资料收集在此处

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