GoSuda

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

By Lemon Mint views ...

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 rungv wewnątrz runsc nie omija ani jednego ograniczenia egzekwowanego przez zewnętrzną piaskownicę gVisor. Zamiast tego gvx tworzy drugie, zagnieżdżone jądro Sentry gVisor działające w przestrzeni użytkownika, z własnym hostlockdown (PR_SET_NO_NEW_PRIVS + seccomp), własnym działającym w pamięci netstack oraz pojedynczym wolumenem ext4lite w postaci pliku. Wewnętrzne kontenery dockerd i runc są podwójnie odizolowane: ich wywołania systemowe, montowania i surowe pakiety nigdy nie opuszczają wewnętrznego jądra gvx dział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).

Zagnieżdżone wykonanie gvx wewnątrz zewnętrznego gVisor

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 trampolin jmp rel32 (syshandler), wymieniając stan rejestrów z Sentry przez pamięć współdzieloną i SYS_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:

  1. Pętla powiadomień użytkownika seccomp (_RUN_SECCOMP_LOOP = 6, gdy t.seccompNotify != nil): podczas syscallThread.init() wywołanie t.installSeccompNotify() instaluje listener SECCOMP_RET_USER_NOTIF (seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)), dzięki czemu Sentry może przesyłać żądania przez ioctl(SECCOMP_IOCTL_NOTIF_SEND) i ioctl(SECCOMP_IOCTL_NOTIF_RECV).
  2. Pętla wywołań systemowych futex (_RUN_SYSCALL_LOOP = 5, gdy t.seccompNotify == nil): syscallThread uruchamia syscall_loop (stub_amd64.s / stub_arm64.s), śpiąc na SYS_FUTEX (FUTEX_WAIT) nad współdzielonym dwustronicowym obszarem syscallSentryMessage (PROT_READ w stub) oraz syscallStubMessage (PROT_READ | PROT_WRITE w 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() ustawia seccompNotifyBusy (atomic.Bool), zapisuje jedno ostrzeżenie dla procesu i pozostawia t.seccompNotify == nil dla tego stub oraz wszystkich kolejnych stubów.
  • W syscallThread.detach() (syscall_thread_amd64.go oraz syscall_thread_arm64.go) warunek t.seccompNotify == nil ustawia regs.Rbx = _RUN_SYSCALL_LOOP (5) na amd64 (lub regs.Regs[9] = _RUN_SYSCALL_LOOP na arm64) 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ą i SYS_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:

  • dockerd tworzy most docker0 oraz pary veth wewnątrz własnego, działającego w przestrzeni użytkownika netstack w gvx.
  • Wszystkie reguły iptables-legacy (-t nat -N DOCKER, MASQUERADE, conntrack, addrtype) są wykonywane względem wewnętrznej implementacji netfilter gvx.
  • Zagnieżdżone kontenery runc montują swoje hierarchie /proc, /sys i /sys/fs/cgroup wewnątrz działającej w pamięci tablicy montowań VFS2 w gvx, nigdy nie wywołując zewnętrznego wywołania systemowego mount(2) w runsc.

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

  1. gvx: Uruchamianie kontenerów Linux, Docker i Kubernetes w jednym nieuprzywilejowanym procesie: Przegląd gvx w systemach Windows (whpx, systrap), macOS (hvf) i Linux (systrap).
  2. Uruchamianie Dockera, docker build i docker compose bez roota i przestrzeni nazw użytkownika: Jak gvx uruchamia docker:dind, gdy root, sudo, unshare(CLONE_NEWUSER), mount(2) i /dev/fuse są zablokowane.
  3. Uruchamianie Docker-in-Docker wewnątrz gVisor i zagnieżdżonych piaskownic z gvx (ten wpis)
  4. Szybkie, izolowane środowiska Docker dla rolloutów RL agentów AI z gvx: Jak używać gvx i jednoplikowych migawek copy-on-write ext4lite do uruchamiania setek równoległych środowisk SWE-bench oraz rolloutów RL z resetami trwającymi 100 milisekund.