Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 50 additions & 20 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,35 +139,65 @@ jobs:
- run: cargo test --target=x86_64-win7-windows-msvc -Z build-std --features=std
- run: cargo test --target=i686-win7-windows-msvc -Z build-std --features=std

sanitizer-linux-aarch64:
name: Sanitizer Linux AArch64
# MemorySanitizer won't run in QEMU so we can't run it in cross:
# https://github.com/llvm/llvm-project/issues/65144
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2025-06-01
components: rust-src
- env:
RUSTFLAGS: -Dwarnings -Zsanitizer=memory
RUSTDOCFLAGS: -Dwarnings -Zsanitizer=memory
run: cargo test -Zbuild-std --target=aarch64-unknown-linux-gnu
sanitizer-linux:
name: Sanitizer Linux
runs-on: ${{ matrix.runner }}
strategy:
matrix:
arch: [
"aarch64",
"x86_64",
]
include:
# MemorySanitizer won't run in QEMU so we can't run it in cross:
# https://github.com/llvm/llvm-project/issues/65144
- arch: aarch64
runner: ubuntu-24.04-arm

sanitizer-linux-x86_64:
name: Sanitizer Linux x86_64
runs-on: ubuntu-24.04
- arch: x86_64
runner: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2025-06-01
components: rust-src
- env:
- name: default configuration
env:
RUSTFLAGS: -Dwarnings -Zsanitizer=memory
RUSTDOCFLAGS: -Dwarnings -Zsanitizer=memory
run: cargo test -Zbuild-std --target=x86_64-unknown-linux-gnu
run: cargo test -Zbuild-std --target=${{ matrix.arch }}-unknown-linux-gnu
- name: --cfg getrandom_backend="linux_getrandom"
env:
RUSTFLAGS: --cfg getrandom_backend="linux_getrandom" -Dwarnings -Zsanitizer=memory
RUSTDOCFLAGS: --cfg getrandom_backend="linux_getrandom" -Dwarnings -Zsanitizer=memory
run: cargo test -Zbuild-std --target=${{ matrix.arch }}-unknown-linux-gnu
- name: --cfg getrandom_backend="linux_raw"
env:
RUSTFLAGS: --cfg getrandom_backend="linux_raw" -Dwarnings -Zsanitizer=memory
RUSTDOCFLAGS: --cfg getrandom_backend="linux_raw" -Dwarnings -Zsanitizer=memory
run: cargo test -Zbuild-std --target=${{ matrix.arch }}-unknown-linux-gnu
- name: --cfg getrandom_backend="linux_fallback"
env:
RUSTFLAGS: --cfg getrandom_backend="linux_fallback" -Dwarnings -Zsanitizer=memory
RUSTDOCFLAGS: --cfg getrandom_backend="linux_fallback" -Dwarnings -Zsanitizer=memory
run: cargo test -Zbuild-std --target=${{ matrix.arch }}-unknown-linux-gnu
- if: ${{ matrix.arch == 'x86_64' }}
name: --cfg getrandom_backend="rdrand"
env:
RUSTFLAGS: --cfg getrandom_backend="rdrand" -Dwarnings -Zsanitizer=memory
RUSTDOCFLAGS: --cfg getrandom_backend="rdrand" -Dwarnings -Zsanitizer=memory
run: cargo test -Zbuild-std --target=${{ matrix.arch }}-unknown-linux-gnu
- name: --cfg getrandom_test_linux_fallback
env:
RUSTFLAGS: --cfg getrandom_test_linux_fallback -Dwarnings -Zsanitizer=memory
RUSTDOCFLAGS: --cfg getrandom_test_linux_fallback -Dwarnings -Zsanitizer=memory
run: cargo test -Zbuild-std --target=${{ matrix.arch }}-unknown-linux-gnu
- name: --cfg getrandom_test_linux_without_fallback
env:
RUSTFLAGS: --cfg getrandom_test_linux_without_fallback -Dwarnings -Zsanitizer=memory
RUSTDOCFLAGS: --cfg getrandom_test_linux_without_fallback -Dwarnings -Zsanitizer=memory
run: cargo test -Zbuild-std --target=${{ matrix.arch }}-unknown-linux-gnu

cross:
name: Cross
Expand Down
9 changes: 8 additions & 1 deletion src/backends.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
//!
//! This module should provide `fill_inner` with the signature
//! `fn fill_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error>`.
//! The function MUST fully initialize `dest` when `Ok(())` is returned.
//! The function MUST fully initialize `dest` when `Ok(())` is returned;
//! the function may need to use `sanitizer::unpoison` as well.
//! The function MUST NOT ever write uninitialized bytes into `dest`,
//! regardless of what value it returns.

Expand All @@ -12,9 +13,11 @@ cfg_if! {
pub use custom::*;
} else if #[cfg(getrandom_backend = "linux_getrandom")] {
mod getrandom;
mod sanitizer;
pub use getrandom::*;
} else if #[cfg(getrandom_backend = "linux_raw")] {
mod linux_raw;
mod sanitizer;
pub use linux_raw::*;
} else if #[cfg(getrandom_backend = "rdrand")] {
mod rdrand;
Expand Down Expand Up @@ -43,6 +46,7 @@ cfg_if! {
pub use unsupported::*;
} else if #[cfg(all(target_os = "linux", target_env = ""))] {
mod linux_raw;
mod sanitizer;
pub use linux_raw::*;
} else if #[cfg(target_os = "espidf")] {
mod esp_idf;
Expand Down Expand Up @@ -102,6 +106,7 @@ cfg_if! {
))] {
mod use_file;
mod linux_android_with_fallback;
mod sanitizer;
pub use linux_android_with_fallback::*;
} else if #[cfg(any(
target_os = "android",
Expand All @@ -116,6 +121,8 @@ cfg_if! {
all(target_os = "horizon", target_arch = "arm"),
))] {
mod getrandom;
#[cfg(any(target_os = "android", target_os = "linux"))]
mod sanitizer;
pub use getrandom::*;
} else if #[cfg(target_os = "solaris")] {
mod solaris;
Expand Down
10 changes: 9 additions & 1 deletion src/backends/getrandom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ mod util_libc;
#[inline]
pub fn fill_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
util_libc::sys_fill_exact(dest, |buf| unsafe {
libc::getrandom(buf.as_mut_ptr().cast(), buf.len(), 0)
let ret = libc::getrandom(buf.as_mut_ptr().cast(), buf.len(), 0);

#[cfg(any(target_os = "android", target_os = "linux"))]
#[allow(unused_unsafe)] // TODO(MSRV 1.65): Remove this.
unsafe {
super::sanitizer::unpoison_linux_getrandom_result(buf, ret);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is unpoisoning needed if we are calling the libc version of getrandom? The comment in sanitizer.rs seems to indicate that the libc calls should handle unpoisioning.

Copy link
Contributor Author

@briansmith briansmith Jun 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See

/// Memory Sanitizer only intercepts `getrandom` on this condition (from its
/// source code):
/// ```c
/// #define SANITIZER_INTERCEPT_GETRANDOM \
/// ((SI_LINUX && __GLIBC_PREREQ(2, 25)) || SI_FREEBSD || SI_SOLARIS)
/// ```
/// So, effectively, we have to assume that it is never intercepted on Linux.
. Basically the way MSAN is configured by Rust, to support older glibc, ensures that the interception is disabled for glibc. And, MSAN won't do interception for non-glibc.

It is possible that the libc has interception built-in, but we have no way of knowing. I think common open source implementations like musl libc don't, except maybe LLVM libc? If we have a way to detect that LLVM libc will be used, we could make an exception for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: Maybe Android bionic has built-in support for unpoisoning, in which case we could avoid unpoisoning on Android. IDK for sure.

Copy link
Member

@josephlr josephlr Jun 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was confused mostly because libc::getrandom will only exist on GLibc 2.25 or later, so I figured we would either have:

  • An older glibc, so we fall back to /dev/urandom or fail depending on the backend
  • A glibc >= 2.25, so libc::getrandom exists and gets properly handled by msan/libc

But if there's an extra level of config between Rust libc and msan, it totally makes sense why this wouldn't work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@josephlr Do you know what the Android bionic situation is? IDK if MSAN even works for Android. Would love to minimize the use of this as much as possible.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have personally not ever used bionic with MSAN. When fuzzing or running tests against C libraries, we usually just run them on Linux, as most of the validation is not for OS specific stuff. But that doesn't mean it's not possible, I've asked some folks at work and we'll see if they get back to me.

Copy link
Member

@josephlr josephlr Jun 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldnt find any mention about MSan in Android's memory debugging guide: https://developer.android.com/ndk/guides/memory-debug and all the other tools I found were also of an ASan like flavor.

I would be fine assuming that:

  1. Android Bionic (as part of the NDK) doesn't support MSan
  2. if this support is added, it will correctly handle the libc-based functions which write buffers (libc::read, libc::getrandom, etc...) unconditionally, as newer versions of the libc will always have libc::getrandom

I might be wrong, but it seems fine to remove this for android.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about MUSL targets?

}

ret
})
}
6 changes: 4 additions & 2 deletions src/backends/linux_android_with_fallback.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! Implementation for Linux / Android with `/dev/urandom` fallback
use super::use_file;
use super::{sanitizer, use_file};
use crate::Error;
use core::{
ffi::c_void,
Expand Down Expand Up @@ -95,7 +95,9 @@ pub fn fill_inner(dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {
// note: `transmute` is currently the only way to convert a pointer into a function reference
let getrandom_fn = unsafe { transmute::<NonNull<c_void>, GetRandomFn>(fptr) };
util_libc::sys_fill_exact(dest, |buf| unsafe {
getrandom_fn(buf.as_mut_ptr().cast(), buf.len(), 0)
let ret = getrandom_fn(buf.as_mut_ptr().cast(), buf.len(), 0);
sanitizer::unpoison_linux_getrandom_result(buf, ret);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, this ends up calling libc::getrandom which I think should unpoision the buffer.

ret
})
}
}
5 changes: 3 additions & 2 deletions src/backends/linux_raw.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Implementation for Linux / Android using `asm!`-based syscalls.
use crate::{Error, MaybeUninit};

use super::sanitizer;
pub use crate::util::{inner_u32, inner_u64};
use crate::{Error, MaybeUninit};

#[cfg(not(any(target_os = "android", target_os = "linux")))]
compile_error!("`linux_raw` backend can be enabled only for Linux/Android targets!");
Expand Down Expand Up @@ -118,6 +118,7 @@ pub fn fill_inner(mut dest: &mut [MaybeUninit<u8>]) -> Result<(), Error> {

loop {
let ret = unsafe { getrandom_syscall(dest.as_mut_ptr().cast(), dest.len(), 0) };
unsafe { sanitizer::unpoison_linux_getrandom_result(dest, ret) };
match usize::try_from(ret) {
Ok(0) => return Err(Error::UNEXPECTED),
Ok(len) => {
Expand Down
59 changes: 59 additions & 0 deletions src/backends/sanitizer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use core::mem::MaybeUninit;

/// Unpoisons `buf` if MSAN support is enabled.
///
/// Most backends do not need to unpoison their output. Rust language- and
/// library- provided functionality unpoisons automatically. Similarly, libc
/// either natively supports MSAN and/or MSAN hooks libc-provided functions
/// to unpoison outputs on success. Only when all of these things are
/// bypassed do we need to do it ourselves.
///
/// The call to unpoison should be done as close to the write as possible.
/// For example, if the backend partially fills the output buffer in chunks,
/// each chunk should be unpoisoned individually. This way, the correctness of
/// the chunking logic can be validated (in part) using MSAN.
pub unsafe fn unpoison(buf: &mut [MaybeUninit<u8>]) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be private? Do we have any existing places where we call this helper?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be private but it is intended to be useful for any future backends that need to do unpoisoning themselves.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some history for this: Originally I wrote the unpoison function and called it only from the linux_raw backend because I expected that libc or MSAN would do the unpoisoning for libc::getrandom. But, tests failed without the unpoisoning. Then I factored out the logic I had added to linux_raw into a reusable function and put that function into sanitizer.rs. Now that one function is the only one that calls unpoison. I considered making unpoison non-public accordingly, but I wanted to emphasize that it is intended to be useful independent of its current single caller, if/when we need it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, I think these are fine to have as-is then.

cfg_if! {
if #[cfg(getrandom_msan)] {
extern "C" {
fn __msan_unpoison(a: *mut core::ffi::c_void, size: usize);
}
let a = buf.as_mut_ptr().cast();
let size = buf.len();
#[allow(unused_unsafe)] // TODO(MSRV 1.65): Remove this.
unsafe {
__msan_unpoison(a, size);
}
} else {
let _ = buf;
}
}
}

/// Interprets the result of the `getrandom` syscall of Linux, unpoisoning any
/// written part of `buf`.
///
/// `buf` must be the output buffer that was originally passed to the `getrandom`
/// syscall.
///
/// `ret` must be the result returned by `getrandom`. If `ret` is negative or
/// larger than the length of `buf` then nothing is done.
///
/// Memory Sanitizer only intercepts `getrandom` on this condition (from its
/// source code):
/// ```c
/// #define SANITIZER_INTERCEPT_GETRANDOM \
/// ((SI_LINUX && __GLIBC_PREREQ(2, 25)) || SI_FREEBSD || SI_SOLARIS)
/// ```
/// So, effectively, we have to assume that it is never intercepted on Linux.
#[cfg(any(target_os = "android", target_os = "linux"))]
pub unsafe fn unpoison_linux_getrandom_result(buf: &mut [MaybeUninit<u8>], ret: isize) {
if let Ok(bytes_written) = usize::try_from(ret) {
if let Some(written) = buf.get_mut(..bytes_written) {
#[allow(unused_unsafe)] // TODO(MSRV 1.65): Remove this.
unsafe {
unpoison(written)
}
}
}
}
7 changes: 1 addition & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,7 @@ pub fn fill_uninit(dest: &mut [MaybeUninit<u8>]) -> Result<&mut [u8], Error> {

// SAFETY: `dest` has been fully initialized by `imp::fill_inner`
// since it returned `Ok`.
Ok(unsafe {
#[cfg(getrandom_msan)]
__msan_unpoison(dest.as_mut_ptr().cast(), dest.len());

util::slice_assume_init_mut(dest)
})
Ok(unsafe { util::slice_assume_init_mut(dest) })
}

/// Get random `u32` from the system's preferred random number source.
Expand Down