Skip to content

Randomness in Cloned Sandboxes

When a Firecracker sandbox boots from a snapshot template, AerolVM clones the entire memory image of the template VM. That is what makes clones start in milliseconds — but it also means every clone begins life with an identical copy of the template's random-number generator state. Two clones that draw "random" bytes after resuming can produce the same values: duplicate UUIDs, duplicate session tokens, duplicate cryptographic nonces or keys.

This page explains what AerolVM handles for you automatically, where a gap remains, and how to close it for your own long-lived processes.

A running Linux VM has random state at two layers, and they are reseeded differently:

LayerExampleFrozen into the snapshot?Reseeded by AerolVM?
Kernel CSPRNGgetrandom(), /dev/urandom, crypto/rand, OpenSSL, secrets, SecureRandomYesYes — automatically on every resume
Userspace PRNGPython random/numpy, Go math/rand, java.util.Random inside a long-lived processYesNo — your process must reseed itself

On every resume-from-snapshot, the in-guest agent force-reseeds the kernel CSPRNG from fresh host entropy. The reseed completes before any process you newly start through the SDK runs, which means:

  • Any process you start after the clone resumes (for example, each exec or code-run invocation spawns a fresh interpreter) seeds from the freshly-reseeded kernel and is already clone-distinct.
  • Anything that reads the OS CSPRNG per call — crypto/rand, Python's secrets, Node's crypto/WebCrypto, Java's SecureRandom — is safe with no action on your part.

In short: fresh processes and crypto RNGs are handled for you.

The one timing caveat is for a process already running inside the template when the clone resumes: it can be scheduled in the brief window before the agent's reseed lands. The engineering deep-dive covers that window and how Firecracker's vmgenid closes it where available — see Snapshot Clone Correctness. It is the same long-lived-process case described next.

The remaining gap: long-lived userspace PRNGs

Section titled “The remaining gap: long-lived userspace PRNGs”

The one case AerolVM cannot fix from outside is a long-lived process baked into the template snapshot that holds a userspace PRNG in its own memory — for example, a preloaded Python interpreter that already did import numpy. The interpreter's random/numpy seed is frozen into the snapshot, and the kernel reseed does not reach into another process's heap. Every clone of that template would then draw the same random/numpy sequence.

The agent publishes a clone-generation token so such a process can detect the clone and reseed itself. The token changes on every resume. Read it either from the well-known file /run/aerolvm/clone-generation (fast, local, no auth) or via the SDK.

cloneGeneration() returns the current token and the time of the last resume. Poll it to detect that a sandbox has been cloned or migrated — for example, to re-run setup. (This is a read-only signal; it does not reseed a process inside the guest.)

const gen = await vm.cloneGeneration(sandboxID);
console.log(gen.generation, gen.resumedAt);
// or, from a Sandbox handle:
const g = await sandbox.cloneGeneration();

If your template keeps a long-lived process alive across the snapshot, have that process read the clone-generation token and reseed its userspace PRNG when the token changes. The snippets below run inside the sandbox, in the language of your workload. Crypto RNGs are already safe (see above) — reseed only the userspace, non-crypto generators you control.

import { readFileSync } from "node:fs";
// Node's Math.random() cannot be reseeded (V8 internal), and crypto /
// WebCrypto read the OS CSPRNG directly — already safe after the kernel
// reseed. Use this hook to reseed any third-party seeded PRNG you hold.
let lastGen: string | undefined;
export function reseedIfCloned(reseed: () => void): void {
let gen: string;
try {
gen = readFileSync("/run/aerolvm/clone-generation", "utf8").trim();
} catch {
return; // file absent: nothing to do
}
if (gen === lastGen) return;
lastGen = gen;
reseed(); // e.g. seedrandom(crypto.randomBytes(16).toString("hex"))
}

For how the kernel reseed and clock resync are implemented on the resume path, see Snapshot Clone Correctness.