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

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 deroot, exposição de/var/run/docker.sockdo 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 doVirtualization.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
gvxnunca enfraquece as fronteiras de segurança do SO host, nunca instala drivers de kernel e nunca exigeroot,sudo,--privilegedno host ouCLONE_NEWUSER. Antes de executar qualquer instrução do guest, ogvxbloqueia o próprio processo do host usando Windows Restricted Tokens e Job Objects, Seatbelt do macOS (deny default) ouPR_SET_NO_NEW_PRIVS+seccompBPF no Linux. Todas as chamadas de sistema do guest, os privilégios virtuais deUID 0, as montagens (VFS2) e as tabelas de rede (netstack) são estritamente confinados dentro do kernelSentryem Go, seguro quanto à memória.
| Runtime | SO do Host | Cold Start | Consumo de Memória | Requer Admin ou sudo | Daemon de VM em Segundo Plano | Execução Direta de Imagem OCI |
|---|---|---|---|---|---|---|
gvx (whpx / hvf) | Windows (amd64, arm64) e macOS (arm64) | ~20 ms | ~30 MiB | Não | Não | Sim |
gvx (systrap) | Windows (amd64, arm64) e Linux (amd64, arm64) | ~25 a 65 ms | ~31 MiB | Não | Não | Sim |
| WSL2 / Docker Desktop | Windows e macOS | 800 a 2500 ms | > 512 MiB | Sim (Configuração Inicial) | Sim | Requer Daemon |
| QEMU (Sistema Completo) | Windows e macOS | 1500 a 4000 ms | > 256 MiB | Não | Não | Requer 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.

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():
- Windows NT (
lockdown_windows.go): ChamaAdjustTokenPrivilegescomSE_PRIVILEGE_REMOVEDpara remover privilégios no próprio lugar, vincula um token de impersonação restritoDISABLE_MAX_PRIVILEGEcom integridade Low (S-1-16-4096), atribui o processo a um Job Object anônimo comJOB_OBJECT_LIMIT_ACTIVE_PROCESS = 1(bloqueando a criação de qualquer processo filho) e habilitaDisallowWin32kSystemCalls = 1por meio deSetProcessMitigationPolicy. - macOS XNU (
lockdown_darwin.go): DefineRLIMIT_NPROC = 0e aplica um perfil Seatbelt (sandbox_init_with_parameters) começando em(version 1)(deny default)que bloqueiaprocess-fork,process-exec*e todas as novas aberturas de arquivos do host. - Linux (
lockdown_linux.go+pkg/gvx/syscallfilter_linux.go): DefinePR_SET_NO_NEW_PRIVSe instala a allowlistseccompBPF do Sentry em todas as threads (SECCOMP_FILTER_FLAG_TSYNC), com regrasEPERMexplícitas paraexecve,execveat,clone,openatesocket.
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:

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
whpxregistra cada arena de 64 MiB depgalloc.MemoryFilena partição uma única vez, por meio deWHvMapGpaRange. As chamadas subsequentes deMapFileeUnmapmodificam apenas as tabelas de páginas do guest dentro da partição, impondo permissõesR/W/Xexatas de 4 KB apesar da granularidade de alocação de 64 KB do Windows. - Troca de Registradores Sem API via
CommPage:- Em
amd64, umSYSCALLde Ring-3 do guest entra em um stubMSR_LSTARde 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 umaCommPagecompartilhada de 4 KB e executaHLTpara sair deWHvRunVirtualProcessor. - Em
arm64, umSVC #0de EL0 do guest entra nos vetores de EL1 (stubgen_a64.go), gravaX0..X8, SP_EL0, ELR_EL1, PSTATEnaCommPagee carrega a partir de GPA zero sem backing para acionar uma saída de memória de volta ao Sentry.
- Em
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 eHV_MEMORY_READ | HV_MEMORY_WRITE | HV_MEMORY_EXEC. Dentro da vCPU, o stub EL1bluepill(bluepill_arm64.go) configuraTCR_EL1.TG0 = 0b00(granule de 4 KB) emTTBR0_EL1e preenche entradas de tabela de páginas Stage-1 de 4 KB com permissões exatasR-XouRW-(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 #0de EL0 do guest é direcionado para oVBAR_EL1de EL1, grava os registradores naCommPagecompartilhada de 4 KB e executaHVC #0para retornar dehv_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_EL1por 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: Emamd64, o patcher (patcher_amd64_xplat.go) substitui instruçõessyscallde 2 bytes (0f 05) por saltosjmp rel32de 5 bytes (ou armadilhas de breakpointCC 90quando uma janela de 5 bytes não está disponível) para uma ilha de caminho rápido. Emarm64,svc #0é substituído por um desvioBpara um slot da ilha. A ilha grava os registradores na páginawinSysmsgcompartilhada e se sincroniza com o Sentry por meio deWaitOnAddresseWakeByAddressSingle, 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:

| Domínio de Syscall | Syscalls Linux Representativas do Guest | Subsistema Interno do gvx (pkg/sentry) | Interação com a Fronteira do SO Host (Após hostlockdown) |
|---|---|---|---|
| A. Processo, Namespace, Montagem e Sincronização | clone, execve, unshare, setns, mount, umount2, pivot_root, setuid, capset, futex, epoll_wait, pipe2 | kernel.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_create | mm.MemoryManager (árvore de VMA de 4 KB) e pgalloc.MemoryFile | Atualizaçõ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_watch | Roteador 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 Netfilter | socket, 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:

| Mecanismo de Armazenamento | Papel de Montagem no gvx | Recurso de Host Subjacente | Chamadas openat / NtCreateFile no Host em Tempo de Execução | Varreduras por Arquivo do Windows Defender (WdFilter.sys) |
|---|---|---|---|---|
fsimpl/erofs | Camadas inferiores somente leitura de imagens OCI | Um único arquivo <sha256>.erofs por camada | 0 | 0 |
fsimpl/ext4lite | Volumes nomeados (-v name:/path) | Um único arquivo <name>.ext4 por volume | 0 | 0 |
fsimpl/hostfs v2 | Workspace do host (-workspace, -v /host:/guest) | Diretório do host fixado em *os.Root | O(profundidade) por abertura sem cache | 1 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 (fallocateno Linux,fcntl(F_PREALLOCATE)no macOS,FileAllocationInfono 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 estendidostrusted.overlay.*esecurity.capability, whiteouts de dispositivos de caractere (0:0), relatório de entradas de diretóriod_typeeRENAME_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ãocom.apple.security.hypervisor(xattr -d com.apple.quarantine rungvremove a quarentena do navegador). Se você compilar localmente comgo installougo buildno macOS, assine o binário comcodesign --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
- gvx: Executando Contêineres Linux, Docker e Kubernetes em Um Único Processo Sem Privilégios (este post)
- Executando Docker, docker build e docker compose Sem Root ou Namespaces de Usuário: Como inicializar o
docker:dindem sandboxes bloqueados nos quaissudo,unshare(CLONE_NEWUSER),mount(2)e/dev/fusesão bloqueados. - Executando Docker-in-Docker Dentro do gVisor e de Sandboxes Aninhados com o gvx: Por que
apt-get install docker.ioe orunscaninhado falham dentro dogVisorpor causa de cinco barreiras no kernel, e como ogvxexecuta odocker:dindaninhado usando um loop de futex em memória compartilhada. - Ambientes Docker Isolados e Rápidos para Rollouts de RL de Agentes de IA com o gvx: Escalonamento de
SWE-benche de rollouts de aprendizado por reforço de agentes com centenas de instâncias paralelas dedocker:dindsem privilégios e redefinições de volumes.ext4com copy-on-write de 100 milissegundos.