GoSuda

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

By Lemon Mint views ...

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 de root, 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 o Virtualization.framework y 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): gvx nunca debilita los límites de seguridad del sistema operativo del host, nunca instala drivers de kernel y nunca requiere root, sudo, --privileged en el host ni CLONE_NEWUSER. Antes de ejecutar una sola instrucción del invitado, gvx bloquea su propio proceso del host mediante Restricted Tokens y Job Objects de Windows, Seatbelt de macOS (deny default) o PR_SET_NO_NEW_PRIVS de Linux junto con seccomp BPF. Todas las llamadas al sistema del invitado, los privilegios virtuales UID 0, los montajes (VFS2) y las tablas de red (netstack) quedan estrictamente confinados dentro del kernel Sentry de Go, seguro frente a fallos de memoria.

RuntimeSistema operativo del hostArranque en fríoHuella de memoriaRequiere admin o sudoDaemon de VM en segundo planoEjecución directa de imagen OCI
gvx (whpx / hvf)Windows (amd64, arm64) y macOS (arm64)~20 ms~30 MiBNoNoSí
gvx (systrap)Windows (amd64, arm64) y Linux (amd64, arm64)~25 a 65 ms~31 MiBNoNoSí
WSL2 / Docker DesktopWindows y macOS800 a 2500 ms> 512 MiBSí (configuración inicial)SíRequiere daemon
QEMU (sistema completo)Windows y macOS1500 a 4000 ms> 256 MiBNoNoRequiere 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.

Ciclo de vida del contenedor gvx y autobloqueo del host

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():

  1. Windows NT (lockdown_windows.go): Llama a AdjustTokenPrivileges con SE_PRIVILEGE_REMOVED para eliminar privilegios in situ, vincula un token de suplantación restringido DISABLE_MAX_PRIVILEGE con integridad Baja (S-1-16-4096), asigna el proceso a un Job Object anónimo con JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 1 (bloqueando la creación de cualquier proceso hijo) y habilita DisallowWin32kSystemCalls = 1 mediante SetProcessMitigationPolicy.
  2. macOS XNU (lockdown_darwin.go): Establece RLIMIT_NPROC = 0 y aplica un perfil Seatbelt (sandbox_init_with_parameters) que parte de (version 1)(deny default) y bloquea process-fork, process-exec* y todas las nuevas aperturas de archivos del host.
  3. Linux (lockdown_linux.go + pkg/gvx/syscallfilter_linux.go): Establece PR_SET_NO_NEW_PRIVS e instala la lista blanca BPF seccomp de Sentry en todos los hilos (SECCOMP_FILTER_FLAG_TSYNC), con reglas EPERM explícitas para execve, execveat, clone, openat y socket.

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:

Mecánica de interceptación de llamadas al sistema y paginación de 4 KB en la plataforma gvx

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: whpx registra cada arena de 64 MiB de pgalloc.MemoryFile en la partición una sola vez mediante WHvMapGpaRange. Las llamadas posteriores a MapFile y Unmap modifican únicamente las tablas de páginas del invitado dentro de la partición, aplicando permisos R/W/X exactos 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, un SYSCALL del invitado en Ring 3 entra en un stub de Ring 0 dentro de la partición mediante MSR_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 una CommPage compartida de 4 KB y ejecuta HLT para salir de WHvRunVirtualProcessor.
    • En arm64, un SVC #0 del invitado en EL0 entra en los vectores de EL1 (stubgen_a64.go), vuelca X0..X8, SP_EL0, ELR_EL1, PSTATE en CommPage y carga desde el GPA sin respaldo en cero para provocar una salida por acceso a memoria hacia Sentry.

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 con HV_MEMORY_READ | HV_MEMORY_WRITE | HV_MEMORY_EXEC. Dentro de la vCPU, el stub EL1 bluepill (bluepill_arm64.go) configura TCR_EL1.TG0 = 0b00 (granularidad de 4 KB) en TTBR0_EL1 y rellena las entradas de tabla de páginas de la etapa 1 de 4 KB con permisos exactos R-X o RW- (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 #0 del invitado en EL0 transfiere el control a VBAR_EL1 en EL1, vuelca los registros en la CommPage compartida de 4 KB y ejecuta HVC #0 para regresar desde hv_vcpu_run al 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: En amd64, el parcheador (patcher_amd64_xplat.go) sustituye las instrucciones syscall (0f 05) de 2 bytes por saltos jmp rel32 de 5 bytes (o por trampas de punto de interrupción CC 90 cuando no hay una ventana de 5 bytes disponible) hacia una isla de ruta rápida. En arm64, svc #0 se parchea con una rama B hacia un slot de isla. La isla vuelca los registros en la página compartida winSysmsg y se sincroniza con Sentry mediante WaitOnAddress y WakeByAddressSingle, 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:

Cómo gvx asigna las llamadas al sistema de Linux del invitado a subsistemas y al sistema operativo del host

Dominio de llamadas al sistemaLlamadas al sistema representativas del invitado LinuxSubsistema 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ónclone, execve, unshare, setns, mount, umount2, pivot_root, setuid, capset, futex, epoll_wait, pipe2kernel.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_createmm.MemoryManager (árbol de VMA de 4 KB) y pgalloc.MemoryFileActualizaciones 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_watchEnrutador 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 Netfiltersocket, 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:

Motores de almacenamiento VFS2 de gvx y netstack de doble vía

Motor de almacenamientoRol de montaje en gvxRecurso de respaldo del hostLlamadas openat / NtCreateFile del host en tiempo de ejecuciónEscaneos por archivo de Windows Defender (WdFilter.sys)
fsimpl/erofsCapas inferiores de imagen OCI de solo lecturaUn único archivo <sha256>.erofs por capa00
fsimpl/ext4liteVolúmenes con nombre (-v name:/path)Un único archivo <name>.ext4 por volumen00
fsimpl/hostfs v2Espacio de trabajo del host (-workspace, -v /host:/guest)Directorio del host fijado mediante *os.RootO(depth) por apertura no cacheada1 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 (fallocate en Linux, fcntl(F_PREALLOCATE) en macOS, FileAllocationInfo en 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 extendidos trusted.overlay.* y security.capability, los whiteouts de dispositivos de caracteres (0:0), el informe de entradas de directorio d_type y RENAME_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 derecho com.apple.security.hypervisor (xattr -d com.apple.quarantine rungv elimina la marca de cuarentena del navegador). Si compila localmente con go install o go build en macOS, firme el binario con codesign --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

  1. gvx: ejecutar contenedores Linux, Docker y Kubernetes en un único proceso sin privilegios (esta entrada)
  2. Ejecutar Docker, docker build y docker compose sin root ni espacios de nombres de usuario: Cómo arrancar docker:dind en sandboxes muy restringidos donde sudo, unshare(CLONE_NEWUSER), mount(2) y /dev/fuse están bloqueados.
  3. Ejecutar Docker-in-Docker dentro de gVisor y sandboxes anidados con gvx: Por qué fallan apt-get install docker.io y runsc anidado dentro de gVisor a través de cinco barreras del kernel, y cómo gvx ejecuta docker:dind anidado mediante un bucle de futex en memoria compartida.
  4. Entornos Docker aislados y rápidos para rollouts de RL de agentes de IA con gvx: Escalado de SWE-bench y de rollouts de aprendizaje por refuerzo de agentes con cientos de instancias paralelas de docker:dind sin privilegios y reinicios de volúmenes .ext4 con copy-on-write en 100 milisegundos.