Why is seeding or creating randomness (e.g. SecureRandom) in a static initializer especially dangerous under build-time initialization?
answer
- build-time seed frozen -> all replicas share one stream
- predictable tokens/IDs/salts across the fleet
- GraalVM forces SecureRandom to run-time init
- 'instance of Random in image heap' build error = feature
- generalizes to clocks, env, secrets
basics
~20 sIf a random generator is created and seeded at build time, that seed is baked into the binary. Every deployed instance then produces the identical random sequence, so tokens, IDs, or keys become predictable across the whole fleet.
solid answer
~50 sBuild-time initialization runs the static initializer during compilation and freezes the resulting object into the image heap. If a `Random` or `SecureRandom` is instantiated (and thus seeded from the build machine's entropy) at build time, that exact seeded state is serialized into the binary. Every process started from that binary — every replica in production — inherits the identical internal state and emits the **same sequence** of 'random' values. For security tokens, session IDs, salts, nonces, or keys that is catastrophic: they become predictable and identical across instances. GraalVM guards against this by forcing `java.security.SecureRandom` to run-time initialization, and it errors if it detects a started `Random`/`SecureRandom` instance in the image heap. The lesson generalizes: never capture entropy, clocks, or per-deployment state at build time — keep such classes at run-time init so they re-seed at startup.
code
java · 19 lines// BAD: live generator captured in a static field (dangerous if build-time init)
public class BadTokens {
static final SecureRandom RNG = new SecureRandom();
static byte[] token() { var b = new byte[32]; RNG.nextBytes(b); return b; }
}
// GOOD: let Spring create the generator at runtime as a bean
@Configuration
class CryptoConfig {
@Bean
SecureRandom secureRandom() { return new SecureRandom(); } // seeded at startup
}
@Component
class GoodTokens {
private final SecureRandom rng;
GoodTokens(SecureRandom rng) { this.rng = rng; }
byte[] token() { var b = new byte[32]; rng.nextBytes(b); return b; }
}go deeper
Grasp that a build-time seed is copied into the binary so all instances produce identical 'random' values.
Explain the image-heap freeze, the fleet-wide predictability, and that GraalVM forces SecureRandom to run-time init.
Connect to CWE-330-style vulnerabilities, the build error as a guardrail, and refactoring to runtime beans.
Audit hand-rolled crypto/token utilities across services, set policy that entropy/clock/secret sources are never in static holders, and verify Spring Security's runtime-seeded generators are used instead.
## Recap of the mechanism Under `--initialize-at-build-time`, a class's static initializer executes **once, on the build machine**, and the resulting object graph is snapshotted into the **image heap** baked into the native binary. It is never re-run at startup. ## Why randomness is the worst case A pseudo-random generator like `java.util.Random` or `java.security.SecureRandom` is deterministic given its internal **seed/state**. Normally each JVM process seeds from fresh OS entropy at startup, so every process's stream differs. Now suppose: ```java public class Tokens { static final SecureRandom RNG = new SecureRandom(); // seeded here static String newToken() { byte[] b = new byte[32]; RNG.nextBytes(b); return Base64.getUrlEncoder().encodeToString(b); } } ``` If `Tokens` is initialized at **build time**, `RNG` is seeded from the *build machine's* entropy once, and that seeded state is frozen into the image. Consequences: 1. **Every deployed replica shares one seed.** Ten pods all emit the *same* token sequence in the same order. 2. **The stream is reproducible.** Anyone who can run the same binary (or leak the image) can reproduce the exact 'random' values. 3. Session IDs, password-reset tokens, CSRF nonces, salts, or generated keys become **predictable and colliding** — a severe vulnerability (think CWE-330 / CWE-337, use of insufficiently random / predictable seed). ## GraalVM's built-in protection GraalVM explicitly forces `java.security.SecureRandom` (and related security classes) to **run-time initialization** in its reachability metadata, precisely so the seed is drawn fresh at startup. If your own class captures a *started* `Random`/`SecureRandom` (or a `Thread`) in the image heap, the build fails with an error like: > `Detected an instance of Random/SplittableRandom in the image heap. Instances created during image generation ... must be initialized at run time.` That error is a feature: it forces you to add `--initialize-at-run-time` for the offending class. ## The generalization Randomness is one instance of a broader rule: **anything non-deterministic or environment-dependent must not be captured at build time.** Same reasoning applies to: - **Clocks**: `Instant.now()`, `System.currentTimeMillis()` freeze to build time. - **Secrets / env**: `System.getenv`, config files read at build. - **UUID-based identity** seeded from the above. ## Fixing it - Prefer **run-time init** (the default for your own classes) so generators re-seed at startup. - If a class must be build-time-init for framework reasons, don't let it hold a live generator — obtain the `SecureRandom` lazily inside a method, or inject it as a Spring bean created at runtime. - In Spring, generating tokens/salts is best done via runtime beans (e.g. a `SecureRandom` bean or Spring Security's crypto utilities), never in static holders. ## Spring specifics Spring Security's password encoders and CSRF token generators use `SecureRandom` internally; because Spring AOT + GraalVM metadata keep those at run-time init, they seed correctly. The risk is in **your own** hand-rolled static token utilities — those are what to audit.
- How does GraalVM prevent this from silently shipping?It forces SecureRandom to run-time init in its reachability metadata, and it fails the build if it detects a started Random/SecureRandom (or Thread) instance in the image heap, forcing an explicit --initialize-at-run-time.
- Beyond randomness, name two other things that must not be captured at build time and why.Clocks (Instant.now/currentTimeMillis freeze to build time, not deploy time) and secrets/env vars (System.getenv captures the build machine's value, usually null or a placeholder).
saying these in an interview costs you the question
- Thinking each deployed instance re-seeds its own generator regardless of build-time init
- Believing SecureRandom is 'secure' so it's safe to initialize at build time
- Assuming the static field re-runs its initializer at startup