Rularea Docker-in-Docker în interiorul gVisor și al sandbox-urilor imbricate cu gvx

Soluție rapidă: rularea securizată a docker:dind într-un sandbox gVisor (runsc) neprivilegiat
Platformele cloud de agenți AI, implementările Kubernetes agent-sandbox (RuntimeClass: gvisor) și harnessurile de evaluare pentru învățarea prin întărire izolează sarcinile de lucru nesigure în interiorul containerelor gVisor (runsc), tocmai pentru a proteja nucleul gazdei. Când o sarcină de lucru dintr-un container runsc neprivilegiat implicit încearcă să instaleze docker.io sau docker-ce prin apt-get și să pornească dockerd, 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.
Păstrarea și consolidarea izolării sandboxului extern: Rularea
rungvîn interiorulrunscnu ocolește nicio restricție impusă de sandboxul externgVisor. În schimb,gvxcreează un al doilea nucleuSentrygVisor imbricat, în spațiul utilizatorului, cu propriulhostlockdown(PR_SET_NO_NEW_PRIVS+seccomp), propriulnetstackîn memorie și un volumext4litedintr-un singur fișier. Containereledockerdșiruncinterioare sunt izolate în sandbox dublu: apelurile lor de sistem, montările și pachetele brute nu părăsesc niciodată nucleulgvxinterior, care rulează în spațiul utilizatorului.
1# În interiorul unui container gVisor (runsc) existent sau al unui Pod Kubernetes:
2# Instalați rungv dintr-o arhivă de lansare GitHub atestată SLSA sau prin 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# Rulați compilări Docker standard și containere prin socket-ul gvx interior
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# Ieșire: 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, întâlnește cinci bariere tehnice succesive.
1. Încărcarea modulelor nucleului (modprobe) față de nucleul virtual 4.4.0
În timpul instalării pachetului și al pornirii dockerd, scripturile de inițializare rulează modprobe pentru a încărca overlay, br_netfilter, ip_tables, nf_nat și xt_conntrack. Sentry-ul gVisor extern blochează init_module(2) și finit_module(2) și raportează o versiune virtuală a nucleului (4.4.0) pentru care nu există directorul /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. Eșecurile iptables-nft (netlink nf_tables) și ale 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. Într-un container gVisor standard, fără privilegii de administrare a rețelei sau fără suport netlink nf_tables, dockerd se blochează în timpul inițializării lanțului NAT DOCKER și a punții 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. Eșecul overlay2 pe rootfs-ul gofer / 9p și epuizarea discului în modul vfs
Cu excepția cazului în care administratorul extern runsc a configurat explicit un punct de montare tmpfs sau filestore dedicat pentru /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, în zece pași de compilare, în 5 GB de scrieri și determinând terminarea sandboxului cu no space left on device sau cu un timeout de I/O.
4. Respingerea montărilor cgroup și /proc ale runc imbricat
Chiar dacă dockerd este forțat să pornească cu dockerd --iptables=false --bridge=none --storage-driver=vfs, rularea docker run sau docker build invocă runc pentru a crea containerul interior. Într-un container gVisor neprivilegiat implicit, runc eșuează la montarea cgroup-urilor, la remontarea /proc sau la încărcarea filtrelor seccomp interioare:
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. 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 listener SECCOMP_FILTER_FLAG_NEW_LISTENER (SECCOMP_RET_USER_NOTIF) pe ierarhia de taskuri. Deoarece sandboxul extern deține deja slotul pentru listener, apelul interior 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 utilizatorului și în propriul strat de platformă, permițând lui rungv să ruleze în interiorul unui container gVisor (runsc) extern sau chiar recursiv în interiorul unui alt container gvx (gvx în gvx).

1. Revenire automată de la systrap cu EBUSY la bucla futex _RUN_SYSCALL_LOOP
În Linux systrap, fiecare subproces de tip stub rulează două categorii de thread-uri:
- Thread-uri de execuție ale oaspetelui (
sysmsg): captează apelurile de sistem ale oaspetelui prinSECCOMP_RET_TRAP(SIGSYS) și prin trampolinejmp rel32modificate binar (syshandler), schimbând starea registrelor cu Sentry-ul prin memorie partajată șiSYS_FUTEX. - Un
syscallThreadstub (syscall_thread.go): execută în subprocesul stub apelurile de sistem ale spațiului de adrese (mmap,munmap,clone) în numele Sentry-ului.
În gVisor-ul upstream, syscallThread acceptă două moduri de trezire în stub_amd64.s și stub_arm64.s:
- Bucla de notificare utilizator seccomp (
_RUN_SECCOMP_LOOP = 6, cândt.seccompNotify != nil): în timpulsyscallThread.init(),t.installSeccompNotify()instalează un listenerSECCOMP_RET_USER_NOTIF(seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)), astfel încât Sentry-ul să poată trimite cereri prinioctl(SECCOMP_IOCTL_NOTIF_SEND)șiioctl(SECCOMP_IOCTL_NOTIF_RECV). - Bucla de apeluri de sistem 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 pagini formată dinsyscallSentryMessage(PROT_READîn stub) șisyscallStubMessage(PROT_READ | PROT_WRITEîn stub).
Gvisor-ul upstream apelează necondiționat t.installSeccompNotify() ori de câte ori seccompNotifyIsSupported este adevărat și abandonează crearea stubului 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 slotul unic de listener 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 ulterioare.- Î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 interior trimite apelurile de sistem ale stub-ului exclusiv prin memorie partajată șiSYS_FUTEX:
1// pkg/sentry/platform/systrap/syscall_thread.go
2// Notifică thread-ul de apeluri de sistem despre o nouă cerere de apel de sistem.
3atomic.AddUint32(&sentryMsg.state, 1)
4futexWakeUint32(&sentryMsg.state)
5
6// Așteaptă răspunsul.
7futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
Deoarece filtrele SECCOMP_RET_TRAP ale thread-urilor oaspete se pot suprapune arbitrar de adânc, iar syscallThread revine la _RUN_SYSCALL_LOOP fără a solicita un al doilea listener de notificare utilizator, gvx rulează corect î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 ext4lite al gvx, 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 de fișiere ext4 cu jurnal JBD2, atribute extinse trusted.overlay.* și security.capability, noduri de tip whiteout pentru dispozitive caracter (0:0), intrări de director d_type și RENAME_EXCHANGE, permițând dockerd interior 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 cu tabelele netfilter ale containerului runsc extern. În schimb:
dockerdîși creează punteadocker0și perechilevethîn interiorulnetstack-ului propriu, din spațiul utilizatorului, 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 memorie agvx, fără a apela niciodată apelul de sistem externmount(2)alrunsc.
Verificarea docker:dind imbricat în interiorul gVisor (sau gvx în gvx)
Puteți verifica ambele scenarii (rungv în interiorul runsc sau rungv în interiorul rungv) cu următoarele comenzi:
1# În interiorul unui container gVisor (runsc) extern sau al unui container rungv extern:
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# Verificați că daemonul Docker interior folosește overlayfs nativ (nu vfs)
14docker info --format 'Storage Driver: {{.Driver}}'
15# Ieșire: Storage Driver: overlayfs
16
17# Rulați o compilare cu mai multe straturi și executați un container imbricat
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# Ieșire: nested-build-ok
31
32kill -INT $DIND_PID
Lectură suplimentară din această serie
- gvx: Rularea containerelor Linux, Docker și 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: Modul în care
gvxruleazădocker:dindatunci cândroot,sudo,unshare(CLONE_NEWUSER),mount(2)și/dev/fusesunt blocate. - Rularea Docker-in-Docker în interiorul gVisor și al sandboxurilor imbricate cu gvx (această postare)
- Medii Docker izolate și rapide pentru rollouturile RL ale agenților AI cu gvx: Modul de utilizare a
gvxși a snapshot-urilor copy-on-write dintr-un singur fișierext4litepentru a rula sute de medii paraleleSWE-benchși RL rollout, cu resetări de 100 de milisecunde.