GoSuda

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

By Lemon Mint views ...

Het beveiligings- en infrastructuurknelpunt in Coding Agent RL Rollouts

Het trainen en evalueren van software engineering agents 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 onbetrouwbare, door LLM gegenereerde code. In praktijkgerichte softwareontwikkelingstaken 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. Vergrendelingsconcurrentie en statuslekkage van 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 extraheren van imagelagen, put de docker0-subnetallocaties uit, veroorzaakt fouten Another app is currently holding the xtables lock in de host-iptables, en laat verweesde containers achter tussen episodes.
  2. Risico op containerontsnapping via geprivilegieerde DinD: Het draaien van standaard docker:dind-containers vereist --privileged (CAP_SYS_ADMIN), wat ruwe host-blokapparaten, cgroup release agents en kernelmodules blootstelt aan onbetrouwbare RL-rollouts.
  3. Beperkingen van hardwarevirtualisatie (/dev/kvm): MicroVM-runtimes (Firecracker, QEMU, Kata Containers) isoleren interne kernels op een zuivere manier, maar zij vereisen /dev/kvm. Standaard Kubernetes-pods zonder geneste virtualisatie, bare-metal-cloudinstanties waarop KVM is uitgeschakeld, en bestaande gVisor (runsc)-sandboxes kunnen geen KVM-microVMs starten.
  4. Trage episode-reset (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-trainingworkers stilvallen terwijl ze op resets van de omgeving wachten.

gvx (rungv-CLI en z.gfire.dev/gvx/pkg/gvx Go SDK) biedt een veilige, weinig inspanning vergende, tokenefficiënte rollout-runtime die alle vier de knelpunten oplost. Elke rollout-omgeving draait als één niet-geprivilegieerd, 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).

Weinig inspanning, tokenefficiënte uitvoering voor agents: Wanneer docker build of docker compose binnen elke episode direct werkt (of door de agent in één beurt van drie commando's kan worden geboot via rungv), verspillen RL-rollouts geen contexttokens of stapbudgetten aan het oplossen van service docker start-ulimit-fouten, iptables-nft-fouten of runc pivot_root-weigeringen. Elk token in het traject gaat naar het oplossen van de daadwerkelijke softwareontwikkelingstaak.

Zero-privilege beveiliging voor onbetrouwbare agent-rollouts: 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). Zelfs als een RL-agent docker run --privileged of destructieve rm -rf /-commando's binnen zijn episode uitvoert, blijven die bewerkingen strikt beperkt tot de user-space Sentry-kernel en het tijdelijke .ext4-volumebestand van die episode.


Rollout-architectuur: parallelle niet-geprivilegieerde instanties + reflink-snapshots

In gvx is elke rollout-worker een gewoon niet-geprivilegieerd 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-addressable erofs OCI-imagecache deelt via de page cache van de host.

Parallelle Agent RL Rollout-architectuur met gvx

Waarom enkelvoudige ext4lite-volumes de resetkosten van RL veranderen

Omdat het ext4lite-bestandssysteem een volledig JBD2-gejournald ext4-volume opslaat (inclusief de overlayfs-laagmappen van Docker, trusted.overlay.*-uitgebreide attributen, whiteout-apparaatknooppunten en imagemetadata) in één regulier hostbestand, wordt het beheer van de episodelevenscyclus één bestandsbewerking:

  1. 100 milliseconden env.reset() via cp --reflink=always:
    • Je bouwt één keer een golden-docker.ext4-bestand met vooraf gepulde 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 minder dan 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 loskoppelen van ep-0042.ext4 geeft binnen ~10 milliseconden 100% van de processen, netwerksockets, mounts en opslag van de episode vrij. Niets wordt ooit gemount in de mounttabel van de host-kernel of geregistreerd in host-cgroups.

Stapsgewijze werkwijze: golden .ext4-snapshot en snelle episode-reset

Je kunt rungv op elk rollout-workerknooppunt installeren, hetzij door de SLSA-geattesteerde vooraf gebouwde binary 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), hetzij 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-docker:dind-instantie met golden-docker.ext4 als back-end
 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 op een nette manier zodat ext4lite de inhoud wegschrijft en comprimeert
20kill -INT $SEED_PID
21wait $SEED_PID

Stap 2: Start parallelle RL-episodes in 100 milliseconden

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

 1EPISODE_ID="ep-0042"
 2SOCK="/srv/rl-episodes/${EPISODE_ID}.sock"
 3
 4# Onmiddellijke 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 niet-geprivilegieerde episode 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# Teardown van de episode na reward-berekening (~10 ms)
25kill -KILL $EP_PID
26rm -f "/srv/rl-episodes/${EPISODE_ID}.ext4" "${SOCK}"

Programmatische rollout-pool met de z.gfire.dev/gvx/pkg/gvx Go SDK

Als je RL-omgevingsserver of evaluatieharnas in Go is geschreven (of een Go-rollout-daemon via gRPC aanroept), 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. Reflink-kloon 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 wall-clock-timeout 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, // 4 GiB limiet
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 agents

Metriek / VereisteGedeelde host-dockerdGeprivilegieerde 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 (enkel niet-geprivilegieerd proces)
Werkt binnen niet-geprivilegieerde K8s-Pods / bestaande runscNeeNeeNee (geen /dev/kvm)Mislukt (EBUSY bij geneste seccomp)Ja (automatische futex-loop-fallback)
Ondersteunt interne docker build & docker composeDeelt host-daemonJaJaMislukt zonder host-DinD-afstemmingJa (overlayfs op ext4lite)
Netwerkisolatie per episodeGedeelde docker0 / xtables-lockAparte namespaceAparte VM-NICAparte netnsGeïsoleerde in-process netstack
Latentie van warme episode-reset (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 platformen (Windows / macOS / Linux)Vereist Linux-VMVereist Linux-VMAlleen Linux KVMAlleen LinuxNative op Windows, macOS en Linux

Verder lezen in deze serie

  1. gvx: Linux-containers, Docker en Kubernetes uitvoeren in één niet-geprivilegieerd proces: Volledige architectuurwalkthrough van gvx op Windows (whpx, systrap), macOS (hvf) en Linux (systrap).
  2. Docker, docker build en docker compose uitvoeren zonder root of user namespaces: Snelle bootstrap in 3 stappen en troubleshootinggids wanneer sudo, unshare(CLONE_NEWUSER), mount(2) en /dev/fuse zijn geblokkeerd.
  3. Docker-in-Docker uitvoeren binnen gVisor en geneste sandboxes met gvx: Diepgaande analyse van waarom docker.io en geneste runsc falen 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-agents met gvx (deze post)