GoSuda

Att köra Docker-in-Docker inuti gVisor och kapslade Sandboxes med gvx

By Lemon Mint views ...

Snabb lösning: Säker körning av docker:dind i en ej privilegierad gVisor (runsc)-sandlåda

Molnbaserade AI-agentplattformar, Kubernetes-deployeringar av agent-sandbox (RuntimeClass: gvisor) och utvärderingsmiljöer för reinforcement learning isolerar opålitliga arbetslaster i gVisor (runsc)-containrar för att specifikt skydda värdens kärna. När en arbetslast i en standardmässig ej privilegierad runsc-container försöker installera docker.io eller docker-ce via apt-get och starta dockerd misslyckas den, även om den har root (UID 0) inuti containern.

Med gvx (rungv) kan du säkert köra docker:dind, docker build och docker compose direkt inuti en ej privilegierad gVisor (runsc)-container (eller kapsla gvx inuti gvx) utan att försvaga den yttre RuntimeClass, exponera värdens sockets eller använda --privileged.

Bevarande och förstärkning av isolering i yttre sandlåda: Att köra rungv inuti runsc innebär inte att någon restriktion som upprätthålls av den yttre gVisor-sandlådan kringgås. I stället skapar gvx en andra, kapslad användarrymdskärna av typen gVisor Sentry med sin egen hostlockdown (PR_SET_NO_NEW_PRIVS + seccomp), sin egen minnesbaserade netstack och en ext4lite-volym i en enda fil. Inre dockerd- och runc-containrar är dubbelt sandlådeisolerade: deras systemanrop, monteringar och råa paket lämnar aldrig den inre gvx-kärnan i användarrymden.

 1# Inuti en befintlig gVisor (runsc)-container eller Kubernetes Pod:
 2# Installera rungv från ett SLSA-attesterat GitHub-releasearkiv 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 vanliga Docker-byggen och containrar mot den inre 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 ... (containeriserad)
17
18docker build -t nested-app:test .
19docker run --rm nested-app:test

Fem skäl till varför direkt Docker-installation misslyckas inuti gVisor (runsc)

När en användare eller en AI-kodningsagent kör apt-get install -y docker.io (eller docker-ce) inuti en standard-runsc-container och försöker starta dockerd, möter den fem på varandra följande tekniska hinder.

1. Laddning av kärnmoduler (modprobe) mot virtuell kärna 4.4.0

Under paketinstallation och uppstart av dockerd kör initialiseringsskript modprobe för att ladda overlay, br_netfilter, ip_tables, nf_nat och xt_conntrack. Den yttre gVisor Sentry blockerar init_module(2) och finit_module(2) och rapporterar en virtuell kärnversion (4.4.0) som saknar katalogen /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. Fel i iptables-nft (nf_tables Netlink) och bryggan docker0

På Ubuntu 22.04, Ubuntu 24.04 och Debian 11/12 väljer apt-get install docker.io iptables-nft (baserat på undersystemet nf_tables netlink) som standardalternativ för /usr/sbin/iptables. I en standard gVisor-container utan nätverksadministratörsbehörighet eller stöd för nf_tables netlink kraschar dockerd vid initiering av NAT-kedjan DOCKER och bryggan 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. Fel vid overlay2 på gofer/9p-rootfs och överbelastning av disk vid vfs

Om inte administratören för den yttre runsc uttryckligen har konfigurerat en dedikerad tmpfs- eller filestore-montering på /var/lib/docker backas containerns rootfs av gVisors gofer (9p/lisafs) eller av en kapslad overlay-montering. Montering av overlay2 ovanpå /var/lib/docker misslyckas:

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 användare eller agenter försöker kringgå detta fel genom att starta dockerd --storage-driver=vfs inaktiveras delning av Copy-on-Write-lager helt. Varje RUN-instruktion i en Dockerfile utför en fullständig rekursiv kopiering (dirCopy) av hela rotfilsystemet, vilket multiplicerar en avbildning på 500 MB över 10 byggsteg till 5 GB skrivningar och avslutar sandlådan med no space left on device eller en I/O-timeout.

4. Avvisningar av kapslad runc för cgroup- och /proc-montering

Även om dockerd tvingas starta med dockerd --iptables=false --bridge=none --storage-driver=vfs anropar docker run eller docker build runc för att skapa den inre containern. I en standardmässig ej privilegierad gVisor-container misslyckas runc vid montering av cgroups, ommontering av /proc eller laddning av inre seccomp-filter:

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. Kapslad runsc misslyckas med SECCOMP_FILTER_FLAG_NEW_LISTENER (EBUSY)

Om en användare försöker köra en annan runsc-instans inuti runsc med plattformen systrap avbryts initieringen omedelbart:

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

Både Linux-kärnan och gVisor Sentry tillåter högst en SECCOMP_FILTER_FLAG_NEW_LISTENER (SECCOMP_RET_USER_NOTIF)-lyssnare per task-hierarki. Eftersom den yttre sandlådan redan innehar den lyssnarplatsen returnerar det inre seccomp(2)-anropet EBUSY (device or resource busy).


Så löser gvx kapslad körning inuti gVisor

gvx hanterar alla fem hindren inom sin egen användarrymdskärna och plattformslager, vilket gör det möjligt för rungv att köras inuti en yttre gVisor (runsc)-container eller till och med rekursivt inuti en annan gvx-container (gvx i gvx).

Kapslad gvx-körning inuti yttre gVisor

1. Automatisk återgång från systrap vid EBUSY till futex-slingan _RUN_SYSCALL_LOOP

I Linux systrap kör varje stub-underprocess två typer av trådar:

  • Trådar för gästkörning (sysmsg): Fångar gästens systemanrop via SECCOMP_RET_TRAP (SIGSYS) och binärt patchade jmp rel32-trampoliner (syshandler), och utbyter registertillstånd med Sentry via delat minne och SYS_FUTEX.
  • En stub-syscallThread (syscall_thread.go): Kör adressrymdens systemanrop (mmap, munmap, clone) inuti stub-underprocessen för Sentrys räkning.

I upstream gVisor stöder syscallThread två väckningslägen i stub_amd64.s och stub_arm64.s:

  1. Seccomp User-Notify-slinga (_RUN_SECCOMP_LOOP = 6, när t.seccompNotify != nil): Under syscallThread.init() installerar t.installSeccompNotify() en SECCOMP_RET_USER_NOTIF-lyssnare (seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)) så att Sentry kan skicka förfrågningar via ioctl(SECCOMP_IOCTL_NOTIF_SEND) och ioctl(SECCOMP_IOCTL_NOTIF_RECV).
  2. Futex-slinga för systemanrop (_RUN_SYSCALL_LOOP = 5, när t.seccompNotify == nil): syscallThread kör syscall_loop (stub_amd64.s / stub_arm64.s) och väntar på SYS_FUTEX (FUTEX_WAIT) över det delade 2-sidiga området syscallSentryMessage (PROT_READ i stubben) och syscallStubMessage (PROT_READ | PROT_WRITE i stubben).

Upstream gVisor anropar ovillkorligen t.installSeccompNotify() när seccompNotifyIsSupported är sant, och avbryter skapandet av stubben om seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...) returnerar EBUSY.

I gvx (pkg/sentry/platform/systrap/seccomp_notify_xplat.go och syscall_thread.go) anropar 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() returnerar unix.EBUSY eftersom en yttre gVisor (runsc) eller yttre gvx-sandlåda redan innehar filterkedjans enda lyssnarplats för processen:

  • Sätter installSeccompNotifyIfFree() seccompNotifyBusy (atomic.Bool), loggar en varning för processen och lämnar t.seccompNotify == nil för den stubben och alla efterföljande stubbar.
  • I syscallThread.detach() (syscall_thread_amd64.go och 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 stället för _RUN_SECCOMP_LOOP (6).
  • I syscallThread.syscall() (syscall_thread.go) skickar den inre Sentry stubbens systemanrop enbart via delat minne och SYS_FUTEX:
1// pkg/sentry/platform/systrap/syscall_thread.go
2// Meddela syscall-tråden om en ny begäran om systemanrop.
3atomic.AddUint32(&sentryMsg.state, 1)
4futexWakeUint32(&sentryMsg.state)
5
6// Vänta på svar.
7futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))

Eftersom SECCOMP_RET_TRAP-filter på gästtrådar kan staplas godtyckligt djupt och syscallThread faller tillbaka till _RUN_SYSCALL_LOOP utan att begära en andra användarnotifieringslyssnare, kör gvx problemfritt inuti runsc och stöder rekursiv kapsling (gvx i gvx).

2. Inbyggt overlayfs på ext4lite-volymer i en enda fil

För att undvika att montera overlayfs ovanpå den yttre gVisor-containerns rotfilsystem på gofer (9p/lisafs) backar -v gvx-docker:/var/lib/docker /var/lib/docker med gvx:s interna filsystem ext4lite, lagrat i en enda vanlig fil (gvx-docker.ext4).

För den yttre runsc är gvx-docker.ext4 bara en vanlig fil som nås via pread64(2) och pwrite64(2). Inuti gvx tillhandahåller ext4lite ett ext4-filsystem med JBD2-journal, med utökade attribut trusted.overlay.* och security.capability, whiteout-noder av teckenenhetstyp (0:0), d_type-katalogposter och RENAME_EXCHANGE. Detta gör att den inre dockerd kan använda sin inbyggda overlayfs-lagringsdrivrutin.

3. Självständig netstack med docker0 och iptables-legacy

Inuti gvx interagerar inte dockerd med den yttre runsc-containerns nätverksgränssnitt eller netfilter-tabeller. I stället:

  • Skapar dockerd sin bro docker0 och veth-par inuti gvx:s egen användarrymds-netstack.
  • Alla regler för iptables-legacy (-t nat -N DOCKER, MASQUERADE, conntrack, addrtype) körs mot gvx:s interna netfilter-implementation.
  • Kapslade runc-containrar monterar sina hierarkier /proc, /sys och /sys/fs/cgroup inuti gvx:s minnesbaserade VFS2-monteringstabell, och anropar aldrig det yttre runsc-systemanropet mount(2).

Verifiering av kapslad docker:dind inuti gVisor (eller gvx i gvx)

Du kan verifiera båda scenarierna (rungv inuti runsc, eller rungv inuti rungv) med följande kommandon:

 1# Inuti en yttre gVisor (runsc)-container eller en yttre 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# Verifiera att den inre Docker-demonen använder native overlayfs (inte vfs)
14docker info --format 'Storage Driver: {{.Driver}}'
15# Utdata: Storage Driver: overlayfs
16
17# Kör en flerlagersbygge och kör en kapslad 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

Fler läsningar i denna serie

  1. gvx: Running Linux Containers, Docker, and Kubernetes in One Unprivileged Process: Översikt över gvx på Windows (whpx, systrap), macOS (hvf) och Linux (systrap).
  2. Running Docker, docker build, and docker compose Without Root or User Namespaces: Hur gvx kör docker:dind när root, sudo, unshare(CLONE_NEWUSER), mount(2) och /dev/fuse är blockerade.
  3. Running Docker-in-Docker Inside gVisor and Nested Sandboxes with gvx (detta inlägg)
  4. Fast Isolated Docker Environments for AI Agent RL Rollouts with gvx: Hur du använder gvx och ext4lite-ögonblicksbilder med copy-on-write i en enda fil för att köra hundratals parallella SWE-bench- och RL-rollout-miljöer med återställningar på 100 millisekunder.