A project to predict Math.random() in V8 engine (Chrome, Node.js) using Z3 solver.
pip install -r requirements.txtOpen Chrome browser console (F12) or run Node.js:
[...Array(5)].map(() => Math.random())Example result:
[0.9715627684052326, 0.6786206219892452, 0.35830200404557, 0.8961429686357996, 0.6562514411348932]Open main.py and replace the sequence array:
sequence = [
0.9715627684052326,
0.6786206219892452,
0.35830200404557,
0.8961429686357996,
0.6562514411348932,
]python3 main.pyYou will get a prediction of the next number:
🎉 [+] SUCCESS! State recovered!
State 0: 0x1a5c8103c84e7fdb
State 1: 0x957454ffb940aa7a
[+] PREDICTION OF THE NEXT NUMBER:
0.10297399847359556
[✓] Done!
V8 uses xorshift128+ for random number generation:
- 128-bit state (two 64-bit numbers)
- Deterministic algorithm
- Cache of 64 pre-generated numbers
- LIFO order (Last-In-First-Out)
- Data Collection: Get 5-10 sequential
Math.random()calls - Reversal: Reverse the array (due to LIFO cache)
- Mantissa Extraction: Get 52-bit mantissa from each number
- Z3 Solver: Solve system of equations to recover state
- Prediction: Generate next numbers from recovered state
V8 stores numbers in cache and returns them in reverse order:
sequence = sequence[::-1] # Reverse!First update xorshift128+ state, then compare:
# Update state
se_s1 ^= se_s1 << 23
se_s1 ^= LShR(se_s1, 17)
se_s1 ^= se_s0
se_s1 ^= LShR(se_s0, 26)
# Then compare
solver.add(mantissa == LShR(se_state0, 12))Due to V8 cache behavior (LIFO - Last-In-First-Out), the predictor can reliably predict only one next Math.random() number.
Why?
- V8 generates numbers in one order but returns them in reverse
- XorShift128+ only works "forward" (no inverse function)
- To predict multiple numbers, you need to collect the entire cache (~64 numbers)
Solution:
To predict a sequence of numbers, repeat the process:
- Get 5 new numbers from Math.random()
- Add them to
main.py - Run prediction
- Repeat for the next number
- ✅ Chrome
- ✅ Node.js
- ✅ Edge (Chromium-based)
- ❌ Firefox (different algorithm)
- ❌ Safari (different algorithm)
- Numbers must be sequential (no intermediate calls)
- Minimum 5 numbers, recommended 8-10
- Obtained from one cache (within 64 calls)
For testing, you can use a fixed seed:
node --random_seed=1337Then in Node.js:
Array.from(Array(5), Math.random)
// [0.9311600617849973, 0.3551442693830502, 0.7923158995678377, ...]Reasons:
-
Numbers are not sequential
- Other
Math.random()calls occurred between them - Solution: Get numbers at once
- Other
-
Not enough data
- Less than 5 numbers
- Solution: Add more numbers (8-10)
-
Not from V8
- Using Firefox/Safari
- Solution: Use Chrome or Node.js
Run with test data (with seed):
# In Node.js
node --random_seed=1337Array.from(Array(5), Math.random)Use these numbers in main.py - should work!
s1 = state0
s0 = state1
state0 = s0
s1 ^= s1 << 23
s1 ^= s1 >> 17
s1 ^= s0
s1 ^= s0 >> 26
state1 = s1
Math.random() returns double in [0, 1):
- Add 1.0 → number in [1, 2)
- Get bit representation
- Extract 52-bit mantissa
- Compare with
(state0 >> 12)
[Sign: 1 bit] [Exponent: 11 bits] [Mantissa: 52 bits]
- V8 Blog: Math.random()
- XorShift128+: Paper
- Z3 Solver: GitHub
For cryptography use:
- In browser:
crypto.getRandomValues() - In Node.js:
crypto.randomBytes()
V8 Version: Tested on V8 12.4.254.14 (Node.js v22.2.0)
Date: 2026-01-17