LWN:Android上使用KVM!

关注了就能看到更多这么棒的文章哦~

KVM for Android

By Jake Edge
November 11, 2020
KVM Forum(https://lwn.net/Archives/ConferenceByYear/#2020-KVM_Forum)
DeepL assisted translation
https://lwn.net/Articles/836693/

谷歌在做一个项目,希望将 Linux 内核虚拟化机制 KVM 加入到 Android 系统中。Will Deacon 在领导这项工作,他来到 KVM Forum(在线会议),介绍了这个项目,包括它的目标以及所面临的一些挑战。不过与过去的一些 Android 项目不同,"protected KVM "采用了 open 的方式进行开发,代码也会推送到 upstream。

Deacon 是内核中 arm64 架构的维护者之一,同时也是内核其他多个部分的维护者和贡献者,包括并发(concurrency)、锁(locking)、原子操作(atomic operations)和内核内存模型(kernel memory model)工具。他已经参与内核工作很久了,但并没有真正的在 KVM 上开发过,他曾经与之最接近的工作是维护 Arm IOMMU 驱动。2019 年开始, 他来到了谷歌的 Android Systems 团队继续工作,"发现我自己要带头做这个 projected KVM project",也就是在 Android 上运行 KVM。

该项目是 5.9 和 5.10 内核中的 arm64 KVM 代码的主要贡献者。他说,现在 KVM 似乎是一个 "热门话题",不仅仅是在 arm64 上,其他的架构也是如此。项目中所有的进展都是刚完成就会推送到 upstream 上,所以他在会议上展示的东西许多都是 "正在进行中的工作"。他希望避免那种自己独立开发进行了一堆工作之后,发现无法合入 mainline 的情况,希望尽力能提供让社区接受的好的解决方案。

Android background

整体来说,Android 系统最新的进展是通用内核映像(GKI,generic kernel image),其目的是减少 Android 内核的碎片化。历史原因,每个手机都有自己的内核版本,这根本无法扩展。这就导致了碎片化(指多种版本带来的支持困境),而碎片化会导致一些系统无法更新,因为厂商需要花费很多精力和经费来更新多个不同的内核版本,每个不同的设备都可能要针对一个特定的内核版本来提供升级。这就会导致某些设备无法更新 Android 版本,因为它们的内核太老,无法拥有较新的 Android 版本所需要的功能。

他说,这种碎片化其实还导致了一个问题,没有得到人们的足够重视:这对 upstream kernel 也是不利的。mainline kernel 主要希望能提供正确的子系统和抽象层,从而能够支持各种硬件,但除非开发人员能够了解所有不同硬件中的不同问题以及解决方案,否则就无法做到这一点。因为代码都被 "藏在这些不同版本的内核中,很难看到它们的整体情况并提取共同点"。这意味着内核开发者无法提出一个对所有人都适用的抽象层。

GKI 就是为了解决这个问题,通过 "长期持续围绕"一个特定的内核版本来解决这个问题,而这个版本是与特定的 Android 版本相关联的。大家会从 module ABI 中选取一个子集,作为该版本内核的 stable interface 来持续进行维护。然后,厂商就可以基于这套接口来开发 driver module,这样哪怕内核不断在合入长期支持(LTS)和安全更新的 patch,厂商的 driver 也可以一直正常工作。

Deacon 笑着说,他能听到观众强烈建议("screaming")Android Systems 团队不要跟着内核的演进而维护 ABI。他承认这样做是有问题的,但也指出团队已经确定了 ABI 中严格挑选过的 symbol 的子集,并将继续维护这些。这一切只会是针对单一的内核版本和 Android 版本的组合。

Android virtualization today

Android 上的 hypervisor 情况很混乱。"如果你认为内核方面的碎片化已经很糟糕了,那 hypervisor 可是要糟糕得多了"。至少所有的 Android 设备都在运行某个版本的 Linux,但在 hypervisor 方面,"这简直就是碎片化的狂野西部"。有些设备根本没有 hypervisor,这让情况简单了一些,但许多设备还是有 hypervisor 的,它们主要用在几种不同的用途下。

第一个主要用途就是增强安全性,目的是保护内核,但有时这个功能本身就有问题。他介绍说,Jann Horn 的 Project Zero 博客文章指出:"这些缓解措施同时也是新增的受攻击面(attack surface)"。他介绍了如何针对其中一些安全增强功能来进行攻击。重要的是,hypervisor 是在拥有更高权限的情况下运行的,所以它所拥有的 bug 可能意味着这些所谓的保护措施实际上并没有真正保护系统。

目前 Android 中 hyporvisor 的另一个用途是用来做粗粒度的内存划分(memory partitioning),就像是一个 IOMMU,但实际上不是。它在启动时将物理内存挖出来多个不同区域,交给各种设备用于 DMA 和其他用途。他说,他很理解为什么需要这样做,但在启动之后,hyporvisor 还可以做很多事情,所以如果只是拿它来做这种用途的话,是一种浪费。

今天 Android 中使用 hyporvisor 的最后一个原因,是他最不喜欢的:在 Android 本身之外运行代码。Armv8 有多个权限级别,称为异常级别(exception levels),从拥有最高权限的 firmware(EL3),到 hypervisor(EL2)和操作系统(EL1)level,再到权限最低的 user(EL0)level。hypervisor 异常 level 不是 firmware level,所以厂商更新这部分代码时不用担心会让系统变砖;它也不是操作系统 level,所以在那里运行的代码不需要与其他任何东西集成起来用。这意味着 EL2 已经成为了一个 "游乐场",在其他地方看起来不合适的代码都会被塞在那里,这是很糟糕的情况,因为 EL2 的权限比需要的权限要高很多。

在大多数情况下,甚至根本没有哪怕一个虚拟机(VM),所以这些 hypervisor 并没有提供通常 hypervisor 该有的服务功能。他的结论是,这种情况下安全和功能都会受到损害。安全性方面,因为可信计算基础(TCB,trusted computing base)增加了,并且由于这个 level 解决方案的碎片化导致更新设备更加困难。而功能上的缺失,是指大家无法在 Android 内获得硬件虚拟化(hardware virtualization)功能。

随后,他介绍了 Armv8 的 "exception level",展示了不同 level 的软件是如何按照从最多权限到最少权限来搭建起来的,同时也介绍了 Arm 上一直就有的一个与这套软件平行的 "trusted" 世界,其中的应用程序可以在可信的(trusted)操作系统和 hypervisor 上运行。"trusted" 的定义只是 "总线上的一些特定 bit",用来允许或者拒绝对某些物理内存区域的访问。需要注意的是,trusted 这一侧的代码可以访问所有的内存,而不受信任侧(untrusted side)的代码则无法访问那些 trusted-only 的内存区域。

实际上,所有的 trusted level 比起 non-secure level 权限都要高,所以举例来说, trusted 操作系统可以对 non-trusted 的 hypervisor 内存区域进行 map 操作,它可以打开访问权限,这样 trusted application 也就可以访问这块区域的内容了。在 Android 世界中,这样做是有问题的,部分原因是由于 trusted 这一侧通常运行的是:数字版权管理(DRM,digital rights management)的第三方代码、各种不知道具体做了什么的二进制程序、做加密操作的代码等等。这些代码可能并不值得信任,而且它们也受到碎片化问题的影响。人们通常所说的 "Android",其实是在系统中权限最小的级别运行着的。

他认为,"trusted" 这个词主要是一个营销术语,让人们觉得运行在那里的代码是安全可靠的。但 "trust" 还有另一个定义,就是 "expect, hope, or suppose",这个含义在这里也是相关的。安卓系统必须期望(expect)这些运行在 trusted 世界的软件不是恶意的,也是未被入侵的,否则的话,安卓系统也完全无法保证安全性了。

现在,Android 项目希望有一种方法来去掉这些第三方代码的权限。这就就需要一个可移植的(portable)环境,能够以一种与 Android 系统隔离开的方式来托管(host)这些服务。这种机制也要能将这些第三方程序相互之间隔离开来。

Enter KVM

一种解决方法是将 trusted 代码移到与 Android 系统相同 level 的虚拟机中。第三方代码的可信度不会比 Android 本身高(或低)。由于目前 Android 系统中还没有虚拟机(VM),如果有一个 hypervisor 可以管理 VM 的话,我们就可以加一些 VM 进来。我们的想法是,利用 GKI 的工作来引入 KVM 作为这个 hypervisor 序,以便将那些第三方代码从权限过高的 trusted 世界中移出来。

所有的 arm64 安卓设备在硬件上来说都是支持虚拟化的,并且有两级(two-stage) MMU,这样就可以把内存划分成不同的区域,使 guest OS 不能访问属于它自己的内存区域之外的地方。从 Linux 3.11(2013 年)开始,arm64 就支持 KVM。取决于是否支持虚拟化主机扩展(VHE,Virtualization Host Extensions),可以分为两种基本模式。VHE 是在 arm64 架构的 v8.1 中新增的,但如果愿意的话,所有的 arm64 处理器都可以使用早期的 non-VHE(nVHE)模式下运行。

在 nVHE 模式下,host 的 kernel 和 guest kernel 都运行在操作系统级别(EL1),而在 EL2 hypervisor 级别有一个虚拟机监视器(VMM, virtual machine monitor)。由于 host kernel 没有直接切换到 guest 所需的权限,因此需要由 VMM 来进行 "world switch"才能实现,这使得 nVHE 模式的运行速度相对来说比较慢。【更新:本文中把 VMM 和 world-switch 的代码弄混了,Deacon 在 LWN 原文下的评论中进行了澄清】

在 v8.1 版本中,VHE 的支持使得 EL2 程序的限制更加少了,所以 host kernel 可以在 EL2 中运行,所有的 guest 都在 EL1 中作为虚拟机 VM 来运行,速度 "快得惊人"。不过,这种模式其实并不符合 Android 的风险模型(threat model)。因为它将 host kernel 和 VMM(通过 ioctl())移动到 TCB 中,host kernel 可以访问 guest 的所有内存。这实际上破坏了可信模型,只有 Android 系统会处于特权地位,这也是不可取的。

我们设想中的 Android security model 要求,即使 host kernel 被入侵,guest 的数据也要保持私密性,而使用 VHE 的 KVM 并没有达到这样的效果。但 nVHE 模式就没有这个问题,所以,可能需要重新审视一下这个问题。不需要对整个 host kernel 都保留在 trusted 世界,而是只需要让 world-switch 这一块保持 trusted 世界就可以了。可以把它扩展一下,管理起 stage-2 page table 以及 guest 的其他功能。在 host kernel 和 VM 之间使用消息传递方式,并且使用一个特殊的 bootloader 来确保 host 不会篡改 VM 的镜像文件。"当我们在做这件事的时候,我们会尝试采用 formal verification 技术,因为 EL2 的代码比 Linux 要简单得多。"

另一种可能性是在 VM 中运行 Android,很多人可能挺喜欢这个想法,但 Deacon 并不真的认为这个方案有优势,并且它有各种困难。interrupt latency(中断延迟)对于 Android VM 来说是个问题。此外,还需要把 device 传递给 guest 来用,他不认为 Arm IOMMU 在这一点上真的能够完全满足需求。

EL2 中的 nVHE 执行环境(execution environment)是一个 "相当可怕的地方",它有自己的一套受限的虚拟地址空间,没有那种运行通常内核代码所需的寻址能力。在那里运行的任何代码都是不可抢占、不可被中断打断的,所以这些代码一定不能是阻塞操作,也不能 schedule 来切换到其他代码执行。EL2 可以访问所有的内存,只要是 map 过得就可以,但是,正因为如此,计划中不会在那里放很多复杂的代码,否则会破坏我们的主要目标。EL2 有 "非常有限的设备访问能力",因为 host kernel 通常处理了所有的设备访问。通常情况下,EL2 没有 console(控制台),尽管他们可能会有一套用作 debug 功能的 debug console。

EL2 的代码需要自成一体,并且确保在 host 被入侵时也能保持安全,而 5.9 之前的 kernel 则不满足这个需求,当时 KVM 可以通过传递一个函数指针来让 EL2 hypercall 来执行任意代码。最近,projected KVM 项目的改动之一就是改用了一套固定的 hypercall 操作来提供哪些有必要的服务。EL2 的 payload 被放到一个单独的 ELF p 之内,使用 symbol prefixing 机制来确保 host kernel 的 symbol 不会被错误调用。在系统启动时,host kernel 会先对静态密钥设置好合适的值,然后再移动到 EL1 来降低自己的权限。EL2 里的 object 就不会继续从 EL1 map 过来,所以这些改动是单程的(无法撤销)。

Open problems

然而,仍然遗留着一些问题,大部分都是关于如何管理虚拟内存的。如今,host kernel 控制着 hypervisor 的虚拟内存,考虑到 Android 的使用场景,这肯定是一个问题。stage-1 mapping 是由 host kernel 创建的,这意味着它可以改变页表,从 hypervisor 控制下偷出来。同时,它也可以向 hypervisor 的任何内存进行写入。

除此之外,guest 的 stage-2 页表也是由 host kernel 管理的。当 EL2 进行 world-switch 时,它只是盲目地切换到这些 page table 上去使用,实际上是假设 host kernel 的配置是正确的。这里显然也需要改变。protected KVM 项目在 Linux 5.11 中有一些 patch 来改变 page table 的处理逻辑,其中一些 patch(如 page table and fault handling, per-CPU data handling)已经合入到 5.10 中了。

将页表处理代码移到 EL2 中,带来了一些有趣的特性。当一个新的 guest 被创建出来时,它的内存就会从 host 上 unmap 掉,这个动作在 Linux 中无法处理。它不像是内存热插拔(memory hotplug),整个内存区域可以全部移除。实际上它只是让分配给 guest 的页面消失。一组 KVM protected memory extension(https://lwn.net/Articles/835342/)的 patch 可以解决这个问题,不过它们尚未合入。这组 patch 将可以处理 guest 内存消失的情况,也可以在 guest 被移除后又重新加回来的情况。

IOMMU 对于避免 DMA 攻击来说是有必要的,但目前的 SoC 都还没有真正准备好。理想情况下,IOMMU 可以简单地重新使用 CPU 中已经在用的页表,因此在 EL2 中只需要很有限的 IOMMU 管理代码。如果在 EL2 中添加多个不同的 IOMMU driver,使得代码变得臃肿,那不是一个好方案。

还有,需要一个 template bootloader,用来启动 guest。这个 bootloader 它将是 "非常非常小" 的,目前的计划是用 bare-metal Rust 来编写。它的功能是检查 VM image 的签名,确保它没有被篡改过。如果验证通过了,bootloader 就可以跳转到这个 VM image 来执行。这个 image 将包含一个 "配套的第二级 bootloader "作为它的一部分,所以 template bootloader 可以保持非常简单。这些都不是特别针对 arm64 的,所以也应该可以用在其他架构中。

Virtual platform

protected KVM 这个项目正在移植适配 Chrome OS VMM(crosvm)来用。他说,crosvm 现在已经包含在 Android open-source project(AOSP)中。KVM 论坛上已经有很多关于它的讨论。crosvm 是用 Rust 编写的,主要关注安全和沙盒,这使它成为一个很好的选项。它还已经实现了许多 virtio 设备,并且是跨架构的,这一点很重要,也许令人惊讶,能做到跨架构,部分原因是得益于 x86 架构的 Cuttlefish virtual Android device。

Protected KVM 为 guest 提供了一个非常基础的 arm64 虚拟平台(virtual platform),包含了许多大家需要的东西。这里有一个主要的区别,它提供的是 Reduced Virtual Interrupt Controller(RVIC,这是一个 paravirtual IC,https://developer.arm.com/architectures/system-architectures/software-standards/rvic ),而不是标准的 GIC(Generic Interrupt Controller),因为后者太复杂了,开发者在 EL2 代码中并不需要那些复杂功能。

在 I/O 方面,解决方案明显是 virtio,他说,但这并不能完全解决问题,因为它假设 host 可以访问所有的 guest memory。即使你采用一些手段绕过这一问题,也意味着 host 可以截取 guest 的 I/O 数据,这意味着 "你必须使用一些聪明的加密技术"。virtio 也没有共享内存设备(shared-memory device),所以需要 bounce buffer(DMA 时需要进行一次数据搬移放在临时 buffer)。这种实现方式可以能工作起来,但是有各种问题,包括性能比较差,所以还在寻求一些其他的解决方案。

他的最后一张幻灯片是一长串还需要做的事情。有一个比较大要做的事情,他之前低估了这里的工作量:要让它与 Android 系统的其他部分能配合起来一起正常工作。有很多事情需要与 Android 系统的 user-space 的软件集成起来,而他作为内核(以及现在 KVM)的开发者的经验并不足以处理好这部分。他鼓励有兴趣的人与 Android Systems 团队联系,或者在 KVM/Arm 邮件列表上发帖。幻灯片的 PDF 版本可以在这里下载(https://mirrors.edge.kernel.org/pub/linux/kernel/people/will/slides/kvmforum-2020-edited.pdf) , 视频可从活动网站上下载(https://lwn.net/Articles/836505/ ),最终也将出现在 YouTube 上。

附录:Will Deacon 在 LWN comment 中的澄清:

KVM for Android

Posted Nov 16, 2020 11:09 UTC (Mon) by wildea01 (subscriber, #71011) [Link]

Thanks for the excellent write-up, Jake! One thing I feel that I should clarify is my use of the term "VMM", which I used to refer to the userspace component of KVM on the host (e.g. QEMU, crosvm or kvmtool). This always lives in userspace, regardless of VHE/nVHE. The primary difference between VHE and nVHE is that the host kernel runs at EL2 or EL1 respectively. With nVHE, EL2 contains some small "world-switch" code installed by the kernel and it is this layer that we are working to extend for Protected KVM. Will

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值