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.
Two layers of randomness
Section titled “Two layers of randomness”A running Linux VM has random state at two layers, and they are reseeded differently:
| Layer | Example | Frozen into the snapshot? | Reseeded by AerolVM? |
|---|---|---|---|
| Kernel CSPRNG | getrandom(), /dev/urandom, crypto/rand, OpenSSL, secrets, SecureRandom | Yes | Yes — automatically on every resume |
| Userspace PRNG | Python random/numpy, Go math/rand, java.util.Random inside a long-lived process | Yes | No — your process must reseed itself |
What AerolVM does automatically
Section titled “What AerolVM does automatically”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
execor 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'ssecrets, Node'scrypto/WebCrypto, Java'sSecureRandom— 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.
Detect a clone from the SDK
Section titled “Detect a clone from 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();gen = client.clone_generation(sandbox_id)print(gen.generation, gen.resumedAt)# or, from a Sandbox handle:g = sandbox.clone_generation()gen, err := client.CloneGeneration(ctx, sandboxID)if err != nil { return err}fmt.Println(gen.Generation, gen.ResumedAt)// or, from a *Sandbox handle:g, err := sandbox.CloneGeneration(ctx)let gen = client.clone_generation(&sandbox_id)?;println!("{} {}", gen.generation, gen.resumed_at);// or, from a Sandbox handle:let g = sandbox.clone_generation()?;CloneGeneration gen = client.cloneGeneration(sandboxId);System.out.println(gen.generation + " " + gen.resumedAt);// or, from a Sandbox handle:CloneGeneration g = sandbox.cloneGeneration();Reseed your own process inside the guest
Section titled “Reseed your own process inside the guest”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"))}import osimport random
_GEN_FILE = "/run/aerolvm/clone-generation"_last_gen = None
def reseed_if_cloned() -> None: global _last_gen try: gen = open(_GEN_FILE).read().strip() except OSError: return # file absent: nothing to do if gen == _last_gen: return _last_gen = gen random.seed(os.urandom(2500)) # reseed the stdlib Mersenne Twister try: import numpy as np np.random.seed(int.from_bytes(os.urandom(4), "little")) except ImportError: pass// math/rand's global source is seeded once at startup and frozen into the// snapshot. Re-seed it from crypto/rand on clone. (math/rand/v2 and// crypto/rand read the OS and need no reseed.)var lastGen string
func reseedIfCloned() { b, err := os.ReadFile("/run/aerolvm/clone-generation") if err != nil { return // file absent: nothing to do } gen := strings.TrimSpace(string(b)) if gen == lastGen { return } lastGen = gen var seed [8]byte _, _ = cryptorand.Read(seed[:]) mathrand.Seed(int64(binary.LittleEndian.Uint64(seed[:])))}// rand::thread_rng() reseeds from the OS periodically, but a StdRng/SmallRng// you hold across the snapshot is frozen. Re-create it from entropy on clone.use rand::SeedableRng;use std::fs;
fn reseed_if_cloned(last_gen: &mut String, rng: &mut rand::rngs::StdRng) { let gen = match fs::read_to_string("/run/aerolvm/clone-generation") { Ok(s) => s.trim().to_string(), Err(_) => return, // file absent: nothing to do }; if &gen == last_gen { return; } *last_gen = gen; *rng = rand::rngs::StdRng::from_entropy();}// java.util.Random is seeded once and frozen into the snapshot. Re-seed it// from SecureRandom on clone. SecureRandom itself reads the OS and is safe.private String lastGen = null;
void reseedIfCloned(java.util.Random rng) { String gen; try { gen = java.nio.file.Files.readString( java.nio.file.Path.of("/run/aerolvm/clone-generation")).trim(); } catch (java.io.IOException e) { return; // file absent: nothing to do } if (gen.equals(lastGen)) { return; } lastGen = gen; rng.setSeed(new java.security.SecureRandom().nextLong());}Further reading
Section titled “Further reading”For how the kernel reseed and clock resync are implemented on the resume path, see Snapshot Clone Correctness.