AVF 架构

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

背景

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

EL2 层允许引入一个管理程序,该管理程序可以将内存和设备隔离到 EL1/EL0 的各个 pVM 中,并提供强大的机密性和完整性保证。

管理程序

受保护的基于内核的虚拟机 (pKVM)构建在Linux KVM 管理程序之上,该管理程序已扩展为能够限制对在创建时标记为“受保护”的来宾虚拟机中运行的有效负载的访问。

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

开机程序

pKVM 启动过程如图 1 所示。第一步是引导加载程序在 EL2 进入支持 pKVM 的 Linux 内核。在早期引导期间,内核检测到它在 EL2 上运行,将自己剥夺到 EL1 的特权,留下 pKVM。从这一点开始,Linux 内核继续正常启动,加载所有必要的设备驱动程序,直到到达用户空间。这些步骤在 pKVM 的控制下发生。

引导过程信任引导加载程序仅在早期引导期间维护内核映像的完整性。当内核被剥夺特权时,它不再被管理程序视为信任,即使内核受到威胁,管理程序也会负责保护自己。

pKVM 引导程序

图 1. pKVM 引导程序

将 Android 内核和虚拟机管理程序置于同一个二进制映像中,可以在它们之间建立非常紧密耦合的通信接口。这种紧密耦合保证了两个组件的原子更新,这避免了保持它们之间接口稳定的需要,并提供了很大的灵活性,而不会影响长期可维护性。当两个组件可以合作而不影响管理程序提供的安全保证时,紧密耦合还允许性能优化。

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

CPU内存访问保护

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

第 2 级 MMU 由 EL2 控制,并支持对第 1 级 MMU 的输出地址应用第二次地址转换,从而生成物理地址 (PA)。管理程序可以使用第 2 阶段转换来控制和转换来自所有来宾 VM 的内存访问。如图2所示,当两个阶段的翻译都使能时,阶段1的输出地址称为中间物理地址(IPA) 注:虚拟地址(VA)被翻译成IPA,然后再翻译成PA。

CPU内存访问保护

图 2. CPU 内存访问保护

从历史上看,KVM 在运行客户机时启用第 2 阶段转换,而在运行主机 Linux 内核时禁用第 2 阶段。此架构允许从主机阶段 1 MMU 进行的内存访问通过阶段 2 MMU,从而允许从主机不受限制地访问来宾内存页面。另一方面,pKVM 甚至在主机上下文中也启用了第 2 阶段保护,并让管理程序负责保护来宾内存页面而不是主机。

KVM 充分利用第 2 阶段的地址转换来为来宾实现复杂的 IPA/PA 映射,这为来宾创造了尽管物理碎片但内存连续的错觉。但是,主机的第 2 阶段 MMU 的使用仅限于访问控制。主机阶段 2 是身份映射的,确保主机 IPA 空间中的连续内存在 PA 空间中是连续的。这种架构允许在页表中使用大型映射,从而减少转换后备缓冲区 (TLB) 的压力。因为标识映射可以被 PA 索引,所以主机阶段 2 也用于直接在页表中跟踪页面所有权。

直接内存访问 (DMA) 保护

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

DMA内存访问保护

图 3. DMA 内存访问保护

至少,IOMMU 硬件提供了以页面粒度授予和撤销设备对物理内存的读/写访问权限的方法。但是,此 IOMMU 硬件限制了设备在 pVM 中的使用,因为它们假设身份映射阶段 2。

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

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

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

Arm 系统内存管理单元 (SMMU) 架构是一个标准且得到良好支持的 Arm 设备 IOMMU,它使隔离和直接分配成为可能。此架构是推荐的参考解决方案。

内存所有权

在启动时,所有非管理程序内存都被假定为主机所有,并由管理程序跟踪。生成 pVM 时,主机会提供内存页面以允许其启动,管理程序会将这些页面的所有权从主机转移到 pVM。因此,管理程序在主机的第 2 阶段页表中放置访问控制限制,以防止它再次访问页面,从而为来宾提供机密性。

主机和来宾之间的通信是通过它们之间受控的内存共享实现的。来宾可以使用超级调用与主机共享他们的一些页面,这会指示管理程序将这些页面重新映射到主机阶段 2 页表中。同样,主机与 TrustZone 的通信是通过内存共享和/或借出操作实现的,所有这些操作都由 pKVM 使用Arm 固件框架 (FF-A) 规范密切监视和控制。

管理程序负责跟踪系统中所有内存页面的所有权,以及它们是否被共享或借给其他实体。大多数状态跟踪是使用附加到主机和访客的第 2 阶段页表的元数据完成的,使用页表条目 (PTE) 中的保留位,顾名思义,这些保留位供软件使用。

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

中断处理和定时器

中断是来宾与设备交互以及 CPU 之间通信方式的重要组成部分,其中处理器间中断 (IPI) 是主要的通信机制。 KVM 模型将所有虚拟中断管理委托给 EL1 中的主机,为此目的,它充当管理程序的不受信任部分。

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

GICv3 支持

EL1 和 EL2 之间的接口必须确保完整的中断状态对 EL1 主机可见,包括与中断相关的管理程序寄存器的副本。这种可见性通常是使用共享内存区域实现的,每个虚拟 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) 作为源/目标的陷阱。

访客界面

来宾可以使用超级调用和对受困区域的内存访问的组合与受保护的来宾进行通信。 Hypercalls 根据SMCCC 标准公开,范围保留给 KVM 的供应商分配。以下超级调用对 pKVM 来宾特别重要。

通用超级调用

  • PSCI 为来宾提供了一种标准机制来控制其 vCPU 的生命周期,包括上线、下线和系统关闭。
  • TRNG 为客户提供了一种标准机制,可以从 pKVM 请求熵,pKVM 将调用中继到 EL3。在无法信任主机虚拟化硬件随机数生成器 (RNG) 的情况下,此机制特别有用。

pKVM 超级调用

  • 与主机共享内存。主机最初无法访问所有来宾内存,但主机访问对于共享内存通信和依赖共享缓冲区的半虚拟化设备是必需的。用于与主机共享和取消共享页面的超调用允许来宾准确地决定哪些内存部分可供 Android 的其余部分访问,而无需握手。
  • 内存访问陷阱到主机。传统上,如果 KVM 来宾访问不对应于有效内存区域的地址,则 vCPU 线程退出到主机并且访问通常用于 MMIO 并由用户空间中的 VMM 模拟。为了促进这种处理,pKVM 需要将有关故障指令的详细信息(例如其地址、寄存器参数和可能的内容)播发回主机,如果没有预料到陷阱,这可能会无意中暴露来自受保护客户机的敏感数据。 pKVM 通过将这些故障视为致命故障来解决此问题,除非来宾先前已发出 hypercall 以将故障 IPA 范围识别为允许访问的范围返回到主机。这个解决方案被称为MMIO 守卫

虚拟 I/O 设备 (virtio)

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

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

尽管前面描述的内存共享超级调用可用于从客户机到主机共享 virtio 数据缓冲区,但这种共享必须以页面粒度执行,并且如果缓冲区大小小于页面大小,最终可能会暴露比所需更多的数据.相反,来宾被配置为从固定的共享内存窗口分配 virtqueue 及其相应的数据缓冲区,并根据需要将数据复制(反弹)到窗口或从窗口复制(反弹)。

虚拟设备

图 4. Virtio 设备

与 TrustZone 互动

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

来自主机的 PSCI 调用被转发到经过最少修改的 EL3 固件。具体来说,重写 CPU 联机或从挂起恢复的入口点,以便在返回到 EL1 的主机之前将阶段 2 页表安装在 EL2。在引导期间,此保护由 pKVM 强制执行。

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

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

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

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

虚拟机监视器

crosvm 是一个虚拟机监视器 (VMM),它通过 Linux 的 KVM 接口运行虚拟机。 crosvm 的独特之处在于它使用 Rust 编程语言和围绕虚拟设备的沙箱来保护主机内核,从而专注于安全性。

文件描述符和 ioctl

KVM 使用构成 KVM API 的 ioctl 将/dev/kvm字符设备暴露给用户空间。 ioctl 属于以下类别:

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

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

在内部,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
可用空间...
虚拟磁盘ALIGN_UP(KERNEL_END, 0x1000000)
核心0x80080000
引导程序0x80200000
BIOS 模式下的 FDT 0x80000000
物理内存基础0x80000000
虚拟机固件0x7FE00000
设备内存0x10000 - 0x40000000

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

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

来宾地址空间中的 MMIO 区域未映射。访客对这些区域的访问被捕获并导致 VM FD 上的 I/O 事件。该机制用于实现虚拟设备。在保护模式下,来宾必须确认其地址空间的一个区域已用于使用 hypercall 的 MMIO,以降低意外信息泄漏的风险。

调度

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

KVM_RUN期间,线程保持可被主机调度程序抢占,但 EL2 管理程序代码的执行除外,它是不可抢占的。来宾 pVM 本身没有控制此行为的机制。

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

虚拟设备

crosvm 支持多种设备,包括:

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

虚拟机固件

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

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

设备开机

将以下步骤序列添加到启用 pKVM 的设备的引导过程中:

  1. Android 引导加载程序 (ABL) 将 pvmfw 从其分区加载到内存中并验证映像。
  2. ABL 从信任根获取其设备标识符组合引擎 (DICE) 机密(复合设备标识符 (CDI) 和引导证书链 (BCC))。
  3. ABL 执行 pvmfw 的秘密 (CDI) 的测量和 DICE 推导,并将它们附加到 pvmfw 二进制文件。
  4. ABL 在 DT 中添加了一个linux,pkvm-guest-firmware-memory reserved memory region 节点,描述了 pvmfw 二进制文件的位置和大小以及它在上一步中导出的秘密。
  5. ABL 将控制权交给 Linux,然后 Linux 初始化 pKVM。
  6. pKVM 从主机的第 2 阶段页表中取消映射 pvmfw 内存区域,并在整个设备正常运行期间保护它免受主机(和来宾)的影响。

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

虚拟机启动

创建 pVM 时,crosvm(或另一个 VMM)必须创建一个足够大的内存槽,以便管理程序用 pvmfw 映像填充。 VMM 也被限制在它可以设置初始值的寄存器列表中(x0-x14 用于主 vCPU,无用于辅助 vCPU)。其余的寄存器被保留并且是 hypervisor-pvmfw ABI 的一部分。

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

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