Sandbox LLM-Generated Code with gVisor and Docker A developer guide published on Sourcefeed by Ji-ho Choi demonstrates how to run untrusted LLM-generated code inside a gVisor sandbox using Docker's runsc runtime, verified against gVisor release 20260831.0 on Ubuntu 24.04. The setup isolates hostile scripts from the host kernel by intercepting syscalls via gVisor's Sentry, with steps covering installation, Docker runtime registration, and a minimal Python image. Sandbox LLM-Generated Code with gVisor and Docker Run untrusted agent-generated code behind gVisor's user-space kernel so hostile scripts never touch your host kernel. Ji-ho Choi https://sourcefeed.dev/u/jiho choi What you'll build An execution path for code your LLM agent writes: a Docker container that runs under gVisor https://gvisor.dev 's runsc runtime instead of the default runc , so untrusted Python is serviced by a user-space kernel and never talks to your host kernel directly. By the end you'll run a generated script under gVisor with network, capabilities, and filesystem writes stripped away, and you'll be able to prove the sandbox is active. The threat model in one sentence: a normal container shares your host kernel, so hostile agent output is one kernel exploit away from the host, while gVisor's Sentry intercepts every syscall the workload makes and forwards only a small, seccomp-filtered set to the real kernel. Prerequisites - A Linux host on x86 64 or ARM64 with kernel 5.6 or newer. Bare metal or a cloud VM both work. Docker https://docs.docker.com/engine/ Desktop on macOS or Windows does not; you can't register custom runtimes in its managed VM, so use a Linux box. - Docker Engine installed and managed by systemd. The --runtime flag has been stable for years, so any recent version works. - Root or sudo access. - Verified against gVisor release 20260831.0 September 2026 on Ubuntu 24.04. The install commands below are for Debian/Ubuntu; the manual binary route in step 1 covers everything else. Step 1: Install gVisor On Debian or Ubuntu, add Google's apt repository and install runsc : sudo apt-get update && sudo apt-get install -y \ apt-transport-https ca-certificates curl gnupg curl -fsSL https://gvisor.dev/archive.key | \ sudo gpg --dearmor -o /usr/share/keyrings/gvisor-archive-keyring.gpg echo "deb arch=$ dpkg --print-architecture signed-by=/usr/share/keyrings/gvisor-archive-keyring.gpg https://storage.googleapis.com/gvisor/releases release main" | \ sudo tee /etc/apt/sources.list.d/gvisor.list /dev/null sudo apt-get update && sudo apt-get install -y runsc On any other distro, grab the latest release binary bundle directly: set -e ARCH=$ uname -m URL=https://storage.googleapis.com/gvisor/releases/release/latest/${ARCH} wget ${URL}/gvisor.tar.zstd ${URL}/gvisor.tar.zstd.sha512 sha512sum -c gvisor.tar.zstd.sha512 sudo tar --zstd -xf gvisor.tar.zstd -C /usr/local/bin rm -f gvisor.tar.zstd gvisor.tar.zstd.sha512 Step 2: Register runsc with Docker runsc ships a helper that writes the runtime entry into /etc/docker/daemon.json for you: sudo runsc install sudo systemctl restart docker Your daemon.json now contains a runtimes entry pointing at the binary /usr/bin/runsc for the apt install, /usr/local/bin/runsc for the manual one . Confirm Docker picked it up: docker info | grep -i runtimes You should see runsc listed alongside runc . The default runtime is still runc , which is what you want: only the untrusted workloads opt in. Step 3: Build the runner image Keep the image minimal and run as a non-root user, because gVisor protects the host kernel, not the container's own filesystem. Create a Dockerfile : FROM python:3.13-slim RUN useradd --create-home runner USER runner WORKDIR /home/runner Build it: docker build -t llm-sandbox . Step 4: Run generated code under the sandbox Simulate agent output. In practice your agent framework writes this file; here we fake it with something that reports where it's running: python mkdir -p untrusted cat untrusted/agent code.py <<'EOF' import platform, sys print "python", sys.version.split 0 print "kernel", platform.release print "result", sum i i for i in range 10 EOF Now execute it under runsc with everything else locked down too. gVisor handles kernel isolation; the remaining flags are defense in depth you'd want even under runc : docker run --rm \ --runtime=runsc \ --network=none \ --cap-drop=ALL \ --security-opt=no-new-privileges \ --read-only \ --tmpfs /tmp:rw,noexec,nosuid,size=64m \ --memory=256m --cpus=1 --pids-limit=128 \ -v "$PWD/untrusted:/home/runner/job:ro" \ llm-sandbox \ python /home/runner/job/agent code.py What each layer buys you: - --runtime=runsc : syscalls hit gVisor's Sentry, not your host kernel. - --network=none : generated code can't exfiltrate data or pull payloads. - --cap-drop=ALL and --security-opt=no-new-privileges : no capabilities, and no way to gain any via setuid binaries. - --read-only plus a small noexec tmpfs: the script gets scratch space but can't persist anything or drop executables. - --memory , --cpus , --pids-limit : an infinite loop or fork bomb burns its own budget, not your machine. - The code mounts in read-only; results come back on stdout. Verify it works First, prove the sandbox is real. gVisor boots its own tiny kernel, and its dmesg output is unmistakable: docker run --runtime=runsc --rm llm-sandbox dmesg Expected output the middle lines are randomized jokes; the first and last are what matter : 0.000000 Starting gVisor... ... 2.613217 Ready Second, compare kernel identities with and without gVisor: docker run --rm llm-sandbox uname -r docker run --rm --runtime=runsc llm-sandbox uname -r The first prints your host's real kernel release, something like 6.8.0-45-generic . The second prints gVisor's synthetic version 4.4.0 at the time of writing , because the workload only ever sees the emulated kernel. Third, the full sandboxed run from step 4 should print: python 3.13.7 kernel 4.4.0 result 285 Your Python patch version may differ. If kernel shows your host release, you forgot --runtime=runsc . Troubleshooting docker: Error response from daemon: unknown or invalid runtime name: runsc Docker doesn't know about the runtime yet. Run sudo runsc install , confirm /etc/docker/daemon.json has the runsc entry, and restart with sudo systemctl restart docker . A docker restart of a container is not enough; the daemon itself must reload. panic: unable to attach: operation not permitted or fork/exec /proc/self/exe: invalid argument The runsc binary's permissions are wrong, which happens with manual installs. Fix with sudo chmod a+rx /usr/local/bin/runsc . The binary must be executable by all users since it re-execs itself in the sandbox. SELinux is not supported: system u:system r:container t:s0... On Fedora, RHEL, and friends, Docker's SELinux labeling conflicts with runsc. Per gVisor's FAQ, run the container with --security-opt label=disable . You're trading SELinux confinement for gVisor's, which is the point of this setup anyway. Container works under runc but fails under runsc gVisor implements most of the Linux syscall surface, not all of it. Reinstall a debug runtime and trace what the workload attempted: sudo runsc install --runtime runsc-debug -- --debug --debug-log=/tmp/runsc-debug.log --strace , restart Docker, rerun with --runtime=runsc-debug , and search the log for unimplemented . For an LLM sandbox this is usually acceptable breakage: exotic syscalls in generated code are a signal, not a feature. Next steps - Pick a platform deliberately. The default syscall interception mode is systrap , which works everywhere including inside VMs. On bare metal, benchmark --platform=kvm passed via runtimeArgs in daemon.json for syscall-heavy workloads. - Keep the runsc-debug runtime around in staging. --strace logs every syscall generated code makes, which doubles as an audit trail for what your agent's output actually does. - Cut Docker out of the hot path: runsc runs OCI bundles directly runsc spec , then runsc run , useful when your agent orchestrator manages rootfs images itself. - Scaling to a cluster? gVisor plugs into Kubernetes through containerd's runsc shim and a RuntimeClass , so agent pods opt in with one line of spec. Start at gVisor's production guide on gvisor.dev. Sources & further reading 1. Installation - gVisor https://gvisor.dev/docs/user guide/install/ — gvisor.dev 2. Docker Quick Start - gVisor https://gvisor.dev/docs/user guide/quick start/docker/ — gvisor.dev 3. Platform Guide - gVisor https://gvisor.dev/docs/architecture guide/platforms/ — gvisor.dev 4. FAQ - gVisor https://gvisor.dev/docs/user guide/faq/ — gvisor.dev 5. gVisor Releases https://github.com/google/gvisor/releases — github.com Ji-ho Choi https://sourcefeed.dev/u/jiho choi · Security & Cloud Editor Ji-ho covers the increasingly tangled overlap between cloud architecture and security, drawing on a background as a penetration tester to keep his reporting grounded in real-world attack paths. He never lets a vendor claim go unquestioned and insists that every buzzword come with a proof of concept. Discussion 0 No comments yet Be the first to weigh in.