The Firecracker security model: how a microVM actually contains untrusted code
When people say a workload "runs in a VM," they usually mean it as a full stop — as if the letters V and M were a security proof. They aren't. A VM is a boundary, and a boundary is only as good as the code enforcing it and the blast radius when that code has a bug. What makes Firecracker interesting isn't that it's a VM; it's that its whole design is organized around a single pessimistic assumption — that any one of its layers might fail — and it stacks enough independent layers that a single failure doesn't hand you the host. This post is an internals walk-through of that stack: the five distinct mechanisms Firecracker uses to contain untrusted code, why each one exists, and what an attacker actually has to defeat to break out.
I'm Ajay; I built PandaStack, where every sandbox, managed database, and hosted app is a Firecracker microVM. This is the security model the whole platform is bet on, so I've had to understand it at the level of "what happens if layer three fails," not the marketing level. The reassuring part, if you're evaluating this for your own untrusted-code problem: the same model is what AWS Lambda and Fargate use to run untrusted, multi-tenant code at enormous scale. Firecracker was built at AWS precisely for that job. You're not adopting an experiment.
Layer 1 — KVM: the guest gets its own kernel, and the CPU enforces it
The layer that does the heavy lifting is hardware virtualization via KVM. This is the difference in kind — not degree — between a microVM and a container. A container is a process on the host: it shares the host's single kernel, and its isolation is a set of kernel features (namespaces, cgroups, seccomp) layered over that shared kernel. A microVM is not a process running your code at all. It's a guest running its own separate kernel, and the guest's instructions execute inside the CPU's virtualization mode (Intel VT-x / AMD-V / Arm virtualization extensions), where the hardware itself refuses to let guest memory accesses reach host physical memory.
Concretely: the guest has its own page tables, and a second layer of address translation (nested paging / EPT) that the guest cannot touch remaps every guest-physical address to host-physical memory the hypervisor chose. Guest code cannot form a pointer to host memory, because the address space it can express doesn't include the host's. When the guest needs something only the host can do — an I/O operation, a privileged instruction — the CPU traps out of guest mode (a VM-exit) and hands control back to the host on a narrow, defined path. The guest never syscalls into the host kernel; it can only trigger these hardware-mediated exits. That's why the blast radius of a compromised guest is, by construction, one VM: to affect anything else, guest code has to defeat the CPU's own memory isolation or find a bug in the tiny amount of host code those VM-exits reach.
This is the property everyone already knows Firecracker for, and it genuinely is the primary wall. But notice what it quietly implies: the interesting attack surface isn't the guest kernel (the guest can already do whatever it wants in there — it's their kernel). The interesting surface is the small pile of host code that handles VM-exits and emulates the devices the guest sees. Every remaining layer exists to shrink and contain that surface.
Layer 2 — a deliberately tiny device model
When guest code does something the host must service — read a disk block, send a network frame — that request lands in the VMM's device emulation code. This is host code parsing input controlled by an attacker, which makes it the richest target in the entire system. The historical lesson here is QEMU: a full-featured VMM, and an extraordinarily capable one, but it emulates a sprawling menagerie of hardware — legacy floppy controllers, dozens of NIC models, sound cards, USB, SCSI, PCI passthrough, an entire BIOS. Every one of those device models is host code reachable from the guest, and VM-escape CVEs have historically clustered exactly there (the infamous ones live in obscure emulated devices most workloads never even use).
Firecracker's answer is austerity. It emulates a minimal set of devices — a handful of virtio devices (block, net, vsock, balloon, rng, entropy) plus a couple of essentials like a serial console and a one-button keyboard controller for clean shutdown — and that's essentially the whole list. No BIOS, no PCI, no USB, no legacy device zoo, no display. Fewer device models means fewer lines of attacker-reachable host code means fewer places for the memory-corruption bug that a VM escape needs. You can't have a vulnerability in a floppy controller you never wrote.
- Fewer devices = smaller attack surface. Each emulated device is host code parsing guest-controlled input; Firecracker ships only the few devices a modern serverless/container workload actually needs.
- virtio, not legacy hardware. Firecracker's devices speak virtio — a paravirtualized interface designed for VMs — rather than faithfully emulating physical chips, which keeps the code paths simple and modern.
- No BIOS, no PCI bus, no bootloader theatrics. The guest is booted directly into a Linux kernel, skipping an entire class of firmware and enumeration code that other VMMs carry.
- The result is a codebase small enough to audit. A minimal, purpose-built VMM is one a security team can actually read end to end — an option that simply doesn't exist for a general-purpose hypervisor.
Layer 3 — the VMM is written in Rust
Layer 2 shrinks the amount of attacker-reachable host code. Layer 3 hardens the code that remains. The overwhelming majority of VM-escape vulnerabilities in the wild are memory-safety bugs — buffer overflows, use-after-free, out-of-bounds reads — in the VMM's C or C++ device emulation. That's the exact bug class Rust is built to eliminate: its ownership and borrow-checking model makes those errors compile-time failures rather than exploitable runtime conditions in the vast majority of code. Firecracker is written in Rust, so the device-parsing code — the most dangerous code in the system — is written in the language least likely to contain the bugs that class of exploit needs.
This isn't a magic wand — Rust has `unsafe` blocks where raw memory work is unavoidable, and logic bugs (a malformed virtio descriptor accepted by correct-but-wrong logic) are still possible. But it dramatically thins the pool of latent vulnerabilities in exactly the place they historically live. Combine it with Layer 2 and the effect compounds: a small amount of code (few devices) written in a memory-safe language (Rust) is a fundamentally smaller haystack for the needle a VM escape requires.
Layer 4 — the jailer: box the VMM before it runs a single guest
Now the pessimistic assumption kicks in. Suppose all three previous layers fail: a guest finds a logic bug in a virtio device, in the Rust `unsafe` path, and breaks out of KVM into the Firecracker process on the host. Where does it land? Whatever privileges, filesystem, and namespaces that process has. If Firecracker runs as root with the full host visible, a VM escape equals host root, and the previous layers only decided how hard that was — not how bad. The jailer exists to make the landing survivable.
In production you don't run `firecracker` directly; you run `jailer`, a companion binary that builds a locked-down host-side box and then `exec`s Firecracker inside it. It places the process in a cgroup (capping CPU and memory before the guest boots), unshares into its own mount, PID, and network namespaces (no host processes, no host mount tree, an isolated network path), pivot_roots into a minimal chroot containing only the few files the VMM needs, creates just the device nodes it requires (principally `/dev/kvm`), and drops from root to an unprivileged uid/gid. So an attacker who fully compromises the VMM lands as an unprivileged user, in an almost-empty chroot, seeing no host PIDs and no host filesystem, with capped resources. They popped a process that can see and do almost nothing.
# Production launches the jailer, NOT firecracker directly.
# Everything before `--` builds the box; everything after is passed to
# the firecracker binary the jailer execs inside that box.
sudo jailer \
--id 6f3c1a9e-microvm \
--exec-file /usr/bin/firecracker \
--uid 30000 \ # never 0 — Firecracker runs unprivileged
--gid 30000 \
--chroot-base-dir /srv/jailer \ # jail root: <base>/firecracker/<id>/root
--netns /var/run/netns/ns-6f3c1a9e \ # pre-created isolated netns
--cgroup-version 2 \
--cgroup cpu.max="50000 100000" \ # CPU capped before boot
--cgroup memory.max=2147483648 \ # 2 GiB memory cap
-- \
--api-sock /run/api.socket \ # relative to the jail root!
--config-file /vm-config.json
# The paths after `--` are interpreted from INSIDE the chroot. Every file
# the VMM needs (kernel, rootfs, sockets) must be staged into the jail first.
# Anything not staged is simply invisible to the VMM.Layer 5 — seccomp on the VMM: a syscall straitjacket
The final layer constrains what the (now unprivileged, chroot'd) VMM process can even ask the host kernel to do. The one door from any userspace process to the kernel is the syscall interface, and Firecracker installs a seccomp-bpf filter on itself at startup: a small BPF program the kernel runs on every syscall the VMM makes, allowlisting only the handful a legitimate VMM needs — the KVM ioctls, the I/O calls for its devices and sockets, memory mapping, the event loop — and killing the process on anything else. The default is deny: a syscall you didn't explicitly permit terminates the process rather than returning an error the attacker can probe around.
Why this matters as the last wall: an attacker who has somehow defeated KVM, the device model, Rust's guarantees, and the jailer's namespaces still finds themselves in a process that cannot `execve` a shell, cannot `open` arbitrary files, cannot `socket` out to the internet — because those syscalls aren't on the allowlist. The intersection of "unprivileged uid," "empty chroot," and "tiny syscall vocabulary" shrinks the set of useful things a compromised VMM can do toward zero. It's a straitjacket, not a shell.
# Conceptual shape of Firecracker's own seccomp allowlist.
# Default action for anything not listed: KILL the process.
default_action: kill_process
allowed_syscalls:
- read / write # serve guest I/O over the virtio devices
- epoll_wait # the VMM event loop
- epoll_ctl
- ioctl # argument-filtered: only the KVM_* operations
- mmap / munmap # set up and tear down guest memory
- futex
- exit_group
# Note: `ioctl` is not blanket-allowed — it's filtered by argument so only
# the specific KVM operations the VMM issues pass. `execve`, `ptrace`,
# `socket` to arbitrary families, etc. are simply ABSENT -> kill on attempt.
# (The real filter is architecture-specific; check the current FC docs.)The two walls: escape doesn't mean you're out
Here's the mental model that ties it together, and the thing worth internalizing. There are two walls, and they defend two different boundaries. The first wall is KVM: it separates the guest from the VMM (the host code). The second wall is the jailer plus seccomp: it separates the VMM from the rest of the host. These are not the same boundary, and — this is the crucial part — an attacker who breaks the first wall has not broken the second. Escaping the VM doesn't drop you onto the host; it drops you into a jailed, unprivileged, seccomp-filtered process that can barely make a syscall. You'd have to defeat that second, independent wall too, using almost none of the tools an attacker normally relies on, because they've all been taken away.
Layer 2 (few devices) and Layer 3 (Rust) sit inside the first wall, making it less likely you breach it at all. But the architecture's real strength is that it never assumes you won't. Each layer is designed on the assumption the others already failed — that's the actual, load-bearing meaning of "defense in depth," as opposed to the checkbox version. Line the layers up as the sequence an attacker must defeat, in order:
- Layer 1 — KVM (hardware): the guest runs its own kernel confined by the CPU's virtualization extensions and nested paging. It cannot address host memory and cannot syscall the host — only trap out via VM-exit onto a narrow, defined path. This is what makes the blast radius one VM.
- Layer 2 — minimal device model: the only attacker-reachable host code is a handful of virtio devices, not QEMU's sprawling hardware zoo. Far fewer places for the memory-corruption bug an escape needs.
- Layer 3 — memory-safe Rust: the device-parsing code — the code most likely to hold that bug — is written in the language that makes the whole bug class a compile error, not a runtime exploit.
- Layer 4 — the jailer: even granting a full VMM compromise, the process is unprivileged, chroot'd into an almost-empty directory, in isolated PID/mount/network namespaces, with capped resources. It sees no host to attack.
- Layer 5 — seccomp-bpf on the VMM: that compromised process can make only the tiny, audited syscall set a legitimate VMM needs; everything else kills it on the spot. No shell, no arbitrary files, no arbitrary sockets.
No single layer is asked to be perfect. KVM is small and heavily audited, but the jailer assumes it might fail. The VMM is memory-safe Rust, but seccomp assumes a bug slipped through anyway. That mutual pessimism is the whole design.
Who actually runs untrusted code on this
This model isn't a research toy. AWS built Firecracker specifically to run untrusted, multi-tenant code, and Lambda and Fargate run on it: functions and containers from customers who don't trust each other, packed densely onto shared hosts, isolated by exactly the stack above. That's the strongest possible existence proof that hardware virtualization with a minimal, jailed, seccomp-filtered VMM is a viable answer to "how do I run code I didn't write next to code someone else didn't write, on the same machine, safely." When you're deciding whether a microVM is enough isolation for arbitrary AI-generated code or user-submitted scripts, the answer is: it's the same boundary the largest serverless platforms on earth stake their multi-tenancy on.
PandaStack runs every workload — a code-interpreter sandbox, a managed Postgres database, a hosted app — as a Firecracker microVM with all five layers in place: the guest under KVM with its own kernel, the minimal virtio device model, the memory-safe Rust VMM, jailed into a fresh chroot with namespaces and cgroups as an unprivileged user, and seccomp on the VMM's syscalls. The jail is rebuilt per VM, so the boundary is per-sandbox, not shared across tenants. And you get the whole hardened stack without touching any of it — a `Sandbox.create` call hands you a live, fully-isolated microVM:
import { Sandbox } from "@pandastack/sdk";
// One call. Behind it: a KVM guest with its own kernel, a minimal
// virtio device model, a Rust VMM jailed into a fresh chroot with
// dropped privileges + cgroups + namespaces, and seccomp on the VMM.
// You configure none of it — the hardened boundary is the default.
const box = await Sandbox.create({ template: "code-interpreter" });
// Run code you did not write, from a user or an LLM, inside the VM.
const result = await box.exec("python3 -c 'print(2 ** 10)'");
console.log(result.stdout); // "1024"
await box.destroy(); // the whole jail is torn down with itNone of that hardening costs you latency at create time. Because every create restores a baked snapshot on demand rather than cold-booting a kernel, a sandbox is live at roughly 179ms p50 (about 203ms p99); the ~3s cold boot happens once per template, then never again. So the security stack isn't a tax you pay in startup time — the two-walls model and sub-second creates are properties of the same system.
The takeaway is the sentence I'd want you to leave with: "it runs in a VM" is the necessary first layer, not the whole answer. The VM protects the host from the guest; the jailer and seccomp protect the host from the VMM; the minimal device model and Rust shrink the odds you ever breach the first wall at all. If you're building on Firecracker directly, run it through the jailer with seccomp enabled — raw `firecracker` as root throws away the entire second wall. If you're building on a platform, this five-layer stack is the kind of hardening you should expect underneath before you hand it someone else's code. PandaStack's core is open source under Apache-2.0, so you can read the exact device model, jailer setup, and seccomp filters the VMM ships with — and run the whole stack on your own KVM hosts.
Frequently asked questions
How does Firecracker actually isolate untrusted code?
Through five independent layers. (1) KVM hardware virtualization: the guest runs its own kernel confined by the CPU's virtualization extensions and nested paging, so it can't address host memory or syscall the host — only trap out via VM-exit. This makes the blast radius one VM. (2) A minimal device model: only a handful of virtio devices are emulated in host code, versus QEMU's huge hardware zoo, so there's far less attacker-reachable code. (3) The VMM is written in Rust, eliminating most of the memory-safety bugs that VM escapes rely on. (4) The jailer runs the VMM unprivileged inside a chroot with isolated namespaces and cgroups. (5) A seccomp-bpf filter restricts the VMM to a tiny syscall allowlist. Each layer assumes the others may have failed.
What is the 'two walls' idea in Firecracker?
There are two distinct boundaries. The first wall is KVM, which separates the untrusted guest from the VMM (Firecracker's host process). The second wall is the jailer plus seccomp, which separates that VMM process from the rest of the host. They defend different things, and breaking the first doesn't break the second: an attacker who escapes the VM lands not on the host but inside a jailed, unprivileged, seccomp-filtered process that can barely make a syscall — no shell, no arbitrary files, no arbitrary network. They'd have to defeat a second, independent wall using almost none of their usual tools. That's why a container escape is a bug report and a Firecracker escape is a research paper.
Why is Firecracker's small device model a security feature?
When a guest does something the host must service — read a disk block, send a packet — the request is parsed by the VMM's device-emulation code, which is host code processing attacker-controlled input. That's the richest target for a VM escape, and historically escape CVEs cluster in emulated devices (often obscure ones like legacy floppy or USB controllers). Firecracker emulates only a minimal set of virtio devices plus a couple of essentials — no BIOS, no PCI, no USB, no legacy hardware — so there's dramatically less host code an attacker can reach. You can't have a vulnerability in a device you never emulate. The small codebase is also small enough for a security team to audit end to end.
Do AWS Lambda and Fargate really use this exact model?
Yes. AWS built Firecracker specifically to run untrusted, multi-tenant code, and both Lambda and Fargate run customer functions and containers on Firecracker microVMs — workloads from customers who don't trust each other, packed densely onto shared hosts and isolated by hardware virtualization plus the jailer and seccomp. It's the strongest available existence proof that a minimal, jailed, seccomp-filtered VMM over KVM is a viable way to run code you didn't write next to other code you didn't write on the same machine. PandaStack uses the same model for its sandboxes, databases, and apps.
Does running Firecracker through the jailer add latency?
No meaningful create-time latency in a snapshot-based platform. The jailer and seccomp setup are part of preparing each microVM, but on PandaStack every create restores a baked Firecracker snapshot on demand rather than cold-booting a kernel, so a fully hardened, jailed, seccomp-filtered sandbox is live at roughly 179ms p50 (about 203ms p99). The ~3s cold boot happens only once per template, then subsequent creates use the snapshot path. The security stack and sub-second creates are properties of the same system — you don't trade one for the other.
49ms p50 cold start. Fork, snapshot, and scale to zero.