AVF 架构

Android 提供了实现 Android 虚拟化框架所需的所有组件的参考实现。目前,此实现仅限于 ARM64。此页面介绍了该框架的架构。

背景

Arm 架构允许多达四个异常级别,其中异常级别 0 (EL0) 的特权最低,异常级别 3 (EL3) 的特权最高。Android 代码库的最大部分(所有用户空间组件)在 EL0 级别运行。通常称为“Android”的其余部分是 Linux 内核,它在 EL1 级别运行。

EL2 层允许引入 Hypervisor,以便将内存和设备隔离到 EL1/EL0 级别的各个 pVM 中,并提供强大的保密性和完整性保证。

Hypervisor

受保护的基于内核的虚拟机 (pKVM) 构建于 Linux KVM Hypervisor 之上,后者已扩展了以下功能:限制对在创建时标记为“受保护”的 guest 虚拟机中运行的有效负载的访问。

KVM/arm64 支持不同的执行模式,具体取决于某些 CPU 功能的可用性,即虚拟化主机扩展 (VHE)(ARMv8.1 及更高版本)。在其中一种模式(通常称为非 VHE 模式)下,Hypervisor 代码在启动期间从内核映像中分离出来并安装在 EL2 级别,而内核本身在 EL1 级别运行。尽管 EL2 组件是 Linux 代码库的一部分,但它是一个小型组件,负责在多个 EL1 级别之间切换。Hypervisor 组件与 Linux 一起编译,但驻留在 vmlinux 映像的单独专用内存部分中。pKVM 利用此设计,通过扩展 Hypervisor 代码的新功能,使其能够限制 Android 主机内核和用户空间,并限制主机对 guest 内存和 Hypervisor 的访问。

pKVM 供应商模块

pKVM 供应商模块是包含设备特定功能的硬件特定模块,例如输入输出内存管理单元 (IOMMU) 驱动程序。这些模块让您可以将需要异常级别 2 (EL2) 访问权限的安全功能移植到 pKVM。

要了解如何实现和加载 pKVM 供应商模块,请参阅实现 pKVM 供应商模块

启动过程

下图描述了 pKVM 启动过程

pKVM boot procedure

图 1. pKVM 启动过程

  1. 引导加载程序在 EL2 级别进入通用内核。
  2. 通用内核检测到它正在 EL2 级别运行,并将其自身降级到 EL1 级别,而 pKVM 及其模块继续在 EL2 级别运行。此外,此时会加载 pKVM 供应商模块。
  3. 通用内核继续正常启动,加载所有必要的设备驱动程序,直到到达用户空间。此时,pKVM 已就位并处理第 2 阶段页表。

启动过程信任引导加载程序仅在早期启动期间维护内核映像的完整性。当内核被降级时,Hypervisor 不再认为它是可信的,然后 Hypervisor 负责保护自身,即使内核受到入侵也是如此。

在同一二进制映像中包含 Android 内核和 Hypervisor 允许它们之间建立非常紧密的通信接口。这种紧密耦合保证了两个组件的原子更新,从而避免了保持它们之间的接口稳定的需要,并提供了极大的灵活性,而不会影响长期可维护性。当两个组件可以在不影响 Hypervisor 提供的安全保证的情况下进行协作时,这种紧密耦合还可以实现性能优化。

此外,在 Android 生态系统中采用 GKI 会自动允许将 pKVM Hypervisor 以与内核相同的二进制文件部署到 Android 设备。

CPU 内存访问保护

Arm 架构指定了一个内存管理单元 (MMU),它分为两个独立的阶段,这两个阶段都可用于实现地址转换和对内存不同部分的访问控制。第 1 阶段 MMU 由 EL1 控制,并允许第一级地址转换。Linux 使用第 1 阶段 MMU 来管理提供给每个用户空间进程及其自身虚拟地址空间的虚拟地址空间。

第 2 级 MMU 由 EL2 控制,并在第 1 级 MMU 的输出地址上启用二级地址转换,从而生成物理地址 (PA)。Hypervisor 可以使用第 2 级转换来控制和转换所有 Guest VM 的内存访问。如图 2 所示,当启用两级转换时,第 1 级的输出地址称为中间物理地址 (IPA)。注意:虚拟地址 (VA) 先转换为 IPA,然后再转换为 PA。

CPU memory access protection

图 2. CPU 内存访问保护

从历史上看,KVM 在运行 Guest 虚拟机时启用第 2 级转换,而在运行宿主机 Linux 内核时禁用第 2 级转换。这种架构允许来自宿主机第 1 级 MMU 的内存访问穿过第 2 级 MMU,从而允许宿主机无限制地访问 Guest 虚拟机内存页。另一方面,pKVM 即使在宿主机上下文中也启用第 2 级保护,并由 Hypervisor 负责保护 Guest 虚拟机内存页,而不是宿主机。

KVM 充分利用第 2 级的地址转换,为 Guest 虚拟机实现复杂的 IPA/PA 映射,从而为 Guest 虚拟机创建连续内存的假象,尽管物理内存是碎片化的。但是,宿主机的第 2 级 MMU 的使用仅限于访问控制。宿主机第 2 级是恒等映射的,确保宿主机 IPA 空间中的连续内存在 PA 空间中也是连续的。这种架构允许在页表中Large Mapping 的使用,从而减少了转换后备缓冲区 (TLB) 的压力。由于恒等映射可以通过 PA 索引,因此宿主机第 2 级也用于直接在页表中跟踪页面所有权。

直接内存访问 (DMA) 保护

如前所述,从 CPU 页表中取消映射 Linux 宿主机的 Guest 虚拟机页面是保护 Guest 虚拟机内存的必要但不充分的步骤。pKVM 还需要防止宿主机内核控制下的 DMA 功能设备进行的内存访问,以及恶意宿主机发起的 DMA 攻击的可能性。为了防止此类设备访问 Guest 虚拟机内存,如图 3 所示,pKVM 要求系统中每个具有 DMA 功能的设备都配备输入输出内存管理单元 (IOMMU) 硬件。

Dma memory access protection

图 3. DMA 内存访问保护

至少,IOMMU 硬件提供了在页面粒度上授予和撤销设备对物理内存的读/写访问权限的方法。但是,这种 IOMMU 硬件限制了 pVM 中设备的使用,因为它们假定为恒等映射的第 2 级。

为了确保虚拟机之间的隔离,代表不同实体生成的内存事务必须能够被 IOMMU 区分,以便可以使用适当的页表集进行转换。

此外,减少 EL2 级的 SoC 特定代码量是减少 pKVM 总体可信计算基 (TCB) 的关键策略,并且与在 Hypervisor 中包含 IOMMU 驱动程序背道而驰。为了缓解这个问题,EL1 级的宿主机负责辅助 IOMMU 管理任务,例如电源管理、初始化以及在适当情况下的中断处理。

但是,将宿主机置于设备状态的控制之下,对 IOMMU 硬件的编程接口提出了额外的要求,以确保权限检查不会被其他方式绕过,例如,在设备重置之后。

Arm 设备的标准且支持良好的 IOMMU,既可以实现隔离,又可以直接分配,是 Arm 系统内存管理单元 (SMMU) 架构。该架构是推荐的参考解决方案。

内存所有权

在启动时,所有非 Hypervisor 内存都被假定为由宿主机拥有,并由 Hypervisor 如此跟踪。当 pVM 启动时,宿主机捐赠内存页以允许其启动,并且 Hypervisor 将这些页面的所有权从宿主机转移到 pVM。因此,Hypervisor 在宿主机的第 2 级页表中设置了访问控制限制,以防止宿主机再次访问这些页面,从而为 Guest 虚拟机提供机密性。

宿主机和 Guest 虚拟机之间的通信通过它们之间受控的内存共享来实现。Guest 虚拟机被允许使用 Hypercall 将它们的一些页面共享回宿主机,Hypercall 指示 Hypervisor 在宿主机的第 2 级页表中重新映射这些页面。同样,宿主机与 TrustZone 的通信通过内存共享和/或借用操作来实现,所有这些操作都由 pKVM 使用 Arm 固件框架 (FF-A) 规范 密切监控和控制。

由于 pVM 的内存需求可能会随时间变化,因此提供了一个 Hypercall,允许调用者放弃属于调用者的指定页面的所有权,将其返回给宿主机。在实践中,此 Hypercall 与 virtio balloon 协议一起使用,以允许 VMM 从 pVM 请求内存返回,并允许 pVM 以受控方式通知 VMM 已放弃的页面。

Hypervisor 负责跟踪系统中所有内存页面的所有权,以及它们是否正在与其他实体共享或借用。大多数状态跟踪都是使用附加到宿主机和 Guest 虚拟机第 2 级页表的元数据完成的,使用页表条目 (PTE) 中的保留位,顾名思义,这些保留位是为软件使用而保留的。

宿主机必须确保它不会尝试访问已被 Hypervisor 设置为不可访问的页面。非法宿主机访问会导致同步异常被 Hypervisor 注入到宿主机中,这可能导致负责的用户空间任务收到 SEGV 信号,或者宿主机内核崩溃。为了防止意外访问,捐赠给 Guest 虚拟机的页面不符合宿主机内核的交换或合并条件。

中断处理和定时器

中断是 Guest 虚拟机与设备交互以及 CPU 之间通信的重要组成部分,其中处理器间中断 (IPI) 是主要的通信机制。KVM 模型是将所有虚拟中断管理委托给 EL1 级的宿主机,为此,宿主机的行为类似于 Hypervisor 的不受信任部分。

pKVM 基于现有的 KVM 代码,提供完整的通用中断控制器版本 3 (GICv3) 仿真。定时器和 IPI 作为此不受信任仿真代码的一部分进行处理。

GICv3 支持

EL1 和 EL2 之间的接口必须确保完整的终端状态对 EL1 宿主机可见,包括与中断相关的 Hypervisor 寄存器的副本。这种可见性通常使用共享内存区域来实现,每个虚拟 CPU (vCPU) 一个共享内存区域。

系统寄存器运行时支持代码可以简化为仅支持软件生成中断寄存器 (SGIR) 和停用中断寄存器 (DIR) 寄存器陷阱。该架构规定这些寄存器始终陷入 EL2,而到目前为止,其他陷阱仅用于缓解勘误。其他一切都在硬件中处理。

在 MMIO 方面,一切都在 EL1 级仿真,重用 KVM 中的所有当前基础设施。最后,等待中断 (WFI) 始终中继到 EL1,因为这是 KVM 使用的基本调度原语之一。

定时器支持

虚拟定时器的比较器值必须在每次陷入 WFI 时暴露给 EL1,以便 EL1 可以在 vCPU 被阻塞时注入定时器中断。物理定时器完全被仿真,所有陷阱都中继到 EL1。

MMIO 处理

为了与虚拟机监视器 (VMM) 通信并执行 GIC 仿真,MMIO 陷阱必须中继回 EL1 级的宿主机以进行进一步分类。pKVM 需要以下内容

  • 访问的 IPA 和大小
  • 写入时的数据
  • 陷入时 CPU 的字节序

此外,使用通用寄存器 (GPR) 作为源/目标的陷阱使用抽象传输伪寄存器中继。

Guest 虚拟机接口

Guest 虚拟机可以使用 Hypercall 和内存访问陷入区域的组合与受保护的 Guest 虚拟机通信。Hypercall 根据 SMCCC 标准 公开,范围由 KVM 为供应商分配保留。以下 Hypercall 对 pKVM Guest 虚拟机尤为重要。

通用 Hypercall

  • PSCI 为 Guest 虚拟机提供了一种标准机制来控制其 vCPU 的生命周期,包括上线、下线和系统关闭。
  • TRNG 为 Guest 虚拟机提供了一种标准机制,用于从 pKVM 请求熵,pKVM 将调用中继到 EL3。当宿主机不可信任以虚拟化硬件随机数生成器 (RNG) 时,此机制尤其有用。

pKVM Hypercall

  • 与宿主机共享内存。所有 Guest 虚拟机内存最初都对宿主机不可访问,但宿主机访问对于共享内存通信和依赖共享缓冲区的半虚拟化设备是必要的。用于与宿主机共享和取消共享页面的 Hypercall 允许 Guest 虚拟机准确地决定将哪些内存部分提供给 Android 的其余部分访问,而无需握手。
  • 向宿主机放弃内存。通常,所有 Guest 虚拟机内存都归 Guest 虚拟机所有,直到它被销毁。对于内存需求随时间变化的长期虚拟机来说,这种状态可能是不够的。relinquish Hypercall 允许 Guest 虚拟机显式地将其页面的所有权转移回宿主机,而无需 Guest 虚拟机终止。
  • 内存访问陷入到宿主机。传统上,如果 KVM Guest 虚拟机访问与有效内存区域不对应的地址,则 vCPU 线程退出到宿主机,并且该访问通常用于 MMIO 并由用户空间中的 VMM 仿真。为了方便这种处理,pKVM 需要向宿主机通告有关故障指令的详细信息,例如其地址、寄存器参数以及可能的内容,如果陷阱不是预期的,这可能会无意中暴露受保护 Guest 虚拟机的敏感数据。pKVM 通过将这些故障视为致命故障来解决此问题,除非 Guest 虚拟机先前已发出 Hypercall 以将故障 IPA 范围标识为允许访问陷入回宿主机的范围。此解决方案称为MMIO 保护

虚拟 I/O 设备 (virtio)

Virtio 是一种流行的、可移植且成熟的标准,用于实现半虚拟化设备并与之交互。暴露给受保护 Guest 虚拟机的大多数设备都是使用 virtio 实现的。Virtio 还支持用于受保护 Guest 虚拟机和 Android 其余部分之间通信的 vsock 实现。

Virtio 设备通常在宿主机用户空间中由 VMM 实现,VMM 拦截来自 Guest 虚拟机到 virtio 设备的 MMIO 接口的陷入内存访问,并仿真预期的行为。MMIO 访问相对昂贵,因为每次访问设备都需要往返 VMM,因此设备和 Guest 虚拟机之间的大部分实际数据传输都是使用内存中的一组 virtqueue 进行的。virtio 的一个关键假设是宿主机可以任意访问 Guest 虚拟机内存。这种假设在 virtqueue 的设计中很明显,virtqueue 可能包含指向 Guest 虚拟机中缓冲区的指针,设备仿真旨在直接访问这些缓冲区。

尽管先前描述的内存共享 Hypercall 可以用于从 Guest 虚拟机到宿主机共享 virtio 数据缓冲区,但这种共享必然以页面粒度执行,并且如果缓冲区大小小于页面大小,则可能最终暴露比所需更多的数据。相反,Guest 虚拟机被配置为从固定的共享内存窗口分配 virtqueue 及其对应的数据缓冲区,数据根据需要复制(弹跳)到窗口和从窗口复制(弹跳)。

Virtual device

图 4. Virtio 设备

与 TrustZone 交互

尽管 Guest 虚拟机无法直接与 TrustZone 交互,但宿主机仍然必须能够向安全世界发出 SMC 调用。这些调用可以指定宿主机无法访问的物理寻址内存缓冲区。由于安全软件通常不知道缓冲区的可访问性,因此恶意宿主机可以使用此缓冲区执行混淆副手攻击(类似于 DMA 攻击)。为了防止此类攻击,pKVM 捕获所有宿主机 SMC 调用到 EL2,并充当宿主机和 EL3 级的安全监视器之间的代理。

来自宿主机的 PSCI 调用将以最小的修改转发到 EL3 固件。具体而言,将重写 CPU 上线或从挂起状态恢复的入口点,以便在返回到 EL1 级的宿主机之前在 EL2 级安装第 2 级页表。在启动期间,此保护由 pKVM 强制执行。

此架构依赖于 SoC 支持 PSCI,最好是通过使用最新版本的 TF-A 作为其 EL3 固件。

Arm 固件框架 (FF-A) 标准化了正常世界和安全世界之间的交互,尤其是在存在安全 Hypervisor 的情况下。规范的主要部分定义了一种与安全世界共享内存的机制,既使用了通用的消息格式,又使用了底层页面的明确定义的权限模型。pKVM 代理 FF-A 消息以确保宿主机不会尝试与安全端共享它没有足够权限的内存。

此架构依赖于安全世界软件强制执行内存访问模型,以确保受信任的应用程序和在安全世界中运行的任何其他软件只能访问内存,前提是内存要么由安全世界独占拥有,要么已使用 FF-A 与其显式共享。在使用 S-EL2 的系统上,强制执行内存访问模型应由安全分区管理器核心 (SPMC)(例如 Hafnium)完成,SPMC 维护安全世界的第 2 级页表。在没有 S-EL2 的系统上,TEE 可以通过其第 1 级页表来强制执行内存访问模型。

如果到 EL2 的 SMC 调用不是 PSCI 调用或 FF-A 定义的消息,则未处理的 SMC 将转发到 EL3。假设(必然受信任的)安全固件可以安全地处理未处理的 SMC,因为固件了解维护 pVM 隔离所需的预防措施。

虚拟机监视器

crosvm 是一个虚拟机监视器 (VMM),它通过 Linux 的 KVM 接口运行虚拟机。crosvm 的独特之处在于它专注于安全,使用 Rust 编程语言和虚拟设备周围的沙箱来保护宿主机内核。有关 crosvm 的更多信息,请参阅其官方文档 此处

文件描述符和 ioctl

KVM 向用户空间公开 /dev/kvm 字符设备,其中 ioctl 构成了 KVM API。ioctl 属于以下类别

  • 系统 ioctl 查询和设置影响整个 KVM 子系统的全局属性,并创建 pVM。
  • VM ioctl 查询和设置创建虚拟 CPU (vCPU) 和设备以及影响整个 pVM 的属性,例如包括内存布局以及虚拟 CPU (vCPU) 和设备的数量。
  • vCPU ioctl 查询和设置控制单个虚拟 CPU 运行的属性。
  • 设备 ioctl 查询和设置控制单个虚拟设备运行的属性。

每个 crosvm 进程运行一个虚拟机实例。此进程使用 KVM_CREATE_VM 系统 ioctl 创建 VM 文件描述符,该文件描述符可用于发出 pVM ioctl。KVM_CREATE_VCPUKVM_CREATE_DEVICE 在 VM FD 上的 ioctl 创建 vCPU/设备并返回指向新资源的文件描述符。vCPU 或设备 FD 上的 ioctl 可用于控制使用 VM FD 上的 ioctl 创建的设备。对于 vCPU,这包括运行 Guest 虚拟机代码的重要任务。

在内部,crosvm 使用边缘触发的 epoll 接口向内核注册 VM 的文件描述符。然后,每当任何文件描述符中有新的事件挂起时,内核都会通知 crosvm。

pKVM 添加了一项新功能 KVM_CAP_ARM_PROTECTED_VM,可用于获取有关 pVM 环境的信息并为 VM 设置保护模式。如果传递了 --protected-vm 标志,crosvm 会在 pVM 创建期间使用此功能来查询和保留适当数量的内存用于 pVM 固件,然后启用保护模式。

内存分配

VMM 的主要职责之一是分配 VM 的内存并管理其内存布局。crosvm 生成 下表大致描述的固定内存布局。

正常模式下的 FDT PHYS_MEMORY_END - 0x200000
可用空间 ...
Ramdisk ALIGN_UP(KERNEL_END, 0x1000000)
内核 0x80080000
引导加载程序 0x80200000
BIOS 模式下的 FDT 0x80000000
物理内存基址 0x80000000
pVM 固件 0x7FE00000
设备内存 0x10000 - 0x40000000

物理内存使用 mmap 分配,并将内存捐赠给 VM,以使用 KVM_SET_USER_MEMORY_REGION ioctl 填充其内存区域(称为内存槽)。因此,所有 Guest 虚拟机 pVM 内存都归管理它的 crosvm 实例所有,如果宿主机开始耗尽可用内存,可能会导致进程被终止(终止 VM)。当 VM 停止时,内存会自动被 Hypervisor 擦除并返回给宿主机内核。

在常规 KVM 下,VMM 保留对所有 Guest 虚拟机内存的访问权限。使用 pKVM,Guest 虚拟机内存从宿主机物理地址空间取消映射,当它捐赠给 Guest 虚拟机时。唯一的例外是 Guest 虚拟机显式共享回的内存,例如用于 virtio 设备。

Guest 虚拟机地址空间中的 MMIO 区域保持未映射状态。Guest 虚拟机对这些区域的访问被捕获,并在 VM FD 上产生 I/O 事件。此机制用于实现虚拟设备。在保护模式下,Guest 虚拟机必须确认其地址空间区域将用于 MMIO,使用 Hypercall,以降低意外信息泄漏的风险。

调度

每个虚拟 CPU 由一个 POSIX 线程表示,并由宿主机 Linux 调度程序调度。线程在 vCPU FD 上调用 KVM_RUN ioctl,导致 Hypervisor 切换到 Guest 虚拟机 vCPU 上下文。宿主机调度程序将 Guest 虚拟机上下文中花费的时间计为相应 vCPU 线程使用的时间。KVM_RUN 在发生必须由 VMM 处理的事件(例如 I/O、中断结束或 vCPU 暂停)时返回。VMM 处理该事件并再次调用 KVM_RUN

KVM_RUN 期间,线程保持可被宿主机调度程序抢占,但 EL2 Hypervisor 代码的执行除外,EL2 Hypervisor 代码不可抢占。Guest 虚拟机 pVM 本身没有机制来控制此行为。

由于所有 vCPU 线程都像任何其他用户空间任务一样被调度,因此它们都受到所有标准 QoS 机制的约束。具体而言,每个 vCPU 线程都可以被绑定到物理 CPU、放置在 cpuset 中、使用利用率钳位进行提升或限制、更改其优先级/调度策略等等。

虚拟设备

crosvm 支持许多设备,包括以下设备

  • virtio-blk 用于复合磁盘映像,只读或读写
  • vhost-vsock 用于与宿主机通信
  • virtio-pci 作为 virtio 传输
  • pl030 实时时钟 (RTC)
  • 16550a UART 用于串行通信

pVM 固件

pVM 固件 (pvmfw) 是 pVM 执行的第一个代码,类似于物理设备的引导 ROM。pvmfw 的主要目标是引导安全启动并派生 pVM 的唯一密钥。pvmfw 不限于与任何特定的操作系统(例如 Microdroid)一起使用,只要该操作系统受 crosvm 支持并且已正确签名即可。

pvmfw 二进制文件存储在同名的闪存分区中,并使用 OTA 进行更新。

设备启动

以下步骤序列已添加到启用 pKVM 的设备的启动过程中

  1. Android Bootloader (ABL) 从其分区将 pvmfw 加载到内存中并验证映像。
  2. ABL 从信任根获取其设备标识符组合引擎 (DICE) 密钥(复合设备标识符 (CDI) 和 DICE 证书链)。
  3. ABL 为 pvmfw 派生必要的 CDI,并将它们附加到 pvmfw 二进制文件。
  4. ABL 向 DT 添加一个 linux,pkvm-guest-firmware-memory 保留内存区域节点,描述 pvmfw 二进制文件及其在上一步中派生的密钥的位置和大小。
  5. ABL 将控制权交给 Linux,Linux 初始化 pKVM。
  6. pKVM 从宿主机的第 2 级页表中取消映射 pvmfw 内存区域,并在整个设备正常运行时间内保护它免受宿主机(和 Guest 虚拟机)的侵害。

设备启动后,按照 启动顺序 部分中的步骤启动 Microdroid Microdroid 文档。

pVM 启动

创建 pVM 时,crosvm(或其他 VMM)必须创建一个足够大的内存槽,以便 Hypervisor 用 pvmfw 映像填充它。VMM 还受到限制,只能设置其初始值的寄存器列表(主 vCPU 为 x0-x14,辅助 vCPU 没有)。其余寄存器是保留的,并且是 Hypervisor-pvmfw ABI 的一部分。

当 pVM 运行时,Hypervisor 首先将主 vCPU 的控制权交给 pvmfw。固件期望 crosvm 已将 AVB 签名的内核(可以是引导加载程序或任何其他映像)和未签名的 FDT 加载到已知偏移量的内存中。pvmfw 验证 AVB 签名,如果成功,则从接收到的 FDT 生成受信任的设备树,从内存中擦除其密钥,并分支到有效负载的入口点。如果其中一个验证步骤失败,固件将发出 PSCI SYSTEM_RESET Hypercall。

在启动之间,有关 pVM 实例的信息存储在一个分区(virtio-blk 设备)中,并使用 pvmfw 的密钥加密,以确保在重新启动后,密钥被配置到正确的实例。