386 lines
15 KiB
C++
386 lines
15 KiB
C++
#ifndef GENMC_MIRI_INTERFACE_HPP
|
|
#define GENMC_MIRI_INTERFACE_HPP
|
|
|
|
// CXX.rs generated headers:
|
|
#include "rust/cxx.h"
|
|
|
|
// GenMC generated headers:
|
|
#include "config.h"
|
|
|
|
// Miri `genmc-sys/src_cpp` headers:
|
|
#include "ResultHandling.hpp"
|
|
|
|
// GenMC headers:
|
|
#include "ExecutionGraph/EventLabel.hpp"
|
|
#include "Support/MemOrdering.hpp"
|
|
#include "Support/RMWOps.hpp"
|
|
#include "Verification/Config.hpp"
|
|
#include "Verification/GenMCDriver.hpp"
|
|
|
|
// C++ headers:
|
|
#include <cstdint>
|
|
#include <format>
|
|
#include <iomanip>
|
|
#include <memory>
|
|
|
|
/**** Types available to both Rust and C++ ****/
|
|
|
|
struct GenmcParams;
|
|
enum class LogLevel : std::uint8_t;
|
|
|
|
struct GenmcScalar;
|
|
struct SchedulingResult;
|
|
struct EstimationResult;
|
|
struct LoadResult;
|
|
struct StoreResult;
|
|
struct ReadModifyWriteResult;
|
|
struct CompareExchangeResult;
|
|
struct MutexLockResult;
|
|
|
|
// GenMC uses `int` for its thread IDs.
|
|
using ThreadId = int;
|
|
|
|
/// 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 */ void set_log_level_raw(LogLevel log_level);
|
|
|
|
struct MiriGenmcShim : private GenMCDriver {
|
|
|
|
public:
|
|
MiriGenmcShim(std::shared_ptr<const Config> conf, Mode mode /* = VerificationMode{} */)
|
|
: GenMCDriver(std::move(conf), nullptr, mode) {}
|
|
|
|
/// 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`.
|
|
/* unsafe */ static auto create_handle(const GenmcParams& params, bool estimation_mode)
|
|
-> std::unique_ptr<MiriGenmcShim>;
|
|
|
|
virtual ~MiriGenmcShim() {}
|
|
|
|
/**** Execution start/end handling ****/
|
|
|
|
// This function must be called at the start of any execution, before any events are
|
|
// reported to GenMC.
|
|
void handle_execution_start();
|
|
// 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.
|
|
std::unique_ptr<std::string> handle_execution_end();
|
|
|
|
/***** Functions for handling events encountered during program execution. *****/
|
|
|
|
/**** Memory access handling ****/
|
|
|
|
[[nodiscard]] LoadResult handle_load(
|
|
ThreadId thread_id,
|
|
uint64_t address,
|
|
uint64_t size,
|
|
MemOrdering ord,
|
|
GenmcScalar old_val
|
|
);
|
|
[[nodiscard]] ReadModifyWriteResult handle_read_modify_write(
|
|
ThreadId thread_id,
|
|
uint64_t address,
|
|
uint64_t size,
|
|
RMWBinOp rmw_op,
|
|
MemOrdering ordering,
|
|
GenmcScalar rhs_value,
|
|
GenmcScalar old_val
|
|
);
|
|
[[nodiscard]] CompareExchangeResult handle_compare_exchange(
|
|
ThreadId thread_id,
|
|
uint64_t address,
|
|
uint64_t size,
|
|
GenmcScalar expected_value,
|
|
GenmcScalar new_value,
|
|
GenmcScalar old_val,
|
|
MemOrdering success_ordering,
|
|
MemOrdering fail_load_ordering,
|
|
bool can_fail_spuriously
|
|
);
|
|
[[nodiscard]] StoreResult handle_store(
|
|
ThreadId thread_id,
|
|
uint64_t address,
|
|
uint64_t size,
|
|
GenmcScalar value,
|
|
GenmcScalar old_val,
|
|
MemOrdering ord
|
|
);
|
|
|
|
void handle_fence(ThreadId thread_id, MemOrdering ord);
|
|
|
|
/**** Memory (de)allocation ****/
|
|
|
|
auto handle_malloc(ThreadId thread_id, uint64_t size, uint64_t alignment) -> uint64_t;
|
|
|
|
/** Returns null on success, or an error string if an error occurs. */
|
|
auto handle_free(ThreadId thread_id, uint64_t address) -> std::unique_ptr<std::string>;
|
|
|
|
/**** Thread management ****/
|
|
void handle_thread_create(ThreadId thread_id, ThreadId parent_id);
|
|
void handle_thread_join(ThreadId thread_id, ThreadId child_id);
|
|
void handle_thread_finish(ThreadId thread_id, uint64_t ret_val);
|
|
void handle_thread_kill(ThreadId thread_id);
|
|
|
|
/**** Blocking instructions ****/
|
|
/// Inform GenMC that the thread should be blocked.
|
|
void handle_assume_block(ThreadId thread_id, AssumeType assume_type);
|
|
|
|
/**** Mutex handling ****/
|
|
auto handle_mutex_lock(ThreadId thread_id, uint64_t address, uint64_t size) -> MutexLockResult;
|
|
auto handle_mutex_try_lock(ThreadId thread_id, uint64_t address, uint64_t size)
|
|
-> MutexLockResult;
|
|
auto handle_mutex_unlock(ThreadId thread_id, uint64_t address, uint64_t size) -> 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. */
|
|
auto schedule_next(const int curr_thread_id, const ActionKind curr_thread_next_instr_kind)
|
|
-> SchedulingResult;
|
|
|
|
/**
|
|
* Check whether there are more executions to explore.
|
|
* If there are more executions, this method prepares for the next execution and returns
|
|
* `true`. Returns true if there are no more executions to explore. */
|
|
auto is_exploration_done() -> bool {
|
|
return GenMCDriver::done();
|
|
}
|
|
|
|
/**** 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.
|
|
|
|
// Note that CXX.rs doesn't support returning a C++ string to Rust by value,
|
|
// it must be behind an indirection like a `unique_ptr` (tested with CXX 1.0.170).
|
|
|
|
/// Get the number of blocked executions encountered by GenMC (cast into a fixed with
|
|
/// integer)
|
|
auto get_blocked_execution_count() const -> uint64_t {
|
|
return static_cast<uint64_t>(getResult().exploredBlocked);
|
|
}
|
|
|
|
/// Get the number of executions explored by GenMC (cast into a fixed with integer)
|
|
auto get_explored_execution_count() const -> uint64_t {
|
|
return static_cast<uint64_t>(getResult().explored);
|
|
}
|
|
|
|
/// Get all messages that GenMC produced (errors, warnings), combined into one string.
|
|
auto get_result_message() const -> std::unique_ptr<std::string> {
|
|
return std::make_unique<std::string>(getResult().message);
|
|
}
|
|
|
|
/// If an error occurred, return a string describing the error, otherwise, return `nullptr`.
|
|
auto get_error_string() const -> std::unique_ptr<std::string> {
|
|
const auto& result = GenMCDriver::getResult();
|
|
if (result.status.has_value())
|
|
return format_error(result.status.value());
|
|
return nullptr;
|
|
}
|
|
|
|
/**** Printing and estimation mode functionality. ****/
|
|
|
|
/// Get the results of a run in estimation mode.
|
|
auto get_estimation_results() const -> EstimationResult;
|
|
|
|
private:
|
|
/** Increment the event index in the given thread by 1 and return the new event. */
|
|
[[nodiscard]] inline auto inc_pos(ThreadId tid) -> Event {
|
|
ERROR_ON(tid >= threads_action_.size(), "ThreadId out of bounds");
|
|
return ++threads_action_[tid].event;
|
|
}
|
|
/** Decrement the event index in the given thread by 1 and return the new event. */
|
|
inline auto dec_pos(ThreadId tid) -> Event {
|
|
ERROR_ON(tid >= threads_action_.size(), "ThreadId out of bounds");
|
|
return --threads_action_[tid].event;
|
|
}
|
|
|
|
/**
|
|
* Helper function for loads that need to reset the event counter when no value is returned.
|
|
* Same syntax as `GenMCDriver::handleLoad`, but this takes a thread id instead of an Event.
|
|
* Automatically calls `inc_pos` and `dec_pos` where needed for the given thread.
|
|
*/
|
|
template <EventLabel::EventLabelKind k, typename... Ts>
|
|
auto handle_load_reset_if_none(ThreadId tid, std::optional<SVal> old_val, Ts&&... params)
|
|
-> HandleResult<SVal> {
|
|
const auto pos = inc_pos(tid);
|
|
const auto ret =
|
|
GenMCDriver::handleLoad<k>(nullptr, pos, old_val, std::forward<Ts>(params)...);
|
|
// If we didn't get a value, we have to reset the index of the current thread.
|
|
if (!std::holds_alternative<SVal>(ret)) {
|
|
dec_pos(tid);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* GenMC uses the term `Action` to refer to a struct of:
|
|
* - `ActionKind`, storing whether the next instruction in a thread may be a load
|
|
* - `Event`, storing the most recent event index added for a thread
|
|
*
|
|
* Here we store the "action" for each thread. In particular we use this to assign event
|
|
* indices, since GenMC expects us to do that.
|
|
*/
|
|
std::vector<Action> threads_action_;
|
|
};
|
|
|
|
/// Get the bit mask that GenMC expects for global memory allocations.
|
|
/// FIXME(genmc): currently we use `get_global_alloc_static_mask()` to ensure the
|
|
/// `SAddr::staticMask` constant is consistent between Miri and GenMC, but if
|
|
/// https://github.com/dtolnay/cxx/issues/1051 is fixed we could share the constant
|
|
/// directly.
|
|
constexpr auto get_global_alloc_static_mask() -> uint64_t {
|
|
return SAddr::staticMask;
|
|
}
|
|
|
|
// CXX.rs generated headers:
|
|
// NOTE: this must be included *after* `MiriGenmcShim` and all the other types we use are defined,
|
|
// otherwise there will be compilation errors due to missing definitions.
|
|
#include "genmc-sys/src/lib.rs.h"
|
|
|
|
/**** Result handling ****/
|
|
// NOTE: these must come after the cxx_bridge generated code, since that contains the actual
|
|
// definitions of these types.
|
|
|
|
namespace GenmcScalarExt {
|
|
inline GenmcScalar uninit() {
|
|
return GenmcScalar {
|
|
.value = 0,
|
|
.provenance = 0,
|
|
.is_init = false,
|
|
};
|
|
}
|
|
|
|
inline GenmcScalar from_sval(SVal sval) {
|
|
return GenmcScalar {
|
|
.value = sval.get(),
|
|
.provenance = sval.getProvenance(),
|
|
.is_init = true,
|
|
};
|
|
}
|
|
|
|
inline SVal to_sval(GenmcScalar scalar) {
|
|
ERROR_ON(!scalar.is_init, "Cannot convert an uninitialized `GenmcScalar` into an `SVal`\n");
|
|
return SVal(scalar.value, scalar.provenance);
|
|
}
|
|
|
|
inline std::optional<SVal> try_to_sval(GenmcScalar scalar) {
|
|
if (scalar.is_init)
|
|
return { SVal(scalar.value, scalar.provenance) };
|
|
return std::nullopt;
|
|
}
|
|
} // namespace GenmcScalarExt
|
|
|
|
namespace LoadResultExt {
|
|
inline LoadResult no_value() {
|
|
return LoadResult {
|
|
.error = std::unique_ptr<std::string>(nullptr),
|
|
.has_value = false,
|
|
.read_value = GenmcScalarExt::uninit(),
|
|
};
|
|
}
|
|
|
|
inline LoadResult from_value(SVal read_value) {
|
|
return LoadResult { .error = std::unique_ptr<std::string>(nullptr),
|
|
.has_value = true,
|
|
.read_value = GenmcScalarExt::from_sval(read_value) };
|
|
}
|
|
|
|
inline LoadResult from_error(std::unique_ptr<std::string> error) {
|
|
return LoadResult { .error = std::move(error),
|
|
.has_value = false,
|
|
.read_value = GenmcScalarExt::uninit() };
|
|
}
|
|
} // namespace LoadResultExt
|
|
|
|
namespace StoreResultExt {
|
|
inline StoreResult ok(bool is_coherence_order_maximal_write) {
|
|
return StoreResult { /* error: */ std::unique_ptr<std::string>(nullptr),
|
|
is_coherence_order_maximal_write };
|
|
}
|
|
|
|
inline StoreResult from_error(std::unique_ptr<std::string> error) {
|
|
return StoreResult { .error = std::move(error), .is_coherence_order_maximal_write = false };
|
|
}
|
|
} // namespace StoreResultExt
|
|
|
|
namespace ReadModifyWriteResultExt {
|
|
inline ReadModifyWriteResult
|
|
ok(SVal old_value, SVal new_value, bool is_coherence_order_maximal_write) {
|
|
return ReadModifyWriteResult { .error = std::unique_ptr<std::string>(nullptr),
|
|
.old_value = GenmcScalarExt::from_sval(old_value),
|
|
.new_value = GenmcScalarExt::from_sval(new_value),
|
|
.is_coherence_order_maximal_write =
|
|
is_coherence_order_maximal_write };
|
|
}
|
|
|
|
inline ReadModifyWriteResult from_error(std::unique_ptr<std::string> error) {
|
|
return ReadModifyWriteResult { .error = std::move(error),
|
|
.old_value = GenmcScalarExt::uninit(),
|
|
.new_value = GenmcScalarExt::uninit(),
|
|
.is_coherence_order_maximal_write = false };
|
|
}
|
|
} // namespace ReadModifyWriteResultExt
|
|
|
|
namespace CompareExchangeResultExt {
|
|
inline CompareExchangeResult success(SVal old_value, bool is_coherence_order_maximal_write) {
|
|
return CompareExchangeResult { .error = nullptr,
|
|
.old_value = GenmcScalarExt::from_sval(old_value),
|
|
.is_success = true,
|
|
.is_coherence_order_maximal_write =
|
|
is_coherence_order_maximal_write };
|
|
}
|
|
|
|
inline CompareExchangeResult failure(SVal old_value) {
|
|
return CompareExchangeResult { .error = nullptr,
|
|
.old_value = GenmcScalarExt::from_sval(old_value),
|
|
.is_success = false,
|
|
.is_coherence_order_maximal_write = false };
|
|
}
|
|
|
|
inline CompareExchangeResult from_error(std::unique_ptr<std::string> error) {
|
|
return CompareExchangeResult { .error = std::move(error),
|
|
.old_value = GenmcScalarExt::uninit(),
|
|
.is_success = false,
|
|
.is_coherence_order_maximal_write = false };
|
|
}
|
|
} // namespace CompareExchangeResultExt
|
|
|
|
namespace MutexLockResultExt {
|
|
inline MutexLockResult ok(bool is_lock_acquired) {
|
|
return MutexLockResult { /* error: */ nullptr, /* is_reset: */ false, is_lock_acquired };
|
|
}
|
|
|
|
inline MutexLockResult reset() {
|
|
return MutexLockResult { /* error: */ nullptr,
|
|
/* is_reset: */ true,
|
|
/* is_lock_acquired: */ false };
|
|
}
|
|
|
|
inline MutexLockResult from_error(std::unique_ptr<std::string> error) {
|
|
return MutexLockResult { /* error: */ std::move(error),
|
|
/* is_reset: */ false,
|
|
/* is_lock_acquired: */ false };
|
|
}
|
|
} // namespace MutexLockResultExt
|
|
|
|
#endif /* GENMC_MIRI_INTERFACE_HPP */
|