.agents/design/code-sandbox/python-isolated-runner.md
改造前,projects/code-sandbox 的 Python 执行路径使用长驻 PythonProcessPool
和 worker.py。同一个 Python 进程会多次执行用户代码,主要依赖
__import__ 白名单、受限 open、AST 检查、替换 builtin 等进程内限制。
GHSA-5jmh-5f2m-89jg 证明该模型存在结构性缺陷:Python 反射链可以绕过 进程内 denylist,并最终获得容器内 OS 命令执行能力。继续补 AST 或字符串 拦截只能覆盖已知 payload,不能作为多租户安全边界。
当前方案已经落地为 Python isolated one-shot warm pool:
worker.py 和 PythonProcessPool 已删除;/sandbox/python 统一使用 PythonIsolatedRunner;chroot、no_new_privs、seccomp、setgid/setuid;/sandbox/python API 兼容:成功返回 { success, data: { codeReturn, log } },失败返回 { success:false, message }。main()、main(variables)、main(a,b)、全局变量注入、print 收集、内置 helper。ProcessPool。POST /sandbox/python
-> queueIdLimiter.run(queueId, ...)
-> PythonIsolatedRunner.execute({ code, variables })
-> 获取干净的 one-shot 预热 Python 进程
-> 无空闲进程时按需 spawn
-> 预热进程已完成 chroot + no_new_privs + seccomp + setgid/setuid
-> bootstrap 注入 helper、variables、受限 builtins
-> exec 用户代码并调用 main
-> stdout 输出 JSON line result/http_request
-> 父进程解析结果、处理 HTTP 代理、监控超时/RSS/输出大小
-> 该 Python 进程销毁,不归还池中
-> 异步补充新的干净预热进程
/sandbox/modules 仍返回 env.SANDBOX_PYTHON_ALLOWED_MODULES,表示 Python
用户可直接 import 的白名单模块,不暴露底层 runner 类型。
src/isolated/python-isolated-runner.tsPythonIsolatedRunner 负责父进程侧调度和资源控制:
SANDBOX_POOL_SIZE 个空闲 Python 进程;init 协议,不执行用户代码;execute() 优先使用空闲预热进程,没有空闲进程时按需创建;Semaphore 控制 Python 任务最大并发,复用 SANDBOX_POOL_SIZE;http_request IPC,并由父进程统一执行网络请求。src/isolated/python-bootstrap.pypython-bootstrap.py 是 Python 子进程入口:
type:"init",完成 native 隔离后输出 type:"ready",再等待一条 task JSON;SystemHelper、system_helper、http_request、count_token、str_to_base64、create_hmac、delay;variables 和历史全局变量写法;open、危险属性拦截、audit hook;print 到内存 log,避免污染 stdout JSON line 协议;main() / main(variables) / main(a,b);result 或 http_request JSON line。native/python-sandboxGo shared library fastgpt_python_sandbox.so 负责 Linux native 隔离:
chroot 到固定 Python sandbox root;chdir("/");setgroups([]);setgid(65537) / setuid(65537);PR_SET_NO_NEW_PRIVS;execve/execveat、ptrace、mount 等不能落地。Python 隔离相关配置已经收敛为内部安全默认,不提供运行时环境变量关闭或改弱:
| 配置 | 当前值 | 说明 |
|---|---|---|
| Python 最大并发 | SANDBOX_POOL_SIZE | 复用现有进程池大小配置 |
| 预热空闲进程数 | SANDBOX_POOL_SIZE | 与 Python 最大并发保持一致 |
| 直接网络 syscall | false | 统一走父进程 http_request 代理 |
| chroot 根目录 | /tmp/fastgpt-python-sandbox | Docker 构建阶段准备 |
| 用户代码 uid/gid | 65537:65537 | native 初始化后降权 |
| native seccomp/chroot/setuid | Linux 固定开启 | 缺少 native 库或 chroot root 时 fail-closed |
保留的 Python 业务配置:
| 变量 | 说明 |
|---|---|
SANDBOX_PYTHON_ALLOWED_MODULES | 用户代码可直接 import 的 Python 模块白名单 |
SANDBOX_MAX_TIMEOUT | 单次执行最大超时 |
SANDBOX_MAX_MEMORY_MB | 子进程树 RSS 软限制 |
SANDBOX_MAX_OUTPUT_MB | stdout/log 输出上限 |
SANDBOX_REQUEST_* | 父进程 HTTP 代理的次数、超时、请求体和响应体限制 |
Python 子进程不允许直接使用网络 syscall。用户代码如需请求外部网络,必须调用:
http_request(url, method='GET', headers=None, body=None, timeout=None)
# 或
system_helper.http_request(...)
SystemHelper.httpRequest(...)
Python bootstrap 向父进程写出:
{
"type": "http_request",
"id": "http-1",
"payload": {
"url": "https://example.com",
"method": "GET",
"headers": {},
"body": null,
"timeout": null
}
}
父进程执行:
父进程再通过 stdin 返回:
{
"type": "http_response",
"id": "http-1",
"success": true,
"payload": {}
}
每个 Python 进程只执行一次任务,因此请求次数计数天然按单次执行归零。
语言层限制用于减少误用和拦截已知危险能力,但不是主安全边界:
__import__ 白名单;open 受限;eval / exec / compile / globals / locals / vars / dir 等禁用;__class__、__base__、__subclasses__、__globals__ 等危险属性拦截;object builtin 替换;os.system、subprocess、socket、ctypes 等事件;OS 层是多租户核心安全边界:
SANDBOX_MAX_MEMORY_MB + RUNTIME_MEMORY_OVERHEAD_MB 后杀进程树;SANDBOX_MAX_OUTPUT_MB 限制;killProcessTree() 处理,优先杀 descendant 和 process group。Docker 镜像使用 Debian bookworm/glibc。Alpine/musl 下 Go c-shared .so
存在兼容风险,不能作为当前 Python native isolation 运行基线。
构建流程:
SANDBOX_BUILD_NATIVE_PYTHON=true pnpm build 构建 Go shared library;build.sh 复制 python-bootstrap.py 和 fastgpt_python_sandbox.so 到 dist;/tmp/fastgpt-python-sandbox chroot root;main();main(variables);main(a,b);print log;__base__ / __subclasses__ 逃逸;getattr;os / sys / subprocess;os.system / subprocess / socket / ctypes;.so 加载;one-shot warm pool 的收益主要在低并发和空闲命中场景:
SANDBOX_POOL_SIZE 以内的首批请求,但每个进程执行一次后仍需销毁并补充,因此稳态吞吐仍不会接近旧长驻 pool;SANDBOX_POOL_SIZE=20 时,Python idle 进程 RSS 粗略约 374MB;实际容器 RSS 还包含 JS worker、Node 主进程、共享页和系统库统计口径。安全优先级高于短任务吞吐。如果未来需要继续优化,应优先评估:
/sandbox/python 只使用 PythonIsolatedRunner;.so 或 chroot root 时 fail-closed;pnpm --filter @fastgpt/code-sandbox exec tsc --noEmit
SANDBOX_MAX_MEMORY_MB=256 pnpm --filter @fastgpt/code-sandbox exec vitest run --coverage.enabled=false
pnpm --filter @fastgpt/code-sandbox build
docker build --build-arg proxy=1 -f projects/code-sandbox/Dockerfile -t fastgpt-code-sandbox-warm-pool .