{"slug": "sandbox-untrusted-ai-generated-code-with-gvisor", "title": "Sandbox Untrusted AI-Generated Code with gVisor", "summary": "Emeka Okafor published a guide for sandboxing untrusted AI-generated Python code using gVisor's user-space kernel, demonstrating a Docker-based setup with no network, no capabilities, and hard CPU, memory, and process limits. The tutorial, verified against gVisor release-20260831.0 on Ubuntu 24.04 with Docker Engine 29.8.0, walks through installing the runsc runtime, registering it with Docker, and running code with flags like --network=none and --cap-drop=ALL to isolate malicious snippets from the host.", "body_md": "# Sandbox Untrusted AI-Generated Code with gVisor\n\nRun LLM-generated Python behind gVisor's user-space kernel with no network, no capabilities, and hard limits.\n\n[Emeka Okafor](https://sourcefeed.dev/u/emeka_okafor)\n\n## What you'll build\n\nA local execution sandbox your agent can call to run LLM-generated Python. Snippets execute inside a Docker container whose \"kernel\" is [gVisor](https://gvisor.dev/)'s user-space sentry, so malicious code talks to an emulated Linux instead of your host, with no network, no capabilities, and hard CPU, memory, and process caps.\n\n``` php\nflowchart LR\n    A[Agent produces code] --> B[sandbox_run.py]\n    B --> C[docker run --runtime=runsc]\n    C --> D[gVisor sentry<br/>user-space kernel]\n    D -->|narrow filtered syscall set| E[Host kernel]\n```\n\n## Prerequisites\n\n- A Linux host, x86_64 or ARM64, kernel 5.6 or newer. gVisor intercepts Linux syscalls, so macOS and Windows are out; use a Linux box, VM, or cloud instance.\n- [Docker Engine](https://docs.docker.com/engine/) managed by systemd. Verified against Docker Engine 29.8.0.\n- Root or sudo access.\n\nCommands below were verified against gVisor `release-20260831.0` (August 2026) on Ubuntu 24.04. gVisor ships a new release every couple of weeks; the steps don't change between releases, only the version string.\n\n## 1. Install the runsc runtime\n\n`runsc` is gVisor's OCI runtime binary, a drop-in replacement for runc. On Debian or Ubuntu, install it from Google's apt repository:\n\n```\nsudo apt-get update && \\\nsudo apt-get install -y 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\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 other distros, download the signed release tarball instead; the [install guide](https://gvisor.dev/docs/user_guide/install/) has the exact commands. Confirm the binary works:\n\n``` bash\n$ runsc --version\nrunsc version release-20260831.0\nspec: 1.2.1\n```\n\n## 2. Register runsc as a Docker runtime\n\n`runsc install` adds a `runsc` entry to the `runtimes` section of `/etc/docker/daemon.json`. Docker only reads that file at startup, so restart the daemon after:\n\n```\nsudo runsc install\nsudo systemctl restart docker\n```\n\nCheck that Docker sees it:\n\n``` bash\n$ docker info | grep -i runtimes\n Runtimes: io.containerd.runc.v2 runc runsc\n```\n\nSmoke test:\n\n```\ndocker run --runtime=runsc --rm hello-world\n```\n\nIf that prints the usual Docker greeting, every syscall it made went through gVisor's sentry, not your kernel.\n\n## 3. Lock down the run command\n\ngVisor covers the syscall layer. Everything else, network, filesystem, resources, and identity, you lock down with plain Docker flags. Run this once to confirm the full stack works and to pull the image:\n\n```\ndocker pull python:3.13-slim\n\ndocker run --rm -i --runtime=runsc \\\n  --network=none \\\n  --cap-drop=ALL \\\n  --security-opt=no-new-privileges \\\n  --read-only --tmpfs /tmp:size=64m \\\n  --memory=256m --cpus=0.5 --pids-limit=128 \\\n  --user 65534:65534 \\\n  python:3.13-slim python3 -c 'print(\"sandboxed\")'\n```\n\nWhat each layer buys you: `--network=none` kills exfiltration and command-and-control outright, `--cap-drop=ALL` and `no-new-privileges` remove privilege escalation paths inside the sandbox, `--read-only` plus a small tmpfs means generated code can scribble in `/tmp` but can't persist anything, the resource caps stop fork bombs and memory balloons, and `--user 65534:65534` (nobody) means even reads of root-owned files fail. None of these flags alone would justify running untrusted code. Stacked on a user-space kernel, the remaining attack surface is the narrow, seccomp-filtered set of host syscalls the sentry itself uses, rather than the hundreds a normal container can reach.\n\n## 4. Wrap it in a runner your agent can call\n\nSave this as `sandbox_run.py`:\n\n``` python\nimport subprocess\nimport sys\nimport uuid\n\nIMAGE = \"python:3.13-slim\"\nTIMEOUT_SECS = 10\n\ndef run_untrusted(code: str) -> subprocess.CompletedProcess:\n    name = f\"sbx-{uuid.uuid4().hex[:12]}\"\n    cmd = [\n        \"docker\", \"run\", \"--rm\", \"-i\", \"--name\", name,\n        \"--runtime=runsc\",\n        \"--network=none\",\n        \"--cap-drop=ALL\",\n        \"--security-opt=no-new-privileges\",\n        \"--read-only\", \"--tmpfs\", \"/tmp:size=64m\",\n        \"--memory=256m\", \"--cpus=0.5\", \"--pids-limit=128\",\n        \"--user\", \"65534:65534\",\n        IMAGE, \"python3\", \"-\",\n    ]\n    try:\n        return subprocess.run(cmd, input=code, text=True,\n                              capture_output=True, timeout=TIMEOUT_SECS)\n    except subprocess.TimeoutExpired:\n        # Killing the docker CLI doesn't kill the container; do it by name.\n        subprocess.run([\"docker\", \"kill\", name], capture_output=True)\n        raise\n\nif __name__ == \"__main__\":\n    result = run_untrusted(sys.stdin.read())\n    print(result.stdout, end=\"\")\n    print(result.stderr, file=sys.stderr, end=\"\")\n    sys.exit(result.returncode)\n```\n\nTwo deliberate choices here. The code goes to `python3 -` over stdin, which keeps it out of argv (visible to anyone running `ps`) and out of shell-quoting trouble. And the timeout handler kills the container by name, because `subprocess` only kills the local `docker` client while the container keeps running on the daemon.\n\nFrom your agent, import `run_untrusted()` and hand back `stdout`, `stderr`, and `returncode` to the model.\n\n## Verify it works\n\nFeed the runner the kind of code you're actually worried about:\n\n``` python\ncat <<'EOF' | python3 sandbox_run.py\nimport os, urllib.request\nprint(\"kernel:\", os.uname().release)\ntry:\n    open(\"/etc/shadow\").read()\nexcept OSError as e:\n    print(\"shadow read blocked:\", e)\ntry:\n    urllib.request.urlopen(\"https://example.com\", timeout=3)\nexcept Exception as e:\n    print(\"network blocked:\", type(e).__name__)\nEOF\n```\n\nExpected output:\n\n```\nkernel: 4.19.0-gvisor\nshadow read blocked: [Errno 13] Permission denied: '/etc/shadow'\nnetwork blocked: URLError\n```\n\nThat first line is the proof. `4.19.0-gvisor` is the release string gVisor's sentry advertises through its emulated `uname(2)`; your host kernel is something else entirely (` uname -r` on the host to compare). The code never spoke to the real kernel. A container escape now requires a gVisor sandbox escape *and* a host kernel exploit, chained.\n\n## Troubleshooting\n\n- **`docker: Error response from daemon: unknown or invalid runtime name: runsc`** : Docker doesn't know about the runtime. Run` sudo runsc install` , then`sudo systemctl restart docker` . The restart is the step people skip.\n- **`flag provided but not defined: -console`** : your Docker Engine predates the current OCI runtime interface. Upgrade Docker to a current version.\n- **`fork/exec /proc/self/exe: invalid argument`** (or a runsc panic containing` unable to attach: operation not permitted` ) when starting containers: the`runsc` binary isn't readable and executable by all users. Fix with`sudo chmod a+rx $(which runsc)` .\n- **`bad address 'somehost'`** if you later enable networking: Docker's embedded DNS for user-defined bridges listens on the host loopback, which gVisor's network stack isolates. Use the default bridge, connect by IP, or better, keep`--network=none` and pass data in through stdin.\n\n## Next steps\n\nBatch-shaped workloads fit this runner as-is; for a persistent service, cap captured output size (a hostile snippet can print gigabytes) and queue executions. On bare metal or nested-virt-enabled VMs, try gVisor's KVM platform instead of the default Systrap; the [platforms guide](https://gvisor.dev/docs/architecture_guide/platforms/) covers the tradeoffs, and the [production guide](https://gvisor.dev/docs/user_guide/production/) covers tuning at scale. Running agents on Kubernetes? gVisor plugs into [containerd](https://gvisor.dev/docs/user_guide/containerd/quick_start/) as a `RuntimeClass`, so pods opt in per-workload. And before you trust the sandbox with anything hotter, read gVisor's [security model](https://gvisor.dev/docs/architecture_guide/security/) to understand exactly what it does and doesn't defend against.\n\n## Sources & further reading\n\n1. \n                                    [Installation](https://gvisor.dev/docs/user_guide/install/)\n                                — gvisor.dev\n2. \n                                    [Docker Quick Start](https://gvisor.dev/docs/user_guide/quick_start/docker/)\n                                — gvisor.dev\n3. \n                                    [FAQ](https://gvisor.dev/docs/user_guide/faq/)\n                                — gvisor.dev\n4. \n                                    [Security Model](https://gvisor.dev/docs/architecture_guide/security/)\n                                — gvisor.dev\n5. \n                                    [gVisor Releases](https://github.com/google/gvisor/releases)\n                                — github.com\n6. \n                                    [Docker Engine 29 release notes](https://docs.docker.com/engine/release-notes/29/)\n                                — docs.docker.com\n\n[Emeka Okafor](https://sourcefeed.dev/u/emeka_okafor)· Security Editor\n\nEmeka has spent over a decade tracking threat actors, vulnerability disclosures, and the evolving landscape of application security, bringing a sharp continent-spanning perspective to his reporting. He's known for translating dense CVE advisories into clear, actionable context that developers and security teams alike actually read.\n\n## Discussion 2\n\nthe syscall filtering is what matters here—ran into this exact problem last year with a code execution feature in an OSS project and ended up going down the seccomp rabbit hole. gvisor's appeal is you get that filtering + resource limits without hand-rolling a syscall allowlist, which is where most projects get sloppy. having the sentry handle it means fewer places for things to slip through.\n\nfinally, a practical way to not get pwned by your own ai code. gvisor overhead is real but beats the alternative of ransomware in prod at 3am.", "url": "https://wpnews.pro/news/sandbox-untrusted-ai-generated-code-with-gvisor", "canonical_source": "https://sourcefeed.dev/a/sandbox-untrusted-ai-generated-code-with-gvisor", "published_at": "2026-09-06 17:40:52+00:00", "updated_at": "2026-09-07 01:59:54.416270+00:00", "lang": "en", "topics": ["ai-safety", "ai-tools", "ai-infrastructure"], "entities": ["Emeka Okafor", "gVisor", "Docker", "runsc", "Ubuntu 24.04"], "alternates": {"html": "https://wpnews.pro/news/sandbox-untrusted-ai-generated-code-with-gvisor", "markdown": "https://wpnews.pro/news/sandbox-untrusted-ai-generated-code-with-gvisor.md", "text": "https://wpnews.pro/news/sandbox-untrusted-ai-generated-code-with-gvisor.txt", "jsonld": "https://wpnews.pro/news/sandbox-untrusted-ai-generated-code-with-gvisor.jsonld"}}