GoSuda

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

By Lemon Mint views ...

Быстрое решение: безопасный запуск docker:dind внутри непривилегированной песочницы gVisor (runsc)

Облачные вычислительные среды ИИ-агентов (например, песочницы Google Agent Compute Engine), развертывания Kubernetes agent-sandbox (RuntimeClass: gvisor) и фреймворки оценки обучения с подкреплением изолируют недоверенные рабочие нагрузки внутри контейнеров 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.

Низкозатратное и экономное по токенам решение для ИИ-агентов: внутри облачной агентской среды на базе gVisor попытка установить docker.io и вручную обойти ошибки /etc/init.d/docker ulimit, сбои iptables-nft, отказы raw-сокетов iptables-legacy, ошибки монтирования overlay2 и сбои runc / pivot_root при docker build обычно расходует более 20 ходов вызовов инструментов и десятки тысяч токенов, прежде чем окончательно завершится неудачей. gvx (rungv) является низкозатратным, экономным по токенам готовым решением: агент выполняет 3 команды за один ход, чтобы запустить работающий пользовательский демон Docker, и сразу выполняет docker build и docker compose без изменения файлов проекта.

Сохранение и усиление изоляции внешней песочницы: запуск rungv внутри runsc не обходит ни одного ограничения, введенного внешней песочницей gVisor. Вместо этого gvx создает второе, вложенное пользовательское ядро gVisor Sentry с собственным hostlockdown (PR_SET_NO_NEW_PRIVS + seccomp), собственным netstack в памяти и одним файловым томом ext4lite. Внутренние контейнеры dockerd и runc защищены двойной песочницей: их системные вызовы, монтирования и сырые пакеты никогда не покидают пользовательское ядро внутреннего gvx.

 1# Внутри существующего контейнера gVisor (runsc) или Kubernetes Pod:
 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. Сбои скрипта инициализации (systemctl / service docker start ulimit) и модуля ядра (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 на корневой файловой системе gofer / 9p и исчерпание диска при использовании vfs

Если администратор внешнего runsc явно не настроил выделенное монтирование tmpfs или filestore в /var/lib/docker, корневая файловая система контейнера опирается на gofer (9p/lisafs) gVisor или на вложенное монтирование 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. Сбои 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 запускает контейнер сборки:

 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).

Вложенное выполнение gvx внутри внешнего gVisor

1. Автоматический откат systrap от EBUSY к futex-циклу _RUN_SYSCALL_LOOP

В Linux systrap каждый подпроцесс-заглушка запускает два вида потоков:

  • Потоки выполнения гостевой системы (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:

  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 в заглушке) и 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 на amd64 присваивается regs.Rbx = _RUN_SYSCALL_LOOP (5) (или 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, не запрашивая второй слушатель user-notification, 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, узлами-маркерами удаления в виде символьных устройств (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# Выполните многослойную сборку и запустите вложенный контейнер
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 и пространств имен пользователей: как gvx запускает docker:dind, когда заблокированы root, sudo, unshare(CLONE_NEWUSER), mount(2) и /dev/fuse.
  3. Запуск Docker-in-Docker внутри gVisor и вложенных песочниц с gvx (этот пост)
  4. Быстрые изолированные среды Docker для RL-развертываний ИИ-агентов с gvx: как использовать gvx и одиночные файловые снимки ext4lite с copy-on-write, чтобы запускать сотни параллельных сред SWE-bench и RL-развертываний со сбросом за 100 миллисекунд.