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

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 privilegiroot, l'esposizione di/var/run/docker.sockdell'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 oVirtualization.frameworked 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):
gvxnon indebolisce mai i confini di sicurezza del sistema operativo host, non installa mai driver del kernel e non richiede mairoot,sudo,--privilegedsull'host oCLONE_NEWUSER. Prima di eseguire una singola istruzione del guest,gvxblocca il proprio processo host tramite Windows Restricted Tokens e Job Objects, macOS Seatbelt (deny default) oppure LinuxPR_SET_NO_NEW_PRIVS+ BPFseccomp. Tutte le system call del guest, i privilegi virtualiUID 0, i mount (VFS2) e le tabelle di rete (netstack) sono rigorosamente confinati all'interno del kernel GoSentry, sicuro dal punto di vista della memoria.
| Runtime | Sistema operativo host | Avvio a freddo | Occupazione di memoria | Admin o sudo necessari | Daemon VM in background | Esecuzione diretta di immagini OCI |
|---|---|---|---|---|---|---|
gvx (whpx / hvf) | Windows (amd64, arm64) e macOS (arm64) | ~20 ms | ~30 MiB | No | No | Sì |
gvx (systrap) | Windows (amd64, arm64) e Linux (amd64, arm64) | ~25 a 65 ms | ~31 MiB | No | No | Sì |
| WSL2 / Docker Desktop | Windows e macOS | 800 a 2500 ms | > 512 MiB | Sì (configurazione iniziale) | Sì | Richiede daemon |
| QEMU (Full System) | Windows e macOS | 1500 a 4000 ms | > 256 MiB | No | No | Richiede 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.

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():
- Windows NT (
lockdown_windows.go): ChiamaAdjustTokenPrivilegesconSE_PRIVILEGE_REMOVEDper rimuovere i privilegi sul posto, associa un token di impersonificazione ristrettoDISABLE_MAX_PRIVILEGEcon livello di integrità Low (S-1-16-4096), assegna il processo a un Job Object anonimo conJOB_OBJECT_LIMIT_ACTIVE_PROCESS = 1(bloccando la creazione di qualsiasi processo figlio) e abilitaDisallowWin32kSystemCalls = 1tramiteSetProcessMitigationPolicy. - macOS XNU (
lockdown_darwin.go): ImpostaRLIMIT_NPROC = 0e applica un profilo Seatbelt (sandbox_init_with_parameters) a partire da(version 1)(deny default), che bloccaprocess-fork,process-exec*e tutte le nuove aperture di file dell'host. - Linux (
lockdown_linux.go+pkg/gvx/syscallfilter_linux.go): ImpostaPR_SET_NO_NEW_PRIVSe installa la allowlist BPFseccompdi Sentry su tutti i thread (SECCOMP_FILTER_FLAG_TSYNC), con regoleEPERMesplicite perexecve,execveat,clone,openatesocket.
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:

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:
whpxregistra ciascuna arena da 64 MiB dipgalloc.MemoryFilenella partizione una sola volta tramiteWHvMapGpaRange. Le successive chiamateMapFileeUnmapmodificano solo le tabelle delle pagine del guest all'interno della partizione, applicando permessiR/W/Xesatti a 4 KB, nonostante la granularità di allocazione di 64 KB di Windows. - Scambio di registri a zero API tramite
CommPage:- Su
amd64, un'istruzioneSYSCALLdel guest in Ring-3 entra in uno stubMSR_LSTARin 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 unaCommPagecondivisa da 4 KB ed esegueHLTper uscire daWHvRunVirtualProcessor. - Su
arm64, un'istruzioneSVC #0del guest in EL0 entra nei vettori EL1 (stubgen_a64.go), salvaX0..X8, SP_EL0, ELR_EL1, PSTATEnellaCommPagee carica dal GPA zero non associato per innescare un'uscita di memoria verso Sentry.
- Su
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, conHV_MEMORY_READ | HV_MEMORY_WRITE | HV_MEMORY_EXEC. All'interno del vCPU, lo stub EL1bluepill(bluepill_arm64.go) configuraTCR_EL1.TG0 = 0b00(granule da 4 KB) inTTBR0_EL1e popola le voci di tabella delle pagine Stage-1 da 4 KB con permessi esattiR-XoppureRW-(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 #0del guest in EL0 viene intercettata daVBAR_EL1in EL1, salva i registri nellaCommPagecondivisa da 4 KB ed esegueHVC #0per restituire il controllo dahv_vcpu_runal 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: Suamd64, il patcher (patcher_amd64_xplat.go) sostituisce le istruzionisyscall(0f 05) di 2 byte con saltijmp rel32di 5 byte (oppure con trap di breakpointCC 90quando una finestra di 5 byte non è disponibile) verso un'isola fast-path. Suarm64,svc #0viene patchata con un ramoBverso uno slot dell'isola. L'isola salva i registri nella pagina condivisawinSysmsge si sincronizza con Sentry tramiteWaitOnAddresseWakeByAddressSingle, 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:

| Dominio di system call | Esempi rappresentativi di system call Linux del guest | Sottosistema interno gvx (pkg/sentry) | Interazione con il confine del sistema operativo host (dopo hostlockdown) |
|---|---|---|---|
| A. Processi, namespace, mount e sincronizzazione | clone, execve, unshare, setns, mount, umount2, pivot_root, setuid, capset, futex, epoll_wait, pipe2 | kernel.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_create | mm.MemoryManager (albero VMA da 4 KB) e pgalloc.MemoryFile | Aggiornamenti 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_watch | Router 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 Netfilter | socket, 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:

| Motore di storage | Ruolo di mount in gvx | Risorsa host di supporto | Chiamate host openat / NtCreateFile a runtime | Scansioni per file di Windows Defender (WdFilter.sys) |
|---|---|---|---|---|
fsimpl/erofs | Strati inferiori in sola lettura dell'immagine OCI | Singolo file <sha256>.erofs per strato | 0 | 0 |
fsimpl/ext4lite | Volumi con nome (-v name:/path) | Singolo file <name>.ext4 per volume | 0 | 0 |
fsimpl/hostfs v2 | Workspace host (-workspace, -v /host:/guest) | Directory host *os.Root fissata | O(depth) per apertura non in cache | 1 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 (fallocatesu Linux,fcntl(F_PREALLOCATE)su macOS,FileAllocationInfosu 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 estesitrusted.overlay.*esecurity.capability, i whiteout di dispositivi a caratteri (0:0), la segnalazione did_typenelle voci delle directory eRENAME_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 dirittocom.apple.security.hypervisor(xattr -d com.apple.quarantine rungvrimuove la quarantena del browser). Se si compila localmente congo installogo buildsu macOS, firmare il binario concodesign --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
- gvx: esecuzione di container Linux, Docker e Kubernetes in un unico processo non privilegiato (questo articolo)
- Esecuzione di Docker, docker build e docker compose senza root né user namespaces: come avviare
docker:dindin sandbox bloccate, dovesudo,unshare(CLONE_NEWUSER),mount(2)e/dev/fusesono disabilitati. - Esecuzione di Docker-in-Docker all'interno di gVisor e di sandbox annidate con gvx: perché
apt-get install docker.ioerunscannidato falliscono all'interno digVisorattraverso cinque barriere del kernel, e comegvxeseguedocker:dindannidato tramite un loop futex in memoria condivisa. - Ambienti Docker isolati e veloci per i rollout RL di agenti AI con gvx: come scalare
SWE-benche i rollout di reinforcement learning per agenti con centinaia di istanze parallele non privilegiate didocker:dinde reset copy-on-write dei volumi.ext4in 100 millisecondi.