# Sandbox Untrusted LLM-Generated Code with gVisor and Firecracker

> Source: <https://sourcefeed.dev/a/sandbox-untrusted-llm-generated-code-with-gvisor-and-firecracker>
> Published: 2026-08-04 17:43:47+00:00

# Sandbox Untrusted LLM-Generated Code with gVisor and Firecracker

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

[Ji-ho Choi](https://sourcefeed.dev/u/jiho_choi)

## What you'll build

A 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.

## 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 steps*do*need 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`

, and`squashfs-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](https://docs.docker.com/engine/).

```
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`

:

``` bash
#!/usr/bin/env bash
# Reads Python source on stdin, executes it in a gVisor sandbox.
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:

``` bash
$ 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:

``` python
$ 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:

```
# uname -r
6.1.141
# ip link
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'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
[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
*some*egress (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 guide](https://gvisor.dev/docs/user_guide/production/)for platform tuning (`systrap`

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

## Sources & further reading

-
[Installation - gVisor](https://gvisor.dev/docs/user_guide/install/)— gvisor.dev -
[Docker Quick Start - gVisor](https://gvisor.dev/docs/user_guide/quick_start/docker/)— gvisor.dev -
[FAQ - gVisor](https://gvisor.dev/docs/user_guide/faq/)— gvisor.dev -
[Getting Started with Firecracker](https://github.com/firecracker-microvm/firecracker/blob/v1.16.1/docs/getting-started.md)— github.com -
[Firecracker v1.16.1 Release](https://github.com/firecracker-microvm/firecracker/releases/tag/v1.16.1)— github.com -
[Docker Engine version 29 release notes](https://docs.docker.com/engine/release-notes/29/)— docs.docker.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.
