From 6fbaad6c5eeea334eba0500c2a49615173bc8caa Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 11 Nov 2026 10:31:32 -0800 Subject: [PATCH] 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. --- Cargo.lock | 1 + crates/js/Cargo.toml | 6 ++++++ crates/js/src/builtins_tests.rs | 11 +++++++++++ crates/js/src/interp.rs | 13 ++++++++----- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b3ec5a0b9..50abba7cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6589,6 +6589,7 @@ name = "vectorcraft-js" version = "0.24.0" dependencies = [ "chrono", + "getrandom 0.4.3", "proptest", "unicode-general-category", ] diff --git a/crates/js/Cargo.toml b/crates/js/Cargo.toml index 5848c1a91..610c8f9bd 100644 --- a/crates/js/Cargo.toml +++ b/crates/js/Cargo.toml @@ -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 } diff --git a/crates/js/src/builtins_tests.rs b/crates/js/src/builtins_tests.rs index e0593d1dd..9d917ae32 100644 --- a/crates/js/src/builtins_tests.rs +++ b/crates/js/src/builtins_tests.rs @@ -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] diff --git a/crates/js/src/interp.rs b/crates/js/src/interp.rs index ed3099352..7d28312e9 100644 --- a/crates/js/src/interp.rs +++ b/crates/js/src/interp.rs @@ -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.