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

Введение: защищенные контейнеры без 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) или LinuxPR_SET_NO_NEW_PRIVSи BPFseccomp. Все системные вызовы гостя, виртуальные привилегии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 Desktop | Windows и 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 заранее открывает кэшированные дескрипторы слоев образа .erofs, дескрипторы файлов томов .ext4, дескриптор *os.Root для /workspace и сетевые сокеты хоста, затем вызывает hostlockdown.Apply():
- 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. - macOS XNU (
lockdown_darwin.go): устанавливаетRLIMIT_NPROC = 0и применяет профиль Seatbelt (sandbox_init_with_parameters), начинающийся с(version 1)(deny default), который блокируетprocess-fork,process-exec*и все новые открытия файлов хоста. - Linux (
lockdown_linux.goиpkg/gvx/syscallfilter_linux.go): устанавливаетPR_SET_NO_NEW_PRIVSи применяет разрешающий список BPFseccompядра 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:

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 заглушка EL1bluepill(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 байт недоступно) в «остров» быстрого пути. Наarm64svc #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) в четыре внутренних домена:

| Домен системных вызовов | Типичные системные вызовы гостевого Linux | Внутренняя подсистема gvx (pkg/sentry) | Взаимодействие с границей ОС хоста (после hostlockdown) |
|---|---|---|---|
| A. Процессы, пространства имен, монтирование и синхронизация | clone, execve, unshare, setns, mount, umount2, pivot_root, setuid, capset, futex, epoll_wait, pipe2 | kernel.Task, виртуальные учетные данные, таблица futex и VFS2 в памяти (procfs, sysfs, cgroupfs v2, tmpfs, overlayfs) | Ноль системных вызовов хоста (100% состояния Go в памяти; полностью невидимо для ОС хоста) |
B. Виртуальная память (MM) | mmap, munmap, mremap, mprotect, brk, madvise, memfd_create | mm.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. Сеть и Netfilter | socket, 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 | Базовый ресурс хоста | Вызовы openat / NtCreateFile на хосте во время выполнения | Посекторные проверки Windows Defender (WdFilter.sys) |
|---|---|---|---|---|
fsimpl/erofs | Нижние слои образов OCI только для чтения | Один файл <sha256>.erofs на слой | 0 | 0 |
fsimpl/ext4lite | Именованные тома (-v name:/path) | Один файл <name>.ext4 на том | 0 | 0 |
fsimpl/hostfs v2 | Рабочая область хоста (-workspace, -v /host:/guest) | Закрепленный каталог хоста *os.Root | O(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}
Дополнительная литература в этой серии
- gvx: запуск Linux-контейнеров, Docker и Kubernetes в одном непривилегированном процессе (этот пост)
- Запуск Docker, docker build и docker compose без root и пространств имен пользователя: как запустить
docker:dindв ограниченных песочницах, где заблокированыsudo,unshare(CLONE_NEWUSER),mount(2)и/dev/fuse. - Запуск Docker-in-Docker внутри gVisor и вложенных песочниц с gvx: почему
apt-get install docker.ioи вложенныйrunscне работают внутриgVisorиз-за пяти барьеров ядра и какgvxзапускает вложенныйdocker:dindс помощью цикла futex в разделяемой памяти. - Быстрые изолированные окружения Docker для развертываний RL-агентов с gvx: масштабирование
SWE-benchи развертываний обучения с подкреплением агентов на сотнях параллельных непривилегированных экземпляровdocker:dindсо сбросом томов.ext4по механизму copy-on-write за 100 миллисекунд.