1. AI Sandbox 的现状与核心矛盾AI Sandbox 这个词最近两年被提得越来越多尤其是在大模型应用落地之后几乎所有做 AI 基础设施的团队都在讨论怎么给模型生成的代码、Agent 执行的动作、用户提交的不可信脚本提供一个安全围栏。但真正动手做过的人都知道这个领域目前的现状可以用四个字概括各显神通各踩各坑。我自己在过去一年多的时间里先后用 Docker、gVisor、Firecracker、seccomp 这几套方案搭过不同规模的沙箱环境从本地开发机上的单机验证到跑在云上的多租户执行平台踩过的坑足够写一本小册子。这篇文章不打算给你讲教科书上的沙箱原理综述而是想把我在实际项目里看到的怪现状、做过的取舍、以及那些文档里不会写的细节原原本本摊开来讲。先说清楚这篇文章适合谁看如果你正在做 AI Agent 的代码执行环境、在线判题系统、插件运行沙箱、或者任何需要跑别人代码但不想被炸的场景那这篇内容应该能帮你少走至少两三个月的弯路。如果你只是听说过 Firecracker 和 gVisor 的名字想知道它们到底差在哪、什么时候该用哪个那也能从里面找到答案。我会尽量用大白话把隔离级别、性能开销、运维复杂度这几件事讲透同时给出可以直接抄的配置和排查思路。所谓怪现状核心矛盾其实就一句话隔离强度、启动速度、资源开销、运维复杂度这四个指标你最多只能同时要两个。市面上所有的方案本质上都是在这四个维度上做取舍没有银弹。下面我按方案类型逐个拆。2. 四大主流方案的真实取舍2.1 Docker最顺手但隔离边界最薄Docker 几乎是所有人做沙箱的第一选择原因很简单生态成熟、上手快、镜像分发方便。你在 Ubuntu 上apt install docker.io或者装个 Docker Desktop几分钟就能跑起来一个容器把用户代码丢进去执行看起来就隔离了。但这里有个巨大的认知误区Docker 的隔离本质上是 namespace cgroup capability 的组合共享的是同一个宿主机内核。这意味着一旦内核有漏洞比如著名的脏牛、或者各种提权 CVE容器逃逸就是分分钟的事。对于跑自己代码的场景Docker 完全够用但对于跑 AI 生成的、来源不可信的代码Docker 的隔离强度是不够的。我实测过一个典型的逃逸路径容器里如果以 root 运行且没有 drop 掉CAP_SYS_ADMIN配合某些内核版本可以直接挂载宿主机设备。所以用 Docker 做 AI Sandbox必须做几件事容器内绝对不能用 root 跑用户代码用--user 1000:1000指定非特权用户必须--cap-dropALL然后按需--cap-add最小集合必须加--security-opt no-new-privileges防止 setuid 提权必须限制资源--memory、--cpus、--pids-limit否则一个 fork 炸弹就能把宿主机拖垮网络默认--network none需要联网时走白名单代理一个我常用的最小化启动命令长这样docker run --rm \ --user 1000:1000 \ --cap-dropALL \ --security-opt no-new-privileges \ --network none \ --memory 256m --memory-swap 256m \ --cpus 0.5 \ --pids-limit 64 \ --read-only \ --tmpfs /tmp:size64m,noexec,nosuid \ -v /sandbox/code:/code:ro \ sandbox-base:latest \ python3 /code/main.py注意--read-only配合--tmpfs这个组合很多人会漏掉。只读根文件系统能挡住大量写文件类的攻击但程序总需要临时目录所以给/tmp挂一个带noexec的 tmpfs既满足需求又防止在临时目录里执行恶意二进制。提示--pids-limit这个参数极其重要我见过不止一次因为没限制进程数用户代码里一个while True: os.fork()直接把宿主机 PID 打满SSH 都登不上去。Docker 的另一个怪现状是启动速度。冷启动一个容器从docker run到进程真正跑起来实测在普通 SSD 上大概 300ms 到 1s 不等取决于镜像层数和存储驱动。对于需要频繁创建销毁沙箱的场景比如每个 AI 请求一个沙箱这个开销累积起来很可观。有人用容器池预热来解决但池子管理本身又是一套复杂度。2.2 gVisor用户态内核隔离和兼容性的平衡点gVisor 是 Google 开源的方案思路很巧妙它实现了一个用户态的 Linux 内核叫 Sentry用户程序的所有系统调用都被 Sentry 拦截并在用户态处理只有极少数必要的调用才透传给宿主机内核。这样即使程序攻破了 Sentry它面对的仍然是一个普通用户态进程离宿主机内核还隔着一层。用 gVisor 最爽的地方是它和 Docker 的集成非常顺滑你只要在 Docker 的 daemon 配置里注册一个runscruntime然后docker run --runtimerunsc就切换过去了。对上层应用几乎透明镜像、命令、参数都不用改。// /etc/docker/daemon.json { runtimes: { runsc: { path: /usr/local/bin/runsc } } }改完systemctl restart docker然后docker run --rm --runtimerunsc \ --user 1000:1000 --cap-dropALL \ --network none --memory 256m \ sandbox-base:latest python3 /code/main.py但 gVisor 的怪现状在于系统调用兼容性。因为 Sentry 是重新实现的 Linux 系统调用接口不是所有调用都支持得完美。我遇到过的情况包括某些 io_uring 操作不支持、部分ptrace相关功能受限、一些冷门的 socket option 行为不一致。跑 Python、Node 这类常规语言基本没问题但如果你要跑的东西依赖特殊系统调用比如某些数据库、或者用了特定内核特性的程序就得先测。性能上gVisor 的 syscall 开销比原生 Docker 高因为多了一层用户态拦截。我做过一个粗略的对比测试跑一个纯计算任务比如矩阵运算gVisor 和原生 Docker 差距在 5% 以内但跑一个 syscall 密集的任务比如大量文件读写、网络收发gVisor 可能慢 30% 到 2 倍不等。所以它适合计算为主、syscall 不密集的 AI 代码执行场景。启动速度方面gVisor 比 Docker 略慢一点点因为要初始化 Sentry实测大概多 100-300ms。这个开销在可接受范围内。2.3 Firecracker微虚拟机隔离最强但最重Firecracker 是 AWS 开源的 microVM 方案本质上是基于 KVM 的轻量级虚拟机。每个沙箱是一个独立的虚拟机有自己的内核隔离强度直接拉满——因为攻击者要逃逸得先攻破 guest 内核再攻破 KVM难度比容器逃逸高好几个数量级。Firecracker 的启动速度在虚拟机里算是极快的官方宣称 125ms 以内实测在配置好的机器上大概 150-300ms 能起来。但注意这个数字是裸 Firecracker的如果你要在上面跑一个完整的 Linux 用户空间还得加上内核启动、init 进程、应用加载的时间实际端到端可能到 500ms 到 1s。用 Firecracker 的怪现状是运维复杂度陡增。它不像 Docker 那样有现成的镜像生态你得自己准备 rootfs、自己管理内核镜像、自己写 API 调用来创建和销毁 VM。虽然现在有firecracker-containerd、Kata Containers这类项目帮你封装但配置起来依然比 Docker 麻烦得多。一个典型的 Firecracker 启动流程大概是这样# 启动 firecracker 进程监听一个 unix socket firecracker --api-sock /tmp/firecracker.sock --config-file vm_config.json其中vm_config.json要指定内核、rootfs、内存、CPU、网络等{ boot-source: { kernel_image_path: /opt/vmlinux.bin, boot_args: consolettyS0 rebootk panic1 pcioff }, drives: [ { drive_id: rootfs, path_on_host: /opt/rootfs.ext4, is_root_device: true, is_read_only: false } ], machine-config: { vcpu_count: 1, mem_size_mib: 256 } }这套东西跑起来之后隔离是真好但你要维护内核版本、rootfs 构建流水线、网络配置Firecracker 默认没有网络得自己配 tap 设备或者走 vsock工作量不小。所以 Firecracker 适合多租户、强隔离要求、且团队有虚拟化运维能力的场景小团队慎入。2.4 seccomp不是独立方案而是必备补丁seccomp 严格来说不算一个独立的沙箱方案它是 Linux 内核提供的一个系统调用过滤机制。你可以给它一个策略规定进程只能调用哪些 syscall其他的直接杀掉或者返回错误。很多人以为用了 gVisor 或者 Firecracker 就不需要 seccomp 了这是个误区。纵深防御的原则是每一层都要加。Docker 默认就带了一个 seccomp profile但那个 profile 比较宽松允许了大量 syscall。做 AI Sandbox 时我建议根据实际需要收紧。一个针对 Python 执行场景的最小 seccomp 策略大概长这样用 Docker 的 profile 格式{ defaultAction: SCMP_ACT_ERRNO, architectures: [SCMP_ARCH_X86_64], syscalls: [ { names: [ read, write, open, close, stat, fstat, mmap, mprotect, munmap, brk, exit, exit_group, futex, clone, execve, wait4, getpid, gettid ], action: SCMP_ACT_ALLOW } ] }这个策略默认拒绝所有 syscall只放行 Python 运行必需的那些。实测下来一个纯计算的 Python 脚本能正常跑但任何试图socket、mount、ptrace的操作都会被直接拒绝。注意seccomp 策略写太严会导致程序莫名其妙崩溃排查起来很痛苦。我的经验是先用SCMP_ACT_LOG模式跑一段时间收集实际用到的 syscall 列表再收紧成SCMP_ACT_ERRNO。别一上来就照抄网上的最小策略大概率跑不起来。seccomp 的怪现状是它和 gVisor 有重叠。gVisor 本身就在用户态拦截 syscall你在外面再套一层 seccomp有些调用会被拦两次。这时候要小心别让 seccomp 把 gVisor 自己需要的 syscall 也拦了否则 Sentry 直接起不来。3. 方案选型的决策逻辑3.1 一张表看清四种方案的定位我把四种方案的关键指标整理成表方便你对照自己的场景做选择维度DockergVisorFirecrackerseccomp隔离强度低共享内核中高用户态内核高独立内核辅助层启动速度300ms-1s400ms-1.3s150ms-1s无额外开销syscall 性能原生慢 30%-2x接近原生轻微开销运维复杂度低中高低生态成熟度极高中中高适用场景可信代码半可信代码不可信代码所有场景叠加这张表里最值得说的是隔离强度和运维复杂度的正相关。你想要越强的隔离就得付出越多的运维成本。Firecracker 隔离最强但你得自己管内核和 rootfsDocker 最省事但隔离最弱。gVisor 卡在中间这也是它这两年越来越受欢迎的原因。3.2 按场景选型的具体建议场景一内部工具跑自己团队的代码。直接用 Docker把前面说的那些加固参数加上就够了。别过度设计gVisor 和 Firecracker 的复杂度对内部工具来说是负担。场景二AI Agent 执行用户提交的代码。这是最典型的 AI Sandbox 场景。我的建议是gVisor seccomp 严格资源限制。gVisor 挡住内核攻击面seccomp 再收一层资源限制防止 DoS。如果预算充足且对隔离有极致要求再上 Firecracker。场景三多租户 SaaS 平台。每个租户的代码都可能恶意这时候 Firecracker 的独立内核就很有价值了。配合容器编排比如 Kata Containers 把 Firecracker 包装成 K8s 的 runtime能兼顾隔离和运维效率。场景四在线判题 / 竞赛系统。这类场景的特点是大量短生命周期的执行启动速度极其关键。gVisor 是比较好的平衡点或者用 Docker 预热池。Firecracker 的启动虽然快但端到端加上用户空间初始化还是偏慢。3.3 一个容易被忽略的维度镜像分发选型时大家盯着隔离和性能但镜像分发这个维度经常被忽略实际却非常影响体验。Docker 的镜像生态是碾压级的docker pull一个基础镜像几秒钟搞定。gVisor 复用 Docker 镜像所以也享受这个生态。但 Firecracker 用的是 ext4 rootfs 或者类似的磁盘镜像你得自己构建、自己分发没有现成的 registry 生态。我踩过的一个坑早期用 Firecracker 时每次更新沙箱环境都要重新构建 rootfs 镜像然后推到一个对象存储再让各个节点拉取。这套流程比docker push/pull麻烦太多而且镜像体积大几百 MB 到 GB 级分发慢。后来我们改用firecracker-containerd它能复用 OCI 镜像才算缓解了这个问题。所以如果你团队没有专门的镜像构建和分发能力Firecracker 的隐性成本会很高。4. 实操中的核心环节与配置细节4.1 资源限制的参数计算资源限制不是随便填个数字就完事填错了要么浪费资源要么被用户代码拖垮。我以内存和 CPU 为例讲讲怎么算。内存一个 Python 解释器启动大概占 20-30MB加上你的代码和数据给 256MB 通常够跑中等复杂度的脚本。但要注意--memory-swap必须和--memory设成一样否则容器可以用 swap等于没限制。另外 Python 的某些库比如 numpy、pandas加载后内存占用会飙升如果你的场景涉及数据分析得给到 512MB 甚至 1GB。CPU--cpus 0.5表示限制到半个核。对于计算密集的 AI 代码这个值要按实际负载调。我的经验是给 0.5 到 1 个核比较合理太低会导致正常代码超时太高则失去限制意义。进程数--pids-limit 64是个比较安全的默认值。Python 多进程、多线程场景下64 个 PID 通常够用同时能挡住 fork 炸弹。如果你的场景需要大量并发可以调到 128 或 256但别不设上限。磁盘--read-only加--tmpfs /tmp:size64m是标配。tmpfs 的大小要按需给太小会导致程序写临时文件失败太大则失去限制意义。64MB 对大多数脚本够用。4.2 网络隔离的三种模式AI Sandbox 的网络策略是个大话题我按隔离强度从高到低列三种模式完全断网--network none。最安全但很多 AI 代码需要联网比如调用 API、下载依赖。适合纯计算场景。白名单代理容器内只能访问一个本地代理代理再按白名单转发。这是我最推荐的模式。实现方式是在宿主机跑一个 HTTP 代理容器通过--network连到一个内部网络环境变量指向代理。代理层做域名白名单和请求审计。受限网络给容器一个独立的 bridge 网络用 iptables 限制出站目标。这种方式配置复杂容易出错我一般不用。提示无论哪种模式都要禁止容器访问宿主机内网地址段比如 169.254.169.254 这个云元数据地址是 SSRF 攻击的重灾区。用 iptables 或者代理层直接 drop 掉。4.3 超时与清理机制沙箱最怕的是僵尸进程和资源泄漏。用户代码可能死循环、可能挂起、可能创建一堆子进程不退出。所以必须有强制的超时和清理。我的做法是双保险容器层面用--stop-timeout配合外部的timeout命令应用层面再设一个执行超时。timeout --signalKILL 30s docker run --rm ... python3 /code/main.pytimeout发 KILL 信号后Docker 的--rm会自动清理容器。但注意如果容器里有子进程KILL 主进程不一定能杀掉所有子进程所以--pids-limit和 cgroup 的清理机制就很重要。实测下来配合--init参数用一个 init 进程做 PID 1负责回收僵尸进程能解决大部分清理问题。docker run --rm --init --pids-limit 64 ...--init这个参数很多人不知道但它对沙箱场景几乎是必需的。没有它用户代码 fork 出来的孤儿进程会变成僵尸累积多了会耗尽 PID。4.4 gVisor 的调优参数gVisor 有一些平台相关的调优参数值得单独说。runsc支持多种平台实现默认是ptrace性能较差推荐用systrap或kvm。// /etc/docker/daemon.json { runtimes: { runsc: { path: /usr/local/bin/runsc, runtimeArgs: [ --platformsystrap, --networknone, --debugfalse ] } } }--platformsystrap比默认的 ptrace 快不少实测 syscall 密集场景能提升 20%-40%。--networknone直接禁用 gVisor 的网络栈如果你不需要网络这个能省不少开销。另外 gVisor 有个--overlay2参数能改善文件系统性能但需要特定的存储配置。如果你的场景涉及大量文件读写值得研究一下。5. 常见问题与排查技巧实录5.1 沙箱启动失败类问题问题permission denied while trying to connect to the Docker API这个报错太常见了本质是当前用户没有权限访问 Docker 的 socket。解决方法有两个把用户加入 docker 组sudo usermod -aG docker $USER然后重新登录或者用 sudo。但注意加入 docker 组等于给了 root 权限生产环境要谨慎。问题docker: Error response from daemon: OCI runtime create failed这类报错信息通常不完整得看dmesg或者journalctl -u docker。常见原因包括seccomp 策略拦了必需的系统调用、cgroup 配置冲突、或者内核版本不支持某个特性。我遇到过一次是 seccomp 把clone3拦了导致新版本 glibc 的程序起不来加上clone3到白名单就好了。问题gVisor 容器启动后立即退出没有任何日志大概率是 Sentry 初始化失败。用runsc --debug模式跑能看到详细日志。常见原因是宿主机内核版本太老或者缺少某些内核特性。gVisor 对内核版本有要求建议 4.14 以上。5.2 性能异常类问题问题沙箱内程序跑得比预期慢很多先确认是不是 syscall 密集。用strace -c统计一下系统调用分布如果 syscall 数量巨大那 gVisor 的开销就体现出来了。解决办法要么换回 Docker如果隔离要求不高要么优化程序减少 syscall比如批量读写代替逐条读写。问题容器启动慢检查镜像层数层数越多启动越慢。用多阶段构建把镜像压到最少层。另外存储驱动也有影响overlay2 通常比 devicemapper 快。如果用了 gVisor确认 platform 是不是 systrap。5.3 隔离失效类问题问题容器内能访问到宿主机文件检查挂载点。-v挂载的目录如果权限没设好容器内可能读写宿主机文件。原则是只挂载必需的目录且尽量只读:ro。绝对不要挂载/var/run/docker.sock那等于把宿主机控制权交出去了。问题容器内能提权检查是否 drop 了所有 capability是否设了no-new-privileges是否用了非 root 用户。这三条缺一不可。我见过有人只做了前两条结果容器内有个 setuid 程序直接提权成功。5.4 一张速查表现象可能原因排查方向启动即退出seccomp 拦截 / 内核不兼容看 dmesg用 LOG 模式跑 seccomp运行超时死循环 / 资源不足加 timeout调大 memory/cpu僵尸进程堆积缺 init 进程加--init参数内存超限被杀限制太严 / 内存泄漏调大--memory检查代码网络不通网络模式配置确认--network和代理设置性能骤降syscall 密集 / platform 不对strace 统计换 systrap5.5 几个血泪教训第一个教训别在生产环境用latest标签的镜像。我有次因为基础镜像更新导致沙箱行为变化排查了半天。所有镜像都要固定版本号。第二个教训seccomp 策略要版本化管理。不同语言、不同版本的运行时需要的 syscall 不一样策略要跟着代码走别一套策略用到底。第三个教训监控沙箱的逃逸尝试。即使隔离做得再好也要假设会被攻破。记录所有被 seccomp 拒绝的 syscall、所有异常的网络请求这些是发现攻击的信号。第四个教训定期做逃逸测试。自己写一些攻击脚本尝试从沙箱里逃出来验证隔离是否有效。这比看文档靠谱得多。6. 我的实际组合方案与后续扩展讲了这么多方案最后说说我自己在项目里实际用的组合。对于中等规模、跑 AI 生成代码的场景我的默认配置是gVisorsystrap 平台 收紧的 seccomp 严格资源限制 白名单代理网络 强制超时 init 进程。这套组合在隔离强度、性能和运维复杂度之间取得了比较好的平衡实测能挡住绝大多数常见攻击同时启动开销在可接受范围内。如果场景升级到多租户、强合规要求我会把 gVisor 换成 Firecracker配合 Kata Containers 做编排代价是运维复杂度上一个台阶。如果只是内部工具那就退回纯 Docker 加加固参数简单省事。这个领域还在快速演进我最近在关注的方向包括基于 eBPF 的运行时行为监控能在沙箱运行时动态检测异常行为、以及一些新的轻量级虚拟化方案。但无论技术怎么变那四个维度的取舍逻辑不会变——想清楚你的场景最看重什么然后接受其他维度的妥协这才是做 AI Sandbox 的正确姿势。最后分享一个我常用的验证方法搭好沙箱后别急着上线先拿几个公开的容器逃逸 PoC 跑一遍看看能不能逃出来。逃不出来说明基本配置到位了逃出来了那就继续加固。这个过程比任何文档都直观。