/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

上周四傍晚,安全组的同事突然端着笔记本站到了我工位旁,脸色严峻地调出了一份抓包记录。那是下午三点多红蓝对抗演练时截获的一段内网流量:公司新上线的报表生成 Agent 在解析一个由测试人员上传的恶意 CSV 文件时,被提示词越狱诱导,大模型直接生成了一段混淆的 Python 脚本。脚本里明晃晃地写着读取 /proc/1/environ 提取云厂商 AccessKey,并尝试通过反弹 Shell 扫描内网 10.0.0.0/16 网段。
虽然网络防火墙拦截了外部出口流量,但看着那条直奔 Kubernetes 集群元数据接口的 HTTP 请求,我后背全是冷汗。很多研发团队以为大模型生成的代码就是“普通的后台脚本”,随手用 os/exec 或者起一个裸 Docker 容器 docker run 就算完事。但面对拥有自主规划能力的 Agent,大模型生成的每一行代码在本质上都属于“不可信的远程任意代码执行(RCE)”。一旦沙箱存在一丝缝隙,容器逃逸、主机凭据泄露、内网横向渗透或者简单的 Fork 炸弹耗尽主机 PID,分分钟就能让整套集群趴下。
大模型代码执行的四大致命风险
要防范沙箱逃逸,必须先摸清大语言模型生成代码在生产环境下的攻击面:
纵深防御体系:只读 Rootfs、Drop ALL 与纯净隔离
为了彻底封死这些漏洞,我们把原先简陋的容器执行方案全部废弃,基于 Linux 内核底层机制重构了一套轻量级代码沙箱执行器。这套架构建立在五道硬性防线之上:
- 第一道防线:只读根文件系统(Read-Only Rootfs)。整个沙箱的根镜像以只读模式挂载,禁止修改系统目录下的任何二进制文件。仅挂载一个大小严格受限(比如 64MB)的独立内存文件系统(tmpfs)作为当前工作区,执行完毕立即彻底卸载销毁。
- 第二道防线:剥离所有 Linux Capabilities(Drop ALL)。强制使用普通用户(UID 10001)运行,并且在进程启动时直接放弃全部 41 种内核 Capabilities,彻底杜绝 CAP_SYS_ADMIN、CAP_NET_RAW 等特权,封死提权可能。
- 第三道防线:网络物理切断(No-Network Namespace)。沙箱容器独享一个没有任何网络接口的 Network Namespace,除了回环设备(lo)以外不配置任何虚拟网卡,哪怕大模型写出天花乱坠的 socket 扫描代码,也会在握手阶段被内核直接返回 Network unreachable。
- 第四道防线:cgroups v2 严格配额。为单次执行强制绑定独立 cgroup:CPU 限制为 0.5 核,内存限制为 256MB(触发立即 OOMKilled),PIDs 限制为 32(防止 Fork 炸弹打爆系统)。
- 第五道防线:内核调用白名单(SECCOMP 过滤)。禁用 ptrace、mount、bpf、sys_chroot 等高危系统调用。
Go 1.27.1 实现的沙箱进程调度执行器
下面是我们用 Go 1.27.1 编写的核心沙箱调度执行引擎。它利用 Go 底层系统调用参数,结合 context 超时控制,实现了确定性的沙箱生命周期管理。
package sandbox
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"syscall"
"time"
)
var (
ErrSandboxTimeout = errors.New("sandbox: execution timed out")
ErrResourceExceed = errors.New("sandbox: resource quota exceeded")
)
// ExecutionConfig 沙箱执行参数约束
type ExecutionConfig struct {
Timeout time.Duration
MemoryLimitMB int64
MaxOutputSize int64
ScriptContent string
}
// ExecutionResult 执行结果与审计信息
type ExecutionResult struct {
ExitCode int
Stdout string
Stderr string
Duration time.Duration
}
// SandboxExecutor 沙箱调度执行器
type SandboxExecutor struct {
baseSandboxImage string
}
func NewSandboxExecutor(baseImage string) *SandboxExecutor {
return &SandboxExecutor{
baseSandboxImage: baseImage,
}
}
// ExecutePythonSafe 在高强度隔离环境下执行未知 Python 脚本
func (s *SandboxExecutor) ExecutePythonSafe(ctx context.Context, cfg ExecutionConfig) (*ExecutionResult, error) {
// 1. 创建临时的只读工作目录挂载点
scratchDir, err := os.MkdirTemp("", "mcp_sandbox_*")
if err != nil {
return nil, fmt.Errorf("failed to create sandbox scratch dir: %w", err)
}
defer os.RemoveAll(scratchDir)
scriptPath := filepath.Join(scratchDir, "main.py")
if err := os.WriteFile(scriptPath, []byte(cfg.ScriptContent), 0400); err != nil {
return nil, fmt.Errorf("failed to write script: %w", err)
}
// 2. 派生带强制超时控制的 Context
execCtx, cancel := context.WithTimeout(ctx, cfg.Timeout)
defer cancel()
// 3. 构造极其苛刻的底层容器运行参数
// 核心:–read-only –network none –cap-drop ALL –pids-limit 32 –memory 256m
cmdArgs := []string{
"run", "–rm",
"–name", fmt.Sprintf("sbx-%d", time.Now().UnixNano()),
"–network", "none",
"–read-only",
"–cap-drop", "ALL",
"–security-opt", "no-new-privileges",
"–pids-limit", "32",
"–cpus", "0.5",
"–memory", fmt.Sprintf("%dm", cfg.MemoryLimitMB),
"–tmpfs", "/tmp:rw,noexec,nosuid,size=32m",
"-v", fmt.Sprintf("%s:/app:ro", scratchDir),
"-w", "/app",
"–user", "10001:10001",
s.baseSandboxImage,
"python3", "main.py",
}
cmd := exec.CommandContext(execCtx, "podman", cmdArgs…)
// 4. 利用 syscall 特性剥离进程组,防止主进程退出后遗留僵尸子进程
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
startTime := time.Now()
output, err := cmd.CombinedOutput()
duration := time.Since(startTime)
if execCtx.Err() == context.DeadlineExceeded {
return nil, ErrSandboxTimeout
}
exitCode := 0
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
exitCode = exitErr.ExitCode()
} else {
return nil, fmt.Errorf("sandbox system failure: %w", err)
}
}
// 截断输出,防止大模型脚本狂打日志吃满网关内存
trimmedOutput := string(output)
if int64(len(trimmedOutput)) > cfg.MaxOutputSize {
trimmedOutput = trimmedOutput[:cfg.MaxOutputSize] + "\\n[OUTPUT TRUNCATED BY SANDBOX]"
}
return &ExecutionResult{
ExitCode: exitCode,
Stdout: trimmedOutput,
Duration: duration,
}, nil
}
踩坑复盘:临时文件写入与 Python 编译缓存
在落地这套极致防卫沙箱时,我们踩过两个非常隐蔽的工程坑:
第一个坑是 Python 字节码写入报错。因为我们把工作目录挂载为只读(:ro),而 Python 默认会在脚本同级目录下生成 __pycache__ 目录与 .pyc 编译文件。沙箱刚上线时,普通代码一跑就抛出 PermissionError: [Errno 30] Read-only file system。解决方法非常简单:在沙箱基础镜像中强制注入环境变量 PYTHONDONTWRITEBYTECODE=1,命令解释器彻底禁止在本地生成任何缓存文件,直接在内存中编译执行。
第二个坑是 数据分析库的临时文件依赖。业务方经常需要使用 matplotlib 和 pandas 导出图表。这些库在底层会尝试在 /tmp 创建字体缓存或临时分块文件。如果完全没有可写目录,脚本就会崩溃;但如果直接放开宿主机可写目录,又会带来逃逸风险。我们的解法是利用 Linux 的 –tmpfs 机制:给沙箱挂载一个挂载选项为 rw,noexec,nosuid,size=32m 的内存临时文件系统。允许它存取临时图表,但通过 noexec 明确禁止任何人在 /tmp 目录下执行可执行二进制或 Shell 脚本,杜绝恶意脚本下载第三方木马并在临时目录执行。
总结
在智能体开发中,功能往往走在安全前面,但安全才是一票否决权。不要相信大模型的“向善”对齐,更不要相信用户的输入是无害的。将所有外部生成的代码视作攻击载荷,利用 Linux 内核最坚固的 Namespaces、cgroups 与只读文件系统为它焊死铁笼,是保障生产系统夜夜能睡安稳觉的唯一工程底线。




