diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f3831d5c..f1b60c02 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -66,6 +66,30 @@ jobs: - if: ${{ matrix.toolchain == 'nightly' }} run: cargo test --benches + sanitizer-tests: + name: Sanitizer Tests + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-22.04] + target: [ + x86_64-unknown-linux-gnu, + ] + toolchain: [nightly] + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@master + with: + components: rust-src + targets: ${{ matrix.target }} + toolchain: ${{ matrix.toolchain }} + - uses: Swatinem/rust-cache@v2 + - run: RUSTFLAGS="-Zsanitizer=memory" cargo test -Zbuild-std --target=${{ matrix.target }} --features=unstable-sanitize + - run: RUSTFLAGS="-Zsanitizer=memory" cargo test -Zbuild-std --target=${{ matrix.target }} --features=unstable-sanitize,std + - run: RUSTFLAGS="-Zsanitizer=memory" cargo test -Zbuild-std --target=${{ matrix.target }} --features=unstable-sanitize,linux_disable_fallback + # Doctests fail to link so use `--all-targets` to avoid them. + - run: RUSTFLAGS="-Zsanitizer=memory" cargo test -Zbuild-std --target=${{ matrix.target }} --features=unstable-sanitize,custom --all-targets + linux-tests: name: Linux Test runs-on: ubuntu-22.04 diff --git a/Cargo.toml b/Cargo.toml index b9357bce..581b3f21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,8 @@ rustc-dep-of-std = [ "libc/rustc-dep-of-std", "wasi/rustc-dep-of-std", ] +# Enable support for sanitizers; Requires Rust feature `cfg_sanitize`. +unstable-sanitize = [] # Unstable/test-only feature to run wasm-bindgen tests in a browser test-in-browser = [] diff --git a/src/custom.rs b/src/custom.rs index 79be7fc2..29b62815 100644 --- a/src/custom.rs +++ b/src/custom.rs @@ -96,6 +96,9 @@ pub fn getrandom_inner(dest: &mut [MaybeUninit]) -> Result<(), Error> { // compatibility with implementations that rely on that (e.g. Rust // implementations that construct a `&mut [u8]` slice from `dest` and // `len`). + // XXX: Because we do this, memory sanitizer isn't able to detect when + // `__getrandom_custom` fails to fill `dest`, but we can't poison `dest` + // here either, for the same reason we have to fill it in the first place. let dest = uninit_slice_fill_zero(dest); let ret = unsafe { __getrandom_custom(dest.as_mut_ptr(), dest.len()) }; match NonZeroU32::new(ret) { diff --git a/src/lib.rs b/src/lib.rs index d1e9ef09..4f8c4436 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,6 +34,19 @@ //! //! Pull Requests that add support for new targets to `getrandom` are always welcome. //! +//! ## Memory Sanitizer (msan) support +//! +//! The `unstable-sanitize` feature adds Memory Sanitizer support. You must use +//! Rust Nightly, e.g. +//! ```sh +//! RUSTFLAGS="-Zsanitizer=memory" \ +//! cargo +nightly test \ +//! -Zbuild-std --target=x86_64-unknown-linux-gnu --features=unstable-sanitize +//! ``` +//! It is assumed that libstd/libc have had their APis instrumented to support +//! sanitizers, so we only provide special support on Linux when using the +//! `getrandom` syscall. +//! //! ## Unsupported targets //! //! By default, `getrandom` will not compile on unsupported targets, but certain @@ -208,6 +221,7 @@ #![no_std] #![warn(rust_2018_idioms, unused_lifetimes, missing_docs)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(feature = "unstable-sanitize", feature(cfg_sanitize))] #[macro_use] extern crate cfg_if; @@ -300,9 +314,11 @@ cfg_if! { mod use_file; mod lazy; mod linux_android; + mod util_syscall_linux; #[path = "linux_android_with_fallback.rs"] mod imp; } else if #[cfg(any(target_os = "android", target_os = "linux"))] { mod util_libc; + mod util_syscall_linux; #[path = "linux_android.rs"] mod imp; } else if #[cfg(target_os = "solaris")] { mod util_libc; diff --git a/src/linux_android.rs b/src/linux_android.rs index 7c1fede4..060efe33 100644 --- a/src/linux_android.rs +++ b/src/linux_android.rs @@ -1,5 +1,5 @@ //! Implementation for Linux / Android without `/dev/urandom` fallback -use crate::{util_libc, Error}; +use crate::{util_libc, util_syscall_linux, Error}; use core::mem::MaybeUninit; pub fn getrandom_inner(dest: &mut [MaybeUninit]) -> Result<(), Error> { @@ -8,12 +8,23 @@ pub fn getrandom_inner(dest: &mut [MaybeUninit]) -> Result<(), Error> { // Also used by linux_android_with_fallback to check if the syscall is available. pub fn getrandom_syscall(buf: &mut [MaybeUninit]) -> libc::ssize_t { - unsafe { + util_syscall_linux::pre_write_range(buf.as_mut_ptr(), buf.len()); + let res = unsafe { libc::syscall( libc::SYS_getrandom, buf.as_mut_ptr().cast::(), buf.len(), 0, - ) as libc::ssize_t - } + ) + } as libc::ssize_t; + if let Ok(written) = usize::try_from(res) { + // XXX: LLVM has support to do this automatically if/when libc is + // compiled with it, but glibc that ships in typical Linux distros + // doesn't. Assume Android's Bionic is similar. `-Zsanitizer=memory` + // is not compatible with `+crt-static` according to rustc. + unsafe { + util_syscall_linux::post_write_range(buf.as_mut_ptr(), written); + } + }; + res } diff --git a/src/util_syscall_linux.rs b/src/util_syscall_linux.rs new file mode 100644 index 00000000..724bd833 --- /dev/null +++ b/src/util_syscall_linux.rs @@ -0,0 +1,104 @@ +// Support for raw system calls on Linux. +// +// # Sanitizers +// +// Currently only Memory Sanitizer is actively supported. +// +// TODO: Support address sanitizer, in particular in `pre_write_range`. +// +// ## Memory Sanitizer +// +// See https://github.com/llvm/llvm-project/commit/ac9ee01fcbfac745aaedca0393a8e1c8a33acd8d: +// LLVM uses: +// ```c +// COMMON_INTERCEPTOR_ENTER(ctx, getrandom, buf, buflen, flags); +// SSIZE_T n = REAL(getrandom)(buf, buflen, flags); +// if (n > 0) { +// COMMON_INTERCEPTOR_WRITE_RANGE(ctx, buf, n); +// } +// ``` +// and: +// ```c +// #define PRE_SYSCALL(name) \ +// SANITIZER_INTERFACE_ATTRIBUTE void __sanitizer_syscall_pre_impl_##name +// #define PRE_WRITE(p, s) COMMON_SYSCALL_PRE_WRITE_RANGE(p, s) +// #define POST_WRITE(p, s) COMMON_SYSCALL_POST_WRITE_RANGE(p, s) +// PRE_SYSCALL(getrandom)(void *buf, uptr count, long flags) { +// if (buf) { +// PRE_WRITE(buf, count); +// } +// } +// +// POST_SYSCALL(getrandom)(long res, void *buf, uptr count, long flags) { +// if (res > 0 && buf) { +// POST_WRITE(buf, res); +// } +// } +// ``` + +use core::mem::MaybeUninit; + +// MSAN defines: +// +// ```c +// #define COMMON_INTERCEPTOR_ENTER(ctx, func, ...) \ +// if (msan_init_is_running) \ +// return REAL(func)(__VA_ARGS__); \ +// ENSURE_MSAN_INITED(); \ +// MSanInterceptorContext msan_ctx = {IsInInterceptorScope()}; \ +// ctx = (void *)&msan_ctx; \ +// (void)ctx; \ +// InterceptorScope interceptor_scope; \ +// __msan_unpoison(__errno_location(), sizeof(int)); +// ``` +// +// * We assume that memory sanitizer will not use the this crate during the +// initialization of msan, so we don't have to worry about +// `msan_init_is_running`. +// * We assume that rustc/LLVM initializes MSAN before executing any Rust code, +// so we don't need to call `ENSURE_MSAN_INITED`. +// * Notice that `COMMON_INTERCEPTOR_WRITE_RANGE` doesn't use `ctx`, which +// means it is oblivious to `IsInInterceptorScope()`, so we don't have to +// call it. More generally, we don't have to worry about interceptor scopes +// because we are not an interceptor. +// * We don't read from `__errno_location()` so we don't need to unpoison it. +// +// Consequently, MSAN's `COMMON_INTERCEPTOR_ENTER` is a no-op. +// +// MSAN defines: +// ```c +// #define COMMON_SYSCALL_PRE_WRITE_RANGE(p, s) \ +// do { \ +// } while (false) +// ``` +// So MSAN's PRE_SYSCALL hook is also a no-op. +// +// Consequently, we have nothing to do before invoking the syscall unless/until +// we support other sanitizers like ASAN. +#[allow(unused_variables)] +pub fn pre_write_range(_ptr: *mut MaybeUninit, _size: usize) {} + +// MSNA defines: +// ```c +// #define COMMON_INTERCEPTOR_WRITE_RANGE(ctx, ptr, size) \ +// __msan_unpoison(ptr, size) +// ``` +// and: +// ```c +// #define COMMON_SYSCALL_POST_WRITE_RANGE(p, s) __msan_unpoison(p, s) +// ``` +#[allow(unused_variables)] +pub unsafe fn post_write_range(ptr: *mut MaybeUninit, size: usize) { + #[cfg(feature = "unstable-sanitize")] + { + #[cfg(sanitize = "memory")] + { + use core::ffi::c_void; + extern "C" { + // void __msan_unpoison(const volatile void *a, size_t size); + fn __msan_unpoison(a: *mut c_void, size: usize); + } + __msan_unpoison(ptr.cast::(), size) + } + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 666f7f57..88533acf 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,4 +1,7 @@ -use super::getrandom_impl; +use super::{getrandom_impl, getrandom_uninit_impl}; +use core::mem::MaybeUninit; +#[cfg(not(feature = "custom"))] +use getrandom::Error; #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] use wasm_bindgen_test::wasm_bindgen_test as test; @@ -6,10 +9,19 @@ use wasm_bindgen_test::wasm_bindgen_test as test; #[cfg(feature = "test-in-browser")] wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); +#[cfg(not(feature = "custom"))] +fn wrapped_getrandom(dest: &mut [u8]) -> Result<&mut [u8], Error> { + getrandom_impl(dest).map(|()| dest) +} + +// Test that APIs are happy with zero-length requests #[test] fn test_zero() { - // Test that APIs are happy with zero-length requests - getrandom_impl(&mut [0u8; 0]).unwrap(); + getrandom_impl(&mut []).unwrap(); +} +#[test] +fn test_zero_uninit() { + getrandom_uninit_impl(&mut []).unwrap(); } // Return the number of bits in which s1 and s2 differ @@ -23,26 +35,38 @@ fn num_diff_bits(s1: &[u8], s2: &[u8]) -> usize { } // Tests the quality of calling getrandom on two large buffers -#[test] + #[cfg(not(feature = "custom"))] -fn test_diff() { - let mut v1 = [0u8; 1000]; - getrandom_impl(&mut v1).unwrap(); +fn test_diff_large(initial: T, f: impl Fn(&mut [T]) -> Result<&mut [u8], Error>) { + let mut v1 = [initial; 1000]; + let r1 = f(&mut v1).unwrap(); - let mut v2 = [0u8; 1000]; - getrandom_impl(&mut v2).unwrap(); + let mut v2 = [initial; 1000]; + let r2 = f(&mut v2).unwrap(); // Between 3.5 and 4.5 bits per byte should differ. Probability of failure: // ~ 2^(-94) = 2 * CDF[BinomialDistribution[8000, 0.5], 3500] - let d = num_diff_bits(&v1, &v2); + let d = num_diff_bits(r1, r2); assert!(d > 3500); assert!(d < 4500); } -// Tests the quality of calling getrandom repeatedly on small buffers +#[cfg(not(feature = "custom"))] #[test] +fn test_large() { + test_diff_large(0u8, wrapped_getrandom); +} + #[cfg(not(feature = "custom"))] -fn test_small() { +#[test] +fn test_large_uninit() { + test_diff_large(MaybeUninit::uninit(), getrandom_uninit_impl); +} + +// Tests the quality of calling getrandom repeatedly on small buffers + +#[cfg(not(feature = "custom"))] +fn test_diff_small(initial: T, f: impl Fn(&mut [T]) -> Result<&mut [u8], Error>) { // For each buffer size, get at least 256 bytes and check that between // 3 and 5 bits per byte differ. Probability of failure: // ~ 2^(-91) = 64 * 2 * CDF[BinomialDistribution[8*256, 0.5], 3*256] @@ -50,25 +74,62 @@ fn test_small() { let mut num_bytes = 0; let mut diff_bits = 0; while num_bytes < 256 { - let mut s1 = vec![0u8; size]; - getrandom_impl(&mut s1).unwrap(); - let mut s2 = vec![0u8; size]; - getrandom_impl(&mut s2).unwrap(); + let mut s1 = vec![initial; size]; + let r1 = f(&mut s1).unwrap(); + let mut s2 = vec![initial; size]; + let r2 = f(&mut s2).unwrap(); num_bytes += size; - diff_bits += num_diff_bits(&s1, &s2); + diff_bits += num_diff_bits(r1, r2); } assert!(diff_bits > 3 * num_bytes); assert!(diff_bits < 5 * num_bytes); } } +#[cfg(not(feature = "custom"))] +#[test] +fn test_small() { + test_diff_small(0u8, wrapped_getrandom); +} + +#[cfg(not(feature = "custom"))] +#[test] +fn test_small_unnit() { + test_diff_small(MaybeUninit::uninit(), getrandom_uninit_impl); +} + #[test] fn test_huge() { let mut huge = [0u8; 100_000]; getrandom_impl(&mut huge).unwrap(); } +#[test] +fn test_huge_uninit() { + let mut huge = [MaybeUninit::uninit(); 100_000]; + getrandom_uninit_impl(&mut huge).unwrap(); + check_initialized(&huge); +} + +#[allow(unused_variables)] +fn check_initialized(buf: &[MaybeUninit]) { + #[cfg(feature = "unstable-sanitize")] + { + #[cfg(sanitize = "memory")] + { + use core::ffi::c_void; + extern "C" { + // void __msan_check_mem_is_initialized(const volatile void *x, size_t size); + fn __msan_check_mem_is_initialized(x: *const c_void, size: usize); + } + unsafe { + __msan_check_mem_is_initialized(buf.as_ptr().cast::(), buf.len()); + } + } + } +} + // On WASM, the thread API always fails/panics #[cfg(not(target_arch = "wasm32"))] #[test] diff --git a/tests/custom.rs b/tests/custom.rs index b085094b..867833a3 100644 --- a/tests/custom.rs +++ b/tests/custom.rs @@ -5,6 +5,7 @@ feature = "custom", not(feature = "js") ))] +#![cfg_attr(feature = "unstable-sanitize", feature(cfg_sanitize))] use wasm_bindgen_test::wasm_bindgen_test as test; @@ -34,7 +35,7 @@ fn super_insecure_rng(buf: &mut [u8]) -> Result<(), Error> { register_custom_getrandom!(super_insecure_rng); -use getrandom::getrandom as getrandom_impl; +use getrandom::{getrandom as getrandom_impl, getrandom_uninit as getrandom_uninit_impl}; mod common; #[test] diff --git a/tests/normal.rs b/tests/normal.rs index 5fff13b3..2e632222 100644 --- a/tests/normal.rs +++ b/tests/normal.rs @@ -5,7 +5,8 @@ feature = "custom", not(feature = "js") )))] +#![cfg_attr(feature = "unstable-sanitize", feature(cfg_sanitize))] // Use the normal getrandom implementation on this architecture. -use getrandom::getrandom as getrandom_impl; +use getrandom::{getrandom as getrandom_impl, getrandom_uninit as getrandom_uninit_impl}; mod common; diff --git a/tests/rdrand.rs b/tests/rdrand.rs index a355c31e..0cb54ae5 100644 --- a/tests/rdrand.rs +++ b/tests/rdrand.rs @@ -1,8 +1,10 @@ // We only test the RDRAND-based RNG source on supported architectures. #![cfg(any(target_arch = "x86_64", target_arch = "x86"))] +#![cfg_attr(feature = "unstable-sanitize", feature(cfg_sanitize))] // rdrand.rs expects to be part of the getrandom main crate, so we need these // additional imports to get rdrand.rs to compile. +use core::mem::MaybeUninit; use getrandom::Error; #[macro_use] extern crate cfg_if; @@ -13,10 +15,15 @@ mod rdrand; #[path = "../src/util.rs"] mod util; -// The rdrand implementation has the signature of getrandom_uninit(), but our -// tests expect getrandom_impl() to have the signature of getrandom(). +use crate::util::slice_assume_init_mut; + fn getrandom_impl(dest: &mut [u8]) -> Result<(), Error> { rdrand::getrandom_inner(unsafe { util::slice_as_uninit_mut(dest) })?; Ok(()) } +fn getrandom_uninit_impl(dest: &mut [MaybeUninit]) -> Result<&mut [u8], Error> { + rdrand::getrandom_inner(dest)?; + Ok(unsafe { slice_assume_init_mut(dest) }) +} + mod common;