Docker-in-Docker Uitvoeren binnen gVisor en Geneste Sandboxes met gvx

Snelle oplossing: docker:dind veilig uitvoeren binnen een niet-geprivilegieerde gVisor (runsc) sandbox
Cloud-AI-agentplatforms, Kubernetes agent-sandbox-implementaties (RuntimeClass: gvisor) en evaluatieomgevingen voor reinforcement learning isoleren onbetrouwbare workloads binnen gVisor (runsc)-containers, specifiek om de host-kernel te beschermen. Wanneer een workload binnen een standaard niet-geprivilegieerde runsc-container probeert docker.io of docker-ce te installeren via apt-get en dockerd te starten, mislukt dit, ook al heeft het proces root (UID 0) binnen de container.
Met gvx (rungv) kunt u docker:dind, docker build en docker compose veilig rechtstreeks uitvoeren binnen een niet-geprivilegieerde gVisor (runsc)-container (of gvx binnen gvx nesten), zonder de buitenste RuntimeClass te verzwakken, host-sockets bloot te stellen of --privileged toe te kennen.
Behoud en versterking van de isolatie van de buitenste sandbox: Het uitvoeren van
rungvbinnenrunscomzeilt geen enkele beperking die door de buitenstegVisor-sandbox wordt afgedwongen. In plaats daarvan maaktgvxeen tweede, genest user-space gVisor-Sentry-kernel aan, met een eigenhostlockdown(PR_SET_NO_NEW_PRIVS+seccomp), een eigen in-memorynetstacken een enkelbestandsext4lite-volume. Internedockerd- enrunc-containers zijn dubbel gesandboxed: hun systeemaanroepen, mounts en ruwe pakketten verlaten de interne user-space-kernel vangvxnooit.
1# Binnen een bestaande gVisor (runsc)-container of Kubernetes Pod:
2# Installeer rungv uit een SLSA-geattesteerd GitHub-releasearchief of 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# Voer standaard Docker-builds en -containers uit via de interne 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# Uitvoer: overlayfs Alpine Linux ... (gecontaineriseerd)
17
18docker build -t nested-app:test .
19docker run --rm nested-app:test
Vijf redenen waarom directe Docker-installatie mislukt binnen gVisor (runsc)
Wanneer een gebruiker of AI-codeeragent apt-get install -y docker.io (of docker-ce) uitvoert binnen een standaard runsc-container en probeert dockerd te starten, loopt die tegen vijf opeenvolgende technische barrières aan.
1. Laden van kernelmodules (modprobe) tegen de virtuele kernel 4.4.0
Tijdens de installatie van pakketten en het opstarten van dockerd voeren initialisatiescripts modprobe uit om overlay, br_netfilter, ip_tables, nf_nat en xt_conntrack te laden. De buitenste gVisor-Sentry blokkeert init_module(2) en finit_module(2) en rapporteert een virtuele kernelversie (4.4.0) waarvoor geen map /lib/modules/4.4.0 bestaat:
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. Fouten bij iptables-nft (nf_tables Netlink) en de docker0-bridge
Op Ubuntu 22.04, Ubuntu 24.04 en Debian 11/12 kiest apt-get install docker.io iptables-nft (ondersteund door het nf_tables netlink-subsysteem) als standaardalternatief voor /usr/sbin/iptables. In een standaard gVisor-container zonder netwerkbeheerprivileges of nf_tables netlink-ondersteuning loopt dockerd vast bij het initialiseren van de DOCKER NAT-chain en de docker0-bridge:
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. Mislukking van overlay2 op gofer/9p-rootfs en vfs-schijfuitputting
Tenzij de beheerder van de buitenste runsc expliciet een speciale tmpfs- of filestore-mount op /var/lib/docker heeft geconfigureerd, wordt de container-rootfs ondersteund door de gofer (9p/lisafs) van gVisor of door een geneste overlay-mount. Het mounten van overlay2 bovenop /var/lib/docker mislukt:
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
Wanneer gebruikers of agents deze fout proberen te omzeilen door dockerd --storage-driver=vfs te starten, schakelt Docker het delen van Copy-on-Write-lagen volledig uit. Elke RUN-instructie in een Dockerfile voert dan een volledige recursieve kopie (dirCopy) uit van het gehele root-bestandssysteem. Hierdoor loopt een image van 500 MB over 10 buildstappen op tot 5 GB aan schrijfacties, waarna de sandbox eindigt met no space left on device of een I/O-timeout.
4. Afwijzing van geneste runc-cgroup- en /proc-mounts
Zelfs als dockerd wordt gedwongen te starten met dockerd --iptables=false --bridge=none --storage-driver=vfs, roept docker run of docker build runc aan om de interne container te maken. In een standaard niet-geprivilegieerde gVisor-container mislukt runc bij het mounten van cgroups, het opnieuw mounten van /proc of het laden van interne seccomp-filters:
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. Genest runsc mislukt met SECCOMP_FILTER_FLAG_NEW_LISTENER (EBUSY)
Als een gebruiker probeert een andere runsc-instantie binnen runsc te draaien met het systrap-platform, breekt de initialisatie direct af:
1creating subprocess: sysmsg: failed to initialize thread: failed to install seccomp notify rules: device or resource busy
Zowel de Linux-kernel als de gVisor-Sentry staan maximaal één SECCOMP_FILTER_FLAG_NEW_LISTENER (SECCOMP_RET_USER_NOTIF)-listener per taakhiërarchie toe. Omdat de buitenste sandbox die listener-plek al bezet houdt, geeft de interne seccomp(2)-aanroep EBUSY terug (device or resource busy).
Hoe gvx geneste uitvoering binnen gVisor mogelijk maakt
gvx pakt alle vijf barrières aan binnen de eigen user-space-kernel en platformlaag, waardoor rungv kan draaien binnen een buitenste gVisor (runsc)-container of zelfs recursief binnen een andere gvx-container (gvx in gvx).

1. Automatische systrap-terugval van EBUSY naar de _RUN_SYSCALL_LOOP futex-lus
In Linux systrap draait elk stub-subproces twee soorten threads:
- Threads voor gastuitvoering (
sysmsg): Onderscheppen gast-systeemaanroepen viaSECCOMP_RET_TRAP(SIGSYS) en binair gepatchtejmp rel32-trampolines (syshandler), en wisselen registerstatus uit met de Sentry via gedeeld geheugen enSYS_FUTEX. - Een stub-
syscallThread(syscall_thread.go): Voert adresruimte-systeemaanroepen (mmap,munmap,clone) uit binnen het stub-subproces namens de Sentry.
In upstream gVisor ondersteunt syscallThread twee wake-upmodi in stub_amd64.s en stub_arm64.s:
- Seccomp User-Notify-lus (
_RUN_SECCOMP_LOOP = 6, wanneert.seccompNotify != nil): TijdenssyscallThread.init()installeertt.installSeccompNotify()eenSECCOMP_RET_USER_NOTIF-listener (seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)), zodat de Sentry verzoeken kan verzenden viaioctl(SECCOMP_IOCTL_NOTIF_SEND)enioctl(SECCOMP_IOCTL_NOTIF_RECV). - Futex-systeemaanroeplus (
_RUN_SYSCALL_LOOP = 5, wanneert.seccompNotify == nil): DesyscallThreadvoertsyscall_loopuit (stub_amd64.s/stub_arm64.s) en slaapt opSYS_FUTEX(FUTEX_WAIT) over het gedeelde gebied van 2 pagina's metsyscallSentryMessage(PROT_READin de stub) ensyscallStubMessage(PROT_READ | PROT_WRITEin de stub).
Upstream gVisor roept t.installSeccompNotify() onvoorwaardelijk aan wanneer seccompNotifyIsSupported waar is, en breekt het aanmaken van de stub af wanneer seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...) EBUSY teruggeeft.
In gvx (pkg/sentry/platform/systrap/seccomp_notify_xplat.go en syscall_thread.go) roept syscallThread.init() t.installSeccompNotifyIfFree() aan:
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}
Wanneer installSeccompNotify() unix.EBUSY teruggeeft omdat een buitenste gVisor (runsc)- of buitenste gvx-sandbox de enkele listener-plek in de filterketen van het proces al bezet houdt:
installSeccompNotifyIfFree()zetseccompNotifyBusy(atomic.Bool), logt één waarschuwing voor het proces en laatt.seccompNotify == nilvoor die stub en alle volgende stubs.- In
syscallThread.detach()(syscall_thread_amd64.goensyscall_thread_arm64.go) zett.seccompNotify == nilregs.Rbx = _RUN_SYSCALL_LOOP(5) opamd64(ofregs.Regs[9] = _RUN_SYSCALL_LOOPoparm64) in plaats van_RUN_SECCOMP_LOOP(6). - In
syscallThread.syscall()(syscall_thread.go) verstuurt de interne Sentry stub-systeemaanroepen uitsluitend via gedeeld geheugen enSYS_FUTEX:
1// pkg/sentry/platform/systrap/syscall_thread.go
2// Meld de syscall-thread over een nieuw systeemaanroepverzoek.
3atomic.AddUint32(&sentryMsg.state, 1)
4futexWakeUint32(&sentryMsg.state)
5
6// Wacht op antwoord.
7futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
Omdat SECCOMP_RET_TRAP-filters op gasttreads willekeurig diep gestapeld kunnen zijn en syscallThread terugvalt op _RUN_SYSCALL_LOOP zonder een tweede user-notification-listener aan te vragen, draait gvx zonder problemen binnen runsc en ondersteunt het recursief nesten (gvx in gvx).
2. Native overlayfs op enkelbestands ext4lite-volumes
Om te voorkomen dat overlayfs bovenop het gofer (9p/lisafs)-rootbestandssysteem van de buitenste gVisor-container wordt gemount, ondersteunt -v gvx-docker:/var/lib/docker /var/lib/docker met het interne bestandssysteem ext4lite van gvx, opgeslagen in één regulier bestand (gvx-docker.ext4).
Voor de buitenste runsc is gvx-docker.ext4 slechts één regulier bestand dat wordt benaderd via pread64(2) en pwrite64(2). Binnen gvx biedt ext4lite een JBD2-gejournaled ext4-bestandssysteem met trusted.overlay.*- en security.capability-uitgebreide attributen, whiteout-knooppunten voor tekenapparaten (0:0), d_type-directoryvermeldingen en RENAME_EXCHANGE. Hierdoor kan de interne dockerd zijn native overlayfs-opslagstuurprogramma gebruiken.
3. Zelfstandige netstack met docker0 en iptables-legacy
Binnen gvx communiceert dockerd niet met de netwerkinterfaces of netfilter-tabellen van de buitenste runsc-container. In plaats daarvan:
dockerdmaakt zijndocker0-bridge enveth-paren aan binnen de eigen user-spacenetstackvangvx.- Alle
iptables-legacy-regels (-t nat -N DOCKER,MASQUERADE,conntrack,addrtype) worden uitgevoerd tegen de interne netfilter-implementatie vangvx. - Geneste
runc-containers mounten hun/proc-,/sys- en/sys/fs/cgroup-hiërarchieën binnen de in-memoryVFS2-mounttabel vangvx, zonder ooit demount(2)-systeemaanroep van de buitensterunscaan te roepen.
Verifiëren van geneste docker:dind binnen gVisor (of gvx in gvx)
U kunt beide scenario's (rungv binnen runsc of rungv binnen rungv) verifiëren met de volgende commando's:
1# Binnen een buitenste gVisor (runsc)-container of een buitenste 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# Controleer of de interne Docker-daemon native overlayfs gebruikt (en niet vfs)
14docker info --format 'Storage Driver: {{.Driver}}'
15# Uitvoer: Storage Driver: overlayfs
16
17# Voer een build met meerdere lagen uit en start een geneste 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# Uitvoer: nested-build-ok
31
32kill -INT $DIND_PID
Verder lezen in deze reeks
- gvx: Linux-containers, Docker en Kubernetes uitvoeren in één niet-geprivilegieerd proces: Overzicht van
gvxop Windows (whpx,systrap), macOS (hvf) en Linux (systrap). - Docker, docker build en docker compose uitvoeren zonder root of user namespaces: Hoe
gvxdocker:dinduitvoert wanneerroot,sudo,unshare(CLONE_NEWUSER),mount(2)en/dev/fusezijn geblokkeerd. - Docker-in-Docker binnen gVisor en geneste sandboxes uitvoeren met gvx (dit bericht)
- Snelle geïsoleerde Docker-omgevingen voor RL-rollouts van AI-agents met gvx: Hoe u
gvxen enkelbestandsext4lite-copy-on-write-snapshots gebruikt om honderden parallelleSWE-bench- en RL-rolloutomgevingen uit te voeren met resets van 100 milliseconden.