GoSuda

Snelle geïsoleerde Docker-omgevingen voor AI-agent RL-rollouts met gvx

By Lemon Mint views ...

Het beveiligings- en infrastructuurknelpunt in RL-rollouts van coderingsagenten

Het trainen en evalueren van softwareengineeringagenten over benchmarks en reinforcement learning (RL)-frameworks zoals SWE-bench, SWE-ReX, R2E-Gym, SWE-Gym, Verl en OpenHands vereist het uitvoeren van duizenden gelijktijdige rollouts van niet-vertrouwde, door LLM's gegenereerde code. Bij echte softwareengineeringtaken bewerkt een agent niet alleen tekstbestanden: hij voert docker build uit, start databases en microservices op via docker compose, voert integratietestsuites uit en inspecteert containerlogs.

Op rolloutschaal (128 tot 1.024 gelijktijdige episodes per cluster) loopt bestaande containerinfrastructuur tegen vier systemische beveiligings- en prestatieknelpunten aan:

  1. Vergrendelingsconflicten en statuslekken in de host-dockerd: Het richten van honderden gelijktijdige RL-workers op één host-/var/run/docker.sock geeft elke agent root-equivalente toegang tot de host-daemon, serialiseert het uitpakken van image-lagen, put de docker0-subnettoewijzingen uit, veroorzaakt de fout Another app is currently holding the xtables lock in host-iptables en laat verweesde containers achter over episodes heen.
  2. Ontsnappingsrisico bij privileged DinD-containers: Standaard docker:dind-containers vereisen --privileged (CAP_SYS_ADMIN), waardoor ruwe host-blokapparaten, cgroup-release-agents en kernelmodules worden blootgesteld aan niet-vertrouwde RL-rollouts.
  3. Beperkingen van hardwarevirtualisatie (/dev/kvm): MicroVM-runtimes (Firecracker, QEMU, Kata Containers) isoleren interne kernels netjes, maar vereisen /dev/kvm. Standaard Kubernetes-pods zonder geneste virtualisatie, bare-metal cloudinstanties waarop KVM is uitgeschakeld, en bestaande gVisor (runsc)-sandboxes kunnen geen KVM-microVM's starten.
  4. Trage episodereset (env.reset())-latentie: Het starten van een nieuwe docker:dind-omgeving en het ophalen of uitpakken van basisimages voor elke RL-episode duurt 10 tot 60 seconden, waardoor GPU-trainingsworkers inactief blijven terwijl ze op omgevingsresets wachten.

gvx (rungv CLI en z.gfire.dev/gvx/pkg/gvx Go SDK) biedt een veilige, defense-in-depth rolloutruntime die alle vier knelpunten oplost. Elke rolloutomgeving draait als één onprivileged, zelfvergrendelend user-space proces (geïsoleerd door Google's gVisor Sentry-kernel, pkg/hostlockdown en een in-memory netstack), waarvan de volledige /var/lib/docker-status zich bevindt in één copy-on-write .ext4-bestand (ext4lite).

Zero-Privilege-beveiliging voor niet-vertrouwde agentrollouts: Elke gvx-episode vergrendelt zijn eigen host-OS-proces voordat gastcode wordt uitgevoerd (PR_SET_NO_NEW_PRIVS + BPF seccomp op Linux, Seatbelt deny default op macOS, Restricted Token + Job Object op Windows). Ook als een RL-agent docker run --privileged of destructieve rm -rf /-commando's binnen zijn episode uitvoert, blijven deze bewerkingen strikt beperkt tot de user-space Sentry-kernel en het vluchtige .ext4-volumebestand van die episode.


Rolloutarchitectuur: parallelle onprivileged instanties + reflink-snapshots

In gvx is elke rolloutworker een gewoon onprivileged OS-proces met zijn eigen gVisor Sentry-kernel, zijn eigen user-space netstack (10.0.2.15/24) en zijn eigen ext4lite-blokvolume (ep-N.ext4), terwijl hij een alleen-lezen, content-adresseerbare erofs-OCI-imagecache deelt via de page cache van de host.

Parallelle agent-RL-rolloutarchitectuur met gvx

Waarom enkelbestands-ext4lite-volumes de RL-resetkosten veranderen

Omdat het ext4lite-bestandssysteem van gvx een volledig JBD2-gejournaled ext4-volume opslaat (inclusief de overlayfs-laagdirectories van Docker, trusted.overlay.*-extended attributes, whiteout-apparaatknooppunten en image-metadata) in één regulier hostbestand, wordt het beheer van de episodecyclus een enkele bestandsbewerking:

  1. 100 milliseconden env.reset() via cp --reflink=always:
    • Je bouwt één keer een golden-docker.ext4-bestand met vooraf opgehaalde basisimages (python:3.11, postgres:16, redis:7 of taakspecifieke SWE-bench-basisimages).
    • Aan het begin van elke RL-episode maakt cp --reflink=always golden-docker.ext4 ep-0042.ext4 (op XFS, Btrfs, ZFS of macOS APFS) binnen 100 milliseconden een onafhankelijke Copy-on-Write-kloon, zonder de onderliggende schijfblokken te kopiëren.
    • Alleen de 4 KiB-blokken die tijdens die specifieke episode zijn gewijzigd, nemen extra schijfruimte in beslag.
  2. Teardown zonder residu (env.close()):
    • Het beëindigen van het rungv-proces (sb.Close() in Go) en het verwijderen van de link naar ep-0042.ext4 geeft in ongeveer 10 milliseconden 100% van de processen, netwerksockets, mounts en opslag van de episode vrij. Er wordt nooit iets gemount in de mounttabel van de host-kernel of geregistreerd in host-cgroups.

Stap-voor-stap-workflow: golden .ext4-snapshot en snelle episodereset

Je kunt rungv op elk rolloutworkerknooppunt installeren, door ofwel het SLSA-geattesteerde vooraf gebouwde binaire bestand te downloaden van GitHub Releases (gh attestation verify rungv-v0.2.0-linux-amd64.tar.gz --repo gfire-sigs/gvx --signer-workflow gfire-sigs/gvx/.github/workflows/release.yml --deny-self-hosted-runners), of door CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest uit te voeren.

Stap 1: maak het golden docker:dind-volume eenmalig aan

 1mkdir -p /srv/rl-snapshots /srv/rl-episodes
 2
 3# 1. Start een seed-instantie van docker:dind met golden-docker.ext4 als backend
 4rungv run \
 5  -p unix:/srv/rl-snapshots/seed.sock:2375 \
 6  --mount type=volume,src=/srv/rl-snapshots/golden-docker.ext4,dst=/var/lib/docker \
 7  -e DOCKER_TLS_CERTDIR= \
 8  docker:dind --tls=false &
 9SEED_PID=$!
10
11# 2. Vul basisimages en buildcache vooraf in golden-docker.ext4
12export DOCKER_HOST="unix:///srv/rl-snapshots/seed.sock"
13until docker info >/dev/null 2>&1; do sleep 0.5; done
14
15docker pull python:3.11-slim
16docker pull redis:7-alpine
17docker pull postgres:16-alpine
18
19# 3. Stop de seed-instantie netjes zodat ext4lite de image wegschrijft en comprimeert
20kill -INT $SEED_PID
21wait $SEED_PID

Stap 2: start parallelle RL-episodes in 100 milliseconden

Elke parallelle rolloutworker kloont golden-docker.ext4 via reflink en start een geïsoleerde docker:dind-instantie waarin alle images al aanwezig zijn in /var/lib/docker:

 1EPISODE_ID="ep-0042"
 2SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"
 3
 4# Directe Copy-on-Write-kloon (~100 ms op XFS/Btrfs/APFS)
 5cp --reflink=always \
 6  /srv/rl-snapshots/golden-docker.ext4 \
 7  "/srv/rl-episodes/${EPISODE_ID}.ext4"
 8
 9# Start een geïsoleerde episode zonder privileges met 4 GiB RAM en 2 CPU's
10rungv run \
11  -m 4g --cpus 2 \
12  -p "unix:${SOCK}:2375" \
13  --mount "type=volume,src=/srv/rl-episodes/${EPISODE_ID}.ext4,dst=/var/lib/docker" \
14  -e DOCKER_TLS_CERTDIR= \
15  docker:dind --tls=false &
16EP_PID=$!
17
18export DOCKER_HOST="unix://${SOCK}"
19until docker info >/dev/null 2>&1; do sleep 0.2; done
20
21# Binnen de rollout-worker zijn images direct beschikbaar zonder netwerkpull
22docker images
23
24# Opruimen van de episode na reward-berekening (~10 ms)
25kill -KILL $EP_PID
26rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"

Programmatische rolloutpool met de z.gfire.dev/gvx/pkg/gvx Go SDK

Als je RL-omgevingsserver of evaluatieharnas is geschreven in Go (of een Go-rolloutdaemon aanroept via gRPC), kun je parallelle containerepisodes rechtstreeks beheren via z.gfire.dev/gvx/pkg/gvx:

 1package main
 2
 3import (
 4	"bytes"
 5	"context"
 6	"fmt"
 7	"log"
 8	"os"
 9	"os/exec"
10	"path/filepath"
11	"time"
12
13	"z.gfire.dev/gvx/pkg/gvx"
14)
15
16func runEpisode(ctx context.Context, goldenExt4, scratchDir string, epID int) error {
17	epExt4 := filepath.Join(scratchDir, fmt.Sprintf("ep-%04d.ext4", epID))
18	defer os.Remove(epExt4)
19
20	// 1. Maak een reflink-kloon van het golden ext4lite-volume
21	cpCmd := exec.CommandContext(ctx, "cp", "--reflink=always", goldenExt4, epExt4)
22	if err := cpCmd.Run(); err != nil {
23		return fmt.Errorf("clone golden volume: %w", err)
24	}
25
26	// 2. Pas een wandkloktimeout per episode toe
27	epCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
28	defer cancel()
29
30	var stdout, stderr bytes.Buffer
31	sb, err := gvx.New(epCtx, gvx.Config{
32		Platform:               gvx.PlatformAuto,
33		NetworkMode:            gvx.NetModeNAT,
34		NetRaw:                 true,
35		AllowPacketSocketWrite: true,
36		AllowSUID:              true,
37		OCIImage:               "docker:dind",
38		Entrypoint:             []string{"sh", "-c"},
39		EntrypointSet:          true,
40		Cmd: []string{
41			"dockerd-entrypoint.sh --tls=false >/var/log/dockerd.log 2>&1 & " +
42				"until docker info >/dev/null 2>&1; do sleep 0.2; done && " +
43				"docker run --rm python:3.11-slim python -c 'print(6 * 7)'",
44		},
45		Envv: []string{"DOCKER_TLS_CERTDIR="},
46		Mounts: []gvx.Mount{
47			{
48				Kind:      gvx.MountVolume,
49				ImagePath: epExt4,
50				GuestPath: "/var/lib/docker",
51			},
52		},
53		Resources: gvx.Resources{
54			MemoryBytes: 4 << 30, // Limiet van 4 GiB
55			CPUMillis:   2000,    // 2,0 CPU's
56		},
57		Stdout: &stdout,
58		Stderr: &stderr,
59	})
60	if err != nil {
61		return fmt.Errorf("init episode %d: %w", epID, err)
62	}
63	defer sb.Close()
64
65	exitCode, err := sb.Run(epCtx)
66	if err != nil {
67		return fmt.Errorf("run episode %d: %w", epID, err)
68	}
69
70	log.Printf("episode %d finished: exit=%d stdout=%q", epID, exitCode, stdout.String())
71	return nil
72}
73
74func main() {
75	ctx := context.Background()
76	if err := runEpisode(ctx, "/srv/rl-snapshots/golden-docker.ext4", "/srv/rl-episodes", 1); err != nil {
77		log.Fatal(err)
78	}
79}

Vergelijking van runtimes voor RL-sandboxes van agenten

Metriek / VereisteGedeelde host-dockerdPrivileged docker:dindFirecracker / KVM MicroVMStandaard gVisor (runsc)gvx (rungv / pkg/gvx)
Host-root of --privileged vereistJa (docker.sock = root)Ja (--privileged)Vereist /dev/kvm + TAP-setupJa (of userns + mount)Nee (één onprivileged proces)
Werkt in onprivileged K8s-pods / bestaande runscNeeNeeNee (geen /dev/kvm)Mislukt (EBUSY bij geneste seccomp)Ja (automatische futex-loop-fallback)
Ondersteunt geneste docker build & docker composeDeelt host-daemonJaJaMislukt zonder host-DinD-afstellingJa (overlayfs op ext4lite)
Netwerkisolatie per episodeGedeelde docker0 / xtables-lockAparte namespaceAparte VM-NICAparte netnsGeïsoleerde in-process netstack
Latentie bij warme episodereset (env.reset())5s tot 30s (container/volume prune)10s tot 60s (koude daemon)~150 ms (VM-snapshot)5s tot 20s~100 ms (cp --reflink=always .ext4)
Lokale ontwikkeling op meerdere platforms (Windows / macOS / Linux)Vereist Linux-VMVereist Linux-VMAlleen Linux KVMAlleen LinuxNative op Windows, macOS en Linux

Verder lezen in deze reeks

  1. gvx: Linux-containers, Docker en Kubernetes draaien in één onprivileged proces: Volledige architectuurrondleiding van gvx op Windows (whpx, systrap), macOS (hvf) en Linux (systrap).
  2. Docker, docker build en docker compose draaien zonder root of user namespaces: Snelle bootstrapgids in 3 stappen met probleemoplossing voor het geval sudo, unshare(CLONE_NEWUSER), mount(2) en /dev/fuse geblokkeerd zijn.
  3. Docker-in-Docker draaien in gVisor en geneste sandboxes met gvx: Diepgaande analyse van waarom docker.io en geneste runsc mislukken binnen gVisor (runsc) en hoe gvx terugvalt op de _RUN_SYSCALL_LOOP futex-loop bij EBUSY.
  4. Snelle geïsoleerde Docker-omgevingen voor RL-rollouts van AI-agenten met gvx (dit bericht)