blob: b3a9880211dec4089137069670cfdf831f1966fc [file] [log] [blame]
use std::str::FromStr;
use std::sync::OnceLock;
pub use cxx::UniquePtr;
pub use self::ffi::*;
/// Defined in "genmc/src/Support/SAddr.hpp".
/// The first bit of all global addresses must be set to `1`.
/// This means the mask, interpreted as an address, is the lower bound of where the global address space starts.
///
/// FIXME(genmc): rework this if non-64bit support is added to GenMC (the current allocation scheme only allows for 64bit addresses).
/// FIXME(genmc): currently we use `get_global_alloc_static_mask()` to ensure the constant is consistent between Miri and GenMC,
/// but if https://github.com/dtolnay/cxx/issues/1051 is fixed we could share the constant directly.
pub const GENMC_GLOBAL_ADDRESSES_MASK: u64 = 1 << 63;
/// GenMC thread ids are C++ type `int`, which is equivalent to Rust's `i32` on most platforms.
/// The main thread always has thread id 0.
pub const GENMC_MAIN_THREAD_ID: i32 = 0;
/// Changing GenMC's log level is not thread safe, so we limit it to only be set once to prevent any data races.
/// This value will be initialized when the first `MiriGenmcShim` is created.
static GENMC_LOG_LEVEL: OnceLock<LogLevel> = OnceLock::new();
// Create a new handle to the GenMC model checker.
// The first call to this function determines the log level of GenMC, any future call with a different log level will panic.
pub fn create_genmc_driver_handle(
params: &GenmcParams,
genmc_log_level: LogLevel,
do_estimation: bool,
) -> UniquePtr<MiriGenmcShim> {
// SAFETY: Only setting the GenMC log level once is guaranteed by the `OnceLock`.
// No other place calls `set_log_level_raw`, so the `logLevel` value in GenMC will not change once we initialize it once.
// All functions that use GenMC's `logLevel` can only be accessed in safe Rust through a `MiriGenmcShim`.
// There is no way to get `MiriGenmcShim` other than through `create_handle`, and we only call it *after* setting the log level, preventing any possible data races.
assert_eq!(
&genmc_log_level,
GENMC_LOG_LEVEL.get_or_init(|| {
unsafe { set_log_level_raw(genmc_log_level) };
genmc_log_level
}),
"Attempt to change the GenMC log level after it was already set"
);
unsafe { MiriGenmcShim::create_handle(params, do_estimation) }
}
impl GenmcScalar {
pub const UNINIT: Self = Self { value: 0, provenance: 0, is_init: false };
pub const fn from_u64(value: u64) -> Self {
Self { value, provenance: 0, is_init: true }
}
pub const fn has_provenance(&self) -> bool {
self.provenance != 0
}
}
impl Default for GenmcParams {
fn default() -> Self {
Self {
estimation_max: 1000, // default taken from GenMC
print_random_schedule_seed: false,
do_symmetry_reduction: false,
// GenMC graphs can be quite large since Miri produces a lot of (non-atomic) events.
print_execution_graphs: ExecutiongraphPrinting::None,
disable_weak_memory_emulation: false,
}
}
}
impl Default for LogLevel {
fn default() -> Self {
// FIXME(genmc): set `Tip` by default once the GenMC tips are relevant to Miri.
Self::Warning
}
}
impl FromStr for SchedulePolicy {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"wf" => SchedulePolicy::WF,
"wfr" => SchedulePolicy::WFR,
"arbitrary" | "random" => SchedulePolicy::Arbitrary,
"ltr" => SchedulePolicy::LTR,
_ => return Err("invalid scheduling policy"),
})
}
}
impl FromStr for LogLevel {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"quiet" => LogLevel::Quiet,
"error" => LogLevel::Error,
"warning" => LogLevel::Warning,
"tip" => LogLevel::Tip,
"debug1" => LogLevel::Debug1Revisits,
"debug2" => LogLevel::Debug2MemoryAccesses,
"debug3" => LogLevel::Debug3ReadsFrom,
_ => return Err("invalid log level"),
})
}
}
#[cxx::bridge]
mod ffi {
/**** Types shared between Miri/Rust and Miri/C++ through cxx_bridge: ****/
/// Parameters that will be given to GenMC for setting up the model checker.
/// The fields of this struct are visible to both Rust and C++.
/// Note that this struct is #[repr(C)], so the order of fields matters.
#[derive(Clone, Debug)]
struct GenmcParams {
/// Maximum number of executions explored in estimation mode.
pub estimation_max: u32,
pub print_random_schedule_seed: bool,
pub do_symmetry_reduction: bool,
pub print_execution_graphs: ExecutiongraphPrinting,
/// Enabling this will set the memory model used by GenMC to "Sequential Consistency" (SC).
/// This will disable any weak memory effects, which reduces the number of program executions that will be explored.
pub disable_weak_memory_emulation: bool,
}
/// This is mostly equivalent to GenMC `VerbosityLevel`, but the debug log levels are always present (not conditionally compiled based on `ENABLE_GENMC_DEBUG`).
/// We add this intermediate type to prevent changes to the GenMC log-level from breaking the Miri
/// build, and to have a stable type for the C++-Rust interface, independent of `ENABLE_GENMC_DEBUG`.
#[derive(Debug)]
enum LogLevel {
/// Disable *all* logging (including error messages on a crash).
Quiet,
/// Log errors.
Error,
/// Log errors and warnings.
Warning,
/// Log errors, warnings and tips.
Tip,
/// Debug print considered revisits.
/// Downgraded to `Tip` if `GENMC_DEBUG` is not enabled.
Debug1Revisits,
/// Print the execution graph after every memory access.
/// Also includes the previous debug log level.
/// Downgraded to `Tip` if `GENMC_DEBUG` is not enabled.
Debug2MemoryAccesses,
/// Print reads-from values considered by GenMC.
/// Also includes the previous debug log level.
/// Downgraded to `Tip` if `GENMC_DEBUG` is not enabled.
Debug3ReadsFrom,
}
#[derive(Debug)]
/// Setting to control which execution graphs GenMC prints after every execution.
enum ExecutiongraphPrinting {
/// Print no graphs.
None,
/// Print graphs of all fully explored executions.
Explored,
/// Print graphs of all blocked executions.
Blocked,
/// Print graphs of all executions.
ExploredAndBlocked,
}
/// This type corresponds to `Option<SVal>` (or `std::optional<SVal>`), where `SVal` is the type that GenMC uses for storing values.
#[derive(Debug, Clone, Copy)]
struct GenmcScalar {
/// The raw byte-level value (discarding provenance, if any) of this scalar.
value: u64,
/// This is zero for integer values. For pointers, this encodes the provenance by
/// storing the base address of the allocation that this pointer belongs to.
/// Operations on `SVal` in GenMC (e.g., `fetch_add`) preserve the `provenance` of the left
/// argument (`left.fetch_add(right, ...)`).
provenance: u64,
/// Indicates whether this value is initialized. If this is `false`, the other fields do not matter.
/// (Ideally we'd use `std::optional` but CXX does not support that.)
is_init: bool,
}
#[must_use]
#[derive(Debug, Clone, Copy)]
enum ExecutionState {
Ok,
Error,
Blocked,
Finished,
}
#[must_use]
#[derive(Debug)]
struct SchedulingResult {
exec_state: ExecutionState,
next_thread: i32,
}
#[must_use]
#[derive(Debug)]
struct EstimationResult {
/// Expected number of total executions.
mean: f64,
/// Standard deviation of the total executions estimate.
sd: f64,
/// Number of explored executions during the estimation.
explored_execs: u64,
/// Number of encounteded blocked executions during the estimation.
blocked_execs: u64,
}
#[must_use]
#[derive(Debug)]
struct LoadResult {
/// If not null, contains the error encountered during the handling of the load.
error: UniquePtr<CxxString>,
/// Indicates whether a value was read or not.
has_value: bool,
/// The value that was read. Should not be used if `has_value` is `false`.
read_value: GenmcScalar,
}
#[must_use]
#[derive(Debug)]
struct StoreResult {
/// If not null, contains the error encountered during the handling of the store.
error: UniquePtr<CxxString>,
/// `true` if the write should also be reflected in Miri's memory representation.
is_coherence_order_maximal_write: bool,
}
#[must_use]
#[derive(Debug)]
struct ReadModifyWriteResult {
/// If there was an error, it will be stored in `error`, otherwise it is `None`.
error: UniquePtr<CxxString>,
/// The value that was read by the RMW operation as the left operand.
old_value: GenmcScalar,
/// The value that was produced by the RMW operation.
new_value: GenmcScalar,
/// `true` if the write should also be reflected in Miri's memory representation.
is_coherence_order_maximal_write: bool,
}
#[must_use]
#[derive(Debug)]
struct CompareExchangeResult {
/// If there was an error, it will be stored in `error`, otherwise it is `None`.
error: UniquePtr<CxxString>,
/// The value that was read by the compare-exchange.
old_value: GenmcScalar,
/// `true` if compare_exchange op was successful.
is_success: bool,
/// `true` if the write should also be reflected in Miri's memory representation.
is_coherence_order_maximal_write: bool,
}
#[must_use]
#[derive(Debug)]
struct MutexLockResult {
/// If there was an error, it will be stored in `error`, otherwise it is `None`.
error: UniquePtr<CxxString>,
/// If true, GenMC determined that we should retry the mutex lock operation once the thread attempting to lock is scheduled again.
is_reset: bool,
/// Indicate whether the lock was acquired by this thread.
is_lock_acquired: bool,
}
/**** These are GenMC types that we have to copy-paste here since cxx does not support
"importing" externally defined C++ types. ****/
#[derive(Clone, Copy, Debug)]
enum SchedulePolicy {
LTR,
WF,
WFR,
Arbitrary,
}
#[derive(Debug)]
/// Corresponds to GenMC's type with the same name.
/// Should only be modified if changed by GenMC.
enum ActionKind {
/// Any MIR terminator that's atomic and that may have load semantics.
/// This includes functions with atomic properties, such as `pthread_create`.
/// If the exact type of the terminator cannot be determined, load is a safe default `Load`.
Load,
/// Anything that's definitely not a `Load`.
NonLoad,
}
#[derive(Debug)]
/// Corresponds to GenMC's type with the same name.
/// Should only be modified if changed by GenMC.
enum MemOrdering {
NotAtomic = 0,
Relaxed = 1,
// We skip 2 in case we support consume.
Acquire = 3,
Release = 4,
AcquireRelease = 5,
SequentiallyConsistent = 6,
}
#[derive(Debug)]
enum RMWBinOp {
Xchg = 0,
Add = 1,
Sub = 2,
And = 3,
Nand = 4,
Or = 5,
Xor = 6,
Max = 7,
Min = 8,
UMax = 9,
UMin = 10,
}
#[derive(Debug)]
enum AssumeType {
User = 0,
Barrier = 1,
Spinloop = 2,
}
// # Safety
//
// This block is unsafe to allow defining safe methods inside.
//
// `get_global_alloc_static_mask` is safe since it just returns a constant.
// All methods on `MiriGenmcShim` are safe by the correct usage of the two unsafe functions
// `set_log_level_raw` and `MiriGenmcShim::create_handle`.
// See the doc comment on those two functions for their safety requirements.
unsafe extern "C++" {
include!("MiriInterface.hpp");
/**** Types shared between Miri/Rust and Miri/C++: ****/
type MiriGenmcShim;
/**** Types shared between Miri/Rust and GenMC/C++:
(This tells cxx that the enums defined above are already defined on the C++ side;
it will emit assertions to ensure that the two definitions agree.) ****/
type ActionKind;
type AssumeType;
type MemOrdering;
type RMWBinOp;
type SchedulePolicy;
/// Set the log level for GenMC.
///
/// # Safety
///
/// This function is not thread safe, since it writes to the global, mutable, non-atomic `logLevel` variable.
/// Any GenMC function may read from `logLevel` unsynchronized.
/// The safest way to use this function is to set the log level exactly once before first calling `create_handle`.
/// Never calling this function is safe, GenMC will fall back to its default log level.
unsafe fn set_log_level_raw(log_level: LogLevel);
/// Create a new `MiriGenmcShim`, which wraps a `GenMCDriver`.
///
/// # Safety
///
/// This function is marked as unsafe since the `logLevel` global variable is non-atomic.
/// This function should not be called in an unsynchronized way with `set_log_level_raw`, since
/// this function and any methods on the returned `MiriGenmcShim` may read the `logLevel`,
/// causing a data race.
/// The safest way to use these functions is to call `set_log_level_raw` once, and only then
/// start creating handles.
/// There should not be any other (safe) way to create a `MiriGenmcShim`.
#[Self = "MiriGenmcShim"]
unsafe fn create_handle(
params: &GenmcParams,
estimation_mode: bool,
) -> UniquePtr<MiriGenmcShim>;
/// Get the bit mask that GenMC expects for global memory allocations.
fn get_global_alloc_static_mask() -> u64;
/// This function must be called at the start of any execution, before any events are reported to GenMC.
fn handle_execution_start(self: Pin<&mut MiriGenmcShim>);
/// This function must be called at the end of any execution, even if an error was found during the execution.
/// Returns `null`, or a string containing an error message if an error occured.
fn handle_execution_end(self: Pin<&mut MiriGenmcShim>) -> UniquePtr<CxxString>;
/***** Functions for handling events encountered during program execution. *****/
/**** Memory access handling ****/
fn handle_load(
self: Pin<&mut MiriGenmcShim>,
thread_id: i32,
address: u64,
size: u64,
memory_ordering: MemOrdering,
old_value: GenmcScalar,
) -> LoadResult;
fn handle_read_modify_write(
self: Pin<&mut MiriGenmcShim>,
thread_id: i32,
address: u64,
size: u64,
rmw_op: RMWBinOp,
ordering: MemOrdering,
rhs_value: GenmcScalar,
old_value: GenmcScalar,
) -> ReadModifyWriteResult;
fn handle_compare_exchange(
self: Pin<&mut MiriGenmcShim>,
thread_id: i32,
address: u64,
size: u64,
expected_value: GenmcScalar,
new_value: GenmcScalar,
old_value: GenmcScalar,
success_ordering: MemOrdering,
fail_load_ordering: MemOrdering,
can_fail_spuriously: bool,
) -> CompareExchangeResult;
fn handle_store(
self: Pin<&mut MiriGenmcShim>,
thread_id: i32,
address: u64,
size: u64,
value: GenmcScalar,
old_value: GenmcScalar,
memory_ordering: MemOrdering,
) -> StoreResult;
fn handle_fence(
self: Pin<&mut MiriGenmcShim>,
thread_id: i32,
memory_ordering: MemOrdering,
);
/**** Memory (de)allocation ****/
fn handle_malloc(
self: Pin<&mut MiriGenmcShim>,
thread_id: i32,
size: u64,
alignment: u64,
) -> u64;
/// Returns true if an error was found.
fn handle_free(
self: Pin<&mut MiriGenmcShim>,
thread_id: i32,
address: u64,
) -> UniquePtr<CxxString>;
/**** Thread management ****/
fn handle_thread_create(self: Pin<&mut MiriGenmcShim>, thread_id: i32, parent_id: i32);
fn handle_thread_join(self: Pin<&mut MiriGenmcShim>, thread_id: i32, child_id: i32);
fn handle_thread_finish(self: Pin<&mut MiriGenmcShim>, thread_id: i32, ret_val: u64);
fn handle_thread_kill(self: Pin<&mut MiriGenmcShim>, thread_id: i32);
/**** Blocking instructions ****/
/// Inform GenMC that the thread should be blocked.
/// Note: this function is currently hardcoded for `AssumeType::User`, corresponding to user supplied assume statements.
/// This can become a parameter once more types of assumes are added.
fn handle_assume_block(
self: Pin<&mut MiriGenmcShim>,
thread_id: i32,
assume_type: AssumeType,
);
/**** Mutex handling ****/
fn handle_mutex_lock(
self: Pin<&mut MiriGenmcShim>,
thread_id: i32,
address: u64,
size: u64,
) -> MutexLockResult;
fn handle_mutex_try_lock(
self: Pin<&mut MiriGenmcShim>,
thread_id: i32,
address: u64,
size: u64,
) -> MutexLockResult;
fn handle_mutex_unlock(
self: Pin<&mut MiriGenmcShim>,
thread_id: i32,
address: u64,
size: u64,
) -> StoreResult;
/***** Exploration related functionality *****/
/// Ask the GenMC scheduler for a new thread to schedule and
/// return whether the execution is finished, blocked, or can continue.
/// Updates the next instruction kind for the given thread id.
fn schedule_next(
self: Pin<&mut MiriGenmcShim>,
curr_thread_id: i32,
curr_thread_next_instr_kind: ActionKind,
) -> SchedulingResult;
/// Check whether there are more executions to explore.
/// If there are more executions, this method prepares for the next execution and returns `true`.
fn is_exploration_done(self: Pin<&mut MiriGenmcShim>) -> bool;
/**** Result querying functionality. ****/
// NOTE: We don't want to share the `VerificationResult` type with the Rust side, since it
// is very large, uses features that CXX.rs doesn't support and may change as GenMC changes.
// Instead, we only use the result on the C++ side, and only expose these getter function to
// the Rust side.
// Each `GenMCDriver` contains one `VerificationResult`, and each `MiriGenmcShim` contains on `GenMCDriver`.
// GenMC builds up the content of the `struct VerificationResult` over the course of an exploration,
// but it's safe to look at it at any point, since it is only accessible through exactly one `MiriGenmcShim`.
// All these functions for querying the result can be safely called repeatedly and at any time,
// though the results may be incomplete if called before `handle_execution_end`.
/// Get the number of blocked executions encountered by GenMC (cast into a fixed with integer)
fn get_blocked_execution_count(self: &MiriGenmcShim) -> u64;
/// Get the number of executions explored by GenMC (cast into a fixed with integer)
fn get_explored_execution_count(self: &MiriGenmcShim) -> u64;
/// Get all messages that GenMC produced (errors, warnings), combined into one string.
fn get_result_message(self: &MiriGenmcShim) -> UniquePtr<CxxString>;
/// If an error occurred, return a string describing the error, otherwise, return `nullptr`.
fn get_error_string(self: &MiriGenmcShim) -> UniquePtr<CxxString>;
/**** Printing functionality. ****/
/// Get the results of a run in estimation mode.
fn get_estimation_results(self: &MiriGenmcShim) -> EstimationResult;
}
}