Запуск Docker-in-Docker внутри gVisor и вложенных песочниц с помощью gvx

Быстрое решение: безопасный запуск docker:dind внутри непривилегированной песочницы gVisor (runsc)
Облачные платформы AI-агентов, развертывания Kubernetes agent-sandbox (RuntimeClass: gvisor) и среды оценки для обучения с подкреплением изолируют недоверенные рабочие нагрузки внутри контейнеров 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.
Сохранение и усиление изоляции внешней песочницы: запуск
rungvвнутриrunscне обходит ни одного ограничения, применяемого внешней песочницейgVisor. Напротив,gvxсоздает вторую, вложенную пользовательскую ядро-песочницуgVisorSentryсо своим собственнымhostlockdown(PR_SET_NO_NEW_PRIVS+seccomp), собственным встроеннымnetstackв памяти и томомext4liteв виде одного файла. Внутренниеdockerdи контейнерыruncнаходятся под двойной песочницей: их системные вызовы, монтирования и сырые пакеты никогда не покидают пользовательское ядро внутреннегоgvx.
1# Внутри существующего контейнера gVisor (runsc) или пода Kubernetes:
2# Установите rungv из архива релиза GitHub с аттестацией 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 через внутренний сокет 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)
Когда пользователь или ИИ-агент для написания кода выполняет apt-get install -y docker.io (или docker-ce) внутри стандартного контейнера runsc и пытается запустить dockerd, он сталкивается с пятью последовательными техническими барьерами.
1. Загрузка модулей ядра (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 (Netlink nf_tables) и моста docker0
В Ubuntu 22.04, Ubuntu 24.04 и Debian 11/12 команда apt-get install docker.io выбирает iptables-nft (основанный на подсистеме netlink nf_tables) в качестве альтернативы по умолчанию /usr/sbin/iptables. В стандартном контейнере gVisor без административных сетевых привилегий или поддержки netlink nf_tables 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 на rootfs gofer / 9p и исчерпание диска при vfs
Если внешний администратор runsc явно не настроил выделенное монтирование tmpfs или filestore в /var/lib/docker, корневая файловая система контейнера опирается на 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 МБ на 10 шагах сборки в 5 ГБ записей и завершая работу песочницы с ошибкой no space left on device или тайм-аутом ввода-вывода.
4. Отказы вложенных монтирований cgroup и /proc в runc
Даже если dockerd принудительно запущен командой dockerd --iptables=false --bridge=none --storage-driver=vfs, выполнение docker run или docker build вызывает runc для создания внутреннего контейнера. В контейнере gVisor без дополнительных прав runc завершается ошибкой при монтировании cgroup, повторном монтировании /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, и 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. Автоматический переход systrap с EBUSY на цикл futex _RUN_SYSCALL_LOOP
В Linux systrap каждый подпроцесс-заглушка (stub) запускает два типа потоков:
- Потоки выполнения гостевых процессов (
sysmsg): перехватывают системные вызовы гостя черезSECCOMP_RET_TRAP(SIGSYS) и бинарно пропатченные трамплиныjmp rel32(syshandler), обмениваясь состоянием регистров с Sentry через разделяемую память иSYS_FUTEX. - Поток заглушки
syscallThread(syscall_thread.go): выполняет системные вызовы адресного пространства (mmap,munmap,clone) внутри подпроцесса-заглушки от имени 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в заглушке) иsyscallStubMessage(PROT_READ | PROT_WRITEв заглушке).
Upstream gVisor безусловно вызывает t.installSeccompNotify() всякий раз, когда seccompNotifyIsSupported равно true, и прерывает создание заглушки, если 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для этой заглушки и всех последующих.- В
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 отправляет системные вызовы заглушки исключительно через разделяемую память и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внутри собственного пользовательскогоnetstackgvx.- Все правила
iptables-legacy(-t nat -N DOCKER,MASQUERADE,conntrack,addrtype) выполняются во внутренней реализации netfiltergvx. - Вложенные контейнеры
runcмонтируют свои иерархии/proc,/sysи/sys/fs/cgroupвнутри таблицы монтированияVFS2gvxв памяти, никогда не вызывая системный вызов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# Выполнить многослойную сборку и запустить вложенный контейнер
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 и пользовательских пространств имен: как
gvxзапускаетdocker:dind, когда заблокированыroot,sudo,unshare(CLONE_NEWUSER),mount(2)и/dev/fuse. - Запуск Docker-in-Docker внутри gVisor и вложенных песочниц с gvx (этот пост)
- Быстрые изолированные среды Docker для RL-развертываний AI-агентов с gvx: как использовать
gvxи снимки copy-on-write на основе одного файлаext4liteдля запуска сотен параллельных средSWE-benchи RL-развертываний со сбросом за 100 миллисекунд.