Isolating LLM Agents

2026-11-01

Most Linux distros do little to isolate user applications by default. A process runs with your UID and gets everything your user can touch. That includes ~/.ssh, browser profiles, .env files, git credentials, etc. Linux does have good primitives that let you build a decent sandbox. You can use some combination of cgroups, seccomp, namespaces, and overlayfs to isolate anything that runs in userspace (see Docker).

The drawback, of course, is that the sandbox relies on services provided by your host kernel, so the sandboxed application shares the same kernel that enforces the sandbox. If the application can talk to your kernel by design, it can also exploit an unpatched kernel bug (of which there are plenty) to get around the isolation primitives and access resources it shouldn’t. For a while, this was accepted as a reasonable tradeoff, and lightweight containers became the standard approach to isolating an application.

LLMs, and the class of bugs they are able to find, change this calculation. We now run agents that execute arbitrary shell commands, read untrusted web pages, and can be steered by prompt injection hidden in that content. The agent itself is untrusted code, and the model driving it is capable of finding exactly the kind of memory safety bugs that can be used to exploit your kernel. The old desktop assumption, that everything running as your user is acting on your intentions, no longer holds.

There is a spectrum between a plain process and a full VM. Some agents ship with their own sandbox mode, built on the same Linux primitives. There is gVisor for intercepting syscalls with a kernel written in Go so fewer syscalls reach the host kernel. The claim is “VM-like performance and container-like resource efficiency”. There are also microVMs like Firecracker and runtimes like Kata that put each container in one. The microVM tools were built for cloud providers running untrusted code from many customers on shared machines. An agent on your laptop is the same problem at a smaller scale.

People like Joanna Rutkowska, who created Qubes OS on top of the open source Xen hypervisor, have long pushed for security through compartmentalization. A qube is a lightweight VM. You run your browser in an “untrusted” qube, your Wi-Fi and USB drivers in driver qubes, and you keep your private keys in an offline qube. Other qubes can request signatures from the offline qube without ever accessing the keys, and a kernel bug in one qube cannot affect another, since each qube has its own kernel. You also get ephemeral VMs, known as disposable qubes, for one-off tasks. They are wiped, along with whatever was in them, when they close.

Qubes has been around for a while, and its design is elegant for its time. Spinning VMs up and down just to get some work done is slow, and each VM runs a full OS, consuming significant memory. It also takes some discipline to maintain strict compartmentalization between VMs. If you cut corners and move a personal file into your browser VM, you break the isolation assumption and lose the security guarantee. You trade convenience for security.

You don’t need a whole OS built around VMs to isolate an agent, though. A single KVM/QEMU guest gets you most of the benefit. Hypervisor code mostly consists of complex state machines written in C, with the same classes of bugs that have been exploited elsewhere. The KVM module is relatively small: roughly 77k lines of code in today’s upstream (Linux 7.3-dev), versus tens of millions of lines in the kernel a container shares. Most of the attack surface comes from QEMU’s device emulation (~660k lines across all architectures in version 11.1.50, of which your guest can reach only a slice). Under libvirt, QEMU runs in user space as an unprivileged user, confined by AppArmor and a seccomp filter. A QEMU escape typically lands the attacker in a restricted process so reaching your files would still require privilege escalation or a confinement bypass.

Here is an example of how to set this up.