all posts

Firecracker vs Bubblewrap: sandboxing untrusted code

Ajay Kumar··8 min read

Bubblewrap (`bwrap`) is the small, sharp tool that confines every Flatpak app on your Linux desktop. It lets an ordinary, non-root user assemble a restricted sandbox — a private mount tree, fresh namespaces, a stripped-down device view — out of kernel primitives, without asking for a single privileged capability. It's genuinely elegant: chroot-on-steroids that any unprivileged user can drive. So how does it stack up against Firecracker, which boots a whole guest kernel under KVM? The honest answer is that they're solving different halves of the same problem, and the deciding question is how hostile the code is.

What Bubblewrap actually is

Bubblewrap is a shared-kernel sandbox. It builds its jail out of Linux user namespaces (so a normal user can become root inside the sandbox without being root outside it), plus bind mounts to control exactly which files the process can see, plus mount/PID/network/IPC namespaces to wall off the rest of the system, and optional seccomp filters to trim the reachable syscall set. The result is a process that sees a tiny, curated slice of your filesystem and system — and nothing else. For confining a desktop app so it can read its own data directory but not your `~/.ssh`, this is exactly the right tool, and it costs almost nothing.

But note the load-bearing word: process. A bwrap sandbox is still a process running on your host's kernel. The namespaces and bind mounts change what it can see; they do not change what it runs on. Every syscall the confined code makes still lands in the same kernel your host is running. That's the whole distinction, and everything below follows from it. (Exact defaults and flag behavior evolve — verify against the current Bubblewrap docs before you rely on a specific guarantee.)

The mental model: Bubblewrap narrows the code's view of the world. Firecracker gives the code a different world. A filesystem jail restricts what a process can reach; a microVM puts a hardware boundary and a separate kernel between the guest and you.

The unprivileged double-edged sword

The best thing about Bubblewrap — no root required — is also the source of its sharpest caveat. Unprivileged sandboxing works because Linux user namespaces let a normal user create and enter namespaces with fake root inside them. That's a lovely capability. It also hands unprivileged users a large, complicated chunk of kernel code they otherwise couldn't reach, and that chunk has a long history of privilege-escalation CVEs. "Unprivileged containers" is a great idea right up until the unprivileged user namespace machinery is itself the vulnerability — several notable local-root exploits over the years have gone straight through it. Some hardened distros ship with unprivileged user namespaces disabled or restricted for precisely this reason.

This isn't a knock on Bubblewrap specifically — bwrap is well written, minimal, and does the confinement it promises. It's a property of the substrate. If your threat model is "a mostly-trusted app that occasionally has a bug," widening the kernel surface a little to gain filesystem confinement is a fine trade. If your threat model is "code that is actively trying to break out," you've now given that code a namespace-creation path into the kernel — and the kernel is the one thing you were hoping to protect.

The Firecracker difference: a real guest kernel

Firecracker doesn't narrow a process's view — it boots a separate guest kernel inside a hardware-virtualized microVM (KVM). The guest reaches the outside world only through a tiny set of emulated virtio devices. There is no shared kernel to attack: a syscall the guest makes lands in the guest's own kernel, not yours. To reach the host, an exploit would have to escape the hypervisor itself — a much smaller, more heavily audited surface than the full Linux syscall interface a bwrap process still sees. This is why AWS Lambda and Fargate run every customer's function in a Firecracker microVM rather than a namespace jail: at scale, against unknown code, the host kernel is not a boundary you want to bet on.

The cost, historically, was startup time — nobody wanted to boot a VM per request. That's the problem snapshot-restore solves: instead of cold-booting, you restore a pre-baked snapshot of an already-running guest. On PandaStack a create is ~49ms to restore and p50 179ms end to end (p99 ~203ms), so VM-grade isolation costs almost nothing in latency versus the near-zero overhead of a namespace jail. A cold boot with no snapshot is only ~3s, and that happens once.

Side by side

  • Isolation model — Bubblewrap: user namespaces + bind mounts + optional seccomp on the shared host kernel. Firecracker: hardware-virtualized microVM with its own guest kernel.
  • Needs root — Bubblewrap: no, that's the point (unprivileged by design). Firecracker: needs KVM access on the host, but the guest is fully unprivileged relative to it.
  • Boot cost — Bubblewrap: effectively instant, it's just a process. Firecracker: ~49ms snapshot restore (p50 179ms create), ~3s cold boot the first time.
  • Kernel surface — Bubblewrap: the full host kernel, plus the user-namespace path (a known CVE source). Firecracker: only the guest kernel plus the small virtio/hypervisor surface.
  • Best fit — Bubblewrap: confining a mostly-trusted app's filesystem/device view (Flatpak, reproducible build steps). Firecracker: actively hostile, multi-tenant, or AI-generated code.

When Bubblewrap is enough

Reach for Bubblewrap when you mostly care about filesystem and namespace isolation and you trust the kernel to hold. That covers a lot of real work: confining a desktop app so it can't read your whole home directory (the Flatpak case), sandboxing a build step so it can only touch its inputs and outputs, giving a script a clean, reproducible mount tree, or restricting what a mostly-trusted helper process can see. In all of these the code isn't your adversary — you're guarding against bugs and accidents, not a determined escape. Here bwrap's near-zero overhead and no-root requirement are exactly what you want, and spinning up a VM would be silly overkill.

# Bubblewrap: an unprivileged filesystem jail for a mostly-trusted step.
# New namespaces, read-only host libs, one writable scratch dir, no network.
bwrap \
  --unshare-all \
  --ro-bind /usr /usr \
  --ro-bind /lib /lib \
  --ro-bind /lib64 /lib64 \
  --ro-bind /bin /bin \
  --proc /proc \
  --dev /dev \
  --bind ./workspace /workspace \
  --chdir /workspace \
  --die-with-parent \
  /bin/sh -c 'make build'

# Great for confining what this process can SEE. But `make` still runs
# on the host kernel: a kernel-level exploit is not contained here.

When you need a VM

The line moves the moment the code becomes untrusted, multi-tenant, or generated at runtime. An AI agent executing model-written commands, a per-user code playground, an ephemeral CI runner for arbitrary third-party repos, a code interpreter running whatever an LLM emits — in all of these you can't review the code before it runs, and a clever payload's whole job may be to break out. A shared-kernel jail asks you to bet that neither a kernel bug nor the user-namespace path will be exploited. A microVM doesn't ask you to make that bet: the blast radius of arbitrary code is one disposable guest with its own kernel, memory, filesystem, and network namespace.

This is the line PandaStack is built on. Every sandbox is its own Firecracker microVM, created in ~49ms via snapshot-restore. The SDK gives you the same write-then-exec-then-read loop you'd script around bwrap, but the boundary underneath it is a hypervisor, not a namespace. A `rm -rf /`, a fork bomb, or an `import os; os.system(...)` is contained to a throwaway VM, not your host.

from pandastack import Sandbox

# Firecracker: a real guest kernel per run, not a view onto yours.
untrusted_code = """
import os
# Whatever this does, it happens inside a disposable guest kernel.
os.system('id; uname -a')
print('done')
"""

sbx = Sandbox.create(template="code-interpreter", ttl_seconds=300)
try:
    sbx.filesystem.write("/workspace/run.py", untrusted_code)
    result = sbx.exec("python3 /workspace/run.py", timeout_seconds=30)
    print("exit:", result.exit_code)
    print(result.stdout)
finally:
    # Tear down the VM — the entire blast radius goes with it.
    sbx.kill()
Don't reach for a VM to run code you wrote and trust — a bwrap jail (or just a subprocess) is simpler and faster there. And a sandbox of either kind isolates execution, not your secrets: never inject credentials the running code shouldn't see into the sandbox environment.

The honest verdict

Bubblewrap and Firecracker aren't really rivals — they're points on a spectrum of how much you're willing to trust the host kernel. Bwrap is the right tool when the code is mostly trusted and you want cheap, unprivileged, filesystem-shaped confinement: Flatpak apps, build sandboxes, reproducible environments. Firecracker (or a platform built on it) is the right tool when the code is hostile, multi-tenant, or written by a model you can't audit, and you can't afford to bet everything on the kernel holding. If "what if this code is actively trying to escape?" is a question you have to take seriously, a filesystem jail on the shared kernel says "probably fine," and a microVM says "contained."

Frequently asked questions

Is Bubblewrap secure enough to run untrusted code?

Bubblewrap confines what a process can see via user namespaces and bind mounts, but the confined code still runs on your host kernel — it's a filesystem/namespace jail, not a syscall-proof boundary. A kernel bug (including in the unprivileged user-namespace path bwrap relies on) can still escape it. For mostly-trusted apps and build steps that's fine; for actively hostile, multi-tenant, or AI-generated code, a hardware-isolated microVM like Firecracker gives each run its own guest kernel. Verify specific bwrap guarantees against its current docs.

Why are unprivileged user namespaces a security concern?

User namespaces let a normal user create namespaces with root privileges inside them without being root outside — which is what makes unprivileged sandboxing like Bubblewrap possible. But they also expose a large, complex slice of kernel code to unprivileged users, and that path has been the root cause of several local privilege-escalation CVEs over the years. Some hardened distributions restrict or disable unprivileged user namespaces for exactly this reason.

Does Firecracker need root like a container runtime?

Firecracker needs access to KVM on the host to create a microVM, but the guest workload itself runs fully unprivileged relative to the host — it can only touch the outside world through a small set of emulated virtio devices, not the host kernel. Bubblewrap's appeal is the opposite: it needs no privileged setup at all, at the cost of keeping the workload on the shared host kernel.

When should I use Bubblewrap instead of a microVM?

Use Bubblewrap when you mostly care about filesystem and namespace isolation and you trust the kernel — confining a desktop app (the Flatpak case), sandboxing a reproducible build step, or restricting a mostly-trusted helper's file access. It's near-zero overhead and needs no root. Use a microVM when the code is untrusted, multi-tenant, or model-generated and a shared-kernel escape would be catastrophic.

How much startup overhead does Firecracker add versus Bubblewrap?

A Bubblewrap sandbox is effectively instant because it's just a process with adjusted namespaces. A Firecracker microVM historically had to boot, but snapshot-restore removes that: PandaStack restores a pre-baked snapshot in ~49ms (p50 179ms create end to end), with a ~3s cold boot only on the very first spawn. So the isolation upgrade costs tens of milliseconds, not seconds.

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.