沙箱里跑段代码,竟然被「操作系统」当场拦下:一次 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 多条规则。也就是说,沙箱里的进程想跟内核打交道,要连过两道安检:

  1. 沙箱自己的 seccomp 白名单 —— 放行了 clone3
  2. 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 应用的日常依赖。而且和传统场景不同:

  1. AI 工作负载的系统调用模式更复杂。推理服务、模型加载、多线程并发,对内核的调用密度和种类都远超一个普通 Web 服务;
  2. 沙箱层层嵌套。Docker 一层 seccomp,沙箱框架(dify-sandbox、gVisor、Firecracker 等)自己又一层,任何一层名单没对齐都会翻车;
  3. 用户不可控环境更多。同一套 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 应用的你:

在这里插入图片描述

  1. 镜像要干净:用官方基础镜像做最小化安装,别把调试工具箱全塞进去——镜像里的每个工具,都可能变成攻击者的武器;
  2. 权限要最小:容器用非 root 用户运行,按需给 capability,坚决不开 --privileged
  3. 名单要显式:跑 AI 沙箱 / MCP 服务时,自己维护 seccomp profile,不要赌默认值适配一切内核;
  4. 资源要限额:用 cpus / mem_limit 把笼子做实,防止 Agent 失控拖垮宿主机;
  5. 换环境要回归:换内核、升 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”!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐