all posts

Nested virtualization and Firecracker: what actually works

Ajay Kumar··10 min read

"Does Firecracker support nested virtualization?" is one of those questions where the answer depends entirely on which question you actually asked, and the person asking almost never knows there are two. I've watched a support thread run for a dozen messages with both sides confident and both sides talking about a different layer of the stack. One person wanted to know whether they could run Firecracker on a cloud instance. The other was explaining why you can't run the Android emulator inside a sandbox. Both answers were correct. Neither was useful.

I'm Ajay; I built PandaStack. This post separates the two questions, gives each one an honest answer, and then explains the mechanism well enough that you can predict the answer yourself next time. We'll cover L0/L1/L2 terminology, how hardware nesting actually works on x86 and on ARM, where you can and can't get nested KVM in practice, the exact commands to check, how to enable it on a host you control, why a Firecracker guest has no /dev/kvm and what that does and doesn't break, the security argument against flipping this on casually, and the architectural conclusion that makes most of the demand for nesting evaporate.

The one-line version. Running Firecracker inside a VM: yes, if the hypervisor beneath you exposes virtualization extensions — this is how everyone develops on a Mac. Running a VM inside a Firecracker microVM: no, Firecracker does not expose virtualization extensions to its guests, so there is no /dev/kvm in there and nothing will hardware-accelerate.

The two questions people conflate

Question one is about what's underneath you. You have a workload that needs Firecracker, and the machine you were given is itself a virtual machine — a cloud instance, a Lima VM on your laptop, a CI runner, a nested lab environment somebody built in 2019 and nobody can turn off. You want to know whether the hypervisor above you will let you be a hypervisor too. This question is about the environment you were handed.

Question two is about what's on top of you. You are already running Firecracker sandboxes, and something a user wants to run inside one is itself a virtualization workload: a Docker build that expects privileged devices, an Android emulator, a QEMU job, a Kubernetes-in-a-box tool, or — my favourite request — another Firecracker. You want to know whether your sandbox can host a hypervisor. This question is about the environment you're offering.

The reason people mix them up is that both get typed into a search bar as "firecracker nested virtualization," and both involve two hypervisors stacked. But they point in opposite directions, they're controlled by different parties, and they have opposite answers. Question one is a configuration problem with a known fix. Question two is a design property of Firecracker, and no configuration fixes it.

L0, L1, L2: fixing the vocabulary first

Every conversation about this gets clearer the moment you number the layers, so let's do that once and then use it consistently for the rest of the post.

  • L0 — the bottom hypervisor, running on physical hardware. Your cloud provider's hypervisor, the KVM on your bare-metal box, or Apple's Virtualization.framework on a Mac. It is the only layer with genuine access to the CPU's virtualization hardware.
  • L1 — a guest of L0 that is itself acting as a hypervisor. Your EC2 instance running KVM. The Lima Linux VM on your MacBook running Firecracker. This is the layer that has to be handed a convincing illusion of virtualization extensions.
  • L2 — a guest of L1. The actual microVM your code runs in. From L2's point of view nothing is unusual; it's just a VM. It has no idea it's two layers deep, and that's the whole point of doing this correctly.
  • The question-one shape — "is there an L0 willing to let me be an L1?" You control L1 and L2; someone else controls L0.
  • The question-two shape — "can my L2 become an L1 for an L3?" You control L0-through-L2; your user controls what runs in L2 and is now asking for another turtle.
Nesting is not a feature you enable at the layer that wants it. It is a favour granted from below, by someone who has to do considerably more work than you do.

How hardware nesting actually works

The CPU's virtualization extensions were designed for one level. Intel's VMX and AMD's SVM add a hardware guest mode, a control structure describing the guest, and instructions to enter and leave it. There is exactly one set of those, and it belongs to whoever is running in host mode — which is L0, always. So when L1 executes the instruction to launch its own guest, the hardware does not helpfully create a second nesting level. It traps.

L0 emulates a hypervisor interface for L1

What makes nesting work is that L0 lies to L1 in a very disciplined way. L0 advertises the virtualization feature bits in the CPUID it presents to L1, so L1's kernel sees VMX or SVM and loads its KVM module happily. Then L0 intercepts every privileged virtualization instruction L1 executes and emulates it in software. L1 writes a control structure describing the VM it wants to run — Intel calls it a VMCS — and L0 catches those writes, keeps a shadow copy, and merges L1's intentions with its own controls into a real control structure that the hardware will actually accept.

So when L1 says "launch my guest," what physically happens is: L0 composes a merged control structure, enters guest mode itself, and runs L2's code directly on the CPU. That part is genuinely fast — L2's ordinary instructions execute natively, exactly as they would one level up. The expensive part is what happens on the way out. An event that would have been a single VM-exit in a one-level setup becomes a sequence: the hardware exits to L0, L0 decides whether this exit belongs to it or to L1, and if it belongs to L1, L0 has to synthesize a VM-exit into L1, let L1's handler run, catch L1's attempt to resume, rebuild the merged structure, and re-enter. One transition becomes several, plus the software work in between.

Hardware has chipped away at this. Intel's VMCS shadowing lets L1 read and write most of its control structure without trapping to L0 at all, which removes a large category of exits. But the fundamental shape survives: L0 is emulating a hypervisor, and emulation costs are paid at the boundary.

Nested paging, twice

Memory has the same problem, in a form that's easier to picture. Normal virtualization uses a second-level page table — Intel's EPT, AMD's NPT — to translate guest-physical addresses to host-physical ones, in hardware, without the hypervisor being involved on every fault. With nesting there are two such translations to compose: L2-physical to L1-physical, described by a table L1 built, and L1-physical to actual machine memory, described by a table L0 built. The hardware still only does one.

So L0 composes them, maintaining a shadow of L1's table that maps L2-physical addresses straight to machine addresses, and keeping that shadow coherent as L1 edits its own tables — more traps, more bookkeeping. The steady state is fine: once the shadow is warm, L2's memory accesses are hardware-translated at full speed. It's the churn that costs, and churn means guest boot, page-table teardown, anything that shuffles mappings aggressively. Which sets up the honest summary of nesting performance: it is workload-dependent to a degree that makes single-number claims meaningless. A compute-bound L2 crunching numbers in userspace barely exits and runs close to unnested speed. An exit-heavy L2 — network interrupts, device register pokes, mapping churn — pays the amplification on every transition and can be noticeably slower. Boot is usually the worst case, which is unfortunate given that fast boot is the entire point of a microVM. I won't quote you a percentage; anyone who does is quoting their workload, not yours.

ARM, EL2, and why the timeline is different

ARM structures this differently. Instead of a host/guest mode split layered onto rings, ARM has numbered exception levels: EL0 for applications, EL1 for kernels, EL2 for the hypervisor, EL3 for firmware. A hypervisor lives at EL2 and its guests run at EL1 and EL0. The Virtualization Host Extensions, added in ARMv8.1, made this practical for Linux by letting a mostly-unmodified host kernel run at EL2 directly rather than through a split shim.

Nested virtualization on ARM — hardware support for a guest that believes it has its own EL2 — arrived considerably later, in ARMv8.4 and refined afterwards, and it is far less broadly available than its x86 equivalent. That's the practical thing to carry: on x86, nested virt is old, widely implemented, and mostly a question of whether your provider turned it on. On ARM, it depends much more on the specific silicon and the specific hypervisor, and "my ARM server supports it" is a claim to verify rather than assume. Apple Silicon is the notable consumer-visible case, where recent chips and recent macOS versions support nesting through Virtualization.framework — and that's the path most of us use daily.

A nested setup can look completely healthy and be quietly slow. If you benchmark a microVM platform inside a nested environment and the numbers disappoint, check the layer count before you blame the platform — you may be measuring L0's emulation of a hypervisor rather than the hypervisor you think you're testing.

Question one: where you can actually get nested KVM

Bare metal and cloud instances

Bare metal always works, by definition — there is no L0 above you lying about anything, so your KVM is the real thing and Firecracker is an ordinary L1 workload. That's the boring, reliable answer, and it's what most Firecracker production deployments run on for that reason plus the performance one. On cloud instances it varies by provider, by instance family, sometimes by image, and it moves. Google Cloud has supported nested virtualization on certain instance types for a long time, exposed as an explicit property you set; other providers offer it on some families and not others, or only on bare-metal-adjacent SKUs. Rather than list specifics that will be stale by the time you read this: check your provider's current documentation for the exact family and flag, then verify empirically inside a running instance with the commands below, because documentation and reality occasionally disagree. If it isn't available, the escape hatch is the same everywhere — rent bare metal, where the question doesn't arise.

Apple Silicon, via Virtualization.framework

This one is a genuinely nice case, because the whole stack is visible and it's how PandaStack is developed. macOS has no /dev/kvm — that's a Linux kernel interface and always will be. What macOS has is Apple's Virtualization.framework driving the M-series chip's ARM virtualization extensions. So: macOS plus Virtualization.framework is L0; a Linux VM booted through it (we drive that with Lima, using the vz VM type with nested virtualization enabled) is L1; and because L0 exposes virtualization to that Linux guest, the guest gets a working /dev/kvm and runs Firecracker microVMs as L2. That's the entire trick behind Firecracker on a MacBook, and the layering is worth internalizing because it's the same shape as the cloud case with different vendors in each slot. I wrote up the setup steps in /blog/run-firecracker-on-mac and the KVM layer underneath in /blog/kvm-explained-for-developers. The caveat is the ARM timeline from earlier: this needs a recent enough chip and a recent enough macOS, so if your Lima guest comes up without /dev/kvm, check those two before anything else.

CI runners, where the honest answer is usually no

This is where people get stuck, and I'd rather be blunt than optimistic. Hosted CI runners are, overwhelmingly, VMs on a provider's hypervisor with nested virtualization deliberately switched off — or containers, where the question doesn't even parse. You cannot enable it from inside, because the flag lives in L0 and L0 belongs to your CI vendor. Loading kvm_intel with nested=1 in a job step does nothing: you're asking the wrong kernel. The realistic options are a self-hosted runner on bare metal or a nested-virt-capable instance; a provider tier that explicitly advertises /dev/kvm; or restructuring the job so it doesn't need a VM at all — build artifacts in a container, test VM behaviour somewhere with hardware virt. That last one is less of a defeat than it sounds. If what you actually wanted was disposable, isolated CI environments, running them on a platform that already has microVMs beats smuggling a hypervisor into a runner; I went through that tradeoff in /blog/ephemeral-github-actions-runners-firecracker.

Checking for it, and turning it on

The capability check, in order

Run these top to bottom on the machine in question. The order matters, because each one narrows down which layer is refusing you, and a lot of "nested virt is broken" tickets are actually a group-membership problem on step three.

# --- 1. Does this kernel see a CPU virtualization extension at all? ------
# vmx = Intel VT-x, svm = AMD-V. This is the flag the layer BELOW you
# decides whether to expose. Zero means nobody is passing it through, or
# the firmware has it switched off on bare metal.
grep -c -E '(vmx|svm)' /proc/cpuinfo
#   0   -> no hardware virt visible here. Nothing below will work. Stop.
#   8   -> visible on 8 logical CPUs. Continue.

# --- 2. Am I already inside a VM? ---------------------------------------
# If "Hypervisor vendor" is populated you are L1 (or deeper), which means
# every question below is about NESTED virt, not plain virt. Worth knowing
# before you spend an afternoon blaming your BIOS.
lscpu | grep -i -E 'virtualization|hypervisor'
#   Virtualization:        VT-x
#   Hypervisor vendor:     KVM          <- you are a guest
#   Virtualization type:   full

# --- 3. Does the device node exist, and can *you* open it? --------------
ls -l /dev/kvm
#   crw-rw---- 1 root kvm 10, 232 /dev/kvm    <- exists
#   No such file or directory                 <- module not loaded, or the
#                                                layer below exposed nothing
#
# Existing is not the same as usable. You need to be root or in the 'kvm'
# group; this is the single most common "but it works for me" difference
# between a developer laptop and a service account.
[ -r /dev/kvm ] && [ -w /dev/kvm ] && echo "kvm usable by $(id -un)"

# --- 4. The friendly wrapper (Debian/Ubuntu: apt install cpu-checker) ---
kvm-ok
#   INFO: /dev/kvm exists
#   KVM acceleration can be used
#   -- or --
#   INFO: Your CPU does not support KVM extensions
#   KVM acceleration can NOT be used

# --- 5. Will THIS kernel let its own guests nest one level further? -----
# Read the file that matches your CPU vendor. This is the switch that
# decides whether an L1 you boot on this box gets a /dev/kvm of its own.
cat /sys/module/kvm_intel/parameters/nested   # Intel
cat /sys/module/kvm_amd/parameters/nested     # AMD
#   Y or 1 -> nesting enabled for guests booted here
#   N or 0 -> your guests will come up with no /dev/kvm, no matter what
#             they do internally

# --- 6. What did the kernel actually say when the module loaded? -------
# The most under-used diagnostic here. It usually names the real reason.
dmesg | grep -i kvm
#   kvm: Nested Virtualization enabled
#   kvm: no hardware support          <- extension not exposed to this kernel
#   kvm: disabled by bios             <- bare metal: go enable VT-x/SVM

The decision tree that falls out of it: step 1 empty means the layer below you is the problem and nothing you do locally will fix it. Step 1 populated but step 3 missing means either the modules aren't loaded (`modprobe kvm_intel`) or firmware has it off. Step 3 present but permission-denied in practice means you're not in the kvm group. Step 5 showing N means you can run VMs but your VMs can't run VMs — which is precisely the nested question, and the next section is how to change it if the machine is yours.

Enabling nested KVM on a host you own

# Enabling nested KVM on an L0 Linux host you control. Note the layer:
# this is run on the HYPERVISOR, not inside the guest that wants nesting.
# Running it inside a guest asks the wrong kernel and changes nothing.

# Current state. An empty read usually means the module isn't loaded yet.
cat /sys/module/kvm_intel/parameters/nested

# Runtime change, not persistent. The module cannot be unloaded while any
# vCPU file descriptor is open, so this requires every VM on the box to be
# stopped first -- which is itself a small hint about how host-wide a
# change this is.
sudo modprobe -r kvm_intel
sudo modprobe kvm_intel nested=1

# Persistent across reboots. Use the line matching your CPU vendor.
echo 'options kvm_intel nested=1' | sudo tee /etc/modprobe.d/kvm-nested.conf
# AMD hosts instead:
# echo 'options kvm_amd nested=1' | sudo tee /etc/modprobe.d/kvm-nested.conf

# If your distro loads kvm early in boot, the initramfs carries a copy of
# modprobe.d and must be rebuilt or your shiny new option is ignored.
sudo update-initramfs -u

# Verify against the kernel, never against the config file you just wrote.
cat /sys/module/kvm_intel/parameters/nested   # expect Y
dmesg | grep -i nested                        # kvm: Nested Virtualization enabled

# On a cloud instance none of the above applies from inside the guest: L0
# is your provider's hypervisor and the switch is an instance-type or image
# property you request at creation time. Check your provider's current
# documentation for the exact family and flag -- these details move.
Before you put that modprobe.d file on a shared host: enabling nesting is a security decision, not a config change. You are turning on a large, privileged emulation path in your kernel and offering it to every guest on the box. On a multi-tenant machine that is a change your threat model has to consent to, and the next section is why.

Question two: running a VM inside a Firecracker microVM

Here the answer is short. Firecracker does not expose virtualization extensions to its guests. It presents a deliberately minimal machine — a handful of virtio devices, a serial console, no PCI, no BIOS, no legacy hardware — and the CPU features it advertises do not include VMX or SVM. So inside a Firecracker microVM there is no /dev/kvm, `kvm-ok` reports no extensions, and the kvm modules will not load usefully even if the guest kernel carries them. This is not an oversight or a roadmap item to wait for; it's downstream of what Firecracker is for. Its entire value proposition is a tiny, auditable VMM with the smallest possible surface between untrusted guests and the host, which is why AWS runs Lambda and Fargate on it. Adding a nested-virtualization emulation layer would be the single largest expansion of that surface anyone could propose. I made the surface argument properly in /blog/firecracker-vmm-security-model; nesting is the thing it's most obviously incompatible with.

What still works inside the guest (more than you'd think)

  • Docker and containers generally — containers are not VMs. They're processes with namespaces and cgroups on the guest's own kernel, and the guest kernel is a real Linux kernel. Running dockerd inside a microVM works, and it gives you the pleasant property that the container's blast radius is a guest you were going to throw away anyway.
  • The caveat on that — the guest kernel must actually be built with what containers need: overlayfs, cgroups v2, and the netfilter/iptables modules. Minimal microVM kernels are minimal on purpose, and a missing netfilter module produces a confusing failure that looks nothing like "your kernel lacks a config option." This is the same wall you hit trying to run a full Kubernetes distribution in a stock microVM kernel — the node comes up, and then service networking doesn't.
  • Cross-architecture emulation via QEMU's TCG — running an arm64 binary on an x86_64 guest, or vice versa, through binfmt_misc and qemu-user, or a full-system TCG emulation of a foreign machine. This never wanted KVM in the first place, because you cannot hardware-accelerate an architecture your CPU isn't.
  • Full-system QEMU without acceleration — `qemu-system-x86_64` with TCG rather than `-enable-kvm` boots real guest kernels inside a microVM. It is slow, in the way software emulation of a CPU is slow. For a firmware test, a kernel boot check, or an IoT image, that is frequently an acceptable trade.
  • Everything that merely sounds like virtualization — chroots, user namespaces, WASM runtimes, language-level VMs, seccomp-sandboxed processes. None of these need hardware virt, and a surprising fraction of "I need nested virt" requests turn out to be one of these.

What does not work, and won't

  • Anything requiring /dev/kvm — KVM-accelerated QEMU, libvirt-managed VMs, another Firecracker, Cloud Hypervisor, or a Kata-style runtime that boots a VM per container. These fail at startup with a missing-device or no-acceleration error, and the error is the truth rather than a permissions puzzle.
  • The Android emulator, and similar SDK emulators — these are QEMU with KVM acceleration wearing a nice UI. Without /dev/kvm they either refuse to start or fall back to an emulation mode so slow it isn't a product.
  • `docker run --privileged` when "privileged" was doing device work — privileged containers work fine as far as capabilities and the guest kernel go. What doesn't work is passing through a device that isn't there. If the container's real requirement was `--device /dev/kvm`, no flag conjures it.
  • Hardware passthrough of any kind — no PCI, no GPU, no VFIO. Firecracker's device model doesn't include the transport those depend on.
  • "Just install the kvm module in the guest" — the module loads against CPU features that were never advertised. It'll log that there's no hardware support and stop, which is the correct behaviour and also the end of that idea.

The security argument, stated fairly

It's tempting to read "nesting is off" as a limitation someone was too lazy to remove. It's closer to the opposite. Think about what L0 does when nesting is enabled: it exposes an emulated virtualization interface, parses control structures written by an untrusted guest, maintains shadow page tables on that guest's behalf, and makes decisions about which exits to reflect where — all in the most privileged code in the system, on data that a guest fully controls. That is a large, intricate, high-value attack surface, and it exists only for guests that want to be hypervisors.

That doesn't make nesting reckless. It's widely deployed, maintained by people who take it seriously, and on a single-tenant host where you control every guest, enabling it is a perfectly ordinary operational decision. The point is narrower: on a host running code you didn't write, for people you've never met, turning on nesting adds a complex privileged emulator to the surface those people can reach. "Just enable nested virt" is a sentence with a security review hiding inside it. There's a quieter availability argument too, and it's the one more likely to actually bite you — nesting bugs tend to manifest as host-level instability rather than clean guest failures, because the confused code is running in the kernel. On a multi-tenant box, a guest that can wedge L0 is a guest that can wedge everyone. Between the two, "the sandbox does not offer hardware virtualization" turns out to be a feature you'd want even if it were free to remove.

The useful conclusion: fan out, don't stack

Almost every request for a VM inside a sandbox is really a request for a second isolation boundary. Someone has an untrusted step inside an already-untrusted job — a build that runs a downloaded script, an agent that wants a clean machine to test in, a test matrix where each case should be unable to see the others — and the mental model says "I need a VM in here." But nesting isn't the only way to get a fresh machine. It's just the way you reach for when creating a machine is expensive. If creating a machine is cheap, the answer is a sibling instead of a child: ask the platform for another guest at the same level, wire the two together over the network or through your orchestrator, and you get the same isolation property with none of the emulation tax and none of the surface expansion. Instead of one deep stack of hypervisors you get a wide, flat fan of guests, each with its own kernel, each independently reclaimable, each observable on its own.

This is exactly why boot latency is an architectural variable and not a vanity metric. On PandaStack every create restores a baked Firecracker snapshot rather than cold-booting: roughly 49ms for the restore step and 179ms p50 end to end, with p99 around 203ms. The ~3s cold boot happens once, at bake time. If you need a guest that shares state with an existing one, a same-host fork lands in 400–750ms (1.2–3.5s cross-host) with copy-on-write memory and disk. At those numbers, spawning a sibling is cheaper than the emulation overhead you'd have paid on a nested guest's boot alone — and capacity isn't the constraint either, since each agent pre-allocates 16,384 /30 subnets, so concurrency is bounded by host CPU and memory rather than network slots. I covered the fork side of this in /blog/snapshot-and-fork-explained.

from pandastack import Sandbox
from concurrent.futures import ThreadPoolExecutor, as_completed
import json

# The pattern you reach for INSTEAD of nesting. Every unit of work that
# "needs its own machine" gets a sibling guest at the same level, not a
# hypervisor stacked inside an existing guest. Same isolation property,
# no nested VM-exit amplification, no /dev/kvm required anywhere.

def run_case(case: dict) -> dict:
    sbx = Sandbox.create(
        template="code-interpreter",
        ttl_seconds=900,          # a wedged case is reclaimed, not babysat
        metadata={"suite": case["suite"], "case": case["id"]},
    )
    try:
        sbx.filesystem.write("/work/case.json", json.dumps(case, sort_keys=True))
        out = sbx.exec(
            "cd /work && python3 -m harness.run --case case.json --out result.json",
            timeout_seconds=600,
        )
        # Fail closed. A guest that died produces no result, not a partial
        # one -- which is the same discipline you'd want from a nested VM
        # and considerably easier to get right here.
        if out.exit_code != 0:
            return {"id": case["id"], "ok": False, "stderr": out.stderr[-2000:]}
        return {
            "id": case["id"],
            "ok": True,
            "result": json.loads(sbx.filesystem.read("/work/result.json")),
        }
    finally:
        # The guest, its kernel, its page cache and its mess go together.
        sbx.kill()


def fan_out(cases: list[dict], width: int = 32) -> list[dict]:
    """Width is bounded by host CPU and RAM -- nothing topological.

    Nesting would have bounded it by how much exit amplification the layer
    below could absorb, which is a lower ceiling and a far less predictable
    one. Flat beats deep whenever creating a machine is cheap.
    """
    with ThreadPoolExecutor(max_workers=width) as pool:
        futures = [pool.submit(run_case, c) for c in cases]
        return [f.result() for f in as_completed(futures)]
A useful test when someone asks for nesting: ask what the inner VM is for. If the answer is "so this step can't see that step," you want a sibling guest. If the answer is "I need to boot a specific kernel and watch it come up," you want either a sibling guest booted from a different template or full-system TCG. Only "I need to run a hypervisor as the workload under test" genuinely requires nesting — and that one is real, and belongs on bare metal.

The five environments, compared

  • Can run Firecracker — Bare metal: yes, always; this is the reference environment. Cloud instance with nested virt: yes, once the instance property is set and /dev/kvm appears. Cloud instance without: no, and nothing you do inside the guest changes it. Apple Silicon via Lima: yes, inside the Linux guest, on a recent chip and macOS. Inside a Firecracker guest: no — there is no /dev/kvm to open.
  • Can run KVM-accelerated QEMU — Bare metal: yes. Cloud instance with nested virt: yes, that's the same capability by another name. Cloud instance without: no; QEMU still runs, but only under TCG. Apple Silicon via Lima: yes inside the Linux guest. Inside a Firecracker guest: no acceleration at all; TCG only, and that is the honest ceiling.
  • Performance character — Bare metal: native; one level of VM-exit, nothing emulated. Cloud instance with nested virt: native for compute, amplified on every exit; boot and I/O-heavy phases hurt most, and the size of the hit is entirely workload-dependent. Cloud instance without: irrelevant, you're not nesting. Apple Silicon via Lima: fine for development, not a benchmarking surface — you are measuring three layers. Inside a Firecracker guest: TCG emulation, i.e. orders of magnitude off native, which is a real cost you accept deliberately.
  • Typical availability — Bare metal: available from every serious provider, at bare-metal prices and provisioning times. Cloud instance with nested virt: specific families only, and the details move — verify against your provider's current documentation. Cloud instance without: the default almost everywhere, including most hosted CI runners. Apple Silicon via Lima: any recent Apple Silicon Mac with a recent macOS; the standard local-development path. Inside a Firecracker guest: universally unavailable, by design, on every Firecracker platform.
  • Security posture — Bare metal: one hypervisor boundary, smallest surface, and you own the whole box. Cloud instance with nested virt: your provider has accepted the nested emulation surface on your behalf; you inherit both the capability and the risk. Cloud instance without: strictly smaller surface, which is exactly why it's the default. Apple Silicon via Lima: a development machine, so the threat model is your own code — treat accordingly. Inside a Firecracker guest: the absence of nesting is a deliberate surface reduction, not a missing feature.
  • Who controls the switch, and what to do when you hit the wall — Bare metal: you, via modprobe.d on your own kernel; nothing to work around. Cloud instance with nested virt: your provider, via an instance property requested at creation — and measure before you commit, since nested boot latency can quietly undo the reason you wanted microVMs. Cloud instance without: your provider, the answer is no, and the move is bare metal or a platform that already runs microVMs for you. Apple Silicon via Lima: you, via the VM config, subject to chip and macOS version — develop locally, benchmark elsewhere. Inside a Firecracker guest: nobody, because it isn't a switch but a property of the VMM; stop nesting and fan out into siblings.

Where this leaves you

Two questions, two answers. Can you run Firecracker inside a VM? Yes, whenever the hypervisor beneath you exposes virtualization extensions to its guests — bare metal always, specific cloud instance families when the flag is set, an Apple Silicon Mac through Virtualization.framework, and usually not a hosted CI runner. The mechanism is that L0 emulates a hypervisor interface for L1: it advertises the feature bits, traps the privileged instructions, shadows the control structures and the page tables, and reflects exits. Compute runs at native speed because L2's ordinary instructions execute on the real CPU; the cost shows up at the boundary, where one transition becomes several, and how much that hurts depends entirely on how exit-heavy your workload is.

Can you run a VM inside a Firecracker microVM? No. Firecracker does not expose VMX or SVM to its guests, so there is no /dev/kvm and nothing hardware-accelerates. Containers still work — they were never VMs — subject to the guest kernel actually shipping overlayfs, cgroups and netfilter. Cross-architecture and full-system emulation via TCG still work, and are often the right answer anyway because they were going to be software emulation regardless. Anything that genuinely wants KVM does not run, and that's a design property rather than a gap: nesting means adding a complex, privileged emulation path to the exact surface you built a microVM to shrink.

The conclusion I'd actually like you to take away is the architectural one. When you want VM-grade isolation for something inside a sandbox, the answer is usually not another hypervisor — it's another sandbox. Fan out across siblings instead of stacking turtles. That instinct only exists once creating a guest is genuinely cheap, which is the whole reason snapshot-restore matters beyond the benchmark: at 179ms p50 to create and 400–750ms to fork on the same host, a second guest costs less than the emulation overhead of a nested one, and it doesn't ask anybody's kernel to pretend to be a CPU feature it isn't.

For the layers around this: /blog/kvm-explained-for-developers covers what KVM and the VM-exit actually are, /blog/run-firecracker-on-mac has the working Apple Silicon setup, /blog/firecracker-vs-qemu explains why Firecracker's device model is so small in the first place, /blog/firecracker-vmm-security-model makes the surface argument properly, and /blog/microvm-vs-vm-vs-container places all of this next to the shared-kernel alternatives.

Frequently asked questions

Can you run Firecracker inside a virtual machine?

Yes, provided the hypervisor beneath you exposes CPU virtualization extensions to its guests — that's nested virtualization. Bare metal always works because there's no hypervisor above you. Cloud instances vary: some providers support nested virtualization on specific instance families as an explicit property you set at creation, and Google Cloud has supported it on certain instance types for a long time, but the exact families and flag names move, so check your provider's current documentation and then verify empirically. Apple Silicon Macs work through Virtualization.framework: you boot a Linux VM (Lima with the vz type and nested virtualization enabled), and that Linux guest gets a real /dev/kvm for Firecracker. Hosted CI runners usually do not work, because nesting is disabled at a layer you don't control and can't enable from inside the job.

Why is there no /dev/kvm inside a Firecracker microVM?

Because Firecracker deliberately does not expose virtualization extensions to its guests. It presents a minimal machine — a few virtio devices, a serial console, no PCI, no BIOS, no legacy hardware — and the CPU features advertised to the guest do not include Intel VMX or AMD SVM. Without those features the guest's KVM modules find no hardware support and /dev/kvm never appears. This isn't an oversight or a pending feature. Firecracker's entire value is a tiny, auditable VMM with the smallest possible surface between untrusted guests and the host, which is why AWS runs Lambda and Fargate on it. Supporting nesting would mean adding a complex privileged emulation path — shadowing guest-written control structures, composing nested page tables, reflecting exits — to precisely the surface the design exists to minimize.

How do I check whether nested virtualization is available on my machine?

Work top down. First, `grep -c -E '(vmx|svm)' /proc/cpuinfo` — zero means the layer below you exposes no virtualization extension and nothing local will fix it. Second, `lscpu | grep -i hypervisor` — if a hypervisor vendor is listed, you're already a guest, so everything below is a nested question. Third, `ls -l /dev/kvm` to confirm the node exists, and check you're root or in the kvm group, since a present-but-unopenable node is the most common false alarm. Fourth, `kvm-ok` (Debian/Ubuntu's cpu-checker package) for a plain-English verdict. Fifth, `cat /sys/module/kvm_intel/parameters/nested` (or kvm_amd) — Y or 1 means guests you boot here can themselves run VMs. Finally, `dmesg | grep -i kvm`, which usually names the actual reason, such as no hardware support or disabled by bios.

Can I run Docker inside a Firecracker microVM?

Yes. Containers are not virtual machines — they're processes isolated with namespaces and cgroups on the guest's own Linux kernel — so they need nothing from the CPU's virtualization extensions. Running dockerd inside a microVM works, and it has a pleasant property: the container's blast radius is a guest you were already planning to discard. The real caveat is the guest kernel's configuration rather than virtualization. Minimal microVM kernels are minimal on purpose, and container tooling expects overlayfs, cgroups v2, and a set of netfilter/iptables modules. When one of those is missing you get a confusing failure that looks nothing like a missing kernel option — the same wall you hit trying to run a full Kubernetes distribution in a stock microVM kernel, where the node comes up fine and then service networking simply doesn't work. What does not work is anything wanting `--device /dev/kvm`, since that device isn't there to pass through.

Should I enable nested virtualization on a multi-tenant host?

Treat it as a security decision rather than a configuration change. Enabling nesting means your kernel starts exposing an emulated virtualization interface to guests: it parses control structures those guests write, maintains shadow page tables on their behalf, and decides which VM-exits to reflect where — all in the most privileged code on the machine, on attacker-controllable data. That's a large, intricate, high-value attack surface, and it exists only to serve guests that want to be hypervisors. On a single-tenant host where you control every guest, enabling it is an ordinary operational choice. On a host running code you didn't write for people you've never met, it deserves a real review. There's an availability angle too: nesting bugs tend to manifest as host-level instability rather than clean guest failures, so a confused guest can affect every neighbour on the box.

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.