diff --git a/.gitignore b/.gitignore index cce0886..9ae821e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ __pycache__ build venv .vscode +.idea .devcontainer/** .cache/** third_party/** diff --git a/codegen/resmockpass.cpp b/codegen/resmockpass.cpp new file mode 100644 index 0000000..c3e4191 --- /dev/null +++ b/codegen/resmockpass.cpp @@ -0,0 +1,142 @@ +#include +#include + +#include +#include +#include + +#include "llvm/ADT/SmallVector.h" +#include "llvm/IR/Module.h" +#include "llvm/Pass.h" +#include "llvm/Passes/PassBuilder.h" +#include "llvm/Passes/PassPlugin.h" + +using namespace llvm; + +// ignore handling by exceptions memory allocation failure. This is very +// unlikely case and shouldn't be in tested program. +// Supporting this case would require -fexcept +constexpr std::string_view ltest_allocation_fun_name = "LtestMemAlloc"; +constexpr std::string_view ltest_deallocation_fun_name = "LtestMemDealloc"; + +SmallVector collectArgs(CallBase& call) { + SmallVector res_args; + for (auto& arg : call.args()) { + res_args.push_back(arg); + } + return res_args; +} + +class ResMockInserter { + public: + explicit ResMockInserter(Module& m) : m(m) { + auto& context = m.getContext(); + auto ptr_type = PointerType::get(context, 0); + + ltest_allocation_fun = m.getOrInsertFunction( + ltest_allocation_fun_name, + FunctionType::get(ptr_type, {IntegerType::get(context, 64)}, false)); + ltest_deallocation_fun = m.getOrInsertFunction( + ltest_deallocation_fun_name, + FunctionType::get(Type::getVoidTy(context), {ptr_type}, false)); + } + void Run() { + IRBuilder<> builder(m.getContext()); + for (auto& f : m) { + // dirty hack, but otherwise boost contextes allocation is alos mocked, + // which produces warning by asan i couldn't debug double free here + std::string demangled_parent = demangle(f.getName()); + if (demangled_parent.find("boost::context") != std::string::npos) { + continue; + } + for (auto& bb : f) { + for (auto& in : bb) { + if (!isa(&in)) { + continue; + } + CallBase& call = *dyn_cast(&in); + auto called_fun = call.getCalledFunction(); + if (!called_fun) { + continue; + } + std::string demangled = demangle(called_fun->getName()); + auto it = replaces.find(demangled); + if (it == replaces.end()) { + continue; + } + auto [rep_callee, rep_args] = (it->second)(call); + builder.SetInsertPoint(&in); + // todo - need we look at invoke? + auto rep = builder.CreateCall(rep_callee, rep_args); + call.replaceAllUsesWith(rep); + to_delete.push_back(&call); + } + } + } + + for (auto& el : to_delete) { + el->eraseFromParent(); + } + } + + private: + FunctionCallee ltest_allocation_fun, ltest_deallocation_fun; + Module& m; + // to avoid problems with correct iteration through instrcution remove + // instructions only at the end + std::vector to_delete; + std::map< + std::string_view, + std::function>(CallBase&)>> + replaces = {{"malloc", + [this](CallBase& a) -> auto { + assert(a.arg_size() == 1 && "args count is 1"); + return std::pair{ltest_allocation_fun, collectArgs(a)}; + }}, + // {"operator new(unsigned long)", + // [this](CallBase& a) -> auto { + // assert(a.arg_size() == 1 && "args count is 1"); + // return std::pair{ltest_allocation_fun, collectArgs(a)}; + // }}, + {"free", [this](CallBase& a) -> auto { + assert(a.arg_size() == 1 && "args count is 1"); + return std::pair{ltest_deallocation_fun, collectArgs(a)}; + }}}; +}; +namespace { + +struct ResMockPass final : public PassInfoMixin { + PreservedAnalyses run(Module& M, ModuleAnalysisManager& AM) { + ResMockInserter inserter(M); + inserter.Run(); + if (verifyModule(M, &errs())) { + report_fatal_error("module verification failed", false); + } + return PreservedAnalyses::none(); + }; +}; + +} // namespace + +extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo +llvmGetPassPluginInfo() { + return {.APIVersion = LLVM_PLUGIN_API_VERSION, + .PluginName = "resmockpass", + .PluginVersion = "v0.1", + .RegisterPassBuilderCallbacks = [](PassBuilder& pb) { + // This parsing we need for testing with opt + pb.registerPipelineParsingCallback( + [](StringRef Name, ModulePassManager& mpm, + ArrayRef) { + if (Name == "resmock") { + mpm.addPass(ResMockPass()); + return true; + } + return false; + }); + pb.registerPipelineStartEPCallback( + [](ModulePassManager& mpm, OptimizationLevel level) { + mpm.addPass(ResMockPass()); + }); + }}; +} diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index c152d76..2e6ae8a 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -8,6 +8,8 @@ set (SOURCE_FILES minimization.cpp minimization_smart.cpp coro_ctx_guard.cpp + os_simulator.cpp + mock_res.cpp ) add_library(runtime SHARED ${SOURCE_FILES}) diff --git a/runtime/include/lib.h b/runtime/include/lib.h index c3d3b99..3cbf302 100644 --- a/runtime/include/lib.h +++ b/runtime/include/lib.h @@ -11,6 +11,7 @@ #include #include "block_manager.h" +#include "mock_res.h" #include "value_wrapper.h" #define panic() assert(false) @@ -42,7 +43,7 @@ struct CoroBase : public std::enable_shared_from_this { // Restart the coroutine from the beginning passing this_ptr as this. // Returns restarted coroutine. - virtual std::shared_ptr Restart(void* this_ptr) = 0; + virtual void Restart(void* this_ptr) = 0; // Resume the coroutine to the next yield. void Resume(); @@ -122,16 +123,18 @@ struct Coro final : public CoroBase { std::function(std::shared_ptr)>; // unsafe: caller must ensure that this_ptr points to Target. - std::shared_ptr Restart(void* this_ptr) override { - /** - * The task must be returned if we want to restart it. - * We can't just Terminate() it because it is the runtime responsibility - * to decide, in which order the tasks should be terminated. - * - */ - assert(IsReturned()); - auto coro = New(func, this_ptr, args, args_to_strings, name, id); - return coro; + void Restart(void* this_ptr) override { + is_returned = false; + this->this_ptr = this_ptr; + ctx = boost::context::fiber_context( + [this, this_ptr](boost::context::fiber_context&& ctx) { + auto real_args = reinterpret_cast*>(args.get()); + auto this_arg = + std::tuple{reinterpret_cast(this_ptr)}; + ret = std::apply(func, std::tuple_cat(this_arg, *real_args)); + is_returned = true; + return std::move(ctx); + }); } // unsafe: caller must ensure that this_ptr points to Target. diff --git a/runtime/include/mock_res.h b/runtime/include/mock_res.h new file mode 100644 index 0000000..a3f71fa --- /dev/null +++ b/runtime/include/mock_res.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +class MemoryHandler { + std::deque memory; + + public: + void* Allocate(size_t); + void Deallocate(void*); + void FreeAllMemory(); +}; + +extern MemoryHandler* memory_handler; + +extern "C" void* LtestMemAlloc(std::size_t size); + +extern "C" void LtestMemDealloc(void* ptr); \ No newline at end of file diff --git a/runtime/include/os_simulator.h b/runtime/include/os_simulator.h new file mode 100644 index 0000000..880febf --- /dev/null +++ b/runtime/include/os_simulator.h @@ -0,0 +1,15 @@ +#pragma once +#include + +#include "mock_res.h" + +class OSSimulator { + MemoryHandler os_memory; + + public: + OSSimulator() { memory_handler = &os_memory; } + bool CanThreadContinue(std::size_t number); + void UpdateState(); + void ResetState(); + ~OSSimulator() { ResetState(); } +}; \ No newline at end of file diff --git a/runtime/include/scheduler.h b/runtime/include/scheduler.h index 93e1173..43face7 100644 --- a/runtime/include/scheduler.h +++ b/runtime/include/scheduler.h @@ -15,6 +15,7 @@ #include "logger.h" #include "minimization.h" #include "minimization_smart.h" +#include "os_simulator.h" #include "pretty_print.h" #include "scheduler_fwd.h" #include "stable_vector.h" @@ -191,7 +192,7 @@ struct BaseStrategyWithThreads : public Strategy { size_t tasks_in_thread = thread.size(); for (size_t i = 0; i < tasks_in_thread; ++i) { if (!IsTaskRemoved(thread[i]->GetId())) { - thread[i] = thread[i]->Restart(state.get()); + thread[i]->Restart(state.get()); } } } @@ -282,58 +283,11 @@ struct BaseStrategyWithThreads : public Strategy { } protected: - // Terminates all running tasks. - // We do it in a dangerous way: in random order. - // Actually, we assume obstruction free here. void TerminateTasks() { - auto& round_schedule = this->round_schedule; - assert(round_schedule.size() == this->threads.size() && - "sizes expected to be the same"); - round_schedule.assign(round_schedule.size(), -1); - - std::vector task_indexes(this->threads.size(), 0); - bool has_nonterminated_threads = true; - while (has_nonterminated_threads) { - has_nonterminated_threads = false; - - for (size_t thread_index = 0; thread_index < this->threads.size(); - ++thread_index) { - auto& thread = this->threads[thread_index]; - auto& task_index = task_indexes[thread_index]; - - // find first non-finished task in the thread - while (task_index < thread.size() && thread[task_index]->IsReturned()) { - task_index++; - } - - if (task_index == thread.size()) { - std::optional releaseTask = - this->sched_checker.ReleaseTask(thread_index); - // Check if we should schedule release task to unblock other tasks - if (releaseTask) { - auto constructor = - *std::find_if(constructors.begin(), constructors.end(), - [=](const TaskBuilder& b) { - return b.GetName() == *releaseTask; - }); - auto task = - constructor.Build(this->state.get(), thread_index, task_index); - auto verified = this->sched_checker.Verify( - std::string(task->GetName()), thread_index); - thread.emplace_back(task); - } - } - - if (task_index < thread.size() && !thread[task_index]->IsBlocked()) { - auto& task = thread[task_index]; - has_nonterminated_threads = true; - // do a single step in this task - task->Resume(); - if (task->IsReturned()) { - OnVerifierTaskFinish(task, thread_index); - debug(stderr, "Terminated: %ld\n", thread_index); - } - } + simulator.ResetState(); + for (auto& thread : threads) { + if (!thread.empty() && !thread.back()->IsReturned()) { + thread.back()->Terminate(); } } state.reset(new TargetObj{}); @@ -368,6 +322,7 @@ struct BaseStrategyWithThreads : public Strategy { std::uniform_int_distribution constructors_distribution; std::mt19937 rng; + OSSimulator simulator; }; // StrategyScheduler generates different sequential histories (using Strategy) @@ -653,15 +608,14 @@ struct TLAScheduler : Scheduler { TLAScheduler(size_t max_tasks, size_t max_rounds, size_t threads_count, size_t max_switches, size_t max_depth, std::vector constructors, ModelChecker& checker, - PrettyPrinter& pretty_printer, std::function cancel_func) + PrettyPrinter& pretty_printer) : max_tasks{max_tasks}, max_rounds{max_rounds}, max_switches{max_switches}, constructors{std::move(constructors)}, checker{checker}, pretty_printer{pretty_printer}, - max_depth(max_depth), - cancel(cancel_func) { + max_depth(max_depth) { for (size_t i = 0; i < threads_count; ++i) { threads.emplace_back(Thread{ .id = i, @@ -708,7 +662,7 @@ struct TLAScheduler : Scheduler { // Actually, we assume obstruction free here. // cancel() func takes care for graceful shutdown void TerminateTasks() { - cancel(); + simulator.ResetState(); for (size_t i = 0; i < threads.size(); ++i) { for (size_t j = 0; j < threads[i].tasks.size(); ++j) { auto& task = threads[i].tasks[j]; @@ -732,7 +686,7 @@ struct TLAScheduler : Scheduler { if (frame.is_new) { // It was a new task. // So restart it from the beginning with the same args. - *task = (*task)->Restart(state.get()); + (*task)->Restart(state.get()); } else { // It was a not new task, hence, we recreated in early. } @@ -942,5 +896,5 @@ struct TLAScheduler : Scheduler { StableVector threads; StableVector frames; Verifier verifier{}; - std::function cancel; + OSSimulator simulator; }; diff --git a/runtime/include/verifying.h b/runtime/include/verifying.h index 60438a2..f4f7244 100644 --- a/runtime/include/verifying.h +++ b/runtime/include/verifying.h @@ -32,14 +32,13 @@ struct DefaultCanceler { template , class LinearSpecEquals = std::equal_to, - class OptionsOverride = NoOverride, class Canceler = DefaultCanceler> + class OptionsOverride = NoOverride> struct Spec { using target_obj_t = TargetObj; using linear_spec_t = LinearSpec; using linear_spec_hash_t = LinearSpecHash; using linear_spec_equals_t = LinearSpecEquals; using options_override_t = OptionsOverride; - using cancel_t = Canceler; }; struct Opts { @@ -133,8 +132,7 @@ template std::unique_ptr MakeScheduler(ModelChecker &checker, Opts &opts, const std::vector &l, std::vector custom_rounds, - PrettyPrinter &pretty_printer, - const std::function &cancel) { + PrettyPrinter &pretty_printer) { std::cout << "strategy = "; switch (opts.typ) { case RR: @@ -151,7 +149,7 @@ std::unique_ptr MakeScheduler(ModelChecker &checker, Opts &opts, std::cout << "tla\n"; auto scheduler = std::make_unique>( opts.tasks, opts.rounds, opts.threads, opts.switches, opts.depth, - std::move(l), checker, pretty_printer, cancel); + std::move(l), checker, pretty_printer); return scheduler; } default: { @@ -218,7 +216,7 @@ int Run(int argc, char *argv[], std::vector custom_rounds = {}) { auto scheduler = MakeScheduler( checker, opts, std::move(task_builders), std::move(custom_rounds), - pretty_printer, &Spec::cancel_t::Cancel); + pretty_printer); std::cout << "\n\n"; std::cout.flush(); return TrapRun(std::move(scheduler), pretty_printer); diff --git a/runtime/lib.cpp b/runtime/lib.cpp index e91e24d..17997c8 100644 --- a/runtime/lib.cpp +++ b/runtime/lib.cpp @@ -47,12 +47,7 @@ ValueWrapper CoroBase::GetRetVal() const { return ret; } -CoroBase::~CoroBase() { - // The coroutine must be returned if we want to restart it. - // We can't just Terminate() it because it is the runtime responsibility to - // decide, in which order the tasks should be terminated. - assert(IsReturned()); -} +CoroBase::~CoroBase() {} std::string_view CoroBase::GetName() const { return name; } @@ -75,15 +70,7 @@ extern "C" void CoroutineStatusChange(char* name, bool start) { CoroYield(); } -void CoroBase::Terminate() { - int tries = 0; - while (!IsReturned()) { - ++tries; - Resume(); - assert(tries < 1000000 && - "coroutine is spinning too long, possible wrong terminating order"); - } -} +void CoroBase::Terminate() { is_returned = true; } void CoroBase::TryTerminate() { for (size_t i = 0; i < 1000 && !is_returned; ++i) { diff --git a/runtime/mock_res.cpp b/runtime/mock_res.cpp new file mode 100644 index 0000000..9319a54 --- /dev/null +++ b/runtime/mock_res.cpp @@ -0,0 +1,46 @@ +#include "mock_res.h" + +#include + +#include "coro_ctx_guard.h" + +MemoryHandler* memory_handler; + +void* MemoryHandler::Allocate(size_t size) { + void* ptr = operator new(size); + memory.push_back(ptr); + return ptr; +} + +void MemoryHandler::Deallocate(void* ptr) { + for (auto m : memory) { + } + auto it = std::find(memory.begin(), memory.end(), ptr); + if (it == memory.end()) { + return; + } + operator delete(*it); + memory.erase(it); +} + +void MemoryHandler::FreeAllMemory() { + for (auto mem : memory) { + operator delete(mem); + } + memory.clear(); +} + +extern "C" void* LtestMemAlloc(std::size_t size) { + if (!ltest_coro_ctx) { + return operator new(size); + } + return memory_handler->Allocate(size); +} + +extern "C" void LtestMemDealloc(void* ptr) { + if (!ltest_coro_ctx) { + operator delete(ptr); + return; + } + memory_handler->Deallocate(ptr); +} diff --git a/runtime/os_simulator.cpp b/runtime/os_simulator.cpp new file mode 100644 index 0000000..e1a4163 --- /dev/null +++ b/runtime/os_simulator.cpp @@ -0,0 +1,6 @@ +#include "os_simulator.h" + +void OSSimulator::ResetState() { memory_handler->FreeAllMemory(); } +bool OSSimulator::CanThreadContinue(std::size_t number) { return true; } + +void OSSimulator::UpdateState() {} diff --git a/verifying/CMakeLists.txt b/verifying/CMakeLists.txt index ca6d48f..425d4ad 100644 --- a/verifying/CMakeLists.txt +++ b/verifying/CMakeLists.txt @@ -4,6 +4,9 @@ include_directories(specs) set (PASS YieldPass) set (PASS_PATH ${CMAKE_BINARY_DIR}/codegen/lib${PASS}.so) +set (RESMOCK_PASS ResMockPass) +set (RESMOCK_PASS_PATH ${CMAKE_BINARY_DIR}/codegen/lib${RESMOCK_PASS}.so) + set (COPASS CoYieldPass) set (COPASS_PATH ${CMAKE_BINARY_DIR}/codegen/lib${COPASS}.so) @@ -46,23 +49,23 @@ function(verify_target_without_plugin target) else() add_executable(${target} ${source_name}) endif() - add_dependencies(${target} runtime plugin_pass) + add_dependencies(${target} runtime plugin_pass resmock_pass) target_include_directories(${target} PRIVATE ${CMAKE_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/third_party) target_link_options(${target} PRIVATE ${CMAKE_ASAN_FLAGS}) - target_compile_options(${target} PRIVATE ${CMAKE_ASAN_FLAGS}) + target_compile_options(${target} PRIVATE -fpass-plugin=${RESMOCK_PASS_PATH} ${CMAKE_ASAN_FLAGS}) target_link_libraries(${target} PRIVATE runtime ${PASS} gflags ${Boost_LIBRARIES}) endfunction() function(verify_target target) verify_target_without_plugin(${target}) - add_dependencies(${target} runtime plugin_pass) - target_compile_options(${target} PRIVATE -fpass-plugin=${PASS_PATH} ${CMAKE_ASAN_FLAGS}) + add_dependencies(${target} runtime plugin_pass resmock_pass) + target_compile_options(${target} PRIVATE -fpass-plugin=${PASS_PATH} -fpass-plugin=${RESMOCK_PASS_PATH} ${CMAKE_ASAN_FLAGS}) endfunction() function(verify_cotarget target) verify_target_without_plugin(${target}) - add_dependencies(${target} runtime coplugin_pass) - target_compile_options(${target} PRIVATE -fplugin=${COPASS_PATH} + add_dependencies(${target} runtime coplugin_pass resmock_pass) + target_compile_options(${target} PRIVATE -fpass-plugin=${RESMOCK_PASS_PATH} -fplugin=${COPASS_PATH} -fpass-plugin=${COPASS_PATH} -mllvm -coroutine-file=${CMAKE_CURRENT_SOURCE_DIR}/${target}.yml ${CMAKE_ASAN_FLAGS}) endfunction() diff --git a/verifying/targets/unique_args.cpp b/verifying/targets/unique_args.cpp index 8d71a92..5e9b45c 100644 --- a/verifying/targets/unique_args.cpp +++ b/verifying/targets/unique_args.cpp @@ -18,7 +18,9 @@ struct CoUniqueArgsTest { Reset(); return limit; }; + const auto mem = std::malloc(100); done[i] = true; + std::free(mem); return {std::count(done.begin(), done.end(), false) == 0 ? l() : std::optional(),