TinyKVM: Fast sandbox that runs on top of Varnish
TinyKVM:构建在 Varnish 之上的极速沙箱
本文介绍了一种基于 KVM 的单进程沙箱
大家好。在从事我的博士研究、libriscv 和一个未命名的游戏(我知道这太多了)之余,我还在开发一个用于单个程序的 KVM 沙箱,也就是一个用户空间模拟器。我希望使用硬件虚拟化技术打造世界上最快的沙箱,或者至少我脑海中有一个明确的目标。
早在 2021 年,我就写过一篇关于在 Varnish 中对每个请求进行沙箱化的博文,标题为 Virtual Machines for Multi-tenancy in Varnish。在文中,我提到将研究使用 KVM 进行沙箱化,而不是使用 RISC-V 模拟器。因此……我继续编写了 TinyKVM。
那么,TinyKVM 是什么,它又带来了什么呢?
TinyKVM 执行常规 Linux 程序,效果与原生执行相同。
TinyKVM 可用于沙箱化常规 Linux 程序,或者将具有专用 API 嵌入到服务器中的程序。
TinyKVM 的设计
为了解释 TinyKVM 究竟是什么,我将列出当前已实现且按预期工作的一些显式功能:
TinyKVM 运行静态 Linux ELF 程序。 还可以使用你创建的 API 对其进行扩展,以便访问外部 HTTP 服务器或缓存。 我最终还将添加动态可执行文件支持。 ⏳ 它目前运行在 AMD64 (x86_64) 上,稍后我会将其移植到 AArch64(64 位 ARM)。
TinyKVM 尽可能为 guest 页面创建 hugepages。 它还可以额外使用 host 上的 hugepages。 结果通常(如果不是总是)比原生程序具有更高的性能。 为了更加强调这一点:https://easyperf.net/blog/2022/09/01/Utilizing-Huge-Pages-For-Code 发现仅仅为执行段分配 2MB 页面就可以为 LLVM 代码库带来 5% 的编译提升。
我快速分配了一些 hugepages 并运行了带有 STREAM 的 TinyKVM,是的,它快了很多。
TinyKVM 在调用 guest 中的函数时,只有 2us 的开销。 与我的 RISC-V 模拟器的 3ns 相比,这似乎很多,但是我们正在进入另一个进程,并且可以使用我们所有的 CPU 功能。 TinyKVM 可以在给定时间后停止执行,而无需在调用期间设置任何线程或信号。 这是常规 Linux 程序无法实现的。 如果没有执行超时,则调用开销为 1.2us,因为我们不再需要计时器。 TinyKVM 可以使用 GDB 进行远程调试。 程序可以在调试后恢复,而且我实际上已经使用它来实时调试 Varnish 中的一个请求,并看到它随后正常完成。 如果可以这么说的话,这很酷。 TinyKVM 可以将自身 fork 成使用写时复制的副本,以允许像 LLM 这样的大型工作负载共享大部分内存。 例如,6GB 的权重每个实例仅需要 260MB 的工作内存,从而使其具有高度的可扩展性。 TinyKVM fork 可以使用常规 Linux 程序无法使用的机制,以创纪录的时间将自身重置为之前的状态。 如果安全性很重要,则可以通过在每次请求后重置 VM 来使其成为临时的。 这样就消除了查看以前请求的痕迹的可能性,并且由于任何形式的持久性都不再可能,因此许多类型的攻击变得不可能。 TinyKVM 实例也可以重置为它没有 fork 自的另一个 VM,但会产生性能损失,因为必须更改页面表。 TinyKVM 仅使用 KVM API 的一小部分,该 API 本身大约有 42k LOC。 TinyKVM 在运行时覆盖的总代码行数未知,但由于不使用任何设备或任何驱动程序,因此总共可能少于 40k LOC。 这可以与例如 350k wasmtime 和 165k FireCracker 相比,它们都足够大,理想情况下也可以在进程 jail 之上运行。 FireCracker “附带”一个 jailer。 对于 TinyKVM,具体来说,KVM 代码库大约为 7k + 37k(用于基础 + x86),而 TinyKVM 大约为 9k LOC。 TinyKVM 在初始化期间创建静态页面表,这种方式甚至适用于像 Go 这样奇怪的运行时。 之后可能更改的唯一页面是写时复制页面。 我们可以说页面表在很大程度上是静态的,这是一个安全优势。 它们在启动后可能不会被修改,并且在 KVM 退出期间会进行运行时健全性检查。 KVM guest 具有类似于进程的单独 PCID/ASID,因此,对于当前和未来的推测错误,它很可能免疫。 TinyKVM guest 具有一个无法修改的微型内核。 SMEP 和 SMAP 以及 CR0.WP 和页面保护都已启用。 整个内核总共使用了 7x 4k 页面。 我们还进入用户模式,并尽量避免内核模式,除非不可能。 你知道吗,你可以在 x86 上的用户模式下处理 CPU 异常吗?
系统调用
通过在模拟器中定义和挂钩系统调用来创建与 host 的 API。在 guest 中,它们通过 SYSCALL/SYSRET 实现,你也可以直接从用户模式使用 OUT 指令,后者是延迟最低的。 这会导致 VM 退出,并检查寄存器以发现正在执行系统调用。 总而言之,往返时间略小于 ~1 微秒,这与 libriscv 的 2ns 大不相同。 但是,你可以设计一个更小的 API,具有更大的输入和输出,而不是许多更小的调用(这在成本较低时才有意义)。
基准测试
重置 VM + 调用 VM guest 的开销。
对于调用开销,我们将重置 VM 的成本作为尾部延迟来衡量,因为可以在(例如)HTTP 响应已经在进行时支付该费用。 如果你不需要重置,只需忽略红色即可。 重置时间会随着工作内存的使用而扩展。
进行内存基准测试是为了查看是否存在明显错误,但没有。 我最喜欢的基准测试之一是在 HTTP 基准测试中每秒编码 1500 个 AVIF 图像(AVIF 编码器即服务):
使用正确的方法,可以非常快速地将 JPEG 转码为 AVIF。
因此我们可以看到每秒 16x98.88 = 1582 个图像转码。 当然,转码器设置和图像大小很重要,但我更关注缩放方面。 在一定程度上,将并发加倍会提高我们的整体性能吗? 是的,确实如此。
有趣的事实:你知道吗,你可以在不通过(有损的?)RGB 转换的情况下将 JPEG 转码为 AVIF 吗? YUV 平面在两种格式中的工作方式相同!
快速沙箱化
那么,到底是什么让它成为世界上最快的沙箱呢? 嗯,首先它不使用任何 I/O,不使用任何驱动程序,也没有虚拟设备,因此它摆脱了其他 KVM 解决方案的常见问题:虚拟化 I/O 会在一定程度上降低性能。 TinyKVM 还非常注重 hugepages,即使它们在 host 中不可用,它仍然可以从较少的页面遍历中受益。 当由真正的 hugepages 支持时,我们会立即看到很大的收益。 我已经针对在 TinyKVM 中运行的基于 CPU 的大型 LLM 工作负载进行了测量,使用相同的种子和设置,并在校正设置后,我发现 TinyKVM 以 99.7% 的原生速度运行。 我不知道这是否意味着虚拟化的最小开销为 0.3%,或者这只是长时间运行时的统计差异。 但我很高兴这种差异很小,无关紧要。 它还可以非常快速地完成对 guest 的函数调用(VM 调用),这意味着我们实际上并没有在任何地方增加任何开销。 我们实际上只是在 CPU 上运行,这正是我们想要的。 因此,只要我们有用于访问数据的零拷贝解决方案,我们就可以安全地处理数据并将其传回。 简而言之,我们只是以原生 CPU 的速度处理数据,而这正是我们想要的。 为了避免与经典的全系统虚拟化相关的开销,并享受我们的新通道,而无需任何额外的负担。
缺点
在 KVM API 中增加 vCPU 数量后,无法减少 vCPU 数量,因此我认为多处理可以通过同时运行更多 VM 并仅使用/滥用自动内存共享来更好地实现。 TinyKVM 确实具有实验性的多处理支持,但之后你无法结束,因此它不是像 Varnish 这样的长时间运行进程的理想选择。 有多种方法可以解决此问题,我不会在此处详细介绍,但我只想提及你可以通过将 VM 重置为另一个不同的 VM 来重用它。 成本很高,但为诸如 VM 共享池之类的酷炫解决方案打开了大门。
未来的工作
- 拥有 Intel TDX/AMD SEV 支持会很好。
- AArch64 端口。
- KVM 具有一种锁定内存的方式,即使 guest 中的内核模式也无法更改它,称为 KVM_MEM_READONLY,我希望它可以成为更进一步锁定 guest 的一部分。 一个潜在的缺点是增加了内存映射的使用。
- TinyKVM 中面向用户的 API 需要进行改进,以变得友好。
- 将我为 Varnish 集成编写的大部分系统调用模拟移入 TinyKVM 本身,这将为动态链接器加载铺平道路。 ld.so 使用带有 fd 的 mmap() 来映射它正在加载的文件。 因此,如果你将 ld.so 加载到你自己的模拟器中,然后将你想要的程序添加为第一个 argv 参数,并在之后添加其参数,那么 ld.so 将为你加载你的程序,前提是你具有该 mmap() 实现。 这就是 libriscv 所做的!
结论
TinyKVM 或许令人惊讶地将自己定位在最小的严肃沙箱解决方案之列,并且也可能是最快的。 它认真对待安全性,并尽量避免复杂的 guest 功能和通常的内核模式。 TinyKVM 具有最小的攻击面,并且除了更好的面向用户的 API 和移植到其他架构之外,没有进一步增加复杂性的雄心。
如果你有兴趣,请查看 TinyKVM 的代码仓库。 文档和面向用户的 API 仍然需要大量工作,但请随意构建项目并使用它运行一些简单的 Linux 程序。 只要它们是静态的并且不需要文件或网络访问,它们就可能会立即运行。
不久之后,我们将 在 Varnish 中开源一个 VMOD,它允许你使用 TinyKVM 安全地转换数据。 它适用于开源 Varnish 和 Varnish Enterprise! 如果你想了解更多信息,请立即联系我们!
博客最初发布在 这里。