GoSuda

gvx: Изпълнение на Linux Containers, Docker и Kubernetes в един непривилегирован процес

By Lemon Mint views ...

Въведение: контейнери със засилена сигурност без root и без хост демони

Изпълнението на Linux контейнери, docker build или Kubernetes на компютър за разработка, CI рънър или мултитенантен облачен възел обикновено налага неудобен компромис със сигурността:

  • На Linux стандартните OCI рантайми (runc, crun, runsc) изискват или привилегии root, или излагане на host /var/run/docker.sock, или --privileged (CAP_SYS_ADMIN), или user namespaces (unshare(CLONE_NEWUSER)), които разширяват повърхността на атака на хост ядрото.
  • На Windows и macOS контейнерните движещи (Docker Desktop, Podman Machine, Lima, OrbStack) стартират пълно Linux ядро на виртуална машина в WSL2, Hyper-V или Virtualization.framework и изпълняват постоянен фонов демон във вътрешността на тази VM.

gvx (CLI rungv и Go SDK z.gfire.dev/gvx/pkg/gvx) предоставя сигурна алтернатива без привилегии. Изградена като проследима спрямо горния поток форк на user-space Linux ядрото Sentry и user-space мрежовия стек netstack на Google gVisor, gvx изпълнява непроменени linux/amd64 и linux/arm64 ELF64 двоични файлове и OCI контейнери директно в един непривилегирован, самозаключващ се хост процес на Windows, macOS и Linux.

Гаранция за сигурност (триетажна защита в дълбочина): gvx никога не отслабва границите на сигурност на хост операционната система, никога не инсталира драйвери на ядрото и никога не изисква root, sudo, --privileged на хоста или CLONE_NEWUSER. Преди да изпълни и една инструкция на гост системата, gvx заключва собствения си хост процес чрез Windows Restricted Tokens и Job Objects, macOS Seatbelt (deny default) или Linux PR_SET_NO_NEW_PRIVS и BPF seccomp. Всички системни извиквания на гост системата, виртуалните привилегии UID 0, монтиранията (VFS2) и мрежовите таблици (netstack) са строго ограничени във вътрешността на безопасното по отношение на паметта Go ядро Sentry.

РантаймХост ОССтуден стартОтпечатък в паметтаНужни права на администратор или sudoФонов VM демонДиректно изпълнение на OCI образ
gvx (whpx / hvf)Windows (amd64, arm64) и macOS (arm64)~20 ms~30 MiBНеНеДа
gvx (systrap)Windows (amd64, arm64) и Linux (amd64, arm64)~25 до 65 ms~31 MiBНеНеДа
WSL2 / Docker DesktopWindows и macOS800 до 2500 ms> 512 MiBДа (първоначална настройка)ДаИзисква демон
QEMU (Full System)Windows и macOS1500 до 4000 ms> 256 MiBНеНеИзисква дисков образ

На Windows и macOS не се зарежда Linux ядро на VM. На хоста не работи фонов демон. На хостовата операционна система не се изискват root, sudo, user namespaces (CLONE_NEWUSER), mount(2), драйвери TUN/TAP или привилегии CAP_NET_ADMIN. Във вътрешността на този непривилегирован процес gvx изпълнява Docker-in-Docker (docker:dind), docker build, docker compose и многовъзлов High-Availability Kubernetes (k3s с вграден etcd и Calico CNI).


Жизнен цикъл на контейнера и триетажно хост заключване

Всяка sandbox среда на gvx работи във вътрешността на един непривилегирован хост процес и спазва тристепенен жизнен цикъл: предварително отваряне на всички необходими хост файлови дескриптори и сокети, заключване на хост процеса на операционната система чрез pkg/hostlockdown и влизане в цикъла за изпълнение на Sentry във вътрешността на процеса.

Жизнен цикъл на gvx контейнер и самозаключване на хоста

Преди да предаде управлението на първата инструкция на гост системата, gvx предварително отваря своите кеширани дескриптори на .erofs слоеве на образа, дескриптори на .ext4 томове, манипулатора *os.Root за /workspace и хост мрежовите сокети, след което извиква hostlockdown.Apply():

  1. Windows NT (lockdown_windows.go): Извиква AdjustTokenPrivileges с SE_PRIVILEGE_REMOVED, за да премахне привилегиите на място, свързва ограничен токен за имперсонация DISABLE_MAX_PRIVILEGE с Low интегритет (S-1-16-4096), присвоява процеса към анонимен Job Object с JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 1 (блокиращ създаването на всякакви дъщерни процеси) и активира DisallowWin32kSystemCalls = 1 чрез SetProcessMitigationPolicy.
  2. macOS XNU (lockdown_darwin.go): Задава RLIMIT_NPROC = 0 и прилага профил на Seatbelt (sandbox_init_with_parameters), започващ от (version 1)(deny default), който блокира process-fork, process-exec* и всички нови отваряния на хост файлове.
  3. Linux (lockdown_linux.go + pkg/gvx/syscallfilter_linux.go): Задава PR_SET_NO_NEW_PRIVS и инсталира позволения списък на BPF seccomp на Sentry за всички нишки (SECCOMP_FILTER_FLAG_TSYNC), с изрични правила EPERM за execve, execveat, clone, openat и socket.

Прихващане на системни извиквания между платформи и странициране от 4 KB (pkg/sentry/platform)

Linux ELF64 двоичните файлове предполагат гранулярност на страниците в паметта от 4 KB (0x1000) за сегментни съпоставяния с mmap(MAP_FIXED). Въпреки това Windows VirtualAlloc и MapViewOfFile3 налагат подравняване от 64 KB (0x10000), а Apple Silicon macOS (darwin/arm64) използва хостови страници от 16 KB (0x4000). Освен това всяка операционна система изисква различен механизъм за прихващане на инструкциите Linux SYSCALL (amd64) и SVC #0 (arm64) без стартиране на Linux VM.

gvx решава и двата проблема чрез четири бекенда за изпълнение, които реализират pkg/sentry/platform.Context:

Механика на прихващане на системни извиквания и странициране от 4KB в gvx платформата

1. Хардуерен двигател за Windows: platform/whpx (WinHvPlatform.dll)

На windows/amd64 и windows/arm64 platform/whpx създава лек хипервайзър дял чрез WHvCreatePartition, без да стартира ядро на гост операционна система:

  • Разделител 64 KB към 4 KB страници: whpx регистрира всяка 64 MiB арена на pgalloc.MemoryFile при дяла еднократно чрез WHvMapGpaRange. Последващите извиквания на MapFile и Unmap променят само таблиците със страници на гост системата във вътрешността на дяла, налагайки точни права R/W/X върху страници от 4 KB, въпреки гранулярността на разпределение от 64 KB на Windows.
  • Обмен на регистри без API чрез CommPage:
    • На amd64 SYSCALL от гост Ring-3 влиза във вътрешен Ring-0 стъб MSR_LSTAR (stubgen_x86.go), който записва GPR регистрите на бързия път за системни извиквания (RAX, RDI, RSI, RDX, R10, R8, R9, RIP, RSP) в споделената CommPage от 4 KB и изпълнява HLT, за да излезе от WHvRunVirtualProcessor.
    • На arm64 SVC #0 от гост EL0 влиза във векторите на EL1 (stubgen_a64.go), записва X0..X8, SP_EL0, ELR_EL1, PSTATE в CommPage и зарежда от незаделен GPA нула, за да предизвика паметно излизане обратно към Sentry.

2. Хардуерен двигател за macOS: platform/hvf (Hypervisor.framework)

На Apple Silicon (darwin/arm64) platform/hvf изпълнява нишките на контейнерите във vCPU на Hypervisor.framework (hv_vm_create, hv_vcpu_run):

  • Двустепенен MMU превод от 4 KB върху 16 KB: Stage-2 (hv_vm_map) съпоставя фрагменти от памет-файла по 1 GiB в пространството на междинните физически адреси (IPA) еднократно с гранулярност на хостовите страници от 16 KB, с HV_MEMORY_READ | HV_MEMORY_WRITE | HV_MEMORY_EXEC. Във вътрешността на vCPU стъбът EL1 bluepill (bluepill_arm64.go) конфигурира TCR_EL1.TG0 = 0b00 (гранула от 4 KB) в TTBR0_EL1 и попълва записите на таблиците със страници Stage-1 от 4 KB с точни права R-X или RW- (UXN=1). Тъй като ARM64 MMU налага сечението на правата на Stage-1 и Stage-2, хардуерът улавя грешките при права на точни граници от 4 KB.
  • Прихващане на системни извиквания: SVC #0 от гост EL0 преминава към EL1 VBAR_EL1, записва регистрите в споделената CommPage от 4 KB и изпълнява HVC #0, за да се върне от hv_vcpu_run към нишката на Go Sentry.

3. Резервен потребителски режим за Windows: platform/systrap (winsystrap)

Когато хардуерната виртуализация не е налице (например във вложени облачни VM), winsystrap изпълнява гост код в потребителски режим на windows/amd64 и windows/arm64:

  • Хардуерна изолация чрез CR3 / TTBR0_EL1: Нишките на гост системата работят във вътрешността на изолирани подпроцеси стъбове, стартирани от генерирано изпълнимо изображение от 1 KB (stubimage_xplat.go), ограничено от токен с Low интегритет и Job Object.
  • Патчване на двоичния код на място и IPC winSysmsg: На amd64 патчерът (patcher_amd64_xplat.go) заменя 2-байтовите инструкции syscall (0f 05) с 5-байтови скокове jmp rel32 (или с капани CC 90, когато 5-байтов прозорец не е наличен) към остров за бърз път. На arm64 svc #0 се патчва с клон B към слот в острова. Островът записва регистрите в споделената страница winSysmsg и се синхронизира със Sentry чрез WaitOnAddress и WakeByAddressSingle, докато непатчнатите капани и грешките на страници спират при Windows Debug Object (WaitForDebugEventEx).

4. Двигател за Linux в потребителски режим: platform/systrap

На linux/amd64 и linux/arm64 systrap изпълнява нишки на гост системата (sysmsg), които прихващат системните извиквания на гост системата чрез SECCOMP_RET_TRAP (SIGSYS) и двоично патчват местата syscall в трамплин в споделена памет (sysmsg), синхронизиран чрез SYS_FUTEX. Междувременно стъбовата нишка syscallThread (syscall_thread.go) изпълнява операции по адресното пространство (mmap, munmap, clone) чрез SECCOMP_RET_USER_NOTIF (_RUN_SECCOMP_LOOP). Когато gvx работи във вътрешността на външна gVisor (runsc) среда, в която единственият слот SECCOMP_FILTER_FLAG_NEW_LISTENER връща EBUSY, gvx автоматично превключва syscallThread към цикъл с futex в споделена памет (_RUN_SYSCALL_LOOP), както е описано в част 3 от тази серия.


Как gvx съпоставя системните извиквания на гост Linux с виртуални подсистеми и хостови манипулатори

След като платформеният двигател прихване инструкцията SYSCALL (amd64) или SVC #0 (arm64) на гост системата и запише регистрите на гост системата в страницата от 4 KB (CommPage, sysmsg или winSysmsg), управлението се връща към горутината на задачата на Go Sentry (pkg/sentry/kernel/task_syscall.go). Вместо да препраща системните извиквания на Linux към ядрото на основната хостова операционна система, Task.doSyscall() запазва първоначалното състояние на регистрите (SyscallSaveOrig()), оценява всички вътрешни за гост системата BPF филтри seccomp и спирания ptrace и изпраща номера на системното извикване през linux.AMD64 или linux.ARM64 (pkg/sentry/syscalls/linux/linux64.go, обхващащ ~370 системни извиквания на Linux) към четири вътрешни домейна:

Как gvx съпоставя системните извиквания на гост Linux с подсистеми и хостовата ОС

Домейн на системни извикванияПредставителни системни извиквания на гост LinuxВътрешна подсистема на gvx (pkg/sentry)Взаимодействие с границата на хостовата ОС (след hostlockdown)
A. Процеси, namespace, монтиране и синхронизацияclone, execve, unshare, setns, mount, umount2, pivot_root, setuid, capset, futex, epoll_wait, pipe2kernel.Task, виртуални идентификационни данни, таблица futex и VFS2 в паметта (procfs, sysfs, cgroupfs v2, tmpfs, overlayfs)Нула хостови системни извиквания (100% състояние в паметта на Go; напълно невидимо за хостовата ОС)
B. Виртуална памет (MM)mmap, munmap, mremap, mprotect, brk, madvise, memfd_createmm.MemoryManager (дърво на VMA от 4 KB) и pgalloc.MemoryFileДвупосочни обновявания на таблиците със страници от 4 KB чрез WHvMapGpaRange, hv_vm_map + TTBR0_EL1 или стъбови mmap/mprotect
C. Файлове и съхранение (VFS2)openat, pread64, pwrite64, getdents64, statx, renameat2, setxattr, mknod, inotify_add_watchМаршрутизатор на таблицата с монтирания vfs.VirtualFilesystem (fsimpl/erofs, fsimpl/ext4lite, fsimpl/hostfs v2)Двупосочни pread/pwrite към 1 закачен FD на .erofs или 1 FD на .ext4, или ограничени относителни обхождания върху *os.Root
D. Мрежа и Netfiltersocket, bind, listen, connect, sendmsg, recvmsg, netlink, setsockopt(IPT_SO_SET_REPLACE)Потребителско пространство pkg/tcpip (lo, eth0, docker0, veth, vxlan, wireguard, ipset, iptables-legacy)Трафикът във вътрешността на sandbox среда използва нула хостови системни извиквания; външният трафик се съпоставя с net.Dial/net.Listen (-net=nat) или с 1 UDP сокет (-net=wg)

Когато обработчикът върне (rval, ctrl, err), Task.doSyscallInvoke() превръща всяка грешка в отрицателен Linux errno (-ExtractErrno(err, sysno)), записва върнатата стойност обратно в споделената страница с регистри (RAX на amd64, X0 на arm64) и възобновява изпълнението на гост системата.


Съхранение (VFS2) и двупътна мрежа в потребителско пространство (netstack)

Всички файлови и мрежови операции на гост системата се обслужват във вътрешността на Go Sentry чрез три файлови системи VFS2 и два трака pkg/netbackend:

Двигатели за съхранение VFS2 и двупътен netstack в gvx

Двигател за съхранениеРоля на монтиране в gvxХостов ресурс зад негоИзвиквания openat / NtCreateFile към хоста при изпълнениеСканирания на файлове от Windows Defender (WdFilter.sys)
fsimpl/erofsДолни слоеве на OCI образ, само за четенеЕдин файл <sha256>.erofs на слой00
fsimpl/ext4liteИменувани томове (-v name:/path)Един файл <name>.ext4 на том00
fsimpl/hostfs v2Работно пространство на хоста (-workspace, -v /host:/guest)Закачена хостова директория *os.RootO(depth) при всяко некеширано отваряне1 при всяко затваряне на хостов файл

Защо ext4lite е важен за Docker, бази данни и мениджъри на пакети

Извличането или записването на десетки хиляди файлове на контейнери директно върху Windows NTFS или macOS APFS предизвиква сканиране от антивирусния minifilter (WdFilter.sys), конфликти при чувствителност към регистър на имената и липсващи разширени атрибути на POSIX.

pkg/sentry/fsimpl/ext4lite реализира чиста Go файлова система ext4 във вътрешността на един предварително отворен хостов файл (%LOCALAPPDATA%\gvx\cache\volumes\<name>.ext4 на Windows, ~/Library/Caches/gvx/volumes/<name>.ext4 на macOS или ~/.cache/gvx/volumes/<name>.ext4 на Linux) със следните характеристики:

  • Блокове от 4 KiB и динамични блокови групи от 16 MiB (meta_bg): Расте динамично, без забавяния при разпределяне на разредени файлове (fallocate на Linux, fcntl(F_PREALLOCATE) на macOS, FileAllocationInfo на Windows), и уплътнява неизползваните крайни групи при чисто размонтиране.
  • Журнал JBD2 в ordered режим (inode 8): Атомарно потвърждаване на транзакции с контролни суми CRC-32 и revoke блокове, плюс възстановяване чрез fsckLite.
  • Поддръжка на горния слой на overlayfs: Реализира разширените атрибути trusted.overlay.* и security.capability, символни whiteout устройства (0:0), отчитане на записи в директории d_type и RENAME_EXCHANGE.

Практически натоварвания: Docker Engine, Kubernetes (k3s + Calico) и Go SDK

Инсталиране на rungv: предварително компилирани издания с SLSA атестация или go install

Можете да изтеглите предварително компилиран архив rungv за linux/amd64, linux/arm64, darwin/arm64, windows/amd64 или windows/arm64 от GitHub Releases (всеки от които е изграден от конвейера за издания SLSA с пакет за произход Sigstore и SHA256SUMS), или да инсталирате директно от изходния код с go install:

1# Вариант A: Изтеглете предварително компилирано издание и проверете 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# Вариант B: Инсталирайте директно чрез Go (Linux / Windows)
9CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest

На macOS (darwin/arm64) предварително компилираният архив на изданието вече е подписан с правото com.apple.security.hypervisor (xattr -d com.apple.quarantine rungv премахва карантината на браузъра). Ако компилирате локално с go install или go build на macOS, подпишете двоичния файл с codesign --sign - --entitlements entitlements.plist --force $(which rungv).

1. Стартиране на непривилегирован Docker Engine (docker:dind)

Според docs/docker-engine.md можете да стартирате непроменен контейнер docker:dind в rungv на Windows, macOS или Linux и да го управлявате със своя хостов CLI docker:

 1# 1. Създайте хостовата директория за пренасочения Unix сокет
 2mkdir -p ~/.gvx
 3
 4# 2. Стартирайте docker:dind в sandbox среда на gvx с том 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

Във втори терминал насочете своя хостов CLI docker към сокета на sandbox средата:

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

2. Стартиране на Kubernetes (k3s + Calico) в непривилегировани sandbox среди

Тъй като gvx реализира /dev/kmsg, cgroup v2 (убиецът на OOM memory.max и ограничаването на честотната лента cpu.max), /proc/diskstats, мрежовите устройства wireguard и vxlan netlink, ipset и iptables-legacy (addrtype, conntrack, statistic, recent), непроменен образ rancher/k3s достига състояние Ready във вътрешността на един процес 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

Можете също да свържете няколко възела gvx през WireGuard (flannel-backend=wireguard-native или мрежа wg0) с вграден etcd и висока наличност, или да стартирате Calico (calico-vxlan.yaml с FELIX_IPTABLESBACKEND=Legacy), за да наложите Kubernetes NetworkPolicy изцяло в потребителското пространство.

3. Вграждане на Linux контейнери в Go чрез z.gfire.dev/gvx/pkg/gvx

Go приложенията могат да вграждат Linux контейнери директно, без външен демон за контейнери:

 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}

Допълнително четиво в тази серия

  1. gvx: Изпълнение на Linux контейнери, Docker и Kubernetes в един непривилегирован процес (тази публикация)
  2. Изпълнение на Docker, docker build и docker compose без root или User Namespaces: Как да стартирате docker:dind в заключени sandbox среди, където sudo, unshare(CLONE_NEWUSER), mount(2) и /dev/fuse са блокирани.
  3. Изпълнение на Docker-in-Docker във вътрешността на gVisor и вложени sandbox среди с gvx: Защо apt-get install docker.io и вложеният runsc се провалят във вътрешността на gVisor през пет бариери на ядрото и как gvx изпълнява вложен docker:dind чрез цикъл с futex в споделена памет.
  4. Бързи изолирани Docker среди за RL rollouts на AI агенти с gvx: Мащабиране на SWE-bench и rollouts за обучение на агенти с подкрепление със стотици паралелни непривилегировани инстанции на docker:dind и 100-милисекундни копия при запис (copy-on-write) на .ext4 томове.