GoSuda

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

By Lemon Mint views ...

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 interiorul runsc nu ocolește nicio restricție impusă de sandboxul extern gVisor. În schimb, gvx creează un al doilea nucleu Sentry gVisor imbricat, în spațiul utilizatorului, cu propriul hostlockdown (PR_SET_NO_NEW_PRIVS + seccomp), propriul netstack în memorie și un volum ext4lite dintr-un singur fișier. Containerele dockerd și runc interioare sunt izolate în sandbox dublu: apelurile lor de sistem, montările și pachetele brute nu părăsesc niciodată nucleul gvx interior, 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).

Execuția gvx imbricată în interiorul gVisor extern

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 prin SECCOMP_RET_TRAP (SIGSYS) și prin trampoline jmp rel32 modificate binar (syshandler), schimbând starea registrelor cu Sentry-ul prin memorie partajată și SYS_FUTEX.
  • Un syscallThread stub (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:

  1. Bucla de notificare utilizator seccomp (_RUN_SECCOMP_LOOP = 6, când t.seccompNotify != nil): în timpul syscallThread.init(), t.installSeccompNotify() instalează un listener SECCOMP_RET_USER_NOTIF (seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)), astfel încât Sentry-ul să poată trimite cereri prin ioctl(SECCOMP_IOCTL_NOTIF_SEND) și ioctl(SECCOMP_IOCTL_NOTIF_RECV).
  2. Bucla de apeluri de sistem futex (_RUN_SYSCALL_LOOP = 5, când t.seccompNotify == nil): syscallThread rulează syscall_loop (stub_amd64.s / stub_arm64.s), așteptând pe SYS_FUTEX (FUTEX_WAIT) peste regiunea partajată de 2 pagini formată din syscallSentryMessage (PROT_READ în stub) și syscallStubMessage (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 == nil pentru acest stub și pentru toate stub-urile ulterioare.
  • În syscallThread.detach() (syscall_thread_amd64.go și syscall_thread_arm64.go), t.seccompNotify == nil setează regs.Rbx = _RUN_SYSCALL_LOOP (5) pe amd64 (sau regs.Regs[9] = _RUN_SYSCALL_LOOP pe arm64) î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ă și SYS_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ă puntea docker0 și perechile veth în interiorul netstack-ului propriu, din spațiul utilizatorului, al gvx.
  • Toate regulile iptables-legacy (-t nat -N DOCKER, MASQUERADE, conntrack, addrtype) sunt executate împotriva implementării netfilter interne a gvx.
  • Containerele runc imbricate își montează ierarhiile /proc, /sys și /sys/fs/cgroup în tabela de montări VFS2 din memorie a gvx, fără a apela niciodată apelul de sistem extern mount(2) al runsc.

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

  1. gvx: Rularea containerelor Linux, Docker și Kubernetes într-un singur proces neprivilegiat: Prezentare generală a gvx pe Windows (whpx, systrap), macOS (hvf) și Linux (systrap).
  2. Rularea Docker, docker build și docker compose fără root sau user namespaces: Modul în care gvx rulează docker:dind atunci când root, sudo, unshare(CLONE_NEWUSER), mount(2) și /dev/fuse sunt blocate.
  3. Rularea Docker-in-Docker în interiorul gVisor și al sandboxurilor imbricate cu gvx (această postare)
  4. 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șier ext4lite pentru a rula sute de medii paralele SWE-bench și RL rollout, cu resetări de 100 de milisecunde.