---
id: 83fb2c108243a7df80db3a6e78290daf
author: Lemon Mint
title: 在 gVisor 内部及嵌套沙箱中使用 gvx 运行 Docker-in-Docker
description: 为什么在 gVisor 容器内安装 docker.io 或嵌套运行 runsc 会因 modprobe、iptables-nft、overlay2、cgroups 和 seccomp EBUSY 而失败,以及 gvx 如何在不使用 --privileged 的情况下,于隔离的用户空间内核中安全地运行嵌套的 docker:dind。
language: zh
date: 2026-11-06T12:02:55.931762327Z
path: /blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03
go_package: z.gfire.dev/gvx
go_repourl: https://github.com/gfire-sigs/gvx.git
category: lowlevel
---

## 快速解决方案：在无特权的 `gVisor`（`runsc`）沙箱中安全运行 `docker:dind`

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

借助 **[gvx](https://github.com/gfire-sigs/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` 用户态内核。

```sh
# 在现有 gVisor (runsc) 容器或 Kubernetes Pod 内：
# 从经 SLSA 认证的 GitHub 发布归档或通过 go install 安装 rungv：
CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest

mkdir -p ~/.gvx
rungv run \
  -p unix:~/.gvx/docker.sock:2375 \
  -v gvx-docker:/var/lib/docker \
  -e DOCKER_TLS_CERTDIR= \
  docker:dind --tls=false &

# 针对内层 gvx 套接字运行标准 Docker 构建和容器
export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
until docker info >/dev/null 2>&1; do sleep 0.5; done
docker info --format '{{.Driver}} {{.OperatingSystem}}'
# 输出：overlayfs Alpine Linux ... (containerized)

docker build -t nested-app:test .
docker 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` 目录并不存在：

```text
modprobe: FATAL: Module overlay not found in directory /lib/modules/4.4.0
modprobe: FATAL: Module br_netfilter not found in directory /lib/modules/4.4.0
modprobe: FATAL: Module ip_tables not found in directory /lib/modules/4.4.0
modprobe: 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` 网桥时崩溃：

```text
failed to start daemon: Error initializing network controller: error obtaining controller instance: failed to create NAT chain DOCKER: iptables failed: iptables -t nat -N DOCKER
iptables v1.8.10 (nf_tables): Could not fetch rule set generation id: Invalid argument
iptables v1.8.10 (legacy): can't initialize iptables table 'nat': Table does not exist (do you need to insmod?)
iptables: No chain/target/match by that name.
Unable 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.)
Error 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` 会失败：

```text
level=error msg="[graphdriver] prior storage driver overlay2 failed: driver not supported"
failed to mount overlay: invalid argument
Error 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 过滤器时失败：

```text
docker: 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:
error 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
cgroups: cgroup mountpoint does not exist: unknown
runc create failed: systemd not running on this host: cannot use systemd cgroup manager
error during container init: error mounting "proc" to rootfs at "/proc": mount proc:/proc (via /proc/self/fd/6), flags: 0xe: operation not permitted
error during container init: error loading seccomp filter into kernel: operation not permitted
```

### 5. 嵌套 `runsc` 因 `SECCOMP_FILTER_FLAG_NEW_LISTENER`（`EBUSY`）而失败

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

```text
creating 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 中的嵌套执行](/assets/images/gvx/gvx-nested-seccomp-futex.png)

### 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()`：

```go
// pkg/sentry/platform/systrap/seccomp_notify_xplat.go
var seccompNotifyBusy atomic.Bool

func (t *syscallThread) installSeccompNotifyIfFree() (*os.File, error) {
	if seccompNotifyBusy.Load() {
		return nil, nil
	}
	f, err := t.installSeccompNotify()
	if errors.Is(err, unix.EBUSY) {
		if seccompNotifyBusy.CompareAndSwap(false, true) {
			log.Warningf("Another seccomp listener exists in the filter chain of the host process. " +
				"The stub threads use the slower futex loop instead of seccomp-unotify.")
		}
		return nil, nil
	}
	return f, err
}
```

当 `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 系统调用：

```go
// pkg/sentry/platform/systrap/syscall_thread.go
// 通知 syscall 线程有新的系统调用请求。
atomic.AddUint32(&sentryMsg.state, 1)
futexWakeUint32(&sentryMsg.state)

// 等待回复。
futexWaitWake(&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`）：

```sh
# 在外层 gVisor (runsc) 容器或外层 rungv 容器内：
mkdir -p ~/.gvx
rungv run \
  -p unix:~/.gvx/docker.sock:2375 \
  -v gvx-docker:/var/lib/docker \
  -e DOCKER_TLS_CERTDIR= \
  docker:dind --tls=false &
DIND_PID=$!

export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
until docker info >/dev/null 2>&1; do sleep 0.5; done

# 验证内层 Docker 守护进程使用的是原生 overlayfs（而非 vfs）
docker info --format 'Storage Driver: {{.Driver}}'
# 输出：Storage Driver: overlayfs

# 运行多层构建并执行嵌套容器
mkdir -p /tmp/nested-test && cd /tmp/nested-test
cat <<'EOF' > Dockerfile
FROM alpine:3.20
RUN adduser -D -u 1001 appuser && mkdir -p /app && chown appuser:appuser /app
USER appuser
WORKDIR /app
RUN echo "nested-build-ok" > status.txt
CMD ["cat", "/app/status.txt"]
EOF

docker build -t nested-test:latest .
docker run --rm nested-test:latest
# 输出：nested-build-ok

kill -INT $DIND_PID
```

---

## 本系列的延伸阅读

1. **[gvx：在一个无特权进程中运行 Linux 容器、Docker 与 Kubernetes](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**：概述 `gvx` 在 Windows（`whpx`、`systrap`）、macOS（`hvf`）和 Linux（`systrap`）上的运行情况。
2. **[无需 root 或用户命名空间运行 Docker、docker build 与 docker compose](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)**：介绍当 `root`、`sudo`、`unshare(CLONE_NEWUSER)`、`mount(2)` 和 `/dev/fuse` 均被阻止时，`gvx` 如何运行 `docker:dind`。
3. **[借助 gvx 在 gVisor 与嵌套沙箱中运行 Docker-in-Docker](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)**（本文）
4. **[面向 AI 智能体 RL 推演的快速隔离 Docker 环境（基于 gvx）](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)**：介绍如何使用 `gvx` 与单文件 `ext4lite` 写时复制快照，以 100 毫秒的重置时间并行运行数百个 `SWE-bench` 和 RL 推演环境。
