Изпълнение на Docker-in-Docker в рамките на gVisor и вложени Sandboxes с gvx

Бързо решение: Сигурно изпълнение на docker:dind в непривилегирован gVisor (runsc) сандбокс
Изчислителните среди за облачни AI агенти (като sandbox средите на Google Agent Compute Engine), Kubernetes внедрявания на agent-sandbox (RuntimeClass: gvisor) и хармонизите за оценка на reinforcement learning изолират недоверени работни натоварвания в gVisor (runsc) контейнери, именно за да защитят ядрото на хоста. Когато работно натоварване в контейнер runsc по подразбиране без привилегии се опита да инсталира docker.io или docker-ce чрез apt-get, да стартира service docker start или dockerd и да изпълни docker build, то се проваля, дори ако има root (UID 0) в контейнера.
С gvx (rungv) можете сигурно да изпълнявате docker:dind, docker build и docker compose директно в непривилегирован контейнер gVisor (runsc) (или да вгнездявате gvx в gvx), без да отслабвате външния RuntimeClass, без да излагате хост сокети и без да предоставяте --privileged.
Решение с ниски усилия и ефективно по токени за AI агенти: В облачна агентна среда, работеща върху
gVisor, опитът за инсталиране наdocker.ioи ръчно заобикаляне на грешкитеulimitв/etc/init.d/docker, отказите наiptables-nft, отказите за raw сокети наiptables-legacy, грешките при монтиране наoverlay2и сривовете наdocker buildвrunc/pivot_rootобикновено изразходва над 20 хода на инструменти и десетки хиляди токени, преди да се провали напълно.gvx(rungv) е решение с ниски усилия и ефективно по токени, което работи веднага: агентът изпълнява 3 команди в един ход, за да стартира работещ потребителски демон на Docker, и незабавно изпълняваdocker buildиdocker compose, без да променя файловете на проекта.
Запазване и укрепване на външната изолация на сандбокса: Изпълнението на
rungvвrunscне заобикаля нито едно ограничение, наложено от външния сандбоксgVisor. Вместо товаgvxсъздава втори, вгнезден потребителскиgVisorSentryкернел със собствен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 ... (containerized)
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. Грешки в init скрипта (systemctl / service docker start ulimit) и в kernel модула (modprobe 4.4.0)
След изпълнение на apt-get install -y docker.io (или docker-ce), опитът за стартиране на демона чрез systemctl start docker или service docker start се проваля незабавно, защото PID 1 не е systemd, а /etc/init.d/docker се опитва да повиши ulimit -n над твърдия таван RLIMIT_NOFILE на сандбокса. Когато dockerd се извиква директно, инициализационните скриптове извикват modprobe, за да заредят overlay, bridge, br_netfilter, ip_tables и nf_nat спрямо виртуалната версия на ядрото на gVisor (4.4.0), която няма директория /lib/modules/4.4.0:
1System has not been booted with systemd as init system (PID 1). Can't operate.
2Failed to connect to bus: Host is down
3/etc/init.d/docker: 62: ulimit: error setting limit (Operation not permitted)
4level=warning msg="Running modprobe bridge br_netfilter failed with message: modprobe: WARNING: Module bridge not found in directory /lib/modules/4.4.0\nmodprobe: WARNING: Module br_netfilter not found in directory /lib/modules/4.4.0\n, error: exit status 1"
5modprobe: FATAL: Module overlay not found in directory /lib/modules/4.4.0
6modprobe: FATAL: Module br_netfilter not found in directory /lib/modules/4.4.0
7modprobe: FATAL: Module ip_tables not found in directory /lib/modules/4.4.0
8modprobe: FATAL: Module nf_nat not found in directory /lib/modules/4.4.0
2. iptables-nft (Netlink на nf_tables), отказите за raw сокети на iptables-legacy и грешките при моста docker0
На Ubuntu 22.04, Ubuntu 24.04 и Debian 11/12, apt-get install docker.io избира iptables-nft (базиран на netlink подсистемата nf_tables) като по подразбиране алтернатива за /usr/sbin/iptables. Освен това, дори ако потребителят или агентът превключи към iptables-legacy (update-alternatives --set iptables /usr/sbin/iptables-legacy), сандбоксовете gVisor без привилегии по подразбиране (--net-raw=false) отхвърлят сокети AF_INET, SOCK_RAW с EPERM, което кара iptables-legacy да се провали с Permission denied (you must be root), дори когато работи като UID 0:
1failed to start daemon: Error initializing network controller: error obtaining controller instance: failed to register "bridge" driver: failed to create NAT chain DOCKER: iptables failed: iptables --wait -t nat -N DOCKER: iptables v1.8.10 (nf_tables): Could not fetch rule set generation id: Invalid argument
2iptables v1.8.10 (legacy): can't initialize iptables table `nat': Permission denied (you must be root)
3Perhaps iptables or your kernel needs to be upgraded.
4iptables v1.8.10 (legacy): can't initialize iptables table 'nat': Table does not exist (do you need to insmod?)
5iptables: No chain/target/match by that name.
6Unable 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.)
7Error initializing network controller: error obtaining controller instance: failed to create bridge "docker0": operation not permitted
3. Грешка на overlay2 при rootfs върху gofer / 9p и изчерпване на диска при 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) на целата коренна файлова система, като умножава образ от 500 MB в 10 стъпки на build до 5 GB записи и прекратява сандбокса с no space left on device или I/O timeout.
4. Грешки при docker build (BuildKit) и вгнездения runc (pivot_root, /proc, cgroups и DNS)
Дори когато dockerd е принуден да стартира с dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs, опитът за изпълнение на docker build -t app . или docker run се проваля незабавно, когато BuildKit или runc стартират контейнера за build:
1ERROR: failed to solve: process "/bin/sh -c ..." did not complete successfully:
2docker: 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:
3error during container init: error jailing process inside rootfs: pivot_root .: invalid argument
4error 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
5cgroups: cgroup mountpoint does not exist: unknown
6runc create failed: systemd not running on this host: cannot use systemd cgroup manager
7error during container init: error mounting "proc" to rootfs at "/proc": mount proc:/proc (via /proc/self/fd/6), flags: 0xe: operation not permitted
8error during container init: error loading seccomp filter into kernel: operation not permitted
9Temporary failure resolving 'deb.debian.org'
10Temporary failure resolving 'archive.ubuntu.com'
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 ядрото, и Sentry на gVisor допускат най-много един слушател SECCOMP_FILTER_FLAG_NEW_LISTENER (SECCOMP_RET_USER_NOTIF) за йерархия от задачи. Тъй като външният сандбокс вече държи този слот за слушател, вътрешното извикване seccomp(2) връща EBUSY (device or resource busy).
Как gvx решава вгнезденото изпълнение вътре в gVisor
gvx преодолява и петте бариери във вътрешния си потребителски кернел и слоя на платформата, като позволява rungv да работи във външен контейнер gVisor (runsc) или дори рекурсивно във друг контейнер gvx (gvx в gvx).

1. Автоматично преминаване от EBUSY към futex цикъла _RUN_SYSCALL_LOOP в systrap
В 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) над споделения 2-страничен регион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) или външен 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// Уведомяване на системната нишка за нова заявка за системно извикване.
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 върху коренната файлова система 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 демон използва нативен 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: Running Linux Containers, Docker, and Kubernetes in One Unprivileged Process: Преглед на
gvxв Windows (whpx,systrap), macOS (hvf) и Linux (systrap). - Running Docker, docker build, and docker compose Without Root or User Namespaces: Как
gvxизпълняваdocker:dind, когатоroot,sudo,unshare(CLONE_NEWUSER),mount(2)и/dev/fuseса блокирани. - Running Docker-in-Docker Inside gVisor and Nested Sandboxes with gvx (тази публикация)
- Fast Isolated Docker Environments for AI Agent RL Rollouts with gvx: Как да използвате
gvxи единичниext4liteснимки с copy-on-write, за да изпълнявате стотици паралелни среди заSWE-benchи RL rollouts със 100-милисекундни нулирания.