MIT 6.1810 · 第 21 课 · nz
容器与虚拟机
如何隔离不受信任的代码?本节课以 Firecracker 和 gVisor 为案例,探讨从 chroot 到容器的隔离技术。
目标:隔离不受信任的代码
操作系统隔离基于内核 + 进程 + 虚拟内存。但:
内核被攻破则隔离失效
现代 Linux 有 350+ 系统调用
许多共享命名空间(fs、pid 等)
动机:无服务器计算 (Serverless)
租户提供应用给提供商
提供商希望在可能相同的服务器上运行应用以提高利用率
隔离挑战: 任意代码,需要隔离不同租户的应用
性能挑战: 负载变化大,需要快速启动新实例
隔离方法概览
方法 攻击面 启动时间 开销
Linux 进程 + chroot 大 快 低
容器 (LXC/Docker) 大(350+ syscalls) 快 低
gVisor 中(53-68 syscalls) 快 中
Firecracker VM 小(36 syscalls) 中 低
Linux 进程与 chroot
Unix/Linux 机制:chroot
chroot /new/root /bin/bash
// / 现在指向 /new/root
好处: "监禁"进程,限制可访问的文件
挑战: 不允许进程逃出监狱(..、符号链接等)
不足: 许多系统调用仍可访问其他共享资源(如 kill)
命名空间 (Namespaces)
命名空间提供了一种作用域机制,限制系统调用中可以命名的资源:
PID 命名空间: 限制进程可以命名的 PID
IPC 命名空间: 共享内存等
NET 命名空间: 网络
UTS 命名空间: 主机名
USER 命名空间: 用户 ID
比 chroot 更好的隔离,适用于不同类型的资源。
Linux 容器
容器镜像 (OCI 格式)
为监禁进程提供"自己的" Linux 环境
动态链接库、配置文件、Python 解释器等
Open Container Initiative (OCI) 标准格式
cgroups
限制/调度资源使用:内存、CPU、磁盘 I/O、网络 I/O
不是安全边界,但对防止 DoS 攻击很重要
为什么命名空间/容器可能不够?
共享 Linux 内核
宽攻击面:350+ 系统调用,许多 ioctl
大量 C 代码——不断发现 bug
内核 bug 让对手逃脱隔离(本地权限提升,LPE)
额外安全机制:seccomp-bpf
过滤进程可以调用的系统调用:
每个进程可选关联一个系统调用过滤器
过滤器是用 BPF 字节码语言编写的小程序
内核在每个系统调用调用前运行过滤器
过滤器可以查看系统调用号、参数等
新进程继承父进程的系统调用过滤器
虚拟机 (VMs)
什么是虚拟机?
<代码>┌─────────────────────────────────┐
│ 硬件 │
│ ┌───────────────────────────┐ │
│ │ VMM (虚拟机监视器) │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ Guest Linux + 应用 │ │ │
│ │ └─────────────────────┘ │ │
│ └───────────────────────────┘ │
└─────────────────────────────────�代码>
虚拟机历史
1960s: IBM 使用 VM 共享大型昂贵机器
1990s: VMWare 在 x86 硬件上重新推广 VM
2000s: 广泛用于云和企业
为什么 VM 比容器更安全?
更小的攻击面: 没有复杂系统调用,只有 x86 + 虚拟设备
更少的 bug: VM 逃逸 bug 发现频率不到一年一次
慢虚拟机:模拟硬件
VMM 解释每条客户指令
维护虚拟机状态(32 个寄存器、satp、模式、RAM、磁盘、网络等)
优点: 有效,我们在实验室使用(xv6 运行在 QEMU 上)
缺点: 慢
快虚拟机:直接在 CPU 上执行
关键洞察: 客户 OS 和应用使用相同的指令集(x86/RISC-V)
使用 CPU 的 add 指令而不是模拟它——快!
如果客户内核执行特权指令怎么办?
解决方案: 在用户模式下运行客户内核
特权指令会 trap 到 VMM → VMM 模拟它
这称为 "trap-and-emulate "。
示例:RISC-V trap and emulate
Guest 用户代码执行 ecall 进行系统调用
CPU trap 到 VMM(ecall 总是产生 trap)
硬件保存 Guest 的 PC 在(真实)sepc 中
VMM trap 处理器:
检查 guest 指令
virtual sepc <- real sepc
virtual mode <- supervisor
virtual scause <- 8 "system call"
real sepc <- virtual stvec
sret:从 trap 返回(设置真实模式为用户)
VT-x/VMX/SVM:硬件支持的虚拟化
现代 Intel(和 AMD)CPU 在硬件中支持虚拟化:
客户可以执行不需要 trap 的特权指令!
可以修改控制寄存器、更改页表、处理异常
可以切换到用户模式并接收系统调用 trap
比 trap-and-emulate 更快,VMM 软件更简单
术语
术语 含义
Root mode 运行主机
Non-root mode 运行客户(内核 + 用户进程)
VMCS VM 控制结构:配置、保存/恢复客户特权状态
VMLAUNCH/VMRESUME 主机 → 客户
VMCALL 客户 → 主机
VT-x 的 EPT(扩展页表)
Guest VA ──(Guest PT)──→ Guest PA ──(EPT)──→ Host PA
第一层:Guest 页表映射 VA → Guest PA(通常)
第二层:EPT 映射 Guest PA → Host PA
VMM 设置 EPT 只有客户自己内存的映射
客户看不到也不能更改 EPT
客户可以自由更改页表,VMM 仍然通过 EPT 提供隔离。
Linux KVM
硬件支持虚拟化的抽象
管理虚拟 CPU、虚拟内存
QEMU 实现虚拟设备、virtio、CPU 指令模拟、BIOS
VM 的缺点
高启动成本:启动 VM 需要很长时间
高开销:每个运行 VM 的内存成本
刚性/粗粒度的资源分配和共享
VMM 本身可能有 bug(QEMU 有 1.4M 行 C 代码)
Firecracker 设计
使用 KVM 处理虚拟 CPU 和内存
用 Rust 重新实现 QEMU
支持最小设备集:virtio 网络、virtio 块、键盘、串口
块设备而不是文件系统:更强的隔离边界
不支持指令模拟(除必要的 CPUID 等)
不支持 BIOS——直接加载内核并运行
Firecracker 实现:Rust
内存安全语言(除了 "unsafe" 代码)
50K 行代码:比 QEMU 小得多
VMM 实现不太可能有缓冲区溢出等 bug
Firecracker VMM 运行在"监禁"进程中
chroot 限制可访问的文件
命名空间限制访问其他进程和网络
以独立用户 ID 运行
seccomp-bpf 限制可以调用的系统调用
gVisor 方案
在独立用户进程中重新实现 OS 系统调用接口:
使用 ptrace 或 KVM 拦截容器的系统调用
用 Go 编写的用户进程实现这些系统调用
更好的语言避免缓冲区溢出等错误
bug 不在内核代码中,可能被 Linux 进程包含
更细粒度的共享和资源分配
gVisor 的缺点
性能开销可能很大(每个系统调用必须重定向到 gVisor 进程)
兼容性问题(真实 Linux vs gVisor)
安全对比:可访问的系统调用
平台 允许的系统调用数
LXC (Docker) 300+(阻止 44 个)
Firecracker 36
gVisor 53-68
各平台优缺点
平台 优点 缺点
Linux 最简单,最少代码,最低开销
隔离弱
LXC 隔离和容器抽象,灵活共享,近原生性能
共享内核,宽攻击面
gVisor 强隔离,灵活共享和资源分配
性能开销
Firecracker 强隔离,比 gVisor 更好的性能
粗粒度
要点总结
隔离是分层构建的
从 chroot 到命名空间到 seccomp 到 VM,每层增加安全性但增加开销。
VM 提供最强隔离
硬件支持的虚拟化(VT-x)让客户直接执行,EPT 提供内存隔离。
Firecracker 平衡安全与性能
最小设备集、Rust 实现、50K 行代码——比 QEMU 更安全。
gVisor 提供灵活隔离
用户空间内核实现,Go 编写,更细粒度的共享和资源分配。
测验
Q1: 为什么容器比 VM 更容易被攻击?
A. 容器有独立的内核
B. 容器共享 Linux 内核,有宽攻击面
C. 容器没有进程隔离
D. 容器不使用虚拟内存
Q2: VT-x 的 EPT(扩展页表)如何工作?
A. 客户直接管理物理内存
B. VMM 直接管理客户页表
C. 客户页表映射 VA→Guest PA,EPT 映射 Guest PA→Host PA
D. EPT 加密客户内存
Q3: 为什么 Firecracker 使用块设备而不是文件系统?
A. 块设备比文件系统快
B. 块设备提供更强的隔离边界(更简单)
C. 文件系统不支持虚拟化
D. 块设备不需要内核
延伸阅读