Uruchamianie Docker-in-Docker wewnątrz gVisor i zagnieżdżonych piaskownic za pomocą gvx

Szybkie rozwiązanie: bezpieczne uruchamianie docker:dind wewnątrz nieuprzywilejowanej piaskownicy gVisor (runsc)
Chmurowe platformy agentów AI, wdrożenia agent-sandbox w Kubernetes (RuntimeClass: gvisor) oraz środowiska ewaluacyjne reinforcement learning izolują niezaufane obciążenia w kontenerach gVisor (runsc), aby chronić jądro hosta. Gdy obciążenie wewnątrz domyślnego, nieuprzywilejowanego kontenera runsc próbuje zainstalować docker.io lub docker-ce za pomocą apt-get i uruchomić dockerd, operacja kończy się niepowodzeniem, nawet jeśli proces działa jako root (UID 0) wewnątrz kontenera.
Za pomocą gvx (rungv) można bezpiecznie uruchomić docker:dind, docker build i docker compose bezpośrednio wewnątrz nieuprzywilejowanego kontenera gVisor (runsc) (lub zagnieździć gvx w gvx), bez osłabiania zewnętrznego RuntimeClass, udostępniania gniazd hosta ani nadawania --privileged.
Zachowanie i wzmocnienie izolacji zewnętrznej piaskownicy: Uruchomienie
rungvwewnątrzrunscnie omija ani jednego ograniczenia egzekwowanego przez zewnętrzną piaskownicęgVisor. Zamiast tegogvxtworzy drugie, zagnieżdżone jądroSentrygVisor działające w przestrzeni użytkownika, z własnymhostlockdown(PR_SET_NO_NEW_PRIVS+seccomp), własnym działającym w pamięcinetstackoraz pojedynczym wolumenemext4litew postaci pliku. Wewnętrzne kontenerydockerdiruncsą podwójnie odizolowane: 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 za pomocą 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# Uruchamiaj standardowe buildy i kontenery Docker za pomocą 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 ... (containerized)
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 programistyczny 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. Ładowanie modułów jądra (modprobe) względem wirtualnego jądra 4.4.0
Podczas instalacji pakietu i uruchamiania dockerd skrypty inicjalizacyjne wywołują modprobe, aby załadować overlay, br_netfilter, ip_tables, nf_nat i xt_conntrack. Zewnętrzny Sentry gVisor blokuje init_module(2) oraz finit_module(2) i raportuje wirtualną wersję jądra (4.4.0), dla której nie istnieje katalog /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. Błędy iptables-nft (netlink nf_tables) oraz mostu docker0
Na Ubuntu 22.04, Ubuntu 24.04 oraz Debianie 11/12 polecenie apt-get install docker.io wybiera iptables-nft (oparte na podsystemie netlink nf_tables) jako domyślną alternatywę /usr/sbin/iptables. W standardowym kontenerze gVisor, bez uprawnień administracyjnych sieci ani obsługi netlink nf_tables, dockerd ulega awarii podczas inicjalizacji łańcucha NAT DOCKER oraz mostu 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. Awaria overlay2 na rootfs gofer / 9p oraz wyczerpanie miejsca przy vfs
Chyba że zewnętrzny administrator runsc jawnie wstępnie skonfigurował dedykowany montaż tmpfs lub filestore w /var/lib/docker, rootfs kontenera jest oparty na gofer (9p/lisafs) gVisor lub zagnieżdżonym montażu 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ą ominąć 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, co mnoży obraz o rozmiarze 500 MB w 10 krokach budowania do 5 GB zapisów i kończy działanie piaskownicy błędem no space left on device lub przekroczeniem czasu operacji I/O.
4. Odrzucenia montowania cgroup i /proc w zagnieżdżonym runc
Nawet jeśli dockerd zostanie wymuszony do uruchomienia z dockerd --iptables=false --bridge=none --storage-driver=vfs, uruchomienie docker run lub docker build wywołuje runc w celu utworzenia wewnętrznego kontenera. W domyślnym, nieuprzywilejowanym kontenerze gVisor runc kończy się niepowodzeniem przy montowaniu cgroup, ponownym montowaniu /proc lub ładowaniu wewnętrznych filtrów 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. 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 przy użyciu platformy systrap, inicjalizacja przerywa się natychmiast:
1creating subprocess: sysmsg: failed to initialize thread: failed to install seccomp notify rules: device or resource busy
Zarówno jądro Linux, jak i Sentry gVisor dopuszczają co najwyżej jednego listenera SECCOMP_FILTER_FLAG_NEW_LISTENER (SECCOMP_RET_USER_NOTIF) na hierarchię zadań. Ponieważ zewnętrzna piaskownica już zajmuje ten slot listenera, wewnętrzne wywołanie seccomp(2) zwraca EBUSY (device or resource busy).
Jak gvx rozwiązuje zagnieżdżone wykonywanie wewnątrz gVisor
gvx eliminuje wszystkie pięć barier we własnym jądrze działającym w przestrzeni użytkownika oraz w warstwie 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 awaryjny przełącznik systrap z EBUSY na pętlę futex _RUN_SYSCALL_LOOP
W Linux systrap każdy podproces stub uruchamia dwa rodzaje wątków:
- Wątki wykonawcze gościa (
sysmsg): przechwytują wywołania systemowe gościa za pomocąSECCOMP_RET_TRAP(SIGSYS) oraz binarnie łatanych trampolinjmp rel32(syshandler), wymieniając stan rejestrów z Sentry przez pamięć współdzieloną iSYS_FUTEX. - Wątek 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 listenerSECCOMP_RET_USER_NOTIF(seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)), dzięki czemu Sentry może przesyłać żą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), śpiąc naSYS_FUTEX(FUTEX_WAIT) nad współdzielonym dwustronicowym obszaremsyscallSentryMessage(PROT_READw stub) orazsyscallStubMessage(PROT_READ | PROT_WRITEw stub).
Upstreamowy gVisor bezwarunkowo wywołuje t.installSeccompNotify(), gdy seccompNotifyIsSupported ma wartość true, i przerywa tworzenie stub, jeśli seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...) zwróci 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 listenera w łańcuchu filtrów procesu:
installSeccompNotifyIfFree()ustawiaseccompNotifyBusy(atomic.Bool), zapisuje jedno ostrzeżenie dla procesu i pozostawiat.seccompNotify == nildla tego stub oraz wszystkich kolejnych stubów.- W
syscallThread.detach()(syscall_thread_amd64.goorazsyscall_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ętrzny Sentry przesyła wywołania systemowe stub wyłącznie przez pamięć współdzieloną iSYS_FUTEX:
1// pkg/sentry/platform/systrap/syscall_thread.go
2// Powiadom wątek wywołań systemowych 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 na wątkach gościa mogą mieć dowolnie dużą głębokość stosu, a syscallThread przechodzi do _RUN_SYSCALL_LOOP bez żądania drugiego listenera powiadomień użytkownika, gvx działa poprawnie wewnątrz runsc i obsługuje rekurencyjne zagnieżdżanie (gvx w gvx).
2. Natywny overlayfs na wolumenach ext4lite w postaci pojedynczego pliku
Aby uniknąć montowania overlayfs na zewnętrznym rootfs kontenera gVisor opartym na gofer (9p/lisafs), -v gvx-docker:/var/lib/docker opiera /var/lib/docker na wewnętrznym systemie plików ext4lite gvx, przechowywanym 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 uzyskuje się dostęp przez pread64(2) i pwrite64(2). Wewnątrz gvx ext4lite udostępnia system plików ext4 z dziennikiem JBD2, z rozszerzonymi atrybutami trusted.overlay.* i security.capability, węzłami whiteout urządzeń znakowych (0:0), wpisami katalogów d_type oraz RENAME_EXCHANGE, co pozwala wewnętrznemu dockerd korzystać z natywnego sterownika pamięci masowej overlayfs.
3. Samodzielny netstack z docker0 i iptables-legacy
Wewnątrz gvx dockerd nie komunikuje się z interfejsami sieciowymi ani tablicami netfilter zewnętrznego kontenera runsc. Zamiast tego:
dockerdtworzy mostdocker0oraz paryvethwewnątrz własnego, działającego w przestrzeni użytkownikanetstackwgvx.- Wszystkie reguły
iptables-legacy(-t nat -N DOCKER,MASQUERADE,conntrack,addrtype) są wykonywane względem wewnętrznej implementacji netfiltergvx. - Zagnieżdżone kontenery
runcmontują swoje hierarchie/proc,/sysi/sys/fs/cgroupwewnątrz działającej w pamięci tablicy montowańVFS2wgvx, nigdy nie wywołując zewnętrznego wywołania systemowegomount(2)wrunsc.
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ą poniższych 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# Sprawdź, 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) i Linux (systrap). - Uruchamianie Dockera, docker build i docker compose bez roota i przestrzeni nazw użytkownika: Jak
gvxuruchamiadocker:dind, gdyroot,sudo,unshare(CLONE_NEWUSER),mount(2)i/dev/fusesą zablokowane. - Uruchamianie 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-benchoraz rolloutów RL z resetami trwającymi 100 milisekund.