GoSuda

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

By Lemon Mint views ...

Rychlé řešení: Bezpečné spuštění docker:dind uvnitř neprivilegovaného sandboxu gVisor (runsc)

Cloudová výpočetní prostředí AI agentů (například sandboxy Google Agent Compute Engine), nasazení agent-sandbox v Kubernetes (RuntimeClass: gvisor) a evaluační prostředí pro reinforcement learning izolují nedůvěryhodné úlohy uvnitř kontejnerů gVisor (runsc), konkrétně za účelem ochrany jádra hostitele. Když se úloha uvnitř výchozího neprivilegovaného kontejneru runsc pokusí nainstalovat docker.io nebo docker-ce pomocí apt-get, spustit service docker start nebo dockerd a provést docker build, selže, a to i v případě, že má uvnitř kontejneru root (UID 0).

S gvx (rungv) můžete bezpečně spustit docker:dind, docker build a docker compose přímo uvnitř neprivilegovaného kontejneru gVisor (runsc) (nebo vnořit gvx do gvx) bez oslabení vnějšího RuntimeClass, bez zpřístupnění hostitelských socketů a bez udělení --privileged.

Řešení s nízkou náročností a úsporou tokenů pro AI agenty: Uvnitř cloudového agentního prostředí chráněného gVisor se pokus o instalaci docker.io a ruční obcházení chyb ulimit v /etc/init.d/docker, selhání iptables-nft, odmítnutí raw socketů u iptables-legacy, chyby při mountování overlay2 a pády runc / pivot_root při docker build typicky spotřebuje 20+ tool-call kroků a desítky tisíc tokenů, než zcela selže. gvx (rungv) je řešení s nízkou náročností a úsporou tokenů, které lze nasadit přímo: agent spustí 3 příkazy v jediném kroku, aby nastartoval funkční uživatelský Docker daemon, a okamžitě provede docker build a docker compose bez úprav projektových souborů.

Zachování a posílení izolace vnějšího sandboxu: Spuštění rungv uvnitř runsc neobchází jediné omezení vynucované vnějším sandboxem gVisor. Místo toho gvx vytváří druhé, vnořené uživatelské jádro Sentry gVisor s vlastním hostlockdown (PR_SET_NO_NEW_PRIVS + seccomp), vlastním paměťovým netstack a svazkem ext4lite v podobě jediného souboru. Vnitřní dockerd a kontejnery runc jsou dvojitě izolovány: jejich systémová volání, připojení souborových systémů a surové pakety nikdy neopustí vnitřní uživatelské jádro gvx.

 1# Uvnitř existujícího kontejneru gVisor (runsc) nebo Kubernetes Pod:
 2# Nainstalujte rungv z archivu vydání na GitHubu attestovaného 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# Spusťte standardní Docker buildy a kontejnery proti vnitřnímu socketu 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 ... (containerized)
17
18docker build -t nested-app:test .
19docker run --rm nested-app:test

Pět důvodů, proč přímá instalace Dockeru selhává uvnitř gVisor (runsc)

Když uživatel nebo AI agent pro kódová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. Selhání init skriptu (systemctl / service docker start ulimit) a kernel modulu (modprobe 4.4.0)

Po spuštění apt-get install -y docker.io (nebo docker-ce) selže pokus o spuštění démona přes systemctl start docker nebo service docker start okamžitě, protože PID 1 není systemd a /etc/init.d/docker se pokouší zvýšit ulimit -n nad tvrdý strop RLIMIT_NOFILE sandboxu. Když je pak dockerd vyvolán přímo, inicializační skripty volají modprobe, aby načetly overlay, bridge, br_netfilter, ip_tables a nf_nat proti virtuální verzi jádra gVisor (4.4.0), která nemá adresář /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. Odmítnutí iptables-nft (netlink nf_tables), odmítnutí raw socketů iptables-legacy a selhání mostu docker0

Na Ubuntu 22.04, Ubuntu 24.04 a Debianu 11/12 vybere apt-get install docker.io jako výchozí alternativu /usr/sbin/iptables nástroj iptables-nft (založený na netlink subsystému nf_tables). Navíc i když uživatel nebo agent přepne na iptables-legacy (update-alternatives --set iptables /usr/sbin/iptables-legacy), výchozí neprivilegované sandboxy gVisor (--net-raw=false) odmítnou sockety AF_INET, SOCK_RAW s EPERM, což způsobí selhání iptables-legacy s Permission denied (you must be root) i při běhu 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. Selhání overlay2 na rootfs gofer / 9p a vyčerpání disku v režimu vfs

Pokud vnější správce runsc výslovně předem nenakonfiguroval vyhrazený připojený tmpfs nebo filestore v /var/lib/docker, je rootfs kontejneru podložen gofer gVisoru (9p/lisafs) nebo vnořeným overlay připojením. 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 vypne sdílení vrstev Copy-on-Write. Každá instrukce RUN v Dockerfile provede úplnou rekurzivní kopii (dirCopy) celého kořenového souborového systému, takže obraz o velikosti 500 MB napříč 10 kroky buildu znamená 5 GB zápisů, což ukončí sandbox chybou no space left on device nebo timeoutem I/O.

4. Selhání docker build (BuildKit) a vnořeného runc (pivot_root, /proc, cgroupy a DNS)

I když je dockerd vynuceně spuštěn s dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs, pokus o spuštění docker build -t app . nebo docker run okamžitě selže, když BuildKit nebo runc spouští build kontejner:

 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. 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 drží, 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 rungv běžet uvnitř vnějšího kontejneru gVisor (runsc) nebo dokonce rekurzivně uvnitř dalšího kontejneru gvx (gvx v gvx).

Vnořené spuštění gvx uvnitř vnějšího gVisoru

1. Automatické přepnutí systrap z EBUSY na futex smyčku _RUN_SYSCALL_LOOP

V systrap na Linuxu běží každý podprocess stubu se dvěma typy vláken:

  • Vlákna provádění hosta (sysmsg): zachytávají systémová volání hosta přes SECCOMP_RET_TRAP (SIGSYS) a binárně upravené trampolíny jmp rel32 (syshandler), přičemž si s Sentry předávají stav registrů přes sdílenou paměť a SYS_FUTEX.
  • Vlákno stubu syscallThread (syscall_thread.go): Provádí systémová volání správy adresního prostoru (mmap, munmap, clone) uvnitř podprocesu stubu jménem Sentry.

V upstreamovém gVisoru podporuje syscallThread dva režimy probuzení v stub_amd64.s a stub_arm64.s:

  1. Smyčka seccomp User-Notify (_RUN_SECCOMP_LOOP = 6, když t.seccompNotify != nil): Během syscallThread.init() instaluje t.installSeccompNotify() listener SECCOMP_RET_USER_NOTIF (seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)), aby mohl Sentry odesílat požadavky přes ioctl(SECCOMP_IOCTL_NOTIF_SEND) a ioctl(SECCOMP_IOCTL_NOTIF_RECV).
  2. Smyčka systémových volání futex (_RUN_SYSCALL_LOOP = 5, když t.seccompNotify == nil): syscallThread spouští syscall_loop (stub_amd64.s / stub_arm64.s) a uspává se na SYS_FUTEX (FUTEX_WAIT) nad sdílenou oblastí 2 stránek syscallSentryMessage (PROT_READ ve stubu) a syscallStubMessage (PROT_READ | PROT_WRITE ve stubu).

Upstreamový gVisor bezpodmínečně volá t.installSeccompNotify() vždy, když je seccompNotifyIsSupported pravda, a přerušuje 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() 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}

Když installSeccompNotify() vrátí unix.EBUSY, protože vnější gVisor (runsc) nebo vnější sandbox gvx již drží jediný slot listeneru v řetězu filtrů procesu:

  • installSeccompNotifyIfFree() nastaví seccompNotifyBusy (atomic.Bool), zaloguje jedno varování pro proces a ponechá t.seccompNotify == nil pro tento stub i všechny následující stuby.
  • V syscallThread.detach() (syscall_thread_amd64.go a syscall_thread_arm64.go) nastaví t.seccompNotify == nil hodnotu regs.Rbx = _RUN_SYSCALL_LOOP (5) na amd64 (nebo regs.Regs[9] = _RUN_SYSCALL_LOOP na arm64) namísto _RUN_SECCOMP_LOOP (6).
  • V syscallThread.syscall() (syscall_thread.go) vnitřní Sentry rozesílá systémová volání stubu výhradně přes sdílenou paměť a SYS_FUTEX:
1// pkg/sentry/platform/systrap/syscall_thread.go
2// Upozorněte vlákno systémových volání na nový požadavek na systémové volání.
3atomic.AddUint32(&sentryMsg.state, 1)
4futexWakeUint32(&sentryMsg.state)
5
6// Počkejte na odpověď.
7futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))

Protože filtry SECCOMP_RET_TRAP procházejí zásobník vláken hosta libovolně hluboko a syscallThread se vrací k _RUN_SYSCALL_LOOP, aniž by požádal o druhý listener uživatelských notifikací, gvx běží čistě uvnitř runsc a podporuje rekurzivní vnoření (gvx uvnitř gvx).

2. Nativní overlayfs na svazcích ext4lite v podobě jediného souboru

Aby se předešlo 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 v 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 přistupovaný 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áře d_type a RENAME_EXCHANGE, což umožňuje vnitřnímu dockerd použít jeho nativní ovladač úložiště overlayfs.

3. Samostatný netstack s docker0 a iptables-legacy

Uvnitř gvx neinteraguje dockerd s síťovými rozhraními ani netfilter tabulkami vnějšího kontejneru runsc. Místo toho:

  • dockerd vytváří svůj most docker0 a páry veth uvnitř vlastního uživatelského netstack v gvx.
  • Všechna pravidla iptables-legacy (-t nat -N DOCKER, MASQUERADE, conntrack, addrtype) se provádějí proti interní implementaci netfilteru v gvx.
  • Vnořené kontejnery runc připojují své hierarchie /proc, /sys a /sys/fs/cgroup uvnitř tabulky připojení VFS2 v paměti gvx a nikdy nevolají systémové volání mount(2) vnějšího runsc.

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í Docker daemon používá nativní overlayfs (nikoli vfs)
14docker info --format 'Storage Driver: {{.Driver}}'
15# Výstup: Storage Driver: overlayfs
16
17# Spusťte vícevrstvý build a proveďte 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

  1. gvx: Spouštění linuxových kontejnerů, Dockeru a Kubernetes v jednom neprivilegovaném procesu: Přehled gvx napříč Windows (whpx, systrap), macOS (hvf) a Linuxem (systrap).
  2. Spouštění Dockeru, docker build a docker compose bez rootu nebo uživatelských jmenných prostorů: Jak gvx spouští docker:dind, když jsou blokovány root, sudo, unshare(CLONE_NEWUSER), mount(2) a /dev/fuse.
  3. Docker-in-Docker uvnitř gVisoru a vnořených sandboxů s gvx (tento příspěvek)
  4. Rychlá izolovaná Docker prostředí pro RL rollouty AI agentů s gvx: Jak používat gvx a jednosouborové snímky ext4lite s copy-on-write k provozování stovek paralelních prostředí SWE-bench a RL rollout s resetem za 100 milisekund.