GoSuda

在 gVisor 内部及嵌套沙箱中使用 gvx 运行 Docker-in-Docker

By Lemon Mint views ...

快速解决方案:在无特权的 gVisor(runsc)沙箱中安全运行 docker:dind

云端 AI 智能体平台、Kubernetes agent-sandbox 部署(RuntimeClass: gvisor)以及强化学习评估框架,会将不可信工作负载隔离在 gVisor(runsc) 容器中,以专门保护宿主内核。当默认的无特权 runsc 容器内的工作负载尝试通过 apt-get 安装 docker.io 或 docker-ce 并启动 dockerd 时,即使其在容器内拥有 root(UID 0)权限,操作仍会失败。

借助 gvx(rungv),你可以安全地直接在无特权的 gVisor(runsc)容器内运行 docker:dind、docker build 和 docker compose(或将 gvx 嵌套于 gvx 之中),且无需削弱外层 RuntimeClass、暴露宿主套接字或授予 --privileged 权限。

保持并强化外层沙箱隔离:在 runsc 内运行 rungv 不会绕过外层 gVisor 沙箱实施的任何限制。相反,gvx 会创建第二个嵌套的用户态 gVisor Sentry 内核,该内核拥有自身的 hostlockdown(PR_SET_NO_NEW_PRIVS + seccomp)、自身的内存内 netstack,以及单文件 ext4lite 卷。内层 dockerd 与 runc 容器经过双重沙箱隔离:其系统调用、挂载操作和原始数据包均不会离开内层 gvx 用户态内核。

 1# 在现有 gVisor (runsc) 容器或 Kubernetes Pod 内:
 2# 从经 SLSA 认证的 GitHub 发布归档或通过 go install 安装 rungv:
 3CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest
 4
 5mkdir -p ~/.gvx
 6rungv run \
 7  -p unix:~/.gvx/docker.sock:2375 \
 8  -v gvx-docker:/var/lib/docker \
 9  -e DOCKER_TLS_CERTDIR= \
10  docker:dind --tls=false &
11
12# 针对内层 gvx 套接字运行标准 Docker 构建和容器
13export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
14until docker info >/dev/null 2>&1; do sleep 0.5; done
15docker info --format '{{.Driver}} {{.OperatingSystem}}'
16# 输出:overlayfs Alpine Linux ... (containerized)
17
18docker build -t nested-app:test .
19docker run --rm nested-app:test

在 gVisor(runsc)内直接安装 Docker 失败的五个原因

当用户或 AI 编程智能体在标准 runsc 容器内运行 apt-get install -y docker.io(或 docker-ce)并尝试启动 dockerd 时,会依次遇到五个技术障碍。

1. 针对虚拟内核 4.4.0 的内核模块加载(modprobe)

在软件包安装和 dockerd 启动期间,初始化脚本会运行 modprobe 以加载 overlay、br_netfilter、ip_tables、nf_nat 和 xt_conntrack。外层 gVisor Sentry 会拦截 init_module(2) 和 finit_module(2),并报告一个虚拟内核版本(4.4.0),而 /lib/modules/4.4.0 目录并不存在:

1modprobe: FATAL: Module overlay not found in directory /lib/modules/4.4.0
2modprobe: FATAL: Module br_netfilter not found in directory /lib/modules/4.4.0
3modprobe: FATAL: Module ip_tables not found in directory /lib/modules/4.4.0
4modprobe: FATAL: Module nf_nat not found in directory /lib/modules/4.4.0

2. iptables-nft(nf_tables Netlink)与 docker0 网桥故障

在 Ubuntu 22.04、Ubuntu 24.04 以及 Debian 11/12 上,apt-get install docker.io 会将 iptables-nft(由 nf_tables netlink 子系统支撑)选为默认的 /usr/sbin/iptables 候选项。在缺乏网络管理权限或 nf_tables netlink 支持的标准 gVisor 容器中,dockerd 在初始化 DOCKER NAT 链和 docker0 网桥时崩溃:

1failed to start daemon: Error initializing network controller: error obtaining controller instance: failed to create NAT chain DOCKER: iptables failed: iptables -t nat -N DOCKER
2iptables v1.8.10 (nf_tables): Could not fetch rule set generation id: Invalid argument
3iptables v1.8.10 (legacy): can't initialize iptables table 'nat': Table does not exist (do you need to insmod?)
4iptables: No chain/target/match by that name.
5Unable to enable SKIP DNAT rule: (iptables failed: iptables --wait -t nat -I DOCKER -i docker0 -j RETURN: iptables: No chain/target/match by that name.)
6Error initializing network controller: error obtaining controller instance: failed to create bridge "docker0": operation not permitted

3. gofer / 9p 根文件系统上的 overlay2 故障与 vfs 磁盘耗尽

除非外层 runsc 管理员已明确预先配置了位于 /var/lib/docker 的专用 tmpfs 或 filestore 挂载,否则容器根文件系统由 gVisor 的 gofer(9p/lisafs)或嵌套的 overlay 挂载支撑。在 /var/lib/docker 上挂载 overlay2 会失败:

1level=error msg="[graphdriver] prior storage driver overlay2 failed: driver not supported"
2failed to mount overlay: invalid argument
3Error starting daemon: error initializing graphdriver: driver not supported: overlay2

当用户或智能体试图通过启动 dockerd --storage-driver=vfs 来绕过此错误时,Docker 会完全禁用写时复制(Copy-on-Write)层共享。Dockerfile 中的每条 RUN 指令都会对整个根文件系统执行完整的递归复制(dirCopy),使一个 500 MB 的镜像在 10 个构建步骤中放大为 5 GB 的写入量,最终导致沙箱因 no space left on device 或 I/O 超时而终止。

4. 嵌套 runc 的 Cgroup 与 /proc 挂载被拒绝

即使 dockerd 被强制以 dockerd --iptables=false --bridge=none --storage-driver=vfs 启动,运行 docker run 或 docker build 仍会调用 runc 来创建内层容器。在默认的无特权 gVisor 容器中,runc 在挂载 cgroup、重新挂载 /proc 或加载内层 seccomp 过滤器时失败:

1docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process:
2error during container init: error mounting "cgroup" to rootfs at "/sys/fs/cgroup": mount cgroup:/sys/fs/cgroup/cpuset (via /proc/self/fd/6), flags: 0xf, data: cpuset: operation not permitted
3cgroups: cgroup mountpoint does not exist: unknown
4runc create failed: systemd not running on this host: cannot use systemd cgroup manager
5error during container init: error mounting "proc" to rootfs at "/proc": mount proc:/proc (via /proc/self/fd/6), flags: 0xe: operation not permitted
6error during container init: error loading seccomp filter into kernel: operation not permitted

5. 嵌套 runsc 因 SECCOMP_FILTER_FLAG_NEW_LISTENER(EBUSY)而失败

如果用户尝试在 runsc 内使用 systrap 平台运行另一个 runsc 实例,初始化会立即中止:

1creating subprocess: sysmsg: failed to initialize thread: failed to install seccomp notify rules: device or resource busy

Linux 内核和 gVisor Sentry 都只允许每个任务层级中至多一个 SECCOMP_FILTER_FLAG_NEW_LISTENER(SECCOMP_RET_USER_NOTIF)监听器。由于外层沙箱已占用该监听器槽位,内层 seccomp(2) 调用返回 EBUSY(device or resource busy)。


gvx 如何解决 gVisor 内的嵌套执行

gvx 在其自身的用户态内核和平台层中解决了全部五个障碍,使 rungv 能够在外层 gVisor(runsc)容器内运行,甚至可以递归地在另一个 gvx 容器内运行(即 gvx 嵌套于 gvx)。

gvx 在外层 gVisor 中的嵌套执行

1. 从 EBUSY 自动回退到 _RUN_SYSCALL_LOOP Futex 循环

在 Linux systrap 中,每个 stub 子进程运行两类线程:

  • 客户执行线程(sysmsg):通过 SECCOMP_RET_TRAP(SIGSYS)以及经二进制修补的 jmp rel32 跳板(syshandler)捕获客户系统调用,并通过共享内存和 SYS_FUTEX 与 Sentry 交换寄存器状态。
  • stub syscallThread(syscall_thread.go):代表 Sentry 在 stub 子进程内执行地址空间相关的系统调用(mmap、munmap、clone)。

在上游 gVisor 中,syscallThread 在 stub_amd64.s 和 stub_arm64.s 中支持两种唤醒模式:

  1. Seccomp User-Notify 循环(_RUN_SECCOMP_LOOP = 6,当 t.seccompNotify != nil 时):在 syscallThread.init() 期间,t.installSeccompNotify() 会安装一个 SECCOMP_RET_USER_NOTIF 监听器(seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)),使 Sentry 能够通过 ioctl(SECCOMP_IOCTL_NOTIF_SEND) 和 ioctl(SECCOMP_IOCTL_NOTIF_RECV) 分发请求。
  2. Futex 系统调用循环(_RUN_SYSCALL_LOOP = 5,当 t.seccompNotify == nil 时):syscallThread 运行 syscall_loop(stub_amd64.s / stub_arm64.s),在共享的 2 页 syscallSentryMessage(stub 中为 PROT_READ)和 syscallStubMessage(stub 中为 PROT_READ | PROT_WRITE)区域上通过 SYS_FUTEX(FUTEX_WAIT)休眠。

只要 seccompNotifyIsSupported 为真,上游 gVisor 就会无条件调用 t.installSeccompNotify(),并且当 seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...) 返回 EBUSY 时,会中止 stub 的创建。

在 gvx(pkg/sentry/platform/systrap/seccomp_notify_xplat.go 与 syscall_thread.go)中,syscallThread.init() 调用 t.installSeccompNotifyIfFree():

 1// pkg/sentry/platform/systrap/seccomp_notify_xplat.go
 2var seccompNotifyBusy atomic.Bool
 3
 4func (t *syscallThread) installSeccompNotifyIfFree() (*os.File, error) {
 5	if seccompNotifyBusy.Load() {
 6		return nil, nil
 7	}
 8	f, err := t.installSeccompNotify()
 9	if errors.Is(err, unix.EBUSY) {
10		if seccompNotifyBusy.CompareAndSwap(false, true) {
11			log.Warningf("Another seccomp listener exists in the filter chain of the host process. " +
12				"The stub threads use the slower futex loop instead of seccomp-unotify.")
13		}
14		return nil, nil
15	}
16	return f, err
17}

当 installSeccompNotify() 因外层 gVisor(runsc)或外层 gvx 沙箱已占用进程过滤器链中唯一的监听器槽位而返回 unix.EBUSY 时:

  • installSeccompNotifyIfFree() 会设置 seccompNotifyBusy(atomic.Bool),为整个进程记录一条警告日志,并使该 stub 及之后所有 stub 的 t.seccompNotify 保持为 nil。
  • 在 syscallThread.detach()(syscall_thread_amd64.go 与 syscall_thread_arm64.go)中,当 t.seccompNotify == nil 时,会在 amd64 上设置 regs.Rbx = _RUN_SYSCALL_LOOP(5),或在 arm64 上设置 regs.Regs[9] = _RUN_SYSCALL_LOOP,而不是 _RUN_SECCOMP_LOOP(6)。
  • 在 syscallThread.syscall()(syscall_thread.go)中,内层 Sentry 仅通过共享内存和 SYS_FUTEX 分发 stub 系统调用:
1// pkg/sentry/platform/systrap/syscall_thread.go
2// 通知 syscall 线程有新的系统调用请求。
3atomic.AddUint32(&sentryMsg.state, 1)
4futexWakeUint32(&sentryMsg.state)
5
6// 等待回复。
7futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))

由于 SECCOMP_RET_TRAP 过滤器可以任意深度地作用于客户线程栈,且 syscallThread 在不申请第二个用户通知监听器的情况下回退到 _RUN_SYSCALL_LOOP,因此 gvx 能够在 runsc 内干净地运行,并支持递归嵌套(即 gvx 嵌套于 gvx)。

2. 基于单文件 ext4lite 卷的原生 overlayfs

为避免在外层 gVisor 容器的 gofer(9p/lisafs)根文件系统之上挂载 overlayfs,-v gvx-docker:/var/lib/docker 使用 gvx 的内部 ext4lite 文件系统来支撑 /var/lib/docker,该文件系统存储于单个普通文件(gvx-docker.ext4)中。

对外层 runsc 而言,gvx-docker.ext4 只是一个普通文件,通过 pread64(2) 和 pwrite64(2) 访问。在 gvx 内部,ext4lite 提供了带 JBD2 日志的 ext4 文件系统,支持 trusted.overlay.* 与 security.capability 扩展属性、字符设备型 whiteout 节点(0:0)、d_type 目录项以及 RENAME_EXCHANGE,从而使内层 dockerd 能够使用其原生的 overlayfs 存储驱动。

3. 带有 docker0 与 iptables-legacy 的自包含 netstack

在 gvx 内部,dockerd 不与外层 runsc 容器的网络接口或 netfilter 表交互。相反:

  • dockerd 在 gvx 自身的用户态 netstack 内创建其 docker0 网桥和 veth 对。
  • 所有 iptables-legacy 规则(-t nat -N DOCKER、MASQUERADE、conntrack、addrtype)均在 gvx 内部的 netfilter 实现上执行。
  • 嵌套的 runc 容器在 gvx 内存中的 VFS2 挂载表内挂载其 /proc、/sys 和 /sys/fs/cgroup 层级,从不调用外层 runsc 的 mount(2) 系统调用。

在 gVisor(或 gvx 嵌套于 gvx)内验证嵌套 docker:dind

你可以使用以下命令验证两种场景(在 runsc 内运行 rungv,或在 rungv 内运行 rungv):

 1# 在外层 gVisor (runsc) 容器或外层 rungv 容器内:
 2mkdir -p ~/.gvx
 3rungv run \
 4  -p unix:~/.gvx/docker.sock:2375 \
 5  -v gvx-docker:/var/lib/docker \
 6  -e DOCKER_TLS_CERTDIR= \
 7  docker:dind --tls=false &
 8DIND_PID=$!
 9
10export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
11until docker info >/dev/null 2>&1; do sleep 0.5; done
12
13# 验证内层 Docker 守护进程使用的是原生 overlayfs(而非 vfs)
14docker info --format 'Storage Driver: {{.Driver}}'
15# 输出:Storage Driver: overlayfs
16
17# 运行多层构建并执行嵌套容器
18mkdir -p /tmp/nested-test && cd /tmp/nested-test
19cat <<'EOF' > Dockerfile
20FROM alpine:3.20
21RUN adduser -D -u 1001 appuser && mkdir -p /app && chown appuser:appuser /app
22USER appuser
23WORKDIR /app
24RUN echo "nested-build-ok" > status.txt
25CMD ["cat", "/app/status.txt"]
26EOF
27
28docker build -t nested-test:latest .
29docker run --rm nested-test:latest
30# 输出:nested-build-ok
31
32kill -INT $DIND_PID

本系列的延伸阅读

  1. gvx:在一个无特权进程中运行 Linux 容器、Docker 与 Kubernetes:概述 gvx 在 Windows(whpx、systrap)、macOS(hvf)和 Linux(systrap)上的运行情况。
  2. 无需 root 或用户命名空间运行 Docker、docker build 与 docker compose:介绍当 root、sudo、unshare(CLONE_NEWUSER)、mount(2) 和 /dev/fuse 均被阻止时,gvx 如何运行 docker:dind。
  3. 借助 gvx 在 gVisor 与嵌套沙箱中运行 Docker-in-Docker(本文)
  4. 面向 AI 智能体 RL 推演的快速隔离 Docker 环境(基于 gvx):介绍如何使用 gvx 与单文件 ext4lite 写时复制快照,以 100 毫秒的重置时间并行运行数百个 SWE-bench 和 RL 推演环境。