> ## Content Index
> Fetch the complete content index at: https://vector-n-volt.ghost.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# Securing LLM Code Execution: Why I Chose gVisor Over Docker and VMs for My Agent Framework (#01)
- URL: https://vector-n-volt.ghost.io/foundation-isolation-runtime/
- Published: 2026-11-03T23:20:10.000Z
- Updated: 2026-11-06T18:42:38.000Z
- Description: My agent framework runs untrusted, LLM-generated scripts. I compare seven isolation options, from a bare process to a full VM, against my threat model and explain why I chose gVisor and which trade-offs I accept.
- Author: Balázs Farkas
- Tags: #dev, foundation, isolation, gvisor, sandboxing

## The problem: an untrusted-code boundary

**Core Premise:** Autonomous agent frameworks that execute LLM-generated bash commands face a harsh reality: you are running untrusted, arbitrary code on your infrastructure.

A hallucinated command can wipe local volumes; a prompt-injected payload can probe internal metadata endpoints, steal environment tokens, or attempt container escapes. Defaulting to standard Docker (runc) leaves the host kernel directly exposed. Conversely, spinning up dedicated virtual machines for every ephemeral tool call destroys reasoning-loop latency and inflates cloud costs.

Below is an architectural evaluation of seven isolation runtimes for agent engines in Go, why gVisor (runsc) hits the operational sweet spot for single-tenant clusters, and the explicit performance trade-offs accepted.

Show the full sectionHide the section 

**My threat model.** I assume the generated code is hostile or at least careless. When untrusted code makes a system call, which software intercepts it, and how much of the host OS is directly exposed?

**My constraints.** These shape the weighing more than any single feature does.

1. **Arbitrary Ecosystem Execution:** The agent must be able to run arbitrary bash commands, install tools (`apt-get`, `pip`, `npm`), execute compiled binaries, and debug scripts.
2. **Short-Lived, High-Frequency Tasks:** Sub-agents spawn for single tool calls (running a script, parsing a repo) and terminate immediately. Cold-start latency directly degrades end-to-end user experience.
3. **High Concurrency & Density:** Running dozens of parallel sub-agent tasks on a single node requires a minimal idle memory footprint per sandbox.
4. **No Guaranteed Hardware Virtualization (`/dev/kvm`):** In managed cloud Kubernetes (EKS, GKE, AKS) or cost-effective CI environments, nested virtualization is often unsupported, restricted, or requires expensive bare-metal instances.
5. **Standard Container Ergonomics:** The runtime must leverage standard OCI container images, public registries, and familiar layering workflows.

**What I mean by "boundary".** The question I ask of every option is simple. When the untrusted code makes a system call, which software handles it, and how much of the host does that software expose? My answers differ by option, and I think that is the most useful lens.

1. Bare Process: \[App\] ────────────────────────────────────────────────────────────────────► \[Host Kernel\]
2. Docker (runc): \[App\] ─────────────────────────────\[Namespaces/cgroups\]───────────────────► \[Host Kernel\] (Shared attack surface)
3. WASM / WASI: \[Wasm Module\] ───────────► \[Wasm Runtime (Wasmtime)\] ─────────────────────► \[Explicit Host Imports\]
4. gVisor (runsc): \[App\] ───────────► \[Sentry (User Kernel)\] ────────────────────────────────► \[Host Kernel\] (Minimal safe subset)
5. microVM: \[App\] ───────────► \[Guest Kernel\] ────────► \[Firecracker + KVM\] ──────────► \[Host Kernel\]
6. Kata Containers:\[App\] ───────────► \[Guest Kernel\] ────────► \[Hypervisor + KVM\] ───────────► \[Host Kernel\]
7. Standard VM: \[App\] ───────────► \[Full Guest OS\] ───────► \[QEMU + KVM\] ─────────────────► \[Host Kernel\]

**How to read the diagrams.** Each option has an interactive block diagram. Red marks the untrusted code. Green marks the component that holds the boundary. Dark marks shared host infrastructure. Circles are interaction points, the diamond is the asset I protect, the square is a threat. Hover or click one for the detail.

## 1\. Bare Linux process

**How the boundary works:** The framework spawns generated scripts directly via os/exec. The only boundary is standard Linux Discretionary Access Control (file permissions and user accounts).

**The Syscall Path:** System calls hit the host Linux kernel directly with zero mediation.

**Why it fails for AI agents:** Even when executed under an unprivileged user, an agent can read world-readable files, probe local loopback services, consume unlimited CPU/memory unless manually wrapped in cgroups, and access any host information exposed to that UID.

**My reading:** this is a fine choice for code I wrote and trust, and the wrong choice for code an LLM wrote. For my use case I rule it out.

## 2\. Docker with runc

**How the boundary works:** Standard Docker uses Linux namespaces (PID, Mount, Network, IPC, UTS) to create isolated views of system resources, alongside cgroups to throttle CPU and memory.

**The Syscall Path:** Syscalls route directly to the shared host Linux kernel. Capabilities can be dropped and seccomp profiles applied, but the underlying execution surface remains the host kernel.

**The Risk Factor:** The Linux kernel exposes approximately 350+ system calls. Even Docker’s default seccomp profile allows roughly 300 of them. Container escapes—whether through kernel exploits (e.g., Dirty COW, Dirty Pipe), /proc or /sys leakage, or eBPF subsystem bugs—have repeatedly allowed escapes, because containers share the host kernel and merely segregate views.

**My reading:** a good boundary for workloads I trust, with a residual risk I am not comfortable carrying for untrusted code.

## 3\. WebAssembly with WASI runtimes (Wasmtime, Wasmer)

**How the boundary works:** Code is compiled to WebAssembly bytecode and executed inside a strict linear memory sandbox managed by a WASM runtime. WASI (WebAssembly System Interface) enforces capability-based security: the module has zero access to the outside world unless explicitly granted by the host via imported functions.

- **The Syscall Path:** **None.** WebAssembly modules cannot execute raw Linux syscalls. Every file read, network socket, or clock lookup is mapped to a capability provided by the host runtime.
- **Why it falls short for autonomous agents:** While WASM delivers sub-millisecond cold starts, negligible memory overhead, and runtimes written in a memory-safe language, it **fails on ecosystem compatibility**. Autonomous agents need to execute arbitrary bash scripts, run `curl`, install untrusted Python packages with C-extensions, and run pre-built binaries. You cannot easily `pip install pandas` or execute raw bash inside standard WASI without running a heavy userland/OS emulator inside the bytecode, which destroys performance and ergonomics.

**My reading:** an attractive, light capability model for code I can compile to Wasm myself, and a poor match for an agent that runs whatever a shell can run. For my use case I do not choose it.

## 4\. gVisor with runsc

**How the boundary works:** Developed by Google, gVisor introduces an application kernel written in Go called the Sentry. The Sentry intercepts all guest system calls in user space, acting as an intermediate OS. File access is mediated by the Sentry, through a separate, isolated process called the Gofer or through directfs, depending on configuration.

**The Syscall Path:** The untrusted code makes a syscall. The Sentry handles and emulates the operation entirely in user space. To interact with the host, the Sentry makes only a restricted set of host system calls, which narrows what the untrusted code can reach in the host kernel.

**Why it fits:**

- runsc is an OCI-compliant runtime that drops directly into Docker or Kubernetes configurations.
- Using the modern systrap platform, gVisor intercepts system calls through seccomp-triggered signals handled by stub processes, without requiring /dev/kvm.
- Startup latency is within the sub-second window required for interactive agent tool loops. **My reading:** it narrows what untrusted code reaches directly, and it keeps the container workflow.

## 5\. microVM with Firecracker

**How the boundary works:** Firecracker, built by AWS, is an open-source Virtual Machine Monitor (VMM) written in Rust. It utilizes the Linux Kernel-based Virtual Machine (/dev/kvm) to launch minimal Linux guest kernels inside lightweight virtual machines.

**The Syscall Path:** Syscalls land inside a dedicated guest kernel. The guest reaches the host only through Firecracker's stripped-down virtio device model (virtio-net, virtio-block, minimal serial console), which the VMM implements on top of hardware virtualization extensions (Intel VT-x / AMD-V). The VMM is itself a host process that uses KVM and the host kernel, so the host kernel stays exposed through KVM and the VMM's own syscalls.

**The Bottleneck:** While Firecracker boasts millisecond-level boot times (5–50 ms) and hardware-level isolation, it strictly requires access to /dev/kvm. Operating Firecracker at scale requires managing guest kernel versions, building uncompressed ext4 rootfs images, and provisioning bare-metal hardware or environments with nested virtualization enabled.

**My reading:** by design this is among the strongest boundaries of the seven for untrusted code. My cost is the KVM requirement and the work of operating guest kernels and root filesystems.

## 6\. Kata Containers

**How the boundary works:** Kata Containers is an OCI-compliant runtime that provisions a dedicated, lightweight virtual machine for each container or Kubernetes Pod. It uses hypervisors like QEMU, Cloud Hypervisor, or Firecracker under the hood, wrapped by `containerd-shim-kata-v2`. **The Syscall Path:** Syscalls land inside a dedicated guest Linux kernel. The guest kernel talks to the hypervisor, which relies on hardware virtualization extensions (KVM) to isolate execution from the host. **My reading:** it is in the same class of boundary as a microVM, with a heavier startup and footprint. A widely used choice for strong container isolation, but operationally heavier and blocked in environments lacking nested KVM.

## 7\. Standard VM with QEMU/KVM

**How the boundary works:** Complete hardware emulation running an off-the-shelf, general-purpose operating system on top of a hypervisor. **The Syscall Path:** Emulated hardware devices handle I/O, and CPU instructions run via hardware virtualization. **Why it fails for agents:** Provisioning full VMs introduces boot latencies ranging from 5 to 30 seconds and baseline memory footprints measured in gigabytes. For an autonomous agent dispatching multiple short-lived commands in a single reasoning loop, traditional VMs introduce unacceptable latency and infrastructure bloat. **My reading:** Unlike microVMs, traditional hypervisors carry more device emulation than short-lived agent execution needs

## My decision

For an agent framework orchestrating ephemeral code execution, gVisor (runsc) gives the balance of security, operational simplicity, and deployment flexibility that fits my constraints best.

- It delivers an architectural boundary where it matters most: the LLM's system calls are handled by the Sentry first, and the host kernel sees only the Sentry's restricted set of calls.
- The Trade-Offs I Explicitly Accept Selecting an engineering architecture means deliberately choosing which drawbacks you are willing to manage:
- The I/O Performance Tax: File access is mediated by the Sentry, through the Gofer file proxy or directfs depending on configuration. Workloads that generate thousands of small file operations (such as running npm install or unpacking large tarballs) experience measurable overhead compared to native runc.
- Compatibility Limitations: Because the Sentry implements Linux kernel interfaces in software, rarely used system call flags, specialized ioctl requests, or direct hardware hooks will fail. For typical Python, Node.js, and Bash agent workflows, compatibility is solid, but it must be verified against your specific toolchains.
- Software-Defined Boundary: gVisor trades the sprawling attack surface of the C-based Linux host kernel (\~30 million lines of code) for the memory-safe, smaller surface of the Go-based Sentry (\~400,000 lines). It is a software boundary, not a hardware-isolated boundary.

**What to take away.**

- I ask which software handles the untrusted code's system calls.
- Runc shares the host kernel, and that is the point I weigh most against it for LLM-generated code.
- VMs, microVMs and Kata Containers give a stronger-looking boundary, and they need KVM and more operational work.
- A Wasm runtime gives a capability model, and it does not run the arbitrary bash and tools my agents need.
- gVisor is my compromise for my threat model and constraints.

## What would change my mind

A sound architectural decision is tied to its underlying assumptions. I would re-evaluate this choice under the following conditions:

- Affordable, Ubiquitous Nested Virtualization: If managed cloud providers make nested KVM universally available and cost-neutral across all standard Kubernetes node types, the operational argument against microVMs weakens significantly.
- Strict Multi-Tenant Isolation: If our framework evolves to execute untrusted code from multiple external customers side-by-side on the same compute node, a software boundary alone may not be enough. Multi-tenant zero-trust necessitates hardware-enforced boundaries (Firecracker or Kata Containers).
- Workloads Shift to Heavy Disk I/O: If agent workflows become predominantly I/O-bound rather than compute- or API-driven, the Gofer filesystem proxy overhead may force a migration to microVMs with native block storage virtualization.

## Run it yourself

The accompanying code, the `agent-sandbox` package, is a reference implementation of the gVisor lifecycle gate in Go. Its `create_agent_container` command creates one isolated gVisor (`runsc`) agent container from a single `agentID`.

*Target Environment:* The command runs as root inside the `agent-host` distro and calls `docker` and `/usr/local/bin/create_agent_net` directly. It never reads or modifies `daemon.json` or any other host configuration.

*Full runbook:* the [chapter 1 runbook](https://github.com/vectorNvolt/vectorNvolt-blog/blob/ch01%5Ffound/p01%5Fp02%5Fp03/chapter01/howto/README.md?ref=vector-n-volt.ghost.io) takes a bare Windows + WSL2 machine to a running agent container, including building the binaries, creating `agent-host` with the `runsc` runtime, and creating the container (its sections 3 and 8 cover this post's runtime).

**Run this evaluation harness strictly within an isolated, disposable environment.** This repository is an architectural reference pattern designed to benchmark isolation boundaries, not an off-the-shelf production system. Hardening an untrusted-code sandbox for enterprise multi-tenancy—including custom seccomp profiles, network egress filtering, and tenant quotas—requires environment-specific design.

## Code references

These are the places in the code where the gVisor decision shows up. I describe what each one does.

Show the full sectionHide the section 

In this post's package, `agent-sandbox`:

- [config.go](https://github.com/vectorNvolt/vectorNvolt-blog/tree/ch01%5Ffound/p01%5Fp02%5Fp03/chapter01/packages/agent-sandbox/internal/config/config.go?ref=vector-n-volt.ghost.io) holds the runtime name, `runsc`, as one constant that the rest of the code reads.
- [s02\_check\_runtime.go](https://github.com/vectorNvolt/vectorNvolt-blog/tree/ch01%5Ffound/p01%5Fp02%5Fp03/chapter01/packages/agent-sandbox/internal/steps/s02%5Fcheck%5Fruntime.go?ref=vector-n-volt.ghost.io) holds `CheckRuntime`. I use it to refuse to go on when the engine does not register runsc. It reports the mismatch and does not repair it.
- [s20\_check\_preconditions.go](https://github.com/vectorNvolt/vectorNvolt-blog/tree/ch01%5Ffound/p01%5Fp02%5Fp03/chapter01/packages/agent-sandbox/internal/steps/s20%5Fcheck%5Fpreconditions.go?ref=vector-n-volt.ghost.io) holds `CheckPreconditions`, which runs the runtime check first, before any other prerequisite.
- [docker.go](https://github.com/vectorNvolt/vectorNvolt-blog/tree/ch01%5Ffound/p01%5Fp02%5Fp03/chapter01/packages/agent-sandbox/internal/services/docker.go?ref=vector-n-volt.ghost.io) holds `RuntimeRegistered`, which asks the engine for its runtime list, and the container creation call that passes the runtime to Docker.
- [s06\_create\_start.go](https://github.com/vectorNvolt/vectorNvolt-blog/tree/ch01%5Ffound/p01%5Fp02%5Fp03/chapter01/packages/agent-sandbox/internal/steps/s06%5Fcreate%5Fstart.go?ref=vector-n-volt.ghost.io) holds the create-and-start step, where I create the container with that runtime.
- [s08\_verify.go](https://github.com/vectorNvolt/vectorNvolt-blog/tree/ch01%5Ffound/p01%5Fp02%5Fp03/chapter01/packages/agent-sandbox/internal/steps/s08%5Fverify.go?ref=vector-n-volt.ghost.io) holds the step that reads the container back and compares its runtime with the configured one.
- [sequence\_test.go](https://github.com/vectorNvolt/vectorNvolt-blog/tree/ch01%5Ffound/p01%5Fp02%5Fp03/chapter01/packages/agent-sandbox/tests/sequence%5Ftest.go?ref=vector-n-volt.ghost.io) holds the tests `TestRuntimeMissingExits1` and `TestRunscAbsentFromRuntimesExits1` for the missing-runtime cases.

The registration of runsc with Docker lives in a separate package, `agent-host`, in its "register runsc runtime and restart docker" step (`s10_register_runtime.go`). Its verification step checks that the engine lists runsc and can run a container with it. I give no link for it here, because that package has its own tag.

## References

The facts I state about gVisor, Firecracker, WebAssembly runtimes and Kata Containers come from their public project sites.

Show the full sectionHide the section 
- gVisor project site and documentation: [gvisor.dev](https://gvisor.dev/?ref=vector-n-volt.ghost.io) and [gvisor.dev/docs](https://gvisor.dev/docs/?ref=vector-n-volt.ghost.io).
- Firecracker project site: [firecracker-microvm.github.io](https://firecracker-microvm.github.io/?ref=vector-n-volt.ghost.io).
- Firecracker source repository and documentation: [github.com/firecracker-microvm/firecracker](https://github.com/firecracker-microvm/firecracker?ref=vector-n-volt.ghost.io).
- Wasmtime project site: [wasmtime.dev](https://wasmtime.dev/?ref=vector-n-volt.ghost.io).
- Wasmer project site: [wasmer.io](https://wasmer.io/?ref=vector-n-volt.ghost.io).
- WebAssembly System Interface (WASI): [wasi.dev](https://wasi.dev/?ref=vector-n-volt.ghost.io).
- Kata Containers project site: [katacontainers.io](https://katacontainers.io/?ref=vector-n-volt.ghost.io).

---

Code for this post (tag `ch01_found/p01_p02_p03`): [https://github.com/vectorNvolt/vectorNvolt-blog/tree/ch01\_found/p01\_p02\_p03/chapter01/packages/agent-sandbox](https://github.com/vectorNvolt/vectorNvolt-blog/tree/ch01%5Ffound/p01%5Fp02%5Fp03/chapter01/packages/agent-sandbox?ref=vector-n-volt.ghost.io)