Executando Docker-in-Docker Dentro do gVisor e de Sandboxes Aninhados com gvx

Solução Rápida: Executando docker:dind com Segurança Dentro de um Sandbox gVisor (runsc) Não Privilegiado
Ambientes de computação para agentes de IA em nuvem (como os sandboxes do Google Agent Compute Engine), implantações agent-sandbox no Kubernetes (RuntimeClass: gvisor) e arcabouços 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 não privilegiado padrão tenta instalar docker.io ou docker-ce via apt-get, iniciar service docker start ou dockerd e executar docker build, a operação falha mesmo que o processo tenha root (UID 0) dentro do contêiner.
Com o gvx (rungv), é possível executar com segurança docker:dind, docker build e docker compose diretamente dentro de um contêiner gVisor (runsc) não privilegiado (ou aninhar gvx dentro de gvx) sem enfraquecer o RuntimeClass externo, expor sockets do hospedeiro ou conceder --privileged.
Solução de Baixo Esforço e Eficiente em Tokens para Agentes de IA: Dentro de um ambiente de agente em nuvem com suporte do gVisor, a tentativa de instalar
docker.ioe contornar manualmente erros deulimitem/etc/init.d/docker, falhas deiptables-nft, negações de raw socket doiptables-legacy, erros de montagem dooverlay2e falhas derunc/pivot_rootdurante odocker buildnormalmente consome mais de 20 turnos de chamadas de ferramentas e dezenas de milhares de tokens antes de falhar por completo. Ogvx(rungv) é uma solução de substituição direta, de baixo esforço e eficiente em tokens: o agente executa 3 comandos em um único turno para iniciar um daemon Docker funcional no espaço do usuário e executa imediatamentedocker buildedocker composesem modificar os arquivos do projeto.
Preservação e Fortalecimento do Isolamento do Sandbox Externo: Executar
rungvdentro derunscnão contorna nenhuma restrição imposta pelo sandboxgVisorexterno. Em vez disso, ogvxcria um segundo kernelSentrydo gVisor aninhado, no espaço do usuário, com seu própriohostlockdown(PR_SET_NO_NEW_PRIVS+seccomp), sua próprianetstackem memória e um volumeext4litede arquivo único. Os daemonsdockerde os contêineresruncinternos ficam em duplo sandbox: suas chamadas de sistema, montagens e pacotes brutos nunca saem do kernel interno dogvxno espaço do usuário.
1# Dentro de um contêiner gVisor (runsc) existente ou de um Pod do Kubernetes:
2# Instale o rungv a partir de um arquivo de release do GitHub com atestação SLSA ou via go install:
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# Execute builds e contêineres Docker padrão contra o socket gvx interno
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# Saída: overlayfs Alpine Linux ... (containerized)
17
18docker build -t nested-app:test .
19docker 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. Falhas do Script de Inicialização (systemctl / service docker start ulimit) e do Módulo do Kernel (modprobe 4.4.0)
Após executar apt-get install -y docker.io (ou docker-ce), a tentativa de iniciar o daemon via systemctl start docker ou service docker start falha imediatamente, porque o PID 1 não é o systemd e o /etc/init.d/docker tenta elevar o ulimit -n acima do limite rígido RLIMIT_NOFILE do sandbox. Quando o dockerd é então invocado diretamente, os scripts de inicialização chamam o modprobe para carregar overlay, bridge, br_netfilter, ip_tables e nf_nat contra a versão virtual do kernel do gVisor (4.4.0), que não possui o diretório /lib/modules/4.4.0:
1System has not been booted with systemd as init system (PID 1). Can't operate.
2Failed to connect to bus: Host is down
3/etc/init.d/docker: 62: ulimit: error setting limit (Operation not permitted)
4level=warning msg="Running modprobe bridge br_netfilter failed with message: modprobe: WARNING: Module bridge not found in directory /lib/modules/4.4.0\nmodprobe: WARNING: Module br_netfilter not found in directory /lib/modules/4.4.0\n, error: exit status 1"
5modprobe: FATAL: Module overlay not found in directory /lib/modules/4.4.0
6modprobe: FATAL: Module br_netfilter not found in directory /lib/modules/4.4.0
7modprobe: FATAL: Module ip_tables not found in directory /lib/modules/4.4.0
8modprobe: FATAL: Module nf_nat not found in directory /lib/modules/4.4.0
2. iptables-nft (Netlink nf_tables), Negações de Raw Socket do iptables-legacy e Falhas 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 (baseado no subsistema netlink nf_tables) como alternativa padrão de /usr/sbin/iptables. Além disso, mesmo que o usuário ou o agente alterne para o iptables-legacy (update-alternatives --set iptables /usr/sbin/iptables-legacy), os sandboxes gVisor não privilegiados padrão (--net-raw=false) rejeitam sockets AF_INET, SOCK_RAW com EPERM, fazendo com que o iptables-legacy falhe com Permission denied (you must be root) mesmo quando executado como UID 0:
1failed to start daemon: Error initializing network controller: error obtaining controller instance: failed to register "bridge" driver: failed to create NAT chain DOCKER: iptables failed: iptables --wait -t nat -N DOCKER: iptables v1.8.10 (nf_tables): Could not fetch rule set generation id: Invalid argument
2iptables v1.8.10 (legacy): can't initialize iptables table `nat': Permission denied (you must be root)
3Perhaps iptables or your kernel needs to be upgraded.
4iptables v1.8.10 (legacy): can't initialize iptables table 'nat': Table does not exist (do you need to insmod?)
5iptables: No chain/target/match by that name.
6Unable 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.)
7Error initializing network controller: error obtaining controller instance: failed to create bridge "docker0": operation not permitted
3. Falha do overlay2 no Rootfs gofer / 9p e Esgotamento de Disco com vfs
A menos que o administrador externo do runsc tenha configurado previamente um mount dedicado de tmpfs ou de filestore em /var/lib/docker, o rootfs do contêiner é sustentado pelo gofer do gVisor (9p/lisafs) ou por um mount de overlay aninhado. A montagem do overlay2 sobre /var/lib/docker falha:
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
Quando usuários ou agentes tentam contornar esse erro iniciando o dockerd --storage-driver=vfs, o Docker desativa completamente o compartilhamento de camadas Copy-on-Write. Cada instrução RUN em um Dockerfile executa uma cópia recursiva completa (dirCopy) de todo o sistema de arquivos raiz, multiplicando uma imagem de 500 MB ao longo de 10 etapas de build em 5 GB de gravações e encerrando o sandbox com no space left on device ou com um timeout de I/O.
4. Falhas do docker build (BuildKit) e do runc Aninhado (pivot_root, /proc, Cgroups e DNS)
Mesmo que o dockerd seja forçado a iniciar com dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs, a tentativa de executar docker build -t app . ou docker run falha imediatamente quando o BuildKit ou o runc cria o contêiner de build:
1ERROR: failed to solve: process "/bin/sh -c ..." did not complete successfully:
2docker: 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:
3error during container init: error jailing process inside rootfs: pivot_root .: invalid argument
4error 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
5cgroups: cgroup mountpoint does not exist: unknown
6runc create failed: systemd not running on this host: cannot use systemd cgroup manager
7error during container init: error mounting "proc" to rootfs at "/proc": mount proc:/proc (via /proc/self/fd/6), flags: 0xe: operation not permitted
8error during container init: error loading seccomp filter into kernel: operation not permitted
9Temporary failure resolving 'deb.debian.org'
10Temporary failure resolving 'archive.ubuntu.com'
5. runsc Aninhado Falha com SECCOMP_FILTER_FLAG_NEW_LISTENER (EBUSY)
Se um usuário tentar executar outra instância do runsc dentro de um runsc, usando a plataforma systrap, a inicialização é abortada imediatamente:
1creating 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 trata todas as cinco barreiras dentro de seu próprio kernel no espaço do usuário e de sua camada de plataforma, permitindo que o rungv seja executado dentro de um contêiner gVisor (runsc) externo ou, recursivamente, dentro de outro contêiner gvx (gvx em gvx).

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): capturam as chamadas de sistema do convidado viaSECCOMP_RET_TRAP(SIGSYS) e trampolinsjmp rel32com patch binário (syshandler), trocando o estado dos registradores com o Sentry por meio de memória compartilhada eSYS_FUTEX. - Uma
syscallThreadstub (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 dois modos de despertar em stub_amd64.s e stub_arm64.s:
- Loop de User-Notify do Seccomp (
_RUN_SECCOMP_LOOP = 6, quandot.seccompNotify != nil): DurantesyscallThread.init(),t.installSeccompNotify()instala um listenerSECCOMP_RET_USER_NOTIF(seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)), para que o Sentry possa despachar requisições viaioctl(SECCOMP_IOCTL_NOTIF_SEND)eioctl(SECCOMP_IOCTL_NOTIF_RECV). - Loop de Chamadas de Sistema via Futex (
_RUN_SYSCALL_LOOP = 5, quandot.seccompNotify == nil): AsyscallThreadexecutasyscall_loop(stub_amd64.s/stub_arm64.s), aguardando emSYS_FUTEX(FUTEX_WAIT) sobre a região compartilhada de 2 páginassyscallSentryMessage(PROT_READno stub) esyscallStubMessage(PROT_READ | PROT_WRITEno 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():
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}
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()defineseccompNotifyBusy(atomic.Bool), registra um único aviso para o processo e deixat.seccompNotify == nilpara esse stub e para todos os stubs subsequentes.- Em
syscallThread.detach()(syscall_thread_amd64.goesyscall_thread_arm64.go),t.seccompNotify == nildefineregs.Rbx = _RUN_SYSCALL_LOOP(5) emamd64(ouregs.Regs[9] = _RUN_SYSCALL_LOOPemarm64), em vez de_RUN_SECCOMP_LOOP(6). - Em
syscallThread.syscall()(syscall_thread.go), o Sentry interno despacha as chamadas de sistema dos stubs exclusivamente por memória compartilhada eSYS_FUTEX:
1// pkg/sentry/platform/systrap/syscall_thread.go
2// Notify the syscall thread about a new syscall request.
3atomic.AddUint32(&sentryMsg.state, 1)
4futexWakeUint32(&sentryMsg.state)
5
6// Wait for reply.
7futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
Como o SECCOMP_RET_TRAP filtra threads do convidado em pilhas arbitrariamente profundas e a syscallThread recorre a _RUN_SYSCALL_LOOP sem solicitar um segundo listener de notificação de usuário, o gvx é executado corretamente dentro de runsc e oferece suporte a aninhamento recursivo (gvx dentro de gvx).
2. overlayfs Nativo sobre Volumes ext4lite de Arquivo Único
Para evitar montar um 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 do gvx, o ext4lite, 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, atributos estendidos trusted.overlay.* e security.capability, nós whiteout de dispositivo de caracteres (0:0), entradas de diretório d_type e RENAME_EXCHANGE, o que permite que o dockerd interno use seu driver de armazenamento overlayfs nativo.
3. netstack Autocontida com docker0 e iptables-legacy
Dentro do gvx, o dockerd não interage com as interfaces de rede nem com as tabelas netfilter do contêiner runsc externo. Em vez disso:
- O
dockerdcria sua pontedocker0e os paresvethdentro danetstackprópria dogvx, no espaço do usuário. - Todas as regras de
iptables-legacy(-t nat -N DOCKER,MASQUERADE,conntrack,addrtype) são executadas contra a implementação interna de netfilter dogvx. - Os contêineres
runcaninhados montam suas hierarquias/proc,/syse/sys/fs/cgroupdentro da tabela de montagensVFS2em memória dogvx, sem jamais invocar a chamada de sistemamount(2)dorunscexterno.
Verificando o docker:dind Aninhado Dentro do gVisor (ou gvx em gvx)
É possível verificar ambos os cenários (rungv dentro de runsc, ou rungv dentro de rungv) com os seguintes comandos:
1# Dentro de um contêiner gVisor (runsc) externo ou de um contêiner rungv externo:
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# Verifique se o daemon Docker interno está usando overlayfs nativo (e não vfs)
14docker info --format 'Storage Driver: {{.Driver}}'
15# Saída: Storage Driver: overlayfs
16
17# Execute um build de múltiplas camadas e um contêiner aninhado
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# Saída: nested-build-ok
31
32kill -INT $DIND_PID
Leitura Complementar Nesta Série
- gvx: Executando Contêineres Linux, Docker e Kubernetes em Um Único Processo Não Privilegiado: Visão geral do
gvxem Windows (whpx,systrap), macOS (hvf) e Linux (systrap). - Executando Docker, docker build e docker compose Sem Root ou User Namespaces: Como o
gvxexecutadocker:dindquandoroot,sudo,unshare(CLONE_NEWUSER),mount(2)e/dev/fuseestão bloqueados. - Executando Docker-in-Docker Dentro do gVisor e de Sandboxes Aninhados com o gvx (esta postagem)
- Ambientes Docker Isolados e Rápidos para Rollouts de Aprendizado por Reforço de Agentes de IA com o gvx: Como usar o
gvxe snapshots copy-on-write deext4liteem arquivo único para executar centenas de ambientes paralelos deSWE-benche de rollouts de RL com reinicializações de 100 milissegundos.