#![cfg(all(unix, not(miri)))]
use std::os::unix::process::ExitStatusExt;
use std::process::{Command, Output};
#[cfg(memlock_limit)]
use vitaminc_protected::LockError;
use vitaminc_protected::{LockPolicy, Locked};
mod common;
#[cfg(memlock_limit)]
use common::lock_required;
#[cfg(target_os = "linux")]
use common::vm_flags;
fn case_ok(case: &str) {
println!("case ok: {case}");
}
#[cfg(memlock_limit)]
fn case_skipped(case: &str, why: &str) {
assert!(
!lock_required(),
"{case} cannot run here ({why}), and this job requires it"
);
println!("case skipped: {case}: {why}");
}
const CASE: &str = "LOCKED_CHILD_CASE";
fn child(case: &str) -> Output {
Command::new(std::env::current_exe().unwrap())
.env(CASE, case)
.args(["--exact", "child_entry", "--nocapture", "--test-threads=1"])
.output()
.unwrap()
}
#[cfg(memlock_limit)]
fn lower_memlock_to_zero() {
let lim = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
let rc = unsafe { libc::setrlimit(libc::RLIMIT_MEMLOCK, &lim) };
assert_eq!(rc, 0, "setrlimit: {}", std::io::Error::last_os_error());
}
#[cfg(memlock_limit)]
fn refusal_can_be_provoked(case: &str) -> bool {
lower_memlock_to_zero();
let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
let probe = unsafe {
libc::mmap(
std::ptr::null_mut(),
page,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
-1,
0,
)
};
assert_ne!(probe, libc::MAP_FAILED);
let locked = unsafe { libc::mlock(probe, page) } == 0;
unsafe {
let _ = libc::munmap(probe, page);
}
if locked {
case_skipped(
case,
"this process can lock memory with RLIMIT_MEMLOCK at zero (root, CAP_IPC_LOCK, or a sanitizer's mlock interceptor)",
);
}
!locked
}
#[test]
fn child_entry() {
let Ok(case) = std::env::var(CASE) else {
return;
};
match case.as_str() {
#[cfg(memlock_limit)]
"refused_best_effort" => {
if !refusal_can_be_provoked(&case) {
return;
}
let key = Locked::new([1u8; 32]).unwrap();
assert!(!key.locked());
#[cfg(target_os = "linux")]
{
let flags = vm_flags(key.risky_ref().as_ptr() as usize);
assert!(flags.split(' ').any(|f| f == "dd"), "VmFlags: {flags}");
}
assert!(
matches!(key.lock_error(), Some(LockError::Refused { .. })),
"{:?}",
key.lock_error()
);
let msg = key.lock_error().unwrap().to_string();
assert!(
msg.contains("RLIMIT_MEMLOCK soft limit is 0 bytes"),
"{msg}"
);
assert!(matches!(
key.require_locked(),
Err(LockError::Refused { .. })
));
}
#[cfg(memlock_limit)]
"refused_strict" => {
if !refusal_can_be_provoked(&case) {
return;
}
LockPolicy::Strict.set().unwrap();
assert_eq!(LockPolicy::current(), LockPolicy::Strict);
let r: Result<Locked<[u8; 32]>, _> = Locked::new([1u8; 32]);
assert!(matches!(r, Err(LockError::Refused { .. })), "{r:?}");
}
"policy_is_set_once" => {
assert_eq!(LockPolicy::current(), LockPolicy::BestEffort);
LockPolicy::Strict.set().unwrap();
LockPolicy::Strict.set().unwrap(); let err = LockPolicy::BestEffort.set().unwrap_err();
assert_eq!(err.current, LockPolicy::Strict);
}
"drop_unmaps" => {
let probe = |addr: usize, len: usize| -> Option<i32> {
let rc = unsafe { libc::msync(addr as *mut libc::c_void, len, libc::MS_ASYNC) };
(rc != 0).then(|| std::io::Error::last_os_error().raw_os_error().unwrap())
};
let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
let key = Locked::new([1u8; 32]).unwrap();
let interior = key.risky_ref().as_ptr() as usize & !(page - 1);
assert_eq!(
probe(interior, page),
None,
"the interior is mapped while live"
);
let ranges = [interior - page, interior, interior + page];
drop(key);
for addr in ranges {
assert_eq!(
probe(addr, page),
Some(libc::ENOMEM),
"{addr:#x} still mapped"
);
}
}
"overrun_faults" => {
let key = Locked::new([1u8; 32]).unwrap();
let start = key.risky_ref().as_ptr();
let past = unsafe { start.add(32) } as *mut u8;
unsafe { std::ptr::write_volatile(past, 1) };
unreachable!("the write into the guard page must fault");
}
other => panic!("unknown case {other}"),
}
case_ok(&case);
}
fn assert_case(out: &Output, case: &str) {
let stdout = String::from_utf8_lossy(&out.stdout);
let ran = stdout.contains(&format!("case ok: {case}"));
let skipped = stdout.contains(&format!("case skipped: {case}"));
assert!(
out.status.success() && (ran || skipped),
"child did not complete case {case}: {}\n{}\n{}",
out.status,
stdout,
String::from_utf8_lossy(&out.stderr)
);
if skipped {
eprintln!(
"{}",
stdout
.lines()
.find(|l| l.contains("case skipped"))
.unwrap_or("")
);
}
}
#[cfg(memlock_limit)]
#[test]
fn a_refused_lock_is_reported_under_best_effort() {
assert_case(&child("refused_best_effort"), "refused_best_effort");
}
#[cfg(memlock_limit)]
#[test]
fn a_refused_lock_fails_the_constructor_under_strict() {
assert_case(&child("refused_strict"), "refused_strict");
}
#[test]
fn the_policy_is_set_once_per_process() {
assert_case(&child("policy_is_set_once"), "policy_is_set_once");
}
#[test]
fn dropping_the_value_unmaps_the_interior_and_both_guards() {
assert_case(&child("drop_unmaps"), "drop_unmaps");
}
#[test]
fn a_write_past_the_value_faults_on_the_guard_page() {
let out = child("overrun_faults");
let signal = out.status.signal();
assert!(
matches!(signal, Some(libc::SIGSEGV) | Some(libc::SIGBUS)),
"expected a fault, got {:?}\n{}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
}