diff --git a/.cargo/config.toml b/.cargo/config.toml index 61aa18600..e243b9240 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -14,3 +14,5 @@ runner = 'node' [resolver] incompatible-rust-versions = "allow" +[build] +rustflags = ['--cfg', 'getrandom_backend="deterministic_testing"'] diff --git a/Cargo.lock b/Cargo.lock index 7c4894727..40ddc221b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,6 +31,15 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -45,9 +54,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "shlex", @@ -59,6 +68,47 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "cipher" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea" +dependencies = [ + "block-buffer", + "crypto-common", + "inout", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -106,6 +156,7 @@ name = "getrandom" version = "0.4.2" dependencies = [ "cfg-if", + "chacha20", "js-sys", "libc", "r-efi", @@ -137,6 +188,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hybrid-array" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1" +dependencies = [ + "typenum", +] + [[package]] name = "id-arena" version = "2.3.0" @@ -155,6 +215,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + [[package]] name = "itoa" version = "1.0.17" @@ -179,9 +248,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libm" @@ -232,9 +301,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "oorandom" @@ -269,9 +338,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -375,6 +444,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/Cargo.toml b/Cargo.toml index 8580a64e3..59bb2f787 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,10 +16,16 @@ features = ["std", "sys_rng"] rustdoc-args = ["--cfg", "getrandom_backend=\"extern_impl\""] [features] +default = ["unsafe_deterministic_testing"] # Implement From for std::io::Error and # use std to retrieve OS error descriptions std = [] +# Feature to enable deterministic testing -- has an MSRV of 1.85 +# +# WARNING: This should only be enabled in dev-dependencies as it is for deterministic testing only. +unsafe_deterministic_testing = ["dep:rand_core", "dep:chacha20"] + # Optional backend: wasm_js # # This flag enables the wasm_js backend and uses it by default on wasm32 where @@ -44,6 +50,11 @@ rand_core = { version = "0.10.0", optional = true } [target.'cfg(all(any(target_os = "linux", target_os = "android"), not(any(all(target_os = "linux", target_env = ""), getrandom_backend = "custom", getrandom_backend = "linux_raw", getrandom_backend = "rdrand", getrandom_backend = "rndr"))))'.dependencies] libc = { version = "0.2.154", default-features = false } +# deterministic_testing +[target.'cfg(getrandom_backend = "deterministic_testing")'.dependencies] +chacha20 = { version = "0.10.0", features = ["rng"], optional = true } + + # apple-other [target.'cfg(any(target_os = "ios", target_os = "visionos", target_os = "watchos", target_os = "tvos"))'.dependencies] libc = { version = "0.2.154", default-features = false } @@ -96,7 +107,7 @@ wasm-bindgen-test = "0.3" [lints.rust.unexpected_cfgs] level = "warn" check-cfg = [ - 'cfg(getrandom_backend, values("custom", "efi_rng", "rdrand", "rndr", "linux_getrandom", "linux_raw", "windows_legacy", "unsupported", "extern_impl"))', + 'cfg(getrandom_backend, values("custom", "efi_rng", "rdrand", "rndr", "linux_getrandom", "linux_raw", "windows_legacy", "unsupported", "extern_impl", "deterministic_testing"))', 'cfg(getrandom_msan)', 'cfg(getrandom_test_linux_fallback)', 'cfg(getrandom_test_linux_without_fallback)', diff --git a/src/backends.rs b/src/backends.rs index 95547d9d3..9281db601 100644 --- a/src/backends.rs +++ b/src/backends.rs @@ -8,7 +8,10 @@ //! regardless of what value it returns. cfg_if! { - if #[cfg(getrandom_backend = "custom")] { + if #[cfg(all(getrandom_backend = "deterministic_testing", feature = "unsafe_deterministic_testing"))] { + mod deterministic; + pub use deterministic::*; + } else if #[cfg(getrandom_backend = "custom")] { mod custom; pub use custom::*; } else if #[cfg(getrandom_backend = "linux_getrandom")] { diff --git a/src/backends/deterministic.rs b/src/backends/deterministic.rs new file mode 100644 index 000000000..456c49dc3 --- /dev/null +++ b/src/backends/deterministic.rs @@ -0,0 +1,92 @@ +//! Deterministic testing backend — seeded ChaCha12 RNG, single-thread only. + +// This module is only compiled under `cfg(test)`, so `std` is always linked +// even though the crate is `#![no_std]`. +extern crate std; + +pub use crate::util::{inner_u32, inner_u64}; + +use crate::Error; + +use chacha20::ChaCha12Rng; +use core::mem::MaybeUninit; +use rand_core::{Rng, SeedableRng}; +use std::{ + collections::HashMap, + sync::{Mutex, OnceLock}, +}; + +/// The RNG, initialised exactly once on first use. +static RNG: OnceLock>> = OnceLock::new(); + +#[inline] +pub fn fill_inner(dest: &mut [MaybeUninit]) -> Result<(), Error> { + // get current thread id + let id = std::thread::current().id(); + + let rng = RNG.get_or_init(|| HashMap::new().into()); + + let mut guard = rng.lock().unwrap(); + + let entry = guard + .entry(id) + .or_insert_with(|| ChaCha12Rng::from_seed([42u8; 32])); + + // SAFETY: `fill_bytes` fully overwrites every byte of the slice, so + // treating uninitialized `MaybeUninit` as `u8` for the purpose of + // writing (never reading) is sound. + let dest_init = unsafe { dest.assume_init_mut() }; + entry.fill_bytes(dest_init); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_deterministic() { + let mut buf = [0u8; 32]; + crate::fill(&mut buf).unwrap(); + assert_eq!( + [ + 0x1b, 0x8c, 0x20, 0xcd, 0xe2, 0xdb, 0xb4, 0x3c, 0xd3, 0xc7, 0x9, 0xb2, 0x90, 0xac, + 0x50, 0xdc, 0xd2, 0xbe, 0x2a, 0x87, 0xa3, 0xa2, 0x45, 0x44, 0xb5, 0xa5, 0x10, 0x9b, + 0xc7, 0x6e, 0xa7, 0xfb, + ], + buf + ); + } + use std::thread; + #[test] + fn test_deterministic_multithread() { + let jh = thread::spawn(|| { + let mut buf = [0u8; 32]; + crate::fill(&mut buf).unwrap(); + assert_eq!( + [ + 0x1b, 0x8c, 0x20, 0xcd, 0xe2, 0xdb, 0xb4, 0x3c, 0xd3, 0xc7, 0x9, 0xb2, 0x90, + 0xac, 0x50, 0xdc, 0xd2, 0xbe, 0x2a, 0x87, 0xa3, 0xa2, 0x45, 0x44, 0xb5, 0xa5, + 0x10, 0x9b, 0xc7, 0x6e, 0xa7, 0xfb, + ], + buf + ); + }); + let jh2 = thread::spawn(|| { + let mut buf = [0u8; 32]; + crate::fill(&mut buf).unwrap(); + assert_eq!( + [ + 0x1b, 0x8c, 0x20, 0xcd, 0xe2, 0xdb, 0xb4, 0x3c, 0xd3, 0xc7, 0x9, 0xb2, 0x90, + 0xac, 0x50, 0xdc, 0xd2, 0xbe, 0x2a, 0x87, 0xa3, 0xa2, 0x45, 0x44, 0xb5, 0xa5, + 0x10, 0x9b, 0xc7, 0x6e, 0xa7, 0xfb, + ], + buf + ); + }); + + jh.join().unwrap(); + jh2.join().unwrap(); + } +}