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..9dec7f545 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" @@ -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" @@ -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" @@ -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..a400b6275 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,11 @@ rustdoc-args = ["--cfg", "getrandom_backend=\"extern_impl\""] # 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 +49,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 +106,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..f494d7ab5 --- /dev/null +++ b/src/backends/deterministic.rs @@ -0,0 +1,51 @@ +//! 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::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> { + let rng = RNG.get_or_init(|| Mutex::new(ChaCha12Rng::from_seed([42u8; 32]))); + + let mut guard = rng.lock().unwrap(); + + // 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() }; + guard.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 + ); + } +}