GoSuda

gvx: запуск Linux-контейнеров, Docker и Kubernetes в одном непривилегированном процессе

By Lemon Mint views ...

Введение: защищенные контейнеры без root и хост-демонов

Запуск Linux-контейнеров, docker build или Kubernetes на машине разработчика, CI-раннере или многопользовательском облачном узле обычно вынуждает идти на неудобный компромисс в области безопасности:

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

gvx (CLI rungv и Go SDK z.gfire.dev/gvx/pkg/gvx) предоставляет безопасную альтернативу без повышенных привилегий. Созданный как форк проекта Google gVisor, отслеживаемый по upstream, с пользовательским ядром Linux (Sentry) и пользовательским сетевым стеком (netstack), 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) строго ограничены внутри безопасного в отношении памяти ядра Sentry на Go.

Среда выполненияОС хостаХолодный запускОбъем памятиТребуются права администратора или sudoФоновый демон ВМПрямой запуск образа 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 и macOSот 800 до 2500 ms> 512 MiBДа (первоначальная настройка)ДаТребуется демон
QEMU (Full System)Windows и macOSот 1500 до 4000 ms> 256 MiBНетНетТребуется образ диска

Ядро Linux в виртуальной машине на Windows или macOS не загружается. Фоновый демон на хосте не запускается. Никакие root, sudo, пространства имен пользователя (CLONE_NEWUSER), mount(2), драйверы TUN/TAP или привилегии CAP_NET_ADMIN на хостовой ОС не требуются. Внутри этого непривилегированного процесса gvx запускает Docker-in-Docker (docker:dind), docker build, docker compose и многоузловой высокодоступный Kubernetes (k3s со встроенным etcd и CNI Calico).


Жизненный цикл контейнера и трехкольцевая самоблокировка хоста

Каждая песочница gvx работает внутри одного непривилегированного процесса хоста и реализует трехфазный жизненный цикл: предварительное открытие всех необходимых дескрипторов файлов и сокетов хоста, блокировку процесса хостовой ОС через pkg/hostlockdown и вход в цикл выполнения Sentry внутри процесса.

gvx Container Lifecycle and Host Self-Lockdown

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

  1. Windows NT (lockdown_windows.go): вызывает AdjustTokenPrivileges с SE_PRIVILEGE_REMOVED, чтобы удалить привилегии на месте, привязывает ограниченный токен олицетворения DISABLE_MAX_PRIVILEGE с низким уровнем целостности (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), а macOS на Apple Silicon (darwin/arm64) использует страницы хоста размером 16 KB (0x4000). Кроме того, каждой ОС требуется свой механизм для перехвата инструкций Linux SYSCALL (amd64) и SVC #0 (arm64) без загрузки виртуальной машины Linux.

gvx решает обе проблемы в четырех бэкендах выполнения, которые реализуют pkg/sentry/platform.Context:

gvx Platform Syscall Interception and 4KB Paging Mechanics

1. Аппаратный движок Windows: platform/whpx (WinHvPlatform.dll)

На windows/amd64 и windows/arm64 platform/whpx создает легковесный раздел гипервизора через WHvCreatePartition, не загружая ядро гостевой ОС:

  • Развязка страниц 64 KB и 4 KB (64 KB-to-4 KB Paging Decoupler): whpx регистрирует каждую арену pgalloc.MemoryFile размером 64 MiB в разделе один раз через WHvMapGpaRange. Последующие вызовы MapFile и Unmap изменяют только таблицы страниц гостя внутри раздела, обеспечивая точные права R/W/X на уровне 4 KB, несмотря на гранулярность выделения 64 KB в Windows.
  • Обмен регистрами без API через CommPage:
    • На amd64 гостевой SYSCALL из Ring-3 переходит в заглушку MSR_LSTAR в Ring-0 внутри раздела (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). Поскольку MMU ARM64 применяет пересечение прав Stage-1 и Stage-2, аппаратное обеспечение перехватывает ошибки прав доступа на точных границах 4 KB.
  • Перехват системного вызова: гостевой SVC #0 из EL0 переходит к VBAR_EL1 в EL1, сохраняет регистры в общую страницу CommPage размером 4 KB и выполняет HVC #0, чтобы вернуться из hv_vcpu_run в поток Go Sentry.

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

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

  • Аппаратная изоляция CR3 / TTBR0_EL1: гостевые потоки работают внутри изолированных подпроцессов-заглушек, загруженных из сгенерированного исполняемого файла размером 1 KB (stubimage_xplat.go), ограниченных токеном с низким уровнем целостности и Job Object.
  • Патчинг бинарного кода на месте и IPC winSysmsg: на amd64 патчер (patcher_amd64_xplat.go) заменяет инструкции syscall (0f 05) длиной 2 байта на переходы jmp rel32 длиной 5 байт (или на ловушки-точки останова CC 90, когда окно в 5 байт недоступно) в «остров» быстрого пути. На arm64 svc #0 патчится переходом B к слоту острова. Остров сохраняет регистры в общую страницу winSysmsg и синхронизируется с Sentry через WaitOnAddress и WakeByAddressSingle, тогда как непатченные ловушки и ошибки страниц останавливаются на отладочном объекте Windows (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) в четыре внутренних домена:

How gvx Maps Guest Linux Syscalls to Subsystems and Host OS

Домен системных вызововТипичные системные вызовы гостевого LinuxВнутренняя подсистема gvx (pkg/sentry)Взаимодействие с границей ОС хоста (после hostlockdown)
A. Процессы, пространства имен, монтирование и синхронизация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 для одного закрепленного FD .erofs или одного 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)Трафик внутри песочницы не использует системных вызовов хоста; внешний трафик отображается на net.Dial/net.Listen (-net=nat) или на один UDP-сокет (-net=wg)

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


Хранилище (VFS2) и двухканальная пользовательская сеть (netstack)

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

gvx VFS2 Storage Engines and Dual-Track Netstack

Движок хранилищаРоль монтирования в 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, символьные устройства-заглушки (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 внутри песочницы 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 на хосте на сокет песочницы:

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 ... (в контейнере)
6
7docker build -t myapp ./app
8docker compose up -d

2. Запуск Kubernetes (k3s + Calico) в непривилегированных песочницах

Поскольку gvx реализует /dev/kmsg, cgroup v2 (убийца OOM memory.max и ограничение пропускной способности cpu.max), /proc/diskstats, сетевые устройства netlink wireguard и vxlan, 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), чтобы полностью обеспечить соблюдение NetworkPolicy Kubernetes в пользовательском пространстве.

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 и пространств имен пользователя: как запустить docker:dind в ограниченных песочницах, где заблокированы sudo, unshare(CLONE_NEWUSER), mount(2) и /dev/fuse.
  3. Запуск Docker-in-Docker внутри gVisor и вложенных песочниц с gvx: почему apt-get install docker.io и вложенный runsc не работают внутри gVisor из-за пяти барьеров ядра и как gvx запускает вложенный docker:dind с помощью цикла futex в разделяемой памяти.
  4. Быстрые изолированные окружения Docker для развертываний RL-агентов с gvx: масштабирование SWE-bench и развертываний обучения с подкреплением агентов на сотнях параллельных непривилегированных экземпляров docker:dind со сбросом томов .ext4 по механизму copy-on-write за 100 миллисекунд.