GoSuda

Kjøring av Docker-in-Docker inne i gVisor og Nested Sandboxes med gvx

By Lemon Mint views ...

Rask løsning: Sikker kjøring av docker:dind inne i en uprivilegert gVisor (runsc)-sandkasse

Plattformer for skybaserte AI-agenter, Kubernetes agent-sandbox-distribusjoner (RuntimeClass: gvisor) og evalueringsrammeverk for forsterkningslæring isolerer upålitelige arbeidslaster inne i gVisor (runsc)-containere, spesifikt for å beskytte vertskjernen. Når en arbeidslast i en standard uprivilegert runsc-container forsøker å installere docker.io eller docker-ce via apt-get og starte dockerd, mislykkes den, selv om den har root (UID 0) inne i containeren.

Med gvx (rungv) kan du sikkert kjøre docker:dind, docker build og docker compose direkte inne i en uprivilegert gVisor (runsc)-container (eller nøste gvx inne i gvx) uten å svekke den ytre RuntimeClass, eksponere vertssockets eller gi --privileged.

Bevaring og styrking av ytre sandkasseisolasjon: Å kjøre rungv inne i runsc omgår ikke en eneste begrensning håndhevet av den ytre gVisor-sandkassen. I stedet oppretter gvx en andre, nøstet brukerromsbasert gVisor Sentry-kjerne med sin egen hostlockdown (PR_SET_NO_NEW_PRIVS + seccomp), sin egen minnebaserte netstack og et enkelt-fil ext4lite-volum. Indre dockerd- og runc-containere er dobbelt sandkassert: deres systemkall, monteringer og rå pakker forlater aldri den indre gvx-brukerromskjernen.

 1# Inne i en eksisterende gVisor (runsc)-container eller Kubernetes-pod:
 2# Installer rungv fra et SLSA-attestert GitHub-utgivelsesarkiv 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# Kjør standard Docker-bygg og -containere mot den indre gvx-socketen
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# Utdata: overlayfs Alpine Linux ... (containerisert)
17
18docker build -t nested-app:test .
19docker run --rm nested-app:test

Fem grunner til at direkte Docker-installasjon mislykkes inne i gVisor (runsc)

Når en bruker eller en AI-kodeagent kjører apt-get install -y docker.io (eller docker-ce) inne i en standard runsc-container og forsøker å starte dockerd, møter den fem sekvensielle tekniske barrierer.

1. Innlasting av kjernemoduler (modprobe) mot virtuell kjerne 4.4.0

Under pakkeinstallasjon og oppstart av dockerd kjører initialiseringsskript modprobe for å laste overlay, br_netfilter, ip_tables, nf_nat og xt_conntrack. Den ytre gVisor Sentry blokkerer init_module(2) og finit_module(2) og rapporterer en virtuell kjerneversjon (4.4.0) som ikke har noen /lib/modules/4.4.0-katalog:

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. Feil med iptables-nft (nf_tables Netlink) og docker0-bridge

På Ubuntu 22.04, Ubuntu 24.04 og Debian 11/12 velger apt-get install docker.io iptables-nft (støttet av nf_tables-netlink-subsystemet) som standard alternativ for /usr/sbin/iptables. I en standard gVisor-container uten nettverksadministratorprivilegier eller støtte for nf_tables netlink, krasjer dockerd under initialisering av DOCKER NAT-kjeden og docker0-bridgen:

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. overlay2-feil på gofer/9p-rotfilsystem og utmattelse av disk ved vfs

Med mindre den ytre runsc-administratoren eksplisitt har forhåndskonfigurert en dedikert tmpfs- eller filestore-montering på /var/lib/docker, støttes containerens rotfilsystem av gVisors gofer (9p/lisafs) eller en nøstet overlay-montering. Montering av overlay2 oppå /var/lib/docker mislykkes:

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 brukere eller agenter forsøker å omgå denne feilen ved å starte dockerd --storage-driver=vfs, deaktiverer Docker fullstendig Copy-on-Write-deling av lag. Hver RUN-instruksjon i en Dockerfile utfører en fullstendig rekursiv kopiering (dirCopy) av hele rotfilsystemet, og multipliserer et 500 MB stort image over 10 byggesteg til 5 GB med skriving, og avslutter sandkassen med no space left on device eller en I/O-tidsavbrudd.

4. Avvisning av nøstet runc-cgroup og montering av /proc

Selv om dockerd tvinges til å starte med dockerd --iptables=false --bridge=none --storage-driver=vfs, kaller docker run eller docker build runc for å opprette den indre containeren. I en standard uprivilegert gVisor-container mislykkes runc ved montering av cgroups, ommontering av /proc eller innlasting av indre seccomp-filtre:

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. Nøstet runsc mislykkes med SECCOMP_FILTER_FLAG_NEW_LISTENER (EBUSY)

Hvis en bruker forsøker å kjøre en annen runsc-instans inne i runsc ved bruk av systrap-plattformen, avbrytes initialiseringen umiddelbart:

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

Både Linux-kjernen og gVisor Sentry tillater høyst én SECCOMP_FILTER_FLAG_NEW_LISTENER (SECCOMP_RET_USER_NOTIF)-lytter per oppgavehierarki. Fordi den ytre sandkassen allerede har den lytterplassen, returnerer det indre seccomp(2)-kallet EBUSY (device or resource busy).


Hvordan gvx løser nøstet kjøring inne i gVisor

gvx håndterer alle fem barrierene inne i sin egen brukerrom-kjerne og plattformlag, og gjør det mulig for rungv å kjøre inne i en ytre gVisor (runsc)-container, eller til og med rekursivt inne i en annen gvx-container (gvx i gvx).

Nøstet gvx-kjøring inne i ytre gVisor

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

I Linux systrap kjører hver stub-underprosess to typer tråder:

  • Gjesteutførelsestråder (sysmsg): Fanger gjestens systemkall via SECCOMP_RET_TRAP (SIGSYS) og binært patchede jmp rel32-trampoliner (syshandler), og utveksler registertilstand med Sentry over delt minne og SYS_FUTEX.
  • En stub-syscallThread (syscall_thread.go): Utfører adresserommets systemkall (mmap, munmap, clone) inne i stub-underprosessen på vegne av Sentry.

I upstream gVisor støtter syscallThread to vekkingsmodi 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-lytter (seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)), slik at Sentry kan sende forespørsler via ioctl(SECCOMP_IOCTL_NOTIF_SEND) og ioctl(SECCOMP_IOCTL_NOTIF_RECV).
  2. Futex-systemkalløkke (_RUN_SYSCALL_LOOP = 5, når t.seccompNotify == nil): syscallThread kjører syscall_loop (stub_amd64.s / stub_arm64.s) og sover på SYS_FUTEX (FUTEX_WAIT) over det delte 2-sides syscallSentryMessage-området (PROT_READ i stub) og syscallStubMessage-området (PROT_READ | PROT_WRITE i stub).

Upstream gVisor kaller ubetinget t.installSeccompNotify() når seccompNotifyIsSupported er sann, og avbryter opprettelsen av stub 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) kaller 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 ytre gVisor (runsc)- eller ytre gvx-sandkasse allerede har prosessfilterkjedens eneste lytterplass:

  • installSeccompNotifyIfFree() setter seccompNotifyBusy (atomic.Bool), logger én advarsel for prosessen, og lar t.seccompNotify == nil være for den stuben og alle påfølgende stubber.
  • I syscallThread.detach() (syscall_thread_amd64.go og syscall_thread_arm64.go) setter 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-systemkall utelukkende via delt minne og SYS_FUTEX:
1// pkg/sentry/platform/systrap/syscall_thread.go
2// Varsle systemkalltråden om en ny systemkallforespørsel.
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å gjestetråder kan stables til vilkårlig dybde, og syscallThread faller tilbake til _RUN_SYSCALL_LOOP uten å be om en andre brukervarsellytter, kjører gvx problemfritt inne i runsc og støtter rekursiv nøsting (gvx i gvx).

2. Innebygd overlayfs på enkeltfil-ext4lite-volumer

For å unngå å montere overlayfs oppå den ytre gVisor-containerens gofer (9p/lisafs)-rotfilsystem, understøtter -v gvx-docker:/var/lib/docker /var/lib/docker med gvxs interne ext4lite-filsystem lagret i en enkelt vanlig fil (gvx-docker.ext4).

For ytre runsc er gvx-docker.ext4 bare én vanlig fil som aksesseres via pread64(2) og pwrite64(2). Inne i gvx gir ext4lite et JBD2-journalført ext4-filsystem med trusted.overlay.*- og security.capability-utvidede attributter, tegnenhetsbaserte whiteout-noder (0:0), d_type-katalogoppføringer og RENAME_EXCHANGE, noe som lar den indre dockerd bruke sin innebygde overlayfs-lagringsdriver.

3. Selvstendig netstack med docker0 og iptables-legacy

Inne i gvx samhandler ikke dockerd med den ytre runsc-containerens nettverksgrensesnitt eller netfilter-tabeller. I stedet:

  • Oppretter dockerd sin docker0-bridge og veth-par inne i gvxs egen brukerrom-netstack.
  • Alle iptables-legacy-regler (-t nat -N DOCKER, MASQUERADE, conntrack, addrtype) kjøres mot gvxs interne netfilter-implementasjon.
  • Nøstede runc-containere monterer sine /proc-, /sys- og /sys/fs/cgroup-hierarkier inne i gvxs minnebaserte VFS2-monteringstabell, og kaller aldri det ytre runsc-mount(2)-systemkallet.

Verifisering av nøstet docker:dind inne i gVisor (eller gvx i gvx)

Du kan verifisere begge scenarioene (rungv inne i runsc, eller rungv inne i rungv) med følgende kommandoer:

 1# Inne i en ytre gVisor (runsc)-container eller en ytre 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# Verifiser at den indre Docker-daemonen bruker innebygd overlayfs (ikke vfs)
14docker info --format 'Storage Driver: {{.Driver}}'
15# Utdata: Storage Driver: overlayfs
16
17# Kjør et flerlags-bygg og utfør en nøstet 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# Utdata: nested-build-ok
31
32kill -INT $DIND_PID

Videre lesing i denne serien

  1. gvx: Kjøring av Linux-containere, Docker og Kubernetes i én uprivilegert prosess: Oversikt over gvx på Windows (whpx, systrap), macOS (hvf) og Linux (systrap).
  2. Kjøring av Docker, docker build og docker compose uten root eller brukernavnrom: Hvordan gvx kjører docker:dind når root, sudo, unshare(CLONE_NEWUSER), mount(2) og /dev/fuse er blokkert.
  3. Docker-in-Docker inne i gVisor og nøstede sandkasser med gvx (dette innlegget)
  4. Raske isolerte Docker-miljøer for RL-utrulling av AI-agenter med gvx: Hvordan bruke gvx og enkeltfil ext4lite copy-on-write-øyeblikksbilder for å kjøre hundrevis av parallelle SWE-bench- og RL-utrullingsmiljøer med 100-millisekunders tilbakestillinger.