Стартиране на Docker-in-Docker вътре в gVisor и Nested Sandboxes с gvx

Бързо решение: Сигурно изпълнение на docker:dind в непривилегирован gVisor (runsc) sandbox
Платформите за AI агенти в облака, deployment-ите на Kubernetes agent-sandbox (RuntimeClass: gvisor) и harness-ите за оценка при обучение с подсилване изолират недоверени работни натоварвания в контейнери gVisor (runsc), именно за да защитят ядрото на хоста. Когато работно натоварване в стандартен непривилегирован контейнер runsc се опита да инсталира docker.io или docker-ce чрез apt-get и да стартира dockerd, операцията се проваля, дори ако процесът има root (UID 0) в контейнера.
С gvx (rungv) можете сигурно да изпълнявате docker:dind, docker build и docker compose директно в непривилегирован контейнер gVisor (runsc) (или да вложите gvx в gvx) без да отслабвате външния RuntimeClass, без да излагате хостови сокети и без да предоставяте --privileged.
Запазване и засилване на изолацията на външния sandbox: Изпълнението на
rungvвrunscне заобикаля нито едно ограничение, наложено от външния sandbox наgVisor. Вместо товаgvxсъздава втори, вложен потребителскиgVisorSentrykernel със собственhostlockdown(PR_SET_NO_NEW_PRIVS+seccomp), собственnetstackв паметта и единиченext4liteтом във файл. Вложенитеdockerdиruncконтейнери са с двойна изолация: техните системни извиквания, монтирания и сурови пакети никога не напускат вътрешното потребителско пространство на ядрото наgvx.
1# Във вече съществуващ контейнер gVisor (runsc) или Kubernetes Pod:
2# Инсталирайте rungv от архив на GitHub release с SLSA атестация или чрез go install:
3CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest
4
5mkdir -p ~/.gvx
6rungv run \
7 -p unix:~/.gvx/docker.sock:2375 \
8 -v gvx-docker:/var/lib/docker \
9 -e DOCKER_TLS_CERTDIR= \
10 docker:dind --tls=false &
11
12# Изпълнявайте стандартни Docker build-ове и контейнери срещу вътрешния gvx сокет
13export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
14until docker info >/dev/null 2>&1; do sleep 0.5; done
15docker info --format '{{.Driver}} {{.OperatingSystem}}'
16# Изход: overlayfs Alpine Linux ... (контейнеризиран)
17
18docker build -t nested-app:test .
19docker run --rm nested-app:test
Пет причини, поради които директната инсталация на Docker се проваля в gVisor (runsc)
Когато потребител или AI агент за кодиране изпълни apt-get install -y docker.io (или docker-ce) в стандартен контейнер runsc и се опита да стартира dockerd, той се сблъсква с пет последователни технически бариери.
1. Зареждане на kernel модули (modprobe) спрямо виртуалното ядро 4.4.0
По време на инсталиране на пакети и стартиране на dockerd скриптовете за инициализация изпълняват modprobe, за да заредят overlay, br_netfilter, ip_tables, nf_nat и xt_conntrack. Външният Sentry на gVisor блокира init_module(2) и finit_module(2) и докладва виртуална версия на ядрото (4.4.0), за която няма директория /lib/modules/4.4.0:
1modprobe: FATAL: Module overlay not found in directory /lib/modules/4.4.0
2modprobe: FATAL: Module br_netfilter not found in directory /lib/modules/4.4.0
3modprobe: FATAL: Module ip_tables not found in directory /lib/modules/4.4.0
4modprobe: FATAL: Module nf_nat not found in directory /lib/modules/4.4.0
2. Провали при iptables-nft (nf_tables Netlink) и моста docker0
На Ubuntu 22.04, Ubuntu 24.04 и Debian 11/12 apt-get install docker.io избира iptables-nft (базиран на подсистемата nf_tables netlink) като алтернатива по подразбиране за /usr/sbin/iptables. В стандартен контейнер gVisor без административни мрежови привилегии или поддръжка на nf_tables netlink dockerd се срива при инициализиране на NAT веригата DOCKER и моста docker0:
1failed to start daemon: Error initializing network controller: error obtaining controller instance: failed to create NAT chain DOCKER: iptables failed: iptables -t nat -N DOCKER
2iptables v1.8.10 (nf_tables): Could not fetch rule set generation id: Invalid argument
3iptables v1.8.10 (legacy): can't initialize iptables table 'nat': Table does not exist (do you need to insmod?)
4iptables: No chain/target/match by that name.
5Unable to enable SKIP DNAT rule: (iptables failed: iptables --wait -t nat -I DOCKER -i docker0 -j RETURN: iptables: No chain/target/match by that name.)
6Error initializing network controller: error obtaining controller instance: failed to create bridge "docker0": operation not permitted
3. Провал на overlay2 върху gofer / 9p rootfs и изчерпване на диска при vfs
Освен ако външният администратор на runsc изрично не е предконфигурирал специален tmpfs или filestore монтаж в /var/lib/docker, rootfs на контейнера се поддържа от gofer на gVisor (9p/lisafs) или от вложен overlay монтаж. Монтирането на overlay2 върху /var/lib/docker се проваля:
1level=error msg="[graphdriver] prior storage driver overlay2 failed: driver not supported"
2failed to mount overlay: invalid argument
3Error starting daemon: error initializing graphdriver: driver not supported: overlay2
Когато потребители или агенти се опитат да заобиколят тази грешка чрез стартиране на dockerd --storage-driver=vfs, Docker напълно деактивира споделянето на слоеве с Copy-on-Write. Всяка инструкция RUN във Dockerfile извършва пълно рекурсивно копиране (dirCopy) на целия root файлов system, като умножава образ от 500 MB през 10 стъпки на build в 5 GB записи и прекратява sandbox-а с no space left on device или I/O timeout.
4. Отхвърляне на вложени runc cgroup и /proc монтирания
Дори ако dockerd бъде принуден да стартира с dockerd --iptables=false --bridge=none --storage-driver=vfs, изпълнението на docker run или docker build извиква runc, за да създаде вътрешния контейнер. В стандартен непривилегирован контейнер gVisor runc се проваля при монтиране на cgroups, при ремонтиране на /proc или при зареждане на вътрешни seccomp филтри:
1docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process:
2error during container init: error mounting "cgroup" to rootfs at "/sys/fs/cgroup": mount cgroup:/sys/fs/cgroup/cpuset (via /proc/self/fd/6), flags: 0xf, data: cpuset: operation not permitted
3cgroups: cgroup mountpoint does not exist: unknown
4runc create failed: systemd not running on this host: cannot use systemd cgroup manager
5error during container init: error mounting "proc" to rootfs at "/proc": mount proc:/proc (via /proc/self/fd/6), flags: 0xe: operation not permitted
6error during container init: error loading seccomp filter into kernel: operation not permitted
5. Вложеният runsc се проваля с SECCOMP_FILTER_FLAG_NEW_LISTENER (EBUSY)
Ако потребител се опита да стартира друга инстанция на runsc вътре в runsc, използвайки платформата systrap, инициализацията прекъсва незабавно:
1creating subprocess: sysmsg: failed to initialize thread: failed to install seccomp notify rules: device or resource busy
Както Linux kernel, така и Sentry на gVisor позволяват най-много един слушател SECCOMP_FILTER_FLAG_NEW_LISTENER (SECCOMP_RET_USER_NOTIF) на йерархия от задачи. Тъй като външният sandbox вече държи този слот за слушател, вътрешното извикване seccomp(2) връща EBUSY (device or resource busy).
Как gvx решава вложеното изпълнение в gVisor
gvx се справя с всичките пет бариери в собствения си потребителски kernel и слой на платформата, което позволява rungv да работи във външен контейнер gVisor (runsc) или дори рекурсивно в друг контейнер gvx (gvx в gvx).

1. Автоматичен fallback от systrap от EBUSY към futex цикъла _RUN_SYSCALL_LOOP
В Linux systrap всеки stub подпроцес изпълнява два вида нишки:
- Нишки за изпълнение на гост (
sysmsg): прихващат системни извиквания на госта чрезSECCOMP_RET_TRAP(SIGSYS) и бинарно пачнатиjmp rel32трамплини (syshandler), като обменят състояние на регистрите със Sentry чрез споделена памет иSYS_FUTEX. - Stub
syscallThread(syscall_thread.go): изпълнява системни извиквания за адресното пространство (mmap,munmap,clone) в stub подпроцеса от името на Sentry.
В upstream gVisor syscallThread поддържа два режима на събуждане в stub_amd64.s и stub_arm64.s:
- Seccomp User-Notify цикъл (
_RUN_SECCOMP_LOOP = 6, когатоt.seccompNotify != nil): По време наsyscallThread.init()t.installSeccompNotify()инсталира слушателSECCOMP_RET_USER_NOTIF(seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)), така че Sentry да може да изпраща заявки чрезioctl(SECCOMP_IOCTL_NOTIF_SEND)иioctl(SECCOMP_IOCTL_NOTIF_RECV). - Цикъл за futex системни извиквания (
_RUN_SYSCALL_LOOP = 5, когатоt.seccompNotify == nil):syscallThreadизпълняваsyscall_loop(stub_amd64.s/stub_arm64.s), като спи върхуSYS_FUTEX(FUTEX_WAIT) над споделената двустранична областsyscallSentryMessage(PROT_READв stub) иsyscallStubMessage(PROT_READ | PROT_WRITEв stub).
В upstream gVisor t.installSeccompNotify() се извиква безусловно, когато seccompNotifyIsSupported е true, а създаването на stub се прекратява, ако seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...) върне EBUSY.
В gvx (pkg/sentry/platform/systrap/seccomp_notify_xplat.go и syscall_thread.go) syscallThread.init() извиква t.installSeccompNotifyIfFree():
1// pkg/sentry/platform/systrap/seccomp_notify_xplat.go
2var seccompNotifyBusy atomic.Bool
3
4func (t *syscallThread) installSeccompNotifyIfFree() (*os.File, error) {
5 if seccompNotifyBusy.Load() {
6 return nil, nil
7 }
8 f, err := t.installSeccompNotify()
9 if errors.Is(err, unix.EBUSY) {
10 if seccompNotifyBusy.CompareAndSwap(false, true) {
11 log.Warningf("Another seccomp listener exists in the filter chain of the host process. " +
12 "The stub threads use the slower futex loop instead of seccomp-unotify.")
13 }
14 return nil, nil
15 }
16 return f, err
17}
Когато installSeccompNotify() върне unix.EBUSY, защото външен gVisor (runsc) или външен sandbox gvx вече държи единствения слот за слушател във веригата от филтри на процеса:
installSeccompNotifyIfFree()задаваseccompNotifyBusy(atomic.Bool), записва едно предупреждение за процеса и оставяt.seccompNotify == nilза този stub и за всички следващи stub-ове.- В
syscallThread.detach()(syscall_thread_amd64.goиsyscall_thread_arm64.go) приt.seccompNotify == nilсе задаваregs.Rbx = _RUN_SYSCALL_LOOP(5) наamd64(илиregs.Regs[9] = _RUN_SYSCALL_LOOPнаarm64) вместо_RUN_SECCOMP_LOOP(6). - В
syscallThread.syscall()(syscall_thread.go) вътрешният Sentry изпраща системните извиквания на stub-а изцяло чрез споделена памет иSYS_FUTEX:
1// pkg/sentry/platform/systrap/syscall_thread.go
2// Уведомяване на syscall нишката за нова заявка за системно извикване.
3atomic.AddUint32(&sentryMsg.state, 1)
4futexWakeUint32(&sentryMsg.state)
5
6// Изчакване на отговор.
7futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
Тъй като филтрите SECCOMP_RET_TRAP върху нишките на госта могат да бъдат произволно дълбоко в стека, а syscallThread преминава към _RUN_SYSCALL_LOOP, без да изисква втори слушател за потребителски нотификации, gvx работи чисто в runsc и поддържа рекурсивно вложване (gvx в gvx).
2. Нативен overlayfs върху единични ext4lite томове
За да се избегне монтиране на overlayfs върху root файловата система gofer (9p/lisafs) на външния контейнер gVisor, -v gvx-docker:/var/lib/docker поддържа /var/lib/docker с вътрешната файлова система ext4lite на gvx, съхранена в единичен обикновен файл (gvx-docker.ext4).
За външния runsc gvx-docker.ext4 е просто един обикновен файл, достъпван чрез pread64(2) и pwrite64(2). Вътре в gvx, ext4lite осигурява файлова система ext4 с JBD2 журнал, с разширени атрибути trusted.overlay.* и security.capability, възли whiteout от тип символно устройство (0:0), записи в директории d_type и RENAME_EXCHANGE, което позволява на вътрешния dockerd да използва нативния си драйвер за съхранение overlayfs.
3. Самостоятелен netstack с docker0 и iptables-legacy
Вътре в gvx, dockerd не взаимодейства с мрежовите интерфейси или netfilter таблиците на външния контейнер runsc. Вместо това:
dockerdсъздава мостаdocker0иvethдвойките вътре в собствения потребителскиnetstackнаgvx.- Всички правила
iptables-legacy(-t nat -N DOCKER,MASQUERADE,conntrack,addrtype) се изпълняват срещу вътрешната netfilter имплементация наgvx. - Вложените
runcконтейнери монтират своите йерархии/proc,/sysи/sys/fs/cgroupвъв вътрешната таблица на монтираниятаVFS2в паметта наgvx, без никога да извикват системното извикванеmount(2)на външнияrunsc.
Проверка на вложен docker:dind в gVisor (или gvx в gvx)
Можете да проверите и двата сценария (rungv в runsc или rungv в rungv) със следните команди:
1# Във външен контейнер gVisor (runsc) или външен контейнер rungv:
2mkdir -p ~/.gvx
3rungv run \
4 -p unix:~/.gvx/docker.sock:2375 \
5 -v gvx-docker:/var/lib/docker \
6 -e DOCKER_TLS_CERTDIR= \
7 docker:dind --tls=false &
8DIND_PID=$!
9
10export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
11until docker info >/dev/null 2>&1; do sleep 0.5; done
12
13# Проверете, че вътрешният Docker daemon използва нативен overlayfs (а не vfs)
14docker info --format 'Storage Driver: {{.Driver}}'
15# Изход: Storage Driver: overlayfs
16
17# Изпълнете многослоен build и стартирайте вложен контейнер
18mkdir -p /tmp/nested-test && cd /tmp/nested-test
19cat <<'EOF' > Dockerfile
20FROM alpine:3.20
21RUN adduser -D -u 1001 appuser && mkdir -p /app && chown appuser:appuser /app
22USER appuser
23WORKDIR /app
24RUN echo "nested-build-ok" > status.txt
25CMD ["cat", "/app/status.txt"]
26EOF
27
28docker build -t nested-test:latest .
29docker run --rm nested-test:latest
30# Изход: nested-build-ok
31
32kill -INT $DIND_PID
Допълнително четене в тази серия
- gvx: Стартиране на Linux контейнери, Docker и Kubernetes в един непривилегирован процес: Преглед на
gvxв Windows (whpx,systrap), macOS (hvf) и Linux (systrap). - Изпълнение на Docker, docker build и docker compose без root или User Namespaces: Как
gvxизпълняваdocker:dind, когатоroot,sudo,unshare(CLONE_NEWUSER),mount(2)и/dev/fuseса блокирани. - Docker-in-Docker в gVisor и вложени sandbox среди с gvx (тази публикация)
- Бързи изолирани Docker среди за RL rollouts на AI агенти с gvx: Как да използвате
gvxи единичниext4liteмоментни снимки copy-on-write, за да стартирате стотици паралелни среди заSWE-benchи RL rollouts с нулиране за 100 милисекунди.