GoSuda

gvx: Att köra Linux Containers, Docker och Kubernetes i en enda Unprivileged Process

By Lemon Mint views ...

Inledning: Säkerhetshärdade containrar utan root eller värddaemoner

Att köra Linux-containrar, docker build eller Kubernetes på en utvecklingsdator, en CI-runner eller en multitenant-molnnod medför vanligtvis en obekväm säkerhetsavvägning:

  • På Linux kräver standardmässiga OCI-runtimes (runc, crun, runsc) antingen root-behörighet, exponering av värdens /var/run/docker.sock, --privileged (CAP_SYS_ADMIN) eller användarnamnrymder (unshare(CLONE_NEWUSER)), vilka utökar värdkärnans attackyta.
  • På Windows och macOS startar containermotorer (Docker Desktop, Podman Machine, Lima, OrbStack) en fullständig Linux-kärna i en virtuell maskin inom WSL2, Hyper-V eller Virtualization.framework och kör en beständig bakgrundsdaemon inuti den virtuella maskinen.

gvx (rungv-CLI:t och z.gfire.dev/gvx/pkg/gvx Go-SDK:t) erbjuder ett säkert alternativ med noll behörighetsbehov. Det är byggt som en uppströmsspårbar fork av Googles gVisor användarlägeskärna för Linux (Sentry) och användarlägesnätverksstacken (netstack), och gvx kör oförändrade linux/amd64- och linux/arm64-ELF64-binärer samt OCI-containrar direkt inuti en enda, obehörig och självlåst värdprocess på Windows, macOS och Linux.

Säkerhetsgaranti (Tre-ringsförsvar i djupet): gvx försvagar aldrig värd-OS:ets säkerhetsgränser, installerar aldrig kärndrivrutiner och kräver aldrig root, sudo, --privileged på värden eller CLONE_NEWUSER. Innan en enda gästinstruktion körs låser gvx sin egen värdprocess med Windows Restricted Tokens och Job Objects, macOS Seatbelt (deny default) eller Linux PR_SET_NO_NEW_PRIVS + BPF seccomp. Alla gästsystemanrop, virtuella UID 0-privilegier, monteringar (VFS2) och nätverkstabeller (netstack) är strikt begränsade inuti den minnessäkra Go-kärnan Sentry.

RuntimeVärd-OSKallstartMinnesfotavtryckAdmin eller sudo krävsBakgrunds-VM-daemonDirekt körning av OCI-avbild
gvx (whpx / hvf)Windows (amd64, arm64) & macOS (arm64)~20 ms~30 MiBNejNejJa
gvx (systrap)Windows (amd64, arm64) & Linux (amd64, arm64)~25 till 65 ms~31 MiBNejNejJa
WSL2 / Docker DesktopWindows & macOS800 till 2500 ms> 512 MiBJa (initial installation)JaKräver daemon
QEMU (Full System)Windows & macOS1500 till 4000 ms> 256 MiBNejNejKräver diskavbild

Ingen Linux-VM-kärna startas på Windows eller macOS. Ingen bakgrundsdaemon körs på värden. Inga root-, sudo-, användarnamnrymds- (CLONE_NEWUSER), mount(2)-, TUN/TAP-drivrutins- eller CAP_NET_ADMIN-privilegier krävs på värd-OS:et. Inuti den obehöriga processen kör gvx Docker-in-Docker (docker:dind), docker build, docker compose och högtillgänglig Kubernetes med flera noder (k3s med inbäddad etcd och Calico CNI).


Containerlivscykel och låsning av värd i tre ringar

Varje gvx-sandlåda körs inuti en obehörig värdprocess och genomdriver en livscykel i tre faser: föröppning av alla nödvändiga värdfilbeskrivare och uttag, låsning av värdprocessen via pkg/hostlockdown samt inträde i den interna Sentry-exekveringsslingan.

gvx Containerlivscykel och självlåsning av värd

Innan kontrollen överförs till den första gästinstruktionen föröppnar gvx sina cachade .erofs-lagerbeskrivare, .ext4-volymfilbeskrivare, /workspace-handtaget *os.Root och värdens nätverksuttag, varefter den anropar hostlockdown.Apply():

  1. Windows NT (lockdown_windows.go): Anropar AdjustTokenPrivileges med SE_PRIVILEGE_REMOVED för att på plats ta bort privilegier, binder en DISABLE_MAX_PRIVILEGE-begränsad personifieringstoken med låg integritetsnivå (S-1-16-4096), tilldelar processen till ett anonymt Job Object med JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 1 (vilket blockerar skapande av underordnade processer) och aktiverar DisallowWin32kSystemCalls = 1 via SetProcessMitigationPolicy.
  2. macOS XNU (lockdown_darwin.go): Sätter RLIMIT_NPROC = 0 och tillämpar en Seatbelt-profil (sandbox_init_with_parameters) som börjar med (version 1)(deny default) och blockerar process-fork, process-exec* samt alla nya öppningar av värdfiler.
  3. Linux (lockdown_linux.go + pkg/gvx/syscallfilter_linux.go): Sätter PR_SET_NO_NEW_PRIVS och installerar Sentrys BPF seccomp-tillåtelselista över alla trådar (SECCOMP_FILTER_FLAG_TSYNC) med explicita EPERM-regler för execve, execveat, clone, openat och socket.

Plattformsoberoende avlyssning av systemanrop och 4 KB-paging (pkg/sentry/platform)

Linux-ELF64-binärer förutsätter en minnessidgranularitet på 4 KB (0x1000) för segmentmappningar via mmap(MAP_FIXED). Windows VirtualAlloc och MapViewOfFile3 tvingar dock fram 64 KB-justering (0x10000), och Apple Silicon macOS (darwin/arm64) använder 16 KB-sidor (0x4000) på värden. Dessutom kräver varje OS en annan mekanism för att avlyssna Linux-instruktionerna SYSCALL (amd64) och SVC #0 (arm64) utan att starta en Linux-VM.

gvx löser båda problemen över fyra exekveringsbackends som implementerar pkg/sentry/platform.Context:

gvx Plattformens avlyssning av systemanrop och 4 KB-paging

1. Windows hårdvarumotor: platform/whpx (WinHvPlatform.dll)

På windows/amd64 och windows/arm64 skapar platform/whpx en lättviktig hypervisor-partition via WHvCreatePartition utan att starta en gästoperativsystemkärna:

  • Frikoppling av 64 KB till 4 KB-paging: whpx registrerar varje 64 MiB-arena i pgalloc.MemoryFile hos partitionen en gång via WHvMapGpaRange. Efterföljande anrop till MapFile och Unmap ändrar endast gästens sidtabeller inuti partitionen, vilket upprätthåller exakta 4 KB-behörigheter R/W/X trots Windows allokeringsgranularitet på 64 KB.
  • Registerutbyte utan API-anrop via CommPage:
    • På amd64 går gästens Ring-3-SYSCALL in i en Ring-0-stub MSR_LSTAR inuti partitionen (stubgen_x86.go), som skriver de snabba systemanropens GPR-register (RAX, RDI, RSI, RDX, R10, R8, R9, RIP, RSP) till en delad 4 KB-CommPage och kör HLT för att avsluta WHvRunVirtualProcessor.
    • På arm64 går gästens EL0-SVC #0 in i EL1-vektorerna (stubgen_a64.go), skriver X0..X8, SP_EL0, ELR_EL1, PSTATE till CommPage och läser från en ej backad GPA-nolla för att utlösa ett minnesavbrott tillbaka till Sentry.

2. macOS hårdvarumotor: platform/hvf (Hypervisor.framework)

På Apple Silicon (darwin/arm64) kör platform/hvf containertrådar inuti Hypervisor.framework-vCPU:er (hv_vm_create, hv_vcpu_run):

  • Tvåstegs-MMU-översättning med 4 KB över 16 KB: Steg 2 (hv_vm_map) mappar 1 GiB-delar av minnesfiler till det mellanliggande fysiska adressutrymmet (IPA) en gång, med 16 KB-granularitet för värdsidor och med HV_MEMORY_READ | HV_MEMORY_WRITE | HV_MEMORY_EXEC. Inuti vCPU:n konfigurerar EL1-stubben bluepill (bluepill_arm64.go) TCR_EL1.TG0 = 0b00 (4 KB-granulat) i TTBR0_EL1 och fyller i 4 KB-sidtabellposter för steg 1 med exakta behörigheter R-X eller RW- (UXN=1). Eftersom ARM64-MMU:n upprätthåller skärningspunkten mellan behörigheterna för steg 1 och steg 2 fångar hårdvaran behörighetsfel vid exakta 4 KB-gränser.
  • Avlyssning av systemanrop: Gästens EL0-SVC #0 går till EL1 VBAR_EL1, skriver register till den delade 4 KB-CommPage och kör HVC #0 för att återvända från hv_vcpu_run till Go-trådens Sentry.

3. Windows användarlägesreserv: platform/systrap (winsystrap)

När hårdvaruppvisning inte är tillgänglig (till exempel inuti kapslade molnvirtuella maskiner) kör winsystrap gästkod i användarläge på windows/amd64 och windows/arm64:

  • Isolering via hårdvarans CR3 / TTBR0_EL1: Gästtrådar körs inuti isolerade stubbunderprocesser som startas från en genererad körbar fil på 1 KB (stubimage_xplat.go), begränsade av en token med låg integritetsnivå och ett Job Object.
  • Binär patchning på plats och winSysmsg-IPC: På amd64 ersätter patcharen (patcher_amd64_xplat.go) 2-byte-instruktionerna syscall (0f 05) med 5-byte-hopp jmp rel32 (eller CC 90-brytpunktsfällor när ett 5-byte-fönster inte är tillgängligt) till en snabbvägsö. På arm64 patchas svc #0 med en B-förgrening till en ö-plats. Ön skriver register till den delade winSysmsg-sidan och synkroniseras med Sentry via WaitOnAddress och WakeByAddressSingle, medan opatchade fällor och sidfel stannar vid ett Windows Debug Object (WaitForDebugEventEx).

4. Linux användarlägesmotor: platform/systrap

På linux/amd64 och linux/arm64 kör systrap gästtrådar (sysmsg) som fångar gästens systemanrop via SECCOMP_RET_TRAP (SIGSYS) och binärpatchar syscall-platser till en trampolin i delat minne (sysmsg) som synkroniseras via SYS_FUTEX. Samtidigt utför en stubbad syscallThread (syscall_thread.go) operationer på adressutrymmet (mmap, munmap, clone) via SECCOMP_RET_USER_NOTIF (_RUN_SECCOMP_LOOP). När gvx körs inuti en yttre gVisor-sandlåda (runsc) där den enda SECCOMP_FILTER_FLAG_NEW_LISTENER-platsen returnerar EBUSY, växlar gvx automatiskt syscallThread till en futex-slinga i delat minne (_RUN_SYSCALL_LOOP), vilket beskrivs i detalj i del 3 i denna serie.


Hur gvx mappar gästens Linux-systemanrop till virtuella delsystem och värdhandtag

När en plattformsmotor har avlyssnat en gästs SYSCALL (amd64) eller SVC #0 (arm64) och skrivit gästregistren till den delade 4 KB-sidan (CommPage, sysmsg eller winSysmsg), återgår kontrollen till Go-goroutinen för Sentry-uppgiften (pkg/sentry/kernel/task_syscall.go). I stället för att vidarebefordra Linux-systemanrop till värd-OS:ets underliggande kärna sparar Task.doSyscall() det ursprungliga registertillståndet (SyscallSaveOrig()), utvärderar eventuella BPF seccomp-filter och ptrace-stopp i gästen och skickar systemanropsnumret vidare via linux.AMD64 eller linux.ARM64 (pkg/sentry/syscalls/linux/linux64.go, som omfattar cirka 370 Linux-systemanrop) till fyra interna domäner:

Hur gvx mappar gästens Linux-systemanrop till delsystem och värd-OS

SystemanropsdomänRepresentativa Linux-systemanrop i gästenInternt gvx-delsystem (pkg/sentry)Interaktion med värd-OS (efter hostlockdown)
A. Process, namnrymd, montering & synkroniseringclone, execve, unshare, setns, mount, umount2, pivot_root, setuid, capset, futex, epoll_wait, pipe2kernel.Task, virtuella credentials, futex-tabell och VFS2 i minnet (procfs, sysfs, cgroupfs v2, tmpfs, overlayfs)Noll systemanrop till värden (100 % Go-tillstånd i minnet; helt osynligt för värd-OS)
B. Virtuellt minne (MM)mmap, munmap, mremap, mprotect, brk, madvise, memfd_createmm.MemoryManager (VMA-träd med 4 KB) och pgalloc.MemoryFileDubbelriktade uppdateringar av 4 KB-sidtabeller via WHvMapGpaRange, hv_vm_map + TTBR0_EL1 eller stubbens mmap/mprotect
C. Filer och lagring (VFS2)openat, pread64, pwrite64, getdents64, statx, renameat2, setxattr, mknod, inotify_add_watchvfs.VirtualFilesystem-monteringstabellens router (fsimpl/erofs, fsimpl/ext4lite, fsimpl/hostfs v2)Dubbelriktade pread/pwrite mot 1 fixerad .erofs-FD eller 1 .ext4-FD, eller avgränsade relativa sökningar på *os.Root
D. Nätverk och Netfiltersocket, bind, listen, connect, sendmsg, recvmsg, netlink, setsockopt(IPT_SO_SET_REPLACE)Användarlägets pkg/tcpip (lo, eth0, docker0, veth, vxlan, wireguard, ipset, iptables-legacy)Trafik inom sandlådan använder noll systemanrop till värden; extern trafik mappas till net.Dial/net.Listen (-net=nat) eller 1 UDP-uttag (-net=wg)

När hanteraren returnerar (rval, ctrl, err) omvandlar Task.doSyscallInvoke() eventuella fel till ett negativt Linux-errno (-ExtractErrno(err, sysno)), skriver returvärdet tillbaka till den delade registersidan (RAX på amd64, X0 på arm64) och återupptar gästens exekvering.


Lagring (VFS2) och tvåspårig nätverkning i användarläge (netstack)

All gästens fil- och nätverksverksamhet hanteras inuti Go-kärnan Sentry via tre VFS2-filsystem och två pkg/netbackend-spår:

gvx VFS2-lagringsmotorer och tvåspårig netstack

LagringsmotorMonteringsroll i gvxUnderliggande värdresursVärdens openat- / NtCreateFile-anrop vid körningWindows Defender (WdFilter.sys) skanningar per fil
fsimpl/erofsSkrivskyddade nedre lager för OCI-avbilderEn enda <sha256>.erofs-fil per lager00
fsimpl/ext4liteNamngivna volymer (-v name:/path)En enda <name>.ext4-fil per volym00
fsimpl/hostfs v2Värdarbetsyta (-workspace, -v /host:/guest)Fixerad värdkatalog som *os.RootO(djup) per ej cachad öppning1 per värdfil som stängs

Varför ext4lite är viktigt för Docker, databaser och paketdomare

Att extrahera eller skriva tiotusentals containerfiler direkt till Windows NTFS eller macOS APFS utlöser skanningar av antivirusfilterdrivrutinen (WdFilter.sys), kollisioner mellan versaler och gemener samt saknade utökade POSIX-attribut.

pkg/sentry/fsimpl/ext4lite implementerar ett rent Go-baserat ext4-filsystem inuti en enda förtöppnad värdfil (%LOCALAPPDATA%\gvx\cache\volumes\<name>.ext4 på Windows, ~/Library/Caches/gvx/volumes/<name>.ext4 på macOS eller ~/.cache/gvx/volumes/<name>.ext4 på Linux) med:

  • Block om 4 KiB och dynamiska blockgrupper om 16 MiB (meta_bg): Växer dynamiskt utan stopp orsakade av allokering av glesa filer (fallocate på Linux, fcntl(F_PREALLOCATE) på macOS, FileAllocationInfo på Windows) och packar ihop oanvända bakre grupper vid ren avmontering.
  • JBD2-journal i ordnat läge (inod 8): Atomära transaktionsbekräftelser med CRC-32-kontrollsummor och återkallningsblock, samt återställning via fsckLite.
  • Stöd för overlayfs övre lager: Implementerar utökade attribut trusted.overlay.* och security.capability, teckenenhets-whiteouts (0:0), rapportering av d_type för katalogposter samt RENAME_EXCHANGE.

Praktiska arbetslaster: Docker-motor, Kubernetes (k3s + Calico) och Go-SDK

Installera rungv: SLSA-attesterade förbyggda utgåvor eller go install

Du kan ladda ner ett förbyggt rungv-arkiv för linux/amd64, linux/arm64, darwin/arm64, windows/amd64 eller windows/arm64 från GitHub Releases (vart och ett byggt av SLSA-release-pipelinen med ett Sigstore-provenansbuntel och SHA256SUMS), eller installera direkt från källkoden med go install:

1# Alternativ A: Ladda ner en förbyggd utgåva och verifiera SLSA-byggprovenans
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# Alternativ B: Installera direkt via Go (Linux / Windows)
9CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest

På macOS (darwin/arm64) är det förbyggda utgåvearkivet redan signerat med rättigheten com.apple.security.hypervisor (xattr -d com.apple.quarantine rungv tar bort webbläsarens karantän). Om du bygger lokalt med go install eller go build på macOS signerar du binären med codesign --sign - --entitlements entitlements.plist --force $(which rungv).

1. Köra en obehörig Docker-motor (docker:dind)

Enligt docs/docker-engine.md kan du köra en oförändrad docker:dind-container inuti rungv på Windows, macOS eller Linux och styra den med din värds docker-CLI:

 1# 1. Skapa värdkatalogen för det vidarebefordrade Unix-uttaget
 2mkdir -p ~/.gvx
 3
 4# 2. Starta docker:dind inuti en gvx-sandlåda med en ext4lite-volym
 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

I en andra terminal pekar du din värds docker-CLI mot sandlådans uttag:

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

2. Köra Kubernetes (k3s + Calico) i obehöriga sandlådor

Eftersom gvx implementerar /dev/kmsg, cgroup v2 (OOM-dödaren memory.max och bandbreddsbegränsningen cpu.max), /proc/diskstats, netlink-enheterna wireguard och vxlan, ipset samt iptables-legacy (addrtype, conntrack, statistic, recent), når en oförändrad rancher/k3s-avbild tillståndet Ready inuti en enda rungv-process:

 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

Du kan även ansluta flera gvx-noder över WireGuard (flannel-backend=wireguard-native eller ett wg0-nät) med inbäddad etcd för hög tillgänglighet, eller köra Calico (calico-vxlan.yaml med FELIX_IPTABLESBACKEND=Legacy) för att helt i användarläge upprätthålla Kubernetes NetworkPolicy.

3. Bädda in Linux-containrar i Go via z.gfire.dev/gvx/pkg/gvx

Go-applikationer kan bädda in Linux-containrar direkt utan en extern containerdaemon:

 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}

Vidare läsning i denna serie

  1. gvx: Köra Linux-containrar, Docker och Kubernetes i en enda obehörig process (detta inlägg)
  2. Köra Docker, docker build och docker compose utan root eller användarnamnrymder: Hur man startar docker:dind i låsta sandlådor där sudo, unshare(CLONE_NEWUSER), mount(2) och /dev/fuse är blockerade.
  3. Köra Docker-in-Docker inuti gVisor och kapslade sandlådor med gvx: Varför apt-get install docker.io och kapslad runsc misslyckas inuti gVisor över fem kärnbarriärer, och hur gvx kör kapslad docker:dind med en futex-slinga i delat minne.
  4. Snabba isolerade Docker-miljöer för RL-utrullningar av AI-agenter med gvx: Skalning av SWE-bench och förstärkningsinlärning för agenter med hundratals parallella obehöriga docker:dind-instanser och kopieringsvid-skrivning-återställningar av .ext4-volymer på 100 millisekunder.