☰
OpenSandbox:让大模型安全执行代码的沙箱架构与实战
2026/10/9 4:04:11 网站建设 项目流程

当 AI 学会“造沙箱”:OpenSandbox 如何让大模型安全地执行代码

我最近在折腾AI Agent项目时发现一个越来越绕不开的问题:大模型本身不会“做事”,它只会“说话”。但当你让它写代码、调接口、处理数据文件,甚至自动运维服务器时,它就必须真正执行代码。代码一旦跑起来,问题就来了——大模型生成的代码能信吗?它会不会误删文件?会不会把API密钥打到日志里?会不会反向连接外部服务器?我在实际项目里踩过几次坑之后,越来越确认一个判断:任何打算让大模型真正“动手”的工程化方案,都绕不开沙箱(Sandbox)这层地基。而 OpenSandbox 这个名字,就是冲着“让大模型安全地执行代码”这个靶心去的。

这篇文章我会把它当成一个完整的工程案例来拆:从最底层的隔离原理,到架构选型、权限模型、网络策略,再到真实跑起来会撞到的坑。无论你是AI应用开发者、DevOps工程师,还是正在做AI Agent框架选型的技术负责人,这篇都应该能帮你少走不少弯路。我会直接讲我做过的实验、踩过的坑、以及最后沉淀下来的那套可用方案。

1. 项目整体拆解:为什么“让大模型执行代码”天然需要沙箱

1.1 从“模型生成代码”到“代码真实运行”,中间隔着安全问题

很多人在最开始接触大模型写代码时,都有一个错觉:模型把代码吐出来,任务就结束了。但实际工程里,代码只有真正在某个环境里跑起来,才叫“执行”。而这一跑,就暴露了一个根本矛盾——大模型生成代码的过程本质上是不可完全预测的。

我拿一个真实的例子来说。我做过一个测试:让大模型根据用户需求自动生成一段Python脚本,用来批量重命名文件夹里的图片。模型给出的代码逻辑上完全正确,但它内部调了一个shutil.rmtree清理临时目录。如果临时目录变量被用户输入污染,这段代码就可能把整个项目目录删掉。这个案例说明一个残酷的事实:你无法在生成阶段通过“提示词”保证代码安全,因为模型理解的是“语义正确”,不是“执行安全”。提示词写得再严格,模型也可能会因为训练数据里的某种模式,在某个边界条件下生成危险行为。

所以,唯一的解法是把“执行”这个动作放进一个可控的容器里。沙箱的职责很明确:你不能阻止大模型生成坏代码,但你可以让坏代码跑不出圈。它能把“代码执行”的副作用(文件系统写入、网络请求、进程创建、资源占用)全部限制在一个预定边界内。OpenSandbox要解决的核心问题就是这个——用一种工程上可落地、性能上可接受、隔离强度足够的方式,让大模型随心所欲“跑代码”,但跑不出乱子。

1.2 传统沙箱为什么不够用:三大硬伤

我们在决定自己造轮子之前,也认真评估过现成的沙箱方案,比如 Docker 容器、Firecracker 微虚拟机、gVisor,以及各类在线判题系统(OJ)用的隔离器。总结下来有三个核心痛点:

第一,启动速度太慢。大模型对话场景里,用户往往在等一个实时反馈。Docker 冷启动一个容器动辄 1 到 2 秒,加上镜像拉取、依赖安装,一次代码执行要 5 秒以上,交互体验完全没法接受。第二,隔离粒度太粗。Docker 容器共享宿主机内核,虽然有了命名空间(namespace)和 cgroup 隔离,但内核漏洞一旦被利用,就能直接影响宿主机。对于多租户环境、或者执行不可信 AI 代码的场景,这个风险是不可接受的。第三,资源回收麻烦。大模型生成的代码很可能陷入死循环,或是申请几百 GB 内存。传统容器方案如果处理不好超时与资源销毁,宿主机就会被拖垮。

这几个痛点催生了 OpenSandbox 的设计出发点:既要一个进程级别的轻量方案,又要具备类似虚拟机的安全边界,还要能在毫秒级完成创建和销毁。它的目标不是替代 Docker,而是专门服务“大模型执行代码”这种短时、高频、不可信、需快速回收的全新工作负载。

1.3 关键设计目标:兼容性、性能、安全三角平衡

设计一个供 AI 执行代码用的沙箱,本质上是做三个维度的平衡:兼容性、性能、安全性。

兼容性要求模型生成的代码“拿过来就能跑”,不能因为沙箱限制了某些系统调用,导致最常见的 Python 库都导入失败。性能要求代码执行上下文切换不要产生明显延迟,让“模型生成代码—执行—返回结果”整个链路控制在秒级。安全则要求即便恶意代码执行,也拿不到宿主机文件、网络、机密信息和计算资源。

这个三角关系做起来非常微妙。安全收紧一个维度,另外两个就会恶化。OpenSandbox 的做法我后面会详细展开,这里先记住一个结论:项目最后选了gVisor 作为内核隔离核心,配合 seccomp 白名单策略和用户态文件系统代理。这套组合兼顾了三者的平衡,也是目前跑下来最稳的路线。

2. 核心架构与选型逻辑:OpenSandbox 的底层实现思路

2.1 为什么选择 gVisor 作为安全边界

先说结论:OpenSandbox 内核隔离层基于 gVisor 构建。gVisor 是 Google 开源的一个“用户态内核”,它拦截应用发起的系统调用,在用户态重新实现 Linux 内核的 ABI。这么做的意义在于,应用访问宿主机内核的唯一路径被替换成一个高度受限的中间层。

这个选择的核心逻辑我需要展开一下。传统宿主机内核暴露的面积太大,漏洞往往是致命的。而 gVisor 有两大独有优势:第一,应用无法直接访问宿主机内核,它面对的是一个模拟出来的 Linux 内核,攻击面大幅缩小;第二,系统调用被捕获后,可以叠加自定义的安全策略。也就是说,我们可以在 gVisor 之上做更细粒度的控制,而不是靠黑名单被动防御。

我做过一个对比实验,让大模型生成代码去读取宿主机一层的敏感文件,比如/etc/shadow。在纯 Docker 容器里,这个请求会直接穿透到宿主机内核并返回文件内容;在 gVisor 沙箱里,因为系统调用在用户态就被劫持,文件访问被拦截并返回“权限不足”。这已经足够说明问题:对不可信代码来说,不能让它有“碰到宿主内核”的机会,而 gVisor 正好能截断这条路。

2.2 用 seccomp 构建白名单:宁可错杀,不可放过

隔离内核只是第一步。沙箱里的代码仍然需要系统调用才能工作,所以,OpenSandbox 必须建立一套“哪些系统调用能被使用”的白名单机制。这里我推荐的做法是用seccomp叠加 BPF 规则。seccomp 全称 Secure Computing Mode,是 Linux 内核提供的一个沙箱机制,它允许进程调用进入内核前,先经过一段字节码(BPF)过滤规则。通俗点说,就像给系统开了一扇安检门,每个系统调用都会过一遍安检门,符合白名单才放行。

OpenSandbox 的白名单策略核心覆盖以下几类:

  • 进程管理类:clone、fork、execve、exit等,但禁止ptrace这类可用于调试注入的系统调用。
  • 文件访问类:只允许沙箱挂载的临时目录读写,禁止访问宿主机其他路径。通过 seccomp 拒绝路径相关调用,从底层掐断文件逃逸。
  • 网络类:DNS 解析和 HTTP 请求有时是必要的,所以会针对socket调用限定协议族(只允许 TCP/UDP)和目标端口白名单,默认禁止 ICMP、RAW_SOCKET。
  • 时间类:clock_gettime等时间调用必须放行,否则很多脚本会直接报错。但settimeofday这类时间篡改调用要禁止。

这里有个直接的经验:白名单永远比黑名单好用,因为黑名单防不住未知攻击。大模型生成的代码虽然“蠢”,但攻击者完全可以利用模型生成任意代码,黑名单的覆盖面永远不够。用白名单,就意味着没被明确放行的调用一旦出现,直接拒绝执行。

2.3 网络策略:让代码像连了条“单向电话线”

代码执行过程中,最危险的动作之一就是网络访问。大模型生成的代码可能被诱导去请求恶意域名、下载木马脚本,甚至把执行结果外发到攻击者服务器。我们称这为数据外带(exfiltration)。OpenSandbox 在网络层面的设计原则,被我们内部叫“单向电话线”——只管打电话进来,不给偷偷把文件传出去的机会。

实现上用了三层控制:

  1. 沙箱默认没有网络,除非用户显式开启。
  2. 即使开启网络,也只允许通过一个透明代理访问,由代理执行访问控制策略(比如只允许 HTTPS、过滤危险域名、限制访问目标 IP 的性属)。
  3. 所有网络流量必须经过审计日志记录,记录访问的域名、时间、字节数。这样一旦发生数据外带,至少能追踪到源头。

这个设计在 AI 应用场景下很实用,我举个最典型的场景:代码里调用了外部天气 API,这本是合法的行为。但大模型生成代码时可能因为 prompt injection 攻击,被诱导把当前对话上下文(包含隐私提示词)贴到某个不存在的服务器上。有了透明代理的网络管控,这种目标不在白名单里的请求链路会在代理层就被熔断。

3. 实操实现:从零搭建一个面向大模型的沙箱执行系统

3.1 基础环境与组件清单

在进入实操之前,先给出我推荐的组件清单。你不需要全套照搬,但核心的几个我建议直接采用:

组件选型用途
内核隔离gVisor(runsc)拦截系统调用,提供独立内核 ABI
容器运行时containerd + runsc 集成管理镜像生命周期,提供 OCI 标准兼容
进程沙箱seccomp BPF系统调用白名单过滤
文件系统overlayfs + 临时挂载沙箱内只读挂载依赖库,可写区域隔离
网络代理自研 HTTP CONNECT 代理统一出口流量,执行域名与协议策略
任务调度Redis Stream + Python Worker管理代码执行任务队列与结果返回
超时控制进程组 cgroup + timer强制终止死循环与超时任务

这组件的选型理念是:每个组件都负责一个具体的边界,而不是用一个大而全的框架解决所有问题。比如 containerd 负责镜像管理,gVisor 负责内核隔离,seccomp 负责系统调用过滤,代理负责网络审计。职责明确、边界清晰,出了问题也知道去哪看日志。

3.2 核心操作步骤:写一个可执行的沙箱 Python API

这里我直接给出一个简化但完整的 Python 实现思路,用于在本地快速体验 OpenSandbox 的核心能力。这个版本不依赖 Kubernetes,单机即可跑通:

import subprocess import tempfile import os import uuid import time ACLOW = { "readonly_dirs": ["/usr", "/lib", "/lib64", "/bin"], "writable_root": "/tmp/sandbox_root", } SANDBOX_TIMEOUT = 10 def run_in_sandbox(code_str: str, resource_limit: dict = None): """把Python代码放进gVisor沙箱执行""" task_id = uuid.uuid4().hex workdir = tempfile.mkdtemp(prefix=f"oss_{task_id}") # 写入用户代码 with open(f"{workdir}/main.py", "w") as f: f.write(code_str) # 默认资源限制 if resource_limit is None: resource_limit = {"memory": "256m", "cpu": "0.5", "time": SANDBOX_TIMEOUT} # 组装runsc命令 cmd = [ "runsc", "run", "--rootless", "true", "--network=none", # 无网络模式 "--overlay", workdir, # 临时文件系统覆盖 f"--memory-limit={resource_limit['memory']}", f"--cpu-limit={resource_limit['cpu']}", "--", "python3", f"{workdir}/main.py", ] start = time.time() try: # 超时通过 timeout 命令强制管理 result = subprocess.run( ["timeout", str(resource_limit["time"])] + cmd, capture_output=True, text=True, timeout=resource_limit["time"] + 3 ) return { "task_id": task_id, "stdout": result.stdout, "stderr": result.stderr, "returncode": result.returncode, "elapsed_ms": int((time.time() - start) * 1000), } except subprocess.TimeoutExpired: return {"task_id": task_id, "error": "execution_timeout", "elapsed_ms": resource_limit["time"] * 1000} finally: # 强制清理沙箱目录 subprocess.run(["rm", "-rf", workdir])

这段代码其实已经具备了一个极简沙箱的核心流程:临时目录创建、代码写入、runsc 容器启动、超时控制、结果返回、目录回收。实际部署时,你还需要在此基础上加一层 gRPC 服务接口,接收大模型的 code 输入,并把结果反馈给模型做下一轮推理。

3.3 关键参数选择与资源限制策略

参数配置是一项极其讲究实际经验的工作。我见过太多人因为在沙箱参数上犯懒,导致线上事故。这里把我的调参记录分享出来:

CPU 限制。官方推荐的 CPU 限制在 0.5 到 1 个核之间。大模型生成的代码绝大多数是 IO 密集型任务,比如文件解析、数据处理,CPU 跑满 0.5 核完全够用。但要注意,过低的 CPU 配额会让一些编译型任务(比如用户代码里临时编译 C 扩展)变得极其缓慢,最终触达超时。

内存限制。256MB 是我测试下来比较稳妥的底线。大模型经常生成的 pandas、numpy 代码,导入一些库就会占用 100MB 以上内存,低于 128MB 时会产生大量 OOM(内存不足)误杀。但也不建议给太大,512MB 以上已经足够,否则单机并发能力会急剧下滑。

超时。有一个容易被忽略的细节:超时不只针对 CPU 时间,还要考虑阻塞时间。代码执行可能停在 socket 读取上,CPU 占用为 0,但进程一直不退出。因此必须强制使用timeout命令或进程组 kill 机制,以 wall-clock 时间兜底。我在实际使用中,把普通代码执行超时设置为 10 秒,复杂数据处理任务 60 秒。大模型生成任务的单次执行不必追求过长,可以通过拆解任务、分步执行来解决长流程需求。

3.4 可写区域与依赖注入:让代码跑得起来

沙箱安全再高,代码跑不起来也是废的。大模型执行代码的一个高频需求就是“安装依赖”。在沙箱环境里,你不可能每次执行都去 pip install,那会拖垮性能。我的方案是:预装基础镜像 + 持久化依赖层。

基础镜像固定预装 Python 3.11、pip、requests、numpy 等常用库。然后在镜像之上叠加一个 overlayfs 的可写层,但是每次执行结束直接销毁可写层。这样既保证代码能用常见库,又确保任何执行中的持久化副作用都不会遗留在沙箱里。

如果模型生成的代码需要临时装一个库,则走两步:

  1. 沙箱内 pip install 到临时目录,例如/tmp/deps。
  2. 代码执行时设置PYTHONPATH=/tmp/deps指向这个临时目录。

请注意,这一步是把“临时依赖安装”和“持久化代码写入”分开处理,沙箱回收时,临时依赖和结果一并清空。这样设计的好处是,不管执行了多少次、装了多少库,宿主机环境始终干净,不会越积越乱。

4. 实战效果:用三个典型场景验证沙箱可靠性

4.1 场景一:大模型自动生成数据处理脚本

我拿一个很典型的任务来测试:让大模型分析一份 CSV 文件,生成统计报表。我们给沙箱传入代码和数据文件,让模型输出 Python 代码,沙箱执行后返回结果。大模型生成代码时会访问文件/data/input.csv,这里就需要沙箱的目录挂载功能:

  • 将数据文件目录只读挂载为/data;
  • 沙箱内所有可写操作限制在/tmp目录;
  • 读取结果后,沙箱自动把/tmp/output.csv拷出。

这个场景跑下来,最大的收益不只是“代码能跑”,而是“出了错不会污染环境”。沙箱内脚本误写了一个超大文件到/tmp,占满磁盘也没关系,沙箱任满随时销毁,宿主机磁盘永生无损。避免了一次次人工清理容器的噩梦。

4.2 场景二:AI Agent 自动写测试用例并执行

我们团队现在开发 AI Agent 时,会让模型自动生成单元测试用例,并在沙箱里执行。这个场景的难点在于,测试代码往往会动态导入和访问项目内部文件,沙箱不能装得太死,否则测试覆盖率极低。OpenSandbox 的做法是分两个阶段:

  • 第一阶段,沙箱以只读模式挂载项目代码目录,允许执行所有导入操作;
  • 第二阶段,当检测到测试代码需要写文件(如输出 mock 数据)时,才切换到临时覆盖层,写入结果不持久化。

测试代码里不可避免地会包含assert语句、异常抛出、甚至故意模拟超时。这套沙箱全都能兜住。我在做这个场景时,最痛快的一点就是:以前跑 AI 生成的测试代码,最怕它打开一个永不关闭的线程池。现在有了配额和超时,进程直接被 kill,测试结果还能给出“execution_timeout”这样的清晰反馈,模型可以根据反馈自动修正代码,形成循环迭代。

4.3 场景三:开放平台里接收陌生人提交的代码

这个场景我建议所有做开放 API 平台的团队都参考一下。如果你对外提供“AI 写代码并运行”的能力,别人提交过来的代码就等同于不可信公共输入。我们在沙箱上追加了加密环境变量保护。

具体做法:沙箱内不存储真实的 API key,只放一个加密的 token,代码执行前才通过沙箱的文件注入接口解密注入到指定环境变量。一旦沙箱销毁,密钥也随之消亡。这个需求是硬性的,以前在普通容器里,受过污染的环境变量非常容易从/proc文件系统泄露出去;在 OpenSandbox 的模型里,/proc访问被隔离,环境变量注入过程也被 seccomp 和代理截断,外部进程几乎不可能读取沙箱内进程的内存和配置。

5. 常见问题与排查技巧实录

5.1 “代码明明在本地能跑,进沙箱就报错”怎么办

这是反馈最多的一个问题。常见的原因有两个:

第一个是系统调用被误杀。比如某个库依赖io_uring、perf_event_open,这些系统调用在白名单中被默认禁用了。处理方法是先看错误日志中具体被拒绝的调用编号。我一般用strace在容器外跑一遍同一段代码,对比确认哪些调用被沙箱阻断,再有针对性地调整白名单。

第二个是路径习惯不同。本地开发用绝对路径/home/user/data.txt,沙箱里没有这个路径,导致FileNotFoundError。这类问题没有捷径,只能在执行前用静态扫描替换常见绝对路径为沙箱内挂载路径,或者在错误信息里指引模型去读取环境变量SANDBOX_DATA_DIR。

5.2 沙箱内网络请求一直超时,但代理是通的

网络超时大概率不是沙箱本身的问题,而是代理的过滤规则没放行该域名。我排查时习惯先看代理日志,确认请求是否命中了规则。如果域名命中了白名单但依然超时,下一排查点就是代理的 TLS 证书验证。很多大模型生成的代码使用未加校验的 requests 库,代理在中间做 HTTPS 解密时可能因为证书链不完整而丢掉请求。

这里有个实际经验:在处理沙箱网络问题时,别一上来就修改 seccomp 或 gVisor 配置。网络策略、文件系统、系统调用,这三者隔离的复杂性完全不同,务必各自独立排查。90% 的问题都出在业务层策略,而非隔离层本身。

5.3 内存明明给了 256MB,执行 numpy 还是被 OOM 杀掉

这个现象比较容易误导人。真相是,numpy 库导入时本身就要 pre-alloc 一些共享内存页,这些页在 gVisor 里计入进程内存开销。所以给 256MB 可能连 import numpy 都过不去,这不算 bug,是各种内存统计口径不同导致的。

我的建议是把基准测试做细:对每个预装库,都记录它在沙箱内的实际启动内存(resident memory)和导入耗时,写成一个基线表。后续配置沙箱资源配额时,直接按“基线内存 + 用户代码估算内存 + 20% 余量”来确定。这对线上稳定性至关重要。

6. 从沙箱到 AI Agent:这套方案的落地点与扩展空间

6.1 沙箱与大模型工作流的闭环设计

沙箱如果孤立使用,价值非常有限。它必须嵌进 AI Agent 的工作流中,形成一个“生成—执行—反馈—修正”的闭环。我在项目里实现了一个标准 API 流程:

def ai_execute(model_client, user_query): # 1. 让模型先生成代码(不执行) generated = model_client.chat(f"请写一段Python代码解决问题:{user_query},只输出代码") # 2. 从输出中提取代码块 code = extract_code_block(generated) # 3. 沙箱执行 result = call_sandbox_execute(code) # 4. 如果失败,把错误信息作为上下文回传给模型二次修正 if result.get("returncode") != 0: fix_prompt = f"代码执行失败,错误:{result['stderr'][:2000]},请修复后重新输出代码。" return ai_execute(model_client, fix_prompt) return result["stdout"]

这个闭环中,沙箱检查点是验证模型生成质量的客观基准。以前我们调试大模型代码,靠肉眼 review,现在靠沙箱执行返回的 exit code 和 stdout,效率提高了数倍。可以说,沙箱不只是安全装置,还是 AI Agent 的“试错沙盘”。

6.2 多 Agent 协作场景下的沙箱编排

热门词里提到的“多 AI 协作”场景,也对沙箱提出了更高要求。当多个 AI Agent 并行工作、各自执行代码并依赖彼此结果时,沙箱之间必须支持可控的数据交换。我在项目里采用的是“沙箱临时共享卷”模式:

  • Agent A 的沙箱执行完,将结果写入它自己的临时目录,比如/tmp/agent_a_out;
  • 调度层为 Agent B 创建沙箱时,把 Agent A 的输出目录以只读方式挂载进 B 的沙箱;
  • 这样既保证了每个 Agent 的执行环境相互隔离,又实现了数据的有序流转。

这种模式的性能损耗非常小,因为共享数据没有经过磁盘或者网络,而是在同一台宿主机上用 overlayfs 直接挂载。更重要的是,每一个 Agent 的执行边界依然清晰,出了问题不会互相牵扯。

6.3 后续扩展:日志审计、模型评估、安全策略自动化

我在跑通基础沙箱功能后,最想强调一个容易被忽视但极其重要的事:日志审计。大模型执行代码的沙箱,每一次 execve、每一个网络请求、每一次文件读写,都应该有完整的审计日志。这不仅是合规要求,更是定位问题和追溯攻击的关键。OpenSandbox 的日志接口我建议实现三层:

  1. 摘要日志:记录任务 ID、耗时、返回码、资源占用;
  2. 行为日志:记录触发了哪些系统调用、访问了哪些域名、读写过哪些文件路径;
  3. 内容日志:当检测到可疑行为时,保存代码本身和输入输出的关键片段。

有了这些日志,后续就能做一件非常有价值的事:用沙箱执行结果去反向评估大模型的代码质量。比如统计模型生成代码的一次执行通过率、常见报错类型分布、平均执行耗时。这些指标对选型大模型、优化 prompt、甚至微调模型都是硬通货。

再往后,安全策略本身也可以用模型来自动生成。我最近在尝试一个方向:把历史攻击样本和绕过尝试喂给大模型,让模型生成新的 seccomp 规则或网络策略建议。虽然现在还要人工过一遍,但基本上说明这个闭环已经开始了自我进化。

7. 实操心得:关于权限、性能与安全的那点私房话

我做了大量实验后,最想说的一点是:别试图让沙箱“完美”。任何声称能完全阻止所有逃逸的沙箱,都是吹牛。沙箱的目的从来不是提供一个数学意义上的完美安全边界,而是把攻击成本提到足够高,让恶意行为变得不划算,同时给防御方争取到足够的检测和响应时间。

在权限模型上,我的原则是“最小权限 + 用完即焚”。每个代码执行任务都是一个全新的临时身份,只具备完成该任务所需的最小权限。任务结束,身份销毁。这套模式不需要维护复杂的权限继承关系,也极大减少了权限误配的可能性。

性能调优上,有一条捷径值得分享:把“创建沙箱”变成“复用沙箱池”。当任务并发量大时,冷启动沙箱的开销会逐渐成为瓶颈。我做了预热机制:预先启动一批沙箱实例,等待任务到位后直接切换执行上下文。这个优化把执行链路从平均 80ms 降到了 20ms 左右。不过这需要在任务间做更严谨的数据清理,否则新任务会读到上一个任务的残留文件。

最后,安全是个无止境的攻防游戏。我今天写的这套方案,很可能明天就有新的绕过姿势。但我始终认为,做沙箱工程这件事,最核心的能力不是堆砌多少层防御,而是你对自己系统的理解有多深。你得清楚每一个系统调用会在哪里被拦截、每一行代码会在哪里脱离隔离边界。把工程做透明,而不是做成黑盒,才是安全感的真正来源。

如果你现在正准备在大模型应用里接入代码执行能力,我建议你直接动手搭一版 gVisor + seccomp + 网络代理的最简模型。跑通一个“让大模型写代码并执行”的闭环,你才能真正理解沙箱里每个参数背后的意义。这个过程不需要很久,但它带给你的体感,比读十篇文章都真实。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询