mech.app

The mech.app newsletter

Agentic AI, minus the noise.

Get practical field notes on AI agents, automation, developer tools and security delivered to your inbox.

No spam. Unsubscribe anytime.

AI Agents

smolvm's Nested Virtualization Problem: Why Agent Code Sandboxes Can't Always Run Where You Need Them

How KVM-based agent sandboxes hit hard infrastructure limits in cloud environments, and why Claude Fable pivoted to GitHub Actions runners.

Source: simonwillison.net
smolvm's Nested Virtualization Problem: Why Agent Code Sandboxes Can't Always Run Where You Need Them

Simon Willison just hit a deployment wall that every agent developer will recognize: the sandbox that works perfectly on your laptop fails silently in production because the cloud environment doesn’t expose /dev/kvm. His research into smolvm as an untrusted code execution layer for agent-generated Python and JavaScript exposed the nested virtualization gap between local dev and hosted runtimes.

The interesting part is not the failure. It’s how Claude Fable autonomously detected the constraint, pivoted to GitHub Actions runners (which do expose KVM), and completed the test battery without human intervention.

The Sandbox Requirements

Willison’s goal was straightforward: execute user-provided data transformation tasks with hard resource limits, no network access, and scoped filesystem permissions. The threat model assumes malicious or buggy code (infinite loops, memory bombs, filesystem traversal).

smolvm promises:

  • Hardware-isolated VMs instead of shared-kernel containers
  • Offline local images (no registry pulls during execution)
  • CPU and RAM limits enforced at the hypervisor layer
  • Guest-enforced timeouts
  • Storage quotas
  • Read-only input mounts and writable output mounts
  • --unprivileged mode for non-root execution

Cold starts clocked in at 0.6 to 1.5 seconds. Warm executions hit 50 milliseconds. For agent-generated code that runs once or twice per user request, that latency is acceptable.

Why smolvm Requires /dev/kvm

smolvm is built on KVM (Kernel-based Virtual Machine), which requires:

  1. /dev/kvm device node
  2. CPU virtualization extensions (Intel VT-x or AMD-V, exposed as vmx or svm flags in /proc/cpuinfo)
  3. Nested virtualization support if the host is already a VM

When you run smolvm machine run, it spawns a lightweight VM using QEMU with KVM acceleration. Without KVM, QEMU falls back to software emulation, which is 10x to 100x slower and unsuitable for interactive agent workloads.

The Claude Code for web environment runs on Firecracker, Amazon’s microVM manager. Firecracker itself is a KVM consumer. The guest OS (Linux 6.18.5-fc-v20) sees 4 vCPUs and 15GB of RAM, but no /dev/kvm and no virtualization flags. Nested virtualization is disabled.

# What Claude Fable saw inside the Claude Code container
$ ls /dev/kvm
ls: cannot access '/dev/kvm': No such file or directory

$ grep -E 'vmx|svm' /proc/cpuinfo
# (no output)

$ smolvm machine run python:3.12 -- python -c "print('hello')"
Error: kvm not available

This is not a bug. It’s a deliberate security and performance boundary. Firecracker guests are designed to be ephemeral, single-tenant, and isolated. Exposing nested virtualization would increase attack surface and complicate resource accounting.

The GitHub Actions Pivot

Claude Fable’s autonomous workaround:

  1. Detected the missing /dev/kvm in the Claude Code environment
  2. Recognized that GitHub Actions ubuntu-latest runners expose KVM
  3. Created a temporary workflow file in a branch
  4. Pushed the branch, triggered the workflow, collected logs
  5. Deleted the workflow file in the final commit

The workflow installed smolvm, ran the full test battery (Python and JavaScript execution, resource limits, filesystem isolation), and captured output. The agent then synthesized results from the GitHub Actions logs.

This is a concrete example of tool-aware planning. The agent didn’t ask for help or fail gracefully. It identified an alternative execution environment with the required primitives and used it.

Architecture Comparison: KVM vs. WASM vs. Docker

Primitivesmolvm (KVM)WASM SandboxDocker (unprivileged)
IsolationHardware VMSoftware runtimeKernel namespaces + cgroups
Nested virt requiredYesNoNo
Cold start0.6-1.5s<10ms0.5-2s
Warm execution~50ms<1ms~50ms
Filesystem accessMount-based, scopedVirtual FS or WASIBind mounts, volume isolation
Network isolationHypervisor-enforcedRuntime policyNetwork namespaces
CPU/RAM limitsHypervisor cgroupsRuntime configcgroups v2
Deployment constraintNeeds /dev/kvmRuns anywhereNeeds Docker daemon

Willison previously built a 362KB MicroPython WASM sandbox for similar use cases. WASM has zero nested virtualization requirements and runs in browsers, serverless runtimes, and CI environments without special permissions. The trade-off is language support: WASM sandboxes are limited to languages that compile to WASM or have WASM interpreters. smolvm supports any language with a Linux binary.

Docker in unprivileged mode (rootless, no --privileged flag) provides namespace isolation without hardware virtualization. The kernel is shared, so a kernel exploit in the container could escalate to the host. For agent-generated code, this is a meaningful risk if the agent is compromised or tricked into generating malicious payloads.

Deployment Shape and Failure Modes

If you’re building an agent that generates and executes code, you need to map your execution environment to the sandbox primitive:

Local development (bare metal or VM with KVM pass-through)
smolvm works. You get hardware isolation and full language support.

Cloud VMs (AWS EC2 bare metal, GCP n2d, Azure Dv5)
smolvm works if you choose instance types that expose nested virtualization. AWS Nitro-based instances support it. Firecracker guests do not.

Serverless (AWS Lambda, Google Cloud Run, Azure Functions)
smolvm does not work. No /dev/kvm. Use WASM sandboxes (Wasmtime, WasmEdge) or gVisor.

Kubernetes (GKE, EKS, AKS)
smolvm works only if nodes expose /dev/kvm and you run privileged pods or use KubeVirt. Most managed Kubernetes clusters do not expose KVM to pods by default.

GitHub Actions (ubuntu-latest runners)
smolvm works. Runners are bare metal or VMs with KVM enabled. This is why Claude Fable’s pivot succeeded.

Failure modes:

  • Silent degradation: QEMU falls back to software emulation if KVM is missing but the binary doesn’t check. Execution becomes unusably slow.
  • Permission errors: /dev/kvm exists but the process lacks read/write permissions. Requires chmod or adding the user to the kvm group.
  • Kernel module not loaded: modprobe kvm and modprobe kvm_intel (or kvm_amd) must succeed before /dev/kvm appears.

Observability and State Management

smolvm execution is stateless by design. Each smolvm machine run invocation:

  1. Boots a fresh VM from a cached image
  2. Mounts specified input directories as read-only
  3. Mounts output directories as writable
  4. Executes the command
  5. Shuts down the VM
  6. Returns exit code and stdout/stderr

There is no persistent state between runs unless you explicitly mount a shared volume. This is ideal for agent workflows where each tool call should be isolated.

For observability, you rely on:

  • Exit codes (0 for success, non-zero for failure)
  • Stdout and stderr capture
  • Execution time (measured externally)
  • Resource usage (if you wrap smolvm in a monitoring harness)

smolvm does not expose internal VM metrics (CPU usage, memory pressure, syscall counts) by default. If you need that telemetry, you would instrument the guest OS or parse hypervisor logs.

Security Boundaries

smolvm’s security model assumes:

  • The host kernel is trusted
  • The hypervisor (KVM + QEMU) is trusted
  • The guest OS image is trusted (you control what goes into the Python or Node.js image)
  • The user-provided code is untrusted

The attack surface is the KVM/QEMU boundary. Historical vulnerabilities (VM escape bugs) exist, but they are rare and patched quickly. The risk is lower than shared-kernel containers but higher than pure WASM sandboxes (which have no kernel at all).

Network isolation is enforced by not configuring a network interface in the VM. Filesystem isolation is enforced by mount namespaces. CPU and RAM limits are enforced by cgroups applied to the QEMU process.

If an attacker controls the code inside the VM, they can:

  • Consume CPU and RAM up to the configured limits
  • Read and write files in mounted directories
  • Attempt VM escape exploits (low probability, high impact)

They cannot:

  • Access the host filesystem outside mounted directories
  • Make network requests
  • Persist state between executions (unless you mount a writable volume)

Code Example: Running Untrusted Python with smolvm

# Install smolvm (requires /dev/kvm)
curl -fsSL https://smolmachines.com/install.sh | sh

# Create input and output directories
mkdir -p /tmp/input /tmp/output
echo '{"numbers": [1, 2, 3, 4, 5]}' > /tmp/input/data.json

# Run untrusted Python code with resource limits
smolvm machine run \
  --cpu 1 \
  --memory 512M \
  --timeout 5s \
  --mount /tmp/input:/input:ro \
  --mount /tmp/output:/output:rw \
  --unprivileged \
  python:3.12 -- python3 -c "
import json
with open('/input/data.json') as f:
    data = json.load(f)
result = sum(data['numbers'])
with open('/output/result.txt', 'w') as f:
    f.write(str(result))
"

# Check output
cat /tmp/output/result.txt
# 15

The --unprivileged flag runs the VM without root inside the guest. The --timeout flag kills the VM after 5 seconds. The --mount flags scope filesystem access.

When to Use smolvm for Agent Sandboxes

Use smolvm when:

  • You need hardware-level isolation for untrusted code
  • You require full language support (Python, Node.js, Ruby, Go, Rust, etc.)
  • You control the deployment environment and can provision /dev/kvm
  • Cold start latency under 2 seconds is acceptable
  • You are running on bare metal, EC2 bare metal, or GitHub Actions

Avoid smolvm when:

  • You are deploying to serverless platforms (Lambda, Cloud Run)
  • You are running inside a VM that does not expose nested virtualization (Firecracker, most Kubernetes pods)
  • You need sub-10ms cold starts (use WASM instead)
  • You only need to sandbox JavaScript or Python (WASM runtimes are simpler)
  • You cannot tolerate the KVM/QEMU attack surface

Technical Verdict

smolvm is a solid choice for agent-generated code execution when you control the infrastructure and can guarantee /dev/kvm access. The hardware isolation is stronger than Docker, the language support is broader than WASM, and the performance is acceptable for interactive agent workflows.

The deployment constraint is real. If your agent runs in a managed environment (serverless, hosted notebooks, Firecracker-based platforms), you will hit the same wall Claude Fable did. The workaround (GitHub Actions, self-hosted runners, or a dedicated VM fleet) adds operational complexity.

For production agent systems, the decision tree is:

  1. Can you provision bare metal or VMs with nested virtualization? Use smolvm.
  2. Can you limit execution to JavaScript or Python? Use a WASM sandbox (Wasmtime, Extism).
  3. Do you need arbitrary language support in a serverless environment? Use gVisor or Firecracker (accept the complexity).

Claude Fable’s autonomous pivot to GitHub Actions is a preview of how agents will route around infrastructure constraints. The agent didn’t fail. It found a different execution environment with the required primitives and completed the task. That adaptability is the real story.