沙箱里跑段代码,竟然被「操作系统」当场拦下:一次 Docker 安全机制的踩坑实录
沙箱里跑段代码,竟然被「操作系统」当场拦下:一次 Docker 安全机制的踩坑实录
事情从一个再普通不过的下午开始:我在自己的 AI 工作流里调用 dify-sandbox 代码沙箱,想让它跑一小段 Python 脚本——就是那种每天要执行几百次、闭着眼睛都能跑通的操作。
然后屏幕上跳出了这行报错:
pthread_create failed: Operation not permitted/error: signal: aborted (core dumped)我当场愣住了。一段连 10 行都不到的代码,连个线程都建不起来?

一、案发现场:一条报错的完整旅行
先交代一下环境,方便大家对号入座:
- Docker 版本:26.1.4
- 宿主机:openEuler 24.03 (LTS-SP1)
- 部署方式:docker-compose 起了一套 AI 服务,里面包含 dify-sandbox 代码沙箱
compose 配置的核心部分长这样:
version: '3.8'
services:
dify-sandbox:
image: dify-sandbox:latest-arm64
# 拉取策略,docker compose build无关,pull always
pull_policy: always
restart: always
environment:
API_KEY: ""
GIN_MODE: "release"
WORKER_TIMEOUT: 300
ENABLE_NETWORK: true
SANDBOX_PORT: 8194
HTTP_PROXY: ""
HTTPS_PROXY: ""
CODE_MAX_STRING_LENGTH: 2000000
TEMPLATE_TRANSFORM_MAX_LENGTH: 2000000
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8194/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
volumes:
- ./dependencies:/dependencies
- ./config:/conf
# 容器内部端口,docker compose内部网络互通
expose:
- 8194
# 如果需要宿主机访问,打开下面端口映射
# ports:
# - "8194:8194"
# 优雅关闭超时,对应terminationGracePeriodSeconds:30
stop_grace_period: 30s
一条 docker-compose up -d 拉起服务,然后通过 dify-sandbox 的接口发起一次脚本执行——这是 AI Agent 里最常见的"代码执行"链路:Agent 想算个数、解析个文档、跑段验证脚本,都靠沙箱代劳。
结果就是开头那一幕:pthread_create failed,沙箱直接把执行进程干掉了,留下一个 core dump 和满脸问号的我。

二、第一轮排查:沙箱自己没毛病
第一反应当然是怀疑沙箱本身。把 dify-sandbox 的源码翻出来看,它执行 Python 脚本前会做一套相当标准的"自我保护":
func InitSeccomp(uid int, gid int, enable_network bool) error {
// 更改子进程目录(chroot,把视野关在自己的目录里)
err := syscall.Chroot(".")
// 切换根目录
err = syscall.Chdir("/")
// 禁止提权(NoNewPrivs)
lib.SetNoNewPrivs()
// 设置 seccomp BPF 过滤规则
err = lib.Seccomp(allowed_syscalls, allowed_not_kill_syscalls)
// 清空子进程用户组,并降权运行
err = syscall.Setgroups([]int{})
err = syscall.Setgid(gid)
err = syscall.Setuid(uid)
}
这里的关键是 Seccomp() 函数:它会加载一份系统调用白名单,白名单外的请求一律拒绝。
按理说,Python 建线程要用的 clone / clone3 系统调用,早就在沙箱的 allowed_syscalls 里放行了。那为什么还会被拦?
同一份镜像、同一份代码,在别的机器上是好的——CentOS 7 上正常,麒麟系统上也正常。变量不在镜像里,那就只能在"机器"上。
一层层往上追,最后在容器运行时这一层,我收到了 Docker 送的"大彩蛋"。
三、真凶:Docker 自带的默认 seccomp 名单
很多人不知道(包括当时的我):Docker 容器一启动,就会默认给容器里所有进程挂上一份内置的 seccomp 过滤规则。
这份默认 profile 藏在 Docker 的源码里,大约有 60 多条规则。也就是说,沙箱里的进程想跟内核打交道,要连过两道安检:
- 沙箱自己的 seccomp 白名单 —— 放行了
clone3; - Docker 的默认 seccomp profile —— 这一份,我们完全没管过。
两条规则是叠加生效的:两边都放行,系统调用才能真正到达内核。任何一边拦下,进程都会被终止。
而问题恰恰出在第二道:Docker 版本较旧、或与宿主机内核适配不佳时,默认 profile 会拦掉部分系统调用。我的 openEuler 24.03 内核比较新,新内核引入的调用路径在 Docker 那份"老名单"里没有对应条目——于是 Python 老老实实建线程,被门口的过滤器按住,直接 abort。

想验证也很简单,官方文档写得明明白白:
- Docker seccomp 安全文档:
docs.docker.com/engine/security/seccomp - Docker 默认 seccomp.json:
github.com/moby/profiles/blob/main/seccomp/default.json
四、补课:容器到底给我们"隔离"了什么
踩完这个坑,我觉得有必要把容器安全这件事从头捋一遍。因为每一个跑 AI 应用的人,其实都天天在用它,只是没意识到。
首先纠正一个常见误解:容器不是虚拟机。虚拟机是虚拟出一整套硬件、装一个完整的客户操作系统;而容器里跑的所有进程,共享的是宿主机的同一个 Linux 内核。
容器比虚拟机轻,代价就是:隔离不是"物理隔开",而是内核提供的几道"软围栏"。一共有四道,层层嵌套:

第一道:Namespace(命名空间)——让容器"看不见"别人。
给进程一套独立的视角:自己的进程表、网络栈、挂载点、用户 ID。容器 A 看不到容器 B 的进程,就像两个住在同一栋楼里、但互相不串门的邻居。
第二道:Cgroups(控制组)——让容器"吃不多"。
限定 CPU、内存、IO 的用量上限。这一道对 AI 场景尤其重要:Agent 一旦跑疯(比如死循环生成内容、无限递归调用工具),Cgroups 能保证它最多把分给自己的资源吃光,而不是把整台宿主机拖垮。
第三道:Capabilities(能力)——让容器"做不了大事"。
Linux 把 root 的巨大权力拆成了几十个小权限(挂载磁盘、修改网卡、加载内核模块……)。容器默认只拿其中一小部分,即使被攻破,也难以撼动宿主机。
第四道:Seccomp——让容器"说不了不该说的话"。
这就是本次翻车的主角,也是四道围栏里最贴近内核的一道,值得单独展开讲。
五、Seccomp:站在进程和内核之间的门卫
Seccomp(Secure Computing Mode)不是什么新东西,1992 年就进了 Linux 内核,比很多读者的年纪都大。如今容器里跑的是它的加强版:Seccomp-BPF 过滤器。
它的世界观很简单:进程对内核的任何请求,都要走"系统调用"这条唯一的窗口。读文件、开网络、建线程,全都是 syscall。而 Seccomp 就坐在这个窗口边上,手里拿着一份名单:
- 在名单里 → 放行(
ActAllow),请求正常送达内核; - 认识但不许做 → 返回一个错误码(
ActErrno),进程自己看着办; - 陌生面孔 → 直接终止进程(
ActKillProcess),连解释的机会都没有。

注意一个反直觉的点:Seccomp 是"白名单"逻辑。它不是"禁止什么",而是"只允许什么"。
白名单逻辑有个天然的坑:内核每年都会新增系统调用,新内核 + 老名单 = 好代码被误伤。这次的 pthread_create failed 就是典型——代码没问题,Python 没问题,只是"递条子"这个动作本身不在名单上。
而沙箱场景下更复杂一层:你的进程前面站着两个门卫——沙箱自己的名单和 Docker 的默认名单,两个都得点头。
六、为什么"在我机器上是好的"不算数
这次踩坑最魔幻的地方在于:同一个 dify-sandbox 镜像、同一份 Python 代码,在不同宿主机上表现完全不同。
| 宿主机环境 | 内核 | 结果 |
|---|---|---|
| CentOS 7(x86 / ARM) | 3.10.x | 正常运行 |
| 麒麟 V10(aarch64) | 4.19.90-52.57.v2207.ky10 | 正常运行 |
| openEuler 24.03 (LTS-SP1) | 6.x | 报错,线程创建失败 |
原因就是前面说的组合效应:内核版本 × Docker 版本 × 默认 seccomp profile,三者共同决定了哪些系统调用能活着走到内核。老名单配上新内核,就会出"适配差"。

所以容器化时代有一句话要改改:不是"在我机器上是好的",而是——容器化的东西,换宿主机、换内核、升级 Docker 之后,都得重新跑一遍关键链路。镜像不变,不代表行为不变。
七、为什么 AI 时代,这事离你更近了
如果只是传统后端,这个坑可能一年踩不到一次。但 AI 应用的形态变了:
AI Agent 的标配架构就是"沙箱里跑代码"。 Agent 需要执行用户提交的脚本、做数据计算、调用各种工具(MCP 工具很多也跑在容器里),厂商的标准答案都是:扔进 Docker 沙箱隔离执行。
于是 seccomp 这类"名字都没听过"的机制,成了 AI 应用的日常依赖。而且和传统场景不同:
- AI 工作负载的系统调用模式更复杂。推理服务、模型加载、多线程并发,对内核的调用密度和种类都远超一个普通 Web 服务;
- 沙箱层层嵌套。Docker 一层 seccomp,沙箱框架(dify-sandbox、gVisor、Firecracker 等)自己又一层,任何一层名单没对齐都会翻车;
- 用户不可控环境更多。同一套 Agent 应用,可能部署在客户的 CentOS、麒麟、openEuler 上——国密和国产化浪潮下,内核生态比以前多样化得多。
一句话:跑 AI 应用的人,多少都得懂点容器安全,至少要知道"门卫"的存在。
八、解决方案:三步收尾
第一步:给容器换上自定义 seccomp 名单
思路是:以 Docker 官方默认 profile 为底子,按需放行缺失的系统调用,然后在 compose 里显式挂载。
# 1. 拷一份官方默认配置当底子
curl -LO https://github.com/moby/profiles/raw/main/seccomp/default.json
在 JSON 里为需要的系统调用补充放行规则:
{
"names": ["clone3"],
"action": "SCMP_ACT_ALLOW",
"args": []
}
然后在 docker-compose.yaml 中启用:
services:
mineru-api:
# ...原有配置...
security_opt:
- seccomp:./seccomp.json
重新 docker-compose up -d,沙箱里的 pthread_create 立刻恢复正常。问题解决。
第二步:确认版本组合
长期看,更省心的做法是把 Docker 升级到与宿主机内核匹配的版本。内核太新 + Docker 太旧,是这类"玄学问题"的高发组合。
应急选项(仅排障用)
--security-opt seccomp=unconfined
这条命令会彻底关掉 seccomp 过滤,能让服务先跑起来。但只建议在临时排障时用——它等于把最里面那道围栏拆了,生产环境千万别裸奔。
九、写在最后:一份容器安全清单
这次踩坑最大的收获,是把"安全"这件事从抽象名词变成了具体的手感。整理成五条清单,送给同样在跑 AI 应用的你:

- 镜像要干净:用官方基础镜像做最小化安装,别把调试工具箱全塞进去——镜像里的每个工具,都可能变成攻击者的武器;
- 权限要最小:容器用非 root 用户运行,按需给 capability,坚决不开
--privileged; - 名单要显式:跑 AI 沙箱 / MCP 服务时,自己维护 seccomp profile,不要赌默认值适配一切内核;
- 资源要限额:用
cpus/mem_limit把笼子做实,防止 Agent 失控拖垮宿主机; - 换环境要回归:换内核、升 Docker 后重跑关键链路。“在我机器上是好的”,在容器时代不算数。
最后想说的是:这次报错让我对"安全"有了新的理解——
安全不是把门焊死,而是把边界画清楚。 Docker 默认给的那道围栏,平时你看不见它;只有当你真的需要穿过它的时候,才知道它一直在那里,忠实地执行着自己的职责。我们要做的,不是绕过它,而是学会和它谈判。
参考链接:
- Docker Seccomp 安全文档:https://docs.docker.com/engine/security/seccomp/
- Docker 默认 seccomp profile:https://github.com/moby/profiles/blob/main/seccomp/default.json
- dify-sandbox:https://github.com/langgenius/dify-sandbox
文末互动:你在工作学习中是否接触过知识图谱、GraphRAG 相关应用?欢迎关注同名公众号“QuietJiang”!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)