Spouštění Docker-in-Docker uvnitř gVisor a Nested Sandboxes pomocí gvx

Rychlé řešení: Bezpečné spuštění docker:dind uvnitř nepriviligovaného sandboxu gVisor (runsc)
Platformy cloudových AI agentů, nasazení Kubernetes agent-sandbox (RuntimeClass: gvisor) a evaluační prostředí pro posilované učení izolují nedůvěryhodné úlohy uvnitř kontejnerů gVisor (runsc), konkrétně za účelem ochrany jádra hostitele. Pokud se úloha uvnitř výchozího nepriviligovaného kontejneru runsc pokusí nainstalovat docker.io nebo docker-ce pomocí apt-get a spustit dockerd, selže, a to i tehdy, když má uvnitř kontejneru oprávnění root (UID 0).
Pomocí gvx (rungv) můžete bezpečně spouštět docker:dind, docker build a docker compose přímo uvnitř nepriviligovaného kontejneru gVisor (runsc) (nebo vnořit gvx do gvx) bez oslabení vnějšího RuntimeClass, zpřístupnění hostitelských socketů nebo udělení --privileged.
Zachování a posílení izolace vnějšího sandboxu: Spuštění
rungvuvnitřrunscneobchází jediné omezení vynucené vnějším sandboxemgVisor. Místo tohogvxvytváří druhé, vnořené uživatelské jádroSentrys vlastnímhostlockdown(PR_SET_NO_NEW_PRIVS+seccomp), vlastním paměťovýmnetstacka svazkemext4litetvořeným jediným souborem. Vnitřní démondockerda kontejneryruncjsou dvojitě izolované: jejich systémová volání, připojení a surové pakety nikdy neopustí vnitřní uživatelské jádrogvx.
1# Uvnitř existujícího kontejneru gVisor (runsc) nebo Kubernetes Pod:
2# Nainstalujte rungv z archivu vydání na GitHubu ověřeného pomocí SLSA nebo 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# Spouštějte standardní sestavení a kontejnery Docker přes vnitřní socket 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# Výstup: overlayfs Alpine Linux ... (v kontejneru)
17
18docker build -t nested-app:test .
19docker run --rm nested-app:test
Pět důvodů, proč přímá instalace Dockeru uvnitř gVisor (runsc) selhává
Když uživatel nebo AI agent pro programování spustí apt-get install -y docker.io (nebo docker-ce) uvnitř standardního kontejneru runsc a pokusí se spustit dockerd, narazí na pět po sobě jdoucích technických překážek.
1. Načítání modulů jádra (modprobe) proti virtuálnímu jádru 4.4.0
Během instalace balíčku a spouštění dockerd inicializační skripty spouštějí modprobe, aby načetly overlay, br_netfilter, ip_tables, nf_nat a xt_conntrack. Vnější Sentry gVisor blokuje init_module(2) a finit_module(2) a hlásí virtuální verzi jádra (4.4.0), pro kterou neexistuje adresář /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. Selhání iptables-nft (netlink nf_tables) a mostu docker0
Na Ubuntu 22.04, Ubuntu 24.04 a Debianu 11/12 instalace apt-get install docker.io vybere iptables-nft (založený na netlink subsystému nf_tables) jako výchozí alternativu /usr/sbin/iptables. Ve standardním kontejneru gVisor bez síťových administrátorských oprávnění nebo podpory netlinku nf_tables dockerd při inicializaci řetězce NAT DOCKER a mostu docker0 havaruje:
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. Selhání overlay2 na rootfs gofer / 9p a vyčerpání disku při použití vfs
Pokud vnější administrátor runsc explicitně nenakonfiguroval vyhrazený připojený tmpfs nebo filestore na /var/lib/docker, je rootfs kontejneru podložen gofer gVisoru (9p/lisafs) nebo vnořeným připojením overlay. Připojení overlay2 nad /var/lib/docker selže:
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
Když se uživatelé nebo agenti pokusí tuto chybu obejít spuštěním dockerd --storage-driver=vfs, Docker zcela zakáže sdílení vrstev Copy-on-Write. Každá instrukce RUN v Dockerfile pak provede úplnou rekurzivní kopii (dirCopy) celého kořenového souborového systému, takže obraz o velikosti 500 MB napříč 10 kroky sestavení vyprodukuje 5 GB zápisů a sandbox skončí chybou no space left on device nebo časovým limitem I/O.
4. Odmítnutí připojení cgroup a /proc ve vnořeném runc
I když je dockerd vynuceně spuštěn s dockerd --iptables=false --bridge=none --storage-driver=vfs, příkaz docker run nebo docker build volá runc, aby vytvořil vnitřní kontejner. Ve výchozím nepriviligovaném kontejneru gVisor runc selže při připojování cgroup, opětovném připojování /proc nebo načítání vnitřních filtrů 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. Vnořený runsc selhává s SECCOMP_FILTER_FLAG_NEW_LISTENER (EBUSY)
Pokud se uživatel pokusí spustit další instanci runsc uvnitř runsc s platformou systrap, inicializace se okamžitě přeruší:
1creating subprocess: sysmsg: failed to initialize thread: failed to install seccomp notify rules: device or resource busy
Jak jádro Linuxu, tak Sentry gVisoru povolují nejvýše jeden listener SECCOMP_FILTER_FLAG_NEW_LISTENER (SECCOMP_RET_USER_NOTIF) na hierarchii úloh. Protože vnější sandbox již tento slot listeneru obsazuje, vnitřní volání seccomp(2) vrací EBUSY (device or resource busy).
Jak gvx řeší vnořené spouštění uvnitř gVisor
gvx řeší všech pět překážek uvnitř vlastního uživatelského jádra a platformní vrstvy, což umožňuje spouštět rungv uvnitř vnějšího kontejneru gVisor (runsc), nebo dokonce rekurzivně uvnitř dalšího kontejneru gvx (gvx v gvx).

1. Automatický přechod z systrap z EBUSY na smyčku futex _RUN_SYSCALL_LOOP
V Linux systrap každý podproces stubu spouští dva typy vláken:
- Vlákna provádění hosta (
sysmsg): Zachycují systémová volání hosta pomocíSECCOMP_RET_TRAP(SIGSYS) a binárně upravených trampolinjmp rel32(syshandler), přičemž si s Sentry vyměňují stav registrů přes sdílenou paměť aSYS_FUTEX. - Stub
syscallThread(syscall_thread.go): Provádí systémová volání pro adresní prostor (mmap,munmap,clone) uvnitř podprocesu stubu jménem Sentry.
V upstream gVisoru podporuje syscallThread dva režimy probuzení v souborech stub_amd64.s a stub_arm64.s:
- Smyčka User-Notify seccomp (
_RUN_SECCOMP_LOOP = 6, kdyžt.seccompNotify != nil): BěhemsyscallThread.init()volát.installSeccompNotify()instalaci listeneruSECCOMP_RET_USER_NOTIF(seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)), aby Sentry mohl odesílat požadavky přesioctl(SECCOMP_IOCTL_NOTIF_SEND)aioctl(SECCOMP_IOCTL_NOTIF_RECV). - Smyčka systémových volání přes futex (
_RUN_SYSCALL_LOOP = 5, kdyžt.seccompNotify == nil):syscallThreadspouštísyscall_loop(stub_amd64.s/stub_arm64.s) a uspává se naSYS_FUTEX(FUTEX_WAIT) nad sdílenou oblastí o dvou stránkách, tvořenousyscallSentryMessage(PROT_READve stubu) asyscallStubMessage(PROT_READ | PROT_WRITEve stubu).
Upstream gVisor bezpodmínečně volá t.installSeccompNotify() vždy, když je seccompNotifyIsSupported pravdivé, a přeruší vytvoření stubu, pokud seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...) vrátí EBUSY.
V gvx (pkg/sentry/platform/systrap/seccomp_notify_xplat.go a syscall_thread.go) volá syscallThread.init() funkci 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}
Pokud installSeccompNotify() vrátí unix.EBUSY, protože vnější sandbox gVisor (runsc) nebo vnější gvx již obsazuje jediný slot listeneru v řetězci filtrů procesu:
installSeccompNotifyIfFree()nastavíseccompNotifyBusy(atomic.Bool), pro proces zaznamená jediné varování a pro tento stub i pro všechny následující stuby ponechát.seccompNotify == nil.- V
syscallThread.detach()(syscall_thread_amd64.goasyscall_thread_arm64.go) nastavít.seccompNotify == nilhodnoturegs.Rbx = _RUN_SYSCALL_LOOP(5) naamd64(neboregs.Regs[9] = _RUN_SYSCALL_LOOPnaarm64) namísto_RUN_SECCOMP_LOOP(6). - V
syscallThread.syscall()(syscall_thread.go) vnitřní Sentry odesílá systémová volání stubu výhradně přes sdílenou paměť aSYS_FUTEX:
1// pkg/sentry/platform/systrap/syscall_thread.go
2// Oznámí vláknu syscall o novém požadavku na systémové volání.
3atomic.AddUint32(&sentryMsg.state, 1)
4futexWakeUint32(&sentryMsg.state)
5
6// Čeká na odpověď.
7futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
Protože filtry SECCOMP_RET_TRAP na vláknech hosta fungují v libovolně hluboké zásobníkové struktuře a syscallThread přechází na _RUN_SYSCALL_LOOP, aniž by požadoval druhý listener pro uživatelské oznámení, gvx běží bez problémů uvnitř runsc a podporuje rekurzivní vnoření (gvx uvnitř gvx).
2. Nativní overlayfs na svazcích ext4lite tvořených jediným souborem
Aby se zabránilo připojení overlayfs nad kořenový souborový systém gofer (9p/lisafs) vnějšího kontejneru gVisor, -v gvx-docker:/var/lib/docker podkládá /var/lib/docker interním souborovým systémem ext4lite od gvx, uloženým v jediném běžném souboru (gvx-docker.ext4).
Pro vnější runsc je gvx-docker.ext4 pouze jeden běžný soubor, ke kterému se přistupuje přes pread64(2) a pwrite64(2). Uvnitř gvx poskytuje ext4lite souborový systém ext4 s žurnálem JBD2, rozšířené atributy trusted.overlay.* a security.capability, whiteout uzly znakových zařízení (0:0), položky adresářů d_type a RENAME_EXCHANGE. To umožňuje vnitřnímu dockerd používat jeho nativní ovladač úložiště overlayfs.
3. Samostatný netstack s docker0 a iptables-legacy
Uvnitř gvx neinteraguje dockerd se síťovými rozhraními ani tabulkami netfilter vnějšího kontejneru runsc. Namísto toho:
dockerdvytváří svůj mostdocker0a dvojicevethuvnitř vlastního uživatelskéhonetstackv rámcigvx.- Všechna pravidla
iptables-legacy(-t nat -N DOCKER,MASQUERADE,conntrack,addrtype) se provádějí proti interní implementaci netfilteru vgvx. - Vnořené kontejnery
runcpřipojují své hierarchie/proc,/sysa/sys/fs/cgroupuvnitř paměťové tabulky připojeníVFS2vgvx, aniž by kdy volaly systémové volánímount(2)vnějšíhorunsc.
Ověření vnořeného docker:dind uvnitř gVisor (nebo gvx v gvx)
Oba scénáře (rungv uvnitř runsc nebo rungv uvnitř rungv) můžete ověřit následujícími příkazy:
1# Uvnitř vnějšího kontejneru gVisor (runsc) nebo vnějšího kontejneru 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# Ověřte, že vnitřní démon Docker používá nativní overlayfs (nikoli vfs)
14docker info --format 'Storage Driver: {{.Driver}}'
15# Výstup: Storage Driver: overlayfs
16
17# Spusťte vícevrstvé sestavení a vnořený kontejner
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# Výstup: nested-build-ok
31
32kill -INT $DIND_PID
Další čtení v této sérii
- gvx: Spouštění Linuxových kontejnerů, Dockeru a Kubernetes v jediném nepriviligovaném procesu: Přehled
gvxnapříč systémy Windows (whpx,systrap), macOS (hvf) a Linux (systrap). - Spouštění Dockeru, docker build a docker compose bez root oprávnění nebo uživatelských jmenných prostorů: Jak
gvxspouštídocker:dind, když jsou blokoványroot,sudo,unshare(CLONE_NEWUSER),mount(2)a/dev/fuse. - Spouštění Docker-in-Docker uvnitř gVisor a vnořených sandboxů pomocí gvx (tento příspěvek)
- Rychlá izolovaná prostředí Dockeru pro rollouty AI agentů při posilovaném učení pomocí gvx: Jak použít
gvxa copy-on-write snapshotyext4litetvořené jediným souborem ke spuštění stovek paralelních prostředíSWE-bencha RL rollout s resetováním za 100 milisekund.