#![allow(clippy::unwrap_used, clippy::panic)]
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Barrier};
use std::time::{Duration, Instant};
use oxicrypt_module::{
Error, KatEntry, SelfTestFailure, State, enter_error_state, initialize_with_tests,
is_operational, require_operational, require_self_test_started, state,
};
use oxicrypt_sha::sha256::{Sha256, sha256};
const CHILD: &str = "OXICRYPT_SELFTEST_GATE_CHILD";
const DEADLINE: Duration = Duration::from_secs(30);
const CHILD_DEADLINE: Duration = Duration::from_mins(1);
fn isolated(name: &str, body: impl FnOnce()) {
if std::env::var_os(CHILD).is_some() {
body();
return;
}
let mut child = std::process::Command::new(std::env::current_exe().unwrap())
.args([name, "--exact", "--test-threads=1"])
.env(CHILD, "1")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
let start = Instant::now();
while child.try_wait().unwrap().is_none() {
if start.elapsed() > CHILD_DEADLINE {
child.kill().unwrap();
panic!("{name} did not finish in its own process within {CHILD_DEADLINE:?}");
}
std::thread::sleep(Duration::from_millis(10));
}
let out = child.wait_with_output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
out.status.success() && stdout.contains("test result: ok. 1 passed"),
"{name} did not pass in its own process:\n{stdout}{}",
String::from_utf8_lossy(&out.stderr)
);
}
const UNSIGNED_TEST_BINARY: &[KatEntry] = &[KatEntry {
name: "integrity not verifiable in an unsigned test binary",
run: || Ok(()),
}];
fn internal() -> Result<(), Error> {
Sha256::new_internal().map(|_| ())
}
fn service() -> Result<(), Error> {
sha256(b"abc").map(|_| ())
}
static PAUSED: AtomicBool = AtomicBool::new(false);
static RELEASE: AtomicBool = AtomicBool::new(false);
static OWNER_ADMITTED: AtomicBool = AtomicBool::new(false);
#[allow(clippy::type_complexity, reason = "three probe results in one slot")]
static OWNER_GATED: std::sync::Mutex<Option<(Result<(), Error>, Result<(), Error>, bool)>> =
std::sync::Mutex::new(None);
static RUNS: AtomicUsize = AtomicUsize::new(0);
static FAIL_AFTER_PAUSE: AtomicBool = AtomicBool::new(false);
static REENTRANT: std::sync::Mutex<Option<Result<(), Error>>> = std::sync::Mutex::new(None);
fn wait_for(flag: &AtomicBool, what: &str) {
let start = Instant::now();
while !flag.load(Ordering::Acquire) {
assert!(start.elapsed() < DEADLINE, "timed out waiting for {what}");
std::thread::yield_now();
}
}
#[allow(clippy::unnecessary_wraps, reason = "a KatEntry's run signature")]
fn paused_self_test() -> Result<(), SelfTestFailure> {
OWNER_ADMITTED.store(internal().is_ok(), Ordering::Release);
*OWNER_GATED.lock().unwrap() = Some((service(), require_operational(), is_operational()));
PAUSED.store(true, Ordering::Release);
wait_for(&RELEASE, "the release of the paused self-test");
if FAIL_AFTER_PAUSE.load(Ordering::Acquire) {
return Err(SelfTestFailure);
}
Ok(())
}
#[allow(clippy::unnecessary_wraps, reason = "a KatEntry's run signature")]
fn latches_error_then_passes() -> Result<(), SelfTestFailure> {
enter_error_state("a conditional self-test fired during initialisation");
Ok(())
}
#[allow(clippy::unnecessary_wraps, reason = "a KatEntry's run signature")]
fn reenters_initialize() -> Result<(), SelfTestFailure> {
let again = initialize_with_tests(UNSIGNED_TEST_BINARY, &[]);
*REENTRANT.lock().unwrap() = Some(again);
Ok(())
}
#[allow(clippy::unnecessary_wraps, reason = "a KatEntry's run signature")]
fn counting_self_test() -> Result<(), SelfTestFailure> {
RUNS.fetch_add(1, Ordering::AcqRel);
Ok(())
}
fn failing_self_test() -> Result<(), SelfTestFailure> {
Err(SelfTestFailure)
}
fn unwinding_self_test() -> Result<(), SelfTestFailure> {
panic!("a self-test that unwinds");
}
fn refused_in(expected: State) -> Result<(), Error> {
Err(Error::NotOperational { current: expected })
}
#[test]
fn selftest_gate_refuses_worker_while_initialisation_is_paused() {
isolated(
"selftest_gate_refuses_worker_while_initialisation_is_paused",
|| {
let tests = [KatEntry {
name: "pauses inside SelfTest",
run: paused_self_test,
}];
let initialiser =
std::thread::spawn(move || initialize_with_tests(UNSIGNED_TEST_BINARY, &tests));
wait_for(&PAUSED, "the initialiser to pause");
assert!(OWNER_ADMITTED.load(Ordering::Acquire));
assert_eq!(
*OWNER_GATED.lock().unwrap(),
Some((
refused_in(State::SelfTest),
refused_in(State::SelfTest),
false
))
);
assert_eq!(state(), State::SelfTest);
assert_eq!(internal(), refused_in(State::SelfTest));
assert_eq!(require_self_test_started(), refused_in(State::SelfTest));
assert_eq!(service(), refused_in(State::SelfTest));
RELEASE.store(true, Ordering::Release);
assert_eq!(initialiser.join().unwrap(), Ok(()));
assert_eq!(state(), State::Operational);
assert_eq!(internal(), Ok(()));
assert_eq!(service(), Ok(()));
},
);
}
#[test]
fn concurrent_initialize_has_one_winner() {
isolated("concurrent_initialize_has_one_winner", || {
const CALLERS: usize = 8;
let start = Arc::new(Barrier::new(CALLERS));
let callers: Vec<_> = (0..CALLERS)
.map(|_| {
let start = Arc::clone(&start);
std::thread::spawn(move || {
let tests = [KatEntry {
name: "counts its runs",
run: counting_self_test,
}];
start.wait();
initialize_with_tests(UNSIGNED_TEST_BINARY, &tests)
})
})
.collect();
let results: Vec<_> = callers.into_iter().map(|c| c.join().unwrap()).collect();
assert_eq!(results.len(), CALLERS);
assert_eq!(results.iter().filter(|r| r.is_ok()).count(), 1);
assert_eq!(
results
.iter()
.filter(|r| **r == Err(Error::AlreadyInitialized))
.count(),
CALLERS - 1
);
assert_eq!(RUNS.load(Ordering::Acquire), 1);
assert_eq!(state(), State::Operational);
});
}
#[test]
fn failed_init_is_terminal() {
isolated("failed_init_is_terminal", || {
let failing = [KatEntry {
name: "fails",
run: failing_self_test,
}];
assert_eq!(
initialize_with_tests(UNSIGNED_TEST_BINARY, &failing),
Err(Error::SelfTestFailed { test: "fails" })
);
assert_eq!(state(), State::Error);
let counting = [KatEntry {
name: "counts its runs",
run: counting_self_test,
}];
assert_eq!(
initialize_with_tests(UNSIGNED_TEST_BINARY, &counting),
Err(Error::AlreadyFailed)
);
let other = std::thread::spawn(move || {
let counting = [KatEntry {
name: "counts its runs",
run: counting_self_test,
}];
initialize_with_tests(UNSIGNED_TEST_BINARY, &counting)
});
assert_eq!(other.join().unwrap(), Err(Error::AlreadyFailed));
assert_eq!(RUNS.load(Ordering::Acquire), 0);
assert_eq!(state(), State::Error);
assert_eq!(internal(), refused_in(State::Error));
});
}
#[test]
fn unwinding_kat_lands_in_error() {
isolated("unwinding_kat_lands_in_error", || {
let unwinding = [KatEntry {
name: "unwinds",
run: unwinding_self_test,
}];
let outcome =
std::panic::catch_unwind(|| initialize_with_tests(UNSIGNED_TEST_BINARY, &unwinding));
assert!(outcome.is_err(), "the self-test did not unwind");
assert_eq!(state(), State::Error);
assert_eq!(internal(), refused_in(State::Error));
assert_eq!(
std::thread::spawn(internal).join().unwrap(),
refused_in(State::Error)
);
assert_eq!(
initialize_with_tests(UNSIGNED_TEST_BINARY, &[]),
Err(Error::AlreadyFailed)
);
});
}
#[test]
fn worker_thread_service_passes_after_init() {
isolated("worker_thread_service_passes_after_init", || {
assert_eq!(initialize_with_tests(UNSIGNED_TEST_BINARY, &[]), Ok(()));
let worker = std::thread::spawn(|| (internal(), service()));
assert_eq!(worker.join().unwrap(), (Ok(()), Ok(())));
});
}
#[test]
fn an_error_latched_during_the_self_tests_survives_their_success() {
isolated(
"an_error_latched_during_the_self_tests_survives_their_success",
|| {
let tests = [KatEntry {
name: "latches Error and passes",
run: latches_error_then_passes,
}];
assert_eq!(
initialize_with_tests(UNSIGNED_TEST_BINARY, &tests),
refused_in(State::Error)
);
assert_eq!(state(), State::Error);
assert_eq!(internal(), refused_in(State::Error));
assert_eq!(service(), refused_in(State::Error));
},
);
}
#[test]
fn a_loser_of_a_failing_winner_gets_already_failed() {
isolated("a_loser_of_a_failing_winner_gets_already_failed", || {
FAIL_AFTER_PAUSE.store(true, Ordering::Release);
let tests = [KatEntry {
name: "pauses, then fails",
run: paused_self_test,
}];
let winner =
std::thread::spawn(move || initialize_with_tests(UNSIGNED_TEST_BINARY, &tests));
wait_for(&PAUSED, "the winner to pause");
assert_eq!(state(), State::SelfTest);
let loser_started = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&loser_started);
let loser = std::thread::spawn(move || {
flag.store(true, Ordering::Release);
initialize_with_tests(UNSIGNED_TEST_BINARY, &[])
});
wait_for(&loser_started, "the loser to start");
RELEASE.store(true, Ordering::Release);
assert_eq!(
winner.join().unwrap(),
Err(Error::SelfTestFailed {
test: "pauses, then fails"
})
);
assert_eq!(loser.join().unwrap(), Err(Error::AlreadyFailed));
assert_eq!(state(), State::Error);
});
}
#[test]
fn a_reentrant_initialize_on_the_owner_returns_without_waiting() {
isolated(
"a_reentrant_initialize_on_the_owner_returns_without_waiting",
|| {
let tests = [KatEntry {
name: "re-enters initialize",
run: reenters_initialize,
}];
assert_eq!(initialize_with_tests(UNSIGNED_TEST_BINARY, &tests), Ok(()));
assert_eq!(
*REENTRANT.lock().unwrap(),
Some(refused_in(State::SelfTest))
);
assert_eq!(state(), State::Operational);
},
);
}