Serverless 沙箱全景:从 ptrace 到 Dandelion
Serverless 沙箱全景:从 ptrace 到 Dandelion
过去几个月,我读了十篇系统论文,合在一起,它们勾勒出 Serverless 沙箱整个设计空间的地图。这篇文章不是逐篇综述。它试图找出断层线——那些定义这个领域走向的张力、汇聚点和权衡。我是作为一个正在构建 Serverless 沙箱(Shimmy)的人来写这篇文章的,所以视角是实用的:你今天究竟会构建什么?
根本性张力
每个沙箱都在进行同一笔交易:隔离强度与执行开销。我读的这些论文覆盖了整个谱系,代价被量化得足够精确,可以画出一幅清晰的图景。
在一个极端,ptrace 给你完全的控制——拦截每个系统调用,检查每个参数,运行任意逻辑——代价是每次钩子 31,201 纳秒。在另一个极端,Dandelion 的 CHERI 后端通过完全消除系统调用实现了 90 微秒以下的冷启动。在这两极之间,一切都是权衡。
这个领域大致分解为四种策略:
- 在内核边界拦截(ptrace、seccomp、SUD、二进制重写)
- 专化 VMM(Firecracker)
- 用软件隔离替换内核边界(Faasm/WebAssembly、Enclosure/MPK)
- 消除内核边界(Dandelion、SigmaOS)
令我惊讶的是,这些策略形成了一个清晰的演化序列。每一代都解决了上一代的瓶颈,但引入了一个新的约束。这个领域正在收敛,而收敛点并不在大多数人预期的地方。
系统调用拦截三部曲:zpoline、lazypoline、K23
zpoline 到 K23 的谱系是迭代式系统设计的经典案例。每篇论文都精确地解决了前一篇留下的缺陷。
zpoline(ATC 2023)发现了一个漂亮的技巧:在 x86-64 上,syscall 是 2 个字节,call rax 也是 2 个字节。由于 rax 持有系统调用号(一个小整数),把 syscall 重写为 call rax 会让执行跳转到虚拟地址 0 处的 nop sled,然后滑入一个蹦床。代价:每次钩子 41 纳秒——比 ptrace 快 761 倍。"调用约定已经把 rax 约束在一个小范围内"这个洞察,是那种事后看来显而易见但需要真正创造力才能发现的观察。
缺陷:zpoline 在加载时重写。JIT 编译的代码、动态加载的库、任何在启动之后生成的东西——全都看不到。并非真正彻底。
lazypoline(DSN 2024)用混合设计修复了这个问题。它使用 Linux 的系统调用用户分发 (Syscall User Dispatch, SUD) 作为慢路径:当一个从未见过的 syscall 指令执行时,内核投递 SIGSYS 信号。信号处理器以 zpoline 的方式重写它,之后所有执行都走快路径。这种懒重写模式——用可靠但慢的机制来发现需要优化的地方,再安装快速机制——在系统调用拦截之外有广泛的适用性。性能:在 Web 服务器基准测试上达到 94-95% 的基线吞吐量,同时保证彻底性。
但 lazypoline 也暴露了一个更深层的问题。这篇论文用 Intel Pin 做的 ABI 分析揭示:40-100% 的常见 coreutils 工具期望扩展 CPU 状态(SSE/AVX/x87)在系统调用前后得到保留。所有之前的二进制重写器都悄悄地破坏了这个状态。光是这个发现——所有之前的重写器都有潜伏的 bug——就足以证明这篇论文的价值。
K23(Middleware 2025)随后系统地列举了 zpoline 和 lazypoline 中五类陷阱:LD_PRELOAD 绕过(在 execve 之前清除环境变量,你的拦截器就消失了)、通过 prctl 禁用 SUD、静态反汇编误识别、未经验证的 NULL 执行,以及非原子的运行时重写。K23 的应对是两阶段设计:离线分析找出热点系统调用位点,在线选择性地只重写这些位点,用 ptrace 覆盖启动序列,用 SUD 作为兜底。结果:98.62% 的基线吞吐量——基本与 zpoline 的 98.93% 持平——同时是唯一能处理所有五类陷阱的系统。
lazypoline 命名的穷举性-效率-表达性三难困境结果是可以解决的,但解法需要三种独立机制的组合(ptrace 用于启动、二进制重写用于热路径、SUD 用于漏网之鱼)。单一机制拦截的时代结束了。
系统调用拦截无法解决的问题
即使是 K23 也要支付一笔不可避免的税:仅仅启用 SUD 就会在所有系统调用上增加 1.23 倍的开销,即使是未被拦截的那些——因为内核在每次系统调用入口都要检查一个选择器字节。这是一个任何用户空间技巧都无法消除的内核级代价。而且整个方法仅限于 x86-64——zpoline 的 nop-sled 技巧依赖于变长指令和非对齐跳转,这两者在 ARM 或 RISC-V 上都不存在。
更根本地,所有这些系统都与它们要沙箱化的代码处于同一地址空间。没有正交的隔离机制(MPK、硬件虚拟化、CHERI),一个有足够动机的攻击者可以覆写蹦床、选择器字节或拦截器本身。这些论文对此很坦诚——K23 明确将"正交的进程内隔离机制"推迟到别处——但这意味着单靠系统调用拦截不是一个完整的沙箱。它是纵深防御栈中的一层。
MicroVM 赌注:Firecracker
Firecracker(NSDI 2020)的核心洞察简单得有些出人意料:专化 VMM,而不是 OS。与其构建新的 OS 或新的隔离机制,把 QEMU 剥离到最小:5 万行 Rust 对比 QEMU 的 140 万行 C。保留 KVM。保留 Linux 访客内核。去掉 BIOS、PCI、VM 迁移、Windows 支持和 40+ 个仿真设备。剩下的是每个 VM 只需 3MB 开销(对比 QEMU 的 131MB)、约 125ms 启动,以及硬件虚拟化提供的完整隔离保证。
对 Serverless 经济学重要的数字:在 128MB 函数内存时,QEMU 在 VMM 开销上浪费了 100%;Firecracker 只浪费 2.3%。这是单台机器上能运行 8,000 个函数还是 100,000+ 个函数的差距。
Firecracker 的 Jailer 作为纵深防御模式很有启发性:即使在把用户代码放入硬件隔离的 VM 之后,VMM 本身也运行在一个 chroot 中,只有 24 个允许的系统调用和 30 个 ioctl。如果 VMM 被攻破,攻击者落入的是另一个沙箱。这种"给沙箱套沙箱"的方式值得借鉴到任何设计中。
Firecracker 牺牲的是冷启动。即使有快照恢复,访客 OS 也代表着不可约简的代价。Dandelion 的测量表明,Firecracker 快照恢复中有 8ms 用于加载访客 OS 快照和重建宿主-访客网络——这些操作的唯一目的是为用户代码提供类 POSIX 接口。这个观察推动了下一代的出现。
无 OS 前沿:Faasm、SigmaOS、Dandelion
三篇论文从不同角度推进了完全消除 OS 边界的目标。
Faasm(ATC 2020)使用 WebAssembly 的线性内存模型作为隔离原语。每个函数得到一个通过零基偏移量访问的连续字节数组;WebAssembly 运行时在编译时强制执行边界,违规时捕获异常。资源隔离来自 Linux cgroups。结果:通过 Proto-Faaslet 快照实现 0.5ms 冷启动(比 Docker 快 5,600 倍)、每个实例 90KB 内存(比容器少 15 倍),以及通过共享内存区域在同一宿主上的函数间共享状态而不破坏隔离的能力。Faasm 证明了容器不是唯一可行的 Serverless 隔离机制。
WebAssembly 的代价是真实的:32 位地址空间限制和缺少编译器优化,使得部分基准测试有 40-240% 的计算开销。对于数据密集型并行工作负载(Faasm 靠其两层共享内存状态架构在这里表现出色),分布式加速足以弥补。对于计算密集型的单函数,它就很痛了。
SigmaOS(SOSP 2024)从云原生的角度切入。它的关键观察是:现代云应用与云基础设施(S3、API、数据库)交互,而不是本地 OS 资源。如果你把函数限制在以云为中心的 API 上,就可以消除两个最昂贵的容器操作:overlay 文件系统创建(约 5ms)和网络命名空间创建(约 100ms)。SigmaOS 的 sigma-containers 只允许 67 个系统调用(对比 Docker 的 352 个),并完全屏蔽所有网络系统调用——连接通过一个可信代理,认证后再递交文件描述符。冷启动:7.7ms。代价:对现有 Linux 应用没有向后兼容性。
Dandelion(SOSP 2025)采取了最激进的立场。它把应用分解为纯计算函数(无系统调用、无网络、无线程)和通信函数(处理 HTTP 的可信平台代码)。计算函数运行在不需要访客 OS 的沙箱中,因为它们不需要系统调用支持。I/O 完全在沙箱外部发生。结果引人注目:CHERI 后端冷启动低于 90 微秒,KVM 后端 889 微秒(没有访客内核),而且——这个数字应该引起每个云经济学家的注意——与 Knative + Firecracker 自动扩缩容相比内存占用减少了 96%,因为按请求创建沙箱消除了预热空闲实例的需要。
Dandelion 的 dlibc 通过用户空间虚拟文件系统(输入/输出集作为文件出现)提供标准 C 接口(malloc、文件 I/O)。从不发出任何系统调用。可信计算基 (TCB) 缩减到约 2000 行 Rust,直接接触隔离和用户代码,而 Firecracker 有约 68,000 行。
这些方法是否会收敛?我认为会。Faasm 消除了容器。SigmaOS 消除了 overlay 文件系统和网络命名空间。Dandelion 消除了访客 OS 和系统调用接口。轨迹很清晰:内核边界正在从 Serverless 执行的关键路径中被推出。用什么取代它,取决于你的信任模型。
Seccomp-eBPF:有状态过滤器
seccomp-eBPF 论文(arXiv 2023)解决了一个不同的问题:让内核自己的过滤机制强大到真正有用。经典的 seccomp-BPF 是无状态的,无法解引用指针,没有同步原语,限制在 4096 条指令以内。这迫使实际部署过于宽松——容器在整个生命周期内必须允许 exec,因为 cBPF 无法表达"初始化时允许一次,之后拒绝"。
eBPF 改变了这一点。这篇论文引入了有状态过滤器(计数系统调用调用次数、实现时间专化)、安全的用户空间内存访问(复制到内核缓冲区以防止 TOCTTOU),以及系统调用串行化(原子变量强制竞争的系统调用串行化)。时间专化的结果令人信服:在启动后限制仅在初始化阶段的系统调用,在六个服务器应用上减少了 33-55% 的攻击面。
性能方面很清晰:eBPF 过滤器与优化的 cBPF 开销相当(约 60 个周期),而 Seccomp Notifier(用户空间代理)慢了 45 倍。这使 eBPF 成为任何需要比静态允许列表更智能的过滤器的明确路径。
问题在于:seccomp-eBPF 还没有合并到 Linux 主线。在 LPC 上,内核维护者的回应实际上是"Seccomp 不需要 eBPF"。这是一个政治问题,不是技术问题,对任何计划依赖它的人来说都很重要。
进程内隔离:轻量级上下文与 Enclosure
两篇论文探索在单个进程内实现隔离,完全避开内核边界。
轻量级上下文 (Lightweight Contexts)(OSDI 2016)向 FreeBSD 添加了一个新的 OS 抽象:lwC 共享线程,但有独立的虚拟内存映射、文件描述符表和凭证。在 lwC 之间切换只需 2 微秒——进程上下文切换的一半——因为它只是一次带有 PCID 标记的 TLB 条目的 CR3 寄存器交换。对 nginx 的评估显示吞吐量影响可以忽略不计。SSL 密钥隔离(把私钥放在单独的 lwC 里)在 10,000 次握手中只有 0.7% 的开销。快照/回滚模式——在处理请求前做 lwC 快照,之后丢弃所有状态——对 Serverless 来说非常优雅:它保证了调用之间没有信息泄露,而不需要重启容器。
持久的教训是:隔离不必昂贵。lwC 从未移植到 Linux(这篇论文是 2016 年的),这是一个社会失败,不是技术失败。
Enclosure(ASPLOS 2021)采取了语言集成的方式。它用一个 with 结构扩展了 Go 和 Python,把闭包绑定到内存视图(哪些包可访问)和系统调用过滤器上。LitterBox 后端使用 Intel VT-x(924ns 切换)或 Intel MPK(86ns 切换)来执行这些策略。MPK 后端在 HTTP 工作负载上的开销为 1.02 倍。
Enclosure 的洞察是:包是进程内隔离的自然单位。默认策略——只有直接依赖可见,所有系统调用被阻止——与开发者实际思考信任的方式一致:"我信任我的代码;我不信任这个 pip 包。" 论文中关于 CHERI 作为未来 LitterBox 后端的讨论很有预见性:能力硬件可以消除 MPK 的 16 键限制,并在没有页对齐约束的情况下实现对象粒度的隔离。
事情的走向
按顺序读完这十篇论文,三个趋势清晰浮现。
第一,内核边界正在失去其对隔离的垄断地位。 Firecracker 把信任边界移到了 Hypervisor。Faasm 移到了 WebAssembly 运行时。Dandelion 移到了一个 2000 行 Rust 的输出解析器。Enclosure 移到了编译器强制执行的包边界与硬件支持的内存视图。每个系统都证明了"进程"不是 Serverless 工作负载唯一的——甚至不是最好的——隔离单位。
第二,硬件正在追上软件的雄心。 Intel MPK 实现了 86ns 的域切换。ARM Morello(CHERI)在单个地址空间中实现了低于 90 微秒的沙箱创建。CHERI 能力模型——每个指针都携带自己的边界和权限,硬件在每次内存访问时强制执行——代表了进程内隔离的终局状态。当 Enclosure 把 CHERI 讨论为未来的后端,而 Dandelion 实现了比所有替代方案更快的 CHERI 隔离后端,他们指向的是同一个未来:硬件强制能力取代内核成为主要隔离原语。
第三,"无系统调用"设计点是可行的,可能是最优的。 Dandelion 证明了对于一类有意义的工作负载(数据处理、ML 推理、查询执行、智能体 AI 工作流),完全消除系统调用——不是过滤它们、不是拦截它们,而是消除它们——在冷启动、内存密度和尾延迟稳定性上带来数量级的改进。96% 的内存减少和 2-3 个数量级的方差减少不是渐进式改进,它们代表了一种不同的运行体制。
我今天会构建什么
如果我从头开始构建一个新的 Serverless 平台——我确实在构建,就是 Shimmy——这些论文告诉我该做什么。
使用分层架构和可插拔的隔离后端。 Dandelion 在这点上做对了:相同的调度器和执行引擎可以针对 KVM、进程、CHERI 或 WebAssembly,取决于信任模型。不要选一个隔离机制。选一个能让你换掉它们的抽象。
对于不可信的任意代码,Firecracker 风格的 MicroVM 仍然是黄金标准。 没有其他方式能为未修改的 Linux 二进制文件提供同等的隔离。但要积极专化——裁剪访客内核,使用快照恢复,根据 Little 定律(L = λ * W)确定预热池大小。
对于受约束的计算函数(无 I/O、无线程),消除 OS。 Dandelion 的计算/通信分离是正确的设计。纯计算函数不需要访客内核,不需要系统调用。在一个内存上下文中运行它们,通过用户空间虚拟文件系统提供标准接口的自定义 libc。冷启动和密度收益太大,不容忽视。
对于系统调用过滤,即使今天用 cBPF,也要为 eBPF 做规划。 时间专化——在启动阶段后阻止初始化期间的系统调用——是一个实质上免费的 33-55% 攻击面缩减。用一种能在内核支持到来时从 cBPF 迁移到 eBPF 的方式写你的过滤器。
对于进程内隔离,长期押注 CHERI。 MPK 的 16 键限制是一个真实的约束。lwC 从未逃出 FreeBSD。CHERI 提供了每指针边界强制执行,没有键限制,没有页对齐要求,沙箱创建低于 100 微秒。它在商品硬件上还没有生产就绪,但 Morello 的结果足够令人信服,值得把你的抽象设计得能容纳它。
永远给沙箱套沙箱。 Firecracker 的 Jailer 模式——在 VMM 本身周围加 chroot、命名空间、24 系统调用允许列表——应该成为任何隔离运行时的默认配置。如果运行时有 bug,攻击者应该落入另一个沙箱,而不是宿主机上。
这个领域正在向一个世界收敛:内核边界是几个隔离选项之一,硬件能力在指针粒度上强制内存安全,而"冷启动问题"通过让沙箱足够廉价到可以按请求创建来解决。我们还没到那里。但读完这十篇论文,轨迹是清晰无误的。
本文综合了以下研究:zpoline(ATC 2023)、lazypoline(DSN 2024)、K23(Middleware 2025)、Firecracker(NSDI 2020)、Faasm(ATC 2020)、Enclosure(ASPLOS 2021)、Lightweight Contexts(OSDI 2016)、Seccomp-eBPF(arXiv 2023)、SigmaOS(SOSP 2024)和 Dandelion(SOSP 2025)。
研究者:Akashi。Shimmy serverless 沙箱项目的一部分。
