Android 提供了实现 Android 虚拟化框架所需的所有组件的参考实现。目前此实现仅限于 ARM64。本页解释了框架架构。
背景
Arm 架构允许最多四个异常级别,异常级别 0 (EL0) 的特权最少,异常级别 3 (EL3) 的特权最多。 Android 代码库的最大部分(所有用户空间组件)在 EL0 上运行。通常称为“Android”的其余部分是在 EL1 上运行的 Linux 内核。
EL2 层允许引入管理程序,该管理程序能够将内存和设备隔离到 EL1/EL0 的单个 pVM 中,并具有强大的机密性和完整性保证。
管理程序
受保护的基于内核的虚拟机 (pKVM)建立在Linux KVM 管理程序之上,该管理程序已扩展为限制访问在创建时标记为“受保护”的来宾虚拟机中运行的有效负载的能力。
KVM/arm64 支持不同的执行模式,具体取决于某些 CPU 功能的可用性,即虚拟化主机扩展 (VHE)(ARMv8.1 及更高版本)。在其中一种模式(通常称为非 VHE 模式)中,管理程序代码在引导期间从内核映像中分离出来并安装在 EL2 上,而内核本身在 EL1 上运行。虽然是 Linux 代码库的一部分,但 KVM 的 EL2 组件是一个小组件,负责多个 EL1 之间的切换,完全由主机内核控制。管理程序组件使用 Linux 编译,但驻留在vmlinux
映像的一个单独的专用内存部分中。 pKVM 通过使用新功能扩展虚拟机管理程序代码来利用这种设计,允许它限制 Android 主机内核和用户空间,并限制主机访问客户内存和虚拟机管理程序。
引导过程
pKVM 引导过程如图 1 所示。第一步是引导加载程序在 EL2 进入启用 pKVM 的 Linux 内核。在早期启动期间,内核检测到它在 EL2 上运行,将自身取消特权到 EL1,留下 pKVM。从此时起,Linux 内核继续正常启动,加载所有必要的设备驱动程序,直到到达用户空间。这些步骤在 pKVM 的控制下发生。
引导过程信任引导加载程序仅在早期引导期间维护内核映像的完整性。当内核被取消特权时,它不再被虚拟机管理程序认为是信任的,即使内核受到损害,它也会负责保护自己。
将 Android 内核和虚拟机管理程序放在同一个二进制映像中,可以在它们之间建立一个非常紧密耦合的通信接口。这种紧密耦合保证了两个组件的原子更新,从而避免了保持它们之间的接口稳定的需要,并在不影响长期可维护性的情况下提供了很大的灵活性。当两个组件可以合作而不影响管理程序提供的安全保证时,紧密耦合还允许性能优化。
此外,Android 生态系统中 GKI 的采用自动允许 pKVM 管理程序以与内核相同的二进制文件部署到 Android 设备。
CPU内存访问保护
Arm 架构指定了一个内存管理单元(MMU),分为两个独立的阶段,这两个阶段都可以用来实现对内存不同部分的地址转换和访问控制。第一级 MMU 由 EL1 控制并允许第一级地址转换。第 1 阶段 MMU 被 Linux 用来管理提供给每个用户空间进程及其自己的虚拟地址空间的虚拟地址空间。
第 2 级 MMU 由 EL2 控制,并允许在第 1 级 MMU 的输出地址上应用第二个地址转换,从而产生物理地址 (PA)。管理程序可以使用阶段 2 转换来控制和转换来自所有来宾 VM 的内存访问。如图 2 所示,当两个阶段的转换都启用时,阶段 1 的输出地址称为中间物理地址 (IPA) 注意:虚拟地址 (VA) 先转换为 IPA,然后再转换为 PA。
从历史上看,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 所示。
至少,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) 作为源/目标的陷阱使用抽象传输伪寄存器进行中继。
访客界面
访客可以使用超级调用和对被困区域的内存访问的组合与受保护的访客进行通信。超级调用根据SMCCC 标准公开,并为 KVM 的供应商分配保留了一个范围。以下超级调用对 pKVM 来宾特别重要。
通用超级调用
- PSCI 为来宾提供了一种标准机制来控制其 vCPU 的生命周期,包括联机、脱机和系统关闭。
- TRNG 为来宾提供了一种标准机制,可以从 pKVM 请求熵,pKVM 将调用中继到 EL3。这种机制在不能信任主机来虚拟化硬件随机数生成器 (RNG) 的情况下特别有用。
pKVM 超级调用
- 与主机共享内存。主机最初无法访问所有客户内存,但对于共享内存通信和依赖共享缓冲区的半虚拟化设备,主机访问是必需的。与主机共享和取消共享页面的超级调用允许访客准确地决定哪些内存部分可供 Android 的其余部分访问,而无需握手。
- 对主机的内存访问陷阱。传统上,如果 KVM 来宾访问的地址与有效内存区域不对应,则 vCPU 线程将退出到主机,并且该访问通常用于 MMIO,并由用户空间中的 VMM 模拟。为了促进这种处理,pKVM 需要将有关故障指令的详细信息(例如其地址、寄存器参数和可能的内容)通告回主机,如果没有预料到陷阱,这可能会无意中暴露受保护来宾的敏感数据。 pKVM 通过将这些故障视为致命故障来解决此问题,除非客户机先前已发出超级调用以将故障 IPA 范围识别为允许访问回主机的范围。此解决方案称为MMIO 防护。
虚拟 I/O 设备 (virtio)
Virtio 是一种流行的、可移植的、成熟的标准,用于实现半虚拟化设备并与之交互。暴露给受保护来宾的大多数设备都是使用 virtio 实现的。 Virtio 还支持用于受保护来宾与 Android 其余部分之间通信的 vsock 实现。
Virtio 设备通常由 VMM 在主机的用户空间中实现,VMM 拦截从 guest 到 virtio 设备的 MMIO 接口的捕获内存访问并模拟预期行为。 MMIO 访问相对昂贵,因为对设备的每次访问都需要往返 VMM 和返回,因此设备和来宾之间的大部分实际数据传输是使用内存中的一组 virtqueue 进行的。 virtio 的一个关键假设是主机可以任意访问客户内存。这种假设在 virtqueue 的设计中很明显,它可能包含指向设备仿真打算直接访问的来宾缓冲区的指针。
尽管前面描述的内存共享超级调用可以用于从客户机共享 virtio 数据缓冲区到主机,但这种共享必须以页面粒度执行,并且如果缓冲区大小小于页面大小,最终可能会暴露比所需更多的数据.相反,来宾配置为从共享内存的固定窗口分配 virtqueue 及其相应的数据缓冲区,并根据需要将数据复制(反弹)到窗口和从窗口复制(反弹)。
与 TrustZone 的交互
尽管来宾无法直接与 TrustZone 交互,但主机仍必须能够向安全世界发出 SMC 调用。这些调用可以指定主机无法访问的物理寻址内存缓冲区。由于安全软件通常不知道缓冲区的可访问性,因此恶意主机可以使用此缓冲区执行混淆代理攻击(类似于 DMA 攻击)。为了防止此类攻击,pKVM 将所有主机 SMC 调用捕获到 EL2,并充当主机和 EL3 上的安全监视器之间的代理。
来自主机的 PSCI 调用被转发到 EL3 固件,只需进行最少的修改。具体来说,CPU 上线或从挂起恢复的入口点被重写,以便在返回到 EL1 的主机之前在 EL2 安装第 2 阶段页表。在引导期间,pKVM 强制执行此保护。
该架构依赖于支持 PSCI 的 SoC,最好使用最新版本的TF-A作为其 EL3 固件。
Arm 固件框架 (FF-A) 标准化了正常世界和安全世界之间的交互,尤其是在存在安全管理程序的情况下。该规范的主要部分定义了一种与安全世界共享内存的机制,同时使用通用消息格式和底层页面定义良好的权限模型。 pKVM 代理 FF-A 消息以确保主机不会尝试与没有足够权限的安全端共享内存。
该架构依赖于执行内存访问模型的安全世界软件,以确保受信任的应用程序和在安全世界中运行的任何其他软件只有在内存由安全世界独占或已使用 FF 明确共享时才能访问内存-一个。在具有 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_VCPU
或KVM_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 |
pVM 固件 | 0x7FE00000 |
设备内存 | 0x10000 - 0x40000000 |
使用mmap
分配物理内存,并将内存捐赠给 VM 以使用KVM_SET_USER_MEMORY_REGION
ioctl 填充其内存区域,称为memslots 。因此,所有来宾 pVM 内存都归属于管理它的 crosvm 实例,如果主机开始耗尽可用内存,可能会导致进程被终止(终止 VM)。当虚拟机停止时,管理程序会自动擦除内存并返回到主机内核。
在常规 KVM 下,VMM 保留对所有来宾内存的访问。使用 pKVM,来宾内存在捐赠给来宾时不会从主机物理地址空间映射。唯一的例外是由来宾显式共享的内存,例如用于 virtio 设备。
来宾地址空间中的 MMIO 区域未映射。来宾对这些区域的访问被捕获并导致 VM FD 上的 I/O 事件。该机制用于实现虚拟设备。在保护模式下,来宾必须确认其地址空间的一个区域用于使用超级调用的 MMIO,以降低意外信息泄漏的风险。
调度
每个虚拟 CPU 都由一个 POSIX 线程表示,并由主机 Linux 调度程序进行调度。该线程调用 vCPU FD 上的KVM_RUN
ioctl,从而导致管理程序切换到来宾 vCPU 上下文。主机调度程序将在来宾上下文中花费的时间计入相应的 vCPU 线程使用的时间。当存在必须由 VMM 处理的事件(例如 I/O、中断结束或 vCPU 停止)时, KVM_RUN
返回。 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 固件
pVM 固件 (pvmfw) 是 pVM 执行的第一个代码,类似于物理设备的引导 ROM。 pvmfw 的主要目标是引导安全启动并导出 pVM 的唯一秘密。 pvmfw 不限于与任何特定操作系统一起使用,例如Microdroid ,只要该操作系统受 crosvm 支持并已正确签名。
pvmfw 二进制文件存储在同名的闪存分区中,并使用OTA进行更新。
设备启动
在启用 pKVM 的设备的引导过程中添加了以下步骤序列:
- Android Bootloader (ABL) 将 pvmfw 从其分区加载到内存中并验证映像。
- ABL 从信任根获取其设备标识符组合引擎 (DICE) 机密(复合设备标识符 (CDI) 和引导证书链 (BCC))。
- ABL 对 pvmfw 的机密 (CDI) 执行测量和 DICE 派生,并将它们附加到 pvmfw 二进制文件中。
- ABL 将
linux,pkvm-guest-firmware-memory
保留内存区域节点添加到 DT,描述 pvmfw 二进制文件的位置和大小以及它在上一步中导出的秘密。 - ABL 将控制权交给 Linux,Linux 初始化 pKVM。
- pKVM 从主机的第 2 阶段页表中取消映射 pvmfw 内存区域,并在整个设备正常运行期间保护它不受主机(和来宾)的影响。
设备启动后,按照Microdroid文档的启动顺序部分中的步骤启动 Microdroid。
pVM 引导
创建 pVM 时,crosvm(或另一个 VMM)必须创建一个足够大的 memslot,以便由管理程序使用 pvmfw 映像填充。 VMM 还受限于可以设置其初始值的寄存器列表(主 vCPU 为 x0-x14,辅助 vCPU 无)。剩余的寄存器是保留的,是 hypervisor-pvmfw ABI 的一部分。
当 pVM 运行时,管理程序首先将主 vCPU 的控制权交给 pvmfw。固件预计 crosvm 已加载 AVB 签名的内核,它可以是引导加载程序或任何其他映像,以及以已知偏移量将未签名的 FDT 加载到内存中。 pvmfw 验证 AVB 签名,如果成功,则从收到的 FDT 生成受信任的设备树,从内存中擦除其秘密,然后分支到有效负载的入口点。如果其中一个验证步骤失败,固件会发出 PSCI SYSTEM_RESET
调用。
在两次启动之间,有关 pVM 实例的信息存储在一个分区(virtio-blk 设备)中,并使用 pvmfw 的密钥进行加密,以确保在重新启动后,该密钥被提供给正确的实例。