传统观点认为你只能二选一:要么选硬件虚拟化的强隔离——代价是重和慢;要么选容器的轻和快——代价是共享宿主内核的弱隔离。Firecracker 的回答是”我全都要”,而它拿到手的方式不是发明更多东西,而是一场系统性的减法:删掉 BIOS,删掉 PCI(后来又有分寸地加回来),删掉 USB、显卡和 QEMU 的上百万行遗产,让”这里没有代码”本身成为最强的安全特性和最快的启动路径。
每一次 AWS Lambda 函数调用的背后,都有一台真正的虚拟机:独立内核、独立页表、硬件级隔离。它启动最快约 125ms,每台开销不到 5MiB 内存,单宿主机每秒能创建最多 150 台(官方口径)。这套东西叫 microVM,实现它的 VMM(Virtual Machine Monitor,虚拟机监控器)叫 Firecracker,2018 年开源,论文 Firecracker: Lightweight Virtualization for Serverless Applications 发表在 NSDI 2020。
本文基于 firecracker-microvm/firecracker commit f2f43b6(版本 1.17.0-dev,约 12 万行 Rust,Apache-2.0)做源码解读,回答几个实现层面的问题:
- 一台”没有 BIOS 的虚拟机”是怎么被引导起来的?
- vCPU 线程里那个
KVM_RUN循环到底长什么样? - 设备模型砍到只剩什么?virtio 队列怎样直接读写 guest 内存?
- “假设 vCPU 线程一启动就在跑恶意代码”,这个威胁模型如何落进 jailer 和 seccomp?
- 快照恢复为什么能到百毫秒级,和 AI agent 沙箱(E2B 等)有什么关系?
先给一句话总结:
Firecracker = KVM 之上的一层极薄用户态设备模型。
它把「虚拟机为什么慢」拆到根因——慢在模拟了一台完整 PC——
然后把这台 PC 里 serverless 用不到的一切全部删掉,
再用 Rust + seccomp + jailer 把剩下的部分层层锁死。
1. 全局架构:一个进程就是一台虚拟机
Firecracker 最反直觉的设计是它的进程模型:一个 Firecracker 进程只管一台 microVM,进程死了虚拟机就死了。没有中心守护进程,没有共享的设备模拟服务。宿主机上跑一千台 microVM,就是一千个彼此独立的普通 Linux 进程——于是 Linux 内核已有的一切进程级安全设施(cgroups、namespaces、seccomp、调度器)都自动适用于”虚拟机”这个粒度。
从 Cargo.toml 看,仓库是一个 workspace,职责拆得很清楚:
| Crate | 路径 | 职责 |
|---|---|---|
| firecracker | src/firecracker | 主进程:参数解析、API server、事件循环装配 |
| vmm | src/vmm | 核心:KVM 封装、vCPU、设备模型、快照、内存 |
| jailer | src/jailer | 安全外壳:chroot/namespace/cgroup/降权后 exec 进 firecracker |
| seccompiler | src/seccompiler | 把 JSON 系统调用白名单编译成 BPF 过滤器 |
| snapshot-editor | src/snapshot-editor | 快照文件离线编辑工具 |
| rebase-snap | src/rebase-snap | 增量快照合并工具 |
运行时的线程模型在官方 docs/design.md 里写得很直白:每个进程只有三类线程——API 线程(处理 REST 控制面,永远不在虚拟机快速路径上)、VMM 线程(事件循环,跑所有设备模拟)、每 guest 核一个 vCPU 线程(跑 KVM_RUN)。
flowchart TB
subgraph host[宿主 Linux]
subgraph jail[jailer: chroot + namespaces + cgroups]
subgraph fc[Firecracker 进程(每 microVM 一个)]
API[API 线程<br/>UNIX socket REST] -->|mpsc channel + eventfd| VMM[VMM 事件循环线程<br/>virtio 设备模拟 / 限速器]
VMM --- V1[vCPU 线程 0<br/>KVM_RUN 循环]
VMM --- V2[vCPU 线程 N<br/>KVM_RUN 循环]
end
end
KVM[/dev/kvm/] --- V1
KVM --- V2
TAP[TAP 设备] --- VMM
end
控制面和数据面的通信也刻意保持原始:API 线程解析 HTTP 请求后,通过 Rust 标准库的 mpsc channel 把请求递给 VMM 线程,再用一个 eventfd 把事件循环唤醒(src/firecracker/src/api_server_adapter.rs)。没有 RPC 框架,没有序列化协议,就是进程内两根管道。HTTP 请求体上限被硬编码为 50KiB(HTTP_MAX_PAYLOAD_SIZE,src/vmm/src/lib.rs)——控制面连”收大请求”的能力都被删掉了。
2. 启动路径:没有 BIOS 的虚拟机怎么开机
一台物理 x86 机器开机要走 BIOS/UEFI → 实模式 → 保护模式 → bootloader → 内核,这条链路是 microVM 启动时间的头号敌人。Firecracker 的做法:全部跳过。VMM 在用户态直接把内核镜像写进 guest 内存、手工填好所有 CPU 寄存器和引导数据结构,然后让 vCPU 从内核入口点直接开跑。
主装配函数是 build_microvm_for_boot()(src/vmm/src/builder.rs:143),把它的骨架抄下来,就是一张 microVM 的”出生流程图”:
pub fn build_microvm_for_boot(...) -> Result<Arc<Mutex<Vmm>>, StartMicrovmError> {
let guest_memory = vm_resources.allocate_guest_memory()?; // 1. 分配 guest 内存
let kvm = Kvm::new(...)?; // 2. 打开 /dev/kvm
let mut vm = KvmVm::new(kvm)?; // 3. KVM_CREATE_VM
let mut vcpus = vm.create_vcpus(vcpu_count)?; // 4. KVM_CREATE_VCPU
vm.register_dram_memory_regions(guest_memory)?; // 5. 内存区域注册进 KVM
let mut device_manager = DeviceManager::new(...)?; // 6. 设备总线
let entry_point = load_kernel(&kernel_file, guest_memory)?; // 7. 内核写入内存
// ... attach balloon/block/net/vsock/entropy 等 virtio 设备
configure_system_for_boot(...)?; // 8. 寄存器 + 引导数据结构
kvm_vm.start_vcpus(vcpus, seccomp_filters.get("vcpu")...)?; // 9. vCPU 线程启动(带 seccomp)
// vCPU 初始为 Paused,resume 后 guest 才开跑
}
第 7 步 load_kernel()(src/vmm/src/arch/x86_64/mod.rs:454)揭示了”无 bootloader 引导”的三条路径:
- ELF vmlinux:直接按 ELF 段加载进 guest 内存,从 ELF 入口进;
- PVH:如果 ELF 带 PVH note(Xen 传统的直接引导协议),优先用 PVH 入口——比传统协议更干净;
- bzImage:从 boot protocol ≥ 2.12 的 64 位入口(加载地址 + 0x200)直接进入,跳过全部实模式 setup 代码。
第 8 步做的事情,在物理机上属于 BIOS 和 bootloader 的职责,这里全由 VMM 用普通内存写操作代劳。看 src/vmm/src/arch/x86_64/layout.rs 里的常量表,guest 物理内存的每一处布局都是编译期定死的:
0x7000 zero page(Linux 引导参数页,boot_params 结构)
0x20000 内核命令行(上限 2048 字节)
0x9fc00 系统保留区起点
0xe0000 ACPI RSDP 表
0x100000 内核本体(1MiB,HIMEM_START)
3GiB..4GiB 32 位 MMIO 设备窗口
连 e820 内存映射表(物理机上由 BIOS 探测生成、告诉内核”哪些内存能用”的表)都是 VMM 手工逐条填的(add_e820_entry,src/vmm/src/arch/x86_64/mod.rs:428)。
这里值得停一下,把”为什么快”想透:Firecracker 的启动快不是优化出来的,而是删出来的。BIOS 初始化、探测硬件、bootloader 解压跳转——这些步骤不是被加速了,而是不存在了。启动时间里剩下的大头是 guest 内核自身的初始化,所以官方还配套维护了裁剪过的 guest 内核配置(resources/guest_configs/),把内核里用不到的驱动也删掉。减法贯穿始终。
3. vCPU 线程:一个 200 行的状态机
vCPU 是虚拟化的心脏,但 Firecracker 的 vCPU 线程主循环出奇地小。每个 vCPU 线程启动后先给自己上 seccomp 过滤器,然后进入一个三态状态机(src/vmm/src/vstate/vcpu.rs:217):
pub fn run(&mut self, seccomp_filter: BpfProgramRef) {
crate::seccomp::apply_filter(seccomp_filter) // 先锁死自己能用的系统调用
.unwrap_or_else(|err| panic!(...));
let mut state = VcpuRunState::Paused;
loop {
state = match state {
VcpuRunState::Running => self.running(),
VcpuRunState::Paused => self.paused(),
VcpuRunState::Finished => break,
};
}
}
Running 态内层就是 KVM 虚拟化的经典循环:run_emulation() 调 KVM_RUN ioctl 让物理 CPU 直接执行 guest 指令,直到 guest 碰了需要 VMM 介入的东西才退出来(VM Exit)。退出原因的处理是一个大 match(src/vmm/src/vstate/vcpu.rs:407):
match self.kvm_vcpu.fd.run() {
Ok(VcpuExit::MmioRead(addr, data)) => // guest 读了设备寄存器 → 查总线转发给设备
Ok(VcpuExit::MmioWrite(addr, data)) => // guest 写了设备寄存器 → 同上
Ok(VcpuExit::Hlt) | Ok(VcpuExit::Shutdown) => // guest 停机
...
}
Paused 态则阻塞在一个 channel 上等控制命令(Resume/SaveState/DumpCpuConfig)——快照保存只允许在 Paused 态做,状态机从类型层面保证了”不会边跑边存”。
值得注意的是这个设计里的分工:vCPU 线程处理同步 I/O(MMIO 读写必须当场答复,guest 在等),VMM 事件循环线程处理异步 I/O(网络包到了、限速令牌回来了)。两类线程各自有各自的 seccomp 白名单——vCPU 线程能用 49 个系统调用,事件循环线程 76 个,API 线程 36 个(数字来自 resources/seccomp/x86_64-unknown-linux-musl.json 实测统计,默认动作 trap)。
4. 设备模型:五种 virtio 设备加一根串口
QEMU 能模拟软驱、声卡、USB 控制器、VGA 显卡和几十种网卡——每一个都是攻击面(NSDI 论文点名的案例是 2015 年的软驱控制器逃逸漏洞 VENOM)。Firecracker 的设备清单短到可以背下来:
- virtio-block / virtio-net / virtio-vsock:存储、网络、宿主通信三件套
- virtio-balloon / virtio-rng / virtio-pmem / virtio-mem:内存回收、熵源、持久内存、内存热插拔
- legacy:一根串口 + 一个只保留 CPU reset 功能的 i8042 键盘控制器残桩
- MMDS:instance metadata 服务(见下文 dumbo)
没有 USB,没有显卡,没有声卡。这不是”还没来得及支持”,而是论文里明说的立场:这些设备在 serverless 工作负载里没有存在理由,不存在的代码没有漏洞。
4.1 virtio-mmio:设备”总线”退化成一段内存地址
virtio 设备需要一个 transport 让 guest 发现并配置它。标准做法是挂在模拟的 PCI 总线上,但 PCI 枚举本身要花启动时间、要模拟配置空间。Firecracker 默认用 virtio-mmio(src/vmm/src/devices/virtio/transport/mmio.rs):每个设备就是 4KiB 一段的 MMIO 寄存器窗口(MMIO_LEN = 0x1000),guest 读偏移 0 会得到魔数 0x74726976(ASCII 的 “virt”)。设备地址通过内核命令行直接告诉 guest:
virtio_mmio.device=4K@0xd0000000:5
guest 内核照单全收,无需任何探测。“总线枚举”这个概念被删掉了,代价是设备不能动态发现——对”配置一次、跑完即弃”的 serverless 场景这不是代价。
4.2 virtio 队列:零拷贝读 guest 内存
virtio 的数据面是共享内存环形队列(src/vmm/src/devices/virtio/queue.rs)。Firecracker 的 Queue 结构直接持有指向 guest 内存中 avail ring / used ring 的裸指针,用 volatile 读写访问:
pub struct Queue {
pub avail_ring_ptr: *mut u16, // 指向 guest 内存中的 available ring
pub used_ring_ptr: *mut u8, // 指向 guest 内存中的 used ring
...
}
pub fn avail_ring_idx_get(&self) -> u16 {
unsafe { self.avail_ring_ptr.add(1).read_volatile() }
}
guest 把请求描述符写进环,敲一下 MMIO 的 doorbell 寄存器触发 VM Exit;VMM 直接顺着描述符链读 guest 内存,处理完把结果挂回 used ring,再通过 KVM irqfd 给 guest 注入中断。数据本体从头到尾不拷贝进 VMM 私有缓冲。块设备后端还有 io_uring 异步引擎(src/vmm/src/io_uring/,要求宿主内核 ≥ 5.10.51),网络后端就是一个 TAP 文件描述符。
4.3 内建限速器与 dumbo:多租户的自我修养
每个块设备和网卡都内嵌一个令牌桶限速器(src/vmm/src/rate_limiter/),带宽和 ops/s 双桶独立记账,桶空了设备暂停处理并挂一个 timerfd 到事件循环等令牌回填。这个设计的动机写在论文的多租户约束里:一千台 microVM 挤在一台宿主机上,任何一台都不能用 I/O 洪水拖垮邻居——限速不是外挂策略,是设备本身的属性。
更有意思的是 src/vmm/src/dumbo/——一个名副其实”极简”(dumb)的用户态 TCP/IPv4 协议栈,只服务一个用途:MMDS(instance metadata 服务)。guest 访问 metadata IP 的流量在 VMM 里被就地拦截、解析、应答,根本不出宿主网卡。为一个 HTTP GET 写整个 TCP 栈看似over-engineering,实则是威胁模型推出来的必然:metadata 里有凭证,这条路径绝不能和真实网络共享任何代码。
4.4 灰度看”删 PCI”:2025 年后它又回来了
有意思的转折:当年为启动速度删掉的 PCI,在近期版本里以 --enable-pci 可选开关的形式回来了(src/vmm/src/pci/、docs/kernel-policy.md)。启用后所有 virtio 设备改走 PCI transport,官方给出的理由是更高吞吐、更低延迟,而且它是设备热插拔(Developer Preview,docs/device-hotplug.md)的前提。
这是一个很好的”设计权衡随约束漂移”的样本:2018 年的 Firecracker 只服务 Lambda 这一种负载,PCI 纯属成本;2026 年的 microVM 生态要跑 AI agent 沙箱、要热插设备、要为异构硬件留口子,PCI 的收益侧变重了。注意它的处理方式——默认仍然关闭,MMIO 路径原封不动。减法哲学没有被推翻,只是从”删掉”进化成”默认不存在,按需显式打开”。
5. 安全模型:假设 vCPU 一启动就在跑恶意代码
docs/design.md 的威胁模型开门见山:
From a security perspective, all vCPU threads are considered to be running malicious code as soon as they have been started.
从这个假设出发,Firecracker 叠了四层互相独立的防线:
flowchart LR
G[恶意 guest 代码] -->|第一层| KVM[KVM 硬件虚拟化边界<br/>EPT 页表 + VT-x]
KVM -->|第二层| RUST[Rust 内存安全<br/>设备模拟代码无缓冲区溢出]
RUST -->|第三层| SEC[seccomp 白名单<br/>vCPU 线程仅 49 个系统调用]
SEC -->|第四层| JAIL[jailer 沙箱<br/>chroot + 5 种 namespace + cgroup + 降权]
JAIL --> HOST[宿主内核]
第一层,KVM。guest 代码跑在硬件虚拟化里,对内存的访问被 EPT 页表钉死在分给它的区域。要突破这层需要 CPU 或 KVM 本身的漏洞。
第二层,Rust。突破 KVM 层的经典路径是打设备模拟代码的内存破坏漏洞(QEMU 时代的主旋律)。Firecracker 的设备模拟全部是 safe-by-default 的 Rust,unsafe 块被 clippy 规则强制要求逐个写注释说明安全性论证(Cargo.toml 里的 undocumented_unsafe_blocks = "warn")。这个技术选型有血统渊源:Firecracker 起家于 Google Chrome OS 的 crosvm(仓库 CREDITS.md 明确致谢),后来大部分重写,两者又共同孵化了 rust-vmm 社区组件库。
第三层,seccomp。就算攻击者在 Firecracker 进程里拿到了任意代码执行,他能对宿主内核发起的系统调用也被 BPF 过滤器锁死。白名单按线程角色最小化,越靠近 guest 的线程权限越小;名单不是手写在代码里,而是 JSON 声明(resources/seccomp/)由 seccompiler 编译成 BPF——审计攻击面时看 JSON 就够了。
第四层,jailer。生产环境里 Firecracker 不直接启动,由 jailer 拉起。Env::run()(src/jailer/src/env.rs:646)的执行序列就是一张沙箱搭建清单:把 firecracker 二进制拷进 chroot → 加入指定网络 namespace → 设 rlimits → 配 cgroups → chroot 进空目录 → 用 mknod 在监狱里手工创建仅有的三个设备节点(/dev/kvm、/dev/net/tun、/dev/urandom)→ 降权到非特权 uid/gid → exec 进 firecracker。从此这个进程看到的文件系统里只有被显式放进来的东西。
四层防线的组合意味着:攻击者要从 guest 打到宿主,需要同时串起 KVM 逃逸 + Rust 代码漏洞 + seccomp 白名单内的内核漏洞 + namespace 逃逸。每一层都在为”上一层被打穿”兜底。
一个持怀疑态度的读者会问:gVisor 也号称多层防御,为什么 Lambda 选了 microVM 而不是用户态内核?论文给的答案是兼容性和性能的权衡——gVisor 用 Sentry 拦截并重新实现系统调用,兼容性永远追不上真内核,且系统调用密集型负载性能损失明显;microVM 里跑的是原版 Linux 内核,guest 兼容性是 100%。代价则是每台 VM 多几 MiB 的内核内存开销。没有免费午餐,只有约束匹配。
6. 快照与 UFFD:为什么它成了 AI agent 沙箱的底座
Firecracker 的快照机制(src/vmm/src/persist.rs:169)把一台暂停的 microVM 序列化成两个文件:状态文件(vCPU 寄存器、设备状态、内存布局元数据,带 CRC 和版本号)和内存文件(guest 内存本体)。恢复时有两条路径,差别很值得品:
- mmap 路径:内存文件直接 mmap 进来。快,但页面按访问惰性从磁盘加载,无法定制加载策略。
- UFFD 路径(
guest_memory_from_uffd,src/vmm/src/persist.rs:543):注册 userfaultfd,guest 每碰一个缺页,缺页事件被发给一个外部进程,由它决定怎么喂这页内存。
UFFD 路径把”内存怎么恢复”变成了可编程的:外部 handler 可以从网络流式拉取、可以多台 VM 共享同一份基底内存做写时复制、可以预取热页。这正是 serverless 平台”预热快照池”玩法的基础设施。
这也解释了 Firecracker 在 AI 时代的第二春。agent 要执行不可信的模型生成代码,容器隔离不够硬,传统 VM 又起不动那么快——microVM 恰好卡在这个空档上。E2B 的沙箱就是 Firecracker microVM,走”预热快照 + 恢复”路径把冷启动压到 150-200ms 量级,服务 Manus、Perplexity 等生产负载;Vercel Sandbox 同样构建在 Firecracker 上。2018 年为 Lambda 的多租户密度设计的那套减法,2026 年原封不动地成了”给每个 agent 一台一次性电脑”的答案。
7. 带走的模型:删除是一种能力
把全文收进一个可复用的心智模型——最小机制原则(minimal mechanism):
安全性 ≈ 攻击面的倒数;攻击面 ≈ 代码量 × 权限。
启动时间 ≈ 必须执行的初始化步骤之和。
两个量都单调依赖「系统里有多少东西」。
所以对特定负载做系统设计时,第一等的杠杆不是加机制,而是:
1. 枚举目标负载真正需要的能力(Firecracker:serverless = 网/盘/串口,没了)
2. 其余一切物理性删除(不是禁用,是不存在)
3. 剩下的部分按「被攻破」假设逐层加锁(Rust → seccomp → jail)
4. 当约束漂移时,以「默认关闭的显式开关」加回能力(PCI 的回归方式)
这个模型迁移性很强:裁剪 guest 内核配置、给 agent 设计工具集、收敛微服务的 API 面,都是同一道题。Firecracker 用 12 万行 Rust 给出的示范是:知道不做什么,和知道做什么同样是硬实力。
对照系统设计里那条”瓶颈塑造架构”的元原理:Firecracker 的稀缺资源是”多租户宿主机上单 VM 的边际成本”(内存开销 × 启动延迟 × 攻击面),于是整个架构长成了它的形状——一进程一 VM、无 BIOS、五种设备、四层监狱。读任何基础设施源码时,先找到它的稀缺资源,架构图就会自己开口说话。
参考来源
论文与官方资料
- Firecracker: Lightweight Virtualization for Serverless Applications — Agache et al., NSDI 2020(USENIX 官方页面,含论文 PDF)
- Firecracker 官网 — 125ms 启动 / <5MiB 开销 / 150 VMs/s 的官方口径
- firecracker-microvm/firecracker — 源码仓库,本文引用基于 commit
f2f43b6 - 仓库内文档:
docs/design.md(架构与威胁模型)、docs/jailer.md、docs/seccomp.md、docs/snapshotting/、docs/kernel-policy.md(PCI 支持)、docs/device-hotplug.md
解读与生态
- the morning paper: Firecracker — Adrian Colyer 的论文精读(内存开销对比 QEMU/Cloud Hypervisor 数据出处)
- google/crosvm — Firecracker 的血统来源(见仓库
CREDITS.md) - rust-vmm — Firecracker 与 crosvm 共同孵化的 Rust 虚拟化组件社区
- E2B — 基于 Firecracker microVM 的 AI agent 代码执行沙箱