Scripts: Math.random gets a new seed on every web page load

Each realm seeded Math.random from std's RandomState. On
wasm32-unknown-unknown its keys come from stack and heap addresses,
which are the same on every page load, so scripts in the web build
drew the same sequence each time the page opened.

The seed now comes from getrandom (crypto.getRandomValues in the
browser, as crates/pdf/src/encrypt.rs already does), with RandomState
kept as the fallback when it fails.
This commit is contained in:
dotCooCoo
2026-11-11 10:31:32 -08:00
parent 7c800f851f
commit 6fbaad6c5e
4 changed files with 26 additions and 5 deletions
Generated
+1
View File
@@ -6589,6 +6589,7 @@ name = "vectorcraft-js"
version = "0.24.0"
dependencies = [
"chrono",
"getrandom 0.4.3",
"proptest",
"unicode-general-category",
]
+6
View File
@@ -12,6 +12,12 @@ workspace = true
[dependencies]
chrono = { workspace = true }
unicode-general-category = "1"
# The seed of Math.random (interp.rs).
getrandom = "0.4"
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
# The browser's crypto.getRandomValues.
getrandom = { version = "0.4", features = ["wasm_js"] }
[dev-dependencies]
proptest = { workspace = true }
+11
View File
@@ -605,6 +605,17 @@ fn math_functions_follow_the_special_cases() {
truth("var n = 0; Math.max({valueOf: function () { n++; return NaN; }}, {valueOf: function () { n++; return 1; }}); n === 2");
}
#[test]
fn math_random_is_seeded_from_the_os_random_source() {
// On wasm32-unknown-unknown `RandomState`'s keys come from memory addresses, which are the
// same on every page load: the seed must come from the OS (the browser's crypto) instead.
let src = include_str!("interp.rs");
let Some(start) = src.find("fn random_seed()") else { panic!("interp.rs has no random_seed") };
let body = &src[start..];
let body = &body[..body.find("\n}").unwrap_or(body.len())];
assert!(body.contains("getrandom::u64()"), "Math.random isn't seeded from the OS random source");
}
// ---- JSON ----
#[test]
+8 -5
View File
@@ -305,13 +305,16 @@ fn index_keys_bytes(len: usize) -> usize {
total
}
/// A seed for `Math.random` that differs from realm to realm, from the standard library's
/// randomly keyed hasher; never zero.
/// A seed for `Math.random` that differs from realm to realm and from run to run, from the OS
/// random source (the browser's crypto on the web), or the standard library's randomly keyed
/// hasher when that fails; never zero.
fn random_seed() -> u64 {
use std::hash::{BuildHasher, Hasher};
let mut h = std::collections::hash_map::RandomState::new().build_hasher();
h.write_u64(0x9e37_79b9_7f4a_7c15);
h.finish() | 1
getrandom::u64().unwrap_or_else(|_| {
let mut h = std::collections::hash_map::RandomState::new().build_hasher();
h.write_u64(0x9e37_79b9_7f4a_7c15);
h.finish()
}) | 1
}
/// The estimated bytes of a binding or property named by `len` code units.