☰
大模型代码执行安全:沙箱隔离与Agent工作流实践
2026/10/16 12:39:00 网站建设 项目流程

先说个我亲眼见过的翻车现场。某个 Agent 项目要做“下载公开数据并清洗”的演示,模型规规矩矩生成了一段 Python,按计划应该解析 CSV 再画个图。结果脚本跑着跑着,多了一步:去读这台机器上的 SSH 私钥目录,然后尝试向外发送。当时机器上碰巧没有敏感凭证,事情最后只是日志里多了一行可疑请求,但这件事给我留下了很深的印象——当我们说“让大模型写代码”时,真正在讨论的,其实是让一段来路不完全确定的代码在我们的信任边界里运行。

OpenSandbox 就是我在这个方向上的一个尝试,它只解决“大模型执行代码”这一环的安全问题。结构上不复杂:模型输出代码,OpenSandbox 在受限环境里执行,再把 stdout、stderr、返回码和文件变更交回给上层应用。真正难的是夹层里的那些决定:用什么隔离技术、网络怎么放行、会话状态怎么存、遇到奇怪进程行为时怎么稳住。这篇文章就按我实际踩坑的顺序,把这些点一个个掰开讲,适合正在做 Agent 应用、AI 编程助手或者数据分析自动化的开发者参考。

1. 裸奔的代价:模型生成的代码为什么不能直接跑

1.1 翻车不是概率问题,是次数问题

很多人对“模型生成的代码跑在宿主机上”这件事的第一反应是:出错概率很低,而且大多数时候模型写得还挺规矩。但这里有个认知偏差——单次出错概率低,不等于长期运行的失败概率低。你每天让 Agent 执行几百次代码,哪怕千分之一的异常率,一个月下来就是必然事件,而且异常往往不是“优雅的报错”,而是静默的破坏。

我见过的情况大致分几类:模型把参数理解错,导致删错了目录;模型为了“省事”直接调 shell 而不是用标准库;模型从网页内容里读到一段“示例代码”,顺手原样执行,结果那段示例其实是恶意脚本。这些都不需要模型有恶意,只要它生成的代码里恰好混进一个危险动作,而你又在宿主机上给了它完整的权限,后果就是整台机器被拖下水。

1.2 把风险分级列清楚,才知道沙箱要挡什么

在做 OpenSandbox 之前,我先把风险按破坏半径排了个序,这样设计隔离层的时候才知道优先级:

风险类型典型行为破坏半径
资源耗尽死循环、大内存分配、fork 炸弹拖垮宿主机,影响其他任务
破坏性操作删除文件、改写配置、安装可疑软件污染开发机或生产环境
数据外带读取环境变量、私钥、云元数据并外传凭据泄露,后续损失不可控
持久化后门写 cron、开机脚本、周期性任务长期驻留,难以发现
横向移动扫描内网、访问其他服务端口从单点突破扩散到整个网络

这里面最容易被低估的是“数据外带”。很多开发者给沙箱开了网络权限,以为只是让模型能 pip install,结果模型生成的代码读了/etc下的配置文件、环境变量里的临时密钥,甚至去访问云厂商的元数据地址,然后通过一个普通 HTTPS 请求把内容送出去。这种泄漏不会当场爆炸,等发现的时候凭据早就转了好几手。

所以沙箱的第一原则很简单:执行环境里的东西,默认都不应该被代码看到;代码能碰到什么,必须由你显式地交到它手上。

2. 三层隔离的取舍:OpenSandbox 为什么不直接用虚拟机

2.1 命名空间、seccomp 和只读根文件系统

很多人听到“沙箱”第一反应是上虚拟机。虚拟机隔离性确实好,但代价是每次启动几百毫秒到几秒的延迟、每实例几百 MB 的内存开销,对“模型写一段脚本跑一下”这种高频小任务来说,成本和速度都很难接受。OpenSandbox 默认走的是另一条路:把容器、内核安全模块和资源限制叠在一起,用三层轻量机制凑出接近虚拟机的隔离效果。

第一层是内核命名空间。它干的事是“制造错觉”:让进程以为自己独占了一台机器,看不到外面的进程列表,看不到宿主机的挂载点,网络栈也是独立的。第二层是 seccomp,它管的是系统调用白名单。命名空间解决“看得见”的问题,seccomp 解决“够得着”的问题——比如把mount、ptrace、reboot这类高风险系统调用直接掐掉,代码就算想干坏事,也没有合适的工具下手。第三层是只读根文件系统,用 overlay 方式把基础镜像设为只读层,代码的写入都落在一小块临时上层里,沙箱一销毁,改动跟着消失。

这个组合看起来不如虚拟机“干净”,但对“大模型生成的代码”这个场景来说是够用的。模型代码不是被刻意混淆的恶意样本,它更像是“可能犯错的普通程序”,所以我们的目标是防住意外和常见的攻击路径,而不是对抗国家级对抗样本。如果哪天要执行的代码来自完全不可信的第三方,再考虑切到带内核隔离的轻量虚拟机后端也不迟。

2.2 资源限制参数:给模型代码提前划好圈

资源限制是沙箱里最容易偷懒的部分,但也是翻车最频繁的部分。最典型的是进程数限制:很多模型代码里藏着递归或 fork 逻辑,一旦失控变成 fork 炸弹,几秒钟就能把宿主机的进程表打满。我日常使用的默认参数如下:

参数默认建议值说明
内存上限512MB防止一次性大数组或内存泄漏
CPU 份额1~2 核限制 CPU 密集任务的占用
进程数上限64对抗 fork 炸弹的关键
可写空间100MB限制临时文件总量
单文件大小10MB防止模型输出超大文件
执行超时60 秒硬超时,先 SIGTERM 再 SIGKILL
输出上限1MB防止不设限的 print 打爆管道

这些数值不是拍脑袋定的,是根据实际任务统计出来的。比如做数据分析时,模型经常要加载几十 MB 的 CSV,512MB 内存就偏紧,需要单独开一个“大数据任务”配置档,把内存放到 2GB,但进程数限制保持不变。反过来,跑纯文本处理脚本时,256MB 其实都够了,没必要给足。核心思路是“按任务类型给最小够用的额度”,而不是一刀切给个大池子。

有一个细节特别值得说:超时信号。直接kill进程经常不顶用,因为进程可能已经把 SIGTERM 信号屏蔽了,或者正在 uninterruptible sleep。我的做法是给执行器一个两段式超时——先发 SIGTERM,等 5 秒,还没退出就 SIGKILL。这一步看起来琐碎,但实际救了我很多次,尤其是模型代码里出现socket.connect()卡死的时候。

2.3 网络策略:默认拒绝出站,白名单放行

沙箱的网络策略是我改动次数最多的部分,也是最容易出安全漏洞的地方。最开始不少实现走的是“放行所有出站流量”的省事路线,理由是模型代码需要下载包、调 API、抓数据。但这条路等于把“数据外带”风险重新放了进来。

OpenSandbox 的默认策略是:出站默认拒绝,需要什么放什么。任务开始前,上层应用声明这个任务需要访问哪些域名,比如只允许pypi.org和某个内部数据源,沙箱就在网络层做域名级白名单。这样做还有个额外好处:模型代码即使因为提示注入被“劫持”,想外传也没有目的地可去。

特别要注意三个“死角”。第一是云元数据地址,比如169.254.169.254这类链路本地地址,必须在禁止名单里,很多临时密钥都在那里。第二是内网 CIDR,沙箱默认不应该能访问宿主机的其他容器或服务,否则一个沙箱被攻破,整个平台都可能被横向穿透。第三是 DNS——这个问题我踩过好几次,放到第 4 节专门讲。

3. 接入 Agent 工作流时的几个关键接口决策

3.1 无状态执行 vs 会话保持

给大模型接沙箱,第一个要做的决策是:每次执行是全新的环境,还是可以延续状态的环境。这直接决定了接口设计和用户体验。

“每次全新”的好处是干净、安全、实现简单,坏处是模型没法做“先装包再反复调试”这种多轮任务,装好的依赖、生成到一半的文件全部丢掉。很多 AI 编程助手类工具就吃了这个亏,用户让模型“跑一下刚改的测试”,结果上一次 pip install 的东西全没了,返回一堆 import error。

“会话保持”的好处是自然,坏处是容易失控:会话时间越长,环境里的脏东西越多,被利用的面越大。我最终采用的是“显式会话 + TTL + 快照回滚”的方案:沙箱有生命周期,默认 30 分钟没活动就销毁;销毁前可以手动打一个快照,下次任务从快照恢复;如果这次执行把环境搞坏了,直接回滚到上一个快照,而不是靠运维手动清理。

这个设计的取舍逻辑是:状态要给,但必须是“可审计、可丢弃、可回滚”的状态,而不是一个无限膨胀的黑盒。

3.2 接口设计:把沙箱当成一个远程进程

OpenSandbox 的对外接口没有搞成复杂的 SDK,核心就是“创建沙箱 + 执行命令 + 读取结果”。示意如下:

创建沙箱:

POST /v1/sandbox { "runtime": "python:3.12", "network": { "allow": ["pypi.org", "data.example.internal"] }, "resources": { "memory_mb": 512, "pids_limit": 64, "disk_mb": 100 } }

执行命令:

POST /v1/sandbox/{id}/exec { "cmd": "python3 main.py", "timeout_sec": 60, "cwd": "/workspace", "env": {} }

返回结果:

{ "exit_code": 0, "stdout": "...", "stderr": "...", "duration_ms": 1832, "fs_changes": ["/workspace/output.csv"] }

这里有个我很坚持的设计:必须返回fs_changes,也就是这次执行改了哪些文件。模型拿到这个字段后,可以自己判断“输出文件生成了没有”“中间产物污染了没有”,不需要上层应用去猜。另一个设计是流式输出:长任务(比如模型要跑训练脚本)不能等全部跑完再返回,exec 接口要支持 SSE 或者 WebSocket 推流,否则用户会以为沙箱卡死了。

3.3 模型侧的“自我约束”只能当锦上添花

解决了沙箱本身,还得面对一个现实:模型并不总是愿意按照约定来。写几千字提示词要求它“不许使用危险命令”,在 99% 的场景下有效,但你不能把安全押在这上面,因为提示注入、上下文污染、模型自身的随机性都可能导致它偏离约定。

我在实践中把模型侧约束当成“减少麻烦”的手段,而不是“安全保障”。具体做法有三个:第一,提示词里要求模型先列计划、确认要执行的命令类型,再做动作;第二,属于“高风险操作”的命令(rm、curl外传、改系统配置)设置人工确认环节,不让模型一口气跑完;第三,静态预检查只做最粗粒度的过滤,比如检测有没有明显的外传特征,但不依赖黑名单——黑名单永远会被绕过,拦截住常见错误就已经降低了大部分事故。

说白了,让模型不容易犯错,比等它犯错之后靠沙箱拦截更划算。沙箱是安全气囊,方向盘还是得扶好。

4. 实测翻车现场:三个高频事故与完整排查链路

4.1 事故一:exit_code 明明是 0,输出却永远为空

第一次把 OpenSandbox 接入 Agent 后,我最先遇到的是个特别诡异的状况:脚本在宿主机上手动跑,print 一切正常,一旦放进沙箱,返回值是 0,但 stdout 永远是空的。第一反应是权限问题,查了一圈不是;后来才发现根因在缓冲区和超时信号。

Python 的 print 默认走 stdout 的块缓冲,只有终端环境才会自动切换成行缓冲。沙箱里执行用的是管道,不是伪终端,所以脚本往缓冲区写数据,还没等到缓冲区刷出来,硬超时到了,SIGKILL 直接把进程杀了,缓冲区里的内容全部丢失。而且这种场景下 exit_code 已经是零,你根本看不出来进程是被杀的。

排查链路是这样的:先用最小复现脚本print("hello"); time.sleep(120)丢进沙箱,等待超时,发现输出为空;接着在宿主机上用管道方式跑同一脚本,复现同样现象,确认是缓冲问题;最后在执行器里加上伪终端支持,或者设置PYTHONUNBUFFERED=1,问题彻底消失。

这个坑给我留下的教训是:沙箱执行环境一定要当成“非交互环境”来处理。所有运行参数要显式指定为无缓冲,不能依赖默认行为。

4.2 事故二:网络白名单开了,pip install 还是卡到超时

第二个高频事故更隐蔽。按上文的设计,我给某个任务只放行了pypi.org的 443 端口,然后模型代码里写了一句pip install requests,结果卡了整整几分钟直到超时。当时我以为是白名单规则写错了,反复检查 TCP 规则,确认 443 是放开的。

后来用curl -v在里面一测,才发现请求一直停在 “Trying …” 阶段——域名解析根本没完成。原因很简单:沙箱默认拒绝了所有出站流量,包括 DNS 的 UDP 53 端口。应用层写了“允许访问 pypi.org”,但网络层并不认识域名,它需要先把域名解析成 IP,这个解析请求被策略挡住了,解析永远超时,后续连接自然全部卡死。

解法有两种:一是把 DNS 解析也加入放行名单,沙箱内的 resolv.conf 指向平台自建的解析服务;二是更省事的办法,在执行任务前把白名单域名预解析成 IP,直接写进沙箱的/etc/hosts,这样连 DNS 都不需要了。我后来统一采用预解析方案,因为它的审计日志更干净,而且不依赖沙箱内的网络解析链路。

这个事故的排查链路价值在于:网络白名单不是只放行目标端口,任何依赖的外部服务(DNS、NTP 等)都要纳入策略设计。漏了一个解析端口,功能层面就表现为“全网卡死”,而且报错信息还特别具有迷惑性。

4.3 事故三:上次安装好的包,这次会话里全没了

第三个事故是关于会话状态。有段时间我图省事,把沙箱生命周期设得很短,任务一结束就销毁环境。结果用户反馈:上次让模型装好的 pandas 版本,这次再运行就变成“ModuleNotFoundError”了。第一反应是模型代码写错,查了半天才发现是沙箱被重建,之前 overlay 上层里的写入全部丢失。

这个问题的根子在于我把“会话状态”和“任务执行”耦合在了一起。模型安装的依赖、生成的中间文件都写在沙箱的可写层,沙箱销毁,这些状态就没了。要保留状态,就必须明确区分“临时可写层”和“持久工作区”。我现在的做法是:每个任务挂载一个持久化 volume 到/workspace,模型的数据、脚本、输出都写在这里;依赖安装的缓存走另外一个可丢弃的层。两者分离后,顶层应用可以自行决定“这次的中间产物要不要留”,而不是被迫接受销毁或保留的二选一。

现象根因解法
exit_code 正常但输出为空块缓冲 + 硬超时 SIGKILL伪终端执行,或显式开无缓冲
白名单开了还是网络超时DNS 解析流量被策略误杀放行解析流量,或预解析注入 hosts
依赖/文件无故消失会话被重建,可写层被丢弃持久工作区与临时层分离

5. 沙箱兜底之后:模型侧的威胁模型与纵深防御

5.1 提示注入:代码执行的源头也可能是上下文

沙箱把“代码执行”这个风险装进了笼子,但还没解决“模型为什么会写出危险代码”的问题。大模型经常要读外部数据,网页、文档、邮件、用户上传的文件都可能是输入。如果这些内容里混入了恶意指令,模型就可能把攻击者的意图当成用户需求,生成并执行有害代码。

对这种场景,沙箱能做的只是“减少损失”,无法阻止行为本身。我的实践是分层处理数据与指令:模型的系统提示词明确区分“环境信息”和“用户数据”,外部读取的内容一律标记为不可信数据源,禁止将其中的指令视为任务要求。同时,沙箱内默认不注入任何真实凭据,模型代码即使被诱导去读环境变量,也拿不到有价值的东西。配合限流和审计日志,可以把单次危害控制在一个很小的范围内。

5.2 模型输出也要当成不可信输入

还有一个容易被忽视的盲区:沙箱保护的是“代码执行”这一环,但模型输出本身如果在另一处被执行,前面做的全部隔离就白费了。比如用户把模型生成的 shell 命令直接复制到宿主机终端里运行,或者把模型给的 YAML 配置直接铺到生产环境——这些场景下,模型输出就是不可信输入,需要同样的谨慎。

我给上层应用的建议是:模型产出的动作尽量走结构化的工具调用,而不是让用户复制粘贴代码。能通过 API 参数表达的,就不要输出原始命令;必须输出命令的,风险分级,高危操作加确认。

5.3 纵深防御清单

最后整理一下 OpenSandbox 完整方案的防御清单,按执行前、执行中、执行后三个阶段排:

  • 执行前:静态预检查,过滤明显恶意外传特征;按任务类型分配最小资源配额;声明网络白名单。
  • 执行中:命名空间 + seccomp 隔离;资源限制和超时兜底;出站流量白名单;零凭据原则。
  • 执行后:审计日志记录命令、文件变更、网络连接;对高频外传和异常文件改动设阈值告警;高危操作留痕并支持回滚。

说实话,把 OpenSandbox 做到现在,我最大的体会是:安全不是某一层做得完美,而是每一层都留了一手,任何一层失效,下一层还能兜住。另外一个实操小技巧想分享给大家:给沙箱的执行器配上伪终端支持,很多“进程不输出”“交互式命令行为异常”的怪问题能少掉一大半,这个改动成本极低,收益却非常高。

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

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

立即咨询