GoSuda

Kørsel af Docker-in-Docker inde i gVisor og Nested Sandboxes med gvx

By Lemon Mint views ...

Hurtig løsning: Sikker kørsel af docker:dind i en uprivilegeret gVisor (runsc)-sandkasse

Cloud-miljøer til AI-agenter (såsom Google Agent Compute Engine-sandkasser), Kubernetes agent-sandbox-deployments (RuntimeClass: gvisor) og evalueringsharnesser til reinforcement learning isolerer utroværdige workloads i gVisor (runsc)-containere for specifikt at beskytte værtens kerne. Når en workload i en standard uprivilegeret runsc-container forsøger at installere docker.io eller docker-ce via apt-get, starte service docker start eller dockerd og køre docker build, fejler den, selvom den har root (UID 0) inde i containeren.

Med gvx (rungv) kan du sikkert køre docker:dind, docker build og docker compose direkte inde i en uprivilegeret gVisor (runsc)-container (eller indlejre gvx i gvx) uden at svække den ydre RuntimeClass, eksponere værtssockets eller give --privileged.

Lavindsats- og token-effektiv løsning for AI-agenter: Inde i et gVisor-baseret cloud-miljø for agenter bruger forsøg på at installere docker.io og manuelt omgå ulimit-fejl i /etc/init.d/docker, iptables-nft-fejl, iptables-legacy-afvisninger af raw sockets, overlay2-mountfejl og runc- / pivot_root-nedbrud under docker build typisk over 20 tool-kald og titusindvis af tokens før et fuldstændigt nederlag. gvx (rungv) er en lavindsats- og token-effektiv drop-in-løsning: agenten kører 3 kommandoer i én tur for at starte en fungerende Docker-daemon i brugerrum og udfører straks docker build og docker compose uden at ændre projektfiler.

Bevarelse og styrkelse af ydre sandkasseisolation: Kørsel af rungv inde i runsc omgår ikke en eneste begrænsning håndhævet af den ydre gVisor-sandkasse. I stedet opretter gvx en anden, indlejret gVisor Sentry-kerne i brugerrum med sin egen hostlockdown (PR_SET_NO_NEW_PRIVS + seccomp), sit eget in-memory netstack og en enkelt-fil ext4lite-volume. Indre dockerd- og runc-containere er dobbelt sandboxede: deres systemkald, mounts og rå pakker forlader aldrig den indre gvx-kerne i brugerrum.

 1# Inde i en eksisterende gVisor (runsc)-container eller Kubernetes Pod:
 2# Installer rungv fra et SLSA-attesteret GitHub release-arkiv eller 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# Kør standard Docker-builds og containere mod den indre 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 ... (containeriseret)
17
18docker build -t nested-app:test .
19docker run --rm nested-app:test

Fem grunde til, at direkte Docker-installation fejler inde i gVisor (runsc)

Når en bruger eller en AI-kodningsagent kører apt-get install -y docker.io (eller docker-ce) inde i en standard runsc-container og forsøger at starte dockerd, møder den fem på hinanden følgende tekniske barrierer.

1. Fejl i init-script (systemctl / service docker start ulimit) og kernemodul (modprobe 4.4.0)

Efter kørsel af apt-get install -y docker.io (eller docker-ce) fejler forsøget på at starte daemonen via systemctl start docker eller service docker start straks, fordi PID 1 ikke er systemd, og fordi /etc/init.d/docker forsøger at hæve ulimit -n over sandkassens hårde RLIMIT_NOFILE-loft. Når dockerd derefter kaldes direkte, kalder initialiseringsscripts modprobe for at indlæse overlay, bridge, br_netfilter, ip_tables og nf_nat mod gVisors virtuelle kerneversion (4.4.0), som ikke har en mappe /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. iptables-nft (nf_tables Netlink), afvisninger af iptables-legacy raw sockets og fejl i docker0-broen

På Ubuntu 22.04, Ubuntu 24.04 og Debian 11/12 vælger apt-get install docker.io iptables-nft (baseret på nf_tables-netlink-undersystemet) som standardalternativ for /usr/sbin/iptables. Selv hvis brugeren eller agenten skifter til iptables-legacy (update-alternatives --set iptables /usr/sbin/iptables-legacy), afviser standard uprivilegerede gVisor-sandkasser (--net-raw=false) AF_INET, SOCK_RAW-sockets med EPERM, hvilket får iptables-legacy til at fejle med Permission denied (you must be root), selv når den kører som 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. overlay2-fejl på gofer- / 9p-rootfs og vfs-diskudmattelse

Medmindre den ydre runsc-administrator udtrykkeligt har konfigureret en dedikeret tmpfs- eller filestore-mount på /var/lib/docker, er containerens rootfs understøttet af gVisors gofer (9p/lisafs) eller en indlejret overlay-mount. Montering af overlay2 oven på /var/lib/docker fejler:

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

Når brugere eller agenter forsøger at omgå denne fejl ved at starte dockerd --storage-driver=vfs, deaktiverer Docker fuldstændigt deling af Copy-on-Write-lag. Hver RUN-instruktion i en Dockerfile udfører en fuld rekursiv kopi (dirCopy) af hele rodfilsystemet, hvilket multiplicerer et 500 MB image over 10 build-trin til 5 GB skrivninger og afslutter sandkassen med no space left on device eller en I/O-timeout.

4. Fejl i docker build (BuildKit) og indlejret runc (pivot_root, /proc, cgroups og DNS)

Selv hvis dockerd tvinges til at starte med dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs, fejler forsøget på at køre docker build -t app . eller docker run straks, når BuildKit eller runc starter build-containeren:

 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. Indlejret runsc fejler med SECCOMP_FILTER_FLAG_NEW_LISTENER (EBUSY)

Hvis en bruger forsøger at køre en anden runsc-instans inde i runsc med systrap-platformen, afbrydes initialiseringen straks:

1creating subprocess: sysmsg: failed to initialize thread: failed to install seccomp notify rules: device or resource busy

Både Linux-kernen og gVisor Sentry tillader højst én SECCOMP_FILTER_FLAG_NEW_LISTENER (SECCOMP_RET_USER_NOTIF)-listener pr. task-hierarki. Fordi den ydre sandkasse allerede har den listener-plads, returnerer det indre seccomp(2)-kald EBUSY (device or resource busy).


Hvordan gvx løser indlejret kørsel inde i gVisor

gvx adresserer alle fem barrierer inde i sin egen kerne i brugerrum og platformslag, hvilket gør det muligt for rungv at køre inde i en ydre gVisor (runsc)-container eller endda rekursivt inde i en anden gvx-container (gvx i gvx).

Indlejret gvx-kørsel inde i en ydre gVisor

1. Automatisk systrap-fallback fra EBUSY til _RUN_SYSCALL_LOOP-futex-løkken

I Linux systrap kører hver stub-underproces to slags tråde:

  • Gæste-eksekveringstråde (sysmsg): Fanger gæstesystemkald via SECCOMP_RET_TRAP (SIGSYS) og binært patchede jmp rel32-trampoliner (syshandler), og udveksler registertilstand med Sentry over delt hukommelse og SYS_FUTEX.
  • En stub-syscallThread (syscall_thread.go): Udfører adresserumssystemkald (mmap, munmap, clone) inde i stub-underprocessen på vegne af Sentry.

I upstream gVisor understøtter syscallThread to vækningstilstande i stub_amd64.s og stub_arm64.s:

  1. Seccomp User-Notify-løkke (_RUN_SECCOMP_LOOP = 6, når t.seccompNotify != nil): Under syscallThread.init() installerer t.installSeccompNotify() en SECCOMP_RET_USER_NOTIF-listener (seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)), så Sentry kan sende forespørgsler via ioctl(SECCOMP_IOCTL_NOTIF_SEND) og ioctl(SECCOMP_IOCTL_NOTIF_RECV).
  2. Futex-systemkaldsløkke (_RUN_SYSCALL_LOOP = 5, når t.seccompNotify == nil): syscallThread kører syscall_loop (stub_amd64.s / stub_arm64.s) og sover på SYS_FUTEX (FUTEX_WAIT) over den delte 2-sides syscallSentryMessage-region (PROT_READ i stubben) og syscallStubMessage-region (PROT_READ | PROT_WRITE i stubben).

Upstream gVisor kalder ubetinget t.installSeccompNotify(), når seccompNotifyIsSupported er sand, og afbryder oprettelsen af stubben, hvis seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...) returnerer EBUSY.

I gvx (pkg/sentry/platform/systrap/seccomp_notify_xplat.go og syscall_thread.go) kalder 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}

Når installSeccompNotify() returnerer unix.EBUSY, fordi en ydre gVisor (runsc)- eller ydre gvx-sandkasse allerede har processens filterkædes ene listener-plads:

  • Sætter installSeccompNotifyIfFree() seccompNotifyBusy (atomic.Bool), logger én advarsel for processen og lader t.seccompNotify == nil for den stub og alle efterfølgende stubber.
  • I syscallThread.detach() (syscall_thread_amd64.go og syscall_thread_arm64.go) sætter t.seccompNotify == nil regs.Rbx = _RUN_SYSCALL_LOOP (5) på amd64 (eller regs.Regs[9] = _RUN_SYSCALL_LOOP på arm64) i stedet for _RUN_SECCOMP_LOOP (6).
  • I syscallThread.syscall() (syscall_thread.go) sender den indre Sentry stub-systemkald udelukkende via delt hukommelse og SYS_FUTEX:
1// pkg/sentry/platform/systrap/syscall_thread.go
2// Underret syscall-tråden om en ny systemkaldsforespørgsel.
3atomic.AddUint32(&sentryMsg.state, 1)
4futexWakeUint32(&sentryMsg.state)
5
6// Vent på svar.
7futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))

Fordi SECCOMP_RET_TRAP-filtre på gæstetråde stables vilkårligt dybt, og syscallThread falder tilbage til _RUN_SYSCALL_LOOP uden at anmode om en anden brugernotifikations-listener, kører gvx problemfrit inde i runsc og understøtter rekursiv indlejring (gvx i gvx).

2. Indbygget overlayfs på enkelt-fil ext4lite-volumes

For at undgå at montere overlayfs oven på den ydre gVisor-containers gofer (9p/lisafs)-rodfilsystem, understøtter -v gvx-docker:/var/lib/docker /var/lib/docker med gvxs interne ext4lite-filsystem, lagret i en enkelt almindelig fil (gvx-docker.ext4).

For den ydre runsc er gvx-docker.ext4 blot én almindelig fil, tilgået via pread64(2) og pwrite64(2). Inde i gvx leverer ext4lite et JBD2-journaleret ext4-filsystem med trusted.overlay.*- og security.capability-udvidede attributter, whiteout-noder af typen tegnenhed (0:0), d_type-mappeindgange og RENAME_EXCHANGE, hvilket gør det muligt for den indre dockerd at bruge sin oprindelige overlayfs-lagringsdriver.

3. Selvstændigt netstack med docker0 og iptables-legacy

Inde i gvx interagerer dockerd ikke med den ydre runsc-containers netværksgrænseflader eller netfilter-tabeller. I stedet:

  • Opretter dockerd sin docker0-bro og veth-par inde i gvxs eget netstack i brugerrum.
  • Udføres alle iptables-legacy-regler (-t nat -N DOCKER, MASQUERADE, conntrack, addrtype) mod gvxs interne netfilter-implementering.
  • Monterer indlejrede runc-containere deres /proc-, /sys- og /sys/fs/cgroup-hierarkier inde i gvxs in-memory VFS2-mount-tabel, uden nogensinde at kalde det ydre runsc-systemkald mount(2).

Verifikation af indlejret docker:dind inde i gVisor (eller gvx i gvx)

Du kan verificere begge scenarier (rungv inde i runsc, eller rungv inde i rungv) med følgende kommandoer:

 1# Inde i en ydre gVisor (runsc)-container eller en ydre 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# Verificer at den indre Docker-daemon bruger indbygget overlayfs (ikke vfs)
14docker info --format 'Storage Driver: {{.Driver}}'
15# Output: Storage Driver: overlayfs
16
17# Kør et build med flere lag og udfør en indlejret 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

Yderligere læsning i denne serie

  1. gvx: Kørsel af Linux-containere, Docker og Kubernetes i én uprivilegeret proces: Oversigt over gvx på tværs af Windows (whpx, systrap), macOS (hvf) og Linux (systrap).
  2. Kørsel af Docker, docker build og docker compose uden root eller user namespaces: Hvordan gvx kører docker:dind, når root, sudo, unshare(CLONE_NEWUSER), mount(2) og /dev/fuse er blokeret.
  3. Docker-in-Docker inde i gVisor og indlejrede sandkasser med gvx (dette indlæg)
  4. Hurtige isolerede Docker-miljøer til AI-agenters RL-rollouts med gvx: Hvordan man bruger gvx og enkelt-fil ext4lite copy-on-write-snapshots til at køre hundredvis af parallelle SWE-bench- og RL-rollout-miljøer med nulstillinger på 100 millisekunder.