GoSuda

gvx: Executando Linux Containers, Docker e Kubernetes em um Único Processo Não Privilegiado

By Lemon Mint views ...

Introdução: Contêineres Endurecidos com Segurança Sem Root ou Daemons no Host

Executar contêineres Linux, docker build ou Kubernetes dentro de uma máquina de desenvolvimento, de um runner de CI ou de um nó de nuvem multilocatário normalmente impõe um compromisso de segurança desconfortável:

  • No Linux, runtimes OCI padrão (runc, crun, runsc) exigem privilégios de root, exposição de /var/run/docker.sock do host, --privileged (CAP_SYS_ADMIN) ou namespaces de usuário (unshare(CLONE_NEWUSER)), que ampliam a superfície de ataque do kernel do host.
  • No Windows e no macOS, os mecanismos de contêiner (Docker Desktop, Podman Machine, Lima, OrbStack) inicializam um kernel completo de máquina virtual Linux dentro do WSL2, do Hyper-V ou do Virtualization.framework, e executam um daemon persistente em segundo plano dentro dessa VM.

gvx (CLI rungv e SDK Go z.gfire.dev/gvx/pkg/gvx) oferece uma alternativa segura e de privilégio zero. Construído como um fork rastreável a montante do kernel Linux em espaço de usuário gVisor do Google (Sentry) e da pilha de rede em espaço de usuário (netstack), o gvx executa binários linux/amd64 e linux/arm64 ELF64 não modificados e contêineres OCI diretamente dentro de um único processo do host, sem privilégios e com bloqueio próprio no Windows, no macOS e no Linux.

Garantia de Segurança (Defesa em Profundidade de Três Anéis): o gvx nunca enfraquece as fronteiras de segurança do SO host, nunca instala drivers de kernel e nunca exige root, sudo, --privileged no host ou CLONE_NEWUSER. Antes de executar qualquer instrução do guest, o gvx bloqueia o próprio processo do host usando Windows Restricted Tokens e Job Objects, Seatbelt do macOS (deny default) ou PR_SET_NO_NEW_PRIVS + seccomp BPF no Linux. Todas as chamadas de sistema do guest, os privilégios virtuais de UID 0, as montagens (VFS2) e as tabelas de rede (netstack) são estritamente confinados dentro do kernel Sentry em Go, seguro quanto à memória.

RuntimeSO do HostCold StartConsumo de MemóriaRequer Admin ou sudoDaemon de VM em Segundo PlanoExecução Direta de Imagem OCI
gvx (whpx / hvf)Windows (amd64, arm64) e macOS (arm64)~20 ms~30 MiBNãoNãoSim
gvx (systrap)Windows (amd64, arm64) e Linux (amd64, arm64)~25 a 65 ms~31 MiBNãoNãoSim
WSL2 / Docker DesktopWindows e macOS800 a 2500 ms> 512 MiBSim (Configuração Inicial)SimRequer Daemon
QEMU (Sistema Completo)Windows e macOS1500 a 4000 ms> 256 MiBNãoNãoRequer Imagem de Disco

Nenhum kernel de VM Linux é inicializado no Windows ou no macOS. Nenhum daemon em segundo plano é executado no host. Não são necessários privilégios de root, sudo, namespaces de usuário (CLONE_NEWUSER), mount(2), drivers TUN/TAP ou CAP_NET_ADMIN no SO host. Dentro desse processo sem privilégios, o gvx executa Docker-in-Docker (docker:dind), docker build, docker compose e Kubernetes de alta disponibilidade com múltiplos nós (k3s com etcd embutido e CNI Calico).


Ciclo de Vida do Contêiner e Bloqueio do Host em Três Anéis

Cada sandbox do gvx é executado dentro de um único processo do host sem privilégios e impõe um ciclo de vida em três fases: pré-abertura de todos os descritores de arquivo e sockets necessários do host, bloqueio do processo do SO host por meio de pkg/hostlockdown e entrada no loop de execução Sentry dentro do processo.

Ciclo de Vida do Contêiner gvx e Autobloqueio do Host

Antes de transferir o controle para a primeira instrução do guest, o gvx pré-abre seus descritores de camadas de imagem .erofs em cache, os descritores de arquivos de volume .ext4, o handle *os.Root de /workspace e os sockets de rede do host, e então invoca hostlockdown.Apply():

  1. Windows NT (lockdown_windows.go): Chama AdjustTokenPrivileges com SE_PRIVILEGE_REMOVED para remover privilégios no próprio lugar, vincula um token de impersonação restrito DISABLE_MAX_PRIVILEGE com integridade Low (S-1-16-4096), atribui o processo a um Job Object anônimo com JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 1 (bloqueando a criação de qualquer processo filho) e habilita DisallowWin32kSystemCalls = 1 por meio de SetProcessMitigationPolicy.
  2. macOS XNU (lockdown_darwin.go): Define RLIMIT_NPROC = 0 e aplica um perfil Seatbelt (sandbox_init_with_parameters) começando em (version 1)(deny default) que bloqueia process-fork, process-exec* e todas as novas aberturas de arquivos do host.
  3. Linux (lockdown_linux.go + pkg/gvx/syscallfilter_linux.go): Define PR_SET_NO_NEW_PRIVS e instala a allowlist seccomp BPF do Sentry em todas as threads (SECCOMP_FILTER_FLAG_TSYNC), com regras EPERM explícitas para execve, execveat, clone, openat e socket.

Interceptação de Syscalls Multiplataforma e Paginação de 4 KB (pkg/sentry/platform)

Binários Linux ELF64 pressupõem granularidade de página de memória de 4 KB (0x1000) para mapeamentos de segmentos mmap(MAP_FIXED). No entanto, VirtualAlloc e MapViewOfFile3 do Windows impõem alinhamento de 64 KB (0x10000), e o macOS em Apple Silicon (darwin/arm64) usa páginas de host de 16 KB (0x4000). Além disso, cada SO exige um mecanismo diferente para interceptar instruções Linux SYSCALL (amd64) e SVC #0 (arm64) sem inicializar uma VM Linux.

O gvx resolve ambos os problemas em quatro backends de execução que implementam pkg/sentry/platform.Context:

Mecânica de Interceptação de Syscalls e Paginação de 4KB da Plataforma gvx

1. Mecanismo de Hardware do Windows: platform/whpx (WinHvPlatform.dll)

Em windows/amd64 e windows/arm64, o platform/whpx cria uma partição de hipervisor leve por meio de WHvCreatePartition sem inicializar um kernel de SO convidado:

  • Desacoplador de Paginação de 64 KB para 4 KB: O whpx registra cada arena de 64 MiB de pgalloc.MemoryFile na partição uma única vez, por meio de WHvMapGpaRange. As chamadas subsequentes de MapFile e Unmap modificam apenas as tabelas de páginas do guest dentro da partição, impondo permissões R/W/X exatas de 4 KB apesar da granularidade de alocação de 64 KB do Windows.
  • Troca de Registradores Sem API via CommPage:
    • Em amd64, um SYSCALL de Ring-3 do guest entra em um stub MSR_LSTAR de Ring-0 dentro da partição (stubgen_x86.go), que grava os GPRs da syscall de caminho rápido (RAX, RDI, RSI, RDX, R10, R8, R9, RIP, RSP) em uma CommPage compartilhada de 4 KB e executa HLT para sair de WHvRunVirtualProcessor.
    • Em arm64, um SVC #0 de EL0 do guest entra nos vetores de EL1 (stubgen_a64.go), grava X0..X8, SP_EL0, ELR_EL1, PSTATE na CommPage e carrega a partir de GPA zero sem backing para acionar uma saída de memória de volta ao Sentry.

2. Mecanismo de Hardware do macOS: platform/hvf (Hypervisor.framework)

Em Apple Silicon (darwin/arm64), o platform/hvf executa threads de contêiner dentro de vCPUs do Hypervisor.framework (hv_vm_create, hv_vcpu_run):

  • Tradução de MMU de 4 KB sobre 16 KB em Dois Estágios: O Stage-2 (hv_vm_map) mapeia blocos de 1 GiB do arquivo de memória para o espaço de endereços físicos intermediários (IPA) uma única vez, com granularidade de página de host de 16 KB e HV_MEMORY_READ | HV_MEMORY_WRITE | HV_MEMORY_EXEC. Dentro da vCPU, o stub EL1 bluepill (bluepill_arm64.go) configura TCR_EL1.TG0 = 0b00 (granule de 4 KB) em TTBR0_EL1 e preenche entradas de tabela de páginas Stage-1 de 4 KB com permissões exatas R-X ou RW- (UXN=1). Como a MMU do ARM64 impõe a interseção das permissões Stage-1 e Stage-2, o hardware captura falhas de permissão em fronteiras exatas de 4 KB.
  • Interceptação de Syscall: O SVC #0 de EL0 do guest é direcionado para o VBAR_EL1 de EL1, grava os registradores na CommPage compartilhada de 4 KB e executa HVC #0 para retornar de hv_vcpu_run à thread Go do Sentry.

3. Fallback em Modo Usuário do Windows: platform/systrap (winsystrap)

Quando a virtualização de hardware não está disponível (como dentro de VMs de nuvem aninhadas), o winsystrap executa código do guest em modo usuário em windows/amd64 e windows/arm64:

  • Isolamento de CR3 / TTBR0_EL1 por Hardware: Threads do guest são executadas dentro de subprocessos de stub isolados, inicializados a partir de um executável gerado de 1 KB (stubimage_xplat.go), confinados por um token de integridade Low e por um Job Object.
  • Patch Binário In-Place e IPC winSysmsg: Em amd64, o patcher (patcher_amd64_xplat.go) substitui instruções syscall de 2 bytes (0f 05) por saltos jmp rel32 de 5 bytes (ou armadilhas de breakpoint CC 90 quando uma janela de 5 bytes não está disponível) para uma ilha de caminho rápido. Em arm64, svc #0 é substituído por um desvio B para um slot da ilha. A ilha grava os registradores na página winSysmsg compartilhada e se sincroniza com o Sentry por meio de WaitOnAddress e WakeByAddressSingle, enquanto as armadilhas não aplicadas e as falhas de página são interrompidas em um Windows Debug Object (WaitForDebugEventEx).

4. Mecanismo em Modo Usuário do Linux: platform/systrap

Em linux/amd64 e linux/arm64, o systrap executa threads do guest (sysmsg) que capturam syscalls do guest por meio de SECCOMP_RET_TRAP (SIGSYS) e aplicam patches binários nos pontos syscall para um trampolim de memória compartilhada (sysmsg), sincronizado via SYS_FUTEX. Enquanto isso, uma thread stub syscallThread (syscall_thread.go) executa operações de espaço de endereçamento (mmap, munmap, clone) via SECCOMP_RET_USER_NOTIF (_RUN_SECCOMP_LOOP). Quando o gvx é executado dentro de um sandbox gVisor externo (runsc), em que o único slot SECCOMP_FILTER_FLAG_NEW_LISTENER retorna EBUSY, o gvx alterna automaticamente o syscallThread para um loop de futex em memória compartilhada (_RUN_SYSCALL_LOOP), conforme detalhado na Parte 3 desta série.


Como o gvx Mapeia Syscalls Linux do Guest para Subsistemas Virtuais e Handles do Host

Assim que um mecanismo de plataforma intercepta uma instrução SYSCALL (amd64) ou SVC #0 (arm64) do guest e grava os registradores do guest na página compartilhada de 4 KB (CommPage, sysmsg ou winSysmsg), o controle retorna à goroutine da tarefa Go do Sentry (pkg/sentry/kernel/task_syscall.go). Em vez de encaminhar syscalls Linux para o kernel do SO host subjacente, Task.doSyscall() salva o estado original dos registradores (SyscallSaveOrig()), avalia quaisquer filtros seccomp BPF e paradas ptrace no guest e despacha o número da syscall por meio de linux.AMD64 ou linux.ARM64 (pkg/sentry/syscalls/linux/linux64.go, cobrindo cerca de 370 syscalls Linux) para quatro domínios internos:

Como o gvx Mapeia Syscalls Linux do Guest para Subsistemas e SO Host

Domínio de SyscallSyscalls Linux Representativas do GuestSubsistema Interno do gvx (pkg/sentry)Interação com a Fronteira do SO Host (Após hostlockdown)
A. Processo, Namespace, Montagem e Sincronizaçãoclone, execve, unshare, setns, mount, umount2, pivot_root, setuid, capset, futex, epoll_wait, pipe2kernel.Task, credenciais virtuais, tabela de futex e VFS2 em memória (procfs, sysfs, cgroupfs v2, tmpfs, overlayfs)Zero syscalls no host (estado Go 100% em memória; completamente invisível ao SO host)
B. Memória Virtual (MM)mmap, munmap, mremap, mprotect, brk, madvise, memfd_createmm.MemoryManager (árvore de VMA de 4 KB) e pgalloc.MemoryFileAtualizações bidirecionais de tabelas de páginas de 4 KB via WHvMapGpaRange, hv_vm_map + TTBR0_EL1 ou mmap/mprotect do stub
C. Arquivos e Armazenamento (VFS2)openat, pread64, pwrite64, getdents64, statx, renameat2, setxattr, mknod, inotify_add_watchRoteador da tabela de montagens de vfs.VirtualFilesystem (fsimpl/erofs, fsimpl/ext4lite, fsimpl/hostfs v2)pread/pwrite bidirecionais em 1 FD .erofs fixado ou 1 FD .ext4, ou percursos relativos restritos em *os.Root
D. Rede e Netfiltersocket, bind, listen, connect, sendmsg, recvmsg, netlink, setsockopt(IPT_SO_SET_REPLACE)pkg/tcpip em espaço de usuário (lo, eth0, docker0, veth, vxlan, wireguard, ipset, iptables-legacy)O tráfego interno ao sandbox usa zero syscalls no host; o tráfego externo é mapeado para net.Dial/net.Listen (-net=nat) ou 1 socket UDP (-net=wg)

Quando o handler retorna (rval, ctrl, err), Task.doSyscallInvoke() converte qualquer erro em um errno Linux negativo (-ExtractErrno(err, sysno)), grava o valor de retorno de volta na página de registradores compartilhada (RAX em amd64, X0 em arm64) e retoma a execução do guest.


Armazenamento (VFS2) e Rede em Espaço de Usuário com Trilha Dupla (netstack)

Todas as operações de arquivo e de rede do guest são atendidas dentro do Sentry em Go por meio de três sistemas de arquivos VFS2 e duas trilhas de pkg/netbackend:

Mecanismos de Armazenamento VFS2 do gvx e Netstack de Trilha Dupla

Mecanismo de ArmazenamentoPapel de Montagem no gvxRecurso de Host SubjacenteChamadas openat / NtCreateFile no Host em Tempo de ExecuçãoVarreduras por Arquivo do Windows Defender (WdFilter.sys)
fsimpl/erofsCamadas inferiores somente leitura de imagens OCIUm único arquivo <sha256>.erofs por camada00
fsimpl/ext4liteVolumes nomeados (-v name:/path)Um único arquivo <name>.ext4 por volume00
fsimpl/hostfs v2Workspace do host (-workspace, -v /host:/guest)Diretório do host fixado em *os.RootO(profundidade) por abertura sem cache1 por fechamento de arquivo do host

Por que o ext4lite É Importante para Docker, Bancos de Dados e Gerenciadores de Pacotes

Extrair ou gravar dezenas de milhares de arquivos de contêineres diretamente em NTFS do Windows ou APFS do macOS aciona varreduras de minifiltros antivírus (WdFilter.sys), colisões de sensibilidade a maiúsculas e minúsculas e ausência de atributos estendidos POSIX.

pkg/sentry/fsimpl/ext4lite implementa um sistema de arquivos ext4 escrito em Go puro dentro de um único arquivo do host pré-aberto (%LOCALAPPDATA%\gvx\cache\volumes\<name>.ext4 no Windows, ~/Library/Caches/gvx/volumes/<name>.ext4 no macOS ou ~/.cache/gvx/volumes/<name>.ext4 no Linux), com:

  • Blocos de 4 KiB e grupos de blocos dinâmicos de 16 MiB (meta_bg): Cresce dinamicamente sem paradas de alocação de arquivos esparsos (fallocate no Linux, fcntl(F_PREALLOCATE) no macOS, FileAllocationInfo no Windows) e compacta grupos finais não utilizados na desmontagem limpa.
  • Journal JBD2 em modo ordenado (inode 8): Confirmações atômicas de transações com somas de verificação CRC-32 e blocos de revogação, além da recuperação via fsckLite.
  • Suporte à camada superior do overlayfs: Implementa os atributos estendidos trusted.overlay.* e security.capability, whiteouts de dispositivos de caractere (0:0), relatório de entradas de diretório d_type e RENAME_EXCHANGE.

Cargas de Trabalho Práticas: Docker Engine, Kubernetes (k3s + Calico) e SDK Go

Instalando o rungv: Versões Pré-compiladas com Atestação SLSA ou go install

Você pode baixar um arquivo rungv pré-compilado para linux/amd64, linux/arm64, darwin/arm64, windows/amd64 ou windows/arm64 em GitHub Releases (cada um construído pelo pipeline de release SLSA com um pacote de procedência Sigstore e SHA256SUMS), ou instalar diretamente a partir do código-fonte com go install:

1# Opção A: Baixe uma versão pré-compilada e verifique a procedência de build SLSA
2gh attestation verify rungv-v0.2.0-linux-amd64.tar.gz --repo gfire-sigs/gvx \
3  --signer-workflow gfire-sigs/gvx/.github/workflows/release.yml \
4  --source-ref refs/tags/v0.2.0 --deny-self-hosted-runners
5shasum -a 256 -c SHA256SUMS --ignore-missing
6tar -xzf rungv-v0.2.0-linux-amd64.tar.gz
7
8# Opção B: Instale diretamente via Go (Linux / Windows)
9CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest

No macOS (darwin/arm64), o arquivo de release pré-compilado já está assinado com a permissão com.apple.security.hypervisor (xattr -d com.apple.quarantine rungv remove a quarentena do navegador). Se você compilar localmente com go install ou go build no macOS, assine o binário com codesign --sign - --entitlements entitlements.plist --force $(which rungv).

1. Executando um Docker Engine Sem Privilégios (docker:dind)

Seguindo docs/docker-engine.md, você pode executar um contêiner docker:dind não modificado dentro do rungv no Windows, no macOS ou no Linux e controlá-lo com a CLI docker do seu host:

 1# 1. Crie o diretório do host para o socket Unix encaminhado
 2mkdir -p ~/.gvx
 3
 4# 2. Inicie o docker:dind dentro de um sandbox do gvx com um volume ext4lite
 5rungv run \
 6  -p unix:~/.gvx/docker.sock:2375 \
 7  -p 127.0.0.1:9000-9019:9000-9019 \
 8  -v gvx-docker:/var/lib/docker \
 9  -e DOCKER_TLS_CERTDIR= \
10  docker:dind --tls=false

Em um segundo terminal, aponte a CLI docker do seu host para o socket do sandbox:

1docker context create gvx --docker host=unix://$HOME/.gvx/docker.sock
2docker context use gvx
3
4docker info --format '{{.Driver}} {{.OperatingSystem}}'
5# Saída: overlayfs Alpine Linux ... (containerized)
6
7docker build -t myapp ./app
8docker compose up -d

2. Executando Kubernetes (k3s + Calico) em Sandboxes Sem Privilégios

Como o gvx implementa /dev/kmsg, cgroup v2 (o OOM killer de memory.max e a limitação de largura de banda de cpu.max), /proc/diskstats, dispositivos netlink wireguard e vxlan, ipset e iptables-legacy (addrtype, conntrack, statistic, recent), uma imagem rancher/k3s não modificada alcança Ready dentro de um único processo rungv:

 1mkdir -p ~/.gvx/k3s
 2
 3rungv run \
 4  -e IPTABLES_MODE=legacy \
 5  -p 127.0.0.1:6443:6443 \
 6  -p 127.0.0.1:30000-30019:30000-30019 \
 7  -v k3s:/var/lib/rancher \
 8  --mount type=bind,src=$HOME/.gvx/k3s,dst=/output \
 9  rancher/k3s:v1.31.4-k3s1 server \
10  --disable=traefik,servicelb \
11  --write-kubeconfig=/output/kubeconfig.yaml \
12  --write-kubeconfig-mode=644

Você também pode unir vários nós gvx por meio do WireGuard (flannel-backend=wireguard-native ou uma malha wg0), com alta disponibilidade do etcd embutido, ou executar o Calico (calico-vxlan.yaml com FELIX_IPTABLESBACKEND=Legacy) para impor NetworkPolicy do Kubernetes inteiramente em espaço de usuário.

3. Incorporando Contêineres Linux em Go via z.gfire.dev/gvx/pkg/gvx

Aplicações Go podem incorporar contêineres Linux diretamente, sem um daemon de contêiner externo:

 1package main
 2
 3import (
 4	"context"
 5	"fmt"
 6	"os"
 7
 8	"z.gfire.dev/gvx/pkg/gvx"
 9)
10
11func main() {
12	ctx := context.Background()
13	sb, err := gvx.New(ctx, gvx.Config{
14		Platform:      gvx.PlatformAuto,
15		NetworkMode:   gvx.NetModeNAT,
16		OCIImage:      "alpine:3.20",
17		WorkspacePath: "./repo",
18		Argv:          []string{"/usr/bin/git", "status"},
19		Stdout:        os.Stdout,
20		Stderr:        os.Stderr,
21	})
22	if err != nil {
23		fmt.Fprintf(os.Stderr, "init sandbox: %v\n", err)
24		os.Exit(1)
25	}
26	defer sb.Close()
27
28	exitCode, err := sb.Run(ctx)
29	if err != nil {
30		fmt.Fprintf(os.Stderr, "run sandbox: %v\n", err)
31		os.Exit(1)
32	}
33	os.Exit(exitCode)
34}

Leituras Adicionais Nesta Série

  1. gvx: Executando Contêineres Linux, Docker e Kubernetes em Um Único Processo Sem Privilégios (este post)
  2. Executando Docker, docker build e docker compose Sem Root ou Namespaces de Usuário: Como inicializar o docker:dind em sandboxes bloqueados nos quais sudo, unshare(CLONE_NEWUSER), mount(2) e /dev/fuse são bloqueados.
  3. Executando Docker-in-Docker Dentro do gVisor e de Sandboxes Aninhados com o gvx: Por que apt-get install docker.io e o runsc aninhado falham dentro do gVisor por causa de cinco barreiras no kernel, e como o gvx executa o docker:dind aninhado usando um loop de futex em memória compartilhada.
  4. Ambientes Docker Isolados e Rápidos para Rollouts de RL de Agentes de IA com o gvx: Escalonamento de SWE-bench e de rollouts de aprendizado por reforço de agentes com centenas de instâncias paralelas de docker:dind sem privilégios e redefinições de volumes .ext4 com copy-on-write de 100 milissegundos.