GoSuda

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

By Lemon Mint views ...

Бързо решение: Сигурно изпълнение на 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 създава втори, вложен потребителски gVisor Sentry kernel със собствен 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).

Вложено изпълнение на gvx във външен gVisor

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:

  1. 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).
  2. Цикъл за 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

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

  1. gvx: Стартиране на Linux контейнери, Docker и Kubernetes в един непривилегирован процес: Преглед на gvx в Windows (whpx, systrap), macOS (hvf) и Linux (systrap).
  2. Изпълнение на Docker, docker build и docker compose без root или User Namespaces: Как gvx изпълнява docker:dind, когато root, sudo, unshare(CLONE_NEWUSER), mount(2) и /dev/fuse са блокирани.
  3. Docker-in-Docker в gVisor и вложени sandbox среди с gvx (тази публикация)
  4. Бързи изолирани Docker среди за RL rollouts на AI агенти с gvx: Как да използвате gvx и единични ext4lite моментни снимки copy-on-write, за да стартирате стотици паралелни среди за SWE-bench и RL rollouts с нулиране за 100 милисекунди.