{"slug": "sandbox-llm-generated-code-with-gvisor-and-docker", "title": "Sandbox LLM-Generated Code with gVisor and Docker", "summary": "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.", "body_md": "# Sandbox LLM-Generated Code with gVisor and Docker\n\nRun untrusted agent-generated code behind gVisor's user-space kernel so hostile scripts never touch your host kernel.\n\n[Ji-ho Choi](https://sourcefeed.dev/u/jiho_choi)\n\n## What you'll build\n\nAn 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.\n\nThe 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.\n\n## Prerequisites\n\n- 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.\n- Docker Engine installed and managed by systemd. The `--runtime` flag has been stable for years, so any recent version works.\n- Root or sudo access.\n- 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.\n\n## Step 1: Install gVisor\n\nOn Debian or Ubuntu, add Google's apt repository and install `runsc`:\n\n```\nsudo apt-get update && sudo apt-get install -y \\\n  apt-transport-https ca-certificates curl gnupg\n\ncurl -fsSL https://gvisor.dev/archive.key | \\\n  sudo gpg --dearmor -o /usr/share/keyrings/gvisor-archive-keyring.gpg\n\necho \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/gvisor-archive-keyring.gpg] https://storage.googleapis.com/gvisor/releases release main\" | \\\n  sudo tee /etc/apt/sources.list.d/gvisor.list > /dev/null\n\nsudo apt-get update && sudo apt-get install -y runsc\n```\n\nOn any other distro, grab the latest release binary bundle directly:\n\n```\n(\n  set -e\n  ARCH=$(uname -m)\n  URL=https://storage.googleapis.com/gvisor/releases/release/latest/${ARCH}\n  wget ${URL}/gvisor.tar.zstd ${URL}/gvisor.tar.zstd.sha512\n  sha512sum -c gvisor.tar.zstd.sha512\n  sudo tar --zstd -xf gvisor.tar.zstd -C /usr/local/bin\n  rm -f gvisor.tar.zstd gvisor.tar.zstd.sha512\n)\n```\n\n## Step 2: Register runsc with Docker\n\n`runsc` ships a helper that writes the runtime entry into `/etc/docker/daemon.json` for you:\n\n```\nsudo runsc install\nsudo systemctl restart docker\n```\n\nYour `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:\n\n```\ndocker info | grep -i runtimes\n```\n\nYou should see `runsc` listed alongside `runc`. The default runtime is still `runc`, which is what you want: only the untrusted workloads opt in.\n\n## Step 3: Build the runner image\n\nKeep 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`:\n\n```\nFROM python:3.13-slim\nRUN useradd --create-home runner\nUSER runner\nWORKDIR /home/runner\n```\n\nBuild it:\n\n```\ndocker build -t llm-sandbox .\n```\n\n## Step 4: Run generated code under the sandbox\n\nSimulate agent output. In practice your agent framework writes this file; here we fake it with something that reports where it's running:\n\n``` python\nmkdir -p untrusted\ncat > untrusted/agent_code.py <<'EOF'\nimport platform, sys\nprint(\"python\", sys.version.split()[0])\nprint(\"kernel\", platform.release())\nprint(\"result\", sum(i * i for i in range(10)))\nEOF\n```\n\nNow 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`:\n\n```\ndocker run --rm \\\n  --runtime=runsc \\\n  --network=none \\\n  --cap-drop=ALL \\\n  --security-opt=no-new-privileges \\\n  --read-only \\\n  --tmpfs /tmp:rw,noexec,nosuid,size=64m \\\n  --memory=256m --cpus=1 --pids-limit=128 \\\n  -v \"$PWD/untrusted:/home/runner/job:ro\" \\\n  llm-sandbox \\\n  python /home/runner/job/agent_code.py\n```\n\nWhat each layer buys you:\n\n- `--runtime=runsc` : syscalls hit gVisor's Sentry, not your host kernel.\n- `--network=none` : generated code can't exfiltrate data or pull payloads.\n- `--cap-drop=ALL` and`--security-opt=no-new-privileges` : no capabilities, and no way to gain any via setuid binaries.\n- `--read-only` plus a small`noexec` tmpfs: the script gets scratch space but can't persist anything or drop executables.\n- `--memory` ,`--cpus` ,`--pids-limit` : an infinite loop or fork bomb burns its own budget, not your machine.\n- The code mounts in read-only; results come back on stdout.\n\n## Verify it works\n\nFirst, prove the sandbox is real. gVisor boots its own tiny kernel, and its `dmesg` output is unmistakable:\n\n```\ndocker run --runtime=runsc --rm llm-sandbox dmesg\n```\n\nExpected output (the middle lines are randomized jokes; the first and last are what matter):\n\n```\n[    0.000000] Starting gVisor...\n...\n[    2.613217] Ready!\n```\n\nSecond, compare kernel identities with and without gVisor:\n\n```\ndocker run --rm llm-sandbox uname -r\ndocker run --rm --runtime=runsc llm-sandbox uname -r\n```\n\nThe 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.\n\nThird, the full sandboxed run from step 4 should print:\n\n```\npython 3.13.7\nkernel 4.4.0\nresult 285\n```\n\nYour Python patch version may differ. If `kernel` shows your host release, you forgot `--runtime=runsc`.\n\n## Troubleshooting\n\n**`docker: Error response from daemon: unknown or invalid runtime name: runsc`**\nDocker 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.\n\n**`panic: unable to attach: operation not permitted` or `fork/exec /proc/self/exe: invalid argument`**\nThe `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.\n\n**`SELinux is not supported: system_u:system_r:container_t:s0...`**\nOn 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.\n\n**Container works under runc but fails under runsc**\ngVisor 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.\n\n## Next steps\n\n- 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.\n- 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.\n- 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.\n- 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.\n\n## Sources & further reading\n\n1. \n                                    [Installation - gVisor](https://gvisor.dev/docs/user_guide/install/)\n                                — gvisor.dev\n2. \n                                    [Docker Quick Start - gVisor](https://gvisor.dev/docs/user_guide/quick_start/docker/)\n                                — gvisor.dev\n3. \n                                    [Platform Guide - gVisor](https://gvisor.dev/docs/architecture_guide/platforms/)\n                                — gvisor.dev\n4. \n                                    [FAQ - gVisor](https://gvisor.dev/docs/user_guide/faq/)\n                                — gvisor.dev\n5. \n                                    [gVisor Releases](https://github.com/google/gvisor/releases)\n                                — github.com\n\n[Ji-ho Choi](https://sourcefeed.dev/u/jiho_choi)· Security & Cloud Editor\n\nJi-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.\n\n## Discussion 0\n\nNo comments yet\n\nBe the first to weigh in.", "url": "https://wpnews.pro/news/sandbox-llm-generated-code-with-gvisor-and-docker", "canonical_source": "https://sourcefeed.dev/a/sandbox-llm-generated-code-with-gvisor-and-docker", "published_at": "2026-09-09 17:39:21+00:00", "updated_at": "2026-09-09 17:55:26.131493+00:00", "lang": "en", "topics": ["ai-safety", "ai-agents", "ai-infrastructure", "developer-tools"], "entities": ["gVisor", "Docker", "runsc", "Ji-ho Choi", "Sourcefeed", "Ubuntu 24.04"], "alternates": {"html": "https://wpnews.pro/news/sandbox-llm-generated-code-with-gvisor-and-docker", "markdown": "https://wpnews.pro/news/sandbox-llm-generated-code-with-gvisor-and-docker.md", "text": "https://wpnews.pro/news/sandbox-llm-generated-code-with-gvisor-and-docker.txt", "jsonld": "https://wpnews.pro/news/sandbox-llm-generated-code-with-gvisor-and-docker.jsonld"}}