cd /news/ai-safety/sandbox-untrusted-llm-generated-code… · home topics ai-safety article
[ARTICLE · art-86543] src=sourcefeed.dev ↗ pub= topic=ai-safety verified=true sentiment=· neutral

Sandbox Untrusted LLM-Generated Code with gVisor and Firecracker

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.

read7 min views1 publishedAug 4, 2026
Sandbox Untrusted LLM-Generated Code with gVisor and Firecracker
Image: Sourcefeed (auto-discovered)

Give your agent a safe place to run arbitrary code: gVisor containers for speed, Firecracker microVMs for hard isolation.

Ji-ho Choi

What you'll build #

A local execution sandbox for LLM agents: a shell wrapper that runs model-generated Python inside a gVisor-isolated container with no network and hard resource caps, plus a Firecracker 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.

Prerequisites #

  • A Linux host, x86_64 or arm64. gVisor needs kernel 4.14.77+, so any current distro works; its default systrap

platform doesn't need KVM. The Firecracker stepsdoneed read/write access to/dev/kvm

— bare metal or a cloud instance with nested virtualization. - Docker Engine and sudo access. Commands assume Ubuntu 24.04 LTS. curl

,wget

, andsquashfs-tools

(sudo apt-get install -y squashfs-tools

) for the Firecracker rootfs step.- Verified August 2026 against gVisor release-20260727.0, Firecracker v1.16.1, Docker Engine 29.7, and the python:3.14-alpine

image.

1. Install gVisor and register the runsc runtime #

gVisor 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

that plugs straight into Docker.

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

Register it as a Docker runtime and restart the daemon — runsc install

writes the runtime entry into /etc/docker/daemon.json

for you:

sudo runsc install
sudo systemctl restart docker
docker run --rm --runtime=runsc hello-world

If hello-world

prints its usual banner, the runtime is wired up.

2. Wrap runsc in a locked-down executor #

gVisor removes the shared-kernel risk; Docker flags remove everything else the code doesn't need. Save this as run-untrusted.sh

:

#!/usr/bin/env bash
set -euo pipefail
exec docker run --rm -i \
  --runtime=runsc \
  --network=none \
  --memory=256m --cpus=0.5 --pids-limit=64 \
  --read-only --tmpfs /tmp:size=16m \
  --cap-drop=ALL --security-opt=no-new-privileges \
  python:3.14-alpine \
  timeout 10 python3 -
chmod +x run-untrusted.sh
echo 'print(sum(range(100)))' | ./run-untrusted.sh

Each flag closes a hole: --network=none

gives the sandbox only a loopback interface, so exfiltration and reverse shells are dead on arrival; --pids-limit

kills fork bombs; --read-only

plus a small tmpfs means nothing persists between runs; --cap-drop=ALL

strips capabilities even inside gVisor's kernel; timeout 10

bounds infinite loops (the container exits non-zero and --rm

cleans it up). Your agent calls this script per snippet — fresh container every time, nothing shared.

3. Fetch Firecracker, a kernel, and a rootfs #

gVisor'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:

lsmod | grep kvm
[ -r /dev/kvm ] && [ -w /dev/kvm ] && echo "OK" || echo "FAIL"

ARCH="$(uname -m)"
release_url="https://github.com/firecracker-microvm/firecracker/releases"
latest_version=$(basename $(curl -fsSLI -o /dev/null -w %{url_effective} ${release_url}/latest))
curl -L ${release_url}/download/${latest_version}/firecracker-${latest_version}-${ARCH}.tgz | tar -xz
mv release-${latest_version}-${ARCH}/firecracker-${latest_version}-${ARCH} firecracker

A microVM needs a guest kernel and a root filesystem. Firecracker's CI publishes both; these commands pick the newest matching your release line:

CI_VERSION=${latest_version%.*}
latest_kernel_key=$(curl "http://spec.ccfc.min.s3.amazonaws.com/?prefix=firecracker-ci/$CI_VERSION/$ARCH/vmlinux-&list-type=2" \
  | grep -oP "(?<=<Key>)(firecracker-ci/$CI_VERSION/$ARCH/vmlinux-[0-9]+\.[0-9]+\.[0-9]{1,3})(?=</Key>)" \
  | sort -V | tail -1)
wget "https://s3.amazonaws.com/spec.ccfc.min/${latest_kernel_key}"
KERNEL_FILE=$(basename "$latest_kernel_key")

latest_ubuntu_key=$(curl "http://spec.ccfc.min.s3.amazonaws.com/?prefix=firecracker-ci/$CI_VERSION/$ARCH/ubuntu-&list-type=2" \
  | grep -oP "(?<=<Key>)(firecracker-ci/$CI_VERSION/$ARCH/ubuntu-[0-9]+\.[0-9]+\.squashfs)(?=</Key>)" \
  | sort -V | tail -1)
ubuntu_version=$(basename $latest_ubuntu_key .squashfs | grep -oE '[0-9]+\.[0-9]+')
wget -O ubuntu-$ubuntu_version.squashfs "https://s3.amazonaws.com/spec.ccfc.min/$latest_ubuntu_key"

unsquashfs ubuntu-$ubuntu_version.squashfs
sudo chown -R root:root squashfs-root
truncate -s 1G ubuntu-$ubuntu_version.ext4
sudo mkfs.ext4 -d squashfs-root -F ubuntu-$ubuntu_version.ext4
ROOTFS_FILE=ubuntu-$ubuntu_version.ext4

The 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/

before the mkfs.ext4

line.

4. Boot the microVM with no NIC attached #

Firecracker takes a JSON config at startup. Note what's missing: no network-interfaces

section means the guest has no network device at all — stronger than a firewall rule, because there's nothing to misconfigure.

cat > vm_config.json <<EOF
{
  "boot-source": {
    "kernel_image_path": "${KERNEL_FILE}",
    "boot_args": "console=ttyS0 reboot=k panic=1"
  },
  "drives": [
    {
      "drive_id": "rootfs",
      "path_on_host": "${ROOTFS_FILE}",
      "is_root_device": true,
      "is_read_only": false
    }
  ],
  "machine-config": {
    "vcpu_count": 1,
    "mem_size_mib": 512
  }
}
EOF

sudo rm -f /tmp/firecracker.socket
sudo ./firecracker --api-sock /tmp/firecracker.socket --config-file vm_config.json

Your terminal becomes the guest's serial console. Log in with root

/ root

, run whatever hostile code you like, then type reboot

— Firecracker treats a guest reboot as shutdown and exits. For repeated runs, keep the ext4 image pristine and copy it per execution.

Verify it works #

Confirm code actually runs under gVisor — its fake dmesg

is unmistakable:

$ docker run --rm --runtime=runsc python:3.14-alpine dmesg | head -n 1
[    0.000000] Starting gVisor...

Confirm the executor computes but can't reach the outside world:

$ echo 'print(sum(range(100)))' | ./run-untrusted.sh
4950
$ echo 'import socket; socket.create_connection(("1.1.1.1", 443), timeout=3)' | ./run-untrusted.sh
Traceback (most recent call last):
  ...
OSError: [Errno 101] Network is unreachable

For Firecracker, the boot log should scroll to an Ubuntu login prompt on ttyS0

in about a second. After logging in as root

, confirm the guest runs its own kernel and has no network interface beyond loopback:

6.1.141
1: lo: <LOOPBACK> mtu 65536 ...

Troubleshooting #

** docker: Error response from daemon: unknown or invalid runtime name: runsc** — Docker doesn't know about the runtime yet. Run

sudo runsc install

, then sudo systemctl restart docker

(a reload isn't enough for runtime changes).** panic: unable to attach: operation not permitted or fork/exec /proc/self/exe: invalid argument** — the

runsc

binary isn't readable/executable by the container user. This bites manual installs; fix with sudo chmod a+rx /usr/local/bin/runsc

(the apt package sets permissions correctly).KVM check prints FAIL — your user can't open

/dev/kvm

. Grant access with sudo setfacl -m u:${USER}:rw /dev/kvm

or add yourself to the kvm

group and re-login. On cloud VMs, /dev/kvm

missing 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

are missing console=ttyS0

, so the kernel is booting silently with output going nowhere. Add it and restart.

Next steps #

  • In production, never run firecracker

bare: wrap it in the project'sjailer, which chroots and drops privileges before the VMM starts. - Cut microVM cold starts to milliseconds with Firecracker snapshots— boot once, snapshot after the interpreter loads, restore per request. - If sandboxed code needs someegress (pip installs, API calls), don't hand it a NIC — proxy through a host-side allowlist, or in gVisor keep--network=none

and mount vetted wheels read-only. - Read gVisor's production guidefor platform tuning (systrap

vs KVM) and its own per-sandbox resource controls.

Sources & further reading #

Installation - gVisor— gvisor.dev - Docker Quick Start - gVisor— gvisor.dev - FAQ - gVisor— gvisor.dev - Getting Started with Firecracker— github.com - Firecracker v1.16.1 Release— github.com - Docker Engine version 29 release notes— docs.docker.com

Ji-ho 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.

── more in #ai-safety 4 stories · sorted by recency
── more on @ji-ho choi 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/sandbox-untrusted-ll…] indexed:0 read:7min 2026-08-04 ·