V. Shahdeo — “Math.random() is not so random: The Illusion of Randomness”: A cryptanalytic analysis of V8’s historical 32-bit precision

11.09.2026

V. Shahdeo — "Math.random() is not so random: The Illusion of Randomness": A cryptanalytic analysis of V8's historical 32-bit precision

A technical article for WordPress.  Subject: evolution  Math.random() in V8, the MWC1616 limitation, cryptanalytic implications, and secure engineering alternatives.

Annotation

V. Shahdeo’s article explores the fundamental difference between statistical “randomness” and cryptographic unpredictability. The central historical fact is that, until late 2015, V8 (Chrome and many versions of Node.js) used the MWC1616 generator, which  Math.random() produced output with only 32 bits of discrete precision: the value belonged to a set of the form  x / 2^32. Therefore, constructing long “random” identifiers by concatenating calls did not transform the generator into a source of 132-bit or 256-bit entropy: the available set of trajectories was limited by the generator’s state and period.

Cryptanalytically, this implies two independent threats: (1) a sharp increase in the probability of collisions due to the small effective state space, and (2) the restoration or narrowing of the internal state from observed outputs, making it possible to predict future values. The story of Betable and the publicly demonstrated predictability of CSGOJackpot lottery outputs demonstrate that RNG selection bias transforms an abstract statistical flaw into a practical compromise of business logic.

Key concepts

TermMeaning for analysisSafety criterion
PRNGDeterministic generator: the state  S_t is updated by the function  F, the output is generated by the function  G.May be suitable for modeling, but is not necessarily resistant to state restoration itself.
CSPRNGA generator whose outputs must be computationally indistinguishable from uniform for an efficient attacker.Knowledge of the algorithm and a portion of the outputs should not allow one to predict the next output with a practical advantage.
Output accuracyThe number of distinct values ​​returned by the API in a single call.It is not identical to the size of the state and does not prove cryptographic resistance.
EntropyA measure of uncertainty; for a secret, the min-entropy is important, not the visual randomness of the string.For a token, the size of the search space must match the threat model.

The basic PRNG model is:
\[ S_{t+1}=F(S_t), \qquad Y_t=G(S_t). \]
Given ,  S_t all futures  Y_{t+k} are computable. Therefore, the cryptographic problem is not to hide the algorithm, but to make the state inaccessible and unrecoverable.

Specification and boundaries

ECMAScript requires  Math.random() a return value within an interval  [0, 1) with an approximately uniform distribution, but leaves the algorithm to the engine’s implementation. This doesn’t guarantee a fixed state length, cryptographic strength, or portability between browsers and engine versions.

This is an important distinction for auditing: an interface with the same name is not a cryptographic primitive. The specific runtime version, algorithm, method of converting internal words to  double, output leaks, and value assignment in the application must be assessed.

Historic V8: MWC1616

The early V8 used the MWC1616, a multiply-with-carry variant with two 16-bit components. In simplified form, its transition can be represented as two multiply-with-carry operations, preserving the low-order 16 bits and the carry; the parts are then combined into a 32-bit word.

Schematic model of the MWC component:
\[ x_{t+1} = (a(x_t \bmod 2^{16}) + \lfloor x_t/2^{16}\rfloor) \bmod 2^{32}. \]
For a pair of components  x,  y the output is assembled from 16-bit parts:
\[ R_t = ((x_{t+1} \bmod 2^{16}) \ll 16) + (y_{t+1} \bmod 2^{16}), \qquad U_t=R_t/2^{32}. \]

A practically significant property of the old V8: the return value  U_t could only take on about  2^32 different values, although the IEEE-754 double binary format allows for the representation of significantly more discrete fractions in this range. The MWC1616 period was limited to a value of the order of  2^32, and the structure also had shorter cycles; for systems with intensive generation, this is a qualitatively different risk profile than what a developer expects when thinking in terms of 128-bit or 132-bit identifiers.

Accuracy does not equal safety

CharacteristicOld V8 / MWC1616Modern V8 / xorshift128+CSPRNG OS / Web Crypto
Exit Math.random()About 32 distinguishable bits per callPractically uses fractional precision double, but the API is not cryptographicOutputs bytes via a secure API
Condition/periodOrder restrictions  2^32, short cycles possible128-bit state, period 2^128 - 1Properties are OS and crypto library dependent; designed for unpredictability
Cryptographic suitabilityNoNoYes, for the regular creation of key material, tokens and nonce with the correct protocol

Methodological caveat.  Improvements to V8 since 2015 have fixed a historical flaw in the 32-bit output of MWC1616, but do not change the regulatory rule:  Math.random() it cannot be used for secrets, keys, tokens, nonces, winnings, or other values ​​that should be difficult for an adversary to predict.

Entropy ceiling

Let’s say an application generates a string of 22 symbols of an alphabet of cardinality 64. The combinatorial space of the strings is  64^22 = 2^132. However, if the entire string is deterministically obtained from the trajectory of a generator with an effective state of about 32 bits, the number of reachable variants and the number of possible future trajectories are limited by the generator, not by the visual length of the string.

Nominal identity space:
\[ |\Omega_{\text{nom}}|=64^{22}=2^{132}. \]
For a deterministic mapping from a set of states,  \mathcal{S} the number of possible outcomes is at most the number of states:
\[ |\mathrm{Im}(H)| \le |\mathcal{S}|, \qquad H:\mathcal{S}\to\Omega_{\text{nom}}. \]
If the effective trajectory has size on the order of  2^{32}, then the upper bound on the effective uncertainty is about 32 bits, not 132 bits.

This illustrates a general cryptanalytic principle: concatenating dependent pseudorandom outputs does not unconditionally add up the entropy. It merely encodes a longer string if all blocks depend on a single small internal state.

Collisions: Betable example

In a historical case described by V. Shahdeo, Betable was building 22-character identifiers on a  Math.random() Node.js database and encountered a duplicate that seemed impossible given the model of independent 132-bit values. The cause wasn’t an incorrect birthday formula, but an incorrect source model: the old V8 didn’t explore the space uniformly  2^132.

For uniform independent samples from a space of size ,  N the probability of at least one collision after  n samples is approximately equal to:
\[ P_{\mathrm{coll}} \approx 1-\exp\!\left(-\frac{n(n-1)}{2N}\right). \]
For small probability: \[ P_{\mathrm{coll}} \approx \frac{n(n-1)}{2N}. \]
Substituting  N=2^{132} for the effective one  N\lesssim2^{32} changes the estimate by a factor of order  2^{100}.

The Betable archival lesson is important for distributed systems, queues, invitation links, and deduplication keys: a “long string” does not prove either uniqueness or entropy. Design documentation must document the source of randomness, the encoding format, the collision handling at the storage level, and the requirement for unpredictability.

Forecasting as cryptanalysis

The second, more dangerous line of attack is state recovery. For unprotected generators, the observed outputs carry equations and constraints on the internal words; the cryptanalyst uses reversibility, linearity, carries, truncation, and brute force on the remaining unknown bits to narrow down the candidate set.

After observing the outputs,  y_1,\ldots,y_m the attacker solves the matching problem:
\[ \text{find } S_0 \in \mathcal{S}:\; G(F^i(S_0))=y_{i+1},\quad 0\le iIf  S_0, the next output is predicted as:
\[ \widehat{y}_{m+1}=G(F^m(S_0)). \]

A historical example cited in the source material is CSGOJackpot: the lottery app published results based on the old Node/V8  Math.random(); observing multiple outputs allowed the MWC state to be reconstructed and subsequent results to be predicted. The vulnerability isn’t rooted in “bad luck,” but in a violation of the unpredictability requirement with an adaptive adversary who can see past rounds and act in the next.

Typical attack chain

  1. Collect public values: odds, numbers, tokens, URL parts, or draw results.
  2. Normalize format: restore scaling  floor(U·M), Base64/hex encoding and lost bits.
  3. Form constraints on the PRNG state and perform brute force, SAT/SMT solution, or algebraic recovery.
  4. Check the candidates on the following observations.
  5. Predict a future secret or financially significant exit and exploit it before the state rotation.

A real-life analog from the history of cryptanalysis is the flaw in predictable nonces in digital signatures: if the ECDSA nonce is insufficiently random, repeated, or partially revealed, the secret key can be recovered from the signatures. Although the MWC mechanism differs from ECDSA, the root flaw is identical: a deterministic/weak source of randomness turns a mathematically strong construction into a solvable system of equations.

Communication with ECDSA and nonce

For an elliptic ECDSA signature, the random nonce  k is critical. When signing a hash  z with a private key,  d is calculated  s = k^{-1}(z + rd) mod n. If  k known to the adversary, the private key is recovered directly; if the same key  k is repeated in two signatures, is recovered first  k, then  d.

ECDSA:
\[ s \equiv k^{-1}(z+rd) \pmod n. \]
Given  k:
\[ d \equiv (sk-z)r^{-1} \pmod n. \]
For two signatures with the same  r (i.e., reused  k):
\[ k \equiv (z_1-z_2)(s_1-s_2)^{-1} \pmod n, \qquad d \equiv (s_1k-z_1)r^{-1} \pmod n. \]

The archival Sony PlayStation 3 incidents and the 2013 Android Bitcoin wallet compromise clearly demonstrate the consequences of reusable or faulty nonces: strong asymmetric cryptography does not compensate for the weakness of nonce generation. Therefore, using  nonces Math.random() for  kkeys, seed phrases, IVs, reset tokens, or CSRF tokens is not a “downgrade,” but a violation of the protocol’s security assumptions.

Modern V8

Following the response to the issue, V8 replaced MWC1616 with xorshift128+, which has a 128-bit internal state and period  2^128 - 1. This change was introduced in V8 in late 2015 with the release of Chrome 49. This represents significant progress for non-cryptographic tasks such as modeling, visualization, gaming, and test data.

However, xorshift128+ remains a non-cryptographic PRNG. Its linear structure over GF(2) is convenient for high speed and statistical tests, but is not designed to protect against state recovery; statistically passing a battery of tests does not prove next-bit unpredictability.

A typical xorshift transition uses linear XOR and shift operations:
\[ x \leftarrow x \oplus (x\ll a),\qquad x \leftarrow x \oplus (x\gg b),\qquad x \leftarrow x \oplus (x\ll c). \]
The linearity of the transition over  GF(2) means that, given sufficient observations and a known output mapping, the state can be sought by solving a system of linear/Boolean constraints. Adding the words ( +) in the “plus” variant can complicate the observable relationship, but does not turn the construction into a CSPRNG.

Secure APIs

In a browser, secret values ​​should be stored using  crypto.getRandomValues() or , when the format is appropriate  crypto.randomUUID(). In Node.js, the default choice is  node:crypto, in particular  randomBytes,  randomInt and  randomUUID; the Node.js documentation describes  randomBytes Web Crypto  getRandomValues as generating cryptographically strong pseudo-random data.

Browser: 256 bit token

function secureToken(bytes = 32) {
  const data = new Uint8Array(bytes);
  crypto.getRandomValues(data);
  return Array.from(data, b => b.toString(16).padStart(2, '0')).join('');
}

const passwordResetToken = secureToken(); // 64 hex-символа = 256 бит сырых данных

Historical context.  Predictable session IDs and password-reset tokens have led to numerous account takeovers: if the value is a bearer secret, guessing it once is sufficient. The code above uses the system’s cryptographic source, not internal state  Math.random().

Node.js: token and range

import { randomBytes, randomInt } from 'node:crypto';

const apiToken = randomBytes(32).toString('base64url');
const lotteryIndex = randomInt(0, 1_000_000); // криптографически стойкий выбор

console.log({ apiToken, lotteryIndex });

Historical context.  In gambling and financial services, publishing PRNG outputs without a cryptographic scheme triggers state recovery. Verifiable integrity requires not [a]  Math.random(), but a commit-reveal protocol, VRF, or another formally defined mechanism along with a CSPRNG.

Uniform mapping to a range

The pattern  Math.floor(Math.random() * m) is inapplicable to secrets simply because of the source. Even with a CSPRNG, the direct remainder  x mod m can introduce modulo bias when  m it doesn’t divide the cardinality of the sample space. For critical scenarios, use  crypto.randomInt() rejection sampling.

For  X, uniform on  \{0,\ldots,2^w-1\}, the distribution  X \bmod m is uniform only if:
\[ m \mid 2^w. \]
For rejection sampling, take  X<L, where:
\[ L=\left\lfloor\frac{2^w}{m}\right\rfloor m, \qquad \text{and return } X\bmod m. \]

Practical audit

Audit objectRed flagCorrection
Session/reset/API tokenMath.random(), time, counter,  uuid– a polyfill without cryptorandomBytes(32) or Web Crypto; store the token hash, set TTL and one-time use
Key, seed, ECDSA nonceAny non-CSPRNG, custom “mixer”A proven crypto library; deterministic ECDSA RFC 6979 where required by the protocol
Lottery/game resultPublic PRNG outputs, predictable seedCSPRNG plus commit–reveal/VRF and log auditing
UUID and IDThe secret ID is created from Math.randomcrypto.randomUUID() or an independent CSPRNG token; the collision will still be processed in the database
Tests and simulationsThe need for reproducibility is mixed with production secretsIsolated seedable PRNG in tests, CSPRNG in production

Conclusions

The historical 32-bit implementation  Math.random() in V8 is a prime example of how API abstraction hides critical generator parameters. Formally, a long 22-character identifier doesn’t acquire 132 bits of cryptographic uncertainty when the entire process is a function of a small deterministic state.

For a cryptanalyst, this is a task of state recovery and prediction; for an engineer, it’s a requirement to distinguish a reproducible PRNG from a cryptographic CSPRNG. The modern xorshift128+ is significantly better than MWC1616 as a general-purpose generator, but it shouldn’t be used for generating secrets: in the browser, use Web Crypto, in Node.js  node:crypto, and entrust cryptographic nonces and keys to proven libraries.

Sources

V. Shahdeo — Math.random() is not so random: cryptanalytical analysis of 32-bit accuracy V8: https://cryptia.ru/v-shahdeo-math-random-is-not-so-random-cryptanalytical-analysis-of-32-bit-accuracy-v8/

  1. V. Shahdeo.  Math.random() is not so random: The Illusion of Randomness . Source material discussed in the article.
  2. V8.  There’s Math.random(), and then there’s Math.random() , 2015. Documents the replacement of MWC1616 with xorshift128+ and the limitations of the 32-bit legacy output.
  3. M. Malone (Betable).  TIFU by using Math.random() , 2015. Archival analysis of identifier collisions.
  4. Node.js Documentation.  Crypto  and  Web Crypto API . Description  randomBytes and  getRandomValues.
  5. NIST SP 800-90A Rev. 1.  Recommendation for Random Number Generation Using Deterministic Random Bit Generators .
  6. RFC 6979.  Deterministic Usage of the Digital Signature Algorithm (DSA) and Elliptic Curve Digital Signature Algorithm (ECDSA) .