GoSuda

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

By Lemon Mint views ...

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 rungv binnen runsc omzeilt geen enkele beperking die door de buitenste gVisor-sandbox wordt afgedwongen. In plaats daarvan maakt gvx een tweede, genest user-space gVisor-Sentry-kernel aan, met een eigen hostlockdown (PR_SET_NO_NEW_PRIVS + seccomp), een eigen in-memory netstack en een enkelbestands ext4lite-volume. Interne dockerd- en runc-containers zijn dubbel gesandboxed: hun systeemaanroepen, mounts en ruwe pakketten verlaten de interne user-space-kernel van gvx nooit.

 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).

Genest gvx-gebruik binnen een buitenste gVisor

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 via SECCOMP_RET_TRAP (SIGSYS) en binair gepatchte jmp rel32-trampolines (syshandler), en wisselen registerstatus uit met de Sentry via gedeeld geheugen en SYS_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:

  1. Seccomp User-Notify-lus (_RUN_SECCOMP_LOOP = 6, wanneer t.seccompNotify != nil): Tijdens syscallThread.init() installeert t.installSeccompNotify() een SECCOMP_RET_USER_NOTIF-listener (seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)), zodat de Sentry verzoeken kan verzenden via ioctl(SECCOMP_IOCTL_NOTIF_SEND) en ioctl(SECCOMP_IOCTL_NOTIF_RECV).
  2. Futex-systeemaanroeplus (_RUN_SYSCALL_LOOP = 5, wanneer t.seccompNotify == nil): De syscallThread voert syscall_loop uit (stub_amd64.s / stub_arm64.s) en slaapt op SYS_FUTEX (FUTEX_WAIT) over het gedeelde gebied van 2 pagina's met syscallSentryMessage (PROT_READ in de stub) en syscallStubMessage (PROT_READ | PROT_WRITE in 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() zet seccompNotifyBusy (atomic.Bool), logt één waarschuwing voor het proces en laat t.seccompNotify == nil voor die stub en alle volgende stubs.
  • In syscallThread.detach() (syscall_thread_amd64.go en syscall_thread_arm64.go) zet t.seccompNotify == nil regs.Rbx = _RUN_SYSCALL_LOOP (5) op amd64 (of regs.Regs[9] = _RUN_SYSCALL_LOOP op arm64) in plaats van _RUN_SECCOMP_LOOP (6).
  • In syscallThread.syscall() (syscall_thread.go) verstuurt de interne Sentry stub-systeemaanroepen uitsluitend via gedeeld geheugen en SYS_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:

  • dockerd maakt zijn docker0-bridge en veth-paren aan binnen de eigen user-space netstack van gvx.
  • Alle iptables-legacy-regels (-t nat -N DOCKER, MASQUERADE, conntrack, addrtype) worden uitgevoerd tegen de interne netfilter-implementatie van gvx.
  • Geneste runc-containers mounten hun /proc-, /sys- en /sys/fs/cgroup-hiërarchieën binnen de in-memory VFS2-mounttabel van gvx, zonder ooit de mount(2)-systeemaanroep van de buitenste runsc aan 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

  1. gvx: Linux-containers, Docker en Kubernetes uitvoeren in één niet-geprivilegieerd proces: Overzicht van gvx op Windows (whpx, systrap), macOS (hvf) en Linux (systrap).
  2. Docker, docker build en docker compose uitvoeren zonder root of user namespaces: Hoe gvx docker:dind uitvoert wanneer root, sudo, unshare(CLONE_NEWUSER), mount(2) en /dev/fuse zijn geblokkeerd.
  3. Docker-in-Docker binnen gVisor en geneste sandboxes uitvoeren met gvx (dit bericht)
  4. Snelle geïsoleerde Docker-omgevingen voor RL-rollouts van AI-agents met gvx: Hoe u gvx en enkelbestands ext4lite-copy-on-write-snapshots gebruikt om honderden parallelle SWE-bench- en RL-rolloutomgevingen uit te voeren met resets van 100 milliseconden.