use memsafe::Secret;
struct SpySource {
data: Vec<u8>,
zeroed_at_drop: std::sync::Arc<std::sync::atomic::AtomicBool>,
}
impl AsMut<[u8]> for SpySource {
fn as_mut(&mut self) -> &mut [u8] {
&mut self.data
}
}
impl Drop for SpySource {
fn drop(&mut self) {
let all_zero = self.data.iter().all(|&b| b == 0);
self.zeroed_at_drop
.store(all_zero, std::sync::atomic::Ordering::SeqCst);
}
}
#[test]
fn from_bytes_zeroizes_source_before_drop() {
use std::sync::atomic::Ordering;
use std::sync::Arc;
let flag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let source = SpySource {
data: b"super-secret-material".to_vec(),
zeroed_at_drop: flag.clone(),
};
assert!(
source.data.iter().any(|&b| b != 0),
"precondition: source is non-zero"
);
let mut secret = match Secret::<32>::from_bytes(source) {
Ok(s) => s,
Err(_) => panic!("from_bytes must succeed for a fitting source"),
};
assert!(
flag.load(Ordering::SeqCst),
"source bytes must be zeroized before the source is dropped"
);
let view = secret.read().unwrap();
assert_eq!(&view[..21], b"super-secret-material");
}
#[test]
fn from_bytes_length_error_does_not_zeroize_source() {
use std::sync::atomic::Ordering;
use std::sync::Arc;
let flag = Arc::new(std::sync::atomic::AtomicBool::new(true));
let source = SpySource {
data: b"way-too-long-for-a-4-byte-secret".to_vec(),
zeroed_at_drop: flag.clone(),
};
let (returned, _err) = match Secret::<4>::from_bytes(source) {
Ok(_) => panic!("expected length-mismatch error"),
Err(e) => e,
};
assert_eq!(returned.data, b"way-too-long-for-a-4-byte-secret");
drop(returned);
assert!(
!flag.load(Ordering::SeqCst),
"length-error path must return the source untouched, not zeroized"
);
}
#[cfg(unix)]
#[test]
fn sealed_page_read_is_fatal() {
if std::env::var_os("MEMSAFE_SEALED_READ_CHILD").is_some() {
let mut secret = Secret::<32>::new_with(|buf| buf[0] = 0xAA).unwrap();
let ptr_addr = {
let view = secret.read().unwrap();
view.as_ptr() as usize
};
let leaked = unsafe { std::ptr::read_volatile(ptr_addr as *const u8) };
eprintln!("SECURITY FAILURE: sealed page was readable, got {leaked:#x}");
std::process::exit(0);
}
let exe = std::env::current_exe().unwrap();
let status = std::process::Command::new(exe)
.args([
"sealed_page_read_is_fatal",
"--exact",
"--nocapture",
"--test-threads=1",
])
.env("MEMSAFE_SEALED_READ_CHILD", "1")
.status()
.unwrap();
if status.code() == Some(127) {
eprintln!("skipping: this environment cannot respawn the test binary");
return;
}
use std::os::unix::process::ExitStatusExt;
assert!(
status.signal().is_some(),
"child must die by SIGSEGV/SIGBUS when reading a sealed page, got: {status:?}"
);
}
#[cfg(unix)]
#[test]
fn sealed_page_write_is_fatal() {
if std::env::var_os("MEMSAFE_SEALED_WRITE_CHILD").is_some() {
let mut secret = Secret::<32>::new_with(|_| {}).unwrap();
let ptr_addr = {
let mut guard = secret.write().unwrap();
guard.as_mut_ptr() as usize
};
unsafe { std::ptr::write_volatile(ptr_addr as *mut u8, 0xFF) };
eprintln!("SECURITY FAILURE: sealed page was writable");
std::process::exit(0);
}
let exe = std::env::current_exe().unwrap();
let status = std::process::Command::new(exe)
.args([
"sealed_page_write_is_fatal",
"--exact",
"--nocapture",
"--test-threads=1",
])
.env("MEMSAFE_SEALED_WRITE_CHILD", "1")
.status()
.unwrap();
if status.code() == Some(127) {
eprintln!("skipping: this environment cannot respawn the test binary");
return;
}
use std::os::unix::process::ExitStatusExt;
assert!(
status.signal().is_some(),
"child must die by signal when writing a sealed page, got: {status:?}"
);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn mlock_failure_rolls_back_cleanly() {
const HUGE: usize = 1 << 42;
let result = Secret::<HUGE>::new_with(|_| {
panic!("init must never run when construction fails before the page is ready");
});
assert!(
result.is_err(),
"locking 4 TiB must fail with a MemoryError"
);
}