GoSuda

Köra Docker-in-Docker inuti gVisor och nästlade Sandboxes med gvx

By Lemon Mint views ...

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

Molnbaserade AI-agentmiljöer för beräkning (till exempel Google Agent Compute Engine-sandlådor), Kubernetes-distributioner av agent-sandbox (RuntimeClass: gvisor) och utvärderingsramverk för förstärkningsinlärning isolerar ej betrodda arbetslaster i gVisor (runsc)-containrar, specifikt för att skydda värdens kärna. När en arbetslast i en standardmässig, icke-privilegierad runsc-container försöker installera docker.io eller docker-ce via apt-get, starta service docker start eller dockerd och köra docker build, 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 icke-privilegierad gVisor (runsc)-container (eller nästla gvx inuti gvx) utan att försvaga den yttre RuntimeClass, exponera värdens socklar eller bevilja --privileged.

Lågansträngd och tokeneffektiv lösning för AI-agenter: I en molnbaserad agentmiljö med stöd av gVisor förbrukar försöket att installera docker.io och manuellt kringgå ulimit-fel i /etc/init.d/docker, iptables-nft-fel, nekade råa socketanrop i iptables-legacy, monteringsfel för overlay2 samt kraschar i runc / pivot_root vid docker build typiskt 20+ verktygsanrop och tiotusentals tokens innan allt slutligen misslyckas. gvx (rungv) är en lågansträngd, tokeneffektiv och direkt användbar lösning: agenten kör 3 kommandon i en och samma tur för att starta en fungerande Docker-daemon i användarutrymme och kör omedelbart docker build och docker compose utan att ändra projektfiler.

Bevarande och förstärkning av den yttre sandlådeisoleringen: Att köra rungv inuti runsc kringgår inte en enda restriktion som upprätthålls av den yttre gVisor-sandlådan. I stället skapar gvx en andra, nästlad gVisor-Sentry-kärna i användarutrymme med egen hostlockdown (PR_SET_NO_NEW_PRIVS + seccomp), eget minnesbaserat netstack och en ext4lite-volym i en enda fil. Inre dockerd- och runc-containrar är dubbelt isolerade: deras systemanrop, monteringar och råa paket lämnar aldrig den inre gvx-kärnan i användarutrymme.

 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 ... (containerized)
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öts den av fem på varandra följande tekniska hinder.

1. Fel i init-skript (systemctl / service docker start ulimit) och kärnmoduler (modprobe 4.4.0)

Efter att ha kört apt-get install -y docker.io (eller docker-ce) misslyckas försöket att starta daemonen via systemctl start docker eller service docker start omedelbart, eftersom PID 1 inte är systemd och /etc/init.d/docker försöker höja ulimit -n över sandlådans hårda RLIMIT_NOFILE-tak. När dockerd sedan anropas direkt anropar init-skripten modprobe för att ladda overlay, bridge, br_netfilter, ip_tables och nf_nat mot gVisors virtuella kärnversion (4.4.0), som saknar katalogen /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), nekade råa socketanrop i iptables-legacy och fel i bryggan docker0

På Ubuntu 22.04, Ubuntu 24.04 och Debian 11/12 väljer apt-get install docker.io iptables-nft (som stöds av delsystemet nf_tables via netlink) som standardalternativ för /usr/sbin/iptables. Även om användaren eller agenten byter till iptables-legacy (update-alternatives --set iptables /usr/sbin/iptables-legacy), avvisar standardmässiga icke-privilegierade gVisor-sandlådor (--net-raw=false) sockets av typen AF_INET, SOCK_RAW med EPERM. Detta gör att iptables-legacy misslyckas med Permission denied (you must be root), även när processen körs 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. Fel med overlay2 på gofer- / 9p-rootfs och vfs-diskutmattning

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 nästlad 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 inaktiverar Docker helt delning av Copy-on-Write-lager. Varje RUN-instruktion i en Dockerfile utför då en fullständig rekursiv kopiering (dirCopy) av hela rotfilsystemet. En avbildning på 500 MB multipliceras över 10 byggsteg till 5 GB skrivningar, vilket avslutar sandlådan med no space left on device eller en I/O-timeout.

4. Fel i docker build (BuildKit) och nästlad runc (pivot_root, /proc, cgroups och DNS)

Även om dockerd tvingas starta med dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs misslyckas försöket att köra docker build -t app . eller docker run omedelbart när BuildKit eller runc startar byggcontainern:

 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. Nästlad 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 uppgiftshierarki. Eftersom den yttre sandlådan redan har lyssnarplatsen returnerar det inre seccomp(2)-anropet EBUSY (device or resource busy).


Hur gvx löser nästlad körning inuti gVisor

gvx hanterar alla fem hindren inuti sin egen kärna i användarutrymme 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).

Nästlad gvx-körning inuti yttre gVisor

1. Automatisk övergång i systrap från EBUSY till futex-loopen _RUN_SYSCALL_LOOP

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

  • Gästexekveringstrådar (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) inne i 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-loop (_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-systemanropsloop (_RUN_SYSCALL_LOOP = 5, när t.seccompNotify == nil): syscallThread kör syscall_loop (stub_amd64.s / stub_arm64.s) och vilar på SYS_FUTEX (FUTEX_WAIT) över den delade regionen med två sidor, syscallSentryMessage (PROT_READ i stubben) och syscallStubMessage (PROT_READ | PROT_WRITE i stubben).

Upstream-gVisor anropar ovillkorligen t.installSeccompNotify() närhelst 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 har filterkedjans enda lyssnarplats:

  • Sätter installSeccompNotifyIfFree() seccompNotifyBusy (atomic.Bool), loggar en varning för processen och lämnar t.seccompNotify == nil för denna stub 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 systemanropsbegäran.
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 nästling (gvx i gvx).

2. Inbyggd 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 typen teckenenhet (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ändigt 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 egna netstack i användarutrymme.
  • Alla regler för iptables-legacy (-t nat -N DOCKER, MASQUERADE, conntrack, addrtype) körs mot gvx:s interna netfilter-implementation.
  • Nästlade runc-containrar monterar sina hierarkier /proc, /sys och /sys/fs/cgroup inuti gvx:s minnesbaserade VFS2-monteringstabell och anropar aldrig den yttre runsc:s systemanrop mount(2).

Verifiering av nästlad 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-daemonen använder inbyggd overlayfs (inte vfs)
14docker info --format 'Storage Driver: {{.Driver}}'
15# Utdata: Storage Driver: overlayfs
16
17# Kör ett bygge med flera lager och exekvera en nästlad 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

Ytterligare läsning 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 man 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.