all posts

Firecracker virtio-rng and Guest Entropy Explained

Ajay Kumar··10 min read

There's a category of bug that never shows up in a demo, passes every functional test, and then quietly hands two of your users the same TLS private key. It has nothing to do with your code being wrong. It's about where the randomness came from. And on a platform that boots machines by restoring the same memory snapshot over and over — which is exactly what a Firecracker sandbox platform does — the randomness story is subtle enough to be worth a whole post. I'm Ajay; I build PandaStack, an open-source Firecracker microVM platform, so I spend a lot of time thinking about what a guest kernel believes about its own entropy at the exact instant it wakes up from a snapshot. Spoiler: it believes something false, and the fix is a device most people never look at.

Why a freshly booted VM is starved of entropy

Start with a normal Linux machine on real hardware. Its cryptographic randomness comes from the kernel's CSPRNG — the thing behind `/dev/random`, `/dev/urandom`, and the `getrandom()` syscall. That CSPRNG isn't magic; it's a deterministic algorithm that produces unpredictable output only because it's been seeded with real entropy: unpredictable bits harvested from the physical world. On bare metal the kernel scrapes those bits from interrupt timing, disk seek jitter, keyboard and mouse timings, and — most importantly on modern CPUs — a hardware RNG instruction like RDRAND. Give the CSPRNG a good seed once, and it stays cryptographically strong forever after.

A freshly booted microVM has almost none of those sources. There's no keyboard, no mouse, no spinning disk with mechanical jitter, and the interrupt timing inside a paravirtualized guest is thin and regular. The classic boot-time entropy problem — the one that gave Linux its old "uninitialized urandom" warnings — is much sharper inside a VM, because a microVM is deliberately stripped of exactly the noisy hardware the kernel used to lean on. So in the first moments of boot, before the CSPRNG has been properly seeded, a program that asks for randomness is in a dangerous spot: either it blocks waiting for entropy that may be slow to arrive, or it gets bytes from a not-yet-fully-seeded pool.

Modern kernels handle this far more carefully than they used to — `getrandom()` blocks until the CSPRNG is initialized rather than handing out weak bytes, and once initialized it never blocks again. But "blocks until initialized" just moves the question: how does a device-starved microVM get initialized quickly enough that your app's first `getrandom()` doesn't stall the whole boot? That's the job the entropy device exists to do.

The mental model: the kernel CSPRNG is a strong stretcher of a good seed. The entire game is getting one good seed into it, fast, on a machine that has almost no natural sources of physical noise. Everything below is about where that seed comes from.

virtio-rng: piping host entropy into the guest

Firecracker's answer is a virtio entropy device — virtio-rng. It's one of the handful of devices Firecracker emulates at all, and its whole purpose is to be a pipe: the guest kernel asks the device for random bytes, and the device fills the request from the host's own well-seeded entropy source. The host is a full Linux machine with real hardware noise and a long-running, properly-initialized CSPRNG. virtio-rng lets the entropy-poor guest borrow from the entropy-rich host.

Mechanically it rides the same virtio machinery as every other Firecracker device: a virtqueue. The guest's `virtio-rng` driver posts buffers into the queue saying "fill these with random bytes," notifies the device, and Firecracker reads host randomness into those guest buffers and marks them done. The guest kernel feeds the result into its CSPRNG as fresh entropy. There's no register-level chip emulation and nothing clever — it's a thin conduit from the host's randomness to the guest's entropy pool, which is exactly why it's a small, auditable device rather than a scary one.

You can watch this from inside a guest. The kernel exposes an estimate of how many bits of entropy it currently has, and whether its CSPRNG is initialized:

# How many bits of entropy the kernel thinks it has right now.
# On modern kernels the CSPRNG only needs to be seeded ONCE; after that
# this number matters far less than whether initialization has happened.
$ cat /proc/sys/kernel/random/entropy_avail
256

# Is a hardware/virtual RNG registered? In a Firecracker guest you should
# see the virtio entropy device here (name may vary by kernel/version).
$ cat /sys/devices/virtual/misc/hw_random/rng_available
virtio_rng.0
$ cat /sys/devices/virtual/misc/hw_random/rng_current
virtio_rng.0

# getrandom() is the API your crypto libraries actually use. On a modern
# kernel it BLOCKS until the CSPRNG is initialized, then never blocks again.
# A quick sanity check that a read returns promptly (not that it's "random"):
$ python3 -c "import os,time; t=time.time(); os.getrandom(32); print(round((time.time()-t)*1000,2), 'ms')"
0.03 ms

# The old trap: reading /dev/urandom before init used to return unseeded
# bytes silently. Don't design around /dev/urandom timing; use getrandom().

One honest caveat: modern Linux can also seed itself very early straight from the CPU's RDRAND/RDSEED instructions when the hardware exposes them, so on many setups the CSPRNG is initialized before virtio-rng even matters. virtio-rng is the belt to that suspenders — it guarantees a host-quality entropy source is available even where the guest can't or won't trust the CPU instruction. In practice you want both, and the exact interplay depends on your guest kernel config, so verify what your kernel actually registers rather than assuming.

The entropy rate limiter (and why it exists)

Here's a subtlety that trips people up: entropy is a shared, finite-feeling host resource, and a guest is untrusted. A hostile or buggy guest could hammer the virtio-rng device in a tight loop, pulling randomness as fast as the host can produce it, and in a multi-tenant fleet that's a noisy-neighbor vector — one tenant draining a host resource that every co-located guest also draws on. So Firecracker lets you attach a rate limiter to the entropy device, the same token-bucket rate-limiter shape it offers for block and network devices: a bandwidth budget (bytes over a refill window) and optionally an operations budget, so no single guest can monopolize host entropy.

The config lives alongside the device definition. Treat the exact field names and units below as illustrative — the rate-limiter object shape and the way you attach the entropy device evolve across Firecracker versions, so verify against the Firecracker API docs for your build before wiring it in:

// PUT the entropy device to the Firecracker API socket before boot.
// The rate_limiter caps how fast this guest can pull host randomness,
// so one tenant can't starve entropy for co-located guests.
//
// NOTE: field names / units are illustrative — verify the exact
// entropy-device + rate_limiter schema against the Firecracker docs
// for YOUR version, as this surface has changed over releases.
{
  "rate_limiter": {
    "bandwidth": {
      "size": 65536,        // bytes per refill window (the token bucket)
      "refill_time": 1000   // window length in milliseconds
    }
  }
}

For most sandbox workloads you don't need to think hard about this: a guest needs a burst of entropy at boot to seed its CSPRNG once, and then very little after that. The rate limiter is there for the pathological case — the guest that treats virtio-rng as an infinite faucet — not the normal one. Set it generously enough that legitimate boot-time seeding is instant, tight enough that a single guest can't drain the host. And keep in mind that the CSPRNG only needs to be seeded once: if you rate-limit so aggressively that initial seeding is slow, you've just made every boot's first `getrandom()` stall, which is the opposite of what you wanted.

The snapshot footgun: shared RNG state

Now the part that actually keeps me up at night, and the reason this post exists. Everything above is about a cold boot. But a snapshot-restore platform doesn't cold-boot — it restores a saved memory image. And a memory image includes the guest kernel's CSPRNG state and, worse, any random state your userspace processes had already computed and cached in memory at the moment the snapshot was taken.

Think about what that means. You boot a template VM once, let it seed its CSPRNG, and snapshot it. Now you restore that one image into a hundred sandboxes. At the instant of restore, all hundred guests have byte-for-byte identical CSPRNG internal state. They are not a hundred independent random number generators. They are one random number generator, cloned a hundred times. If nothing reseeds them, the next `getrandom()` in each of those hundred guests returns the same bytes. Generate a TLS key, a session token, a nonce, a UUID that's supposed to be unpredictable — and you've generated the same one in every clone.

This is the real cryptographic footgun of snapshot-restore: restore one memory image into many guests and they share RNG state, so they can produce duplicate TLS keys, colliding nonces, and predictable "random" tokens. Two VMs that agree on a random number are no longer random — they're a coincidence you can factor. If your platform restores the same snapshot into many sandboxes, reseeding after restore is not optional.

This isn't a hypothetical. It's a rerun of a well-documented disaster. The 2012 "Mining Ps and Qs" study scanned the public internet and found large numbers of RSA and DSA keys that shared factors or were outright duplicated — and the root cause was devices generating keys at first boot with insufficient or identical entropy. Cloning a VM from a snapshot is the same failure with a faster clock: instead of thousands of routers independently under-seeding, you have one under-diversified seed copied into every restore. The keys don't look weak. They look fine. They're just not unique, and "not unique" is fatal for a private key.

How a guest reseeds after restore

The good news is that this problem is solvable, and both the kernel and the hypervisor have grown machinery specifically for it. There are a few layers, and a robust platform leans on all of them.

  • VM-generation-ID / snapshot-restore detection. The mechanism that fixes this cleanly is a hardware-exposed "VM generation ID" — a value the hypervisor changes whenever a VM is restored from a snapshot or forked. Modern Linux watches this ID; when it changes, the kernel knows it's a clone and immediately reseeds its CSPRNG, mixing in fresh entropy and marking previously-generated state as no longer trustworthy for the new instance. This is the load-bearing defense against snapshot cloning. Whether it's active depends on your guest kernel and how the VMM exposes the generation ID, so verify it's wired up for your Firecracker build rather than assuming — this is the single most important thing to confirm.
  • virtio-rng after restore. Because the entropy device is a live pipe to the host, the restored guest can immediately pull fresh, host-diverse randomness the moment it resumes — a different draw for every clone, because the host CSPRNG keeps moving. That fresh draw is what a reseed mixes in.
  • RDRAND / ARCH_RANDOM. On CPUs that expose it, the guest can also pull entropy straight from the hardware RNG instruction, which returns fresh bytes per call regardless of what the snapshot froze. This is a per-instance source that a cloned memory image can't have pre-computed, so it's a natural ingredient for a post-restore reseed.
  • Explicit userspace reseeding for the truly paranoid. Kernel-level reseed protects anything that calls getrandom() after the reseed fires. But a userspace process that generated and cached a secret in its own memory before the snapshot — a pre-forked TLS context, a seeded application PRNG — won't magically un-cache it. The belt-and-suspenders answer is to generate long-lived secrets after restore, not bake them into the template.

The order of operations that keeps you safe: the hypervisor signals "you were restored," the guest kernel reseeds its CSPRNG from virtio-rng plus RDRAND, and only after that does anything cryptographic ask for randomness. Get that ordering right and every clone diverges immediately; get it wrong and every clone marches in lockstep until the first natural reseed, which might be far too late — the TLS handshake fires in the first second.

What a snapshot-restore platform has to guarantee

This is where the internals stop being trivia and start being an operational responsibility, because a platform like PandaStack restores from snapshots as its normal, every-request path. Every sandbox create restores a baked template snapshot — the restore itself is around 49ms, an end-to-end create is roughly 179ms p50 and 203ms p99, and only the very first spawn of a template pays the ~3s cold boot. Forks are the same story with copy-on-write memory: a same-host fork lands in 400–750ms, cross-host in 1.2–3.5s. Every one of those paths clones a memory image, which means every one of those paths clones CSPRNG state. The whole moat — fast, cheap, VM-per-request isolation — runs directly into the shared-entropy problem unless reseed-on-restore is treated as a hard invariant, not a nice-to-have.

So the rule I hold the platform to is simple to state: a restored guest must reseed before it does anything cryptographic, and no long-lived secret may be baked into a template that gets cloned. Concretely that means the entropy device is present on every guest, the VM-generation-ID reseed path is confirmed working on the guest kernel we ship, and the template-baking discipline is that you bake the environment, never the secrets — TLS keys, host keys, tokens, and application seeds are generated on the guest after restore, when it's already a unique instance. From the SDK you don't touch any of this; you just need to know it's happening under every create:

from pandastack import Sandbox

# Every sandbox here is a snapshot RESTORE of one baked template image.
# That means every one of these starts from the SAME frozen memory —
# including CSPRNG state — until the guest reseeds on restore.
sandboxes = [Sandbox.create(template="base") for _ in range(5)]

# If reseed-on-restore is working, each guest has already mixed in fresh
# host + hardware entropy, so these must all DIFFER. If they ever match,
# the reseed path is broken and you have a duplicate-key footgun.
for sbx in sandboxes:
    r = sbx.exec("python3 -c \"import os; print(os.urandom(16).hex())\"")
    print(r.stdout.strip())    # expect 5 distinct 32-hex-char lines

# The rule the PLATFORM must hold, not you: a restored guest reseeds
# before it does anything cryptographic. The rule for YOUR code: never
# bake a long-lived secret (TLS key, token, host key) into a template
# snapshot that gets cloned. Generate it AFTER create, on the live guest.
for sbx in sandboxes:
    sbx.destroy()

If you run your own Firecracker fleet, the takeaway is the same even without a platform in front of it: attach the entropy device, confirm your guest kernel reseeds on snapshot-restore via the generation ID, and audit your template images for any secret material that was generated once and would therefore be identical across every restore. That last audit catches the sneaky cases — an SSH host key baked into the rootfs, a pre-generated API token in the image, a service that seeds its own PRNG at build time. Those don't get fixed by kernel reseeding, because they were computed before the snapshot and cached where the reseed can't reach.

The bottom line

A fresh microVM is entropy-starved because it's deliberately stripped of the noisy hardware Linux normally harvests randomness from, and virtio-rng fixes that by piping host entropy into the guest so its CSPRNG can seed quickly — with an optional rate limiter so one tenant can't drain host randomness. But the sharper, less-obvious danger belongs to snapshots: restore one memory image into many guests and they share CSPRNG state, which is a straight path to duplicate TLS keys and predictable nonces — the Mining Ps and Qs failure, sped up. The defense is reseed-on-restore: a VM-generation-ID that tells the guest kernel it was cloned so it reseeds from virtio-rng and RDRAND before anything cryptographic runs, plus the discipline of never baking secrets into a template. On a platform whose entire performance story is snapshot-restore, that's not an edge case to remember — it's an invariant to enforce on every create. Two VMs that agree on a random number aren't lucky. They're a bug you can factor.

Frequently asked questions

Why is a freshly booted microVM starved of entropy?

The Linux CSPRNG behind /dev/random and getrandom() only produces unpredictable output after it's been seeded with real physical entropy — normally harvested from interrupt timing, disk jitter, input-device timing, and hardware RNG instructions. A microVM is deliberately stripped of most of that noisy hardware (no keyboard, mouse, or spinning disk, and thin paravirtualized interrupt timing), so at early boot the CSPRNG may not be initialized yet. Modern kernels handle this by blocking getrandom() until initialization rather than returning weak bytes, but that just means you need a fast seed source — which is what Firecracker's virtio-rng entropy device provides by piping host randomness into the guest.

What is Firecracker's virtio-rng device and what does it do?

virtio-rng is Firecracker's paravirtualized entropy device — one of the small set of devices it emulates. It's a thin pipe: the guest kernel's virtio-rng driver posts buffers into a virtqueue asking to be filled with random bytes, Firecracker fills them from the host's well-seeded CSPRNG, and the guest mixes the result into its own entropy pool. This lets an entropy-poor guest borrow randomness from the entropy-rich host so its CSPRNG initializes quickly. On CPUs that expose RDRAND/RDSEED the guest can also seed straight from the hardware instruction; virtio-rng is the guaranteed host-quality source alongside that. Verify exactly what your guest kernel registers, since it depends on kernel config.

What is the entropy rate limiter for?

Because entropy is an untrusted-guest-accessible host resource, a hostile or buggy guest could hammer virtio-rng in a loop and drain host randomness in a multi-tenant fleet — a noisy-neighbor vector. Firecracker lets you attach a token-bucket rate limiter (a bandwidth budget over a refill window, optionally an operations budget) to the entropy device, the same shape it offers for block and network devices, so no single guest can monopolize host entropy. Set it generous enough that boot-time seeding is instant but tight enough to cap abuse — the CSPRNG only needs seeding once, so over-aggressive limits just stall the first getrandom(). Verify the exact field names and units against the Firecracker docs for your version.

Why is restoring the same snapshot into many VMs a cryptographic risk?

A memory snapshot includes the guest kernel's CSPRNG state (and any random state userspace already cached in memory). Restore that one image into many guests and, at the instant of restore, every clone has byte-for-byte identical CSPRNG state — they're one RNG cloned N times, not N independent RNGs. Without reseeding, the next getrandom() returns the same bytes in every clone, so they can generate duplicate TLS private keys, colliding nonces, and predictable tokens. It's the 2012 'Mining Ps and Qs' duplicate-key disaster with a faster clock. The keys look fine; they're just not unique, which is fatal for a secret. Any platform that restores the same snapshot into many sandboxes must reseed after restore.

How does a guest reseed its RNG after a snapshot restore?

The clean fix is a hypervisor-exposed VM generation ID that changes on restore or fork; modern Linux watches it and, when it changes, immediately reseeds its CSPRNG and stops trusting previously-generated state for the new instance. The reseed mixes in fresh entropy from virtio-rng (a live pipe to the host, so a different draw per clone) and from RDRAND/ARCH_RANDOM where available (fresh per call, so a cloned image can't have pre-computed it). The critical ordering is: hypervisor signals 'you were restored' → kernel reseeds → only then does anything cryptographic ask for randomness. Confirm the generation-ID path is active for your guest kernel and Firecracker build, and never bake long-lived secrets into a cloned template, since userspace secrets cached before the snapshot won't be fixed by a kernel reseed.

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.