blob: 20c827221a92a6806668cb022670c05505796185 [file] [log] [blame] [edit]
/** This file contains functionality related to creation of the GenMCDriver,
* including translating settings set by Miri. */
#include "MiriInterface.hpp"
// CXX.rs generated headers:
#include "genmc-sys/src/lib.rs.h"
// GenMC headers:
#include "Support/Error.hpp"
#include "Support/Verbosity.hpp"
#include "Verification/InterpreterCallbacks.hpp"
// C++ headers:
#include <cstdint>
/**
* Translate the Miri-GenMC `LogLevel` to the GenMC `VerbosityLevel`.
* Downgrade any debug options to `Tip` if `ENABLE_GENMC_DEBUG` is not enabled.
*/
static auto to_genmc_verbosity_level(const LogLevel log_level) -> VerbosityLevel {
switch (log_level) {
case LogLevel::Quiet:
return VerbosityLevel::Quiet;
case LogLevel::Error:
return VerbosityLevel::Error;
case LogLevel::Warning:
return VerbosityLevel::Warning;
case LogLevel::Tip:
return VerbosityLevel::Tip;
#ifdef ENABLE_GENMC_DEBUG
case LogLevel::Debug1Revisits:
return VerbosityLevel::Debug1;
case LogLevel::Debug2MemoryAccesses:
return VerbosityLevel::Debug2;
case LogLevel::Debug3ReadsFrom:
return VerbosityLevel::Debug3;
#else
// Downgrade to `Tip` if the debug levels are not available.
case LogLevel::Debug1Revisits:
case LogLevel::Debug2MemoryAccesses:
case LogLevel::Debug3ReadsFrom:
return VerbosityLevel::Tip;
#endif
default:
WARN_ONCE(
"unknown-log-level",
"Unknown `LogLevel`, defaulting to `VerbosityLevel::Tip`."
);
return VerbosityLevel::Tip;
}
}
/* unsafe */ void set_log_level_raw(LogLevel log_level) {
// The `logLevel` is a static, non-atomic variable.
// It should never be changed if `MiriGenmcShim` still exists, since any of its methods may read
// the `logLevel`, otherwise it may cause data races.
logLevel = to_genmc_verbosity_level(log_level);
}
/* unsafe */ auto MiriGenmcShim::create_handle(const GenmcParams& params, bool estimation_mode)
-> std::unique_ptr<MiriGenmcShim> {
auto conf = std::make_shared<Config>();
// Set whether GenMC should print execution graphs after every explored/blocked execution.
conf->printExecGraphs =
(params.print_execution_graphs == ExecutiongraphPrinting::Explored ||
params.print_execution_graphs == ExecutiongraphPrinting::ExploredAndBlocked);
conf->printBlockedExecs =
(params.print_execution_graphs == ExecutiongraphPrinting::Blocked ||
params.print_execution_graphs == ExecutiongraphPrinting::ExploredAndBlocked);
// `1024` is the default value that GenMC uses.
// If any thread has at least this many events, a warning/tip will be printed.
//
// Miri produces a lot more events than GenMC, so the graph size warning triggers on almost
// all programs. The current value is large enough so the warning is not be triggered by any
// reasonable programs.
// FIXME(genmc): The emitted warning mentions features not supported by Miri ('--unroll'
// parameter).
// FIXME(genmc): A more appropriate limit should be chosen once the warning is useful for
// Miri.
conf->warnOnGraphSize = 1024 * 1024;
// We only support the `RC11` memory model for Rust, and `SC` when weak memory emulation is
// disabled.
conf->model = params.disable_weak_memory_emulation ? ModelType::SC : ModelType::RC11;
// This prints the seed that GenMC picks for randomized scheduling during estimation mode.
conf->printRandomScheduleSeed = params.print_random_schedule_seed;
// FIXME(genmc): supporting IPR requires annotations for `assume` and `spinloops`.
conf->ipr = false;
// FIXME(genmc): supporting BAM requires `Barrier` support + detecting whether return value
// of barriers are used.
conf->disableBAM = true;
// Instruction caching could help speed up verification by filling the graph from cache, if
// the list of values read by all load events in a thread have been seen before. Combined
// with not replaying completed threads, this can also reducing the amount of Mir
// interpretation required by Miri. With the current setup, this would be incorrect, since
// Miri doesn't give GenMC the actual values read by non-atomic reads.
conf->instructionCaching = false;
// Many of Miri's checks work under the assumption that threads are only executed in an
// order that could actually happen during a normal execution. Formally, this means that
// replaying an execution needs to respect the po-rf-relation of the executiongraph (po ==
// program-order, rf == reads-from). This means, any event in the graph, when replayed, must
// happen after any events that happen before it in the same graph according to the program
// code, and all (non-atomic) reads must happen after the write event they read from.
//
// Not replaying completed threads means any read event from that thread never happens in
// Miri's memory, so this would only work if there are never any non-atomic reads from any
// value written by the skipped thread.
conf->replayCompletedThreads = true;
// FIXME(genmc): implement symmetry reduction.
ERROR_ON(
params.do_symmetry_reduction,
"Symmetry reduction is currently unsupported in GenMC mode."
);
conf->symmetryReduction = params.do_symmetry_reduction;
// Set the scheduling policy. GenMC uses `WFR` for estimation mode.
// For normal verification, `WF` has the best performance and is the GenMC default.
// Other scheduling policies are used by GenMC for testing and for modes currently
// unsupported with Miri such as bounding, which uses LTR.
conf->schedulePolicy = estimation_mode ? SchedulePolicy::WFR : SchedulePolicy::WF;
// Set the min and max number of executions tested in estimation mode.
conf->estimationMin = 10; // default taken from GenMC
conf->estimationMax = params.estimation_max;
// Deviation threshold % under which estimation is deemed good enough.
conf->sdThreshold = 10; // default taken from GenMC
// Set the mode used for this driver, either estimation or verification.
const auto mode = estimation_mode ? GenMCDriver::Mode(GenMCDriver::EstimationMode {})
: GenMCDriver::Mode(GenMCDriver::VerificationMode {});
// Running Miri-GenMC without race detection is not supported.
// Disabling this option also changes the behavior of the replay scheduler to only schedule
// at atomic operations, which is required with Miri. This happens because Miri can generate
// multiple GenMC events for a single MIR terminator. Without this option, the scheduler
// might incorrectly schedule an atomic MIR terminator because the first event it creates is
// a non-atomic (e.g., `StorageLive`).
conf->disableRaceDetection = false;
// Miri can already check for unfreed memory. Also, GenMC cannot distinguish between memory
// that is allowed to leak and memory that is not.
conf->warnUnfreedMemory = false;
// Validate the config and exit if there are any errors
std::vector<std::string> warnings;
auto config_valid = conf->validate(warnings);
for (const auto& w : warnings)
WARN("{}", w);
if (auto* errors = std::get_if<ConfigErrorList>(&config_valid); errors) {
for (const auto& e : *errors)
LOG(VerbosityLevel::Error, "{}", e);
exit(EUSER);
}
// Create the actual driver and Miri-GenMC communication shim.
auto driver = std::make_unique<MiriGenmcShim>(std::move(conf), mode);
// FIXME(genmc,HACK): Until a proper solution is implemented in GenMC, these callbacks will
// allow Miri to return information about global allocations and override uninitialized memory
// checks for non-atomic loads (Miri handles those without GenMC, so the error would be wrong).
auto interpreter_callbacks = InterpreterCallbacks {
// Miri already ensures that memory accesses are valid, so this check doesn't matter.
// We check that the address is static, but skip checking if it is part of an actual
// allocation.
.isStaticallyAllocated = [](SAddr addr) { return addr.isStatic(); },
// FIXME(genmc,error reporting): Once a proper a proper API for passing such information is
// implemented in GenMC, Miri should use it to improve the produced error messages.
.getStaticName = [](SAddr addr) { return "[UNKNOWN STATIC]"; },
// This function is called to get the initial value stored at the given address.
//
// From a Miri perspective, this API doesn't work very well: most memory starts out
// "uninitialized";
// only statics have an initial value. And their initial value is just a sequence of bytes,
// but GenMC expect this to be already split into separate atomic variables. So we return a
// dummy value.
// This value should never be visible to the interpreted program.
// GenMC does not understand uninitialized memory the same way Miri does, which may cause
// this function to be called. The returned value can be visible to Miri or the user:
// - Printing the execution graph may contain this value in place of uninitialized values.
// FIXME(genmc): NOTE: printing the execution graph is not yet implemented.
// - Non-atomic loads may return this value, but Miri ignores values of non-atomic loads.
// - Atomic loads will *not* see this value once mixed atomic-non-atomic support is added.
// Currently, atomic loads can see this value, unless initialized by an *atomic* store.
// FIXME(genmc): update this comment once mixed atomic-non-atomic support is added.
//
// FIXME(genmc): implement proper support for uninitialized memory in GenMC.
// Ideally, the initial value getter would return an `optional<SVal>`, since the memory
// location may be uninitialized.
.initValGetter = [](const AAccess& a) { return SVal(0xDEAD); },
// Miri serves non-atomic loads from its own memory and these GenMC checks are wrong in that
// case. This should no longer be required with proper mixed-size access support.
.skipUninitLoadChecks = [](const MemAccessLabel* access_label
) { return access_label->getOrdering() == MemOrdering::NotAtomic; },
};
driver->setInterpCallbacks(std::move(interpreter_callbacks));
return driver;
}