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

快速解决方案:在无特权的 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会创建第二个嵌套的用户态 gVisorSentry内核,该内核拥有自身的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)。

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 中支持两种唤醒模式:
- 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)分发请求。 - 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
本系列的延伸阅读
- gvx:在一个无特权进程中运行 Linux 容器、Docker 与 Kubernetes:概述
gvx在 Windows(whpx、systrap)、macOS(hvf)和 Linux(systrap)上的运行情况。 - 无需 root 或用户命名空间运行 Docker、docker build 与 docker compose:介绍当
root、sudo、unshare(CLONE_NEWUSER)、mount(2)和/dev/fuse均被阻止时,gvx如何运行docker:dind。 - 借助 gvx 在 gVisor 与嵌套沙箱中运行 Docker-in-Docker(本文)
- 面向 AI 智能体 RL 推演的快速隔离 Docker 环境(基于 gvx):介绍如何使用
gvx与单文件ext4lite写时复制快照,以 100 毫秒的重置时间并行运行数百个SWE-bench和 RL 推演环境。