diff --git a/Cargo.lock b/Cargo.lock index 302d30fd9..d018aa027 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 64204423f..5d59d92d2 100644 --- a/crates/js/src/interp.rs +++ b/crates/js/src/interp.rs @@ -307,13 +307,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.