diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 469925b7..10027577 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -382,6 +382,29 @@ jobs: RUSTFLAGS: -Dwarnings --cfg getrandom_backend="rdrand" run: cargo build -Z build-std=core --target=${{ matrix.target }} + build-rndr: + name: RNDR Build + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + targets: aarch64-unknown-linux-gnu, aarch64-apple-darwin + - uses: Swatinem/rust-cache@v2 + - name: RNDR enabled at compile time (Linux) + env: + RUSTFLAGS: -Dwarnings --cfg getrandom_backend="rndr" -C target-feature=+rand + run: cargo build --target=aarch64-unknown-linux-gnu + - name: Runtime RNDR detection without std (Linux) + env: + RUSTFLAGS: -Dwarnings --cfg getrandom_backend="rndr" + run: cargo build --target=aarch64-unknown-linux-gnu + - name: Runtime RNDR detection with std (macOS) + env: + RUSTFLAGS: -Dwarnings --cfg getrandom_backend="rndr" + run: cargo build --target=aarch64-unknown-linux-gnu --features std + build-esp-idf: name: ESP-IDF Build runs-on: ubuntu-22.04 diff --git a/.github/workflows/workspace.yml b/.github/workflows/workspace.yml index 13dba99a..80e4b6ef 100644 --- a/.github/workflows/workspace.yml +++ b/.github/workflows/workspace.yml @@ -59,6 +59,10 @@ jobs: run: cargo clippy -Zbuild-std=core --target x86_64-unknown-netbsd - name: Fortranix SGX (rdrand.rs) run: cargo clippy -Zbuild-std=core --target x86_64-fortanix-unknown-sgx + - name: RNDR (rndr.rs) + env: + RUSTFLAGS: -Dwarnings --cfg getrandom_backend="rndr" + run: cargo clippy -Zbuild-std=core --target aarch64-unknown-linux-gnu - name: Solaris (solaris.rs) run: cargo clippy -Zbuild-std=core --target x86_64-pc-solaris - name: SOLID (solid.rs) diff --git a/Cargo.toml b/Cargo.toml index 83e22501..88ce5637 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ rustc-dep-of-std = ["compiler_builtins", "core"] [lints.rust.unexpected_cfgs] level = "warn" check-cfg = [ - 'cfg(getrandom_backend, values("custom", "rdrand", "linux_getrandom", "wasm_js", "esp_idf"))', + 'cfg(getrandom_backend, values("custom", "rdrand", "rndr", "linux_getrandom", "wasm_js", "esp_idf"))', 'cfg(getrandom_browser_test)', 'cfg(getrandom_test_linux_fallback)', ] diff --git a/src/error.rs b/src/error.rs index d0299267..b27ef26b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -52,6 +52,10 @@ impl Error { pub const NODE_ES_MODULE: Error = Self::new_internal(14); /// Calling Windows ProcessPrng failed. pub const WINDOWS_PROCESS_PRNG: Error = Self::new_internal(15); + /// RNDR register read failed due to a hardware issue. + pub const RNDR_FAILURE: Error = Self::new_internal(16); + /// RNDR register is not supported on this target. + pub const RNDR_NOT_AVAILABLE: Error = Self::new_internal(17); /// Codes below this point represent OS Errors (i.e. positive i32 values). /// Codes at or above this point, but below [`Error::CUSTOM_START`] are @@ -149,23 +153,26 @@ impl fmt::Display for Error { } fn internal_desc(error: Error) -> Option<&'static str> { - match error { - Error::UNSUPPORTED => Some("getrandom: this target is not supported"), - Error::ERRNO_NOT_POSITIVE => Some("errno: did not return a positive value"), - Error::UNEXPECTED => Some("unexpected situation"), - Error::IOS_SEC_RANDOM => Some("SecRandomCopyBytes: iOS Security framework failure"), - Error::WINDOWS_RTL_GEN_RANDOM => Some("RtlGenRandom: Windows system function failure"), - Error::FAILED_RDRAND => Some("RDRAND: failed multiple times: CPU issue likely"), - Error::NO_RDRAND => Some("RDRAND: instruction not supported"), - Error::WEB_CRYPTO => Some("Web Crypto API is unavailable"), - Error::WEB_GET_RANDOM_VALUES => Some("Calling Web API crypto.getRandomValues failed"), - Error::VXWORKS_RAND_SECURE => Some("randSecure: VxWorks RNG module is not initialized"), - Error::NODE_CRYPTO => Some("Node.js crypto CommonJS module is unavailable"), - 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"), - _ => None, - } + let desc = match error { + Error::UNSUPPORTED => "getrandom: this target is not supported", + Error::ERRNO_NOT_POSITIVE => "errno: did not return a positive value", + Error::UNEXPECTED => "unexpected situation", + Error::IOS_SEC_RANDOM => "SecRandomCopyBytes: iOS Security framework failure", + Error::WINDOWS_RTL_GEN_RANDOM => "RtlGenRandom: Windows system function failure", + Error::FAILED_RDRAND => "RDRAND: failed multiple times: CPU issue likely", + Error::NO_RDRAND => "RDRAND: instruction not supported", + Error::WEB_CRYPTO => "Web Crypto API is unavailable", + Error::WEB_GET_RANDOM_VALUES => "Calling Web API crypto.getRandomValues failed", + Error::VXWORKS_RAND_SECURE => "randSecure: VxWorks RNG module is not initialized", + Error::NODE_CRYPTO => "Node.js crypto CommonJS module is unavailable", + Error::NODE_RANDOM_FILL_SYNC => "Calling Node.js API crypto.randomFillSync failed", + Error::NODE_ES_MODULE => "Node.js ES modules are not directly supported, see https://docs.rs/getrandom#nodejs-es-module-support", + Error::WINDOWS_PROCESS_PRNG => "ProcessPrng: Windows system function failure", + Error::RNDR_FAILURE => "RNDR: Could not generate a random number", + Error::RNDR_NOT_AVAILABLE => "RNDR: Register not supported", + _ => return None, + }; + Some(desc) } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index 5cc40941..e2084d96 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,6 +42,7 @@ //! | ----------------- | -------------------- | -------------------- | -------------- //! | `linux_getrandom` | Linux, Android | `*‑linux‑*` | [`getrandom`][1] system call (without `/dev/urandom` fallback). Bumps minimum supported Linux kernel version to 3.17 and Android API level to 23 (Marshmallow). //! | `rdrand` | x86, x86-64 | `x86_64-*`, `i686-*` | [`RDRAND`] instruction +//! | `rndr` | AArch64 | `aarch64-*` | [`RNDR`] register //! | `esp_idf` | ESP-IDF | `*‑espidf` | [`esp_fill_random`]. WARNING: can return low quality entropy without proper hardware configuration! //! | `wasm_js` | Web Browser, Node.js | `wasm*‑*‑unknown` | [`Crypto.getRandomValues`] if available, then [`crypto.randomFillSync`] if on Node.js (see [WebAssembly support]) //! | `custom` | All targets | `*` | User-provided custom implementation (see [custom backend]) @@ -51,7 +52,7 @@ //! `RUSTFLAGS` environment variable: //! //! ```sh -//! RUSTFLAGS='getrandom_backend="linux_getrandom"' cargo build +//! RUSTFLAGS='--cfg getrandom_backend="linux_getrandom"' cargo build //! ``` //! //! Enabling an opt-in backend will replace backend used by default. Doing it for a wrong target @@ -215,6 +216,7 @@ //! [`RtlGenRandom`]: https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-rtlgenrandom //! [`Crypto.getRandomValues`]: https://www.w3.org/TR/WebCryptoAPI/#Crypto-method-getRandomValues //! [`RDRAND`]: https://software.intel.com/en-us/articles/intel-digital-random-number-generator-drng-software-implementation-guide +//! [`RNDR`]: https://developer.arm.com/documentation/ddi0601/2024-06/AArch64-Registers/RNDR--Random-Number //! [`CCRandomGenerateBytes`]: https://opensource.apple.com/source/CommonCrypto/CommonCrypto-60074/include/CommonRandom.h.auto.html //! [`cprng_draw`]: https://fuchsia.dev/fuchsia-src/zircon/syscalls/cprng_draw //! [`crypto.randomFillSync`]: https://nodejs.org/api/crypto.html#cryptorandomfillsyncbuffer-offset-size @@ -292,6 +294,8 @@ cfg_if! { mod lazy; #[path = "rdrand.rs"] mod imp; + } else if #[cfg(getrandom_backend = "rndr")] { + #[path = "rndr.rs"] mod imp; } else if #[cfg(getrandom_backend = "wasm_js")] { #[cfg(not(all( any(target_arch = "wasm32", target_arch = "wasm64"), diff --git a/src/rndr.rs b/src/rndr.rs new file mode 100644 index 00000000..f27d9d28 --- /dev/null +++ b/src/rndr.rs @@ -0,0 +1,111 @@ +//! RNDR register backend for aarch64 targets +// Arm Architecture Reference Manual for A-profile architecture +// ARM DDI 0487K.a, ID032224, D23.2.147 RNDR, Random Number + +#[cfg(not(target_arch = "aarch64"))] +compile_error!("the `rndr` backend can be enabled only for AArch64 targets!"); + +use crate::{util::slice_as_uninit, Error}; +use core::arch::asm; +use core::mem::{size_of, MaybeUninit}; + +const RETRY_LIMIT: usize = 5; + +/// Read a random number from the aarch64 RNDR register +/// +/// Callers must ensure that FEAT_RNG is available on the system +/// The function assumes that the RNDR register is available +/// If it fails to read a random number, it will retry up to 5 times +/// After 5 failed reads the function will return `None` +#[target_feature(enable = "rand")] +unsafe fn rndr() -> Option { + for _ in 0..RETRY_LIMIT { + let mut x: u64; + let mut nzcv: u64; + + // AArch64 RNDR register is accessible by s3_3_c2_c4_0 + asm!( + "mrs {x}, RNDR", + "mrs {nzcv}, NZCV", + x = out(reg) x, + nzcv = out(reg) nzcv, + ); + + // If the hardware returns a genuine random number, PSTATE.NZCV is set to 0b0000 + if nzcv == 0 { + return Some(x); + } + } + + None +} + +#[target_feature(enable = "rand")] +unsafe fn rndr_fill(dest: &mut [MaybeUninit]) -> Option<()> { + let mut chunks = dest.chunks_exact_mut(size_of::()); + for chunk in chunks.by_ref() { + let src = rndr()?.to_ne_bytes(); + chunk.copy_from_slice(slice_as_uninit(&src)); + } + + let tail = chunks.into_remainder(); + let n = tail.len(); + if n > 0 { + let src = rndr()?.to_ne_bytes(); + tail.copy_from_slice(slice_as_uninit(&src[..n])); + } + Some(()) +} + +fn is_rndr_available() -> bool { + cfg_if::cfg_if! { + if #[cfg(target_feature = "rand")] { + true + } else if #[cfg(target_os = "linux")] { + /// Check whether FEAT_RNG is available on the system + /// + /// Requires the caller either be running in EL1 or be on a system supporting MRS + /// emulation. Due to the above, the implementation is currently restricted to Linux. + /// + /// Relying on runtime detection bumps minimum supported Linux kernel version to 4.11. + fn mrs_check() -> bool { + let mut id_aa64isar0: u64; + + // If FEAT_RNG is implemented, ID_AA64ISAR0_EL1.RNDR (bits 60-63) are 0b0001 + // This is okay to do from EL0 in Linux because Linux will emulate MRS as per + // https://docs.kernel.org/arch/arm64/cpu-feature-registers.html + unsafe { + asm!( + "mrs {id}, ID_AA64ISAR0_EL1", + id = out(reg) id_aa64isar0, + ); + } + + (id_aa64isar0 >> 60) & 0xf >= 1 + } + + #[path = "../src/lazy.rs"] mod lazy; + static RNDR_GOOD: lazy::LazyBool = lazy::LazyBool::new(); + RNDR_GOOD.unsync_init(mrs_check) + } else if #[cfg(feature = "std")] { + extern crate std; + #[path = "../src/lazy.rs"] mod lazy; + static RNDR_GOOD: lazy::LazyBool = lazy::LazyBool::new(); + RNDR_GOOD.unsync_init(|| std::arch::is_aarch64_feature_detected!("rand")) + } else { + compile_error!( + "RNDR `no_std` runtime detection is currently supported only on Linux targets. \ + Either enable the `std` crate feature, or `rand` target feature at compile time." + ); + } + } +} + +pub fn getrandom_inner(dest: &mut [MaybeUninit]) -> Result<(), Error> { + if is_rndr_available() { + // SAFETY: after this point, we know the `rand` target feature is enabled + unsafe { rndr_fill(dest).ok_or(Error::RNDR_FAILURE) } + } else { + Err(Error::RNDR_NOT_AVAILABLE) + } +}