{"slug": "sandbox-untrusted-llm-generated-code-with-gvisor-and-firecracker", "title": "Sandbox Untrusted LLM-Generated Code with gVisor and Firecracker", "summary": "A new tutorial by Ji-ho Choi demonstrates how to sandbox untrusted LLM-generated code using gVisor containers and Firecracker microVMs, providing a local execution environment for AI agents. The setup includes a shell wrapper that runs model-generated Python in a gVisor-isolated container with no network and resource caps, and a Firecracker path for hardware virtualization. Verified against gVisor release-20260727.0, Firecracker v1.16.1, Docker Engine 29.7, and python:3.14-alpine.", "body_md": "# Sandbox Untrusted LLM-Generated Code with gVisor and Firecracker\n\nGive your agent a safe place to run arbitrary code: gVisor containers for speed, Firecracker microVMs for hard isolation.\n\n[Ji-ho Choi](https://sourcefeed.dev/u/jiho_choi)\n\n## What you'll build\n\nA local execution sandbox for LLM agents: a shell wrapper that runs model-generated Python inside a [gVisor](https://gvisor.dev/)-isolated container with no network and hard resource caps, plus a [Firecracker](https://firecracker-microvm.github.io/) microVM path for when you want a full hardware-virtualization boundary. Your agent pipes code in, gets stdout back, and the host never trusts a single syscall.\n\n## Prerequisites\n\n- A Linux host, x86_64 or arm64. gVisor needs kernel 4.14.77+, so any current distro works; its default\n`systrap`\n\nplatform doesn't need KVM. The Firecracker steps*do*need read/write access to`/dev/kvm`\n\n— bare metal or a cloud instance with nested virtualization. - Docker Engine and sudo access. Commands assume Ubuntu 24.04 LTS.\n`curl`\n\n,`wget`\n\n, and`squashfs-tools`\n\n(`sudo apt-get install -y squashfs-tools`\n\n) for the Firecracker rootfs step.- Verified August 2026 against gVisor release-20260727.0, Firecracker v1.16.1, Docker Engine 29.7, and the\n`python:3.14-alpine`\n\nimage.\n\n## 1. Install gVisor and register the runsc runtime\n\ngVisor is an application kernel: syscalls from your workload are intercepted and served by a userspace kernel written in Go, so untrusted code never talks to the host kernel directly. It ships as an OCI runtime called `runsc`\n\nthat plugs straight into [Docker](https://docs.docker.com/engine/).\n\n```\nsudo apt-get update && \\\nsudo apt-get install -y apt-transport-https ca-certificates curl gnupg\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\nsudo apt-get update && sudo apt-get install -y runsc\n```\n\nRegister it as a Docker runtime and restart the daemon — `runsc install`\n\nwrites the runtime entry into `/etc/docker/daemon.json`\n\nfor you:\n\n```\nsudo runsc install\nsudo systemctl restart docker\ndocker run --rm --runtime=runsc hello-world\n```\n\nIf `hello-world`\n\nprints its usual banner, the runtime is wired up.\n\n## 2. Wrap runsc in a locked-down executor\n\ngVisor removes the shared-kernel risk; Docker flags remove everything else the code doesn't need. Save this as `run-untrusted.sh`\n\n:\n\n``` bash\n#!/usr/bin/env bash\n# Reads Python source on stdin, executes it in a gVisor sandbox.\nset -euo pipefail\nexec docker run --rm -i \\\n  --runtime=runsc \\\n  --network=none \\\n  --memory=256m --cpus=0.5 --pids-limit=64 \\\n  --read-only --tmpfs /tmp:size=16m \\\n  --cap-drop=ALL --security-opt=no-new-privileges \\\n  python:3.14-alpine \\\n  timeout 10 python3 -\nchmod +x run-untrusted.sh\necho 'print(sum(range(100)))' | ./run-untrusted.sh\n```\n\nEach flag closes a hole: `--network=none`\n\ngives the sandbox only a loopback interface, so exfiltration and reverse shells are dead on arrival; `--pids-limit`\n\nkills fork bombs; `--read-only`\n\nplus a small tmpfs means nothing persists between runs; `--cap-drop=ALL`\n\nstrips capabilities even inside gVisor's kernel; `timeout 10`\n\nbounds infinite loops (the container exits non-zero and `--rm`\n\ncleans it up). Your agent calls this script per snippet — fresh container every time, nothing shared.\n\n## 3. Fetch Firecracker, a kernel, and a rootfs\n\ngVisor's boundary is a hardened userspace kernel. If your threat model wants hardware virtualization — the same line AWS Lambda draws around customer code — use Firecracker, a KVM-based VMM that boots a minimal VM in ~125 ms. First confirm KVM access, then grab the latest release binary:\n\n```\nlsmod | grep kvm\n[ -r /dev/kvm ] && [ -w /dev/kvm ] && echo \"OK\" || echo \"FAIL\"\n\nARCH=\"$(uname -m)\"\nrelease_url=\"https://github.com/firecracker-microvm/firecracker/releases\"\nlatest_version=$(basename $(curl -fsSLI -o /dev/null -w %{url_effective} ${release_url}/latest))\ncurl -L ${release_url}/download/${latest_version}/firecracker-${latest_version}-${ARCH}.tgz | tar -xz\nmv release-${latest_version}-${ARCH}/firecracker-${latest_version}-${ARCH} firecracker\n```\n\nA microVM needs a guest kernel and a root filesystem. Firecracker's CI publishes both; these commands pick the newest matching your release line:\n\n```\nCI_VERSION=${latest_version%.*}\nlatest_kernel_key=$(curl \"http://spec.ccfc.min.s3.amazonaws.com/?prefix=firecracker-ci/$CI_VERSION/$ARCH/vmlinux-&list-type=2\" \\\n  | grep -oP \"(?<=<Key>)(firecracker-ci/$CI_VERSION/$ARCH/vmlinux-[0-9]+\\.[0-9]+\\.[0-9]{1,3})(?=</Key>)\" \\\n  | sort -V | tail -1)\nwget \"https://s3.amazonaws.com/spec.ccfc.min/${latest_kernel_key}\"\nKERNEL_FILE=$(basename \"$latest_kernel_key\")\n\nlatest_ubuntu_key=$(curl \"http://spec.ccfc.min.s3.amazonaws.com/?prefix=firecracker-ci/$CI_VERSION/$ARCH/ubuntu-&list-type=2\" \\\n  | grep -oP \"(?<=<Key>)(firecracker-ci/$CI_VERSION/$ARCH/ubuntu-[0-9]+\\.[0-9]+\\.squashfs)(?=</Key>)\" \\\n  | sort -V | tail -1)\nubuntu_version=$(basename $latest_ubuntu_key .squashfs | grep -oE '[0-9]+\\.[0-9]+')\nwget -O ubuntu-$ubuntu_version.squashfs \"https://s3.amazonaws.com/spec.ccfc.min/$latest_ubuntu_key\"\n\nunsquashfs ubuntu-$ubuntu_version.squashfs\nsudo chown -R root:root squashfs-root\ntruncate -s 1G ubuntu-$ubuntu_version.ext4\nsudo mkfs.ext4 -d squashfs-root -F ubuntu-$ubuntu_version.ext4\nROOTFS_FILE=ubuntu-$ubuntu_version.ext4\n```\n\nThe squashfs is unpacked and rebuilt as ext4 because Firecracker drives are block devices. This is also your hook for customization: drop your language runtime and executor into `squashfs-root/`\n\nbefore the `mkfs.ext4`\n\nline.\n\n## 4. Boot the microVM with no NIC attached\n\nFirecracker takes a JSON config at startup. Note what's missing: no `network-interfaces`\n\nsection means the guest has no network device at all — stronger than a firewall rule, because there's nothing to misconfigure.\n\n```\ncat > vm_config.json <<EOF\n{\n  \"boot-source\": {\n    \"kernel_image_path\": \"${KERNEL_FILE}\",\n    \"boot_args\": \"console=ttyS0 reboot=k panic=1\"\n  },\n  \"drives\": [\n    {\n      \"drive_id\": \"rootfs\",\n      \"path_on_host\": \"${ROOTFS_FILE}\",\n      \"is_root_device\": true,\n      \"is_read_only\": false\n    }\n  ],\n  \"machine-config\": {\n    \"vcpu_count\": 1,\n    \"mem_size_mib\": 512\n  }\n}\nEOF\n\nsudo rm -f /tmp/firecracker.socket\nsudo ./firecracker --api-sock /tmp/firecracker.socket --config-file vm_config.json\n```\n\nYour terminal becomes the guest's serial console. Log in with `root`\n\n/ `root`\n\n, run whatever hostile code you like, then type `reboot`\n\n— Firecracker treats a guest reboot as shutdown and exits. For repeated runs, keep the ext4 image pristine and copy it per execution.\n\n## Verify it works\n\nConfirm code actually runs under gVisor — its fake `dmesg`\n\nis unmistakable:\n\n``` bash\n$ docker run --rm --runtime=runsc python:3.14-alpine dmesg | head -n 1\n[    0.000000] Starting gVisor...\n```\n\nConfirm the executor computes but can't reach the outside world:\n\n``` python\n$ echo 'print(sum(range(100)))' | ./run-untrusted.sh\n4950\n$ echo 'import socket; socket.create_connection((\"1.1.1.1\", 443), timeout=3)' | ./run-untrusted.sh\nTraceback (most recent call last):\n  ...\nOSError: [Errno 101] Network is unreachable\n```\n\nFor Firecracker, the boot log should scroll to an Ubuntu login prompt on `ttyS0`\n\nin about a second. After logging in as `root`\n\n, confirm the guest runs its own kernel and has no network interface beyond loopback:\n\n```\n# uname -r\n6.1.141\n# ip link\n1: lo: <LOOPBACK> mtu 65536 ...\n```\n\n## Troubleshooting\n\n** docker: Error response from daemon: unknown or invalid runtime name: runsc** — Docker doesn't know about the runtime yet. Run\n\n`sudo runsc install`\n\n, then `sudo systemctl restart docker`\n\n(a reload isn't enough for runtime changes).** panic: unable to attach: operation not permitted or fork/exec /proc/self/exe: invalid argument** — the\n\n`runsc`\n\nbinary isn't readable/executable by the container user. This bites manual installs; fix with `sudo chmod a+rx /usr/local/bin/runsc`\n\n(the apt package sets permissions correctly).**KVM check prints FAIL** — your user can't open\n\n`/dev/kvm`\n\n. Grant access with `sudo setfacl -m u:${USER}:rw /dev/kvm`\n\nor add yourself to the `kvm`\n\ngroup and re-login. On cloud VMs, `/dev/kvm`\n\nmissing entirely means no nested virtualization — pick a bare-metal instance type or enable nested virt; gVisor's systrap path works either way.**Firecracker starts but the console stays blank** — your `boot_args`\n\nare missing `console=ttyS0`\n\n, so the kernel is booting silently with output going nowhere. Add it and restart.\n\n## Next steps\n\n- In production, never run\n`firecracker`\n\nbare: wrap it in the project's[jailer](https://github.com/firecracker-microvm/firecracker/blob/main/docs/jailer.md), which chroots and drops privileges before the VMM starts. - Cut microVM cold starts to milliseconds with Firecracker\n[snapshots](https://github.com/firecracker-microvm/firecracker/blob/main/docs/snapshotting/snapshot-support.md)— boot once, snapshot after the interpreter loads, restore per request. - If sandboxed code needs\n*some*egress (pip installs, API calls), don't hand it a NIC — proxy through a host-side allowlist, or in gVisor keep`--network=none`\n\nand mount vetted wheels read-only. - Read gVisor's\n[production guide](https://gvisor.dev/docs/user_guide/production/)for platform tuning (`systrap`\n\nvs KVM) and its own per-sandbox resource controls.\n\n## Sources & further reading\n\n-\n[Installation - gVisor](https://gvisor.dev/docs/user_guide/install/)— gvisor.dev -\n[Docker Quick Start - gVisor](https://gvisor.dev/docs/user_guide/quick_start/docker/)— gvisor.dev -\n[FAQ - gVisor](https://gvisor.dev/docs/user_guide/faq/)— gvisor.dev -\n[Getting Started with Firecracker](https://github.com/firecracker-microvm/firecracker/blob/v1.16.1/docs/getting-started.md)— github.com -\n[Firecracker v1.16.1 Release](https://github.com/firecracker-microvm/firecracker/releases/tag/v1.16.1)— github.com -\n[Docker Engine version 29 release notes](https://docs.docker.com/engine/release-notes/29/)— docs.docker.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-untrusted-llm-generated-code-with-gvisor-and-firecracker", "canonical_source": "https://sourcefeed.dev/a/sandbox-untrusted-llm-generated-code-with-gvisor-and-firecracker", "published_at": "2026-08-04 17:43:47+00:00", "updated_at": "2026-08-04 18:28:03.017139+00:00", "lang": "en", "topics": ["ai-safety", "ai-agents", "ai-infrastructure", "developer-tools"], "entities": ["Ji-ho Choi", "gVisor", "Firecracker", "Docker", "AWS Lambda", "runsc"], "alternates": {"html": "https://wpnews.pro/news/sandbox-untrusted-llm-generated-code-with-gvisor-and-firecracker", "markdown": "https://wpnews.pro/news/sandbox-untrusted-llm-generated-code-with-gvisor-and-firecracker.md", "text": "https://wpnews.pro/news/sandbox-untrusted-llm-generated-code-with-gvisor-and-firecracker.txt", "jsonld": "https://wpnews.pro/news/sandbox-untrusted-llm-generated-code-with-gvisor-and-firecracker.jsonld"}}