From 1a7f87cc63683c07eba9ba3968d9048750bcb8b2 Mon Sep 17 00:00:00 2001 From: Kajetan Puchalski Date: Mon, 15 Jul 2024 14:38:30 +0100 Subject: [PATCH] generators: Expose specific generators Currently users of getrandom cannot choose which specific generator they want to use if multiple are available on their system. As the preferred generator may vary on a case-by-case basis, it is worth exposing an interface that enables doing so. Without affecting the preexisting functionality, this commit adds a new "generators" module with a macro that can be used to generate function wrappers with the same signature as getrandom() for each specific generator. The generated functions can be used analogously to getrandom(), as follows: getrandom(&mut buf).unwrap(); // <-- unchanged, default getrandom usage generators::use_file(&mut buf).unwrap(); generators::linux_android_with_fallback(&mut buf).unwrap(); Due to the way conditional compilation of modules is currently handled in the crate, this change required a rework of lib.rs in order to split up selecting which modules to compile based on the target and selecting which module to use as the preferred or default one. In order to achieve that, a new module cfg_module.rs was added to contain a macro cfg_if_module which takes a module name and a block of code, then resolves to a cfg_if! containing the given block conditional on the targets that the module can be compiled for. This way the targets for each module can be configured once within the macro and then reused across the crate as shown in the snippet below. This approach avoids having to keep several sets of long target config attributes in sync in different parts of the crate and makes the code more maintainable. cfg_if_module!(linux_android_with_fallback, { mod linux_android_with_fallback; }); This change does not intend to modify the default backend selection logic. This split also makes it possible to avoid having to repeat the same module declarations for modules that are dependencies of more than one backend (especially mod util_libc). As an example, an aarch64 Linux platform with the RNDR register available could use the use_file, linux_android, linux_android_with_fallback, rndr or rndr_with_fallback backends. All of them will be compiled and accessible through "mod generators", but only one of them will be selected and compiled as "mod imp" to be the crate default. --- src/{apple-other.rs => apple_other.rs} | 0 src/cfg_module.rs | 230 +++++++++++++++++++++++++ src/error.rs | 3 + src/generators.rs | 67 +++++++ src/lib.rs | 57 ++++-- 5 files changed, 342 insertions(+), 15 deletions(-) rename src/{apple-other.rs => apple_other.rs} (100%) create mode 100644 src/cfg_module.rs create mode 100644 src/generators.rs diff --git a/src/apple-other.rs b/src/apple_other.rs similarity index 100% rename from src/apple-other.rs rename to src/apple_other.rs diff --git a/src/cfg_module.rs b/src/cfg_module.rs new file mode 100644 index 00000000..82067336 --- /dev/null +++ b/src/cfg_module.rs @@ -0,0 +1,230 @@ +/// Compile a block of tokens if `module` is supported on the target platform +/// This is a convenience macro in order to avoid repeating lists of +/// supported targets in `cfg_if` blocks. +/// +/// The target configs in this macro are supposed to describe platform +/// compatibility for each of the modules, regardless of the policy choice +/// on which module is preferred for a specific target. +/// +/// Usage: +/// +/// ```rust +/// cfg_if_module!(use_file, { +/// // any code that requires use_file to be supported +/// }); +/// ``` +macro_rules! cfg_if_module { + ( $(util_libc, { $($tokens:tt)* })+ ) => {$( + cfg_if! { + if #[cfg(any( + target_os = "android", target_os = "linux", target_os = "solaris", + target_os = "netbsd", target_os = "haiku", target_os = "redox", + target_os = "nto", target_os = "aix", target_os = "vxworks", + target_os = "dragonfly", target_os = "freebsd", target_os = "hurd", + target_os = "illumos", target_os = "macos", target_os = "openbsd", + target_os = "vita", target_os = "emscripten" + ))] { + $($tokens)* + } + } + )*}; + + ( $(use_file, { $($tokens:tt)* })+ ) => {$( + cfg_if! { + if #[cfg(any( + target_os = "linux", target_os = "android", target_os = "macos", + target_os = "freebsd", target_os = "haiku", target_os = "redox", + target_os = "nto", target_os = "aix", + ))] { + $($tokens)* + } + } + )*}; + + ( $(getentropy, { $($tokens:tt)* })+ ) => {$( + cfg_if! { + if #[cfg(any( + target_os = "macos", target_os = "openbsd", + target_os = "vita", target_os = "emscripten", + ))] { + $($tokens)* + } + } + )*}; + + ( $(getrandom, { $($tokens:tt)* })+ ) => {$( + cfg_if! { + if #[cfg(any( + target_os = "dragonfly", target_os = "freebsd", + target_os = "hurd", target_os = "illumos", + // Check for target_arch = "arm" to only include the 3DS. Does not + // include the Nintendo Switch (which is target_arch = "aarch64"). + all(target_os = "horizon", target_arch = "arm"), + ))] { + $($tokens)* + } + } + )*}; + + ( $(linux_android, { $($tokens:tt)* })+ ) => {$( + cfg_if! { + if #[cfg(any( + target_os = "linux", target_os = "android", + ))] { + $($tokens)* + } + } + )*}; + + ( $(linux_android_with_fallback, { $($tokens:tt)* })+ ) => {$( + cfg_if! { + if #[cfg(all( + not(feature = "linux_disable_fallback"), + any( + // Rust supports Android API level 19 (KitKat) [0] and the next upgrade targets + // level 21 (Lollipop) [1], while `getrandom(2)` was added only in + // level 23 (Marshmallow). Note that it applies only to the "old" `target_arch`es, + // RISC-V Android targets sufficiently new API level, same will apply for potential + // new Android `target_arch`es. + // [0]: https://blog.rust-lang.org/2023/01/09/android-ndk-update-r25.html + // [1]: https://github.com/rust-lang/rust/pull/120593 + all( + target_os = "android", + any(target_arch = "aarch64", target_arch = "arm", + target_arch = "x86", target_arch = "x86_64", + ) + ), + // Only on these `target_arch`es Rust supports Linux kernel versions (3.2+) + // that precede the version (3.17) in which `getrandom(2)` was added: + // https://doc.rust-lang.org/stable/rustc/platform-support.html + all( + target_os = "linux", + any( + target_arch = "aarch64", target_arch = "arm", target_arch = "s390x", + target_arch = "powerpc", target_arch = "powerpc64", + target_arch = "x86", target_arch = "x86_64", + // Minimum supported Linux kernel version for MUSL targets + // is not specified explicitly (as of Rust 1.77) and they + // are used in practice to target pre-3.17 kernels. + target_env = "musl", + ), + ) + ), + ))] { + $($tokens)* + } + } + )*}; + + ( $(solaris, { $($tokens:tt)* })+ ) => {$( + cfg_if! { + if #[cfg(target_os = "solaris")] { + $($tokens)* + } + } + )*}; + + ( $(netbsd, { $($tokens:tt)* })+ ) => {$( + cfg_if! { + if #[cfg(target_os = "netbsd")] { + $($tokens)* + } + } + )*}; + + ( $(fuchsia, { $($tokens:tt)* })+ ) => {$( + cfg_if! { + if #[cfg(target_os = "fuchsia")] { + $($tokens)* + } + } + )*}; + + ( $(apple_other, { $($tokens:tt)* })+ ) => {$( + cfg_if! { + if #[cfg(any( + target_os = "ios", target_os = "visionos", target_os = "watchos", target_os = "tvos" + ))] { + $($tokens)* + } + } + )*}; + + ( $(wasi, { $($tokens:tt)* })+ ) => {$( + cfg_if! { + if #[cfg(all(target_arch = "wasm32", target_os = "wasi"))] { + $($tokens)* + } + } + )*}; + + ( $(hermit, { $($tokens:tt)* })+ ) => {$( + cfg_if! { + if #[cfg(target_os = "hermit")] { + $($tokens)* + } + } + )*}; + + ( $(vxworks, { $($tokens:tt)* })+ ) => {$( + cfg_if! { + if #[cfg(target_os = "vxworks")] { + $($tokens)* + } + } + )*}; + + ( $(solid, { $($tokens:tt)* })+ ) => {$( + cfg_if! { + if #[cfg(target_os = "solid_asp3")] { + $($tokens)* + } + } + )*}; + + ( $(espidf, { $($tokens:tt)* })+ ) => {$( + cfg_if! { + if #[cfg(target_os = "espidf")] { + $($tokens)* + } + } + )*}; + + ( $(windows7, { $($tokens:tt)* })+ ) => {$( + cfg_if! { + if #[cfg(all(windows, target_vendor = "win7"))] { + $($tokens)* + } + } + )*}; + + ( $(windows, { $($tokens:tt)* })+ ) => {$( + cfg_if! { + if #[cfg(all(windows, not(target_vendor = "win7")))] { + $($tokens)* + } + } + )*}; + + ( $(rdrand, { $($tokens:tt)* })+ ) => {$( + cfg_if! { + if #[cfg(any( + all(target_arch = "x86_64", target_env = "sgx"), + all(feature = "rdrand", any(target_arch = "x86_64", target_arch = "x86")) + ))] { + $($tokens)* + } + } + )*}; + + ( $(js, { $($tokens:tt)* })+ ) => {$( + cfg_if! { + if #[cfg(all( + feature = "js", target_os = "unknown", + any(target_arch = "wasm32", target_arch = "wasm64"), + ))] { + $($tokens)* + } + } + )*}; +} diff --git a/src/error.rs b/src/error.rs index 5eff99eb..c9ac418b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -58,6 +58,8 @@ impl Error { pub const NODE_ES_MODULE: Error = internal_error(14); /// Calling Windows ProcessPrng failed. pub const WINDOWS_PROCESS_PRNG: Error = internal_error(15); + /// No hardware RNGs are available on this target. + pub const NO_HW: Error = internal_error(18); /// Codes below this point represent OS Errors (i.e. positive i32 values). /// Codes at or above this point, but below [`Error::CUSTOM_START`] are @@ -175,6 +177,7 @@ fn internal_desc(error: Error) -> Option<&'static str> { Error::NODE_RANDOM_FILL_SYNC => Some("Calling Node.js API crypto.randomFillSync failed"), Error::NODE_ES_MODULE => Some("Node.js ES modules are not directly supported, see https://docs.rs/getrandom#nodejs-es-module-support"), Error::WINDOWS_PROCESS_PRNG => Some("ProcessPrng: Windows system function failure"), + Error::NO_HW => Some("getrandom: No hardware RNGs are available on this target"), _ => None, } } diff --git a/src/generators.rs b/src/generators.rs new file mode 100644 index 00000000..e554d402 --- /dev/null +++ b/src/generators.rs @@ -0,0 +1,67 @@ +//! Access functions for specific random number generators + +use crate::getrandom; +use crate::util::slice_as_uninit_mut; +use crate::Error; + +// Shared macro to generate access functions for each backend module +macro_rules! define_generator_fns { + ($($module:ident),+) => {$( + cfg_if_module!($module, { + use crate::$module; + #[doc = concat!(" Access function for the ", stringify!($module), " generator.")] + pub fn $module(dest: &mut [u8]) -> Result<(), Error> { + let uninit_dest = unsafe { slice_as_uninit_mut(dest) }; + if !uninit_dest.is_empty() { + $module::getrandom_inner(uninit_dest)?; + } + Ok(()) + } + }); + )+}; +} + +// Generate access functions for all backends supported by the target platform +define_generator_fns!( + use_file, + getentropy, + getrandom, + linux_android, + linux_android_with_fallback, + solaris, + netbsd, + fuchsia, + apple_other, + wasi, + hermit, + vxworks, + solid, + espidf, + windows7, + windows, + rdrand, + js +); + +/// Fill `dest` with random bytes from a hardware random number generator +/// Returns an Error if no hardware RNGs are available +#[inline] +#[allow(unreachable_code)] +pub fn hardware(_dest: &mut [u8]) -> Result<(), Error> { + cfg_if_module!(rdrand, { + return rdrand(_dest); + }); + Err(Error::NO_HW) +} + +/// Fill `dest` with random bytes from a hardware random number generator +/// Falls back to getrandom() if no hardware RNGs are available +#[inline] +#[allow(unreachable_code)] +pub fn hardware_with_fallback(dest: &mut [u8]) -> Result<(), Error> { + if hardware(dest).is_ok() { + Ok(()) + } else { + getrandom(dest) + } +} diff --git a/src/lib.rs b/src/lib.rs index f5e13c36..b5c221e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -208,6 +208,7 @@ #![no_std] #![warn(rust_2018_idioms, unused_lifetimes, missing_docs)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![allow(clippy::duplicate_mod)] #[macro_use] extern crate cfg_if; @@ -227,6 +228,40 @@ mod error_impls; pub use crate::error::Error; +#[macro_use] +mod cfg_module; + +macro_rules! define_supported_modules { + ($($module:ident),+) => {$( + cfg_if_module!($module, { + mod $module; + }); + )+}; +} + +// Define all modules supported by the target +define_supported_modules!( + util_libc, + use_file, + getentropy, + getrandom, + linux_android, + linux_android_with_fallback, + solaris, + netbsd, + fuchsia, + apple_other, + wasi, + hermit, + vxworks, + solid, + espidf, + windows7, + windows, + rdrand, + js +); + // System-specific implementations. // // These should all provide getrandom_inner with the signature @@ -236,7 +271,6 @@ pub use crate::error::Error; // regardless of what value it returns. cfg_if! { if #[cfg(any(target_os = "haiku", target_os = "redox", target_os = "nto", target_os = "aix"))] { - mod util_libc; #[path = "use_file.rs"] mod imp; } else if #[cfg(any( target_os = "macos", @@ -244,7 +278,6 @@ cfg_if! { target_os = "vita", target_os = "emscripten", ))] { - mod util_libc; #[path = "getentropy.rs"] mod imp; } else if #[cfg(any( target_os = "dragonfly", @@ -255,7 +288,6 @@ cfg_if! { // include the Nintendo Switch (which is target_arch = "aarch64"). all(target_os = "horizon", target_arch = "arm"), ))] { - mod util_libc; #[path = "getrandom.rs"] mod imp; } else if #[cfg(all( not(feature = "linux_disable_fallback"), @@ -297,29 +329,22 @@ cfg_if! { ) ), ))] { - mod util_libc; - mod use_file; - mod linux_android; #[path = "linux_android_with_fallback.rs"] mod imp; } else if #[cfg(any(target_os = "android", target_os = "linux"))] { - mod util_libc; #[path = "linux_android.rs"] mod imp; } else if #[cfg(target_os = "solaris")] { - mod util_libc; #[path = "solaris.rs"] mod imp; } else if #[cfg(target_os = "netbsd")] { - mod util_libc; #[path = "netbsd.rs"] mod imp; } else if #[cfg(target_os = "fuchsia")] { #[path = "fuchsia.rs"] mod imp; } else if #[cfg(any(target_os = "ios", target_os = "visionos", target_os = "watchos", target_os = "tvos"))] { - #[path = "apple-other.rs"] mod imp; + #[path = "apple_other.rs"] mod imp; } else if #[cfg(all(target_arch = "wasm32", target_os = "wasi"))] { #[path = "wasi.rs"] mod imp; } else if #[cfg(target_os = "hermit")] { #[path = "hermit.rs"] mod imp; } else if #[cfg(target_os = "vxworks")] { - mod util_libc; #[path = "vxworks.rs"] mod imp; } else if #[cfg(target_os = "solid_asp3")] { #[path = "solid.rs"] mod imp; @@ -329,10 +354,10 @@ cfg_if! { #[path = "windows7.rs"] mod imp; } else if #[cfg(windows)] { #[path = "windows.rs"] mod imp; - } else if #[cfg(all(target_arch = "x86_64", target_env = "sgx"))] { - #[path = "rdrand.rs"] mod imp; - } else if #[cfg(all(feature = "rdrand", - any(target_arch = "x86_64", target_arch = "x86")))] { + } else if #[cfg(any( + all(target_arch = "x86_64", target_env = "sgx"), + all(feature = "rdrand", any(target_arch = "x86_64", target_arch = "x86")) + ))] { #[path = "rdrand.rs"] mod imp; } else if #[cfg(all(feature = "js", any(target_arch = "wasm32", target_arch = "wasm64"), @@ -352,6 +377,8 @@ cfg_if! { } } +pub mod generators; + /// Fill `dest` with random bytes from the system's preferred random number /// source. ///