@ghumare64: https://x.com/ghumare64/status/2055329887431393309
摘要
深入探讨为何本地编码代理(如Claude Code和Codex)正逐渐转向libkrun而非Firecracker进行沙盒隔离,原因是Firecracker无法在macOS上原生运行。本文还介绍了iii-sandbox,一个基于libkrun构建的开源硬件隔离执行层。
查看缓存全文
缓存时间: 2026/05/16 01:09
为什么智能体沙箱正在向 libkrun 汇聚,而非 Firecracker
每一个重要的编码智能体现在都运行在你的笔记本上。Claude Code、Codex、OpenCode、Cursor 的智能体模式,以及新一代的 CLI 优先工具。它们安装一个二进制文件,接管一个目录,然后开始生成真正想要运行的代码。
那些高呼 Firecracker 作为未来每个人都需要的沙箱的人,假设智能体和沙箱都生活在别人的数据中心里。这种模型仍然承载着很多智能体。但是正在席卷世界的智能体形态——Claude Code、Codex、本地 CLI 智能体——运行在开发者的 MacBook 上,而 Firecracker 在那里无法启动。不是“还没”,而是在架构上它就不可能。
所以,对于任何在 2026 年构建智能体基础设施的人来说,问题不再是“我们该封装哪个微型虚拟机监控器”,而是“哪个微型虚拟机监控器能封装我们智能体实际运行的所有地方”?这个问题目前只有一个答案,而且它不是 Firecracker。
答案是 libkrun。而答案中更有趣的部分根本不是 VMM,而是你放进它里面的东西。
过去几个月,我一直在 iii 上开发 iii-sandbox。iii-sandbox 是 iii 的硬件隔离执行层,iii 是一个开源引擎,其中每个原语(一个 HTTP 路由、一个队列、一个 cron、一个智能体、一个沙箱)都是同样的东西:一个 worker,它通过 WebSocket 连接到引擎,并注册 函数 和 触发器。沙箱只是那些 worker 中的一个。这篇文章深入探讨了我们为什么基于 libkrun 构建它,客户机内部有什么,以及我们在实际困难部分学到的经验。
回顾:为什么需要一个内核边界
这部分众所周知,所以我长话短说。一个 Linux 容器是命名空间加 cgroups 加 seccomp 过滤器,叠加在单个共享内核之上。那个内核暴露了数百个系统调用。其中任何一个有 bug,多租户故事就结束了。对于一个在你的后端内部运行你自己的代码、存活 40ms 的请求来说,这没问题。但对于一个调用它五秒前生成的命令、针对一个故障模式未知的模型、针对用户关心的文件系统的智能体来说,这就有问题了。
完整虚拟机通过蛮力解决这个问题。每个客户机都有自己的内核。虚拟机监控器(Linux 在 Intel/AMD 上是 KVM,macOS 上是 Apple 的 Hypervisor.framework,Microsoft 是 Hyper-V)调解特权操作。代价总是速度和体积:经典的 QEMU 虚拟机模拟了一台 1998 年的 PC,因为那是 1998 年的操作系统想要启动的东西。
“微型虚拟机”的想法(由 AWS 为 Lambda 推广,并于 2018 年作为 Firecracker 开源)是:删掉那台 1998 年的 PC。没有 BIOS。没有 PCI。没有 VGA。保留 KVM、一个串行控制台,以及客户机实际需要的少数 virtio 设备。结果是在亚秒级启动和几 MB 开销下实现了硬件隔离。这是每个云沙箱供应商都汇聚到这个设计的原因,因为它解决了正确的问题。
改变的是沙箱需要运行的地方。
Mac 上的 Firecracker 不是个东西
Firecracker 依赖于 /dev/kvm。KVM 是一个 Linux 内核模块。没有 KVM 的 macOS 移植,将来也不会有,因为 Hypervisor.framework 已经在操作系统架构中占据了那个位置。Firecracker 自己的 FAQ 明确声明:Linux x86_64 或 aarch64 且启用 KVM。
你可以在 Mac 上运行 Firecracker,就像你可以在 Mac 上运行任何 Linux 二进制文件一样:在另一个 Linux 虚拟机内部。这让你得到了嵌套虚拟化(一个客户机虚拟机运行在另一个虚拟机内部),这是 Apple Silicon 在 M3+ 上支持的,但有真实的性能代价,并且意味着你的“本地沙箱”现在是一个位于 UTM 虚拟机内部、再位于 macOS 内部的微型虚拟机。启动时间叠加。磁盘镜像叠加。网络叠加。这对演示来说可以,但对一个想在开发者心流中每隔几秒启动一个全新隔离环境的智能体来说就不行了。
与此同时,那些真正在发布的智能体(Claude Code、Codex、OpenCode、每个 IDE 嵌入的编码智能体)作为原生二进制文件在 macOS 上运行。它们写入用户的真实文件系统,驱动用户真实的 git,访问用户真实的端口。它们想要的“沙箱”是一个能在智能体已经运行的任何机器上几百毫秒内启动的 Linux 微型虚拟机,挂载智能体的仓库,可选网络,可流式输出结果。智能体不关心底层是哪个虚拟机监控器。它关心的是它能在一个 MacBook Pro 上、一个 CI runner 上、一个 EC2 实例上,以同样的方式、同样的 API 工作。
目前只有一个开源 VMM 达到了那个标准。它就是 libkrun。
libkrun 是什么
libkrun 最初是 Red Hat 的一个项目,用于在 macOS 上驱动 podman machine。它是一个动态库(Linux 上是 libkrun.so,macOS 上是 libkrun.dylib),将 VMM 作为一个你可以链接的函数,而不是一个你可以执行的进程暴露出来。你给它一个内核(libkrunfw)、一个根文件系统(你想直通挂载的任何目录)、一个执行规范(“以这些参数、这个 env、这个 rlimit 运行 /init”),然后它就地启动一个微型虚拟机并等待它退出。
关键细节:libkrun 是虚拟机监控器无关的。在 Linux 上,它通过 Firecracker 使用的同一个 /dev/kvm 使用 KVM。在 macOS Apple Silicon 上,它直接使用 Hypervisor.framework。Intel Mac 不是支持的目标(Apple Intel 硬件已停止支持)。一个 API,同一套设备模型,你的用户实际运行的两个主机操作系统。
它还有意丢弃了 Firecracker 的一个默认设置:传统块设备。Firecracker 将客户机根文件系统暴露为一个 virtio-block 设备,后端是主机上的 ext4 镜像。libkrun 将客户机根文件系统暴露为一个 virtio-fs 主机目录,挂载到客户机中。无需创建镜像。当你更改文件时无需重建镜像。你指向 libkrun 一个目录,客户机就看到那个目录。
对于一个启动五秒前用户从 OCI 仓库拉取的任意根文件系统的沙箱来说,这要好得多。你将 OCI 镜像解压到一个目录,将目录交给 libkrun,客户机读取的就是主机磁盘上的相同字节。不需要 mkfs.ext4。不需要回环设备。当你缓存失效时不需要重新打包。
代价是 virtio-fs 有其自身的怪癖(我们稍后会讲到的根目录 readdir 错误),以及基于 smoltcp 的网络路径比 Firecracker 的 TAP 设备模型更固执。我们也会讲到这些。但最重要的一点是:libkrun 是目前唯一一个在用户一半运行的操作系统上提供 Firecracker 等级隔离的开源 VMM。
iii-sandbox,一次启动的层面
iii-sandbox 就像其他任何 iii worker 一样。它通过 WebSocket 连接到引擎,注册十四个函数(四个生命周期操作加十个文件系统操作),并等待 iii.trigger() 调用。内部,它是一个守护进程,拥有一个活跃的微型虚拟机注册表和磁盘上的三样东西:一个根文件系统缓存(每个镜像名一个提取的 OCI 镜像)、一个每个沙箱的覆盖树,以及每个沙箱一个用于 shell 通道的 Unix 套接字。
当一个调用者触发 sandbox::create 时,以下是实际序列,附有代码引用:
// crates/iii-worker/src/sandbox_daemon/create.rs
let id = Uuid::new_v4();
let layout = OverlayLayout::for_sandbox(id);
layout.ensure_dirs()?;
let shell_sock = layout.base().join("shell.sock");
let boot = launcher.boot(&BootParams {
rootfs, workdir, shell_sock,
cpus, memory_mb, env, network,
}).await?;
launcher 是使守护进程可测试的间接层:VmLauncher 是一个 trait,生产实现将父 iii-worker 二进制文件 fork 为 __vm-boot(一个隐藏子命令),启动参数作为 CLI 标志。那个子进程实际上链接了 libkrun。
fork 是故意的。libkrun 是一个 C 库,带有相当多的不安全 Rust 绑定代码。如果客户机内核恐慌或 libkrun 本身段错误,父守护进程不应随之死亡。崩溃隔离让我们每次启动付出一次 fork+exec 的代价,与 libkrun 启动本身相比这是可以接受的。
__vm-boot 子进程大致做以下事情:
// crates/iii-worker/src/cli/vm_boot.rs (删节版)
let passthrough_fs = PassthroughFs::builder()
.root_dir(&args.rootfs)
.build()?;
let mut builder = VmBuilder::new()
.machine(|m| m.vcpus(args.vcpus as u8)
.memory_mib(args.ram as usize)
.hyperthreading(false)
.nested_virt(false))
.kernel(|k| k.krunfw_path(libkrunfw_path).init_path("/init.krun"))
.fs(move |fs| fs.tag("/dev/root").custom(Box::new(passthrough_fs)));
// 可选:用于网络出口的 smoltcp 用户空间 TCP/IP
if args.network {
let mut network = SmoltcpNetwork::new(NetworkConfig::default(), args.slot);
network.start(tokio_rt.handle().clone());
builder = builder.net(|net| net.mac(network.guest_mac())
.custom(network.take_backend()));
}
// 命名的 virtio-console 端口:一个用于控制 RPC,一个用于 `exec` shell
builder = builder.console(move |mut c| {
if let Some(path) = console_output_path { c = c.output(path); }
if let Some(fd) = guest_control_fd {
c = c.port("iii.control", fd, fd);
}
if let Some(fd) = guest_shell_fd {
c = c.port("iii.exec", fd, fd);
}
c
});
builder.exec(|e| e.path("/init.krun").workdir(&args.workdir)
.rlimit("RLIMIT_NOFILE", 65536, 65536)
.env("III_WORKER_CMD", &worker_cmd)
// ...
);
其中有几件值得挑出来,因为每一件都是我们不得不做出非显而易见决定的地方。
内核是 libkrunfw,而不是你管理的内核镜像。 libkrunfw 提供一个精选、加固的 Linux 内核作为动态库。第一次启动一个全新的 iii 安装时,它从 GitHub 发布下载;后续启动 dlopen 缓存的副本。我们选择这个而不是发布我们自己的 vmlinux,原因与大多数项目不派生 C 编译器相同:“微型虚拟机满意的 Linux 内核”表面区域足够大,以至于 Red Hat 的人作为一个专注的子项目来做会比我们做得更好。
没有 virtio-net + TAP。 libkrun 的网络模型是主机上的一个 smoltcp 用户空间 TCP/IP 栈,通过共享内存队列桥接到客户机的 virtio-net。没有主机端的 tap0。没有内核路由更改。没有 iptables 规则。对于 macOS(TAP 设备需要苹果已积极弃用的内核扩展)这意味着网络无需请求 root 权限或安装 tuntap kext 就能正常工作。
对安全性的影响也值得一提:smoltcp 是从客户机到主机的整个网络攻击面。它是一个纯 Rust 的 TCP/IP 实现,设计用于裸机嵌入式用途,没有堆分配,足够小以供审计。主机端的桥接将客户机的出站连接转换为主机上的 tokio::net::TcpStream。客户机内的环回地址被重写为网关 IP,以便尝试访问 localhost:3000 的智能体到达用户实际的开发服务器,这是智能体 99% 时间想要的行为。
virtio-console 命名端口,而不是 vsock。 Firecracker(以及大多数微型虚拟机)使用 virtio-vsock 进行主机-客户机 RPC:一种内核内套接字类型,由 (context_id, port) 寻址,不经过 IP。我们首先尝试了那个。问题在于 macOS 上通过 Hypervisor.framework 的 vsock 支持只实现了一半,并且没有通过 libkrun 的公共 API 暴露。可行的替代方案是带命名端口的 virtio-console,libkrun 原生支持。我们在主机上打开一个 socketpair(AF_UNIX),将一端作为命名客户机端口 (iii.control, iii.exec) 交给 libkrun,客户机内的 init 二进制文件从 /dev/vport0p1 读取。线缆是双向字节流;我们在此基础上分层一个行帧 JSON 协议。
这听起来像降级。确实有点。但它保留的属性是唯一重要的:客户机无法通过主机明确附加的端口之外的方式寻址主机。主机上没有 127.0.0.1:port 是客户机可以在没有主机以此端口启动虚拟机的情况下访问的。相同的安全模型,稍臃肿一些的传输层。
没有 PCI,没有 virtio-block,没有 ACPI。 libkrun 的机器模型大致是:一个 MMIO 总线、一个 PMU、构建器附加的 virtio 设备,仅此而已。没有 pci=off 启动标志,因为一开始就没有 PCI。内核命令行在被传给客户机时大部分是空的。
从 sandbox::create 到“shell 套接字已绑定,准备 exec”的整个启动过程在守护进程中被记录为 boot_phase: shell_sock_wait(总启动时间),并且在热守护进程(libkrunfw 已缓存,根文件系统已提取,代码签名已应用)上,它是 README 声称的几百毫秒。冷路径(全新安装的第一次启动)主要由 libkrunfw 下载和 macOS 代码签名主导,这两者我们都通过一个原子的 PROVISION_DONE 标志缓存。
init 二进制文件才是真正的产品
iii-sandbox 中耗时最长且最重要的部分不在 libkrun 里。而是在 iii-init 中,这是我们放入每个客户机的 PID 1 二进制文件。
一个微型虚拟机没有 systemd。它几乎没有一个用户空间。当客户机内核启动时,它执行 VMM 指向 init= 的任何二进制文件。那个二进制文件就是 PID 1。如果 PID 1 退出,内核就会恐慌。如果 PID 1 阻塞在某事上,整个虚拟机都会阻塞。如果 PID 1 泄漏文件描述符或不回收子进程,你就会得到史上最糟糕的进程表损坏。
你可以使用 systemd 作为 PID 1。真正的 Linux 发行版就是这么做的。但它是一个 1.5M 的二进制文件,功能集设计用于启动笔记本电脑,对于一个只存活 4 分钟并运行六个命令的沙箱来说,你不需要其中的 95%。
你可以使用 tini 作为 PID 1。这是容器的标准。它只有 12K,回收子进程,转发信号,不做其他事。但它是沙箱的错误原语,因为 PID 1 在微型虚拟机中实际要做的事情比 tini 多,比 systemd 少。
一个 iii-sandbox 客户机内部的 PID 1 二进制文件按顺序做五件事:
// crates/iii-init/src/main.rs
fn run() -> Result<(), InitError> {
iii_init::root_pivot::pivot_to_tmpfs_root()?;
iii_init::mount::mount_filesystems()?;
iii_init::mount::mount_virtiofs_shares();
iii_init::mount::override_proc_meminfo();
iii_init::rlimit::raise_nofile()?;
iii_init::network::configure_network()?;
iii_init::supervisor::exec_worker()?;
Ok(())
}
这就是整个启动过程。每一行都在做一些因为真实边缘情况而存在的工作。
pivot_to_tmpfs_root 将 / 从 virtio-fs 根文件系统切换到 tmpfs,并通过每个目录的绑定挂载重新暴露根文件系统条目。这是因为 libkrun 的 virtio-fs 根文件系统有一个 readdir 错误,其中 ls / 枚举条目的模式会在小于 1GB RAM 的客户机上 OOM 杀死调用者。我们在 bun 对 / 的启动探测中遇到了这个问题。解决方法是绝不让任何进程直接读取 virtio-fs 根目录:tmpfs 挂载到 /,下面所有内容绑定挂载。
override_proc_meminfo 伪造 /proc/meminfo::MemTotal 为每个 worker 的 RAM 上限。这是因为 bun 的内存分配器(底层是 Zig 的 GeneralPurposeAllocator)直接读取 MemTotal,而忽略 cgroup v2 的 memory.max。因此,一个以 memory_mb: 512 启动的沙箱会让 bun 尝试根据通过 virtio 视角看到的宿主机 RAM 来分配内存。修复方法是通过绑定挂载覆盖 /proc/meminfo,并伪造总量。
raise_nofile 将 RLIMIT_NOFILE 提高到 1M。这是因为 node 的 fs.promises 和 Python 的 asyncio 在并行测试运行期间会打开足够多的文件描述符,超过默认的 1024 限制。主机端的 vm_boot.rs 也在 init 二进制文件运行之前通过 libkrun 的 KRUN_RLIMITS 设置它,因为某些工作负载的动态加载器会打开足够多的共享对象,在我们的 Rust 代码启动之前就触碰了限制。双重保险。
configure_network 从环境变量读取 III_INIT_IP、III_INIT_GW、III_INIT_CIDR、III_INIT_DNS,并相应配置 eth0 和 /etc/resolv.conf。或者,
相似文章
@jerryjliu0: Agent 与文件沙盒是 2026 年的热门方向。这是 @itsclelia 提供的一个巧妙参考实现,向你展示了……
该参考实现展示了如何利用 Rust、LiteParse 和 microsandbox,在本地沙盒中安全运行 LLM Agent,从而处理和分析各类文档。该开源 CLI 工具借助 OpenAI 的 GPT 模型与原生 bash 命令,在隔离环境中执行文件检索与分析任务。
@ishaan_jaff: 我们正在开源 LiteLLM Agent 平台,可以在隔离的 K8s 沙箱中运行 Claude Code、Codex、Hermes 或任何编码代理…
LiteLLM 正在开源其 Agent 平台,允许开发者在隔离的 Kubernetes 沙箱中运行 Claude Code、Codex 和 Hermes 等编码代理,而不会暴露真实的 API 密钥。
我在无头Linux上为codex构建了一个计算机使用沙箱框架。GPU直通、计算机使用和codex的sudo权限全部工作。这是一个完美的开发沙箱,可以在最小化"rm -rf /"风险的同时实现全自动工作。
作者使用LXC容器构建了一个AI沙箱管理器,允许Codex代理在无头Linux上拥有完整的sudo权限和GPU直通,同时保护主机系统免受灾难性错误的影响。
@markokraemer: 是的,我们正在构建 SandboxAgent,它只是一个基于 opencode 的运行时,运行在沙盒中。一个随机功能是远程会话…
Markokraemer 宣布了 SandboxAgent,这是一个基于 opencode 的运行时,运行在沙盒中,支持远程会话存储和 Git 原生版本控制,用于集中数据和隔离操作。
Canopy
Canopy 允许在原生 macOS 上并行运行多个 Claude Code 会话,每个会话均处于沙箱环境中。