gvx: Rularea containerelor Linux, Docker și Kubernetes într-un singur proces neprivilegiat

Introducere: containere securizate fără root sau daemoni pe gazdă
Rularea containerelor Linux, a comenzii docker build sau a Kubernetes pe o mașină de dezvoltare, un runner CI sau un nod cloud multi-tenant impune, de regulă, un compromis incomod în materie de securitate:
- Pe Linux, runtime-urile OCI standard (
runc,crun,runsc) necesită fie privilegiiroot, fie expunerea/var/run/docker.sockal gazdei, fie--privileged(CAP_SYS_ADMIN), fie namespace-uri de utilizator (unshare(CLONE_NEWUSER)), care extind suprafața de atac a kernel-ului gazdei. - Pe Windows și macOS, motoarele de containere (
Docker Desktop,Podman Machine,Lima,OrbStack) pornesc un kernel complet de mașină virtuală Linux în WSL2, Hyper-V sauVirtualization.frameworkși rulează un daemon persistent în fundal în interiorul acestei VM.
gvx (CLI-ul rungv și SDK-ul Go z.gfire.dev/gvx/pkg/gvx) oferă o alternativă sigură, fără privilegii. Construit ca un fork, ce poate fi urmărit în amonte, al kernel-ului Linux în user-space Sentry și al stivei de rețea în user-space netstack ale proiectului gVisor al Google, gvx execută binare linux/amd64 și linux/arm64 ELF64 nemodificate, precum și containere OCI, direct în interiorul unui singur proces gazdă neprivilegiat, cu auto-blocare, pe Windows, macOS și Linux.
Garanție de securitate (apărare în profunzime cu trei inele):
gvxnu slăbește niciodată granițele de securitate ale sistemului de operare gazdă, nu instalează niciodată drivere de kernel și nu necesită niciodatăroot,sudo,--privilegedpe gazdă sauCLONE_NEWUSER. Înainte de a executa vreo instrucțiune a oaspetelui,gvxîși blochează propriul proces gazdă folosind Windows Restricted Tokens și Job Objects, macOS Seatbelt (deny default) sau LinuxPR_SET_NO_NEW_PRIVS+ BPFseccomp. Toate apelurile de sistem ale oaspetelui, privilegiile virtualeUID 0, montările (VFS2) și tabelele de rețea (netstack) sunt strict confinate în interiorul kernel-ului GoSentry, sigur din punct de vedere al memoriei.
| Runtime | Sistem de operare gazdă | Pornire la rece | Amprentă de memorie | Necesită admin sau sudo | Daemon VM în fundal | Rulare directă a imaginii OCI |
|---|---|---|---|---|---|---|
gvx (whpx / hvf) | Windows (amd64, arm64) și macOS (arm64) | ~20 ms | ~30 MiB | Nu | Nu | Da |
gvx (systrap) | Windows (amd64, arm64) și Linux (amd64, arm64) | ~25 până la 65 ms | ~31 MiB | Nu | Nu | Da |
| WSL2 / Docker Desktop | Windows și macOS | 800 până la 2500 ms | > 512 MiB | Da (configurare inițială) | Da | Necesită daemon |
| QEMU (sistem complet) | Windows și macOS | 1500 până la 4000 ms | > 256 MiB | Nu | Nu | Necesită imagine de disc |
Pe Windows sau macOS nu pornește niciun kernel de mașină virtuală Linux. Pe gazdă nu rulează niciun daemon în fundal. Pe sistemul de operare gazdă nu sunt necesare privilegii root, sudo, namespace-uri de utilizator (CLONE_NEWUSER), mount(2), drivere TUN/TAP sau CAP_NET_ADMIN. În interiorul acestui proces neprivilegiat, gvx rulează Docker-in-Docker (docker:dind), docker build, docker compose și Kubernetes de înaltă disponibilitate, multi-nod (k3s cu etcd încorporat și CNI Calico).
Ciclul de viață al containerului și izolarea gazdei în trei inele
Fiecare sandbox gvx rulează într-un singur proces gazdă neprivilegiat și aplică un ciclu de viață în trei faze: deschiderea anticipată a tuturor descriptorilor de fișiere și a socket-urilor gazdei necesare, blocarea procesului sistemului de operare gazdă prin pkg/hostlockdown și intrarea în bucla de execuție Sentry din proces.

Înainte de a transfera controlul către prima instrucțiune a oaspetelui, gvx deschide anticipat descriptorii de fișiere ai straturilor de imagine .erofs din cache, descriptorii de fișiere ai volumelor .ext4, handle-ul *os.Root pentru /workspace și socket-urile de rețea ale gazdei, după care invocă hostlockdown.Apply():
- Windows NT (
lockdown_windows.go): ApeleazăAdjustTokenPrivilegescuSE_PRIVILEGE_REMOVEDpentru a elimina privilegiile în loc, leagă un token de impersonare restricționatDISABLE_MAX_PRIVILEGEla integritate Low (S-1-16-4096), atribuie procesul unui Job Object anonim cuJOB_OBJECT_LIMIT_ACTIVE_PROCESS = 1(care blochează crearea oricărui proces copil) și activeazăDisallowWin32kSystemCalls = 1prinSetProcessMitigationPolicy. - macOS XNU (
lockdown_darwin.go): SeteazăRLIMIT_NPROC = 0și aplică un profil Seatbelt (sandbox_init_with_parameters) pornind de la(version 1)(deny default), care blocheazăprocess-fork,process-exec*și toate deschiderile de fișiere noi ale gazdei. - Linux (
lockdown_linux.go+pkg/gvx/syscallfilter_linux.go): SeteazăPR_SET_NO_NEW_PRIVSși instalează lista de permisiuni BPFseccompa Sentry pe toate firele de execuție (SECCOMP_FILTER_FLAG_TSYNC), cu reguliEPERMexplicite pentruexecve,execveat,clone,openatșisocket.
Interceptarea apelurilor de sistem multiplatformă și paginarea de 4 KB (pkg/sentry/platform)
Binarele Linux ELF64 presupun o granularitate a paginilor de memorie de 4 KB (0x1000) pentru mapările segmentelor mmap(MAP_FIXED). Totuși, VirtualAlloc și MapViewOfFile3 din Windows impun alinierea la 64 KB (0x10000), iar macOS pe Apple Silicon (darwin/arm64) folosește pagini gazdă de 16 KB (0x4000). În plus, fiecare sistem de operare necesită un alt mecanism pentru a intercepta instrucțiunile Linux SYSCALL (amd64) și SVC #0 (arm64), fără a porni o mașină virtuală Linux.
gvx rezolvă ambele probleme prin patru backend-uri de execuție care implementează pkg/sentry/platform.Context:

1. Motorul hardware Windows: platform/whpx (WinHvPlatform.dll)
Pe windows/amd64 și windows/arm64, platform/whpx creează o partiție hipervizor ușoară prin WHvCreatePartition, fără a porni kernelul unui sistem de operare oaspete:
- Decuplorul de paginare 64 KB-4 KB:
whpxînregistrează fiecare arenă de 64 MiB dinpgalloc.MemoryFilela partiție o singură dată, prinWHvMapGpaRange. Apelurile ulterioareMapFileșiUnmapmodifică doar tabelele de pagini ale oaspetelui din interiorul partiției, aplicând permisiuniR/W/Xexacte de 4 KB, în ciuda granularității de alocare de 64 KB a Windows. - Schimb de registre fără API prin
CommPage:- Pe
amd64, unSYSCALLdin Ring-3 al oaspetelui intră într-un stubMSR_LSTARdin Ring-0, aflat în interiorul partiției (stubgen_x86.go), care salvează înCommPage, o pagină partajată de 4 KB, registrele GPR ale apelului de sistem rapid (RAX, RDI, RSI, RDX, R10, R8, R9, RIP, RSP) și executăHLTpentru a ieși dinWHvRunVirtualProcessor. - Pe
arm64, unSVC #0din EL0 al oaspetelui intră în vectorii EL1 (stubgen_a64.go), salveazăX0..X8, SP_EL0, ELR_EL1, PSTATEînCommPageși citește de la GPA zero, neasociat, pentru a declanșa o ieșire de memorie către Sentry.
- Pe
2. Motorul hardware macOS: platform/hvf (Hypervisor.framework)
Pe Apple Silicon (darwin/arm64), platform/hvf rulează firele containerelor în vCPU-urile Hypervisor.framework (hv_vm_create, hv_vcpu_run):
- Traducere MMU în două etape, de la pagini de 4 KB peste pagini de 16 KB: Stage-2 (
hv_vm_map) mapează, o singură dată, bucăți de fișier de memorie de 1 GiB în spațiul de adrese fizice intermediare (IPA), la granularitatea paginilor gazdă de 16 KB, cuHV_MEMORY_READ | HV_MEMORY_WRITE | HV_MEMORY_EXEC. În interiorul vCPU-ului, stub-ul EL1bluepill(bluepill_arm64.go) configureazăTCR_EL1.TG0 = 0b00(granulă de 4 KB) înTTBR0_EL1și populează intrările tabelelor de pagini Stage-1 de 4 KB cu permisiuni exacteR-XsauRW-(UXN=1). Deoarece MMU-ul ARM64 impune intersecția permisiunilor Stage-1 și Stage-2, hardware-ul interceptează erorile de permisiune la limitele exacte de 4 KB. - Interceptarea apelurilor de sistem: Un
SVC #0din EL0 al oaspetelui este interceptat deVBAR_EL1din EL1, salvează registrele înCommPage, o pagină partajată de 4 KB, și executăHVC #0pentru a reveni dinhv_vcpu_runcătre firul Go al Sentry.
3. Rezerva în mod utilizator pentru Windows: platform/systrap (winsystrap)
Atunci când virtualizarea hardware nu este disponibilă (de exemplu, în VM-uri cloud imbricate), winsystrap rulează codul oaspetelui în mod utilizator pe windows/amd64 și windows/arm64:
- Izolarea hardware prin
CR3/TTBR0_EL1: Firele oaspetelui rulează în subprocese stub izolate, pornite dintr-un executabil generat de 1 KB (stubimage_xplat.go), confinate de un token cu integritate Low și de un Job Object. - Patch binar pe loc și IPC
winSysmsg: Peamd64, patcher-ul (patcher_amd64_xplat.go) înlocuiește instrucțiunilesyscall(0f 05) de 2 octeți cu salturijmp rel32de 5 octeți (sau cu capcane de breakpointCC 90, atunci când nu este disponibilă o fereastră de 5 octeți), care duc către o insulă cu cale rapidă. Pearm64,svc #0este patch-uit cu o ramurăBcătre un slot din insulă. Insula salvează registrele în pagina partajatăwinSysmsgși se sincronizează cu Sentry prinWaitOnAddressșiWakeByAddressSingle, în timp ce capcanele nepatch-uite și erorile de pagină se opresc pe un Windows Debug Object (WaitForDebugEventEx).
4. Motorul Linux în mod utilizator: platform/systrap
Pe linux/amd64 și linux/arm64, systrap rulează fire ale oaspetelui (sysmsg) care interceptează apelurile de sistem ale oaspetelui prin SECCOMP_RET_TRAP (SIGSYS) și patch-uiesc binar punctele syscall într-un trampolin de memorie partajată (sysmsg), sincronizat prin SYS_FUTEX. În paralel, un syscallThread stub (syscall_thread.go) execută operațiile asupra spațiului de adrese (mmap, munmap, clone) prin SECCOMP_RET_USER_NOTIF (_RUN_SECCOMP_LOOP). Atunci când gvx rulează în interiorul unui sandbox gVisor extern (runsc), unde unicul slot SECCOMP_FILTER_FLAG_NEW_LISTENER returnează EBUSY, gvx trece automat syscallThread pe o buclă futex cu memorie partajată (_RUN_SYSCALL_LOOP), după cum este detaliat în Partea 3 a acestei serii.
Cum mapează gvx apelurile de sistem Linux ale oaspetelui către subsisteme virtuale și handle-uri ale gazdei
Odată ce un motor de platformă interceptează un SYSCALL (amd64) sau o instrucțiune SVC #0 (arm64) a oaspetelui și scrie registrele oaspetelui în pagina partajată de 4 KB (CommPage, sysmsg sau winSysmsg), controlul revine la goroutina task-ului Go Sentry (pkg/sentry/kernel/task_syscall.go). În loc să redirecționeze apelurile de sistem Linux către kernel-ul sistemului de operare gazdă, Task.doSyscall() salvează starea originală a registrelor (SyscallSaveOrig()), evaluează eventualele filtre BPF seccomp din oaspete și opririle ptrace, apoi distribuie numărul apelului de sistem prin linux.AMD64 sau linux.ARM64 (pkg/sentry/syscalls/linux/linux64.go, care acoperă ~370 de apeluri de sistem Linux) în patru domenii interne:

| Domeniu de apeluri de sistem | Apeluri de sistem Linux reprezentative ale oaspetelui | Subsistem intern gvx (pkg/sentry) | Interacțiunea cu granița sistemului de operare gazdă (după hostlockdown) |
|---|---|---|---|
| A. Procese, namespace-uri, montări și sincronizare | clone, execve, unshare, setns, mount, umount2, pivot_root, setuid, capset, futex, epoll_wait, pipe2 | kernel.Task, acreditări virtuale, tabela futex și VFS2 în memorie (procfs, sysfs, cgroupfs v2, tmpfs, overlayfs) | Zero apeluri de sistem ale gazdei (100% stare Go în memorie; complet invizibil pentru sistemul de operare gazdă) |
B. Memorie virtuală (MM) | mmap, munmap, mremap, mprotect, brk, madvise, memfd_create | mm.MemoryManager (arbore VMA de 4 KB) și pgalloc.MemoryFile | Actualizări bidirecționale ale tabelelor de pagini de 4 KB prin WHvMapGpaRange, hv_vm_map + TTBR0_EL1 sau prin mmap/mprotect ale stub-ului |
C. Fișiere și stocare (VFS2) | openat, pread64, pwrite64, getdents64, statx, renameat2, setxattr, mknod, inotify_add_watch | Routerul tabelei de montări vfs.VirtualFilesystem (fsimpl/erofs, fsimpl/ext4lite, fsimpl/hostfs v2) | pread/pwrite bidirecționale pe 1 FD .erofs fixat sau pe 1 FD .ext4, sau parcurgeri relative limitate pe *os.Root |
| D. Rețea și Netfilter | socket, bind, listen, connect, sendmsg, recvmsg, netlink, setsockopt(IPT_SO_SET_REPLACE) | pkg/tcpip în user-space (lo, eth0, docker0, veth, vxlan, wireguard, ipset, iptables-legacy) | Traficul din interiorul sandbox-ului folosește zero apeluri de sistem ale gazdei; traficul extern se mapează pe net.Dial/net.Listen (-net=nat) sau pe 1 socket UDP (-net=wg) |
Atunci când handler-ul returnează (rval, ctrl, err), Task.doSyscallInvoke() convertește orice eroare într-un errno Linux negativ (-ExtractErrno(err, sysno)), scrie valoarea de return înapoi în pagina partajată a registrelor (RAX pe amd64, X0 pe arm64) și reia execuția oaspetelui.
Stocarea (VFS2) și rețeaua în user-space cu dublă cale (netstack)
Toate operațiile de fișiere și de rețea ale oaspetelui sunt deservite în interiorul Go Sentry, prin trei sisteme de fișiere VFS2 și două piste pkg/netbackend:

| Motor de stocare | Rolul montării în gvx | Resursa de bază a gazdei | Apeluri openat / NtCreateFile ale gazdei la rulare | Scanări per fișier ale WdFilter.sys (Windows Defender) |
|---|---|---|---|---|
fsimpl/erofs | Straturi inferioare de imagine OCI, doar citire | Un singur fișier <sha256>.erofs per strat | 0 | 0 |
fsimpl/ext4lite | Volume numite (-v name:/path) | Un singur fișier <name>.ext4 per volum | 0 | 0 |
fsimpl/hostfs v2 | Spațiul de lucru al gazdei (-workspace, -v /host:/guest) | Director de gazdă fixat *os.Root | O(depth) per deschidere necacheată | 1 per închidere de fișier al gazdei |
De ce contează ext4lite pentru Docker, baze de date și managerii de pachete
Extragerea sau scrierea a zeci de mii de fișiere de container direct pe NTFS (Windows) sau APFS (macOS) declanșează scanări ale filtrelor antivirus (WdFilter.sys), coliziuni de sensibilitate la majuscule și lipsa atributelor extinse POSIX.
pkg/sentry/fsimpl/ext4lite implementează un sistem de fișiere ext4 pur Go în interiorul unui singur fișier al gazdei deschis anticipat (%LOCALAPPDATA%\gvx\cache\volumes\<name>.ext4 pe Windows, ~/Library/Caches/gvx/volumes/<name>.ext4 pe macOS sau ~/.cache/gvx/volumes/<name>.ext4 pe Linux), cu:
- Blocuri de 4 KiB și grupuri de blocuri dinamice de 16 MiB (
meta_bg): Crește dinamic, fără blocaje de alocare a fișierelor sparse (fallocatepe Linux,fcntl(F_PREALLOCATE)pe macOS,FileAllocationInfope Windows) și compactează grupurile finale neutilizate la demontarea curată. - Jurnal JBD2 în modul ordonat (inode 8): Confirmări atomice ale tranzacțiilor cu sume de control CRC-32 și blocuri de revocare, plus recuperare
fsckLite. - Suport pentru stratul superior
overlayfs: Implementează atributele extinsetrusted.overlay.*șisecurity.capability, whiteout-uri de dispozitive caracter (0:0), raportarea intrărilor de directord_typeșiRENAME_EXCHANGE.
Sarcini practice: Docker Engine, Kubernetes (k3s + Calico) și SDK-ul Go
Instalarea rungv: versiuni precompilate atestate SLSA sau go install
Puteți descărca o arhivă rungv precompilată pentru linux/amd64, linux/arm64, darwin/arm64, windows/amd64 sau windows/arm64 de la GitHub Releases (fiecare construită de pipeline-ul de lansare SLSA, cu un pachet de proveniență Sigstore și SHA256SUMS), sau o puteți instala direct din sursă, cu go install:
1# Opțiunea A: Descarcă o versiune precompilată și verifică proveniența construcției 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țiunea B: Instalează direct prin Go (Linux / Windows)
9CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest
Pe macOS (
darwin/arm64), arhiva precompilată este deja semnată cu drepturilecom.apple.security.hypervisor(xattr -d com.apple.quarantine rungvelimină carantina browserului). Dacă construiți local cugo installsaugo buildpe macOS, semnați binarul cucodesign --sign - --entitlements entitlements.plist --force $(which rungv).
1. Rularea unui Docker Engine neprivilegiat (docker:dind)
Urmând docs/docker-engine.md, puteți rula un container docker:dind nemodificat în rungv pe Windows, macOS sau Linux și îl puteți controla cu CLI-ul docker al gazdei:
1# 1. Creează directorul gazdei pentru socket-ul Unix redirecționat
2mkdir -p ~/.gvx
3
4# 2. Pornește docker:dind într-un sandbox gvx cu un volum 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
Într-un al doilea terminal, indicați CLI-ul docker al gazdei către socket-ul sandbox-ului:
1docker context create gvx --docker host=unix://$HOME/.gvx/docker.sock
2docker context use gvx
3
4docker info --format '{{.Driver}} {{.OperatingSystem}}'
5# Ieșire: overlayfs Alpine Linux ... (containerized)
6
7docker build -t myapp ./app
8docker compose up -d
2. Rularea Kubernetes (k3s + Calico) în sandbox-uri neprivilegiate
Deoarece gvx implementează /dev/kmsg, cgroup v2 (memory.max cu OOM killer și cpu.max cu limitarea lățimii de bandă), /proc/diskstats, dispozitivele netlink wireguard și vxlan, ipset și iptables-legacy (addrtype, conntrack, statistic, recent), o imagine rancher/k3s nemodificată ajunge în starea Ready în interiorul unui singur proces 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
Puteți, de asemenea, să uniți mai multe noduri gvx prin WireGuard (flannel-backend=wireguard-native sau o rețea mesh wg0), cu disponibilitate ridicată etcd încorporat, sau să rulați Calico (calico-vxlan.yaml cu FELIX_IPTABLESBACKEND=Legacy) pentru a aplica NetworkPolicy Kubernetes complet în user-space.
3. Încorporarea containerelor Linux în Go prin z.gfire.dev/gvx/pkg/gvx
Aplicațiile Go pot încorpora containere Linux direct, fără un daemon extern de containere:
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}
Lecturi suplimentare din această serie
- gvx: Rularea containerelor Linux, Docker și Kubernetes într-un singur proces neprivilegiat (această postare)
- Rularea Docker, docker build și docker compose fără root sau namespace-uri de utilizator: Cum se pornește
docker:dindîn sandbox-uri restricționate, undesudo,unshare(CLONE_NEWUSER),mount(2)și/dev/fusesunt blocate. - Rularea Docker-in-Docker în gVisor și în sandbox-uri imbricate cu gvx: De ce
apt-get install docker.ioșirunscimbricat eșuează în interiorulgVisordin cauza a cinci bariere ale kernel-ului și cum ruleazăgvxundocker:dindimbricat, folosind o buclă futex cu memorie partajată. - Medii Docker izolate și rapide pentru rollout-urile RL ale agenților AI cu gvx: Scalarea rollout-urilor
SWE-benchși ale învățării prin întărire a agenților cu sute de instanțedocker:dindneprivilegiate, în paralel, și resetări de volume.ext4copy-on-write de 100 de milisecunde.