Uruchamianie Docker-in-Docker wewnątrz gVisor i zagnieżdżonych Sandboxes z użyciem gvx

Szybkie rozwiązanie: bezpieczne uruchamianie docker:dind w nieuprzywilejowanej piaskownicy gVisor (runsc)
Środowiska obliczeniowe agentów AI w chmurze (takie jak piaskownice Google Agent Compute Engine), wdrożenia agent-sandbox w Kubernetes (RuntimeClass: gvisor) oraz uprzęże ewaluacyjne uczenia ze wzmocnieniem izolują niezaufane obciążenia wewnątrz kontenerów gVisor (runsc), aby chronić jądro hosta. Gdy obciążenie działające w domyślnym, nieuprzywilejowanym kontenerze runsc próbuje zainstalować docker.io lub docker-ce za pomocą apt-get, uruchomić service docker start lub dockerd oraz wykonać docker build, kończy się to niepowodzeniem, nawet gdy ma root (UID 0) wewnątrz kontenera.
Dzięki gvx (rungv) można bezpiecznie uruchamiać docker:dind, docker build i docker compose bezpośrednio w nieuprzywilejowanym kontenerze gVisor (runsc) (lub zagnieżdżać gvx wewnątrz gvx) bez osłabiania zewnętrznego RuntimeClass, udostępniania gniazd hosta ani nadawania --privileged.
Rozwiązanie niskonakładowe i efektywne tokenowo dla agentów AI: w środowisku chmurowym agenta opartym na gVisor próba instalacji
docker.ioi ręcznego obchodzenia błędówulimitw/etc/init.d/docker, awariiiptables-nft, odmów dostępu do surowych gniazdiptables-legacy, błędów montowaniaoverlay2oraz awariirunc/pivot_rootpodczasdocker buildzazwyczaj zużywa ponad 20 tur wywołań narzędzi i dziesiątki tysięcy tokenów, zanim całkowicie zawiedzie.gvx(rungv) to niskonakładowe, efektywne tokenowo rozwiązanie typu drop-in: agent wykonuje 3 polecenia w jednej turze, aby uruchomić działający demon Docker w przestrzeni użytkownika, a następnie natychmiast wykonujedocker buildidocker composebez modyfikowania plików projektu.
Zachowanie i wzmacnianie izolacji zewnętrznej piaskownicy: uruchomienie
rungvwewnątrzrunscnie omija żadnego ograniczenia wymuszanego przez zewnętrzną piaskownicęgVisor. Zamiast tegogvxtworzy drugie, zagnieżdżone jądrogVisor Sentrydziałające w przestrzeni użytkownika, z własnymhostlockdown(PR_SET_NO_NEW_PRIVS+seccomp), własnym pamięciowymnetstackoraz jednoplikowym wolumenemext4lite. Wewnętrzne kontenerydockerdiruncsą podwójnie izolowane: ich wywołania systemowe, montowania i surowe pakiety nigdy nie opuszczają wewnętrznego jądragvxdziałającego w przestrzeni użytkownika.
1# Wewnątrz istniejącego kontenera gVisor (runsc) lub Poda Kubernetes:
2# Zainstaluj rungv z archiwum wydania GitHub z atestacją SLSA lub przez 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# Uruchom standardowe buildy i kontenery Docker względem wewnętrznego gniazda 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# Wynik: overlayfs Alpine Linux ... (w kontenerze)
17
18docker build -t nested-app:test .
19docker run --rm nested-app:test
Pięć powodów, dla których bezpośrednia instalacja Dockera kończy się niepowodzeniem wewnątrz gVisor (runsc)
Gdy użytkownik lub agent kodujący AI uruchamia apt-get install -y docker.io (lub docker-ce) w standardowym kontenerze runsc i próbuje uruchomić dockerd, napotyka pięć kolejnych barier technicznych.
1. Skrypt inicjalizacyjny (systemctl / service docker start ulimit) oraz błędy modułów jądra (modprobe 4.4.0)
Po wykonaniu apt-get install -y docker.io (lub docker-ce) próba uruchomienia demona przez systemctl start docker lub service docker start kończy się natychmiast niepowodzeniem, ponieważ PID 1 nie jest systemd, a /etc/init.d/docker próbuje podnieść ulimit -n powyżej twardego limitu RLIMIT_NOFILE piaskownicy. Gdy dockerd jest następnie wywoływany bezpośrednio, skrypty inicjalizacyjne wywołują modprobe, aby załadować overlay, bridge, br_netfilter, ip_tables i nf_nat względem wirtualnej wersji jądra gVisor (4.4.0), która nie ma katalogu /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), odmowy surowych gniazd iptables-legacy oraz awarie mostu docker0
W Ubuntu 22.04, Ubuntu 24.04 oraz Debian 11/12 polecenie apt-get install docker.io wybiera iptables-nft (oparte na podsystemie netlink nf_tables) jako domyślną alternatywę /usr/sbin/iptables. Co więcej, nawet jeśli użytkownik lub agent przełączy się na iptables-legacy (update-alternatives --set iptables /usr/sbin/iptables-legacy), domyślne nieuprzywilejowane piaskownice gVisor (--net-raw=false) odrzucają gniazda AF_INET, SOCK_RAW z błędem EPERM, powodując, że iptables-legacy kończy się niepowodzeniem z komunikatem Permission denied (you must be root), nawet gdy działa jako 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. Awaria overlay2 na rootfs gofer / 9p oraz wyczerpanie miejsca przy vfs
O ile administrator zewnętrznego runsc nie skonfigurował wcześniej jawnie dedykowanego montowania tmpfs lub filestore pod /var/lib/docker, rootfs kontenera opiera się na gofer (9p/lisafs) gVisor lub na zagnieżdżonym montowaniu overlay. Montowanie overlay2 na /var/lib/docker kończy się niepowodzeniem:
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
Gdy użytkownicy lub agenci próbują obejść ten błąd, uruchamiając dockerd --storage-driver=vfs, Docker całkowicie wyłącza współdzielenie warstw Copy-on-Write. Każda instrukcja RUN w Dockerfile wykonuje pełne rekurencyjne kopiowanie (dirCopy) całego systemu plików root, mnożąc obraz o rozmiarze 500 MB w 10 krokach budowania w 5 GB zapisów i kończąc działanie piaskownicy błędem no space left on device lub przekroczeniem czasu operacji we/wy.
4. Awarie docker build (BuildKit) i zagnieżdżonego runc (pivot_root, /proc, cgroups i DNS)
Nawet jeśli dockerd zostanie wymuszony do uruchomienia z dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs, próba wykonania docker build -t app . lub docker run kończy się natychmiast niepowodzeniem, gdy BuildKit lub runc uruchamia kontener budowania:
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. Zagnieżdżony runsc kończy się błędem SECCOMP_FILTER_FLAG_NEW_LISTENER (EBUSY)
Jeśli użytkownik próbuje uruchomić kolejną instancję runsc wewnątrz runsc, używając platformy systrap, inicjalizacja natychmiast się przerywa:
1creating subprocess: sysmsg: failed to initialize thread: failed to install seccomp notify rules: device or resource busy
Zarówno jądro Linuksa, jak i gVisor Sentry dopuszczają co najwyżej jednego słuchacza SECCOMP_FILTER_FLAG_NEW_LISTENER (SECCOMP_RET_USER_NOTIF) w hierarchii zadań. Ponieważ zewnętrzna piaskownica już zajmuje ten slot słuchacza, wewnętrzne wywołanie seccomp(2) zwraca EBUSY (device or resource busy).
Jak gvx rozwiązuje zagnieżdżone wykonywanie wewnątrz gVisor
gvx usuwa wszystkie pięć barier wewnątrz własnego jądra w przestrzeni użytkownika oraz warstwy platformy, umożliwiając rungv działanie wewnątrz zewnętrznego kontenera gVisor (runsc), a nawet rekurencyjnie wewnątrz innego kontenera gvx (gvx w gvx).

1. Automatyczny fallback systrap z EBUSY do pętli futex _RUN_SYSCALL_LOOP
W systrap w Linuksie każdy podproces stub uruchamia dwa rodzaje wątków:
- Wątki wykonania gościa (
sysmsg): przechwytują wywołania systemowe gościa przezSECCOMP_RET_TRAP(SIGSYS) oraz binarnie łatane trampolinyjmp rel32(syshandler), wymieniając stan rejestrów z Sentry przez pamięć współdzieloną iSYS_FUTEX. - Stub
syscallThread(syscall_thread.go): wykonuje wywołania systemowe przestrzeni adresowej (mmap,munmap,clone) wewnątrz podprocesu stub w imieniu Sentry.
W upstreamowym gVisor syscallThread obsługuje dwa tryby wybudzania w stub_amd64.s i stub_arm64.s:
- Pętla powiadomień użytkownika seccomp (
_RUN_SECCOMP_LOOP = 6, gdyt.seccompNotify != nil): podczassyscallThread.init()wywołaniet.installSeccompNotify()instaluje słuchaczaSECCOMP_RET_USER_NOTIF(seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)), aby Sentry mógł przekazywać żądania przezioctl(SECCOMP_IOCTL_NOTIF_SEND)iioctl(SECCOMP_IOCTL_NOTIF_RECV). - Pętla wywołań systemowych futex (
_RUN_SYSCALL_LOOP = 5, gdyt.seccompNotify == nil):syscallThreaduruchamiasyscall_loop(stub_amd64.s/stub_arm64.s), usypiając się naSYS_FUTEX(FUTEX_WAIT) nad wspólnym, dwustronicowym regionemsyscallSentryMessage(PROT_READw stubie) isyscallStubMessage(PROT_READ | PROT_WRITEw stubie).
Upstreamowy gVisor bezwarunkowo wywołuje t.installSeccompNotify() zawsze, gdy seccompNotifyIsSupported ma wartość true, i przerywa tworzenie stuba, jeśli seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...) zwraca EBUSY.
W gvx (pkg/sentry/platform/systrap/seccomp_notify_xplat.go oraz syscall_thread.go) syscallThread.init() wywołuje 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}
Gdy installSeccompNotify() zwraca unix.EBUSY, ponieważ zewnętrzna piaskownica gVisor (runsc) lub zewnętrzne gvx już zajmuje jedyny slot słuchacza w łańcuchu filtrów procesu:
installSeccompNotifyIfFree()ustawiaseccompNotifyBusy(atomic.Bool), zapisuje jedno ostrzeżenie dla procesu i pozostawiat.seccompNotify == nildla tego stuba oraz wszystkich kolejnych stubów.- W
syscallThread.detach()(syscall_thread_amd64.goisyscall_thread_arm64.go) warunekt.seccompNotify == nilustawiaregs.Rbx = _RUN_SYSCALL_LOOP(5) naamd64(lubregs.Regs[9] = _RUN_SYSCALL_LOOPnaarm64) zamiast_RUN_SECCOMP_LOOP(6). - W
syscallThread.syscall()(syscall_thread.go) wewnętrzne Sentry wysyła wywołania stuba wyłącznie przez pamięć współdzieloną iSYS_FUTEX:
1// pkg/sentry/platform/systrap/syscall_thread.go
2// Powiadom wątek syscall o nowym żądaniu wywołania systemowego.
3atomic.AddUint32(&sentryMsg.state, 1)
4futexWakeUint32(&sentryMsg.state)
5
6// Czekaj na odpowiedź.
7futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
Ponieważ filtry SECCOMP_RET_TRAP działają na wątkach gościa o dowolnie dużej głębokości stosu, a syscallThread przełącza się na _RUN_SYSCALL_LOOP bez żądania drugiego słuchacza powiadomień użytkownika, gvx działa poprawnie wewnątrz runsc i obsługuje rekurencyjne zagnieżdżanie (gvx wewnątrz gvx).
2. Natywny overlayfs na jednoplikowych wolumenach ext4lite
Aby uniknąć montowania overlayfs na korzeniowym systemie plików kontenera zewnętrznego gVisor (gofer, 9p/lisafs), -v gvx-docker:/var/lib/docker udostępnia /var/lib/docker z wewnętrznego systemu plików ext4lite gvx, przechowywanego w pojedynczym zwykłym pliku (gvx-docker.ext4).
Dla zewnętrznego runsc plik gvx-docker.ext4 jest po prostu jednym zwykłym plikiem, do którego dostęp odbywa się przez pread64(2) i pwrite64(2). Wewnątrz gvx ext4lite zapewnia system plików ext4 z dziennikiem JBD2, atrybuty rozszerzone trusted.overlay.* i security.capability, węzły whiteout w postaci urządzeń znakowych (0:0), wpisy katalogów d_type oraz RENAME_EXCHANGE, co pozwala wewnętrznemu dockerd używać natywnego sterownika pamięci masowej overlayfs.
3. Samodzielny netstack z docker0 i iptables-legacy
Wewnątrz gvx dockerd nie współdziała z interfejsami sieciowymi ani tabelami netfilter zewnętrznego kontenera runsc. Zamiast tego:
dockerdtworzy swój mostdocker0oraz paryvethwewnątrz własnegonetstackgvxdziałającego w przestrzeni użytkownika.- Wszystkie reguły
iptables-legacy(-t nat -N DOCKER,MASQUERADE,conntrack,addrtype) są wykonywane względem wewnętrznej implementacji netfiltergvx. - Wewnętrzne kontenery
runcmontują swoje hierarchie/proc,/sysi/sys/fs/cgroupwewnątrz tabeli montowańVFS2gvxdziałającej w pamięci, nigdy nie wywołując zewnętrznego wywołania systemowegomount(2)zrunsc.
Weryfikacja zagnieżdżonego docker:dind wewnątrz gVisor (lub gvx w gvx)
Oba scenariusze (rungv wewnątrz runsc lub rungv wewnątrz rungv) można zweryfikować za pomocą następujących poleceń:
1# Wewnątrz zewnętrznego kontenera gVisor (runsc) lub zewnętrznego kontenera 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# Zweryfikuj, czy wewnętrzny demon Docker używa natywnego overlayfs (a nie vfs)
14docker info --format 'Storage Driver: {{.Driver}}'
15# Wynik: Storage Driver: overlayfs
16
17# Uruchom wielowarstwowy build i wykonaj zagnieżdżony kontener
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# Wynik: nested-build-ok
31
32kill -INT $DIND_PID
Dalsza lektura w tej serii
- gvx: uruchamianie kontenerów Linux, Docker i Kubernetes w jednym nieuprzywilejowanym procesie: przegląd
gvxw systemach Windows (whpx,systrap), macOS (hvf) oraz Linux (systrap). - Uruchamianie Dockera, docker build i docker compose bez roota ani przestrzeni nazw użytkownika: jak
gvxuruchamiadocker:dind, gdyroot,sudo,unshare(CLONE_NEWUSER),mount(2)i/dev/fusesą zablokowane. - Docker-in-Docker wewnątrz gVisor i zagnieżdżonych piaskownic z gvx (ten wpis)
- Szybkie izolowane środowiska Docker dla rolloutów RL agentów AI z gvx: jak używać
gvxi jednoplikowych migawek copy-on-writeext4litedo uruchamiania setek równoległych środowiskSWE-benchi rolloutów RL z resetami trwającymi 100 milisekund.