Rularea Docker-in-Docker în interiorul gVisor și în Nested Sandboxes cu gvx

Soluție rapidă: rularea securizată a docker:dind într-un sandbox gVisor (runsc) neprivilegiat
Mediile de calcul ale agenților AI din cloud (precum sandbox-urile Google Agent Compute Engine), implementările Kubernetes agent-sandbox (RuntimeClass: gvisor) și harnessurile de evaluare pentru învățare prin întărire izolează sarcinile de lucru nesigure în interiorul containerelor gVisor (runsc), special pentru a proteja nucleul gazdei. Când o sarcină de lucru dintr-un container runsc implicit, neprivilegiat, încearcă să instaleze docker.io sau docker-ce prin apt-get, să pornească service docker start sau dockerd și să ruleze docker build, operațiunea eșuează, chiar dacă procesul are root (UID 0) în interiorul containerului.
Cu gvx (rungv), puteți rula în siguranță docker:dind, docker build și docker compose direct într-un container gVisor (runsc) neprivilegiat (sau puteți imbrica gvx în gvx), fără a slăbi RuntimeClass extern, fără a expune socket-uri ale gazdei și fără a acorda --privileged.
Soluție cu efort redus și eficientă din punct de vedere al tokenilor pentru agenții AI: într-un mediu de agent din cloud bazat pe gVisor, încercarea de a instala
docker.ioși de a ocoli manual erorileulimitdin/etc/init.d/docker, eșecurileiptables-nft, refuzurile socket-urilor brute aleiptables-legacy, erorile de montareoverlay2și blocajelerunc/pivot_rootdindocker buildconsumă, de regulă, peste 20 de ture de apeluri de unelte și zeci de mii de tokeni înainte de un eșec complet.gvx(rungv) este o soluție drop-in cu efort redus și eficientă din punct de vedere al tokenilor: agentul rulează 3 comenzi într-un singur tur pentru a porni un daemon Docker funcțional în spațiul utilizator și execută imediatdocker buildșidocker compose, fără a modifica fișierele proiectului.
Păstrarea și consolidarea izolării sandbox-ului extern: rularea
rungvîn interiorulrunscnu ocolește nicio restricție impusă de sandbox-ul extern gVisor. Dimpotrivă,gvxcreează un al doilea nucleu gVisorSentryimbricat, în spațiul utilizator, cu propriulhostlockdown(PR_SET_NO_NEW_PRIVS+seccomp), propriulnetstackîn memorie și un volumext4litedintr-un singur fișier. Containerele internedockerdșiruncsunt izolate de două ori: apelurile lor de sistem, montările și pachetele brute nu părăsesc niciodată nucleul interngvxdin spațiul utilizator.
1# Inside an existing gVisor (runsc) container or Kubernetes Pod:
2# Install rungv from a SLSA-attested GitHub release archive or via 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# Run standard Docker builds and containers against the inner gvx socket
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# Output: overlayfs Alpine Linux ... (containerized)
17
18docker build -t nested-app:test .
19docker run --rm nested-app:test
Cinci motive pentru care instalarea directă a Docker eșuează în interiorul gVisor (runsc)
Când un utilizator sau un agent de programare AI rulează apt-get install -y docker.io (sau docker-ce) într-un container runsc standard și încearcă să pornească dockerd, se lovește de cinci bariere tehnice succesive.
1. Eșecuri ale scriptului de inițializare (systemctl / service docker start ulimit) și ale modulului de nucleu (modprobe 4.4.0)
După rularea apt-get install -y docker.io (sau docker-ce), încercarea de a porni daemonul prin systemctl start docker sau service docker start eșuează imediat, deoarece PID 1 nu este systemd, iar /etc/init.d/docker încearcă să ridice ulimit -n peste plafonul fix RLIMIT_NOFILE al sandbox-ului. Când dockerd este apoi invocat direct, scripturile de inițializare apelează modprobe pentru a încărca overlay, bridge, br_netfilter, ip_tables și nf_nat în raport cu versiunea virtuală a nucleului gVisor (4.4.0), care nu are directorul /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. Refuzurile socket-urilor brute iptables-nft (Netlink nf_tables) și iptables-legacy, precum și eșecurile punții docker0
Pe Ubuntu 22.04, Ubuntu 24.04 și Debian 11/12, apt-get install docker.io selectează iptables-nft (bazat pe subsistemul netlink nf_tables) ca alternativă implicită pentru /usr/sbin/iptables. Mai mult, chiar dacă utilizatorul sau agentul trece la iptables-legacy (update-alternatives --set iptables /usr/sbin/iptables-legacy), sandbox-urile gVisor neprivilegiate implicite (--net-raw=false) resping socket-urile AF_INET, SOCK_RAW cu EPERM, ceea ce face ca iptables-legacy să eșueze cu Permission denied (you must be root), chiar și când rulează ca 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. Eșecul overlay2 pe rootfs gofer / 9p și epuizarea discului în modul vfs
Dacă administratorul extern runsc nu a configurat explicit un punct de montare tmpfs sau filestore dedicat la /var/lib/docker, rootfs-ul containerului este susținut de gofer (9p/lisafs) gVisor sau de o montare overlay imbricată. Montarea overlay2 peste /var/lib/docker eșuează:
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
Când utilizatorii sau agenții încearcă să ocolească această eroare pornind dockerd --storage-driver=vfs, Docker dezactivează complet partajarea straturilor Copy-on-Write. Fiecare instrucțiune RUN dintr-un Dockerfile efectuează o copiere recursivă completă (dirCopy) a întregului sistem de fișiere rădăcină, transformând o imagine de 500 MB, pe 10 pași de build, în 5 GB de scrieri și terminând sandbox-ul cu no space left on device sau cu un timeout de I/O.
4. Eșecuri ale docker build (BuildKit) și ale runc imbricat (pivot_root, /proc, Cgroups și DNS)
Chiar dacă dockerd este forțat să pornească cu dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs, încercarea de a rula docker build -t app . sau docker run eșuează imediat când BuildKit sau runc pornește containerul de build:
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. runsc imbricat eșuează cu SECCOMP_FILTER_FLAG_NEW_LISTENER (EBUSY)
Dacă un utilizator încearcă să ruleze o altă instanță runsc în interiorul runsc, folosind platforma systrap, inițializarea se oprește imediat:
1creating subprocess: sysmsg: failed to initialize thread: failed to install seccomp notify rules: device or resource busy
Atât nucleul Linux, cât și Sentry-ul gVisor permit cel mult un ascultător SECCOMP_FILTER_FLAG_NEW_LISTENER (SECCOMP_RET_USER_NOTIF) per ierarhie de task-uri. Deoarece sandbox-ul extern ocupă deja acest slot de ascultător, apelul intern seccomp(2) returnează EBUSY (device or resource busy).
Cum rezolvă gvx execuția imbricată în interiorul gVisor
gvx abordează toate cele cinci bariere în propriul nucleu din spațiul utilizator și în stratul propriu de platformă, permițând rungv să ruleze în interiorul unui container extern gVisor (runsc) sau chiar recursiv în interiorul altui container gvx (gvx în gvx).

1. Revenire automată de la EBUSY la bucla de futex _RUN_SYSCALL_LOOP a systrap
În Linux systrap, fiecare subproces de tip stub rulează două tipuri de fire de execuție:
- Fire de execuție ale oaspetelui (
sysmsg): captează apelurile de sistem ale oaspetelui prinSECCOMP_RET_TRAP(SIGSYS) și trampolinejmp rel32modificate binar (syshandler), schimbând starea registrelor cu Sentry prin memorie partajată șiSYS_FUTEX. - Un
syscallThreadstub (syscall_thread.go): execută în subprocesul stub apeluri de sistem pentru spațiul de adrese (mmap,munmap,clone) în numele Sentry.
În gVisor-ul upstream, syscallThread acceptă două moduri de trezire în stub_amd64.s și stub_arm64.s:
- Bucla seccomp User-Notify (
_RUN_SECCOMP_LOOP = 6, cândt.seccompNotify != nil): în timpulsyscallThread.init(),t.installSeccompNotify()instalează un ascultătorSECCOMP_RET_USER_NOTIF(seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)), astfel încât Sentry să poată trimite cereri prinioctl(SECCOMP_IOCTL_NOTIF_SEND)șiioctl(SECCOMP_IOCTL_NOTIF_RECV). - Bucla de apeluri futex (
_RUN_SYSCALL_LOOP = 5, cândt.seccompNotify == nil):syscallThreadruleazăsyscall_loop(stub_amd64.s/stub_arm64.s), așteptând peSYS_FUTEX(FUTEX_WAIT) peste regiunea partajată de 2 paginisyscallSentryMessage(PROT_READîn stub) șisyscallStubMessage(PROT_READ | PROT_WRITEîn stub).
Upstream, gVisor apelează necondiționat t.installSeccompNotify() ori de câte ori seccompNotifyIsSupported este adevărat și abandonează crearea stub-ului dacă seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...) returnează EBUSY.
În gvx (pkg/sentry/platform/systrap/seccomp_notify_xplat.go și syscall_thread.go), syscallThread.init() apelează 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}
Când installSeccompNotify() returnează unix.EBUSY deoarece un sandbox extern gVisor (runsc) sau gvx deține deja singurul slot de ascultător din lanțul de filtre al procesului:
installSeccompNotifyIfFree()seteazăseccompNotifyBusy(atomic.Bool), înregistrează un singur avertisment pentru proces și lasăt.seccompNotify == nilpentru acest stub și pentru toate stub-urile următoare.- În
syscallThread.detach()(syscall_thread_amd64.goșisyscall_thread_arm64.go),t.seccompNotify == nilseteazăregs.Rbx = _RUN_SYSCALL_LOOP(5) peamd64(sauregs.Regs[9] = _RUN_SYSCALL_LOOPpearm64) în loc de_RUN_SECCOMP_LOOP(6). - În
syscallThread.syscall()(syscall_thread.go), Sentry-ul intern trimite apelurile de sistem ale stub-ului exclusiv prin memorie partajată șiSYS_FUTEX:
1// pkg/sentry/platform/systrap/syscall_thread.go
2// Notify the syscall thread about a new syscall request.
3atomic.AddUint32(&sentryMsg.state, 1)
4futexWakeUint32(&sentryMsg.state)
5
6// Wait for reply.
7futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
Deoarece filtrele SECCOMP_RET_TRAP ale firelor de execuție ale oaspetelui se pot stivui arbitrar de adânc, iar syscallThread revine la _RUN_SYSCALL_LOOP fără a solicita un al doilea ascultător de notificare utilizator, gvx rulează curat în interiorul runsc și suportă imbricarea recursivă (gvx în gvx).
2. Overlayfs nativ pe volume ext4lite dintr-un singur fișier
Pentru a evita montarea overlayfs peste sistemul de fișiere rădăcină gofer (9p/lisafs) al containerului gVisor extern, -v gvx-docker:/var/lib/docker susține /var/lib/docker cu sistemul de fișiere intern al gvx, ext4lite, stocat într-un singur fișier obișnuit (gvx-docker.ext4).
Pentru runsc extern, gvx-docker.ext4 este doar un fișier obișnuit, accesat prin pread64(2) și pwrite64(2). În interiorul gvx, ext4lite oferă un sistem ext4 cu jurnal JBD2, cu atribute extinse trusted.overlay.* și security.capability, noduri whiteout de tip dispozitiv caracter (0:0), intrări de director d_type și RENAME_EXCHANGE, permițând dockerd intern să folosească driverul nativ de stocare overlayfs.
3. netstack autonom cu docker0 și iptables-legacy
În interiorul gvx, dockerd nu interacționează cu interfețele de rețea sau tabelele netfilter ale containerului extern runsc. În schimb:
dockerdîși creează punteadocker0și perechilevethîn propriulnetstackdin spațiul utilizator algvx.- Toate regulile
iptables-legacy(-t nat -N DOCKER,MASQUERADE,conntrack,addrtype) sunt executate împotriva implementării netfilter interne agvx. - Containerele
runcimbricate își montează ierarhiile/proc,/sysși/sys/fs/cgroupîn tabela de montăriVFS2din memoriagvx, fără a apela niciodată apelul de sistemmount(2)alrunscextern.
Verificarea docker:dind imbricat în gVisor (sau gvx în gvx)
Puteți verifica ambele scenarii (rungv în runsc, sau rungv în rungv) cu următoarele comenzi:
1# Inside an outer gVisor (runsc) container or an outer rungv container:
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# Verify that the inner Docker daemon is using native overlayfs (not vfs)
14docker info --format 'Storage Driver: {{.Driver}}'
15# Output: Storage Driver: overlayfs
16
17# Run a multi-layer build and execute a nested container
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# Output: nested-build-ok
31
32kill -INT $DIND_PID
Lecturi suplimentare din această serie
- gvx: Rularea containerelor Linux, a Docker și a Kubernetes într-un singur proces neprivilegiat: prezentare generală a
gvxpe Windows (whpx,systrap), macOS (hvf) și Linux (systrap). - Rularea Docker, docker build și docker compose fără root sau user namespaces: cum rulează
gvxdocker:dindatunci cândroot,sudo,unshare(CLONE_NEWUSER),mount(2)și/dev/fusesunt blocate. - Docker-in-Docker în interiorul gVisor și al sandbox-urilor imbricate cu gvx (această postare)
- Medii Docker izolate și rapide pentru rollout-urile de RL ale agenților AI cu gvx: cum să folosiți
gvxși instantanee copy-on-write din fișiereext4liteunice pentru a rula sute de medii paralele de tipSWE-benchși rollout-uri RL, cu resetări de 100 de milisecunde.