机密计算虚拟机

Hyper-V 可以创建和运行作为机密计算 (CoCo) VM 的 Linux 客户机。这样的虚拟机与物理处理器协同工作,以更好地保护虚拟机内存中数据的机密性和完整性,即使在虚拟机监控程序/VMM 被攻破并可能恶意行为的情况下也是如此。Hyper-V 上的 CoCo VM 共享 Linux 中用于 x86 虚拟化的机密计算 中描述的通用 CoCo VM 威胁模型和安全目标。请注意,Linux 中特定于 Hyper-V 的代码将 CoCo VM 称为“隔离虚拟机”或“隔离虚拟机”。

Hyper-V 上的 Linux CoCo VM 需要以下各项的协作和交互:

  • 支持 CoCo VM 的处理器的物理硬件

  • 硬件运行支持 CoCo VM 的 Windows/Hyper-V 版本

  • 虚拟机运行支持作为 CoCo VM 的 Linux 版本

物理硬件要求如下:

  • 具有 SEV-SNP 的 AMD 处理器。Hyper-V 不运行使用 AMD SME、SEV 或 SEV-ES 加密的客户虚拟机,并且此类加密不足以支持 Hyper-V 上的 CoCo VM。

  • 具有 TDX 的 Intel 处理器

要创建 CoCo VM,在创建虚拟机时必须向 Hyper-V 指定“隔离 VM”属性。虚拟机一旦创建,就无法从 CoCo VM 更改为普通 VM,反之亦然。

操作模式

Hyper-V CoCo VM 可以两种模式运行。该模式在创建虚拟机时选择,并且在虚拟机的生命周期内无法更改。

  • 完全启发模式。在这种模式下,客户操作系统被启发以理解和管理作为 CoCo VM 运行的所有方面。

  • 半虚拟化模式。在这种模式下,客户机和主机之间的半虚拟化层提供了一些作为 CoCo VM 运行所需的操作。与完全启发模式相比,客户操作系统可以具有更少的 CoCo 启发。

从概念上讲,完全启发模式和半虚拟化模式可以被视为跨越作为 CoCo VM 运行所需的客户机启发程度的光谱上的点。完全启发模式是光谱的一端。半虚拟化模式的完整实现是光谱的另一端,其中作为 CoCo VM 运行的所有方面都由半虚拟化程序处理,并且没有内存加密或其他 CoCo VM 方面知识的普通客户操作系统可以成功运行。但是,Hyper-V 半虚拟化模式的实现并没有走这么远,而是处于光谱的中间位置。CoCo VM 的某些方面由 Hyper-V 半虚拟化程序处理,而客户操作系统必须为其他方面进行启发。不幸的是,没有针对半虚拟化程序中可能提供的特性/功能的标准化枚举,也没有客户操作系统查询半虚拟化程序以获取其提供的特性/功能的标准化机制。对半虚拟化程序提供的功能的理解是硬编码在客户操作系统中的。

半虚拟化模式与 Coconut 项目 相似,该项目旨在提供有限的半虚拟化程序,为客户机提供服务,例如虚拟 TPM。但是,Hyper-V 半虚拟化程序通常比目前为 Coconut 设想的 CoCo VM 处理更多方面,因此更接近“不需要客户机启发”的光谱末端。

在 CoCo VM 威胁模型中,半虚拟化程序位于客户机安全域中,必须受到客户操作系统的信任。这意味着,虚拟机监控程序/VMM 必须像保护自己免受潜在恶意客户机的侵害一样,保护自己免受潜在恶意半虚拟化程序的侵害。

完全启发模式与半虚拟化模式的硬件架构方法因底层处理器而异。

  • 对于 AMD SEV-SNP 处理器,在完全启发模式下,客户操作系统在 VMPL 0 中运行,并完全控制客户机上下文。在半虚拟化模式下,客户操作系统在 VMPL 2 中运行,半虚拟化程序在 VMPL 0 中运行。在 VMPL 0 中运行的半虚拟化程序具有 VMPL 2 中的客户操作系统不具备的权限。某些操作需要客户机调用半虚拟化程序。此外,在半虚拟化模式下,客户操作系统在 SEV-SNP 架构定义的“虚拟内存顶部 (vTOM)”模式下运行。当使用半虚拟化程序时,此模式简化了客户机对内存加密的管理。

  • 对于 Intel TDX 处理器,在完全启发模式下,客户操作系统在 L1 VM 中运行。在半虚拟化模式下,使用 TD 分区。半虚拟化程序在 L1 VM 中运行,客户操作系统在嵌套的 L2 VM 中运行。

Hyper-V 向客户机公开一个描述 CoCo 模式的合成 MSR。此 MSR 指示底层处理器是使用 AMD SEV-SNP 还是 Intel TDX,以及是否正在使用半虚拟化程序。可以轻松构建一个可以在任一架构上以及任一模式下正确启动和运行的单个内核映像。

半虚拟化程序影响

在半虚拟化模式下运行会影响通用 Linux 内核 CoCo VM 功能的以下方面:

  • 初始客户机内存设置。当在半虚拟化模式下创建新虚拟机时,半虚拟化程序首先运行并将客户机物理内存设置为加密状态。客户机 Linux 执行正常的内存初始化,但明确将适当的范围标记为已解密(共享)。在半虚拟化模式下,Linux 不执行早期启动内存设置步骤,而这些步骤在完全启发模式下使用 AMD SEV-SNP 时尤其棘手。

  • #VC/#VE 异常处理。在半虚拟化模式下,Hyper-V 将客户机 CoCo VM 配置为将 #VC 和 #VE 异常分别路由到 VMPL 0 和 L1 VM,而不是客户机 Linux。因此,这些异常处理程序不在客户机 Linux 中运行,也不是半虚拟化模式下 Linux 客户机的必需启发。

  • CPUID 标志。AMD SEV-SNP 和 Intel TDX 都提供了客户机中的 CPUID 标志,指示虚拟机正在使用各自的硬件支持运行。虽然这些 CPUID 标志在完全启发的 CoCo VM 中可见,但半虚拟化程序会过滤掉这些标志,并且客户机 Linux 看不到它们。在整个 Linux 内核中,大多已消除对这些标志的显式测试,而支持 cc_platform_has() 函数,目的是抽象化 SEV-SNP 和 TDX 之间的差异。但是,cc_platform_has() 抽象还允许 Hyper-V 半虚拟化程序配置选择性地启用 CoCo VM 功能的各个方面,即使未设置 CPUID 标志也是如此。例外情况是 SEV-SNP 上的早期启动内存设置,它会测试 CPUID SEV-SNP 标志。但是在 Hyper-V 半虚拟化模式 VM 中没有该标志可以实现期望的效果,或者不运行 SEV-SNP 特定的早期启动内存设置。

  • 设备仿真。在半虚拟化模式下,Hyper-V 半虚拟化程序提供诸如 IO-APIC 和 TPM 等设备的仿真。由于仿真发生在客户机上下文中的半虚拟化程序中(而不是虚拟机监控程序/VMM 上下文中),因此对这些设备的 MMIO 访问必须是加密引用,而不是完全启发 CoCo VM 中将使用的解密引用。__ioremap_caller() 函数已得到增强,可以通过回调来检查是否应将特定地址范围视为已加密(私有)。请参阅“is_private_mmio”回调。

  • 加密/解密内存转换。在 CoCo VM 中,在加密和解密之间转换客户机内存需要与虚拟机监控程序/VMM 协调。这是通过从 __set_memory_enc_pgtable() 调用的回调来完成的。在完全启发模式下,使用这些回调的正常 SEV-SNP 和 TDX 实现。在半虚拟化模式下,使用一组特定于 Hyper-V 的回调。这些回调调用半虚拟化程序,以便半虚拟化程序可以协调转换并根据需要通知虚拟机监控程序。请参阅 hv_vtom_init(),其中设置了这些回调。

  • 中断注入。在完全启发模式下,恶意的虚拟机监控程序可能会在违反 x86/x64 架构规则的时间将中断注入到客户操作系统中。为了获得全面保护,客户操作系统应包括使用支持 CoCo 的处理器提供的中断注入管理功能的启发。在半虚拟化模式下,半虚拟化程序会协调向客户操作系统进行中断注入,并确保客户操作系统仅看到“合法”的中断。半虚拟化程序使用支持 CoCo 的物理处理器提供的中断注入管理功能,从而屏蔽了客户操作系统的这些复杂性。

Hyper-V Hypercalls

在完全启发模式下,Linux 客户机发出的 hypercall 会像在非 CoCo VM 中一样直接路由到虚拟机监控程序。但是在半虚拟化模式下,正常的 hypercall 首先会陷入半虚拟化程序,半虚拟化程序可能会反过来调用虚拟机监控程序。但是半虚拟化程序在这方面是特殊的,Linux 客户机发出的少数几个 hypercall 必须始终直接路由到虚拟机监控程序。这些 hypercall 站点会测试是否存在半虚拟化程序,并使用特殊的调用序列。例如,请参阅 hv_post_message()。

客户机与 Hyper-V 的通信

除了 Linux CoCo VM 中内存加密的通用 Linux 内核处理之外,Hyper-V 还具有使用 Linux 客户机和主机之间共享的内存进行通信的 VMBus 和 VMBus 设备。必须将此共享内存标记为已解密才能启用通信。此外,由于威胁模型包括被攻破且可能恶意的主机,因此客户机必须防范通过此共享内存向主机泄露任何意外数据。

这些 Hyper-V 和 VMBus 内存页面被标记为已解密:

  • VMBus 监视器页面

  • 合成中断控制器 (synic) 相关页面(除非由半虚拟化程序提供)

  • 每个 CPU 的 hypercall 输入和输出页面(除非与半虚拟化程序一起运行)

  • VMBus 环形缓冲区。直接映射在 __vmbus_establish_gpadl() 中被标记为已解密。在 hv_ringbuffer_init() 中创建的二级映射也必须包含“decrypted”属性。

当 guest 向与 host 共享的内存写入数据时,它必须确保只写入预期的数据。在复制到共享内存之前,必须将填充或未使用的字段初始化为零,这样就不会无意中将随机内核数据提供给 host。

同样,当 guest 读取与 host 共享的内存时,它必须在对其进行操作之前验证数据,这样恶意 host 就无法诱使 guest 暴露意外的数据。执行此类验证可能很棘手,因为 host 可以在验证期间甚至之后修改共享内存区域。对于在 VMBus 环形缓冲区中从 host 传递到 guest 的消息,会验证消息的长度,并将消息复制到临时(加密的)缓冲区中以进行进一步的验证和处理。复制会增加少量开销,但这是防止恶意 host 的唯一方法。请参阅 hv_pkt_iter_first()。

许多 VMBus 设备的驱动程序都通过添加代码来完全验证通过 VMBus 接收的消息进行了“加固”,而不是假设 Hyper-V 正在协同工作。此类驱动程序在 vmbus_devs[] 表中标记为“allowed_in_isolated”。在 CoCo VM 中不需要的其他 VMBus 设备的驱动程序尚未进行加固,并且不允许在 CoCo VM 中加载。请参阅 vmbus_is_valid_offer(),其中排除了此类设备。

两个 VMBus 设备依赖于 Hyper-V host 进行 DMA 数据传输:用于磁盘 I/O 的 storvsc 和用于网络 I/O 的 netvsc。storvsc 使用正常的 Linux 内核 DMA API,因此通过解密的 swiotlb 内存进行反弹缓冲是隐式完成的。netvsc 有两种数据传输模式。第一种模式通过 netvsc 驱动程序显式分配的发送和接收缓冲区空间,用于大多数较小的包。这些发送和接收缓冲区在 __vmbus_establish_gpadl() 中被标记为已解密。由于 netvsc 驱动程序显式地将数据包复制到/从这些缓冲区复制,因此加密和解密内存之间的反弹缓冲等效已经成为数据路径的一部分。第二种模式使用正常的 Linux 内核 DMA API,并通过 swiotlb 内存隐式地进行反弹缓冲,就像 storvsc 中一样。

最后,VMBus 虚拟 PCI 驱动程序在 CoCo VM 中需要特殊处理。Linux PCI 设备驱动程序使用 Linux PCI 子系统提供的标准 API 访问 PCI 配置空间。在 Hyper-V 上,这些函数直接访问 MMIO 空间,并且访问会陷入 Hyper-V 进行仿真。但是在 CoCo VM 中,内存加密会阻止 Hyper-V 读取 guest 指令流来模拟访问。因此,在 CoCo VM 中,这些函数必须使用显式描述访问的参数进行 hypercall。请参阅 _hv_pcifront_read_config() 和 _hv_pcifront_write_config() 以及指示使用 hypercall 的“use_calls”标志。

load_unaligned_zeropad()

在加密和解密之间转换内存时,set_memory_encrypted() 或 set_memory_decrypted() 的调用者有责任确保内存在转换过程中未被使用且未被引用。转换有多个步骤,并且包括与 Hyper-V host 的交互。在所有步骤完成之前,内存处于不一致的状态。在状态不一致时引用可能会导致无法干净修复的异常。

但是,内核 load_unaligned_zeropad() 机制可能会进行 set_memory_encrypted() 或 set_memory_decrypted() 的调用者无法阻止的随机引用,因此 #VC 或 #VE 异常处理程序中有特定的代码来修复这种情况。但是,在 Hyper-V 上运行的 CoCo VM 可能会被配置为使用 paravisor 运行,并且 #VC 或 #VE 异常会被路由到 paravisor。没有架构方法可以将异常转发回 guest 内核,在这种情况下,#VC/#VE 处理程序中的 load_unaligned_zeropad() 修复代码不会运行。

为了避免这个问题,用于通知 hypervisor 转换的 Hyper-V 特定函数会将页面标记为“不存在”,同时转换正在进行中。如果 load_unaligned_zeropad() 导致随机引用,则会生成正常的页错误,而不是 #VC 或 #VE,并且基于页错误的 load_unaligned_zeropad() 处理程序会修复引用。当加密/解密转换完成时,页面会再次标记为“存在”。请参阅 hv_vtom_clear_present() 和 hv_vtom_set_host_visibility()。