---
id: 83fb2c108243a7df80db3a6e78290daf
author: Lemon Mint
title: Executando Docker-in-Docker Dentro do gVisor e de Sandboxes Aninhados com gvx
description: Por que a instalação do docker.io ou o aninhamento do runsc dentro de um contêiner gVisor falha em modprobe, iptables-nft, overlay2, cgroups e seccomp (EBUSY), e como o gvx executa com segurança um docker:dind aninhado dentro de um kernel isolado em espaço de usuário, sem --privileged.
language: pt
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
---

## Solução Rápida: Executando `docker:dind` de Forma Segura Dentro de um Sandbox `gVisor` (`runsc`) Sem Privilégios

Plataformas de agentes de IA em nuvem, implantações `agent-sandbox` no Kubernetes (`RuntimeClass: gvisor`) e harnesses de avaliação de aprendizado por reforço isolam cargas de trabalho não confiáveis em contêineres **gVisor (`runsc`)** especificamente para proteger o kernel do hospedeiro. Quando uma carga de trabalho dentro de um contêiner `runsc` padrão, sem privilégios, tenta instalar `docker.io` ou `docker-ce` via `apt-get` e iniciar o `dockerd`, a operação falha, mesmo que o processo seja `root` (`UID 0`) dentro do contêiner.

Com o **[gvx](https://github.com/gfire-sigs/gvx)** (`rungv`), é possível executar **de forma segura** `docker:dind`, `docker build` e `docker compose` diretamente dentro de um contêiner gVisor (`runsc`) sem privilégios (ou aninhar `gvx` dentro de `gvx`), **sem enfraquecer o `RuntimeClass` externo, expor sockets do hospedeiro ou conceder `--privileged`**.

> **Preservação e Fortalecimento do Isolamento do Sandbox Externo**: Executar `rungv` dentro de `runsc` não contorna nenhuma restrição imposta pelo sandbox gVisor externo. Em vez disso, o `gvx` cria um segundo kernel gVisor `Sentry` aninhado, em espaço do usuário, com seu próprio `hostlockdown` (`PR_SET_NO_NEW_PRIVS` + `seccomp`), seu próprio `netstack` em memória e um volume `ext4lite` de arquivo único. Os `dockerd` e os contêineres `runc` internos ficam em sandbox duplo: suas chamadas de sistema, montagens e pacotes brutos nunca saem do kernel do `gvx` interno, em espaço do usuário.

```sh
# Dentro de um contêiner gVisor (runsc) existente ou de um Pod do Kubernetes:
# Instale o rungv a partir de um arquivo de release do GitHub com atestação SLSA ou via go install:
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 &

# Execute builds e contêineres Docker padrão contra o socket interno do gvx
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}}'
# Saída: overlayfs Alpine Linux ... (containerized)

docker build -t nested-app:test .
docker run --rm nested-app:test
```

---

## Cinco Motivos pelos Quais a Instalação Direta do Docker Falha Dentro do `gVisor` (`runsc`)

Quando um usuário ou um agente de codificação com IA executa `apt-get install -y docker.io` (ou `docker-ce`) dentro de um contêiner `runsc` padrão e tenta iniciar o `dockerd`, ele encontra cinco barreiras técnicas sequenciais.

### 1. Carregamento de Módulos do Kernel (`modprobe`) Contra o Kernel Virtual `4.4.0`

Durante a instalação de pacotes e a inicialização do `dockerd`, os scripts de inicialização executam `modprobe` para carregar `overlay`, `br_netfilter`, `ip_tables`, `nf_nat` e `xt_conntrack`. O Sentry do gVisor externo bloqueia `init_module(2)` e `finit_module(2)` e informa uma versão virtual do kernel (`4.4.0`) que não possui o diretório `/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. Falhas do `iptables-nft` (Netlink `nf_tables`) e da Ponte `docker0`

No Ubuntu 22.04, no Ubuntu 24.04 e no Debian 11/12, o `apt-get install docker.io` seleciona o `iptables-nft` (apoiado no subsistema netlink `nf_tables`) como a alternativa padrão `/usr/sbin/iptables`. Em um contêiner gVisor padrão, sem privilégios administrativos de rede ou suporte a netlink `nf_tables`, o `dockerd` falha ao inicializar a cadeia NAT `DOCKER` e a ponte `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. Falha do `overlay2` sobre o Rootfs `gofer` / `9p` e Esgotamento de Disco com `vfs`

A menos que o administrador do `runsc` externo tenha explicitamente pré-configurado uma montagem dedicada de `tmpfs` ou de `filestore` em `/var/lib/docker`, o rootfs do contêiner é apoiado pelo `gofer` do gVisor (`9p`/`lisafs`) ou por uma montagem overlay aninhada. Montar `overlay2` sobre `/var/lib/docker` falha:

```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
```

Quando usuários ou agentes tentam contornar esse erro iniciando `dockerd --storage-driver=vfs`, o Docker desabilita completamente o compartilhamento de camadas por Copy-on-Write. Cada instrução `RUN` em um `Dockerfile` realiza uma cópia recursiva completa (`dirCopy`) de todo o sistema de arquivos raiz, o que transforma uma imagem de 500 MB distribuída em 10 etapas de build em 5 GB de escritas e encerra o sandbox com `no space left on device` ou com um timeout de E/S.

### 4. Rejeições de Montagens de Cgroup e de `/proc` no `runc` Aninhado

Mesmo que o `dockerd` seja forçado a iniciar com `dockerd --iptables=false --bridge=none --storage-driver=vfs`, a execução de `docker run` ou `docker build` invoca o `runc` para criar o contêiner interno. Em um contêiner gVisor padrão, sem privilégios, o `runc` falha ao montar cgroups, ao remontar `/proc` ou ao carregar filtros seccomp internos:

```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` Aninhado Falha com `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`EBUSY`)

Se um usuário tenta executar outra instância de `runsc` dentro de `runsc` usando a plataforma `systrap`, a inicialização é abortada imediatamente:

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

Tanto o kernel Linux quanto o Sentry do gVisor permitem **no máximo um** listener `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`SECCOMP_RET_USER_NOTIF`) por hierarquia de tarefas. Como o sandbox externo já ocupa esse slot de listener, a chamada interna `seccomp(2)` retorna `EBUSY` (`device or resource busy`).

---

## Como o `gvx` Resolve a Execução Aninhada Dentro do `gVisor`

O `gvx` aborda as cinco barreiras dentro de seu próprio kernel em espaço do usuário e de sua camada de plataforma, permitindo que o `rungv` execute dentro de um contêiner gVisor externo (`runsc`) ou até recursivamente dentro de outro contêiner `gvx` (`gvx` em `gvx`).

![Execução aninhada do gvx dentro de um gVisor externo](/assets/images/gvx/gvx-nested-seccomp-futex.png)

### 1. Fallback Automático do `systrap`, de `EBUSY` para o Loop de Futex `_RUN_SYSCALL_LOOP`

No `systrap` do Linux, cada subprocesso stub executa dois tipos de threads:

- **Threads de execução do convidado (`sysmsg`)**: Interceptam chamadas de sistema do convidado via `SECCOMP_RET_TRAP` (`SIGSYS`) e trampolins `jmp rel32` com patches binários (`syshandler`), trocando o estado de registradores com o Sentry por meio de memória compartilhada e `SYS_FUTEX`.
- **Uma `syscallThread` stub (`syscall_thread.go`)**: Executa chamadas de sistema do espaço de endereçamento (`mmap`, `munmap`, `clone`) dentro do subprocesso stub em nome do Sentry.

No gVisor upstream, a `syscallThread` oferece suporte a dois modos de despertar em `stub_amd64.s` e `stub_arm64.s`:

1. **Loop de User-Notify do Seccomp (`_RUN_SECCOMP_LOOP = 6`, quando `t.seccompNotify != nil`)**: Durante `syscallThread.init()`, `t.installSeccompNotify()` instala um listener `SECCOMP_RET_USER_NOTIF` (`seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)`) para que o Sentry possa despachar requisições via `ioctl(SECCOMP_IOCTL_NOTIF_SEND)` e `ioctl(SECCOMP_IOCTL_NOTIF_RECV)`.
2. **Loop de Chamadas de Sistema por Futex (`_RUN_SYSCALL_LOOP = 5`, quando `t.seccompNotify == nil`)**: A `syscallThread` executa `syscall_loop` (`stub_amd64.s` / `stub_arm64.s`), aguardando em `SYS_FUTEX` (`FUTEX_WAIT`) sobre a região compartilhada de duas páginas, composta por `syscallSentryMessage` (`PROT_READ` no stub) e `syscallStubMessage` (`PROT_READ | PROT_WRITE` no stub).

O gVisor upstream chama incondicionalmente `t.installSeccompNotify()` sempre que `seccompNotifyIsSupported` é verdadeiro e aborta a criação do stub se `seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...)` retornar `EBUSY`.

No `gvx` (`pkg/sentry/platform/systrap/seccomp_notify_xplat.go` e `syscall_thread.go`), `syscallThread.init()` chama `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
}
```

Quando `installSeccompNotify()` retorna `unix.EBUSY` porque um sandbox `gVisor` (`runsc`) ou `gvx` externo já ocupa o slot único de listener da cadeia de filtros do processo:

- `installSeccompNotifyIfFree()` define `seccompNotifyBusy` (`atomic.Bool`), registra um único aviso para o processo e mantém `t.seccompNotify == nil` para esse stub e para todos os stubs subsequentes.
- Em `syscallThread.detach()` (`syscall_thread_amd64.go` e `syscall_thread_arm64.go`), `t.seccompNotify == nil` define `regs.Rbx = _RUN_SYSCALL_LOOP` (`5`) em `amd64` (ou `regs.Regs[9] = _RUN_SYSCALL_LOOP` em `arm64`), em vez de `_RUN_SECCOMP_LOOP` (`6`).
- Em `syscallThread.syscall()` (`syscall_thread.go`), o Sentry interno despacha as chamadas de sistema do stub exclusivamente por memória compartilhada e `SYS_FUTEX`:

```go
// pkg/sentry/platform/systrap/syscall_thread.go
// Notifica a thread de chamadas de sistema sobre uma nova requisição de syscall.
atomic.AddUint32(&sentryMsg.state, 1)
futexWakeUint32(&sentryMsg.state)

// Aguarda a resposta.
futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
```

Como os filtros `SECCOMP_RET_TRAP` atuam sobre as pilhas das threads do convidado com profundidade arbitrária, e a `syscallThread` recorre a `_RUN_SYSCALL_LOOP` sem solicitar um segundo listener de notificação de usuário, o `gvx` executa de forma consistente dentro de `runsc` e oferece suporte a aninhamento recursivo (`gvx` dentro de `gvx`).

### 2. `overlayfs` Nativo sobre Volumes `ext4lite` de Arquivo Único

Para evitar a montagem de `overlayfs` sobre o sistema de arquivos raiz `gofer` (`9p`/`lisafs`) do contêiner gVisor externo, `-v gvx-docker:/var/lib/docker` sustenta `/var/lib/docker` com o sistema de arquivos interno `ext4lite` do `gvx`, armazenado em um único arquivo regular (`gvx-docker.ext4`).

Para o `runsc` externo, `gvx-docker.ext4` é apenas um arquivo regular acessado via `pread64(2)` e `pwrite64(2)`. Dentro do `gvx`, o `ext4lite` fornece um sistema de arquivos `ext4` com journaling JBD2, com atributos estendidos `trusted.overlay.*` e `security.capability`, nós de dispositivo de caractere para whiteouts (`0:0`), entradas de diretório `d_type` e `RENAME_EXCHANGE`, permitindo que o `dockerd` interno utilize seu driver de armazenamento `overlayfs` nativo.

### 3. `netstack` Autocontido com `docker0` e `iptables-legacy`

Dentro do `gvx`, o `dockerd` não interage com as interfaces de rede do contêiner `runsc` externo nem com suas tabelas netfilter. Em vez disso:

- O `dockerd` cria sua ponte `docker0` e os pares `veth` dentro do `netstack` próprio do `gvx`, em espaço do usuário.
- Todas as regras `iptables-legacy` (`-t nat -N DOCKER`, `MASQUERADE`, `conntrack`, `addrtype`) são executadas na implementação interna de netfilter do `gvx`.
- Os contêineres `runc` aninhados montam suas hierarquias `/proc`, `/sys` e `/sys/fs/cgroup` dentro da tabela de montagens `VFS2` em memória do `gvx`, sem jamais invocar a syscall `mount(2)` do `runsc` externo.

---

## Verificação do `docker:dind` Aninhado Dentro do `gVisor` (ou de `gvx` em `gvx`)

É possível verificar ambos os cenários (`rungv` dentro de `runsc` ou `rungv` dentro de `rungv`) com os seguintes comandos:

```sh
# Dentro de um contêiner gVisor (runsc) externo ou de um contêiner rungv externo:
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

# Verifique se o daemon Docker interno está usando overlayfs nativo (e não vfs)
docker info --format 'Storage Driver: {{.Driver}}'
# Saída: Storage Driver: overlayfs

# Execute um build de múltiplas camadas e um contêiner aninhado
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
# Saída: nested-build-ok

kill -INT $DIND_PID
```

---

## Leitura Complementar Nesta Série

1. **[gvx: Executando Contêineres Linux, Docker e Kubernetes em um Único Processo sem Privilégios](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**: Visão geral do `gvx` nas plataformas Windows (`whpx`, `systrap`), macOS (`hvf`) e Linux (`systrap`).
2. **[Executando Docker, docker build e docker compose Sem Root ou User Namespaces](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)**: Como o `gvx` executa `docker:dind` quando `root`, `sudo`, `unshare(CLONE_NEWUSER)`, `mount(2)` e `/dev/fuse` estão bloqueados.
3. **[Executando Docker-in-Docker Dentro do gVisor e de Sandboxes Aninhados com o gvx](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)** (esta postagem)
4. **[Ambientes Docker Isolados e Rápidos para Rollouts de RL de Agentes de IA com o gvx](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)**: Como usar o `gvx` e snapshots copy-on-write de arquivo único `ext4lite` para executar centenas de ambientes paralelos de `SWE-bench` e rollouts de RL com redefinições em 100 milissegundos.
