gvx: Ejecución de Linux Containers, Docker y Kubernetes en un único proceso sin privilegios

Introducción: contenedores reforzados en seguridad sin root ni daemons del host
Ejecutar contenedores Linux, docker build o Kubernetes dentro de una máquina de desarrollo, un runner de CI o un nodo de nube multiinquilino suele imponer un compromiso de seguridad incómodo:
- En Linux, los runtimes OCI estándar (
runc,crun,runsc) requieren privilegios deroot, exposición del host en/var/run/docker.sock,--privileged(CAP_SYS_ADMIN) o espacios de nombres de usuario (unshare(CLONE_NEWUSER)) que amplían la superficie de ataque del kernel del host. - En Windows y macOS, los motores de contenedores (
Docker Desktop,Podman Machine,Lima,OrbStack) arrancan un kernel completo de máquina virtual Linux dentro de WSL2, Hyper-V oVirtualization.frameworky ejecutan un daemon persistente en segundo plano dentro de esa VM.
gvx (la CLI rungv y el SDK de Go z.gfire.dev/gvx/pkg/gvx) proporciona una alternativa segura y de privilegio cero. Construido como una bifurcación del kernel Linux en espacio de usuario Sentry y de la pila de red en espacio de usuario netstack de gVisor de Google, con seguimiento de cambios upstream, gvx ejecuta binarios ELF64 linux/amd64 y linux/arm64 sin modificar y contenedores OCI directamente dentro de un único proceso del host sin privilegios y autobloqueado en Windows, macOS y Linux.
Garantía de seguridad (defensa en profundidad en tres anillos):
gvxnunca debilita los límites de seguridad del sistema operativo del host, nunca instala drivers de kernel y nunca requiereroot,sudo,--privilegeden el host niCLONE_NEWUSER. Antes de ejecutar una sola instrucción del invitado,gvxbloquea su propio proceso del host mediante Restricted Tokens y Job Objects de Windows, Seatbelt de macOS (deny default) oPR_SET_NO_NEW_PRIVSde Linux junto conseccompBPF. Todas las llamadas al sistema del invitado, los privilegios virtualesUID 0, los montajes (VFS2) y las tablas de red (netstack) quedan estrictamente confinados dentro del kernelSentryde Go, seguro frente a fallos de memoria.
| Runtime | Sistema operativo del host | Arranque en frío | Huella de memoria | Requiere admin o sudo | Daemon de VM en segundo plano | Ejecución directa de imagen OCI |
|---|---|---|---|---|---|---|
gvx (whpx / hvf) | Windows (amd64, arm64) y macOS (arm64) | ~20 ms | ~30 MiB | No | No | Sí |
gvx (systrap) | Windows (amd64, arm64) y Linux (amd64, arm64) | ~25 a 65 ms | ~31 MiB | No | No | Sí |
| WSL2 / Docker Desktop | Windows y macOS | 800 a 2500 ms | > 512 MiB | Sí (configuración inicial) | Sí | Requiere daemon |
| QEMU (sistema completo) | Windows y macOS | 1500 a 4000 ms | > 256 MiB | No | No | Requiere imagen de disco |
En Windows o macOS no arranca ningún kernel de máquina virtual Linux. No se ejecuta ningún daemon en segundo plano en el host. No se requieren privilegios de root, sudo, espacios de nombres de usuario (CLONE_NEWUSER), mount(2), drivers TUN/TAP ni CAP_NET_ADMIN en el sistema operativo del host. Dentro de ese proceso sin privilegios, gvx ejecuta Docker-in-Docker (docker:dind), docker build, docker compose y Kubernetes de alta disponibilidad con múltiples nodos (k3s con etcd embebido y el CNI Calico).
Ciclo de vida del contenedor y bloqueo del host en tres anillos
Cada sandbox de gvx se ejecuta dentro de un único proceso del host sin privilegios y aplica un ciclo de vida de tres fases: preabrir todos los descriptores de archivo y sockets del host necesarios, bloquear el proceso del sistema operativo del host mediante pkg/hostlockdown y entrar en el bucle de ejecución Sentry dentro del proceso.

Antes de transferir el control a la primera instrucción del invitado, gvx preabre sus descriptores de capas de imagen .erofs en caché, sus descriptores de archivos de volumen .ext4, el handle *os.Root de /workspace y los sockets de red del host, y después invoca hostlockdown.Apply():
- Windows NT (
lockdown_windows.go): Llama aAdjustTokenPrivilegesconSE_PRIVILEGE_REMOVEDpara eliminar privilegios in situ, vincula un token de suplantación restringidoDISABLE_MAX_PRIVILEGEcon integridad Baja (S-1-16-4096), asigna el proceso a un Job Object anónimo conJOB_OBJECT_LIMIT_ACTIVE_PROCESS = 1(bloqueando la creación de cualquier proceso hijo) y habilitaDisallowWin32kSystemCalls = 1medianteSetProcessMitigationPolicy. - macOS XNU (
lockdown_darwin.go): EstableceRLIMIT_NPROC = 0y aplica un perfil Seatbelt (sandbox_init_with_parameters) que parte de(version 1)(deny default)y bloqueaprocess-fork,process-exec*y todas las nuevas aperturas de archivos del host. - Linux (
lockdown_linux.go+pkg/gvx/syscallfilter_linux.go): EstablecePR_SET_NO_NEW_PRIVSe instala la lista blanca BPFseccompde Sentry en todos los hilos (SECCOMP_FILTER_FLAG_TSYNC), con reglasEPERMexplícitas paraexecve,execveat,clone,openatysocket.
Interceptación de llamadas al sistema multiplataforma y paginación de 4 KB (pkg/sentry/platform)
Los binarios ELF64 de Linux asumen una granularidad de página de memoria de 4 KB (0x1000) para los mapeos de segmentos con mmap(MAP_FIXED). Sin embargo, VirtualAlloc y MapViewOfFile3 de Windows imponen una alineación de 64 KB (0x10000), y macOS en Apple Silicon (darwin/arm64) usa páginas del host de 16 KB (0x4000). Además, cada sistema operativo requiere un mecanismo distinto para interceptar las instrucciones SYSCALL (amd64) y SVC #0 (arm64) de Linux sin arrancar una VM Linux.
gvx resuelve ambos problemas mediante cuatro backends de ejecución que implementan pkg/sentry/platform.Context:

1. Motor de hardware de Windows: platform/whpx (WinHvPlatform.dll)
En windows/amd64 y windows/arm64, platform/whpx crea una partición de hipervisor ligera mediante WHvCreatePartition sin arrancar el kernel de un SO invitado:
- Desacoplador de paginación de 64 KB a 4 KB:
whpxregistra cada arena de 64 MiB depgalloc.MemoryFileen la partición una sola vez medianteWHvMapGpaRange. Las llamadas posteriores aMapFileyUnmapmodifican únicamente las tablas de páginas del invitado dentro de la partición, aplicando permisosR/W/Xexactos de 4 KB pese a la granularidad de asignación de 64 KB de Windows. - Intercambio de registros sin llamadas a la API mediante
CommPage:- En
amd64, unSYSCALLdel invitado en Ring 3 entra en un stub de Ring 0 dentro de la partición medianteMSR_LSTAR(stubgen_x86.go), que vuelca los GPR de la ruta rápida de llamadas al sistema (RAX, RDI, RSI, RDX, R10, R8, R9, RIP, RSP) en unaCommPagecompartida de 4 KB y ejecutaHLTpara salir deWHvRunVirtualProcessor. - En
arm64, unSVC #0del invitado en EL0 entra en los vectores de EL1 (stubgen_a64.go), vuelcaX0..X8, SP_EL0, ELR_EL1, PSTATEenCommPagey carga desde el GPA sin respaldo en cero para provocar una salida por acceso a memoria hacia Sentry.
- En
2. Motor de hardware de macOS: platform/hvf (Hypervisor.framework)
En Apple Silicon (darwin/arm64), platform/hvf ejecuta los hilos del contenedor dentro de vCPU de Hypervisor.framework (hv_vm_create, hv_vcpu_run):
- Traducción MMU de dos etapas de 4 KB sobre 16 KB: La etapa 2 (
hv_vm_map) mapea fragmentos de 1 GiB del archivo de memoria en el espacio de direcciones físicas intermedias (IPA) una sola vez, con granularidad de página del host de 16 KB y conHV_MEMORY_READ | HV_MEMORY_WRITE | HV_MEMORY_EXEC. Dentro de la vCPU, el stub EL1bluepill(bluepill_arm64.go) configuraTCR_EL1.TG0 = 0b00(granularidad de 4 KB) enTTBR0_EL1y rellena las entradas de tabla de páginas de la etapa 1 de 4 KB con permisos exactosR-XoRW-(UXN=1). Como la MMU de ARM64 aplica la intersección de los permisos de la etapa 1 y la etapa 2, el hardware captura los fallos de permiso en límites exactos de 4 KB. - Interceptación de llamadas al sistema: Un
SVC #0del invitado en EL0 transfiere el control aVBAR_EL1en EL1, vuelca los registros en laCommPagecompartida de 4 KB y ejecutaHVC #0para regresar desdehv_vcpu_runal hilo Go de Sentry.
3. Alternativa en modo usuario de Windows: platform/systrap (winsystrap)
Cuando la virtualización por hardware no está disponible (por ejemplo, dentro de VM anidadas en la nube), winsystrap ejecuta el código del invitado en modo usuario en windows/amd64 y windows/arm64:
- Aislamiento por hardware de
CR3/TTBR0_EL1: Los hilos del invitado se ejecutan dentro de subprocesos stub aislados que se arrancan desde un ejecutable generado de 1 KB (stubimage_xplat.go), confinados por un token de integridad Baja y un Job Object. - Parcheo binario in situ e IPC
winSysmsg: Enamd64, el parcheador (patcher_amd64_xplat.go) sustituye las instruccionessyscall(0f 05) de 2 bytes por saltosjmp rel32de 5 bytes (o por trampas de punto de interrupciónCC 90cuando no hay una ventana de 5 bytes disponible) hacia una isla de ruta rápida. Enarm64,svc #0se parchea con una ramaBhacia un slot de isla. La isla vuelca los registros en la página compartidawinSysmsgy se sincroniza con Sentry medianteWaitOnAddressyWakeByAddressSingle, mientras que las trampas no parcheadas y los fallos de página se detienen en un Windows Debug Object (WaitForDebugEventEx).
4. Motor en modo usuario de Linux: platform/systrap
En linux/amd64 y linux/arm64, systrap ejecuta hilos del invitado (sysmsg) que interceptan las llamadas al sistema del invitado mediante SECCOMP_RET_TRAP (SIGSYS) y parchean en binario los sitios syscall hacia un trampolín de memoria compartida (sysmsg) sincronizado mediante SYS_FUTEX. Mientras tanto, un hilo auxiliar syscallThread (syscall_thread.go) ejecuta las operaciones sobre el espacio de direcciones (mmap, munmap, clone) mediante SECCOMP_RET_USER_NOTIF (_RUN_SECCOMP_LOOP). Cuando gvx se ejecuta dentro de un sandbox externo de gVisor (runsc) donde la única ranura SECCOMP_FILTER_FLAG_NEW_LISTENER devuelve EBUSY, gvx cambia automáticamente syscallThread a un bucle de futex en memoria compartida (_RUN_SYSCALL_LOOP), tal como se detalla en la Parte 3 de esta serie.
Cómo gvx asigna las llamadas al sistema de Linux del invitado a subsistemas virtuales y handles del host
Una vez que un motor de plataforma intercepta una instrucción SYSCALL (amd64) o SVC #0 (arm64) del invitado y escribe los registros del invitado en la página compartida de 4 KB (CommPage, sysmsg o winSysmsg), el control regresa a la goroutine de tarea de Go Sentry (pkg/sentry/kernel/task_syscall.go). En lugar de reenviar las llamadas al sistema de Linux al kernel del sistema operativo subyacente, Task.doSyscall() guarda el estado original de los registros (SyscallSaveOrig()), evalúa los filtros BPF seccomp y las paradas ptrace del invitado, y despacha el número de llamada al sistema mediante linux.AMD64 o linux.ARM64 (pkg/sentry/syscalls/linux/linux64.go, que cubre unas 370 llamadas al sistema de Linux) hacia cuatro dominios internos:

| Dominio de llamadas al sistema | Llamadas al sistema representativas del invitado Linux | Subsistema interno de gvx (pkg/sentry) | Interacción con el límite del SO del host (tras hostlockdown) |
|---|---|---|---|
| A. Procesos, espacios de nombres, montajes y sincronización | clone, execve, unshare, setns, mount, umount2, pivot_root, setuid, capset, futex, epoll_wait, pipe2 | kernel.Task, credenciales virtuales, tabla de futex y VFS2 en memoria (procfs, sysfs, cgroupfs v2, tmpfs, overlayfs) | Cero llamadas al sistema del host (100 % estado Go en memoria; completamente invisible para el SO del host) |
B. Memoria virtual (MM) | mmap, munmap, mremap, mprotect, brk, madvise, memfd_create | mm.MemoryManager (árbol de VMA de 4 KB) y pgalloc.MemoryFile | Actualizaciones bidireccionales de tablas de páginas de 4 KB mediante WHvMapGpaRange, hv_vm_map + TTBR0_EL1 o mmap/mprotect del stub |
C. Archivos y almacenamiento (VFS2) | openat, pread64, pwrite64, getdents64, statx, renameat2, setxattr, mknod, inotify_add_watch | Enrutador de la tabla de montajes vfs.VirtualFilesystem (fsimpl/erofs, fsimpl/ext4lite, fsimpl/hostfs v2) | pread/pwrite bidireccionales sobre 1 FD .erofs fijado o 1 FD .ext4, o recorridos relativos acotados sobre *os.Root |
| D. Redes y Netfilter | socket, bind, listen, connect, sendmsg, recvmsg, netlink, setsockopt(IPT_SO_SET_REPLACE) | pkg/tcpip en espacio de usuario (lo, eth0, docker0, veth, vxlan, wireguard, ipset, iptables-legacy) | El tráfico interno del sandbox usa cero llamadas al sistema del host; el tráfico externo se asigna a net.Dial/net.Listen (-net=nat) o a 1 socket UDP (-net=wg) |
Cuando el manejador devuelve (rval, ctrl, err), Task.doSyscallInvoke() convierte cualquier error en un errno negativo de Linux (-ExtractErrno(err, sysno)), escribe el valor de retorno de nuevo en la página compartida de registros (RAX en amd64, X0 en arm64) y reanuda la ejecución del invitado.
Almacenamiento (VFS2) y redes en espacio de usuario de doble vía (netstack)
Todas las operaciones de archivos y de red del invitado se atienden dentro de Go Sentry a través de tres sistemas de archivos VFS2 y dos pistas de pkg/netbackend:

| Motor de almacenamiento | Rol de montaje en gvx | Recurso de respaldo del host | Llamadas openat / NtCreateFile del host en tiempo de ejecución | Escaneos por archivo de Windows Defender (WdFilter.sys) |
|---|---|---|---|---|
fsimpl/erofs | Capas inferiores de imagen OCI de solo lectura | Un único archivo <sha256>.erofs por capa | 0 | 0 |
fsimpl/ext4lite | Volúmenes con nombre (-v name:/path) | Un único archivo <name>.ext4 por volumen | 0 | 0 |
fsimpl/hostfs v2 | Espacio de trabajo del host (-workspace, -v /host:/guest) | Directorio del host fijado mediante *os.Root | O(depth) por apertura no cacheada | 1 por cierre de archivo del host |
Por qué ext4lite importa para Docker, bases de datos y gestores de paquetes
Extraer o escribir decenas de miles de archivos de contenedor directamente en NTFS de Windows o APFS de macOS activa los escaneos del minifiltro antivirus (WdFilter.sys), colisiones por sensibilidad a mayúsculas y la ausencia de atributos extendidos POSIX.
pkg/sentry/fsimpl/ext4lite implementa un sistema de archivos ext4 en Go puro dentro de un único archivo del host preabierto (%LOCALAPPDATA%\gvx\cache\volumes\<name>.ext4 en Windows, ~/Library/Caches/gvx/volumes/<name>.ext4 en macOS o ~/.cache/gvx/volumes/<name>.ext4 en Linux), con las siguientes características:
- Bloques de 4 KiB y grupos de bloques dinámicos de 16 MiB (
meta_bg): Crece dinámicamente sin bloqueos por asignación de archivos dispersos (fallocateen Linux,fcntl(F_PREALLOCATE)en macOS,FileAllocationInfoen Windows) y compacta los grupos finales no utilizados al desmontar limpiamente. - Diario JBD2 en modo ordenado (inodo 8): Confirmaciones de transacciones atómicas con sumas de comprobación CRC-32 y bloques de revocación, además de recuperación mediante
fsckLite. - Soporte de capa superior de
overlayfs: Implementa los atributos extendidostrusted.overlay.*ysecurity.capability, los whiteouts de dispositivos de caracteres (0:0), el informe de entradas de directoriod_typeyRENAME_EXCHANGE.
Cargas de trabajo prácticas: motor Docker, Kubernetes (k3s + Calico) y SDK de Go
Instalar rungv: versiones precompiladas con atestación SLSA o go install
Puede descargar un archivo rungv precompilado para linux/amd64, linux/arm64, darwin/arm64, windows/amd64 o windows/arm64 desde Releases de GitHub (cada uno compilado por el pipeline de publicación SLSA con un paquete de procedencia de Sigstore y SHA256SUMS), o instalarlo directamente desde el código fuente con go install:
1# Opción A: Descargar una versión precompilada y verificar la procedencia de compilación 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# Opción B: Instalar directamente mediante Go (Linux / Windows)
9CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest
En macOS (
darwin/arm64), el archivo de la versión precompilada ya está firmado con el derechocom.apple.security.hypervisor(xattr -d com.apple.quarantine rungvelimina la marca de cuarentena del navegador). Si compila localmente congo installogo builden macOS, firme el binario concodesign --sign - --entitlements entitlements.plist --force $(which rungv).
1. Ejecutar un motor Docker sin privilegios (docker:dind)
Siguiendo docs/docker-engine.md, puede ejecutar un contenedor docker:dind sin modificar dentro de rungv en Windows, macOS o Linux y controlarlo con su CLI docker del host:
1# 1. Crear el directorio del host para el socket Unix reenviado
2mkdir -p ~/.gvx
3
4# 2. Iniciar docker:dind dentro de un sandbox de gvx con un volumen 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
En una segunda terminal, apunte su CLI docker del host al socket del sandbox:
1docker context create gvx --docker host=unix://$HOME/.gvx/docker.sock
2docker context use gvx
3
4docker info --format '{{.Driver}} {{.OperatingSystem}}'
5# Salida: overlayfs Alpine Linux ... (en contenedor)
6
7docker build -t myapp ./app
8docker compose up -d
2. Ejecutar Kubernetes (k3s + Calico) en sandboxes sin privilegios
Dado que gvx implementa /dev/kmsg, cgroup v2 (el OOM killer de memory.max y la limitación de ancho de banda de cpu.max), /proc/diskstats, los dispositivos netlink wireguard y vxlan, ipset e iptables-legacy (addrtype, conntrack, statistic, recent), una imagen rancher/k3s sin modificar alcanza el estado Ready dentro de un único proceso 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
También puede unir varios nodos gvx mediante WireGuard (flannel-backend=wireguard-native o una malla wg0) con alta disponibilidad de etcd embebido, o ejecutar Calico (calico-vxlan.yaml con FELIX_IPTABLESBACKEND=Legacy) para aplicar NetworkPolicy de Kubernetes íntegramente en espacio de usuario.
3. Incrustar contenedores Linux en Go mediante z.gfire.dev/gvx/pkg/gvx
Las aplicaciones en Go pueden incrustar contenedores Linux directamente, sin un daemon de contenedores 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}
Más lecturas de esta serie
- gvx: ejecutar contenedores Linux, Docker y Kubernetes en un único proceso sin privilegios (esta entrada)
- Ejecutar Docker, docker build y docker compose sin root ni espacios de nombres de usuario: Cómo arrancar
docker:dinden sandboxes muy restringidos dondesudo,unshare(CLONE_NEWUSER),mount(2)y/dev/fuseestán bloqueados. - Ejecutar Docker-in-Docker dentro de gVisor y sandboxes anidados con gvx: Por qué fallan
apt-get install docker.ioyrunscanidado dentro degVisora través de cinco barreras del kernel, y cómogvxejecutadocker:dindanidado mediante un bucle de futex en memoria compartida. - Entornos Docker aislados y rápidos para rollouts de RL de agentes de IA con gvx: Escalado de
SWE-benchy de rollouts de aprendizaje por refuerzo de agentes con cientos de instancias paralelas dedocker:dindsin privilegios y reinicios de volúmenes.ext4con copy-on-write en 100 milisegundos.