GoSuda

gvx: Esecuzione di Linux Containers, Docker e Kubernetes in un unico processo non privilegiato

By Lemon Mint views ...

Introduzione: contenitori rafforzati dal punto di vista della sicurezza, senza root né daemon sull'host

Eseguire container Linux, docker build o Kubernetes all'interno di una macchina di sviluppo, di un runner CI o di un nodo cloud multi-tenant comporta di solito un compromesso di sicurezza poco confortevole:

  • Su Linux, i runtime OCI standard (runc, crun, runsc) richiedono privilegi root, l'esposizione di /var/run/docker.sock dell'host, --privileged (CAP_SYS_ADMIN) oppure user namespaces (unshare(CLONE_NEWUSER)), che ampliano la superficie di attacco del kernel dell'host.
  • Su Windows e macOS, i motori di container (Docker Desktop, Podman Machine, Lima, OrbStack) avviano un kernel Linux completo in una macchina virtuale all'interno di WSL2, Hyper-V o Virtualization.framework ed eseguono un daemon persistente in background all'interno di tale VM.

gvx (CLI rungv e SDK Go z.gfire.dev/gvx/pkg/gvx) offre un'alternativa sicura e a privilegi zero. Realizzato come fork tracciabile a monte del kernel Linux in user-space gVisor di Google (Sentry) e dello stack di rete in user-space (netstack), gvx esegue direttamente binari linux/amd64 e linux/arm64 ELF64 non modificati e container OCI all'interno di un unico processo host non privilegiato e auto-bloccato su Windows, macOS e Linux.

Garanzia di sicurezza (difesa in profondità a tre anelli): gvx non indebolisce mai i confini di sicurezza del sistema operativo host, non installa mai driver del kernel e non richiede mai root, sudo, --privileged sull'host o CLONE_NEWUSER. Prima di eseguire una singola istruzione del guest, gvx blocca il proprio processo host tramite Windows Restricted Tokens e Job Objects, macOS Seatbelt (deny default) oppure Linux PR_SET_NO_NEW_PRIVS + BPF seccomp. Tutte le system call del guest, i privilegi virtuali UID 0, i mount (VFS2) e le tabelle di rete (netstack) sono rigorosamente confinati all'interno del kernel Go Sentry, sicuro dal punto di vista della memoria.

RuntimeSistema operativo hostAvvio a freddoOccupazione di memoriaAdmin o sudo necessariDaemon VM in backgroundEsecuzione diretta di immagini OCI
gvx (whpx / hvf)Windows (amd64, arm64) e macOS (arm64)~20 ms~30 MiBNoNoSì
gvx (systrap)Windows (amd64, arm64) e Linux (amd64, arm64)~25 a 65 ms~31 MiBNoNoSì
WSL2 / Docker DesktopWindows e macOS800 a 2500 ms> 512 MiBSì (configurazione iniziale)SìRichiede daemon
QEMU (Full System)Windows e macOS1500 a 4000 ms> 256 MiBNoNoRichiede immagine disco

Su Windows o macOS non viene avviato alcun kernel di macchina virtuale Linux. Sull'host non viene eseguito alcun daemon in background. Sul sistema operativo host non sono richiesti privilegi root, sudo, user namespaces (CLONE_NEWUSER), mount(2), driver TUN/TAP o CAP_NET_ADMIN. All'interno di quel processo non privilegiato, gvx esegue Docker-in-Docker (docker:dind), docker build, docker compose e Kubernetes High-Availability multi-nodo (k3s con etcd integrato e CNI Calico).


Ciclo di vita del container e blocco dell'host a tre anelli

Ogni sandbox gvx viene eseguita all'interno di un unico processo host non privilegiato e applica un ciclo di vita in tre fasi: pre-apertura di tutti i file descriptor e di tutti i socket host richiesti, blocco del processo del sistema operativo host tramite pkg/hostlockdown e ingresso nel ciclo di esecuzione Sentry in-process.

Ciclo di vita del container gvx e auto-blocco dell'host

Prima di trasferire il controllo alla prima istruzione del guest, gvx pre-apre i descrittori delle immagini .erofs in cache, i descrittori dei volumi .ext4, l'handle *os.Root di /workspace e i socket di rete dell'host, quindi invoca hostlockdown.Apply():

  1. Windows NT (lockdown_windows.go): Chiama AdjustTokenPrivileges con SE_PRIVILEGE_REMOVED per rimuovere i privilegi sul posto, associa un token di impersonificazione ristretto DISABLE_MAX_PRIVILEGE con livello di integrità Low (S-1-16-4096), assegna il processo a un Job Object anonimo con JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 1 (bloccando la creazione di qualsiasi processo figlio) e abilita DisallowWin32kSystemCalls = 1 tramite SetProcessMitigationPolicy.
  2. macOS XNU (lockdown_darwin.go): Imposta RLIMIT_NPROC = 0 e applica un profilo Seatbelt (sandbox_init_with_parameters) a partire da (version 1)(deny default), che blocca process-fork, process-exec* e tutte le nuove aperture di file dell'host.
  3. Linux (lockdown_linux.go + pkg/gvx/syscallfilter_linux.go): Imposta PR_SET_NO_NEW_PRIVS e installa la allowlist BPF seccomp di Sentry su tutti i thread (SECCOMP_FILTER_FLAG_TSYNC), con regole EPERM esplicite per execve, execveat, clone, openat e socket.

Intercettazione cross-platform delle system call e paginazione da 4 KB (pkg/sentry/platform)

I binari Linux ELF64 presuppongono una granularità delle pagine di memoria di 4 KB (0x1000) per i mapping dei segmenti mmap(MAP_FIXED). Tuttavia, VirtualAlloc e MapViewOfFile3 di Windows impongono un allineamento a 64 KB (0x10000), mentre macOS su Apple Silicon (darwin/arm64) utilizza pagine host da 16 KB (0x4000). Inoltre, ciascun sistema operativo richiede un meccanismo diverso per intercettare le istruzioni Linux SYSCALL (amd64) e SVC #0 (arm64) senza avviare una VM Linux.

gvx risolve entrambi i problemi tramite quattro backend di esecuzione che implementano pkg/sentry/platform.Context:

Meccanismi di intercettazione delle system call e di paginazione da 4KB in gvx

1. Motore hardware Windows: platform/whpx (WinHvPlatform.dll)

Su windows/amd64 e windows/arm64, platform/whpx crea una partizione hypervisor leggera tramite WHvCreatePartition senza avviare il kernel di un sistema operativo guest:

  • Disaccoppiatore di paginazione da 64 KB a 4 KB: whpx registra ciascuna arena da 64 MiB di pgalloc.MemoryFile nella partizione una sola volta tramite WHvMapGpaRange. Le successive chiamate MapFile e Unmap modificano solo le tabelle delle pagine del guest all'interno della partizione, applicando permessi R/W/X esatti a 4 KB, nonostante la granularità di allocazione di 64 KB di Windows.
  • Scambio di registri a zero API tramite CommPage:
    • Su amd64, un'istruzione SYSCALL del guest in Ring-3 entra in uno stub MSR_LSTAR in Ring-0 interno alla partizione (stubgen_x86.go), che salva i GPR della system call fast-path (RAX, RDI, RSI, RDX, R10, R8, R9, RIP, RSP) in una CommPage condivisa da 4 KB ed esegue HLT per uscire da WHvRunVirtualProcessor.
    • Su arm64, un'istruzione SVC #0 del guest in EL0 entra nei vettori EL1 (stubgen_a64.go), salva X0..X8, SP_EL0, ELR_EL1, PSTATE nella CommPage e carica dal GPA zero non associato per innescare un'uscita di memoria verso Sentry.

2. Motore hardware macOS: platform/hvf (Hypervisor.framework)

Su Apple Silicon (darwin/arm64), platform/hvf esegue i thread dei container all'interno dei vCPU di Hypervisor.framework (hv_vm_create, hv_vcpu_run):

  • Traduzione MMU a due stadi, 4 KB su 16 KB: lo Stage-2 (hv_vm_map) mappa porzioni di memory-file da 1 GiB nello spazio degli Intermediate Physical Address (IPA) una sola volta, con granularità di pagina host di 16 KB, con HV_MEMORY_READ | HV_MEMORY_WRITE | HV_MEMORY_EXEC. All'interno del vCPU, lo stub EL1 bluepill (bluepill_arm64.go) configura TCR_EL1.TG0 = 0b00 (granule da 4 KB) in TTBR0_EL1 e popola le voci di tabella delle pagine Stage-1 da 4 KB con permessi esatti R-X oppure RW- (UXN=1). Poiché la MMU ARM64 applica l'intersezione dei permessi Stage-1 e Stage-2, l'hardware intercetta le violazioni di permesso ai confini esatti di 4 KB.
  • Intercettazione delle system call: Un'istruzione SVC #0 del guest in EL0 viene intercettata da VBAR_EL1 in EL1, salva i registri nella CommPage condivisa da 4 KB ed esegue HVC #0 per restituire il controllo da hv_vcpu_run al thread Go di Sentry.

3. Fallback user-mode per Windows: platform/systrap (winsystrap)

Quando la virtualizzazione hardware non è disponibile (ad esempio all'interno di VM cloud annidate), winsystrap esegue il codice guest in user mode su windows/amd64 e windows/arm64:

  • Isolamento hardware tramite CR3 / TTBR0_EL1: I thread del guest vengono eseguiti all'interno di sottoprocessi stub isolati, avviati da un eseguibile generato da 1 KB (stubimage_xplat.go), confinati da un token a bassa integrità e da un Job Object.
  • Patching binario sul posto e IPC winSysmsg: Su amd64, il patcher (patcher_amd64_xplat.go) sostituisce le istruzioni syscall (0f 05) di 2 byte con salti jmp rel32 di 5 byte (oppure con trap di breakpoint CC 90 quando una finestra di 5 byte non è disponibile) verso un'isola fast-path. Su arm64, svc #0 viene patchata con un ramo B verso uno slot dell'isola. L'isola salva i registri nella pagina condivisa winSysmsg e si sincronizza con Sentry tramite WaitOnAddress e WakeByAddressSingle, mentre i trap non patchati e i page fault si fermano su un Windows Debug Object (WaitForDebugEventEx).

4. Motore user-mode Linux: platform/systrap

Su linux/amd64 e linux/arm64, systrap esegue thread del guest (sysmsg) che intercettano le system call del guest tramite SECCOMP_RET_TRAP (SIGSYS) e applicano il patching binario dei siti syscall verso un trampolino in memoria condivisa (sysmsg), sincronizzato tramite SYS_FUTEX. Nel frattempo, un thread stub syscallThread (syscall_thread.go) esegue le operazioni sullo spazio degli indirizzi (mmap, munmap, clone) tramite SECCOMP_RET_USER_NOTIF (_RUN_SECCOMP_LOOP). Quando gvx viene eseguito all'interno di una sandbox gVisor (runsc) esterna, in cui l'unico slot SECCOMP_FILTER_FLAG_NEW_LISTENER restituisce EBUSY, gvx passa automaticamente syscallThread a un loop futex in memoria condivisa (_RUN_SYSCALL_LOOP), come descritto in dettaglio nella Parte 3 di questa serie.


Come gvx mappa le system call Linux del guest su sottosistemi virtuali e handle dell'host

Una volta che un motore di piattaforma ha intercettato un'istruzione SYSCALL (amd64) o SVC #0 (arm64) del guest e ha scritto i registri del guest nella pagina condivisa da 4 KB (CommPage, sysmsg o winSysmsg), il controllo torna al goroutine Go del task di Sentry (pkg/sentry/kernel/task_syscall.go). Anziché inoltrare le system call Linux al kernel del sistema operativo host sottostante, Task.doSyscall() salva lo stato originale dei registri (SyscallSaveOrig()), valuta gli eventuali filtri BPF seccomp del guest e le fermate ptrace, quindi invia il numero della system call tramite linux.AMD64 o linux.ARM64 (pkg/sentry/syscalls/linux/linux64.go, che copre circa 370 system call Linux) verso quattro domini interni:

Come gvx mappa le system call Linux del guest su sottosistemi e sistema operativo host

Dominio di system callEsempi rappresentativi di system call Linux del guestSottosistema interno gvx (pkg/sentry)Interazione con il confine del sistema operativo host (dopo hostlockdown)
A. Processi, namespace, mount e sincronizzazioneclone, execve, unshare, setns, mount, umount2, pivot_root, setuid, capset, futex, epoll_wait, pipe2kernel.Task, credenziali virtuali, tabella dei futex e VFS2 in memoria (procfs, sysfs, cgroupfs v2, tmpfs, overlayfs)Zero system call host (stato Go interamente in memoria; completamente invisibile al sistema operativo host)
B. Memoria virtuale (MM)mmap, munmap, mremap, mprotect, brk, madvise, memfd_createmm.MemoryManager (albero VMA da 4 KB) e pgalloc.MemoryFileAggiornamenti bidirezionali delle tabelle delle pagine da 4 KB tramite WHvMapGpaRange, hv_vm_map + TTBR0_EL1, oppure mmap/mprotect dello stub
C. File e archiviazione (VFS2)openat, pread64, pwrite64, getdents64, statx, renameat2, setxattr, mknod, inotify_add_watchRouter della tabella di mount di vfs.VirtualFilesystem (fsimpl/erofs, fsimpl/ext4lite, fsimpl/hostfs v2)pread/pwrite bidirezionali su 1 FD .erofs fissato o 1 FD .ext4, oppure attraversamenti relativi limitati su *os.Root
D. Rete e Netfiltersocket, bind, listen, connect, sendmsg, recvmsg, netlink, setsockopt(IPT_SO_SET_REPLACE)pkg/tcpip user-space (lo, eth0, docker0, veth, vxlan, wireguard, ipset, iptables-legacy)Il traffico interno alla sandbox non utilizza alcuna system call host; il traffico esterno viene mappato su net.Dial/net.Listen (-net=nat) oppure su 1 socket UDP (-net=wg)

Quando il gestore restituisce (rval, ctrl, err), Task.doSyscallInvoke() converte qualsiasi errore in un errno Linux negativo (-ExtractErrno(err, sysno)), riscrive il valore di ritorno nella pagina condivisa dei registri (RAX su amd64, X0 su arm64) e riprende l'esecuzione del guest.


Storage (VFS2) e networking user-space a doppio binario (netstack)

Tutte le operazioni su file e rete del guest vengono gestite all'interno del Sentry in Go tramite tre filesystem VFS2 e due tracciati pkg/netbackend:

Motori di storage VFS2 e netstack a doppio binario in gvx

Motore di storageRuolo di mount in gvxRisorsa host di supportoChiamate host openat / NtCreateFile a runtimeScansioni per file di Windows Defender (WdFilter.sys)
fsimpl/erofsStrati inferiori in sola lettura dell'immagine OCISingolo file <sha256>.erofs per strato00
fsimpl/ext4liteVolumi con nome (-v name:/path)Singolo file <name>.ext4 per volume00
fsimpl/hostfs v2Workspace host (-workspace, -v /host:/guest)Directory host *os.Root fissataO(depth) per apertura non in cache1 per chiusura di file host

Perché ext4lite è importante per Docker, database e package manager

Estrarre o scrivere decine di migliaia di file di container direttamente su NTFS di Windows o su APFS di macOS attiva le scansioni dei minifilter antivirus (WdFilter.sys), collisioni di sensibilità alle maiuscole e minuscole e l'assenza di attributi estesi POSIX.

pkg/sentry/fsimpl/ext4lite implementa un filesystem ext4 interamente in Go all'interno di un unico file host pre-aperto (%LOCALAPPDATA%\gvx\cache\volumes\<name>.ext4 su Windows, ~/Library/Caches/gvx/volumes/<name>.ext4 su macOS oppure ~/.cache/gvx/volumes/<name>.ext4 su Linux), con:

  • Blocchi da 4 KiB e block group dinamici da 16 MiB (meta_bg): crescono dinamicamente senza stalli nell'allocazione di file sparsi (fallocate su Linux, fcntl(F_PREALLOCATE) su macOS, FileAllocationInfo su Windows) e compattano i gruppi finali inutilizzati durante uno smontaggio pulito.
  • Journal JBD2 in modalità ordered (inode 8): commit atomici delle transazioni con checksum CRC-32 e blocchi di revoca, più il recupero fsckLite.
  • Supporto per lo strato superiore di overlayfs: implementa gli attributi estesi trusted.overlay.* e security.capability, i whiteout di dispositivi a caratteri (0:0), la segnalazione di d_type nelle voci delle directory e RENAME_EXCHANGE.

Carichi di lavoro pratici: Docker Engine, Kubernetes (k3s + Calico) e SDK Go

Installare rungv: release precompilate con attestazione SLSA oppure go install

È possibile scaricare un archivio rungv precompilato per linux/amd64, linux/arm64, darwin/arm64, windows/amd64 o windows/arm64 da GitHub Releases (ciascuno generato dalla pipeline di release SLSA con un bundle di provenienza Sigstore e SHA256SUMS), oppure installare direttamente dal sorgente con go install:

1# Opzione A: scarica una release precompilata e verifica la provenienza di 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# Opzione B: installa direttamente tramite Go (Linux / Windows)
9CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest

Su macOS (darwin/arm64), l'archivio di release precompilato è già firmato con il diritto com.apple.security.hypervisor (xattr -d com.apple.quarantine rungv rimuove la quarantena del browser). Se si compila localmente con go install o go build su macOS, firmare il binario con codesign --sign - --entitlements entitlements.plist --force $(which rungv).

1. Esecuzione di un Docker Engine non privilegiato (docker:dind)

Seguendo docs/docker-engine.md, è possibile eseguire un container docker:dind non modificato all'interno di rungv su Windows, macOS o Linux e controllarlo con la CLI docker dell'host:

 1# 1. Crea la directory host per il socket Unix inoltrato
 2mkdir -p ~/.gvx
 3
 4# 2. Avvia docker:dind all'interno di una sandbox gvx con un 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

In un secondo terminale, puntare la CLI docker dell'host al socket della sandbox:

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

2. Esecuzione di Kubernetes (k3s + Calico) in sandbox non privilegiate

Poiché gvx implementa /dev/kmsg, cgroup v2 (OOM killer tramite memory.max e throttling della banda tramite cpu.max), /proc/diskstats, i dispositivi netlink wireguard e vxlan, ipset e iptables-legacy (addrtype, conntrack, statistic, recent), un'immagine rancher/k3s non modificata raggiunge lo stato Ready all'interno di un singolo 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

È inoltre possibile collegare più nodi gvx tramite WireGuard (flannel-backend=wireguard-native oppure una mesh wg0) con alta disponibilità tramite etcd integrato, oppure eseguire Calico (calico-vxlan.yaml con FELIX_IPTABLESBACKEND=Legacy) per applicare le NetworkPolicy di Kubernetes interamente nello user space.

3. Incorporare container Linux in Go tramite z.gfire.dev/gvx/pkg/gvx

Le applicazioni Go possono incorporare container Linux direttamente, senza un daemon di container esterno:

 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}

Ulteriori letture in questa serie

  1. gvx: esecuzione di container Linux, Docker e Kubernetes in un unico processo non privilegiato (questo articolo)
  2. Esecuzione di Docker, docker build e docker compose senza root né user namespaces: come avviare docker:dind in sandbox bloccate, dove sudo, unshare(CLONE_NEWUSER), mount(2) e /dev/fuse sono disabilitati.
  3. Esecuzione di Docker-in-Docker all'interno di gVisor e di sandbox annidate con gvx: perché apt-get install docker.io e runsc annidato falliscono all'interno di gVisor attraverso cinque barriere del kernel, e come gvx esegue docker:dind annidato tramite un loop futex in memoria condivisa.
  4. Ambienti Docker isolati e veloci per i rollout RL di agenti AI con gvx: come scalare SWE-bench e i rollout di reinforcement learning per agenti con centinaia di istanze parallele non privilegiate di docker:dind e reset copy-on-write dei volumi .ext4 in 100 millisecondi.