diff --git a/docs/design/src/plugins/my_transition.cpp b/.clang-complete similarity index 100% rename from docs/design/src/plugins/my_transition.cpp rename to .clang-complete diff --git a/.clang-tidy b/.clang-tidy index 39037ccb..30485f6b 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,20 +1,32 @@ -Checks: '-*,clang-diagnostic-*,llvm-*,misc-*,-misc-unused-parameters-misc-non-private-member-variables-in-classes,readability-identifier-naming' +--- +Checks: 'clang-diagnostic-*,clang-analyzer-*' +WarningsAsErrors: '' +HeaderFileExtensions: + - '' + - h + - hh + - hpp + - hxx +ImplementationFileExtensions: + - c + - cc + - cpp + - cxx +HeaderFilterRegex: '' +AnalyzeTemporaryDtors: false +FormatStyle: none CheckOptions: - - key: readability-identifier-naming.ClassCase - value: CamelCase - - key: readability-identifier-naming.EnumCase - value: CamelCase - - key: readability-identifier-naming.FunctionCase - value: camelBack - - key: readability-identifier-naming.MemberCase - value: camelBack - - key: readability-identifier-naming.ParameterCase - value: camelBack - - key: readability-identifier-naming.UnionCase - value: CamelCase - - key: readability-identifier-naming.VariableCase - value: camelBack - - key: readability-identifier-naming.NamespaceCase - value: lower_case - - key: readability-identifier-naming.InlineNamespaceCase - value: lower_case + cert-dcl16-c.NewSuffixes: 'L;LL;LU;LLU' + google-readability-namespace-comments.ShortNamespaceLines: '10' + cert-err33-c.CheckedFunctions: '::aligned_alloc;::asctime_s;::at_quick_exit;::atexit;::bsearch;::bsearch_s;::btowc;::c16rtomb;::c32rtomb;::calloc;::clock;::cnd_broadcast;::cnd_init;::cnd_signal;::cnd_timedwait;::cnd_wait;::ctime_s;::fclose;::fflush;::fgetc;::fgetpos;::fgets;::fgetwc;::fopen;::fopen_s;::fprintf;::fprintf_s;::fputc;::fputs;::fputwc;::fputws;::fread;::freopen;::freopen_s;::fscanf;::fscanf_s;::fseek;::fsetpos;::ftell;::fwprintf;::fwprintf_s;::fwrite;::fwscanf;::fwscanf_s;::getc;::getchar;::getenv;::getenv_s;::gets_s;::getwc;::getwchar;::gmtime;::gmtime_s;::localtime;::localtime_s;::malloc;::mbrtoc16;::mbrtoc32;::mbsrtowcs;::mbsrtowcs_s;::mbstowcs;::mbstowcs_s;::memchr;::mktime;::mtx_init;::mtx_lock;::mtx_timedlock;::mtx_trylock;::mtx_unlock;::printf_s;::putc;::putwc;::raise;::realloc;::remove;::rename;::scanf;::scanf_s;::setlocale;::setvbuf;::signal;::snprintf;::snprintf_s;::sprintf;::sprintf_s;::sscanf;::sscanf_s;::strchr;::strerror_s;::strftime;::strpbrk;::strrchr;::strstr;::strtod;::strtof;::strtoimax;::strtok;::strtok_s;::strtol;::strtold;::strtoll;::strtoul;::strtoull;::strtoumax;::strxfrm;::swprintf;::swprintf_s;::swscanf;::swscanf_s;::thrd_create;::thrd_detach;::thrd_join;::thrd_sleep;::time;::timespec_get;::tmpfile;::tmpfile_s;::tmpnam;::tmpnam_s;::tss_create;::tss_get;::tss_set;::ungetc;::ungetwc;::vfprintf;::vfprintf_s;::vfscanf;::vfscanf_s;::vfwprintf;::vfwprintf_s;::vfwscanf;::vfwscanf_s;::vprintf_s;::vscanf;::vscanf_s;::vsnprintf;::vsnprintf_s;::vsprintf;::vsprintf_s;::vsscanf;::vsscanf_s;::vswprintf;::vswprintf_s;::vswscanf;::vswscanf_s;::vwprintf_s;::vwscanf;::vwscanf_s;::wcrtomb;::wcschr;::wcsftime;::wcspbrk;::wcsrchr;::wcsrtombs;::wcsrtombs_s;::wcsstr;::wcstod;::wcstof;::wcstoimax;::wcstok;::wcstok_s;::wcstol;::wcstold;::wcstoll;::wcstombs;::wcstombs_s;::wcstoul;::wcstoull;::wcstoumax;::wcsxfrm;::wctob;::wctrans;::wctype;::wmemchr;::wprintf_s;::wscanf;::wscanf_s;' + llvm-else-after-return.WarnOnUnfixable: 'false' + cert-str34-c.DiagnoseSignedUnsignedCharComparisons: 'false' + google-readability-namespace-comments.SpacesBeforeComments: '2' + cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic: 'true' + google-readability-braces-around-statements.ShortStatementLines: '1' + google-readability-function-size.StatementThreshold: '800' + llvm-qualified-auto.AddConstToQualified: 'false' + llvm-else-after-return.WarnOnConditionVariables: 'false' + cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField: 'false' +SystemHeaders: false +... diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f3136ef2..c18781e1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,8 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.6.0 hooks: - id: trailing-whitespace - - id: check-added-large-files \ No newline at end of file + - id: check-added-large-files + - id: check-case-conflict + - id: end-of-file-fixer diff --git a/dmtcp b/dmtcp deleted file mode 160000 index 2763a276..00000000 --- a/dmtcp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2763a2762bf43f4309cb50728629302a936c3456 diff --git a/dmtcp/BUG.README b/dmtcp/BUG.README new file mode 100644 index 00000000..02cb2c14 --- /dev/null +++ b/dmtcp/BUG.README @@ -0,0 +1,7 @@ +Sept. 21, 2024 + +export LD_LIBRARY_PATH=/home/gene/dmtcp.git/lib/dmtcp + +~/dmtcp.git/bin/dmtcp_launch -i3 --disable-alloc-plugin --with-plugin ~/mcmini-pirtle/dmtcp/build/libmcmini.so ./src/examples/hello-world & + +dmtcp_commnd -c diff --git a/dmtcp/CMakeLists.txt b/dmtcp/CMakeLists.txt new file mode 100644 index 00000000..c2fa5fce --- /dev/null +++ b/dmtcp/CMakeLists.txt @@ -0,0 +1,122 @@ +cmake_minimum_required(VERSION 3.10) +project(McMini-Revamp + VERSION 1.0.0 + DESCRIPTION "A bite-sized model checker" + LANGUAGES C CXX) + +# Require C11 and C++11 +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_C_STANDARD_REQUIRED TRUE) +set(CMAKE_CXX_STANDARD_REQUIRED TRUE) + +# Project configuration +option(BUILD_TESTS OFF) +option(VERBOSE_TESTING OFF) +set(MCMINI_DIR "${CMAKE_SOURCE_DIR}") +set(MCMINI_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/include") +set(MCMINI_CMAKE_MODULE_DIR "${CMAKE_SOURCE_DIR}/cmake") +set(MCMINI_WITH_DMTCP OFF) + +# Project source files +set(MCMINI_C_SRC + src/common/exit.c + src/common/mem.c + src/common/runner_mailbox.c + src/common/shm_config.c +) +set(MCMINI_CPP_SRC + src/mcmini/mcmini.cpp + src/mcmini/constants.cpp + src/mcmini/signal.cpp + src/mcmini/visible_object.cpp + src/mcmini/coordinator/coordinator.cpp + src/mcmini/log/filter.cpp + src/mcmini/log/logger.cpp + src/mcmini/model/detached_state.cpp + src/mcmini/model/diff_state.cpp + src/mcmini/model/program.cpp + src/mcmini/model/cond_var_arbitrary_policy.cpp + src/mcmini/model/cond_var_default_policy.cpp + src/mcmini/model/cond_var_single_grp_policy.cpp + src/mcmini/model/cond_var_wakegroup.cpp + src/mcmini/model/state_sequence.cpp + src/mcmini/model/transition_registry.cpp + src/mcmini/model/transition_sequence.cpp + src/mcmini/model/transitions/condition_variables.cpp + src/mcmini/model/transitions/mutex.cpp + src/mcmini/model/transitions/process.cpp + src/mcmini/model/transitions/thread.cpp + src/mcmini/model/transitions/semaphore.cpp + src/mcmini/model_checking/algorithms/classic_dpor.cpp + src/mcmini/model_checking/algorithms/clock_vector.cpp + src/mcmini/real_world/dmtcp_process_source.cpp + src/mcmini/real_world/fifo.cpp + src/mcmini/real_world/fork_process_source.cpp + src/mcmini/real_world/local_linux_process.cpp + src/mcmini/real_world/resources.cpp + src/mcmini/real_world/shm.cpp + src/mcmini/real_world/target.cpp +) +set(LIBMCMINI_C_SRC + src/common/exit.c + src/common/mem.c + src/common/multithreaded_fork.c + src/common/runner_mailbox.c + src/common/shm_config.c + src/lib/dmtcp-callback.c + src/lib/entry.c + src/lib/interception.c + src/lib/log.c + src/lib/main.c + src/lib/record.c + src/lib/sem-wrappers.c + src/lib/template/loop.c + src/lib/template/sig.c + src/lib/wrappers.c + src/mcmini/Thread_queue.c +) + +set(MCMINI_EXTRA_COMPILER_FLAGS -Wall -Werror) +set(MCMINI_EXTRA_COMPILER_DEFINITIONS "") +set(MCMINI_EXTRA_LINK_FLAGS -lrt -pthread) + +set(LIBMCMINI_EXTRA_COMPILER_FLAGS -Wall -Werror -fPIC) +set(LIBMCMINI_EXTRA_COMPILER_DEFINITIONS MC_SHARED_LIBRARY=1 DMTCP=1 LOGGING_ROOT="${CMAKE_SOURCE_PATH}") + +# -lrt -> shared memory +# -pthread -> libpthread.so +# -lm -> math library +# -ldl -> dlsym etc. +set(LIBMCMINI_EXTRA_LINK_FLAGS -lrt -pthread -lm -ldl) + +# libmcmini.so -> the dylib which is loaded +add_library(libmcmini SHARED "${LIBMCMINI_C_SRC}") +set_target_properties(libmcmini PROPERTIES OUTPUT_NAME "mcmini") + +target_include_directories(libmcmini PUBLIC "${MCMINI_INCLUDE_DIR}") +target_compile_definitions(libmcmini + PUBLIC + "${LIBMCMINI_EXTRA_COMPILER_DEFINITIONS}") +target_compile_options(libmcmini + PRIVATE + "${LIBMCMINI_EXTRA_COMPILER_FLAGS}") +target_link_libraries(libmcmini + PUBLIC + "${LIBMCMINI_EXTRA_LINK_FLAGS}") + +add_executable(mcmini "${MCMINI_CPP_SRC}" "${MCMINI_C_SRC}") +target_include_directories(mcmini PUBLIC "${MCMINI_INCLUDE_DIR}") +target_compile_definitions(mcmini + PUBLIC + "${MCMINI_EXTRA_COMPILER_DEFINITIONS}") +target_compile_options(mcmini + PRIVATE + "${MCMINI_EXTRA_COMPILER_FLAGS}") +target_link_libraries(mcmini + PUBLIC + "${MCMINI_EXTRA_LINK_FLAGS}") + +add_subdirectory(src/examples) +add_subdirectory(test) diff --git a/dmtcp/Cond_Var_Readme.md b/dmtcp/Cond_Var_Readme.md new file mode 100644 index 00000000..09a2425a --- /dev/null +++ b/dmtcp/Cond_Var_Readme.md @@ -0,0 +1,118 @@ +This document provides a detailed explanation of the algorithm used to manage condition variables +to support checkpoint and replay and model checking at restart time. + +## Thread State Tracking in Condition Variables +Each condition variable maintains a waiting_threads queue. The queue contains thread_queue_node entries that store: +- The thread's ID (runner_id_t) +- The thread's condition variable state (thread_cv_state) +## Recording Phase +Track condition variable states and thread queus to enable replay at restart time. +Each state in condition_variable_state corresponds to a phase in the lifescycle + of a condition variable. The status of a condition variable can be one of the following: + - CV_UNINITIALIZED: The condition variable has not been initialized yet via mc_pthread_cond_init. + - CV_INITIALIZED: The condition variable has been initialized and ready for use (post mc_pthread_cond_init). + - CV_PREWAITING: A thread is releasing the mutex but hasn't fully entered the wait state yet + (mc_pthread_cond_wait in progress), hence entered outer waiting room. This prevents checkpointing during the + unsafe gap between mutex unlock and wait. + - CV_WAITING: The thread has successfully entered the wait state i.e, consumed the signal or successfully returned + from libpthread_cond_timed_wait. (mc_pthread_cond_wait). + - CV_SIGNALED: A signal/broadcast has been sent; the condition variable resumes operation (mc_pthread_cond_signal/broadcast). + - CV_DESTROYED: The condition variable has been destroyed (post mc_pthread_cond_destroy). + Note that the status of a condition variable can be CV_SIGNALED even when there are no threads waiting on it. + This is because a thread can signal a condition variable even when no threads are waiting on it (spurious wakeup). + +## Thread State Transitions + +When a thread calls pthread_cond_wait(): +- It's added to the waiting queue with state CV_PREWAITING. +- After successfully entering wait, its state changes to CV_WAITING. + +When signaled: +- Its state changes to CV_SIGNALED +- The thread eventually dequeues and processes the signal + +## Lost Wakeup Tracking + +- The system tracks lost wakeups when signals are sent to condition variables with no waiting threads. +- These are recorded to ensure correct reconstruction during restart. + +## Serializing Thread-Condition Relationships +The system uses a two-level approach to serialize thread relationships: + +### CV_WAITERS_QUEUE Entries + +- For each thread in a condition variable's waiting queue, a specialized entry is created. +- Contains: condition variable location, thread ID, and thread's CV state +- This preserves the complete thread queue state including waiting order + +### State Preservation + +- During serialization, the system traverses each condition variable's waiting queue. +- For each thread, it records its specific relationship with the condition variable. +- This enables accurate reconstruction during deserialization. + +## Post-Restart Thread State Reconstruction +During restart, the system reconstructs condition variable states and thread relationships: + +### Creating State Map + +- CV_WAITERS_QUEUE entries are processed to build a cv_waiting_threads map +- Maps each condition variable address to an ordered list of (thread_id, cv_state) pairs + +### Policy-Based Management +- Each condition variable has a policy object (ConditionVariableArbitraryPolicy) that: + - Maintains thread waiting queue order + - Preserves thread-specific state for each queued thread + - Manages signaling and state transitions + +### Thread State Re-establishment +- During model object creation, waiting threads are added with their correct states +- The add_waiter_with_state() method ensures each thread's state is preserved + +## Post-Restart Handling By Condition Variable State + +### Case 1: CV_UNINITIALIZED +- Thread behavior: No special handling required +- System action: Condition variable will be initialized when first accessed + +### Case 2: CV_INITIALIZED +- Thread behavior: Thread sends COND_INIT_TYPE message via mailbox +- System action: + - Scheduler initializes condition variable using libpthread_cond_init() + - No thread state tracking needed as no threads are waiting + +### Case 3: CV_PREWAITING +- Thread behavior: + - Thread sends COND_ENQUEUE_TYPE message to re-enter waiting queue + - Thread waits for scheduler acknowledgment + - Thread sends COND_WAIT_TYPE to block until signaled +- System action: + - Scheduler adds thread to condition variable's waiting queue with CV_PREWAITING state + - Scheduler ensures mutex is released (critical for preventing deadlocks) + - Maintains pre-checkpoint thread state to ensure correct handling + +### Case 4: CV_WAITING +- Thread behavior: + - Thread sends COND_ENQUEUE_TYPE message to re-register in waiting queue + - Thread sends COND_WAIT_TYPE to re-enter wait state +- System action: + - Scheduler adds thread to waiting queue with CV_WAITING state + - Reconstructs exact pre-checkpoint waiting order + - Thread properly blocks waiting for signal + +### Case 5: CV_SIGNALED +- Thread behavior: + - If thread was signaled pre-checkpoint, it resumes execution + - If thread was waiting, it follows CV_WAITING behavior +- System action: + - Scheduler identifies which threads were signaled pre-checkpoint + - These threads are added to "wake groups" in the policy + - Ensures signaled threads receive priority for resumption + - Maintains proper FIFO ordering for signal processing + +### Case 6: CV_DESTROYED +- Thread behavior: Thread sends COND_DESTROY_TYPE message +- System action: + - Scheduler verifies no threads are waiting (or handles error) + - Resources associated with condition variable are properly released + - No further operations allowed on the condition variable \ No newline at end of file diff --git a/dmtcp/include/dmtcp.h b/dmtcp/include/dmtcp.h new file mode 100644 index 00000000..d0e774e4 --- /dev/null +++ b/dmtcp/include/dmtcp.h @@ -0,0 +1,619 @@ +/**************************************************************************** + * TODO: Replace this header with appropriate header showing MIT OR BSD * + * License * + * Copyright (C) 2006-2008 by Jason Ansel, Kapil Arya, and Gene Cooperman * + * jansel@csail.mit.edu, kapil@ccs.neu.edu, gene@ccs.neu.edu * + * * + * This file, dmtcp.h, is placed in the public domain. * + * The motivation for this is to allow anybody to freely use this file * + * without restriction to statically link this file with any software. * + * This allows that software to communicate with the DMTCP libraries. * + * - Jason Ansel, Kapil Arya, and Gene Cooperman * + * jansel@csail.mit.edu, kapil@ccs.neu.edu, gene@ccs.neu.edu * + ****************************************************************************/ + +#ifndef DMTCP_H +#define DMTCP_H + +#include +#include +#include +#include +#include + +#ifndef __USE_GNU +# define __USE_GNU_NOT_SET +# define __USE_GNU +#endif // ifndef __USE_GNU +#include /* for NEXT_FNC() */ +#include /* for NEXT_FNC() */ +#include /* for NEXT_FNC() */ +#ifdef __USE_GNU_NOT_SET +# undef __USE_GNU_NOT_SET +# undef __USE_GNU +#endif // ifdef __USE_GNU_NOT_SET + +#ifndef DMTCP_PACKAGE_VERSION +# include "dmtcp/version.h" +#endif + +#ifdef __cplusplus +# define EXTERNC extern "C" +#else // ifdef __cplusplus +# define EXTERNC +#endif // ifdef __cplusplus + +/* Define to the version of this package. */ +#define DMTCP_PLUGIN_API_VERSION "3" + +#ifdef __cplusplus +namespace dmtcp { +// Used by 'DMTCP_RESTART_PAUSE_WHILE(cond)', below; Defined in threadlist.cpp. +extern volatile int restartPauseLevel; +} +#endif // ifdef __cplusplus + +#ifdef __cplusplus +extern "C" { +#endif // ifdef __cplusplus + +#define LIB_PRIVATE __attribute__((visibility("hidden"))) +#define ATOMIC_SHARED_GLOBAL volatile __attribute((aligned)) +// Same as global macro, but by convnetion, use this for local variables: +#define ATOMIC_SHARED volatile __attribute((aligned)) +#define ATTR_TLS_INITIAL_EXEC __attribute__((tls_model("initial-exec"))) + +typedef enum eDmtcpEvent { + DMTCP_EVENT_INIT, + DMTCP_EVENT_EXIT, + + DMTCP_EVENT_PRE_EXEC, + DMTCP_EVENT_POST_EXEC, + + DMTCP_EVENT_ATFORK_PREPARE, + DMTCP_EVENT_ATFORK_PARENT, + DMTCP_EVENT_ATFORK_CHILD, + DMTCP_EVENT_ATFORK_FAILED, + + DMTCP_EVENT_VFORK_PREPARE, + DMTCP_EVENT_VFORK_PARENT, + DMTCP_EVENT_VFORK_CHILD, + DMTCP_EVENT_VFORK_FAILED, + + DMTCP_EVENT_PTHREAD_START, + DMTCP_EVENT_PTHREAD_EXIT, + DMTCP_EVENT_PTHREAD_RETURN, + + DMTCP_EVENT_PRESUSPEND, + DMTCP_EVENT_PRECHECKPOINT, + DMTCP_EVENT_RESUME, + DMTCP_EVENT_RESTART, + DMTCP_EVENT_RUNNING, + DMTCP_EVENT_THREAD_RESUME, + + DMTCP_EVENT_OPEN_FD, + DMTCP_EVENT_REOPEN_FD, + DMTCP_EVENT_CLOSE_FD, + DMTCP_EVENT_DUP_FD, + + DMTCP_EVENT_VIRTUAL_TO_REAL_PATH, + DMTCP_EVENT_REAL_TO_VIRTUAL_PATH, + + nDmtcpEvents +} DmtcpEvent_t; + +typedef union _DmtcpEventData_t { + struct { + int serializationFd; + char *filename; + size_t maxArgs; + const char **argv; + size_t maxEnv; + const char **envp; + } preExec; + + struct { + int serializationFd; + } postExec; + + struct { + int isRestart; + } resumeUserThreadInfo, nameserviceInfo; + + struct { + int fd; + const char *path; + int flags; + mode_t mode; + } openFd; + + struct { + int fd; + const char *path; + int flags; + } reopenFd; + + struct { + int fd; + } closeFd; + + struct { + int oldFd; + int newFd; + } dupFd; + + struct { + char *path; + } realToVirtualPath, virtualToRealPath; +} DmtcpEventData_t; + +typedef void (*HookFunctionPtr_t)(DmtcpEvent_t, DmtcpEventData_t *); + +typedef struct { + const char *pluginApiVersion; + const char *dmtcpVersion; + + const char *pluginName; + const char *authorName; + const char *authorEmail; + const char *description; + + void (*event_hook)(const DmtcpEvent_t event, DmtcpEventData_t *data); +} DmtcpPluginDescriptor_t; + +// Used by dmtcp_get_restart_env() +typedef enum eDmtcpGetRestartEnvErr { + RESTART_ENV_SUCCESS = 0, + RESTART_ENV_NOTFOUND = -1, + RESTART_ENV_TOOLONG = -2, + RESTART_ENV_DMTCP_BUF_TOO_SMALL = -3, + RESTART_ENV_INTERNAL_ERROR = -4, + RESTART_ENV_NULL_PTR = -5, +} DmtcpGetRestartEnvErr_t; + +typedef enum eDmtcpMutexType +{ + DMTCP_MUTEX_NORMAL, + DMTCP_MUTEX_RECURSIVE, + DMTCP_MUTEX_LLL +} DmtcpMutexType; + +typedef struct +{ + uint32_t futex; + // 'owner' can't use pid_t. This must work with 32- and 64-bit processes. + uint32_t owner; + uint32_t count; + DmtcpMutexType type; +} DmtcpMutex; + +#define DMTCP_MUTEX_INITIALIZER {0, 0, 0, DMTCP_MUTEX_NORMAL} +#define DMTCP_MUTEX_INITIALIZER_RECURSIVE {0, 0, 0, DMTCP_MUTEX_RECURSIVE} +#define DMTCP_MUTEX_INITIALIZER_LLL {0, 0, 0, DMTCP_MUTEX_LLL} + +typedef struct +{ + int32_t nReaders: 10; + int32_t nWriters: 10; + int32_t nReadersQueued: 10; + int32_t unused: 2; +} DmtcpRWLockStatus; + +typedef struct { + DmtcpRWLockStatus status; + int32_t writerTid; + uint32_t readerFutex; + uint32_t writerFutex; +} DmtcpRWLock; + +void DmtcpMutexInit(DmtcpMutex *mutex, DmtcpMutexType type); +int DmtcpMutexLock(DmtcpMutex *mutex); +int DmtcpMutexTryLock(DmtcpMutex *mutex); +int DmtcpMutexUnlock(DmtcpMutex *mutex); + +void DmtcpRWLockInit(DmtcpRWLock *rwlock); +int DmtcpRWLockRdLock(DmtcpRWLock *rwlock); +int DmtcpRWLockRdLockIgnoreQueuedWriter(DmtcpRWLock *rwlock); +int DmtcpRWLockTryRdLock(DmtcpRWLock *rwlock); +int DmtcpRWLockWrLock(DmtcpRWLock *rwlock); +int DmtcpRWLockUnlock(DmtcpRWLock *rwlock); + + +#define RESTART_ENV_MAXSIZE 12288*10 + +// Internal usage only. Shouldn't be used directly by the plugin. Use +// DMTCP_DECL_PLUGIN instead. +void dmtcp_initialize_plugin(void) __attribute((weak)); + +#define DMTCP_DECL_PLUGIN(descr) \ + EXTERNC void dmtcp_initialize_plugin() \ + { \ + dmtcp_register_plugin(descr); \ + void (*fn)() = NEXT_FNC(dmtcp_initialize_plugin); \ + if (fn != NULL) { \ + (*fn)(); \ + } \ + } + +typedef struct DmtcpUniqueProcessId { + uint64_t _hostid; // gethostid() + uint64_t _time; // time() + pid_t _pid; // getpid() + uint32_t _computation_generation; // computationGeneration() +} DmtcpUniqueProcessId; + +int dmtcp_unique_pids_equal(DmtcpUniqueProcessId a, DmtcpUniqueProcessId b); + +// FIXME: +// If a plugin is not compiled with defined(__PIC__) and we can verify +// that we're using DMTCP (environment variables), and dmtcp_is_enabled +// or dmtcp_checkpoint expands to 0, then we should print a warning +// at run-time. + +// These utility functions require compiling the target app with -fPIC + +/** + * Returns 1 if executing under dmtcp_launch, 0 otherwise + * See: test/plugin/applic-initiated-ckpt and applic-delayed-ckpt + * directories for exammples: + */ +int dmtcp_is_enabled(void) __attribute((weak)); +#define dmtcp_is_enabled() (dmtcp_is_enabled ? dmtcp_is_enabled() : 0) + +/** + * Checkpoint the entire distributed computation + * (Does not necessarily block until checkpoint is complete. + * Use dmtcp_get_generation() to test if checkpoint is complete.) + * NOTE: This macro is blocking. dmtcp_checkpoint() will not return + * until a checkpoint is taken. This guarantees that the + * current _thread_ blocks until the current process has been + * checkpointed. It guarantees nothing about other threads or + * other processes. + * + returns DMTCP_AFTER_CHECKPOINT if the checkpoint succeeded. + * + returns DMTCP_AFTER_RESTART after a restart. + * + returns <=0 on error. + * See: test/plugin/applic-initiated-ckpt directory for an exammple: + */ +int dmtcp_checkpoint(void) __attribute__((weak)); +#define dmtcp_checkpoint() \ + (dmtcp_checkpoint ? dmtcp_checkpoint() : DMTCP_NOT_PRESENT) + +/** + * Prevent a checkpoint from starting until dmtcp_enable_checkpoint() is + * called. + * + Has (recursive) lock semantics, only one thread may acquire it at time. + * + Only prevents checkpoints locally, remote processes may be suspended. + * Thus, send or recv to another checkpointed process may create deadlock. + * + Returns 1 on success, <=0 on error + * See: test/plugin/applic-delayed-ckpt directory for an exammple: + */ +int dmtcp_disable_ckpt(void) __attribute__((weak)); +#define dmtcp_disable_ckpt() \ + (dmtcp_disable_ckpt ? dmtcp_disable_ckpt() : DMTCP_NOT_PRESENT) + +/** + * Re-allow checkpoints, opposite of dmtcp_disable_ckpt(). + * + Returns 1 on success, <=0 on error + * See: test/plugin/applic-delayed-ckpt directory for an exammple: + */ +int dmtcp_enable_ckpt(void) __attribute__((weak)); +#define dmtcp_enable_ckpt() \ + (dmtcp_enable_ckpt ? dmtcp_enable_ckpt() : DMTCP_NOT_PRESENT) + +/* + * Global barriers are required when a plugin needs inter-node synchronization, + * such as using the coordinator name-service database. Currently, only the + * socket, RM, and InfiniBand plugins need global barriers. All other plugins + * handle node-local resources such as files, pids, etc., and are fine with + * using local barriers. + * A simple thumb rule is to always insert a global-barrier between registering + * and querying the coordinator name-service database. + */ +void dmtcp_global_barrier(const char *barrier) __attribute((weak)); +void dmtcp_local_barrier(const char *barrier) __attribute((weak)); + +void dmtcp_get_local_ip_addr(struct in_addr *in) __attribute((weak)); + +const char *dmtcp_get_tmpdir(void); + +const char *dmtcp_get_ckpt_dir(void) __attribute((weak)); +#define dmtcp_get_ckpt_dir() \ + (dmtcp_get_ckpt_dir ? dmtcp_get_ckpt_dir() : "") + +int dmtcp_set_ckpt_dir(const char *) __attribute((weak)); +#define dmtcp_set_ckpt_dir(d) \ + (dmtcp_set_ckpt_dir ? dmtcp_set_ckpt_dir(d) : DMTCP_NOT_PRESENT) + +const char *dmtcp_get_ckpt_filename(void) __attribute__((weak)); +const char *dmtcp_get_ckpt_files_subdir(void); +int dmtcp_should_ckpt_open_files(void); +int dmtcp_allow_overwrite_with_ckpted_files(void); +int dmtcp_skip_truncate_file_at_restart(const char* path); +void dmtcp_set_restore_buf_addr(void *new_addr, uint64_t len); +uint64_t dmtcp_restore_buf_len(); + +int dmtcp_get_ckpt_signal(void); +const char *dmtcp_get_uniquepid_str(void) __attribute__((weak)); + +/* + * ComputationID + * ComputationID of a computation is the unique-pid of the first process of + * the computation. Even if that process dies, the rest of the computation + * retains the same computation ID. + * + * With --enable-unique-checkpoint-filenames, the ComputationID also includes + * the checkpoint generation number (starting from 1 for the first + * checkpoint). This number is the same for the entire computation at a + * given point in time. Dmtcp coordinator increments this number prior + * to sending the SUSPEND message, and it is sent to the workers as a part + * of the SUSPEND message. + */ +const char *dmtcp_get_computation_id_str(void); +uint64_t dmtcp_get_coordinator_timestamp(void); + +// Generation is 0 before first checkpoint, and then successively incremented. +uint32_t dmtcp_get_generation(void) __attribute__((weak)); +int checkpoint_is_pending(void) __attribute__((weak)); + +/** + * Gets the coordinator-specific status of DMTCP. + * - Returns DMTCP_IS_PRESENT if running under DMTCP and DMTCP_NOT_PRESENT + * otherwise. + * - Side effects: modifies the arguments + * + * Args: + * numPeers: Number of processes connected to dmtcp_coordinator + * isRunning: 1 if all processes connected to dmtcp_coordinator are in a + * running state + */ +int dmtcp_get_coordinator_status(int *numPeers, int *isRunning) +__attribute__((weak)); +#define dmtcp_get_coordinator_status(p, r) \ + (dmtcp_get_coordinator_status ? dmtcp_get_coordinator_status(p, r) \ + : DMTCP_NOT_PRESENT) + +/** + * Queries local state of this process, not global state seen by DMTCP coord. + * - Returns DMTCP_IS_PRESENT if running under DMTCP and DMTCP_NOT_PRESENT + * otherwise. + * - Side effects: modifies the arguments + * + * Args: + * numCheckpoints: The number of times this process has been checkpointed + * (excludes restarts) + * numRestarts: The number of times this process has been restarted + */ +int dmtcp_get_local_status(int *numCheckpoints, int *numRestarts) +__attribute__((weak)); +#define dmtcp_get_local_status(c, r) \ + (dmtcp_get_local_status ? dmtcp_get_local_status(c, r) : DMTCP_NOT_PRESENT) + +// Is DMTCP in the running state? +// (e.g., not in pre-ckpt, post-ckpt, post-restart event)? +int dmtcp_is_running_state(void); + +// Primarily for use by the modify-env plugin. +DmtcpGetRestartEnvErr_t dmtcp_get_restart_env(const char *name, + char *value, + size_t maxvaluelen); + +// Get pathname of target executable under DMTCP control. +const char *dmtcp_get_executable_path(); + +/* If your plugin invokes wrapper functions before DMTCP is initialized, + * then call this prior to your first wrapper function call. + */ +void dmtcp_initialize(void) __attribute((weak)); + +// FOR EXPERTS ONLY: +int dmtcp_is_protected_fd(int fd); +DmtcpUniqueProcessId dmtcp_get_uniquepid(); +DmtcpUniqueProcessId dmtcp_get_coord_id(); +DmtcpUniqueProcessId dmtcp_get_computation_id(); + +// FOR EXPERTS ONLY: +int dmtcp_get_readlog_fd(void); +void dmtcp_block_ckpt_signal(void); +void dmtcp_unblock_ckpt_signal(void); + +// FOR EXPERTS ONLY: +void dmtcp_close_protected_fd(int fd); +int dmtcp_protected_environ_fd(void); + +/* FOR EXPERTS ONLY: + * The DMTCP internal pid plugin ensures that the application sees only + * a virtual pid, which can be translated to the current real pid + * assigned to the kernel on a restart. The pid plugin places wrappers + * around all system calls referring to a pid. If your application + * discovers a pid without going through a system call (e.g., through + * the proc filesystem), use this to virtualize the pid. + */ +pid_t dmtcp_real_to_virtual_pid(pid_t realPid) __attribute((weak)); +pid_t dmtcp_virtual_to_real_pid(pid_t virtualPid) __attribute((weak)); + +#define mcmini_virtual_pid(PID) \ + (dmtcp_is_enabled() ? dmtcp_real_to_virtual_pid((PID)) : (PID)) +#define mcmini_real_pid(PID) \ + (dmtcp_is_enabled() ? dmtcp_virtual_to_real_pid((PID)) : (PID)) + +// bq_file -> "batch queue file"; used only by batch-queue plugin +int dmtcp_is_bq_file(const char *path) __attribute((weak)); +int dmtcp_bq_should_ckpt_file(const char *path, int *type) __attribute((weak)); +int dmtcp_bq_restore_file(const char *path, + const char *savedFilePath, + int fcntlFlags, + int type) __attribute((weak)); + +/* These next two functions are defined in contrib/ckptfile/ckptfile.cpp + * But they are currently used only in src/plugin/ipc/file/fileconnection.cpp + * and in a trivial fashion. These are intended for future extensions. + */ +int dmtcp_must_ckpt_file(const char *path) __attribute((weak)); +void dmtcp_get_new_file_path(const char *abspath, + const char *cwd, + char *newpath) __attribute((weak)); +int dmtcp_must_overwrite_file(const char *path) __attribute((weak)); + +void dmtcp_initialize(void) __attribute((weak)); + +void dmtcp_register_plugin(DmtcpPluginDescriptor_t) __attribute((weak)); + +// These are part of the internal implementation of DMTCP plugins +int dmtcp_plugin_disable_ckpt(void); +#define DMTCP_PLUGIN_DISABLE_CKPT() dmtcp_plugin_disable_ckpt() + +void dmtcp_plugin_enable_ckpt(void); +#define DMTCP_PLUGIN_ENABLE_CKPT() dmtcp_plugin_enable_ckpt() + +void dmtcp_add_to_ckpt_header(const char *key, const char *value); + +typedef struct dt_tag { + char *base_addr; /* Base address shared object is loaded at. */ + + // ElfW(Sym) *dynsym; // On disk, dynsym would be dynamic symbols only + ElfW(Sym) * symtab; // Same as dynsym, for in-memory symbol table. + // ElfW(Word) n_symtab; + ElfW(Half) * versym; + + /* elf.h lies. DT_VERDEF is offset from base_addr, not addr. */ + ElfW(Verdef) * verdef; + ElfW(Word) verdefnum; + + // ElfW(Word) first_ext_def; + char *strtab; + Elf32_Word *hash; + Elf32_Word *gnu_hash; +} dt_tag; + +void *dmtcp_dlsym(void *handle, const char *symbol) __attribute((weak)); +void *dmtcp_dlvsym(void *handle, char *symbol, const char *version); +void *dmtcp_dlsym_lib(const char *libname, const char *symbol); +void * +dlsym_default_internal_library_handler(void *handle, + const char *symbol, + const char *version, + dt_tag *tags_p, + Elf32_Word *default_symbol_index_p); +void * +dlsym_default_internal_flag_handler(void *handle, + const char *libname, + const char *symbol, + const char *version, + void *addr, + dt_tag *tags_p, + Elf32_Word *default_symbol_index_p); + +/* + * Returns the offset of the given function within the given shared library + * or LIB_FNC_OFFSET_FAILED if the function does not exist in the library + */ +#define LIB_FNC_OFFSET_FAILED ((uint64_t)-1) +uint64_t dmtcp_dlsym_lib_fnc_offset(const char *libname, const char *symbol); + +#define NEXT_FNC(func) \ + ({ \ + static __typeof__(&func)_real_ ## func = (__typeof__(&func)) - 1; \ + if (_real_ ## func == (__typeof__(&func)) - 1) { \ + if (dmtcp_initialize) { \ + dmtcp_initialize(); \ + } \ + _real_ ## func = (__typeof__(&func))dmtcp_dlsym(RTLD_NEXT, # func); \ + } \ + _real_ ## func; \ + }) + +/* + * It uses dmtcp_dlvsym to get the function with the specified version in the + * next library in the library-search order. + */ +# define NEXT_FNC_V(func, ver) \ + ({ \ + static __typeof__(&func) _real_##func = (__typeof__(&func)) -1; \ + if (_real_##func == (__typeof__(&func)) -1) { \ + if (dmtcp_initialize) { \ + dmtcp_initialize(); \ + } \ + _real_##func = (__typeof__(&func)) dmtcp_dlvsym(RTLD_NEXT, #func, ver); \ + } \ + _real_##func; \ + }) + +/* + * It uses dmtcp_dlsym to get the default function (in case of symbol + * versioning) in the library with the given name. + * + * One possible usecase could be for bypassing the plugin layers and directly + * jumping to a symbol in libc. + */ +# define NEXT_FNC_LIB(lib, func) \ + ({ \ + static __typeof__(&func) _real_##func = (__typeof__(&func)) -1; \ + if (_real_##func == (__typeof__(&func)) -1) { \ + if (dmtcp_initialize) { \ + dmtcp_initialize(); \ + } \ + _real_##func = (__typeof__(&func)) dmtcp_dlsym_lib(lib, #func); \ + } \ + _real_##func; \ + }) + +// =================================================================== +// DMTCP utilities + +#ifndef DMTCP_AFTER_CHECKPOINT + +// Return value of dmtcp_checkpoint +# define DMTCP_AFTER_CHECKPOINT 1 + +// Return value of dmtcp_checkpoint +# define DMTCP_AFTER_RESTART 2 +#endif // ifndef DMTCP_AFTER_CHECKPOINT +#ifndef DMTCP_NOT_PRESENT +# define DMTCP_NOT_PRESENT 3 +#endif // ifndef DMTCP_NOT_PRESENT +#ifndef DMTCP_IS_PRESENT +# define DMTCP_IS_PRESENT 4 +#endif // ifndef DMTCP_IS_PRESENT + +#define dmtcp_get_ckpt_filename() \ + (dmtcp_get_ckpt_filename ? dmtcp_get_ckpt_filename() : NULL) + +#define dmtcp_get_uniquepid_str() \ + (dmtcp_get_uniquepid_str ? dmtcp_get_uniquepid_str() : NULL) + +/* dmtcp_launch, dmtcp_restart return a unique rc (default: 99) + * TYPICAL USAGE: exit(DMTCP_FAIL_RC) + * Use this to distinguish DMTCP failing versus the target application failing. + */ +#define DMTCP_FAIL_RC_PARAM \ + (getenv("DMTCP_FAIL_RC") && atoi(getenv("DMTCP_FAIL_RC")) \ + ? atoi(getenv("DMTCP_FAIL_RC")) \ + : 99) + +#define DMTCP_FAIL_RC \ + (getenv("DMTCP_ABORT_ON_FAILURE") \ + ? abort(), 99 /* not reached */ \ + : DMTCP_FAIL_RC_PARAM) + +/// Pointer to a "void foo();" function +typedef void (*dmtcp_fnptr_t)(void); + +#ifdef HAS_PR_SET_PTRACER +#define DMTCP_SETUP_PTRACE() prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0); +#else +#define DMTCP_SETUP_PTRACE() +#endif // ifdef HAS_PR_SET_PTRACER + +// Usage: DMTCP_RESTART_PAUSE_WHILE(restartPauseLevel == ); +#define DMTCP_RESTART_PAUSE_WHILE(condition) \ + do { \ + if (condition) { \ + DMTCP_SETUP_PTRACE(); \ + while (condition); \ + } \ + } while (0) + +#ifdef __cplusplus +} // extern "C" { +#endif // ifdef __cplusplus +#endif // ifndef DMTCP_H diff --git a/dmtcp/include/dmtcp/version.h b/dmtcp/include/dmtcp/version.h new file mode 100644 index 00000000..4915c3bb --- /dev/null +++ b/dmtcp/include/dmtcp/version.h @@ -0,0 +1,21 @@ +/**************************************************************************** + * TODO: Replace this header with appropriate header showing MIT OR BSD * + * License * + * Copyright (C) 2006-2008 by Jason Ansel, Kapil Arya, and Gene Cooperman * + * jansel@csail.mit.edu, kapil@ccs.neu.edu, gene@ccs.neu.edu * + * * + * This file, dmtcp.h, is placed in the public domain. * + * The motivation for this is to allow anybody to freely use this file * + * without restriction to statically link this file with any software. * + * This allows that software to communicate with the DMTCP libraries. * + * - Jason Ansel, Kapil Arya, and Gene Cooperman * + * jansel@csail.mit.edu, kapil@ccs.neu.edu, gene@ccs.neu.edu * + ****************************************************************************/ + +#ifndef __DMTCP_VERSION_H__ +#define __DMTCP_VERSION_H__ + +/* Define to the version of this package. */ +#define DMTCP_PACKAGE_VERSION "3.0.0" + +#endif // ifndef __DMTCP_VERSION_H__ diff --git a/dmtcp/include/mcmini/Thread_queue.h b/dmtcp/include/mcmini/Thread_queue.h new file mode 100644 index 00000000..e9c4c0de --- /dev/null +++ b/dmtcp/include/mcmini/Thread_queue.h @@ -0,0 +1,35 @@ +#ifndef THREAD_QUEUE_H +#define THREAD_QUEUE_H +#include +#include +#include "spy/checkpointing/cv_status.h" // For condition_variable_status +#include "mcmini/defines.h" + + +typedef struct thread_queue_node { + runner_id_t thread; + condition_variable_status thread_cv_state; + struct thread_queue_node *next; +} thread_queue_node; + +typedef struct thread_queue { + thread_queue_node *front; + thread_queue_node *rear; + int size; +} thread_queue; + +thread_queue_node* create_thread_queue_node(); +thread_queue* create_thread_queue(); +void init_thread_queue(thread_queue *queue); +void enqueue_thread(thread_queue *queue, runner_id_t thread , condition_variable_status cv_state); +runner_id_t dequeue_thread(thread_queue *queue); +runner_id_t peek_thread(thread_queue *queue); +bool is_in_thread_queue(thread_queue *queue, runner_id_t thread); +bool is_queue_empty(thread_queue *queue); +condition_variable_status get_thread_cv_state(thread_queue *queue, runner_id_t thread); +void update_thread_cv_state(thread_queue *queue, runner_id_t thread, condition_variable_status new_state); +runner_id_t get_waiting_thread_node(thread_queue *queue); +void print_thread_queue(const thread_queue *queue); +int remove_thread_from_queue(thread_queue* queue, runner_id_t tid) ; + +#endif // THREAD_QUEUE_H diff --git a/dmtcp/include/mcmini/common/exit.h b/dmtcp/include/mcmini/common/exit.h new file mode 100644 index 00000000..72b635b8 --- /dev/null +++ b/dmtcp/include/mcmini/common/exit.h @@ -0,0 +1,12 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +void mc_exit(int); +void mc_exit_mt(int status); + +#ifdef __cplusplus +} +#endif // extern "C" diff --git a/dmtcp/include/mcmini/common/shm_config.h b/dmtcp/include/mcmini/common/shm_config.h new file mode 100644 index 00000000..8841babd --- /dev/null +++ b/dmtcp/include/mcmini/common/shm_config.h @@ -0,0 +1,26 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "mcmini/defines.h" +#include "mcmini/real_world/mailbox/runner_mailbox.h" +#include "mcmini/real_world/process/template_process.h" +#include "mcmini/spy/checkpointing/rec_list.h" + +const extern size_t shm_size; +void mc_get_shm_handle_name(char *dst, size_t sz); + +struct mcmini_shm_file { + struct template_process_t tpt; + struct rec_list rec_list; + runner_mailbox mailboxes[MAX_TOTAL_THREADS_IN_PROGRAM]; +}; + +#ifdef __cplusplus +} +#endif // extern "C" diff --git a/dmtcp/include/mcmini/common/thread.h b/dmtcp/include/mcmini/common/thread.h new file mode 100644 index 00000000..5558f0de --- /dev/null +++ b/dmtcp/include/mcmini/common/thread.h @@ -0,0 +1,11 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +void mc_set_thread_name(const char *); + +#ifdef __cplusplus +} +#endif // extern "C" diff --git a/dmtcp/include/mcmini/constants.hpp b/dmtcp/include/mcmini/constants.hpp new file mode 100644 index 00000000..031c54c9 --- /dev/null +++ b/dmtcp/include/mcmini/constants.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include + +class constants { + public: + static pid_t getpid(); + static size_t subsystem_log_size(); + static size_t file_log_size(); +}; diff --git a/docs/design/include/mcmini/coordinator/coordinator.hpp b/dmtcp/include/mcmini/coordinator/coordinator.hpp similarity index 73% rename from docs/design/include/mcmini/coordinator/coordinator.hpp rename to dmtcp/include/mcmini/coordinator/coordinator.hpp index d4bbe1ce..e8a7f334 100644 --- a/docs/design/include/mcmini/coordinator/coordinator.hpp +++ b/dmtcp/include/mcmini/coordinator/coordinator.hpp @@ -2,11 +2,12 @@ #include "mcmini/coordinator/model_to_system_map.hpp" #include "mcmini/forwards.hpp" +#include "mcmini/log/logger.hpp" #include "mcmini/model/program.hpp" #include "mcmini/model/transition_registry.hpp" #include "mcmini/model/visible_object.hpp" #include "mcmini/real_world/process_source.hpp" -#include "mcmini/real_world/runner.hpp" +#include "mcmini/real_world/remote_address.hpp" /** * @brief A mechanism which synchronizes a McMini model of a program @@ -99,7 +100,7 @@ class coordinator { * undefined (most likely this would lead to deadlocks etc.). */ coordinator(model::program &&initial_state, - model::transition_registry &&runtime_transition_mapping, + model::transition_registry runtime_transition_mapping, std::unique_ptr &&process_source); ~coordinator() = default; @@ -108,11 +109,12 @@ class coordinator { } /** - * @brief Returns the number of steps into the program the coordinator has - * directed programs. + * @brief Returns the number of steps (thread routine/visible operation calls) + * intercepted by `libmcmini.so` loaded into the current live process the + * coordinator is managing. * * The depth into the program is the number of transitions which have been - * executed by the coordinator. + * scheduled for execution by the coordinator. */ uint32_t get_depth_into_program() const { return current_program_model.get_trace().count(); @@ -123,13 +125,13 @@ class coordinator { * number of steps into execution. * * The coordinator can be scheduled to restore the model and the external - * world to correspond to how it looked in the past. This is useful for model - * checkers that want to. + * world to correspond to how it looked in the past. This is used by model + * checkers to restore previously modeled states. * * The method has no effect if `n == get_depth_into_program()`. * - * @throws an exception is raised if the step `n` r - * + * @throws an exception is raised if the step `n` is greater than then the + * execution depth of the current program. */ void return_to_depth(uint32_t n); @@ -156,20 +158,50 @@ class coordinator { * newly discovered. After execution, any such objects will be recorded. * * @param id the runner (thread) which should run. + * @param execution_error is raised if the runner cannot be executed by + * the coordinator. */ - void execute_runner(runner::runner_id_t id); + void execute_runner(real_world::process::runner_id_t id); private: + logging::logger logger = logging::logger("coord"); model::program current_program_model; model::transition_registry runtime_transition_mapping; std::unique_ptr current_process_handle; std::unique_ptr process_source; + std::unordered_map, model::state::objid_t> + system_address_mapping; - /// @brief A mapping between remote addresses in the processes produced by - /// `process_source` and those of the - std::unordered_map system_address_mapping; + void assign_new_process_handle() { + // NOTE: This is INTENTIONALLY split into two separate statements, i.e. + // assiging `nullptr` and THEN calling `force_new_process()`. We want to + // destroy the old process first before we create a new one. This is + // necessary because the processes generated by the process source may share + // resources, so we want to be sure that any shared resources are + // relinquished before creating a new process. + // + // The trouble is that C++ will evaluate the right-hand expression before + // the assignment, which means that there will be TWO processes at the same + // time for a brief moment in between the right-hand evaluation, the + // assignment, and the destruction of the previous process handle. This has + // the potential to introduce a race condition between these shared + // resources. By splitting the statement into two, we ensure that C++ first + // calls the destructor for the current process handle and THEN creates the + // new one, but not concurrently (see C++ evaluation ordering on + // cppreference for more details). + // + // NOTE: This assumes that by the time the `real_world::process` has been + // destroyed, the process to which is corresponds no longer exists. That is, + // the process must be destroyed _synchronously_ upon destruction or else + // already be destroyed prior to destruction. + log_verbose(logger) << "Removing the current branch process"; + this->current_process_handle = nullptr; + log_verbose(logger) + << "Removed the current branch process. Creating a new branch process"; + this->current_process_handle = this->process_source->force_new_process(); + log_verbose(logger) << "New branch process created by the coordinator"; + } - /// Allow modifications through the `model_to_system_map` (implementation - /// detail) + // Allow modifications through the `model_to_system_map` (impl detail) friend model_to_system_map; -}; \ No newline at end of file +}; diff --git a/dmtcp/include/mcmini/coordinator/model_to_system_map.hpp b/dmtcp/include/mcmini/coordinator/model_to_system_map.hpp new file mode 100644 index 00000000..91674ec5 --- /dev/null +++ b/dmtcp/include/mcmini/coordinator/model_to_system_map.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include + +#include "mcmini/coordinator/coordinator.hpp" +#include "mcmini/forwards.hpp" +#include "mcmini/model/state.hpp" +#include "mcmini/real_world/remote_address.hpp" + +/** + * @brief A mapping between the remote addresses pointing to the C/C++ structs + * the objects in McMini's model are emulating. + * + * As McMini explores different paths of execution of the target program at + * runtime, it may discover new visible objects. However, visible objects are + * only a _representation in the McMini model_ of the actual underlying + * structs containing the information used to implement the primitive. The + * underlying process, however, refers to these objects as pointers to the + * multi-threaded primitives (e.g. a `pthread_mutex_t*` in + * `pthread_mutex_lock()`). McMini therefore needs to maintain a + * correspondence between these addresses and the identifiers in McMini's + * model used to represent those objects to the model checker. + * + * @important: Handles are assumed to remain valid _across process source + * invocations_. In the future, we could support the ability to _remap_ + * process handles dynamically during each new re-execution scheduled by + * the coordinator to handle aliasing etc. by using the trace as a total + * ordering on object-creation events. Until we run into this issue, we leave it + * for future development. + */ +class model_to_system_map final { + private: + coordinator &_coordinator; + friend coordinator; + + public: + /* + * Prevent external construction (only the coordinator can construct + * instances of this class) + */ + model_to_system_map(coordinator &coordinator) : _coordinator(coordinator) {} + model_to_system_map() = delete; + + model::state::objid_t get_model_of_object( + real_world::remote_address) const; + model::state::runner_id_t get_model_of_runner( + real_world::remote_address) const; + bool contains(real_world::remote_address addr) const { + return get_model_of_object(addr) != model::invalid_objid; + } + bool contains_runner(real_world::remote_address addr) const { + return get_model_of_runner(addr) != model::invalid_rid; + } + + using runner_generation_function = + std::function; + + /** + * @brief Record the presence of a new visible object that is + * represented with the system id. + * + * @param remote_process_visible_object_handle the address containing + * the data for the new visible object across process handles of the + * `real_world::process_source` in the coordinator + */ + model::state::objid_t observe_object(real_world::remote_address, + const model::visible_object_state *); + + // TODO: Does it make sense to be able to add a runner without a transition + // and then later (retroactively) give it a transition (in _this_ interface + // that is) + model::state::runner_id_t observe_runner(real_world::remote_address, + const model::runner_state *); + void observe_runner_transition(const model::transition *); + model::state::runner_id_t observe_runner(real_world::remote_address, + const model::runner_state *, + const model::transition *); + model::state::runner_id_t observe_runner(real_world::remote_address, + const model::runner_state *, + runner_generation_function f); +}; diff --git a/dmtcp/include/mcmini/coordinator/restore-objects.hpp b/dmtcp/include/mcmini/coordinator/restore-objects.hpp new file mode 100644 index 00000000..6f70f09b --- /dev/null +++ b/dmtcp/include/mcmini/coordinator/restore-objects.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/dmtcp/include/mcmini/defines.h b/dmtcp/include/mcmini/defines.h new file mode 100644 index 00000000..15af3062 --- /dev/null +++ b/dmtcp/include/mcmini/defines.h @@ -0,0 +1,46 @@ +#pragma once + +#include + +#define MCMINI_INLINE +#define MCMINI_LIBRARY_ENTRY_POINT +#define MCMINI_EXPORT __attribute__((visibility(default))) +#define MCMINI_PRIVATE __attribute__((visibility(hidden))) + +#ifdef __cplusplus +#define MCMINI_THREAD_LOCAL thread_local +#else +#define MCMINI_THREAD_LOCAL _Thread_local +#endif + +#ifdef __cplusplus +#define MCMINI_NO_RETURN [[noreturn]] +#else +#define MCMINI_NO_RETURN __attribute__((__noreturn__)) +#endif + +#define MAX_TOTAL_TRANSITIONS_IN_PROGRAM (1500u) +#define MAX_TOTAL_STATES_IN_STATE_STACK (MAX_TOTAL_TRANSITIONS_IN_PROGRAM + 1u) +#define MAX_TOTAL_THREADS_IN_PROGRAM (20u) +#define MAX_TOTAL_VISIBLE_OBJECTS_IN_PROGRAM (10000u) +#define MAX_TOTAL_STATE_OBJECTS_IN_PROGRAM \ + (MAX_TOTAL_THREADS_IN_PROGRAM + MAX_TOTAL_VISIBLE_OBJECTS_IN_PROGRAM) + +#define THREAD_SHM_OFFSET (128) + +typedef uint16_t runner_id_t; +typedef uint64_t trid_t; +#define RUNNER_ID_MAX UINT16_MAX +#define RID_MAIN_THREAD ((runner_id_t)0) +#define RID_INVALID ((runner_id_t)-1) +#define RID_CHECKPOINT_THREAD ((runner_id_t)-2) +#define RID_PTHREAD_CREATE_FAILED ((runner_id_t)-3) + +#define FORK_IS_CHILD_PID(pid) ((pid) == 0) +#define FORK_IS_PARENT_PID(pid) (!(FORK_IS_CHILD_PID(pid))) + +#define PTHREAD_SUCCESS (0) +#define SEM_FLAG_SHARED (1) + +typedef void *(*thread_routine)(void *); +typedef void (*free_function)(void *); diff --git a/docs/design/include/mcmini/forwards.hpp b/dmtcp/include/mcmini/forwards.hpp similarity index 65% rename from docs/design/include/mcmini/forwards.hpp rename to dmtcp/include/mcmini/forwards.hpp index b4448241..7bd96f05 100644 --- a/docs/design/include/mcmini/forwards.hpp +++ b/dmtcp/include/mcmini/forwards.hpp @@ -1,5 +1,8 @@ #pragma once +class coordinator; +class model_to_system_map; + namespace model { class state; class mutable_state; @@ -8,7 +11,7 @@ class state_sequence; class transition; } // namespace model -class coordinator; -class model_to_system_map; - -// TODO: Place all forward declarations here +namespace real_world { +class local_linux_process; +class fork_process_source; +} // namespace real_world diff --git a/dmtcp/include/mcmini/lib/entry.h b/dmtcp/include/mcmini/lib/entry.h new file mode 100644 index 00000000..ac3738b7 --- /dev/null +++ b/dmtcp/include/mcmini/lib/entry.h @@ -0,0 +1,10 @@ +#pragma once + +#include "mcmini/defines.h" + +extern volatile void *global_shm_start; +extern MCMINI_THREAD_LOCAL runner_id_t tid_self; + +void mc_prevent_addr_randomization(void); +void mc_install_sig_handlers(void); +runner_id_t mc_register_this_thread(void); diff --git a/dmtcp/include/mcmini/lib/log.h b/dmtcp/include/mcmini/lib/log.h new file mode 100644 index 00000000..9d1d570a --- /dev/null +++ b/dmtcp/include/mcmini/lib/log.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +#define MCMINI_LOG_MINIMUM_LEVEL (MCMINI_LOG_DEBUG) + +enum log_level { + MCMINI_LOG_VERBOSE, + MCMINI_LOG_DEBUG, + MCMINI_LOG_INFO, + MCMINI_LOG_WARNING, + MCMINI_LOG_ERROR, + MCMINI_LOG_FATAL, + MCMINI_LOG_DISABLE +}; + +#define RELATIVE_PATH(file) ((char*)(file) + (sizeof(LOGGING_ROOT) - 1)) +#define __RELATIVE_FILE__ RELATIVE_PATH(__FILE__) + +#define log_verbose(...) \ + mcmini_log(MCMINI_LOG_VERBOSE, __RELATIVE_FILE__, __LINE__, __VA_ARGS__) +#define log_debug(...) mcmini_log(MCMINI_LOG_DEBUG, __RELATIVE_FILE__, __LINE__, __VA_ARGS__) +#define log_info(...) mcmini_log(MCMINI_LOG_INFO, __RELATIVE_FILE__, __LINE__, __VA_ARGS__) +#define log_warn(...) mcmini_log(MCMINI_LOG_WARNING, __RELATIVE_FILE__, __LINE__, __VA_ARGS__) +#define log_error(...) mcmini_log(MCMINI_LOG_ERROR, __RELATIVE_FILE__, __LINE__, __VA_ARGS__) +#define log_fatal(...) mcmini_log(MCMINI_LOG_FATAL, __RELATIVE_FILE__, __LINE__, __VA_ARGS__) + +void mcmini_log_set_level(int level); +void mcmini_log_toggle(bool enable); +void mcmini_log(int level, const char *file, int line, const char *fmt, ...); diff --git a/dmtcp/include/mcmini/lib/sig.h b/dmtcp/include/mcmini/lib/sig.h new file mode 100644 index 00000000..94670055 --- /dev/null +++ b/dmtcp/include/mcmini/lib/sig.h @@ -0,0 +1,7 @@ +#pragma once + +#include +#include + +bool is_bad_signal(int signo); +void mc_template_receive_sigchld(int sig, siginfo_t *, void *); diff --git a/dmtcp/include/mcmini/lib/template.h b/dmtcp/include/mcmini/lib/template.h new file mode 100644 index 00000000..8df67f97 --- /dev/null +++ b/dmtcp/include/mcmini/lib/template.h @@ -0,0 +1,5 @@ +#pragma once + +void mc_exit_main_thread_in_child(void); +void mc_prepare_new_child_process(pid_t ppid_before_fork, pid_t model_checker_pid); +void mc_template_process_loop_forever(pid_t (*)(void)); diff --git a/dmtcp/include/mcmini/log/filter.hpp b/dmtcp/include/mcmini/log/filter.hpp new file mode 100644 index 00000000..a1fc8b87 --- /dev/null +++ b/dmtcp/include/mcmini/log/filter.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include + +#include "mcmini/log/severity_level.hpp" + +namespace logging { +class filter { + public: + filter() = default; + filter(std::unordered_map mapping) + : registered_subsystems(std::move(mapping)) {} + + public: + virtual bool apply(const std::string &subsystem, severity_level) const = 0; + + protected: + std::unordered_map registered_subsystems; + std::vector> registered_regexes; +}; + +class whitelist_filter : public filter { + public: + whitelist_filter() = default; + whitelist_filter(whitelist_filter &&) = default; + whitelist_filter(const whitelist_filter &) = default; + whitelist_filter &operator=(whitelist_filter &&) = default; + whitelist_filter &operator=(const whitelist_filter &) = default; + whitelist_filter(std::unordered_map mapping) + : filter(std::move(mapping)) {} + bool apply(const std::string &, severity_level) const override; +}; + +class blacklist_filter : public filter { + public: + blacklist_filter() = default; + blacklist_filter(blacklist_filter &&) = default; + blacklist_filter(const blacklist_filter &) = default; + blacklist_filter &operator=(blacklist_filter &&) = default; + blacklist_filter &operator=(const blacklist_filter &) = default; + blacklist_filter(std::unordered_map mapping) + : filter(std::move(mapping)) {} + bool apply(const std::string &, severity_level) const override; +}; + +class composite_filter : public filter { + public: + composite_filter(const std::string &ini_file); + composite_filter(const std::vector &whitelists, + const std::vector &blacklists) + : whitelists(whitelists), blacklists(blacklists) {} + bool apply(const std::string &subsystem, severity_level sl) const override { + for (const whitelist_filter &wl : whitelists) + if (!wl.apply(subsystem, sl)) return false; + for (const blacklist_filter &bl : blacklists) + if (!bl.apply(subsystem, sl)) return false; + return true; + } + + private: + std::vector whitelists; + std::vector blacklists; +}; + +class severity_filter : public filter { + public: + severity_filter(severity_level sev) : minimum_level(sev) {} + bool apply(const std::string &, severity_level severity) const override { + return severity >= minimum_level; + } + + private: + severity_level minimum_level; +}; + +class permissive_filter : public filter { + public: + bool apply(const std::string &, severity_level) const override { + return true; + } +}; + +} // namespace logging diff --git a/dmtcp/include/mcmini/log/log_control.hpp b/dmtcp/include/mcmini/log/log_control.hpp new file mode 100644 index 00000000..61b6354b --- /dev/null +++ b/dmtcp/include/mcmini/log/log_control.hpp @@ -0,0 +1,116 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "mcmini/constants.hpp" +#include "mcmini/log/filter.hpp" +#include "mcmini/log/severity_level.hpp" +#include "mcmini/misc/rwlock.hpp" + +namespace logging { +class log_control { + public: + using subsystem_list = std::unordered_map; + + /// @brief Permit all logs from all subsystems + /// + /// This is the most permissive log. All log messages will succeed at + /// the _global_ level. Sink-specific filtering may still apply if the + /// sinks have been configured that way. + void allow_everything(); + + /// @brief Permit logs only from those subsystems whose severity is at + /// least as important as that in the mapping + /// + /// Whitelist logging permits the logs from only those subsystems on + /// the list to successfully be processed by the sink frontends. + /// Filtering is interpreted as follows: + /// + /// Given a log from subsystem "A" with severity "B", if the whitelist + /// does not contain subsystem "A", the log is dropped; otherwise, if + /// the severity level "B" is less important than the severity + /// permitted for the subsytem, the log is again dropped. If both + /// checks pass, the log is forwarded to the sinks registered with the + /// controller. + void whitelist(const subsystem_list &mapping) { + whitelist(whitelist_filter(std::move(mapping))); + } + + /// @brief Permit logs only from those subsystems which pass the given filter. + /// + /// Whitelist logging permits the logs from only those subsystems on + /// the list to successfully be processed by the sink frontends. + /// Filtering is interpreted as follows: + /// + /// Given a log from subsystem "A" with severity "B", if the whitelist + /// does not contain subsystem "A", the log is dropped; otherwise, if + /// the severity level "B" is less important than the severity + /// permitted for the subsytem, the log is tested against any regex patterns + /// matched with the filter. The most permissive severity level is chosen and + /// compared with the severity of the log record. If thses checks pass, the + /// log is forwarded to the sinks registered with the controller. + void whitelist(whitelist_filter wl); + + /// @brief Permit all logs except for those from subsystems with a + /// severity less important than that permitted by the subsystem. + /// + /// Blacklist logging prevents logs from those subsystems on the list. + /// Any log that is not on the blacklist will be successfully + /// processed by the sink frontends. Filtering is interpreted as + /// follows: + /// + /// Given a log from subsystem "A" with severity "B", if the blacklist + /// does not contain subsystem "A", the log is processed; otherwise, + /// if the severity level "B" is at least as important as the severity + /// permitted for the subsytem, the log is again processed. If both + /// checks fail, the log is dropped. + void blacklist(const subsystem_list &mapping) { + blacklist(blacklist_filter(std::move(mapping))); + } + + /// @brief Permit all logs except for those from subsystems with a + /// severity less important than that permitted by the subsystem. + /// + /// Blacklist logging prevents logs from those subsystems on the list. + /// Any log that is not on the blacklist will be successfully + /// processed by the sink frontends. Filtering is interpreted as + /// follows: + /// + /// Given a log from subsystem "A" with severity "B", if the blacklist + /// does not contain subsystem "A", the log is processed; otherwise, + /// if the severity level "B" is at least as important as the severity + /// permitted for the subsytem, the log is again processed. If both + /// checks fail, the log is dropped. + void blacklist(blacklist_filter bl); + + /// @brief Permit logs from all subsystems whose severity level is at + /// least as great as the level provided. + /// + /// This is a convience method that whitelists all subsystems whose + /// logging severity level is at least as high as the provided one. + /// This is more efficient than simply listing all subsytems in a + /// whitelist as only the severity level need be checked. + void allow_everything_over(severity_level); + + public: + void set_filter(filter *filt); + + public: + inline static log_control &instance() { + static log_control lc; + return lc; + } + void log_raw(const std::string &instance, const std::string &subsystem, + const std::string &message, const severity_level severity, + const char *file = __FILE__, int line = __LINE__); + + private: + RWLock filter_lock; + std::unique_ptr active_filter; +}; +} // namespace logging diff --git a/dmtcp/include/mcmini/log/logger.hpp b/dmtcp/include/mcmini/log/logger.hpp new file mode 100644 index 00000000..09f94e0c --- /dev/null +++ b/dmtcp/include/mcmini/log/logger.hpp @@ -0,0 +1,124 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "mcmini/log/log_control.hpp" +#include "mcmini/log/severity_level.hpp" +#include "mcmini/model/program.hpp" + +#define log_severity(logger, severity) \ + logger.make_stream(__FILE__, __LINE__) << severity +#define log_very_verbose(logger) \ + log_severity(logger, logging::severity_level::very_verbose) +#define log_verbose(logger) \ + log_severity(logger, logging::severity_level::verbose) +#define log_debug(logger) log_severity(logger, logging::severity_level::debug) +#define log_info(logger) log_severity(logger, logging::severity_level::info) +#define log_unexpected(logger) \ + log_severity(logger, logging::severity_level::unexpected) +#define log_error(logger) log_severity(logger, logging::severity_level::error) +#define log_critical(logger) \ + log_severity(logger, logging::severity_level::critical) +#define log_abort(logger) log_severity(logger, logging::severity_level::abort) + +namespace logging { +class logger { + public: + logger() = default; + logger(const std::string &subsystem) : subsystem(subsystem) {} + + public: + template + void set_instance(T *instance) { + std::stringstream strm; + strm << "0x" << std::hex << reinterpret_cast(instance); + this->instance = strm.str(); + } + + public: + struct stream { + public: + ~stream() { flush(); } + template + stream &operator<<(const T &value) { + ostream << value; + return *this; + } + + template + stream &operator<<(const std::unordered_set &set) { + ostream << "["; + for (const T &t : set) ostream << t << ", "; + ostream << "]"; + return *this; + } + + stream &operator<<(const model::program &prog) { + prog.dump_state(this->ostream); + return *this; + } + + stream &operator<<(severity_level severity) { + if (severity != current_severity) { + flush(); + this->current_severity = severity; + } + // Ignore otherwise + return *this; + } + + private: + stream &operator=(stream &&) = default; + stream(stream &&) = default; + explicit stream(logger *log, const char *file = __FILE__, + int line = __LINE__) + : log(log), file(file), line(line) {} + void flush() { + if (ostream.str() != "") { + this->log->log_raw(ostream.str(), current_severity, file, line); + this->ostream = std::stringstream(); + }; + } + + private: + logger *log; + const char *file; + int line; + + severity_level current_severity = severity_level::info; + std::stringstream ostream; + + private: + friend class logger; + }; + + public: + template + stream operator<<(const T &item) { + return make_stream(__FILE__, __LINE__); + } + + stream make_stream(const char *file, int line) { + return logging::logger::stream(this, file, line); + } + + public: + inline void log_raw(const std::string &message, severity_level severity, + const char *file = __FILE__, int line = __LINE__) { + log_control::instance().log_raw(instance, subsystem, message, severity, + file, line); + } + + private: + std::string instance; + std::string subsystem; + + private: + friend struct stream; +}; +} // namespace logging diff --git a/dmtcp/include/mcmini/log/severity_level.hpp b/dmtcp/include/mcmini/log/severity_level.hpp new file mode 100644 index 00000000..27220642 --- /dev/null +++ b/dmtcp/include/mcmini/log/severity_level.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include + +namespace logging { +enum severity_level : uint32_t { + nothing, + very_verbose, + verbose, + debug, + info, + unexpected, + error, + critical, + abort, + everything +}; +} // namespace logging diff --git a/dmtcp/include/mcmini/log/severity_level_parser.hpp b/dmtcp/include/mcmini/log/severity_level_parser.hpp new file mode 100644 index 00000000..747eb5d5 --- /dev/null +++ b/dmtcp/include/mcmini/log/severity_level_parser.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +#include "mcmini/log/severity_level.hpp" + +namespace logging { +inline severity_level parse_severity(const std::string& levelStr) { + std::string s(levelStr); + std::transform(s.begin(), s.end(), s.begin(), ::tolower); + if (s == "nothing") return nothing; + if (s == "very-verbose" || s == "vv") return very_verbose; + if (s == "verbose" || s == "v") return verbose; + if (s == "debug") return debug; + if (s == "info") return info; + if (s == "unexpected") return unexpected; + if (s == "error") return error; + if (s == "critical") return critical; + if (s == "abort") return abort; + if (s == "everything") return everything; + return nothing; +} +} // namespace logging diff --git a/dmtcp/include/mcmini/mcmini.h b/dmtcp/include/mcmini/mcmini.h new file mode 100644 index 00000000..ecc6afb8 --- /dev/null +++ b/dmtcp/include/mcmini/mcmini.h @@ -0,0 +1,23 @@ +#pragma once + +#ifdef __cplusplus +#error "This file should only be included in `libmcmini.so`" +#endif + +#include "mcmini/common/exit.h" +#include "mcmini/common/shm_config.h" +#include "mcmini/lib/entry.h" +#include "mcmini/lib/sig.h" +#include "mcmini/lib/template.h" +#include "mcmini/lib/log.h" +#include "mcmini/mem.h" +#include "mcmini/plugins.h" +#include "mcmini/real_world/mailbox/runner_mailbox.h" +#include "mcmini/real_world/process/template_process.h" +#include "mcmini/spy/checkpointing/alloc.h" +#include "mcmini/spy/checkpointing/record.h" +#include "mcmini/spy/checkpointing/transitions.h" +#include "mcmini/spy/intercept/interception.h" +#include "mcmini/spy/intercept/wrappers.h" +#include "mcmini/Thread_queue.h" + diff --git a/dmtcp/include/mcmini/mcmini.hpp b/dmtcp/include/mcmini/mcmini.hpp new file mode 100644 index 00000000..32894838 --- /dev/null +++ b/dmtcp/include/mcmini/mcmini.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "mcmini/forwards.hpp" +#include "mcmini/misc/asserts.hpp" +#include "mcmini/model/transition.hpp" +#include "mcmini/model/visible_object.hpp" +#include "mcmini/model_checking/algorithm.hpp" +#include "mcmini/real_world/process.hpp" +#include "mcmini/real_world/process_source.hpp" +#include "mcmini/signal.hpp" +#include "mcmini/misc/cond/cond_var_arbitrary_policy.hpp" +#include "mcmini/misc/cond/cond_var_default_policy.hpp" +#include "mcmini/misc/cond/cond_var_policy.hpp" +#include "mcmini/misc/cond/cond_var_single_grp_policy.hpp" +#include "mcmini/misc/cond/cond_var_wakegroup.hpp" diff --git a/dmtcp/include/mcmini/mem.h b/dmtcp/include/mcmini/mem.h new file mode 100644 index 00000000..0d2154ca --- /dev/null +++ b/dmtcp/include/mcmini/mem.h @@ -0,0 +1,14 @@ +#pragma once +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// memcpy implementation but with volatile memory +volatile void *memset_v(volatile void *, int ch, size_t n); +volatile void *memcpy_v(volatile void *, const volatile void *, size_t n); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/docs/design/include/mcmini/misc/append-only.hpp b/dmtcp/include/mcmini/misc/append-only.hpp similarity index 87% rename from docs/design/include/mcmini/misc/append-only.hpp rename to dmtcp/include/mcmini/misc/append-only.hpp index c5ffb68c..b764fc02 100644 --- a/docs/design/include/mcmini/misc/append-only.hpp +++ b/dmtcp/include/mcmini/misc/append-only.hpp @@ -16,6 +16,7 @@ struct append_only { using const_iterator = typename std::vector::const_iterator; append_only() = default; + append_only(size_type count) : contents(count) {} append_only(std::vector &&contents) : contents(std::move(contents)) {} append_only(append_only &&) = default; append_only(const append_only &) = default; @@ -35,6 +36,12 @@ struct append_only { const_reference at(size_t i) const { return contents.at(i); } iterator begin() { return this->contents.begin(); } iterator end() { return this->contents.end(); } + iterator erase(iterator first, iterator last) { + return this->contents.erase(first, last); + } + iterator erase(const_iterator first, const_iterator last) { + return this->contents.erase(first, last); + } const_iterator begin() const { return this->contents.cbegin(); } const_iterator end() const { return this->contents.cend(); } const_iterator cbegin() const { return this->contents.cbegin(); } @@ -43,4 +50,4 @@ struct append_only { const_reference operator[](size_type pos) const { return this->contents[pos]; } -}; \ No newline at end of file +}; diff --git a/docs/design/include/mcmini/misc/asserts.hpp b/dmtcp/include/mcmini/misc/asserts.hpp similarity index 100% rename from docs/design/include/mcmini/misc/asserts.hpp rename to dmtcp/include/mcmini/misc/asserts.hpp diff --git a/dmtcp/include/mcmini/misc/cond/cond_var_arbitrary_policy.hpp b/dmtcp/include/mcmini/misc/cond/cond_var_arbitrary_policy.hpp new file mode 100644 index 00000000..e73eeba8 --- /dev/null +++ b/dmtcp/include/mcmini/misc/cond/cond_var_arbitrary_policy.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "cond_var_single_grp_policy.hpp" +// #include "mcmini/spy/checkpointing/objects.h" + +struct ConditionVariableArbitraryPolicy : + public ConditionVariableSingleGroupPolicy { + virtual void receive_signal_message() override; + virtual bool has_waiters() const override; + void add_to_wake_groups (const std::vector& threads) override; + void add_waiter_with_state(runner_id_t tid, condition_variable_status state) override; + condition_variable_status get_thread_cv_state(runner_id_t tid) override; + void update_thread_cv_state(runner_id_t tid, condition_variable_status state) override; + ConditionVariablePolicy* clone() const override; + std::deque return_wait_queue() const ; + std::vector return_wake_groups() const ; + + // protected: + // std::unordered_map threads_with_states; +}; + +using condition_variable_arbitrary_policy = + ConditionVariableArbitraryPolicy; \ No newline at end of file diff --git a/dmtcp/include/mcmini/misc/cond/cond_var_default_policy.hpp b/dmtcp/include/mcmini/misc/cond/cond_var_default_policy.hpp new file mode 100644 index 00000000..43251467 --- /dev/null +++ b/dmtcp/include/mcmini/misc/cond/cond_var_default_policy.hpp @@ -0,0 +1,24 @@ +#include "cond_var_wakegroup.hpp" +#include "cond_var_policy.hpp" + +#include +#include +#include + +/* + * Threads are queued by the wakeup policy into *wake groups*. A + * wake group describes a mutually-exclusive set of threads which + * can escape the condition variable. See the documentation for + * `mcmini::WakeGroup` for more details. + */ +struct ConditionVariableDefaultPolicy : + public ConditionVariablePolicy { + virtual void receive_broadcast_message() override; + virtual bool thread_can_exit(runner_id_t tid) const override; + virtual void wake_thread(runner_id_t tid) override; + virtual bool has_waiters() const; + + protected: + std::unordered_set broadcast_eligible_threads; + std::vector wake_groups; + }; \ No newline at end of file diff --git a/dmtcp/include/mcmini/misc/cond/cond_var_policy.hpp b/dmtcp/include/mcmini/misc/cond/cond_var_policy.hpp new file mode 100644 index 00000000..a83544ac --- /dev/null +++ b/dmtcp/include/mcmini/misc/cond/cond_var_policy.hpp @@ -0,0 +1,155 @@ +#pragma once + +#include "mcmini/defines.h" +#include "cond_var_wakegroup.hpp" +#include "mcmini/spy/checkpointing/objects.h" + + +#include +#include +#include + + +/** + * @brief A state machine determining how threads waiting on a + * condition variable are selected for being woken + * + * A `ConditionVariablePolicy` encapsulates a particular behavior a + * condition variable could exhibit. Abstractly, condition variables + * manage sleeping threads and change each thread's eligibility for + * waking up and "escaping" the condition variable as the condition + * variable receives signals and broadcast messages. A policy + * determines, for each sleeping thread and for each sequence of + * thread additions and signals/broadcasts to a condition variable, + * whether a given thread is allowed to consume a signal. A policy + * emulates the true runtime behavior of threads interacting with + * condition variables, viz. the atomic consumption of a + * signal/broadcast message + */ + +class ConditionVariablePolicy { +public: + /** + * @brief Simulate sending a signal to the condition variable + * implementing this policy + * + * When signaling, it allows a single thread to awaken from the + * condition variable. Which thread is woken depends on the + * underlying behavior of the condition variable as described by the + * condition variable's wakeup policy. When signal the policy is + * made aware of a signal sent to a condition variable, any internal + * state should be updated to properly reflect the fact that the + * condition variable has been signaled. + * + * @note for condition variables that support spurious wake-ups, it + * is possible for a thread to wake up without the condition + * variable first receiving a signal/broadcast. + */ + virtual void receive_signal_message() = 0; + + /** + * @brief Simulate sending a broadcast message to the condition + * variable implementing this policy + * + * When a condition variable is sent a broadcast message, it wakes + * all sleeping threads. The order in which threads actually exit + * the condition variable depends on the underlying behavior of the + * condition variable as described by the condition variable's + * wakeup policy. When the signal policy is made aware of a + * broadcast sent to a condition variable, any internal state should + * be updated to properly reflect the fact that the condition + * variable has been signaled. + * + * @note for condition variables that support spurious wake-ups, it + * is possible for a thread to wake up without the condition + * variable first receiving a broadcast. + */ + virtual void receive_broadcast_message() = 0; + + /** + * @brief Whether or not the given thread can currently escape a + * condition variable implementing this policy + * + * The conditions which may affect whether or not a thread is + * allowed to exit from the condition variable are arbitrary. + * Each policy decides, based on any internal state managed by the + * policy tracking changes to the state of the condition variable, + * under what conditions a thread is enabled + * + * @note whether or not a thread is enabled can change based on the + * number of and/or relative ordering of signals/broadcasts sent to + * the condition variable. + * + * @param tid the identity of the thread to check + * @return true if the thread can exit from the condition variable + * @return false if the thread is not allowed to wake from the + * condition variable + */ + virtual bool thread_can_exit(runner_id_t tid) const = 0; + + /** + * @brief Removes the given thread from the management + * of the condition variable + * + * After calling `wakeThread()` on the policy, the supplied thread + * is no longer managed by the policy and all references to the + * thread are removed from the condition variable. Conceptually, + * the thread has now "woken" and has "escaped" the condition + * variable + * + * @note after the given thread has woken, the criterion for + * whether a thread is allowed to exit may have changed. Thus the + * return type + * + * @param tid the thread to wake up and allow to escape from the + * condition variable + * + * @throws invalid_thread_wakeup_exception if the thread is now + * allowed to exit the condition variable + */ + virtual void wake_thread(runner_id_t tid) = 0; + + /** + * @brief Mark a new thread as sleeping on the condition variable + * + * @param tid the thread to mark as sleeping on the condition + * variable + */ + virtual void add_waiter(runner_id_t tid) = 0; + + virtual bool has_waiters() const = 0; + + virtual ConditionVariablePolicy* clone() const = 0; + + virtual ~ConditionVariablePolicy() = default; + + virtual std::deque return_wait_queue() const = 0; + virtual std::vector return_wake_groups() const = 0; + + virtual void add_to_wake_groups (const std::vector& threads) = 0; + + virtual void add_waiter_with_state(runner_id_t tid, condition_variable_status state) = 0; + + virtual condition_variable_status get_thread_cv_state(runner_id_t tid) = 0; + + virtual void update_thread_cv_state(runner_id_t tid, condition_variable_status state) = 0; + + struct invalid_thread_addition : public std::exception { + const char * + what() const noexcept override + { + return "Attempted to put to sleep a thread already sleeping on " + "a condition variable"; + } + }; + + struct invalid_thread_wakeup_exception : public std::exception { + const char * + what() const noexcept override + { + return "Attempted to wake an ineligible thread on a condition " + "variable"; + } + }; +}; + diff --git a/dmtcp/include/mcmini/misc/cond/cond_var_single_grp_policy.hpp b/dmtcp/include/mcmini/misc/cond/cond_var_single_grp_policy.hpp new file mode 100644 index 00000000..46a07582 --- /dev/null +++ b/dmtcp/include/mcmini/misc/cond/cond_var_single_grp_policy.hpp @@ -0,0 +1,17 @@ +#include "cond_var_default_policy.hpp" +#include "mcmini/spy/checkpointing/objects.h" + +#include +#include + + struct ConditionVariableSingleGroupPolicy : + public ConditionVariableDefaultPolicy { + virtual void receive_broadcast_message() override; + virtual void wake_thread(runner_id_t tid) override; + virtual void add_waiter(runner_id_t tid) override; + virtual bool has_waiters() const; + + protected: + std::deque wait_queue; + std::unordered_map threads_with_states; + }; diff --git a/dmtcp/include/mcmini/misc/cond/cond_var_wakegroup.hpp b/dmtcp/include/mcmini/misc/cond/cond_var_wakegroup.hpp new file mode 100644 index 00000000..9da27ecf --- /dev/null +++ b/dmtcp/include/mcmini/misc/cond/cond_var_wakegroup.hpp @@ -0,0 +1,75 @@ +/* File copied from classic mcmini */ +#pragma once + +#include "mcmini/defines.h" + +#include +#include +#include + + + +/** + * @brief A set of threads eligible to escape a condition + * variable + * + * A *wake group* is a set of threads for which the transition + * `pthread_cond_wait()` is enabled for those threads by virtue of + * having been sleeping on the condition variable at the time a + * signal/broadcast was sent to the condition variable. + * + * A wake group represents a mutually-exclusive group of threads which + * can consume a signal. The semantics of a condition variable are + * such that signal consumption and the waking of a thread are atomic; + * that is, the act of consuming the signal and waking the threads is + * atomic. + * + * Only a single thread can wake from a wake group created by a + * signal message, while all threads in a broadcast wake group are + * allowed to wakeup + */ + +struct WakeGroup final : public std::vector { +public: + + WakeGroup(WakeGroup &&) = default; + WakeGroup(const WakeGroup &) = default; + WakeGroup &operator=(const WakeGroup &) = default; + WakeGroup &operator=(WakeGroup &&) = default; + + explicit WakeGroup(const std::vector &vec) + : std::vector(vec) + {} + + explicit WakeGroup(std::initializer_list list) + : std::vector(std::move(list)) + {} + + template + WakeGroup(InputIt first, InputIt last) + : std::vector(first, last) + {} + + /** + * @brief Whether or not the given thread is contained in the group + * + * @param tid the thread to test for membership in the wake group + * @return true if the thread is contained in the wake group + * @return false if the thread is not contained in the wake group + */ + bool contains(runner_id_t tid) const; + + /** + * @brief Removes the given thread _tid_ from the group + * + * @param tid the thread to remove from the group + */ + void remove_candidate_thread(runner_id_t tid); + + /** + * @brief + * + * @param tid + */ + void wake_thread(runner_id_t tid); +}; diff --git a/dmtcp/include/mcmini/misc/ddt.hpp b/dmtcp/include/mcmini/misc/ddt.hpp new file mode 100644 index 00000000..f8db3476 --- /dev/null +++ b/dmtcp/include/mcmini/misc/ddt.hpp @@ -0,0 +1,191 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +template +struct copy_cv { + using type = Target; +}; + +template +struct copy_cv { + using type = typename std::add_const::type; +}; + +template +struct copy_cv { + using type = typename std::add_volatile::type; +}; + +template +struct copy_cv { + using type = typename std::add_cv::type; +}; + +template +struct double_dispatch_member_function_table; + +template +struct double_dispatch_member_function_table { + private: + using opaque_callback = ReturnType (InterfaceType::*)(InterfaceType*, + Args...); + using stored_callback = ReturnType (*)(InterfaceType*, InterfaceType*, + opaque_callback, Args...); + + std::unordered_map< + std::type_index, + std::unordered_map>> + internal_table; + + std::unordered_map> + interface_member_function_table; + + // In the intermediate + // See "https://en.cppreference.com/w/cpp/language/reinterpret_cast" + // """ + // 10) A pointer to member function can be converted to pointer to a + // different member function of a different type. Conversion back to the + // original type yields the original value, otherwise the resulting + // pointer cannot be used safely. + // """ + // The reinterpret_cast<> here is used to restore the specific callback + // function specified at registration-time. Since function will only be + // invoked if the RTTI runtime type-check lookup in the `call`, the cast is + // safe. Furthermore, since the reinterpret_cast restores the original + // pointer-to-member function pointer, subsequently invoking the callback + // through `well_defined_handle` is defined. + template + static ReturnType casting_function(InterfaceType* t1, InterfaceType* t2, + opaque_callback callback, Args... args) { + auto well_defined_handle = + reinterpret_cast>(callback); + return (static_cast(t1)->*well_defined_handle)(static_cast(t2), + std::forward(args)...); + } + + template + static ReturnType casting_function_reverse(InterfaceType* t1, + InterfaceType* t2, + opaque_callback callback, + Args... args) { + auto well_defined_handle = + reinterpret_cast>(callback); + return (static_cast(t2)->*well_defined_handle)(static_cast(t1), + std::forward(args)...); + } + + template + static ReturnType casting_function_interface_table(InterfaceType* t1, + InterfaceType* t2, + opaque_callback callback, + Args... args) { + auto well_defined_handle = + reinterpret_cast>(callback); + return (static_cast(t1)->*well_defined_handle)(t2, + std::forward(args)...); + } + + public: + static_assert( + std::is_polymorphic::value, + "InterfaceType must be a polymorphic type for double dispatch table " + "to function properly. See the example and documentation for typeid() on " + "cppreference.com (https://en.cppreference.com/w/cpp/language/typeid) " + "for more details on why this is necessary"); + + template + using member_function_callback = ReturnType (T1::*)(T2*, Args...) const; + + template + using interface_function_callback = ReturnType (T1::*)(InterfaceType*, + Args...) const; + template + void register_dd_entry(interface_function_callback callback) { + static_assert(std::is_base_of::value, + "T must be a subclass of InterfaceType"); + if (interface_member_function_table.count(std::type_index(typeid(T))) > 0) { + throw std::runtime_error("A function is already registered for (" + + std::string(typeid(T).name()) + ")"); + } + interface_member_function_table[std::type_index(typeid(T))] = + std::make_pair(casting_function_interface_table, + reinterpret_cast(callback)); + } + + template + void register_dd_entry(member_function_callback callback) { + static_assert(std::is_base_of::value, + "T1 must be a subclass of InterfaceType"); + static_assert(std::is_base_of::value, + "T2 must be a subclass of InterfaceType"); + // In the intermediate + // See "https://en.cppreference.com/w/cpp/language/reinterpret_cast" + // """ + // 10) A pointer to member function can be converted to pointer to a + // different member function of a different type. Conversion back to the + // original type yields the original value, otherwise the resulting pointer + // cannot be used safely. + // """ + // The reinterpret_cast<> here is used to store the variable-type callback + // _callback_ registered for the particular combination + if (internal_table.count(std::type_index(typeid(T1))) && + internal_table.at(std::type_index(typeid(T1))) + .count(std::type_index(typeid(T2))) > 0) { + throw std::runtime_error( + "A function is already registered for the pair (" + + std::string(typeid(T1).name()) + ", " + + std::string(typeid(T2).name()) + ")"); + } + const auto unspecified_callback_handle = + reinterpret_cast(callback); + internal_table[std::type_index(typeid(T1))][std::type_index(typeid(T2))] = + std::make_pair(casting_function, unspecified_callback_handle); + + internal_table[std::type_index(typeid(T2))][std::type_index(typeid(T1))] = + std::make_pair(casting_function_reverse, + unspecified_callback_handle); + } + + ReturnType call_or(ReturnType fallback, InterfaceType* t1, InterfaceType* t2, + Args... args) const { + const auto t1_type = std::type_index(typeid(*t1)); + const auto t2_type = std::type_index(typeid(*t2)); + if (internal_table.count(t1_type) > 0) { + if (internal_table.at(t1_type).count(t2_type) > 0) { + const auto& pair = internal_table.at(t1_type).at(t2_type); + return pair.first(t1, t2, pair.second, std::forward(args)...); + } + } else if (interface_member_function_table.count(t1_type) > 0) { + const auto& pair = interface_member_function_table.at(t1_type); + return pair.first(t1, t2, pair.second, std::forward(args)...); + } else if (interface_member_function_table.count(t2_type) > 0) { + const auto& pair = interface_member_function_table.at(t2_type); + // NOTE: t2 should come before t1 here since + // `casting_function_interface_table` always casts its first argument + return pair.first(t2, t1, pair.second, std::forward(args)...); + } + return fallback; + } + + ReturnType call(InterfaceType* t1, InterfaceType* t2, Args... args) const { + const auto t1_type = std::type_index(typeid(*t1)); + const auto t2_type = std::type_index(typeid(*t2)); + if (internal_table.count(t1_type) > 0) { + if (internal_table.at(t1_type).count(t2_type) > 0) { + const auto& pair = internal_table.at(t1_type).at(t2_type); + return pair.first(t1, t2, pair.second, std::forward(args)...); + } + } + throw std::runtime_error( + "Attempted to invoke a method but missing runtime entry"); + } +}; diff --git a/dmtcp/include/mcmini/misc/extensions/memory.hpp b/dmtcp/include/mcmini/misc/extensions/memory.hpp new file mode 100644 index 00000000..73d67432 --- /dev/null +++ b/dmtcp/include/mcmini/misc/extensions/memory.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace extensions { + +template +void delete_all(ForwardIt first, ForwardIt last) { + for (; first != last; ++first) delete *first; +} + +} // namespace extensions diff --git a/docs/design/include/mcmini/misc/extensions/unique_ptr.hpp b/dmtcp/include/mcmini/misc/extensions/unique_ptr.hpp similarity index 100% rename from docs/design/include/mcmini/misc/extensions/unique_ptr.hpp rename to dmtcp/include/mcmini/misc/extensions/unique_ptr.hpp diff --git a/dmtcp/include/mcmini/misc/ini-parser.hpp b/dmtcp/include/mcmini/misc/ini-parser.hpp new file mode 100644 index 00000000..864b21f9 --- /dev/null +++ b/dmtcp/include/mcmini/misc/ini-parser.hpp @@ -0,0 +1,138 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using SectionMap = + std::unordered_map>; + +class IniParser : public SectionMap { + public: + IniParser() + : fieldSep_('='), escapeChar_('\\'), commentPrefixes_({"#", ";"}) {} + IniParser(const std::string &filename) : IniParser() { + std::ifstream in(filename); + if (!in) throw std::runtime_error("Cannot open file: " + filename); + safe_decode(in); + } + IniParser(std::istream &is) : IniParser() { decode(is); } + + private: + char fieldSep_; + char escapeChar_; + std::vector commentPrefixes_; + + private: + inline void trim(std::string &s) { + s.erase(s.begin(), std::find_if(s.begin(), s.end(), + [](int ch) { return !std::isspace(ch); })); + s.erase(std::find_if(s.rbegin(), s.rend(), + [](int ch) { return !std::isspace(ch); }) + .base(), + s.end()); + } + + inline void removeComments(std::string &line) { + for (const auto &prefix : commentPrefixes_) { + size_t pos = 0; + while (true) { + pos = line.find(prefix, pos); + if (pos == std::string::npos) break; + if (pos == 0 || line[pos - 1] != escapeChar_) { + line.erase(pos); + break; + } else { + line.erase(pos - 1, 1); + pos += prefix.size(); + } + } + } + } + + void safe_decode(std::istream &is) { + try { + decode(is); + } catch (const std::runtime_error &e) { + this->clear(); + throw e; + } + } + + void decode(std::istream &is) { + this->clear(); + SectionMap keyLineTracker; + std::unordered_map> + lineTracker; + std::string line, currentSection; + std::string lastNonRelativeSection; + int lineNo = 0; + while (std::getline(is, line)) { + ++lineNo; + removeComments(line); + trim(line); + if (line.empty()) continue; + if (line.front() == '[') { + size_t endPos = line.find(']'); + if (endPos == std::string::npos) + throw std::runtime_error("Line " + std::to_string(lineNo) + + ": section not closed"); + std::string secName = line.substr(1, endPos - 1); + trim(secName); + if (secName.empty()) + throw std::runtime_error("Line " + std::to_string(lineNo) + + ": empty section name"); + if (secName.front() == '.') { + if (!lastNonRelativeSection.empty()) + secName = lastNonRelativeSection + secName; + else + secName = secName.substr(1); + } else { + lastNonRelativeSection = secName; + } + currentSection = secName; + if (this->find(currentSection) == this->end()) { + (*this)[currentSection] = + std::unordered_map(); + lineTracker[currentSection] = std::unordered_map(); + } + continue; + } + size_t sepPos = line.find(fieldSep_); + if (sepPos == std::string::npos) + throw std::runtime_error("Line " + std::to_string(lineNo) + + ": no separator '" + + std::string(1, fieldSep_) + "' found"); + std::string key = line.substr(0, sepPos); + std::string value = line.substr(sepPos + 1); + trim(key); + trim(value); + if (key.empty()) + throw std::runtime_error("Line " + std::to_string(lineNo) + + ": empty key"); + if (currentSection.empty()) currentSection = "global"; + if (this->find(currentSection) == this->end()) { + (*this)[currentSection] = + std::unordered_map(); + lineTracker[currentSection] = std::unordered_map(); + } + if ((*this)[currentSection].count(key)) { + int firstLine = lineTracker[currentSection][key]; + std::stringstream ss; + ss << "The key `" << key << "` in section `" << currentSection + << "` has already been assigned at line " << firstLine << " to `" + << (*this)[currentSection][key] << "`. Overriding to `" << value + << "` at line " << lineNo << "...\n"; + throw std::runtime_error(ss.str()); + } else { + lineTracker[currentSection][key] = lineNo; + } + (*this)[currentSection][key] = value; + } + } +}; diff --git a/dmtcp/include/mcmini/misc/injective_function.hpp b/dmtcp/include/mcmini/misc/injective_function.hpp new file mode 100644 index 00000000..1095f8f4 --- /dev/null +++ b/dmtcp/include/mcmini/misc/injective_function.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +template +struct injective_function { + private: + struct invalid_insertion : public std::runtime_error { + invalid_insertion(const char* msg) : std::runtime_error(msg) {} + }; + + // INVARIANT: `forward` and `backward` are mirrors of one another. The keys of + // one are the values of the other. + std::unordered_map forward; + std::unordered_map backward; + + public: + using value_type = typename decltype(forward)::value_type; + using iterator = typename decltype(forward)::iterator; + using size_type = typename decltype(forward)::size_type; + + auto begin() -> decltype(forward.begin()) { return forward.begin(); } + auto end() -> decltype(forward.end()) { return forward.end(); } + auto begin() const -> decltype(forward.cbegin()) { return forward.cbegin(); } + auto end() const -> decltype(forward.cend()) { return forward.cend(); } + auto cbegin() -> decltype(forward.cbegin()) const { return forward.cbegin(); } + auto cend() -> decltype(forward.cend()) const { return forward.cend(); } + + bool empty() const { return this->forward.empty(); } + size_type size() const { return this->forward.size(); } + bool is_mapped(const RangeType& rt) const { return this->backward.count(rt); } + + RangeType& at(const DomainType& key) { return this->forward.at(key); } + const RangeType& at(const DomainType& key) const { + return this->forward.at(key); + } + DomainType& range_at(const RangeType& key) { return this->backward.at(key); } + const DomainType& range_at(const RangeType& key) const { + return this->backward.at(key); + } + size_type count_domain(const DomainType& key) const { + return this->forward.count(key); + } + size_type count_range(const RangeType& key) const { + return this->backward.count(key); + } + + std::pair insert(const value_type& vt) { + std::pair result = this->forward.insert(vt); + if (result.second && this->count_range(vt.second)) + throw invalid_insertion( + "A value in the domain already maps to this value. This would break " + "the one-to-one invariance of this mapping"); + if (result.second) { + this->backward.insert({vt.second, vt.first}); + } + return result; + } +}; diff --git a/dmtcp/include/mcmini/misc/rwlock.hpp b/dmtcp/include/mcmini/misc/rwlock.hpp new file mode 100644 index 00000000..4217dcf5 --- /dev/null +++ b/dmtcp/include/mcmini/misc/rwlock.hpp @@ -0,0 +1,54 @@ +#include + +class RWLock { + public: + // According to the man page of `pthread_rwlockattr_setkind_np(3)`: + // + // """ + // PTHREAD_RWLOCK_PREFER_READER_NP + // This is the default. A thread may hold multiple read + // locks; that is, read locks are recursive. + // """ + // Therefore, the RWLock is implicitly safe to use recursively either strictly + // as a reader or strictly as a writer. + RWLock() { pthread_rwlock_init(&lock_, nullptr); } + + ~RWLock() { pthread_rwlock_destroy(&lock_); } + RWLock(const RWLock&) = delete; + RWLock& operator=(const RWLock&) = delete; + + void lock_read() { pthread_rwlock_rdlock(&lock_); } + void unlock_read() { pthread_rwlock_unlock(&lock_); } + void lock_write() { pthread_rwlock_wrlock(&lock_); } + void unlock_write() { pthread_rwlock_unlock(&lock_); } + + class ReadGuard { + public: + explicit ReadGuard(RWLock& rwlock) : rwlock_(rwlock) { + rwlock_.lock_read(); + } + ~ReadGuard() { rwlock_.unlock_read(); } + ReadGuard(const ReadGuard&) = delete; + ReadGuard& operator=(const ReadGuard&) = delete; + + private: + RWLock& rwlock_; + }; + + class WriteGuard { + public: + explicit WriteGuard(RWLock& rwlock) : rwlock_(rwlock) { + rwlock_.lock_write(); + } + + ~WriteGuard() { rwlock_.unlock_write(); } + WriteGuard(const WriteGuard&) = delete; + WriteGuard& operator=(const WriteGuard&) = delete; + + private: + RWLock& rwlock_; + }; + + private: + pthread_rwlock_t lock_; +}; diff --git a/dmtcp/include/mcmini/model/config.hpp b/dmtcp/include/mcmini/model/config.hpp new file mode 100644 index 00000000..f492590d --- /dev/null +++ b/dmtcp/include/mcmini/model/config.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include + +#include "mcmini/defines.h" +#include "mcmini/log/severity_level.hpp" + +namespace model { + +struct config { + /** + * The maximum number of transitions that can be run + * by any single thread while running the model checker + */ + uint64_t max_thread_execution_depth; + + /** + * The trace id to stop the model checker at + * to print the contents of the transition stack + */ + trid_t target_trace_id; + + /** + * Whether model checking should halt at the first encountered deadlock + */ + bool stop_at_first_deadlock = false; + + /** + * Informs McMini that the target executable should be run under DMTCP with + * `libmcmini.so` configured in record mode. + */ + bool record_target_executable_only = false; + + /** + * Informs McMini that model checking with the checkpoint file should occur + * using `multithreaded_fork()` + a template process instead of + * `dmtcp_restart` to create new branches + */ + bool use_multithreaded_fork = false; + + /** + * The time between consecutive checkpoint images when `libmcmini.so` is + * running in record mode. + */ + std::chrono::seconds checkpoint_period; + + /** + * The path to the checkpoint file that should be used to begin deep debugging + * from. + */ + std::string checkpoint_file = ""; + + // Name of the target executable that will be model checked + std::string target_executable = ""; + + // A list of arguments to be passed to the executable on launch + std::vector target_executable_args; + + // Default severity level for logging. Overridden if a blacklist/whitelist + // file path is provided (TODO). + logging::severity_level global_severity_level = logging::severity_level::info; +}; +} // namespace model diff --git a/dmtcp/include/mcmini/model/defines.hpp b/dmtcp/include/mcmini/model/defines.hpp new file mode 100644 index 00000000..252c98fb --- /dev/null +++ b/dmtcp/include/mcmini/model/defines.hpp @@ -0,0 +1,4 @@ +#pragma once +#include + +using runner_id_t = uint16_t; diff --git a/dmtcp/include/mcmini/model/exception.hpp b/dmtcp/include/mcmini/model/exception.hpp new file mode 100644 index 00000000..0e404425 --- /dev/null +++ b/dmtcp/include/mcmini/model/exception.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +namespace model { + +/// @brief An error that is raised when a program encounters undefined behavior. +struct undefined_behavior_exception : public std::runtime_error { + undefined_behavior_exception(const char *what) : std::runtime_error(what) {} +}; +} // namespace model diff --git a/dmtcp/include/mcmini/model/objects/condition_variables.hpp b/dmtcp/include/mcmini/model/objects/condition_variables.hpp new file mode 100644 index 00000000..5bec5cf0 --- /dev/null +++ b/dmtcp/include/mcmini/model/objects/condition_variables.hpp @@ -0,0 +1,147 @@ +#pragma once + +#include "mcmini/misc/extensions/unique_ptr.hpp" +#include "mcmini/model/visible_object_state.hpp" +#include "mcmini/misc/cond/cond_var_arbitrary_policy.hpp" +#include "mcmini/model/objects/mutex.hpp" +#include "mcmini/Thread_queue.h" +#include + + +namespace model { +namespace objects { + +struct condition_variable : public model::visible_object_state { + public: + /* The four possible states for a condition variable */ + enum state { + cv_uninitialized, + cv_initialized, + cv_waiting, + cv_signaled, + cv_transitional, + cv_destroyed + }; + + private: + state current_state = state::cv_uninitialized; + bool hadwaiters; + mutable unsigned int numRemainingSpuriousWakeups = 0; + runner_id_t running_thread; + pthread_mutex_t* associated_mutex; + int waiting_count = 0; + int prev_waiting_count = 0; + int lost_wakeups = 0; + ConditionVariablePolicy* policy = new ConditionVariableArbitraryPolicy(); + + public: + condition_variable() = default; + ~condition_variable() = default; + condition_variable(const condition_variable &) = default; + condition_variable(state s) : current_state(s) {} + condition_variable(state s, ConditionVariablePolicy* p) : current_state(s), policy(p) {} + condition_variable(state s, int count) : current_state(s) {} + condition_variable(state s, runner_id_t tid, pthread_mutex_t* mutex, int count) + : current_state(s), running_thread(tid), associated_mutex(mutex), waiting_count(count){} + + condition_variable(state s, runner_id_t tid, pthread_mutex_t* mutex, int count, + const std::vector>& thread_states) + : current_state(s), running_thread(tid), associated_mutex(mutex), waiting_count(count) { + // Initialize the policy according to the states of the threads in waiting queue + for (const auto& thread_with_state : thread_states) { + if (thread_with_state.second == CV_PREWAITING || thread_with_state.second == CV_WAITING) { + this->policy->add_waiter_with_state(thread_with_state.first,thread_with_state.second); + } + } + std::vector signaled_threads; + for (const auto& thread_with_state : thread_states) { + if (thread_with_state.second == CV_SIGNALED) { + signaled_threads.push_back(thread_with_state.first); + } + } + if (!signaled_threads.empty()) { + // If there are any threads that have been signaled, we should + // add them to the wake groups in the policy. + this->policy->add_to_wake_groups(signaled_threads); + } + } + // ---- State Observation --- // + bool operator==(const condition_variable &other) const { + return this->current_state == other.current_state; + } + bool operator!=(const condition_variable &other) const { + return this->current_state != other.current_state; + } + + bool is_initialized() const { return this->current_state == cv_initialized ; } + bool is_waiting() const { return this->current_state == cv_waiting ; } + bool is_signaled() const { return this->current_state == cv_signaled ; } + bool is_uninitialized() const { return this->current_state == cv_uninitialized ;} + bool is_transitional() const { return this->current_state == cv_transitional;} + bool is_destroyed() const { return this->current_state == cv_destroyed;} + + ConditionVariablePolicy* get_policy() const {return this->policy;} + + void set_associated_mutex(pthread_mutex_t* mutex) { + this->associated_mutex = mutex; + } + + pthread_mutex_t* get_mutex() const {return this->associated_mutex;} + + bool has_waiters() const {return this->policy->has_waiters();} + + void remove_waiter(runner_id_t tid) const { + this->policy->wake_thread(tid); + // If there are any spurious wake ups allowed, + // we always allow the thread to wake up + // due to a spurious wake up and decrement the + // number of future such wakeups allowed + if (this->numRemainingSpuriousWakeups > 0) { + this->numRemainingSpuriousWakeups--; + } + } + + void add_waiter(runner_id_t tid) { + this->policy->add_waiter(tid); + } + + bool waiter_can_exit(runner_id_t tid) const{ + return this->numRemainingSpuriousWakeups > 0 || + this->policy->thread_can_exit(tid); + } + + void send_signal_message() const{ + this->policy->receive_signal_message(); + } + + void send_broadcast_message() const { + this->policy->receive_broadcast_message(); + } + + void check_for_lost_wakeup(bool is_signal, int prev_waiting_count) { + if (!is_signal) return; + // Check if any thread was signaled + bool any_thread_signaled = false; + for (auto id : this->policy->return_wait_queue()) { + if (this->policy->get_thread_cv_state(id) == CV_SIGNALED) { + any_thread_signaled = true; + break; + } + } + // Tf np thread was signaled and there were waiting threads, it's a lost wakeup + if (!any_thread_signaled && prev_waiting_count > 0) { + lost_wakeups++; + fprintf(stderr, "WARNING: Lost wakeup detected on condition variable\n"); + } + } + + std::unique_ptr clone() const override { + return extensions::make_unique(*this); + } + + std::string to_string() const override { + return "condition_variable(state: " + std::to_string(current_state); + } +}; +} // namespace objects +} // namespace model diff --git a/docs/design/include/mcmini/model/objects/mutex.hpp b/dmtcp/include/mcmini/model/objects/mutex.hpp similarity index 63% rename from docs/design/include/mcmini/model/objects/mutex.hpp rename to dmtcp/include/mcmini/model/objects/mutex.hpp index ea9246ba..66789238 100644 --- a/docs/design/include/mcmini/model/objects/mutex.hpp +++ b/dmtcp/include/mcmini/model/objects/mutex.hpp @@ -9,32 +9,39 @@ namespace objects { struct mutex : public model::visible_object_state { public: /* The four possible states for a mutex */ - enum state_type { uninitialized, unlocked, locked, destroyed }; + enum state { uninitialized, unlocked, locked, destroyed }; private: - state_type current_state = state_type::uninitialized; + state current_state = state::uninitialized; + pthread_mutex_t* location; + runner_id_t owner; public: mutex() = default; ~mutex() = default; mutex(const mutex &) = default; - mutex(state_type state) : current_state(state) {} - static std::unique_ptr make(state_type state) { - return extensions::make_unique(state); - } - + mutex(state s) : current_state(s) {} + mutex(state s, pthread_mutex_t* loc) : current_state(s), location(loc) {} + mutex(state s, pthread_mutex_t* loc, runner_id_t tid): current_state(s), location(loc), owner(tid) {} + // ---- State Observation --- // bool operator==(const mutex &other) const { - return this->current_state == other.current_state; + return this->current_state == other.current_state && this->owner == other.owner; + } bool operator!=(const mutex &other) const { return this->current_state != other.current_state; } + bool is_locked_by(runner_id_t tid) const { return current_state == locked && owner ==tid; } + bool is_locked() const { return this->current_state == locked; } bool is_unlocked() const { return this->current_state == unlocked; } bool is_destroyed() const { return this->current_state == destroyed; } bool is_initialized() const { return this->current_state != uninitialized; } + + pthread_mutex_t* get_location() const { return this->location; } + std::unique_ptr clone() const override { return extensions::make_unique(*this); } diff --git a/dmtcp/include/mcmini/model/objects/semaphore.hpp b/dmtcp/include/mcmini/model/objects/semaphore.hpp new file mode 100644 index 00000000..6716948a --- /dev/null +++ b/dmtcp/include/mcmini/model/objects/semaphore.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include "mcmini/misc/extensions/unique_ptr.hpp" +#include "mcmini/model/visible_object_state.hpp" + +namespace model { +namespace objects { +struct semaphore : public model::visible_object_state { + public: + enum state { uninitialized, initialized, destroyed }; + + private: + int _count = 0; + state current_state = state::uninitialized; + + public: + semaphore() = default; + ~semaphore() = default; + semaphore(const semaphore &) = default; + explicit semaphore(state s) : semaphore(s, 0) {} + explicit semaphore(unsigned count) : semaphore(initialized, count) {} + explicit semaphore(state s, int count) : _count(count), current_state(s) {} + void wait() { this->_count--; } + void post() { this->_count++; } + void destroy() { this->current_state = state::destroyed; } + unsigned count() const { return this->_count; } + bool will_block() const { return this->_count <= 0; } + std::unique_ptr clone() const override { + return extensions::make_unique(*this); + } + std::string to_string() const override { + return "semaphore(count: " + std::to_string(_count) + ")"; + } +}; +} // namespace objects +} // namespace model diff --git a/docs/design/include/mcmini/model/objects/thread.hpp b/dmtcp/include/mcmini/model/objects/thread.hpp similarity index 68% rename from docs/design/include/mcmini/model/objects/thread.hpp rename to dmtcp/include/mcmini/model/objects/thread.hpp index 74801f39..e500a4df 100644 --- a/docs/design/include/mcmini/model/objects/thread.hpp +++ b/dmtcp/include/mcmini/model/objects/thread.hpp @@ -4,28 +4,36 @@ #include "mcmini/misc/extensions/unique_ptr.hpp" #include "mcmini/model/visible_object_state.hpp" +#include "mcmini/spy/checkpointing/objects.h" namespace model { namespace objects { -struct thread : public model::visible_object_state { +struct thread : public model::runner_state { public: /* The four possible states for a mutex */ - enum state_type { embryo, running, exited, killed }; + enum state { running, exited, killed, embryo }; private: - state_type current_state = state_type::embryo; + state current_state = state::embryo; public: thread() = default; ~thread() = default; thread(const thread &) = default; - thread(state_type state) : current_state(state) {} - static std::unique_ptr make(state_type state) { - return extensions::make_unique(state); + thread(state s) : current_state(s) {} + thread(thread_status s) { + switch (s) { + case ALIVE: { + this->current_state = state::running; + break; + } + case EXITED: { + this->current_state = state::exited; + break; + } + } } - static std::unique_ptr make() { return thread::make(embryo); } - // ---- State Observation --- // bool operator==(const thread &other) const { return this->current_state == other.current_state; @@ -37,7 +45,7 @@ struct thread : public model::visible_object_state { bool is_running() const { return this->current_state == running; } bool has_exited() const { return this->current_state == exited; } bool is_killed() const { return this->current_state == killed; } - + bool is_active() const override { return this->is_running(); } std::unique_ptr clone() const override { return extensions::make_unique(*this); } diff --git a/dmtcp/include/mcmini/model/pending_transitions.hpp b/dmtcp/include/mcmini/model/pending_transitions.hpp new file mode 100644 index 00000000..668db878 --- /dev/null +++ b/dmtcp/include/mcmini/model/pending_transitions.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include +#include + +#include "mcmini/misc/extensions/unique_ptr.hpp" +#include "mcmini/model/defines.hpp" +#include "mcmini/model/transition.hpp" + +namespace model { + +/** + * @brief A collection of "next steps" for a set of threads running in a + * `model::program` + * + * An important component of a program are the possible ways that is can evolve. + * Evolution is described in McMini as _transitions_ -- functions of state which + * produce a state `s'` from a given state `s`. Conceptually, + * `pending_transitions` is a mapping of runner ids to transitions. + */ +struct pending_transitions final { + private: + using runner_id_t = ::runner_id_t; + std::map _contents; + + public: + pending_transitions() = default; + pending_transitions(pending_transitions &&) = default; + pending_transitions(const pending_transitions &) = delete; + pending_transitions &operator=(pending_transitions &&) = default; + pending_transitions &operator=(const pending_transitions &) = delete; + ~pending_transitions() { + for (const auto &p : _contents) delete p.second; + } + auto begin() -> decltype(_contents.begin()) { return _contents.begin(); } + auto end() -> decltype(_contents.end()) { return _contents.end(); } + auto begin() const -> decltype(_contents.cbegin()) { + return _contents.cbegin(); + } + auto end() const -> decltype(_contents.cend()) { return _contents.cend(); } + auto cbegin() -> decltype(_contents.cbegin()) const { + return _contents.cbegin(); + } + auto cend() -> decltype(_contents.cend()) const { return _contents.cend(); } + size_t size() const { return this->_contents.size(); } + /** + * @brief Returns the transition mapped to id `id`, or `nullptr` if no such + * runner has been mapped to an id. + * + * @param id the id of the runner whose transition should be retrieved. + */ + const transition *get_transition_for_runner(runner_id_t id) const { + if (_contents.count(id) > 0) { + return _contents.at(id); + } + return nullptr; + } + + std::unique_ptr replace_managed( + const transition *new_transition) noexcept { + return std::unique_ptr(replace_unowned(new_transition)); + } + + /// @brief Replace the pending operation for `new_transition->get_executor()` + /// @param new_transition the transition that will be used to replace any + /// current transition in the struct (ownership is acquired) + /// @return a pointer to the old transition. This pointer must be freed using + /// `delete`. + const transition *replace_unowned(const transition *new_transition) noexcept { + runner_id_t id = new_transition->get_executor(); + const transition *old_transition = _contents[id]; + _contents[id] = new_transition; + return old_transition; + } + + void set_transition(const transition *new_transition) noexcept { + delete replace_unowned(new_transition); + } +}; + +} // namespace model diff --git a/dmtcp/include/mcmini/model/program.hpp b/dmtcp/include/mcmini/model/program.hpp new file mode 100644 index 00000000..0b20094a --- /dev/null +++ b/dmtcp/include/mcmini/model/program.hpp @@ -0,0 +1,128 @@ +#pragma once + +#include +#include + +#include "mcmini/model/defines.hpp" +#include "mcmini/model/pending_transitions.hpp" +#include "mcmini/model/state/state_sequence.hpp" +#include "mcmini/model/transitions/transition_sequence.hpp" + +namespace model { + +/** + * @brief A capture of the states visited in a particular branch of the state + * space of a program undergoing verification; a trace describing how the + * current state has come about as a sequence of transitions; and the + * immediately visible "next steps" that can be taken by the program in its + * current state. + * + * A program is an interface between a live process running on the CPU under + * control of the operating system and the view of that process from the + * perspective of the verifier. A _model::program_ is a container that + * comprises of the following three components: + * + * - a sequence `S` of transitions that have occurred called a _trace_. + * - a model of the state of the program undergoing verification after each + * point `t` in trace `S`. + * - a mapping between the individual execution units of the program ("threads") + * and the next thread routine ("transition") that thread will run once + * executed. + * + * Each component corresponds directly to the theoretical models described in + * Flanagan et al., Rodriguez et al., etc; specifically + * + * - the sequence `S` of transitions is some linearization of transitions that + * the given program has followed to reach its current state. + * - the model of the program at each point models how the state of the program + * at each point during the sequence (s_0, s_1, ..., s_N). Each transition `t_i + * in `S` describes how the program went from state `s_i` to `s_(i+1)`. + * - the mapping of execution units to threads represents `next(s_N, p)`. + * + * Conceptually, the program's _current state_ is that state reached from + * executing the sequence `S` of transitions in the order they appear from the + * initial state `s_0` of the program. This is sometimes represented as + * `state(S)` where `S` is some transition sequence and `s_0` is assumed to be + * implied. + */ +class program { + private: + state_sequence state_seq; + transition_sequence trace; + pending_transitions next_steps; + + public: + using runner_id_t = ::runner_id_t; + program(); + program(const state &initial_state, + pending_transitions &&initial_first_steps); + program(program &&) = default; + program(const program &) = delete; + static program starting_from_main(); + + std::unordered_set get_enabled_runners() const; + size_t get_num_runners() const { return next_steps.size(); } + mutable_state &get_current_state() { return this->state_seq; } + pending_transitions &get_pending_transitions() { return this->next_steps; } + const state_sequence &get_state_sequence() const { return this->state_seq; } + const transition_sequence &get_trace() const { return this->trace; } + const pending_transitions &get_pending_transitions() const { + return this->next_steps; + } + const transition *get_pending_transition_for(runner_id_t rid) const { + return next_steps.get_transition_for_runner(rid); + } + + using runner_generation_function = + std::function; + + /// @brief Introduce a new object into the model with initial state `s` + /// @param s the initial state of the new object to add to the model + /// @return the id assigned to the object in the model + state::objid_t discover_object(const visible_object_state *s); + + /// @brief Introduce a new runner into the model with initial state `s` + /// @param s the initial state of the new object to add to the model + /// @param f a function which, when passed the id that will be vended to the + /// runner, produces the first transition that runner is executing in the + /// model (i.e. the very first pending operation). + /// @return the id assigned to the runner. + state::runner_id_t discover_runner(const runner_state *s, + runner_generation_function f); + + /// @brief Introduce a new runner into the model with initial state `s` + /// running transition `next_transition` + /// @param s the initial state of the new object to add to the model + /// @param next_transition the next operation the runner will execute in + /// state `s` + /// @return the id assigned to the runner. + state::runner_id_t discover_runner(const runner_state *s, + const transition *next_transition); + + /// @brief Model the execution of runner `p` and replace its next operation + /// with `next_pending_operation`. + /// + /// @param p the id of the runner whose next transition should be simulated. + /// @param npo (short for `next_pending_operation`) the next transition this + /// is pending after `p` executes; that is, this is the transition that `p` + /// will run in the state modeled _after_ `next_s_p` is executed. + /// @throws an runtime exception is raised if the transition replacing + /// `next_s_p` is not executed by `p` or if `p` is not currently known to the + /// model. + void model_execution_of(runner_id_t p, const transition *npo); + + /// @brief Restore the model as if it were `n` steps into execution. + /// + /// @param n the number of transitions to consider executed to result in the + /// program model after the method has executed. Formally, if `t_0, t_1, ..., + /// t_(n-1), ..., t_k` is contained in the current trace, then the state after + /// this method is called will be `s_(n+1)` or `s_0` if `n = 0`. + void restore_model_at_depth(uint32_t n); + + // MARK: Program State + bool is_in_deadlock() const; + std::ostream &dump_state(std::ostream&) const; +}; +// + +}; // namespace model diff --git a/dmtcp/include/mcmini/model/runner_state.hpp b/dmtcp/include/mcmini/model/runner_state.hpp new file mode 100644 index 00000000..bc9d8014 --- /dev/null +++ b/dmtcp/include/mcmini/model/runner_state.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "mcmini/model/visible_object_state.hpp" + +namespace model { + +class runner_state : public visible_object_state { + public: + virtual ~runner_state() = default; + + virtual bool is_active() const = 0; + virtual bool has_exited() const = 0; + bool is_terminated() const { return !is_active(); } +}; + +} // namespace model diff --git a/dmtcp/include/mcmini/model/state.hpp b/dmtcp/include/mcmini/model/state.hpp new file mode 100644 index 00000000..c161816e --- /dev/null +++ b/dmtcp/include/mcmini/model/state.hpp @@ -0,0 +1,127 @@ +#pragma once + +#include +#include +#include + +#include "mcmini/forwards.hpp" +#include "mcmini/misc/asserts.hpp" +#include "mcmini/model/defines.hpp" +#include "mcmini/model/runner_state.hpp" +#include "mcmini/model/visible_object.hpp" + +namespace model { +class state { + public: + using objid_t = uint32_t; + using runner_id_t = ::runner_id_t; + + virtual ~state() = default; + virtual size_t count() const = 0; + virtual size_t runner_count() const = 0; + virtual bool is_runner(objid_t id) const = 0; + virtual bool contains_object_with_id(objid_t id) const = 0; + virtual bool contains_runner_with_id(runner_id_t id) const = 0; + virtual objid_t get_objid_for_runner(runner_id_t id) const = 0; + virtual runner_id_t get_runner_id_for_obj(objid_t id) const = 0; + virtual const visible_object_state *get_state_of_object(objid_t id) const = 0; + virtual const runner_state *get_state_of_runner(runner_id_t id) const = 0; + virtual std::unique_ptr mutable_clone() const = 0; + virtual std::string debug_string() const { return ""; } + + // TODO: Potentially provide an interface here that conforms to C++11's + // iteration (a begin() and end() as virtual functions perhaps). + // Each subclass can return the same `state_iterator` type that + // is defined elsewhere which should provide a pair of objid_t + // and const visible_object_state* associated with that id. + + template + const concrete_visible_object_state *get_state_of_object(objid_t id) const { + static_assert(std::is_base_of::value, + "Concrete type must be a subtype of `visible_object_state`"); + return static_cast( + this->get_state_of_object(id)); + } + + template + const concrete_visible_object_state *get_state_of_runner( + runner_id_t id) const { + static_assert(std::is_base_of::value, + "Concrete type must be a subtype of `visible_object_state`"); + return static_cast( + this->get_state_of_runner(id)); + } +}; + +class mutable_state : public state { + public: + virtual ~mutable_state() = default; + + /** + * @brief Begin tracking a new visible object _obj_ to this state. + * + * @param initial_state the initial state of the object. + * @return the new id that is assigned to the object. This id is unique from + * every other id assigned to the objects in this state. + */ + virtual objid_t add_object(const visible_object_state *initial_state) = 0; + + /** + * @brief Begin tracking a new visible object, but consider it as the state of + * a new runner. + * + * @return the id of the runner that was just added. This is NOT the same as + * the runner's object id. + */ + virtual runner_id_t add_runner(const runner_state *initial_state) = 0; + + /** + * @brief Adds the given state _state_ for the object with id _id_. + * + * @note: If the object with id `id` is _not_ a visible object tracking states + * of type `visible_object_state_type`, the behavior of this function is + * undefined. + */ + virtual void add_state_for_obj(objid_t id, const visible_object_state *) = 0; + + /** + * @brief Adds the given state _state_ for the runner with id _id_. + * + * This is equivalent to first retrieving the object id of the runner in this + * state and then asking for that object's state. + */ + virtual void add_state_for_runner(runner_id_t id, const runner_state *) = 0; + + /** + * @brief Creates a copy of the given state. + * + * All visible objects in the underlying state are copied into the new state + * and are independently modifiable with respect to the first state. + */ + std::unique_ptr clone() const { return this->mutable_clone(); } + + template + const concrete_visible_object_state *get_state_of_object(objid_t id) const { + return (static_cast(this)) + ->get_state_of_object(id); + } + + template + const concrete_visible_object_state *get_state_of_runner( + runner_id_t id) const { + static_assert(std::is_base_of::value, + "Concrete type must be a subtype of `visible_object_state`"); + return (static_cast(this)) + ->get_state_of_runner(id); + } +}; + +constexpr static auto invalid_objid = + std::numeric_limits::max(); +constexpr static auto invalid_rid = + std::numeric_limits::max(); + +} // namespace model diff --git a/dmtcp/include/mcmini/model/state/detached_state.hpp b/dmtcp/include/mcmini/model/state/detached_state.hpp new file mode 100644 index 00000000..c1d27445 --- /dev/null +++ b/dmtcp/include/mcmini/model/state/detached_state.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include "mcmini/misc/append-only.hpp" +#include "mcmini/misc/extensions/unique_ptr.hpp" +#include "mcmini/misc/injective_function.hpp" +#include "mcmini/model/state.hpp" +#include "mcmini/model/visible_object.hpp" + +namespace model { + +/** + * @brief A collection of visible object states. + * + * A `detached_state` is one which defines a state of a program outside of the + * context of a sequence; that is, a detached state represents the states + */ +class detached_state : public model::mutable_state { + protected: + append_only visible_objects; + injective_function runner_to_obj_map; + + public: + detached_state() = default; + detached_state(std::vector &&); + detached_state(append_only &&); + detached_state(const detached_state &) = default; + detached_state(detached_state &&) = default; + detached_state &operator=(const detached_state &) = default; + detached_state &operator=(detached_state &&) = default; + + /* `state` overrrides */ + size_t count() const override { return visible_objects.size(); } + size_t runner_count() const override { return runner_to_obj_map.size(); } + + objid_t get_objid_for_runner(runner_id_t id) const override; + runner_id_t get_runner_id_for_obj(objid_t id) const override; + bool is_runner(objid_t id) const override; + bool contains_object_with_id(state::objid_t id) const override; + bool contains_runner_with_id(runner_id_t id) const override; + const visible_object_state *get_state_of_object(objid_t id) const override; + const runner_state *get_state_of_runner(runner_id_t id) const override; + objid_t add_object(const visible_object_state *) override; + runner_id_t add_runner(const runner_state *) override; + void add_state_for_obj(objid_t id, const visible_object_state *) override; + void add_state_for_runner(runner_id_t id, const runner_state *) override; + std::unique_ptr mutable_clone() const override; +}; + +} // namespace model diff --git a/dmtcp/include/mcmini/model/state/diff_state.hpp b/dmtcp/include/mcmini/model/state/diff_state.hpp new file mode 100644 index 00000000..88fa77a8 --- /dev/null +++ b/dmtcp/include/mcmini/model/state/diff_state.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include "mcmini/misc/injective_function.hpp" +#include "mcmini/model/state.hpp" +#include "mcmini/model/visible_object.hpp" + +namespace model { + +/// @brief A state which defines its own state as a change in state from some +/// base state `base_state` +class diff_state : public mutable_state { + private: + const state &base_state; + + public: + injective_function new_runners; + std::unordered_map new_object_states; + + diff_state(const state &s) : base_state(s) {} + diff_state(const diff_state &ds) + : base_state(ds.base_state), new_object_states(ds.new_object_states) {} + diff_state(detached_state &&) = delete; + diff_state &operator=(const diff_state &) = delete; + detached_state &operator=(detached_state &&) = delete; + + const state &get_base() const { return this->base_state; } + bool differs_from_base() const { + return !this->new_object_states.empty() || !this->new_runners.empty(); + } + + /* `mutable_state` overrrides */ + size_t count() const override { + size_t count = this->new_object_states.size() + base_state.count(); + for (const auto &p : new_object_states) { + // Each item in `new_object_states` that is also in `base_state` defines + // states for _previously existing_ objects. These objects are accounted + // for in `count` (double-counted), hence the `--` + if (base_state.contains_object_with_id(p.first)) count--; + } + return count; + } + size_t runner_count() const override { + return base_state.runner_count() + new_runners.size(); + } + objid_t get_objid_for_runner(runner_id_t id) const override; + runner_id_t get_runner_id_for_obj(objid_t id) const override; + bool is_runner(objid_t id) const override; + bool contains_object_with_id(state::objid_t id) const override; + bool contains_runner_with_id(runner_id_t id) const override; + const visible_object_state *get_state_of_object(objid_t id) const override; + const runner_state *get_state_of_runner(runner_id_t id) const override; + objid_t add_object(const visible_object_state *) override; + runner_id_t add_runner(const runner_state *) override; + void add_state_for_obj(objid_t id, const visible_object_state *) override; + void add_state_for_runner(runner_id_t id, const runner_state *) override; + std::unique_ptr mutable_clone() const override; +}; +} // namespace model diff --git a/dmtcp/include/mcmini/model/state/state_sequence.hpp b/dmtcp/include/mcmini/model/state/state_sequence.hpp new file mode 100644 index 00000000..4e6396b1 --- /dev/null +++ b/dmtcp/include/mcmini/model/state/state_sequence.hpp @@ -0,0 +1,112 @@ +#pragma once + +#include +#include + +#include "mcmini/forwards.hpp" +#include "mcmini/misc/append-only.hpp" +#include "mcmini/model/state/detached_state.hpp" +#include "mcmini/model/state/diff_state.hpp" +#include "mcmini/model/transition.hpp" + +namespace model { + +/** + * @brief A sequence of states. + * + * A _state sequence_ is a sequence of states with the following properties: + * + * 1. for each state `s_i`, there exists some transition `t_i` such that `t_i` + * is defined at `s_i` (`t_i` is enabled there) and `s_{i+1} = t_i(s_i)`. That + * is, given any pair of states `s_i` and `s_j` in the sequence (i <= j), there + * exists a sequence of transitions `t_i, ..., t_j` such that `s_j = + * t_j(t_{j-1}(...(t_i(s_i))...))` + * + * A `state_sequence` is itself a `model::mutable_state` that is conceptually + * represented by the final state in the sequence. A state sequence is never + * empty, + */ +class state_sequence final : public mutable_state { + private: + class element; + injective_function runner_to_obj_map; + append_only visible_objects; + + /// @brief Inserts an instance of `element` in the `states_in_sequence` + void push_state_snapshot(); + + // INVARIANT: As new states are added to the visible objects in the + // mapping `visible_objects`, new state views are also added with the + // appropriate object states replaced for the _last element_ of + // `states_in_sequence`. When new objects are added to `visible_objects`, a + // corresponding object state is added to the _last_ element in the sequence. + append_only states_in_sequence; + + /// @brief Retrieves the state which represents that sequence as a whole. + element &get_representative_state() const { + return *this->states_in_sequence.back(); + } + + public: + state_sequence(); + ~state_sequence(); + state_sequence(const state &); + state_sequence(state_sequence &&); + state_sequence(std::vector &&); + state_sequence(append_only &&); + state_sequence &operator=(const state_sequence &&) = delete; + state_sequence &operator=(const state_sequence &) = delete; + + size_t count() const override; + size_t runner_count() const override; + size_t get_num_states_in_sequence() const; + + objid_t get_objid_for_runner(runner_id_t id) const override; + runner_id_t get_runner_id_for_obj(objid_t id) const override; + bool is_runner(objid_t id) const override; + bool contains_object_with_id(state::objid_t id) const override; + bool contains_runner_with_id(runner_id_t id) const override; + const visible_object_state *get_state_of_object(objid_t id) const override; + const runner_state *get_state_of_runner(runner_id_t id) const override; + objid_t add_object(const visible_object_state *) override; + runner_id_t add_runner(const runner_state *) override; + void add_state_for_obj(objid_t id, const visible_object_state *) override; + void add_state_for_runner(runner_id_t id, const runner_state *) override; + std::unique_ptr mutable_clone() const override; + + /* Applying transitions */ + + /** + * @brief Applies the given transition to the final state of the sequence if + * it is enabled there and pushes the adds the transformed state to the + * sequence. + * + * @return whether the transition was enabled at the final state in the + * sequence. If the transition was disabled there, a new state will _not_ be + * added to the sequence and a `disabled` status will be returned. Otherwise, + * the transition is _defined_ at the final state and a new state `s'` is + * added to the end of the sequence. + */ + transition::status follow(const transition &t); + + const state &front() const; + const state &back() const; + const state &state_at(size_t i) const; + + /** + * @brief Moves the contents from index 0 to index _index_ (inclusive) of this + * sequence produce a the sequence formed by entries 0-index. + * + * Any `state` references which were vended by the sequence via the function + * `state_sequence::state_at()` which point to indices past _index_ are no + * longer valid after the sequence is consumed. All other views into the + * sequence remain valid. + * + * @param num_states the number of states that should be left in the sequence + * after consumption. If `num_states` is large than the number of states in + * this sequence, the method has no effect. + */ + void consume_into_subsequence(size_t num_states); +}; + +} // namespace model diff --git a/docs/design/include/mcmini/model/transition.hpp b/dmtcp/include/mcmini/model/transition.hpp similarity index 58% rename from docs/design/include/mcmini/model/transition.hpp rename to dmtcp/include/mcmini/model/transition.hpp index c1648372..55617e93 100644 --- a/docs/design/include/mcmini/model/transition.hpp +++ b/dmtcp/include/mcmini/model/transition.hpp @@ -3,8 +3,9 @@ #include #include "mcmini/forwards.hpp" -#include "mcmini/misc/optional.hpp" +#include "mcmini/model/defines.hpp" #include "mcmini/model/state.hpp" +#include "mcmini/model/state/diff_state.hpp" namespace model { @@ -41,7 +42,7 @@ namespace model { * `model::transition` allows the transition to produce a state _even if * the process which is set to execute the transition isn't truly in a position * where the transition is being executed_. Transitions perform _look ups_ on - * the particular state they are given + * the particular state they are given. * * For example, consider a transition "post(sem)" which takes a visible object * (a semaphore) with id `sem` as an argument. The sempahore `sem` may exist in @@ -52,10 +53,36 @@ namespace model { * is _not_ executing a `post(sem)`. However, the implementation of "thread 1 * executes post(sem)" _would be defined_ in state `s_2`. In other words, a * `model::state` _excludes_ the state of the _processes_; that is the - * responsibility of `model::program`. + * responsibility of `model::program` to ensure that this transition is indeed + * the next one for the process running it. + * + * @note Our definition of transition is more in line with Abdulla et al. 2017, + * viz. one in which we assume a program is a collection of processes each of + * which can be represented as partial functions executing atomically; + * therefore, every transition in McMini is executed by _some_ runner, even if + * the runner itself is virtual (e.g. a transition that affects multiple + * threads). */ class transition { public: + using runner_id_t = ::runner_id_t; + + transition(runner_id_t executor) : executor(executor) {} + runner_id_t get_executor() const { return executor; } + + /** + * @brief A result of a modification to a state. + * + * There are two possible outcomes attempting to apply a transition + * function: + * + * 1. Either the transition _exists_ at this state and is thus defined at + * the given state. + * 2. Or else the transition is _not_ defined as this state and the + * transition is disabled. + */ + enum class status { exists, disabled, undefined }; + /** * @brief Attempts to produce a state _s'_ from state _s_ through the * application of this transition function on argument _s_ @@ -67,35 +94,49 @@ class transition { * * @param s the state to pass as an argument to the transition. * @returns the resulting state _s'_ that would be produced if this transition - * were applied to state _s_ if such a transition is defined at _s_, and - * otherwise `nullptr` + * were applied to state _s_ if such a transition is defined at _s_, along + * with the enabledness of the given transition. If the transition is not + * defined, a `diff_state` with base `s` is returned, but no other + * modifications are made to the base. + * @note a pair is NOT redundant here: it's possible for a transition to be + * enabled at state `s` but have no effect on it. Hence, we _cannot_ simply + * rely on the fact that the `diff_state` has changed w.r.t its base to + * determine enabledness; instead, we must return both the `diff_state` AND + * the enabled status. + * @note construction of a new `diff_state` in the `else` branch ensures that + * partial modifications to `s` are not transferred out of the call to + * the method into the resulting `diff_state`. Some parts of the transition + * may have modified the state, and only then would the transition have + * determined it is no longer enabled. This would be discouraged, but it's + * possible, so we opt for the defensive stance. + * @note the `diff_state` that is returned retains the reference to the state + * `s` supplied to this function. In other words, the state with respect to + * which the diff is defined is `s`. Keep this in mind if you plan to store + * the `diff_state` or continue using it after calling this function. */ - std::unique_ptr apply_to(const state& s) const { - std::unique_ptr s_prime = s.mutable_clone(); - return modify(*s_prime) == status::exists - ? std::unique_ptr(std::move(s_prime)) - : std::unique_ptr(); + std::pair apply_to(const state& s) const { + diff_state s_prime{s}; + return s.get_state_of_runner(executor)->is_active() && + modify(s_prime) == status::exists + ? std::make_pair(s_prime, status::exists) + : std::make_pair(diff_state{s}, status::disabled); + } + bool is_enabled_in(const state& s) const { + return apply_to(s).second == status::exists; } + bool is_disabled_in(const state& s) const { return !is_enabled_in(s); } - /** - * @brief Whether the transition is defined in the given state. - * - * @param s the state to determine if this transition is enabled. - */ - bool is_enabled_in(const state& s) const { return apply_to(s) != nullptr; } + /// @return The exit code of the program should this transition be executed, + /// or `-1` if the program would not exit with the execution of this + /// transition. + virtual int program_exit_code() const { return -1; } - /** - * @brief A result of a modification to a state. - * - * There are two possible outcomes attempting to apply a transition - * function: - * - * 1. Either the transition _exists_ at this state and is thus defined at - * the given state. - * 2. Or else the transition is _not_ defined as this state and the - * transition is disabled. - */ - enum class status { exists, disabled }; + /// @brief Whether the transition, if executed, would cause the program to + /// exit abnormally if executed + /// + /// A transition that causes a program to abnormally abort execution is one + /// with the equivalent semantics to calling `abort(2)`. For example + virtual bool aborts_program_execution() const { return false; } /** * @brief Fire the transition as if it were run from state _state_. @@ -120,7 +161,14 @@ class transition { // coordinator::context::model_to_system_map&) const = 0;` virtual std::string to_string() const = 0; + std::string debug_string() const { + return "thread " + std::to_string(this->executor) + ": " + to_string(); + } virtual ~transition() = default; + + protected: + /// @brief The thread/runner which actually executes this transition. + const runner_id_t executor; }; -} // namespace model \ No newline at end of file +} // namespace model diff --git a/docs/design/include/mcmini/model/transition_registry.hpp b/dmtcp/include/mcmini/model/transition_registry.hpp similarity index 81% rename from docs/design/include/mcmini/model/transition_registry.hpp rename to dmtcp/include/mcmini/model/transition_registry.hpp index 13d73347..ff4c7f59 100644 --- a/docs/design/include/mcmini/model/transition_registry.hpp +++ b/dmtcp/include/mcmini/model/transition_registry.hpp @@ -4,8 +4,8 @@ #include #include -#include "mcmini/coordinator/coordinator.hpp" #include "mcmini/model/transition.hpp" +#include "mcmini/real_world/mailbox/runner_mailbox.h" namespace model { @@ -27,7 +27,9 @@ class transition_registry final { using runtime_type_id = uint32_t; using rttid = runtime_type_id; using transition_discovery_callback = - std::unique_ptr (*)(std::istream&, model_to_system_map&); + transition *(*)(state::runner_id_t, const volatile runner_mailbox &, + model_to_system_map &); + static transition_registry default_registry(); /** * @brief Marks the specified transition subclass as possible to encounter at @@ -37,10 +39,8 @@ class transition_registry final { * associate with the returned id * @returns a positive integer which conceptually represents the transition. */ - template - runtime_type_id register_transition(transition_discovery_callback callback) { - static_assert(std::is_base_of::value, - "Must be a subclass of `model::transition`"); + void register_transition(rttid rttid, + transition_discovery_callback callback) { // TODO: Mapping between types and the serialization // function pointers. For plugins loaded by McMini, each will have the // chance to register the transitions it defines. Here the RTTI needs to @@ -48,8 +48,7 @@ class transition_registry final { // here. See the `ld` man page and specifically the two linker flags // `--dynamic-list-cpp-typeinfo` and `-E` for details. `-E` is definitely // sufficient it seems in my small testing - runtime_callbacks.push_back(callback); - return runtime_callbacks.size() - 1; + runtime_callbacks.insert({rttid, callback}); } /** @@ -72,14 +71,15 @@ class transition_registry final { * id this registry assigned. * * @returns a function pointer which can produce a new transition of the type - * assigned id `rttid`. + * assigned id `rttid`, or `nullptr` if no such `rttid` has been registered. */ transition_discovery_callback get_callback_for(runtime_type_id rttid) { + if (this->runtime_callbacks.count(rttid) == 0) return nullptr; return this->runtime_callbacks.at(rttid); } private: - std::vector runtime_callbacks; + std::unordered_map runtime_callbacks; }; -} // namespace model \ No newline at end of file +} // namespace model diff --git a/dmtcp/include/mcmini/model/transitions/condition_variables/callbacks.hpp b/dmtcp/include/mcmini/model/transitions/condition_variables/callbacks.hpp new file mode 100644 index 00000000..7063dd5a --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/condition_variables/callbacks.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "mcmini/coordinator/model_to_system_map.hpp" +#include "mcmini/model/state.hpp" +#include "mcmini/model/transitions/condition_variables/condition_variables_signal.hpp" +#include "mcmini/model/transitions/condition_variables/condition_variables_wait.hpp" +#include "mcmini/model/transitions/condition_variables/condition_variable_enqueue_thread.hpp" +#include "mcmini/model/transitions/condition_variables/condition_variables_destroy.hpp" +#include "mcmini/model/transitions/condition_variables/condition_variable_brdcast.hpp" +#include "mcmini/real_world/mailbox/runner_mailbox.h" + +model::transition* cond_init_callback(runner_id_t, + const volatile runner_mailbox&, + model_to_system_map&); +model::transition* cond_waiting_thread_enqueue_callback(runner_id_t, + const volatile runner_mailbox&, + model_to_system_map&); +model::transition* cond_wait_callback(runner_id_t, + const volatile runner_mailbox&, + model_to_system_map&); +model::transition* cond_signal_callback(runner_id_t, + const volatile runner_mailbox&, + model_to_system_map&); +model::transition* cond_broadcast_callback(runner_id_t, + const volatile runner_mailbox&, + model_to_system_map&); +model::transition* cond_destroy_callback(runner_id_t, + const volatile runner_mailbox&, + model_to_system_map&); diff --git a/dmtcp/include/mcmini/model/transitions/condition_variables/condition_variable_brdcast.hpp b/dmtcp/include/mcmini/model/transitions/condition_variables/condition_variable_brdcast.hpp new file mode 100644 index 00000000..edcd2ea5 --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/condition_variables/condition_variable_brdcast.hpp @@ -0,0 +1,102 @@ +#pragma once + +#include "mcmini/model/objects/condition_variables.hpp" +#include "mcmini/model/objects/mutex.hpp" +#include "mcmini/model/transitions/mutex/mutex_lock.hpp" +#include "mcmini/model/transition.hpp" +#include "mcmini/model/transitions/condition_variables/condition_variables_init.hpp" +#include "mcmini/model/transitions/condition_variables/condition_variables_signal.hpp" +#include "mcmini/model/transitions/condition_variables/condition_variables_wait.hpp" +#include "mcmini/model/transitions/condition_variables/condition_variable_enqueue_thread.hpp" +#include "mcmini/model/transitions/condition_variables/condition_variables_destroy.hpp" + +namespace model { +namespace transitions { + +struct condition_variable_broadcast : public model::transition { + private: + const state::objid_t cond_id; + mutable bool had_waiters = false; + + public: + condition_variable_broadcast(runner_id_t executor, state::objid_t cond_id) + : transition(executor), cond_id(cond_id) {} + ~condition_variable_broadcast() = default; + + status modify(model::mutable_state& s) const override { + using namespace model::objects; + + // Retrieve the state of the condition variable + const condition_variable* cv = s.get_state_of_object(cond_id); + if (cv->is_uninitialized()) { + return status::undefined; + } + if (cv->is_destroyed()) { + return status::undefined; + } + + // Check if there are waiters (if not, broadcast is a no-op but still valid) + if (!cv->has_waiters()) { + return status::exists; + } + + // Find all CV_WAITING threads (not CV_PREWAITING) + std::vector waiting_threads; + const auto& wait_queue = cv->get_policy()->return_wait_queue(); + for (auto tid : wait_queue) { + if (cv->get_policy()->get_thread_cv_state(tid) == CV_WAITING) { + waiting_threads.push_back(tid); + } + } + + // Add only CV_WAITING threads to wake groups + if (!waiting_threads.empty()) { + cv->get_policy()->add_to_wake_groups(waiting_threads); + } + + // Make the threads in the wake groups eligible to be woken up by adding them all to broadcast_eligible_threads + cv->send_broadcast_message(); + + // Update condition variable state + const int new_waiting_count = cv->get_policy()->return_wait_queue().size(); + condition_variable::state new_state = new_waiting_count > 0 + ? condition_variable::cv_waiting + : condition_variable::cv_signaled; + + s.add_state_for_obj(cond_id, new condition_variable(new_state, executor, + cv->get_mutex(), + new_waiting_count)); + return status::exists; + } + state::objid_t get_id() const { return this->cond_id;} + std::string to_string() const override { + return "pthread_cond_broadcast(condition_variable:" + std::to_string(cond_id) + ")"; + } + + // MARK: Model checking functions + bool coenabled_with(const condition_variable_wait* cw) const { + return true; // Broadcast can be co-enabled with a wait operation + } + + bool coenabled_with(const mutex_unlock* mu) const { + return true; // Broadcast can happen concurrently with mutex unlock + } + + bool coenabled_with(const condition_variable_destroy* cd) const { + return this->cond_id != cd->get_id(); // Can't broadcast and destroy same CV + } + + bool depends(const condition_variable_wait* cw) const { + return this->cond_id == cw->get_id(); // Dependent with wait on same CV + } + + bool depends(const condition_variable_signal* cs) const { + return this->cond_id == cs->get_id(); // Dependent with signal on same CV + } + + bool depends(const condition_variable_destroy* cd) const { + return this->cond_id == cd->get_id(); // Dependent with destroy on same CV + } +}; +} // namespace transitions +} // namespace model diff --git a/dmtcp/include/mcmini/model/transitions/condition_variables/condition_variable_enqueue_thread.hpp b/dmtcp/include/mcmini/model/transitions/condition_variables/condition_variable_enqueue_thread.hpp new file mode 100644 index 00000000..73f2b1b1 --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/condition_variables/condition_variable_enqueue_thread.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include "mcmini/model/objects/condition_variables.hpp" +#include "mcmini/model/objects/mutex.hpp" +#include "mcmini/model/transitions/mutex/mutex_lock.hpp" +#include "mcmini/model/transition.hpp" +#include "mcmini/model/transitions/condition_variables/condition_variables_init.hpp" + +namespace model { +namespace transitions { + +struct condition_variable_enqueue_thread : public model::transition{ + private: + const state::objid_t cond_id; + const state::objid_t mutex_id; + public: + condition_variable_enqueue_thread(runner_id_t executor, state::objid_t cond_id, state::objid_t mutex_id) + : transition(executor), cond_id(cond_id), mutex_id(mutex_id) {} + ~condition_variable_enqueue_thread() = default; + + status modify(model::mutable_state& s) const override { + using namespace model::objects; + + const condition_variable* cv = s.get_state_of_object(cond_id); + const mutex* m = s.get_state_of_object(mutex_id); + + if (m->is_unlocked()) { + return status::disabled; + } + + if(!m->is_locked_by(executor)) { + return status::disabled; + } + + condition_variable_status current_state = cv->get_policy()->get_thread_cv_state(executor); + if (current_state == CV_PREWAITING) { + // Thread not fully in wait state - update to WAITING before proceeding + cv->get_policy()->update_thread_cv_state(executor, CV_WAITING); + } + + cv->get_policy()->add_waiter_with_state(executor, CV_WAITING); + const int new_waiting_count = cv->get_policy()->return_wait_queue().size(); + + s.add_state_for_obj(cond_id, new condition_variable(condition_variable::cv_waiting, executor, m->get_location(), new_waiting_count)); + s.add_state_for_obj(mutex_id, new mutex(mutex::unlocked)); + return status::exists; + } + state::objid_t get_id() const { return this->cond_id; } + state::objid_t get_mutex_id() const { return this->mutex_id; } + std::string to_string() const override { + return "pthread_cond_wait(cond:" + std::to_string(cond_id) + ", mutex:" + std::to_string(mutex_id) + "(awake -> asleep))"; + } + + // MARK: Model checking functions + bool depends (const condition_variable_init* ci) const { + return this->cond_id == ci->get_id(); + } + + bool depends (const condition_variable_wait* cw) const { + return this->cond_id == cw->get_id(); + } + + bool depends (const condition_variable_signal* cs) const { + return this->cond_id == cs->get_id(); + } + + bool depends (const mutex_lock* ml) const { + return this->mutex_id == ml->get_id(); + } + + bool depends (const mutex_unlock* mu) const { + return this->mutex_id == mu->get_id(); + } + + bool coenabled_with (const condition_variable_enqueue_thread* cwt ) const { + return this->cond_id != cwt->get_id(); + } + + bool coenabled_with (const mutex_lock* ml) const { + return this->mutex_id != ml->get_id(); + } + + bool coenabled_with (const mutex_unlock* mu) const { + return this->mutex_id != mu->get_id(); + } +}; +} +} \ No newline at end of file diff --git a/dmtcp/include/mcmini/model/transitions/condition_variables/condition_variables_destroy.hpp b/dmtcp/include/mcmini/model/transitions/condition_variables/condition_variables_destroy.hpp new file mode 100644 index 00000000..e9e2c938 --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/condition_variables/condition_variables_destroy.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include "mcmini/model/objects/condition_variables.hpp" +#include "mcmini/model/objects/mutex.hpp" +#include "mcmini/model/transitions/mutex/mutex_lock.hpp" +#include "mcmini/model/transition.hpp" +#include "mcmini/model/transitions/condition_variables/condition_variables_init.hpp" +#include "mcmini/model/transitions/condition_variables/condition_variables_wait.hpp" +#include "mcmini/model/transitions/condition_variables/condition_variables_signal.hpp" +#include "mcmini/model/transitions/condition_variables/condition_variable_enqueue_thread.hpp" + +namespace model { +namespace transitions { + +struct condition_variable_destroy : public model::transition { + private: + const state::objid_t cond_id; + + public: + condition_variable_destroy(runner_id_t executor, state::objid_t cond_id) + : transition(executor), cond_id(cond_id) {} + ~condition_variable_destroy() = default; + + status modify(model::mutable_state& s) const override { + using namespace model::objects; + + // Retrieve the state of the condition variable + const condition_variable* cv = s.get_state_of_object(cond_id); + + if (cv->is_uninitialized()) { + return status::undefined; + } + if (cv->is_destroyed()) { + return status::undefined; + } + + // Check if there are still threads waiting on this condition variable + if (cv->has_waiters()) { + return status::undefined; // Error: destroying condition variable with waiters + } + + // Mark condition variable as destroyed + s.add_state_for_obj(cond_id, new condition_variable(condition_variable::cv_destroyed, + executor, + nullptr, // Clear mutex association + 0)); + return status::exists; + } + state::objid_t get_id() const { return this->cond_id; } + std::string to_string() const override { + return "pthread_cond_destroy(condition_variable:" + std::to_string(cond_id) + ")"; + } + // MARK: Model checking functions + bool coenabled_with(const condition_variable_wait* cw) const { + return this->cond_id != cw->get_id(); // Can't destroy CV if threads waiting + } + + bool coenabled_with(const condition_variable_signal* cs) const { + return this->cond_id != cs->get_id(); // Can't signal and destroy same CV + } + + bool depends(const condition_variable_wait* cw) const { + return this->cond_id == cw->get_id(); // Dependent with wait on same CV + } + + bool depends(const condition_variable_signal* cs) const { + return this->cond_id == cs->get_id(); // Dependent with signal on same CV + } + +}; +} // namespace transitions +} // namespace model diff --git a/dmtcp/include/mcmini/model/transitions/condition_variables/condition_variables_init.hpp b/dmtcp/include/mcmini/model/transitions/condition_variables/condition_variables_init.hpp new file mode 100644 index 00000000..889ead25 --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/condition_variables/condition_variables_init.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "mcmini/model/objects/condition_variables.hpp" +#include "mcmini/model/transition.hpp" + +namespace model { +namespace transitions { + +struct condition_variable_init : public model::transition{ + private: + const state::objid_t condition_variable_id; /* The condition variable this transition initializes */ + + public: + condition_variable_init(runner_id_t executor, state::objid_t condition_variable_id) + : transition(executor), condition_variable_id(condition_variable_id) {} + ~condition_variable_init() = default; + + status modify(model::mutable_state& s) const override { + using namespace model::objects; + s.add_state_for_obj(condition_variable_id, new condition_variable(condition_variable::cv_initialized)); + return status::exists; + } + state::objid_t get_id() const { return this->condition_variable_id; } + std::string to_string() const override { + return "pthread_cond_init(condition_variable:" + std::to_string(condition_variable_id) + ")"; + } +}; +} // namespace transitions +} // namespace model diff --git a/dmtcp/include/mcmini/model/transitions/condition_variables/condition_variables_signal.hpp b/dmtcp/include/mcmini/model/transitions/condition_variables/condition_variables_signal.hpp new file mode 100644 index 00000000..7a0602ad --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/condition_variables/condition_variables_signal.hpp @@ -0,0 +1,113 @@ +#pragma once + +#include "mcmini/model/objects/condition_variables.hpp" +#include "mcmini/model/transition.hpp" +#include "mcmini/model/transitions/condition_variables/condition_variables_wait.hpp" +#include "mcmini/model/transitions/mutex/mutex_unlock.hpp" + +namespace model { +namespace transitions { + +struct condition_variable_signal : public model::transition { + private: + const state::objid_t cond_id; /* The condition variable this transition signals */ + mutable bool had_waiters = false; + state::objid_t get_objid_by_location(const mutable_state& s, pthread_mutex_t* mutex_location) const { + + for (state::objid_t id = 0; id < invalid_objid; id++){ + if (!s.contains_object_with_id(id)) continue; + + // const visible_object_state* obj = s.state::get_state_of_object(id); + const visible_object_state* obj = s.get_state_of_object(id); + const objects::mutex* m = dynamic_cast(obj); + if (m && m->get_location() == mutex_location) { + return id; + } + } + return invalid_objid; + } + + public: + condition_variable_signal(runner_id_t executor, state::objid_t cond_id) + : transition(executor), cond_id(cond_id) {} + ~condition_variable_signal() = default; + + status modify(model::mutable_state& s) const override { + using namespace model::objects; + + // Retrive the state of the condition variable + const condition_variable* cv = s.get_state_of_object(cond_id); + + // Count waiting threads BEFORE signal + int prev_waiting_count = 0; + for (auto id: cv->get_policy()->return_wait_queue()) { + if (cv->get_policy()->get_thread_cv_state(id) == CV_WAITING) { + prev_waiting_count++; + } + } + + if (cv->is_uninitialized()) { + return status::undefined; + } + + if (cv->is_destroyed()) { + return status::undefined; + } + + // Check if there are waiters (if not, signal is a no-op but still valid) + if (!cv->has_waiters()) { + return status::exists; //valid transition (lost wakeup) + } + + if (!cv->has_waiters()) { + return status::exists; // valid transition (lost wakeup) + } + + // Find only CV_WAITING threads (not CV_PREWAITING) + std::vector waiting_threads; + const auto& wait_queue = cv->get_policy()->return_wait_queue(); + for (auto tid : wait_queue) { + if (cv->get_policy()->get_thread_cv_state(tid) == CV_WAITING) { + waiting_threads.push_back(tid); + } + } + + // Add only CV_WAITING threads to wake groups + if (!waiting_threads.empty()) { + cv->get_policy()->add_to_wake_groups(waiting_threads); + } + + // Update the condition variable state + const int new_waiting_count = cv->get_policy()->return_wait_queue().size(); + condition_variable::state new_state = new_waiting_count > 0 + ? condition_variable::cv_waiting + : condition_variable::cv_signaled; + + s.add_state_for_obj(cond_id, new condition_variable(new_state, new_waiting_count)); + condition_variable* mutable_cv = new condition_variable(new_state, new_waiting_count); + mutable_cv->check_for_lost_wakeup(true, prev_waiting_count); // Check for lost wakeup if this was a signal + + return status::exists; + } + + state::objid_t get_id() const { return this->cond_id; } + std::string to_string() const override { + return "pthread_cond_signal(cond:" + std::to_string(cond_id) + ")"; + } + // MARK: Model checking functions + bool depends(const condition_variable_wait* cw) const { + return this->cond_id == cw->get_id(); + } + bool depends(const mutex_lock* ml) const { + return this->cond_id == ml->get_id(); + } + bool coenabled_with(const mutex_unlock* mu) const { + return this->cond_id != mu->get_id(); + } + bool coenabled_with(const condition_variable_wait* cw) const { + return this->cond_id != cw->get_id(); + } + +}; +} // namespace transitions +} // namespace model diff --git a/dmtcp/include/mcmini/model/transitions/condition_variables/condition_variables_wait.hpp b/dmtcp/include/mcmini/model/transitions/condition_variables/condition_variables_wait.hpp new file mode 100644 index 00000000..3944d796 --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/condition_variables/condition_variables_wait.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include "mcmini/model/objects/condition_variables.hpp" +#include "mcmini/model/objects/mutex.hpp" +#include "mcmini/model/transitions/mutex/mutex_lock.hpp" +#include "mcmini/model/transition.hpp" +#include "mcmini/model/transitions/condition_variables/condition_variables_signal.hpp" +#include "mcmini/model/transitions/condition_variables/condition_variables_init.hpp" + +namespace model { +namespace transitions { + +struct condition_variable_wait : public model::transition { + private: + const state::objid_t cond_id; /* The condition variable this transition waits on */ + const state::objid_t mutex_id; /* The mutex this transition waits on */ + + public: + condition_variable_wait(runner_id_t executor, state::objid_t cond_id, state::objid_t mutex_id) + : transition(executor), cond_id(cond_id), mutex_id(mutex_id) {} + ~condition_variable_wait() = default; + + status modify(model::mutable_state& s) const override { + using namespace model::objects; + + // Retrive the state of the condition variable and the associated mutex + const condition_variable* cv = s.get_state_of_object(cond_id); + const mutex* m = s.get_state_of_object(mutex_id); + + if (cv->is_uninitialized() || cv->is_destroyed()) { + return status::undefined; + } + + // For the resumption phase, we now expect that the thread is currently waiting , so it should not already hold the mutex. + if (m->is_locked_by(executor)) { + return status::disabled; + } + + if (!(cv->waiter_can_exit(executor) && m->get_location() == cv->get_mutex() && m->is_unlocked())) { + return status::disabled; + } + + // Reacquire the mutex: update its state to "locked" with the executor. + s.add_state_for_obj(mutex_id, new mutex(mutex::locked, m->get_location(), executor)); + + // remove the executor from the wake group + cv->remove_waiter(executor); + + const int new_waiting_count = cv->get_policy()->return_wait_queue().size(); + condition_variable::state new_state = new_waiting_count > 0 + ? condition_variable::cv_waiting + : condition_variable::cv_signaled; + s.add_state_for_obj(cond_id, new condition_variable(new_state, executor, m->get_location(), new_waiting_count)); + return status::exists; + } + state::objid_t get_id() const { return this->cond_id; } + std::string to_string() const override { + return "thread: " + std::to_string(executor) + " pthread_cond_wait (cond: )" + std::to_string(cond_id) + + ", mutex: " + std::to_string(mutex_id) + ") (asleep -> awake)"; + } + + // MARK: Model checking functions + bool depends(const condition_variable_init* ci) const { + return this->cond_id == ci->get_id(); + } + bool depends(const mutex_lock* ml) const { + return this->mutex_id == ml->get_id(); + } + bool coenabled_with(const condition_variable_wait* cw) const { + return this->cond_id != cw->get_id(); + } +}; +} // namespace transitions +} // namespace model diff --git a/dmtcp/include/mcmini/model/transitions/mutex/callbacks.hpp b/dmtcp/include/mcmini/model/transitions/mutex/callbacks.hpp new file mode 100644 index 00000000..e4927c0a --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/mutex/callbacks.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "mcmini/coordinator/model_to_system_map.hpp" +#include "mcmini/model/state.hpp" +#include "mcmini/model/transitions/mutex/mutex_init.hpp" +#include "mcmini/model/transitions/mutex/mutex_lock.hpp" +#include "mcmini/model/transitions/mutex/mutex_unlock.hpp" +#include "mcmini/real_world/mailbox/runner_mailbox.h" + +model::transition* mutex_init_callback(runner_id_t, + const volatile runner_mailbox&, + model_to_system_map&); +model::transition* mutex_lock_callback(runner_id_t, + const volatile runner_mailbox&, + model_to_system_map&); +model::transition* mutex_unlock_callback(runner_id_t, + const volatile runner_mailbox&, + model_to_system_map&); diff --git a/docs/design/include/mcmini/model/transitions/mutex/mutex_init.hpp b/dmtcp/include/mcmini/model/transitions/mutex/mutex_init.hpp similarity index 51% rename from docs/design/include/mcmini/model/transitions/mutex/mutex_init.hpp rename to dmtcp/include/mcmini/model/transitions/mutex/mutex_init.hpp index 957d5da8..c664b2af 100644 --- a/docs/design/include/mcmini/model/transitions/mutex/mutex_init.hpp +++ b/dmtcp/include/mcmini/model/transitions/mutex/mutex_init.hpp @@ -8,21 +8,22 @@ namespace transitions { struct mutex_init : public model::transition { private: - state::objid_t mutex_id; /* The mutex this transition initializes */ + const state::objid_t mutex_id; /* The mutex this transition initializes */ public: - mutex_init(state::objid_t mutex_id) : mutex_id(mutex_id) {} + mutex_init(runner_id_t executor, state::objid_t mutex_id) + : transition(executor), mutex_id(mutex_id) {} ~mutex_init() = default; status modify(model::mutable_state& s) const override { using namespace model::objects; - s.add_state_for(mutex_id, mutex::make(mutex::unlocked)); + s.add_state_for_obj(mutex_id, new mutex(mutex::unlocked)); return status::exists; } - + state::objid_t get_id() const { return this->mutex_id; } std::string to_string() const override { - return "mutex_init(" + std::to_string(mutex_id) + ")"; + return "pthread_mutex_init(mutex:" + std::to_string(mutex_id) + ")"; } }; } // namespace transitions -} // namespace model \ No newline at end of file +} // namespace model diff --git a/dmtcp/include/mcmini/model/transitions/mutex/mutex_lock.hpp b/dmtcp/include/mcmini/model/transitions/mutex/mutex_lock.hpp new file mode 100644 index 00000000..bff37451 --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/mutex/mutex_lock.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include "mcmini/model/objects/mutex.hpp" +#include "mcmini/model/transition.hpp" +#include "mcmini/model/transitions/mutex/mutex_unlock.hpp" +#include "mcmini/model/transitions/mutex/mutex_init.hpp" + +namespace model { +namespace transitions { + +struct mutex_lock : public model::transition { + private: + const state::objid_t mutex_id; /* The mutex this transition locks */ + + public: + mutex_lock(runner_id_t executor, state::objid_t mutex_id) + : transition(executor), mutex_id(mutex_id) {} + ~mutex_lock() = default; + + status modify(model::mutable_state& s) const override { + using namespace model::objects; + + // A `mutex_lock` cannot be applied to a mutex already locked. + const mutex* ms = s.get_state_of_object(mutex_id); + if (ms->is_locked()) { + return status::disabled; + } + s.add_state_for_obj(mutex_id, new mutex(mutex::locked, ms->get_location(), this->executor)); + return status::exists; + } + state::objid_t get_id() const { return this->mutex_id; } + std::string to_string() const override { + return "pthread_mutex_lock(mutex:" + std::to_string(mutex_id) + ")"; + } + + // MARK: Model checking functions + bool depends(const mutex_init* mi) const { + return this->mutex_id == mi->get_id(); + } + bool depends(const mutex_lock* ml) const { + return this->mutex_id == ml->get_id(); + } + bool coenabled_with(const mutex_unlock* mu) const { + return this->mutex_id != mu->get_id(); + } +}; +} // namespace transitions +} // namespace model diff --git a/dmtcp/include/mcmini/model/transitions/mutex/mutex_unlock.hpp b/dmtcp/include/mcmini/model/transitions/mutex/mutex_unlock.hpp new file mode 100644 index 00000000..b100089a --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/mutex/mutex_unlock.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "mcmini/model/objects/mutex.hpp" +#include "mcmini/model/transition.hpp" + +namespace model { +namespace transitions { + +struct mutex_unlock : public model::transition { + private: + const state::objid_t mutex_id; /* The mutex this transition unlocks */ + + public: + mutex_unlock(runner_id_t executor, state::objid_t mutex_id) + : transition(executor), mutex_id(mutex_id) {} + ~mutex_unlock() = default; + + status modify(model::mutable_state& s) const override { + using namespace model::objects; + // TODO: If the mutex already unlocked, this would be erroneous program + // behavior. We should distinguish between this and other cases. + const mutex* ms = s.get_state_of_object(mutex_id); + // Validate ownership + if (!ms->is_locked_by(this->executor)) { + return status::disabled; + } + + s.add_state_for_obj(mutex_id, new mutex(mutex::unlocked, ms->get_location(), 0)); + return status::exists; + } + state::objid_t get_id() const { return this->mutex_id; } + std::string to_string() const override { + return "pthread_mutex_unlock(mutex:" + std::to_string(mutex_id) + ")"; + } +}; +} // namespace transitions +} // namespace model diff --git a/dmtcp/include/mcmini/model/transitions/process/abort.hpp b/dmtcp/include/mcmini/model/transitions/process/abort.hpp new file mode 100644 index 00000000..31366576 --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/process/abort.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "mcmini/model/transition.hpp" + +namespace model { +namespace transitions { + +struct process_abort : public model::transition { + public: + process_abort(state::runner_id_t executor) : transition(executor) {} + ~process_abort() = default; + + status modify(model::mutable_state& s) const override { + // We ensure that aborting is never enabled. This ensures that it will never + // be explored by any model checking algorithm + return status::exists; + } + + bool aborts_program_execution() const override { return true; } + + std::string to_string() const override { return "abort(2) (syscall)"; } +}; + +} // namespace transitions +} // namespace model diff --git a/dmtcp/include/mcmini/model/transitions/process/callbacks.hpp b/dmtcp/include/mcmini/model/transitions/process/callbacks.hpp new file mode 100644 index 00000000..b83f6205 --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/process/callbacks.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "mcmini/coordinator/model_to_system_map.hpp" +#include "mcmini/model/transition.hpp" + +model::transition* process_exit_callback(runner_id_t, + const volatile runner_mailbox&, + model_to_system_map&); +model::transition* process_abort_callback(runner_id_t, + const volatile runner_mailbox&, + model_to_system_map&); diff --git a/dmtcp/include/mcmini/model/transitions/process/exit.hpp b/dmtcp/include/mcmini/model/transitions/process/exit.hpp new file mode 100644 index 00000000..e822bc20 --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/process/exit.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "mcmini/model/transition.hpp" + +namespace model { +namespace transitions { + +struct process_exit : public model::transition { + public: + int exit_code = -1; + process_exit(state::runner_id_t executor) : process_exit(executor, -1) {} + process_exit(state::runner_id_t executor, int exit_code) + : transition(executor), exit_code(exit_code) {} + ~process_exit() = default; + + status modify(model::mutable_state& s) const override { + // We ensure that exiting is never enabled. This ensures that it will never + // be explored by any model checking algorithm + return status::exists; + } + + int program_exit_code() const override { return exit_code; } + + std::string to_string() const override { return "exit(2) (syscall)"; } +}; + +} // namespace transitions +} // namespace model diff --git a/dmtcp/include/mcmini/model/transitions/semaphore/callbacks.hpp b/dmtcp/include/mcmini/model/transitions/semaphore/callbacks.hpp new file mode 100644 index 00000000..533c6e1b --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/semaphore/callbacks.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "mcmini/coordinator/model_to_system_map.hpp" +#include "mcmini/model/state.hpp" +#include "mcmini/real_world/mailbox/runner_mailbox.h" + +model::transition* sem_init_callback(runner_id_t, + const volatile runner_mailbox&, + model_to_system_map&); +model::transition* sem_post_callback(runner_id_t, + const volatile runner_mailbox&, + model_to_system_map&); +model::transition* sem_wait_callback(runner_id_t, + const volatile runner_mailbox&, + model_to_system_map&); +model::transition* sem_destroy_callback(runner_id_t, + const volatile runner_mailbox&, + model_to_system_map&); diff --git a/dmtcp/include/mcmini/model/transitions/semaphore/sem_destroy.hpp b/dmtcp/include/mcmini/model/transitions/semaphore/sem_destroy.hpp new file mode 100644 index 00000000..b5a32cf5 --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/semaphore/sem_destroy.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "mcmini/model/objects/semaphore.hpp" +#include "mcmini/model/transition.hpp" + +namespace model { +namespace transitions { + +struct sem_destroy : public model::transition { + private: + const state::objid_t sem_id; + + public: + sem_destroy(runner_id_t executor, state::objid_t sem_id) + : transition(executor), sem_id(sem_id), count(count) {} + ~sem_destroy() = default; + status modify(model::mutable_state& s) const override { + using namespace model::objects; + s.add_state_for_obj(sem_id, new semaphore(semaphore::destroyed)); + return status::exists; + } + state::objid_t get_id() const { return this->sem_id; } + std::string to_string() const override { + return "sem_init(semaphore:" + std::to_string(sem_id) + ")"; + } +}; +} // namespace transitions +} // namespace model diff --git a/dmtcp/include/mcmini/model/transitions/semaphore/sem_init.hpp b/dmtcp/include/mcmini/model/transitions/semaphore/sem_init.hpp new file mode 100644 index 00000000..9a6c1abe --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/semaphore/sem_init.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "mcmini/model/objects/semaphore.hpp" +#include "mcmini/model/transition.hpp" + +namespace model { +namespace transitions { + +struct sem_init : public model::transition { + private: + const state::objid_t sem_id; + unsigned count = 0; + + public: + sem_init(runner_id_t executor, state::objid_t sem_id) + : sem_init(executor, sem_id, 0) {} + sem_init(runner_id_t executor, state::objid_t sem_id, unsigned count) + : transition(executor), sem_id(sem_id), count(count) {} + ~sem_init() = default; + status modify(model::mutable_state& s) const override { + using namespace model::objects; + s.add_state_for_obj(sem_id, new semaphore(count)); + return status::exists; + } + state::objid_t get_id() const { return this->sem_id; } + std::string to_string() const override { + return "sem_init(semaphore:" + std::to_string(sem_id) + ")"; + } +}; +} // namespace transitions +} // namespace model diff --git a/dmtcp/include/mcmini/model/transitions/semaphore/sem_post.hpp b/dmtcp/include/mcmini/model/transitions/semaphore/sem_post.hpp new file mode 100644 index 00000000..88045ff8 --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/semaphore/sem_post.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "mcmini/model/objects/semaphore.hpp" +#include "mcmini/model/transition.hpp" + +namespace model { +namespace transitions { + +struct sem_post : public model::transition { + private: + const state::objid_t sem_id; + + public: + sem_post(runner_id_t executor, state::objid_t sem_id) + : transition(executor), sem_id(sem_id) {} + ~sem_post() = default; + status modify(model::mutable_state& s) const override { + using namespace model::objects; + const semaphore* sem = s.get_state_of_object(sem_id); + s.add_state_for_obj(sem_id, new semaphore(sem->count() + 1)); + return status::exists; + } + state::objid_t get_id() const { return this->sem_id; } + std::string to_string() const override { + return "sem_post(semaphore:" + std::to_string(sem_id) + ")"; + } +}; +} // namespace transitions +} // namespace model diff --git a/dmtcp/include/mcmini/model/transitions/semaphore/sem_wait.hpp b/dmtcp/include/mcmini/model/transitions/semaphore/sem_wait.hpp new file mode 100644 index 00000000..3dd2a360 --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/semaphore/sem_wait.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "mcmini/model/objects/semaphore.hpp" +#include "mcmini/model/transition.hpp" + +namespace model { +namespace transitions { + +struct sem_wait : public model::transition { + private: + const state::objid_t sem_id; + + public: + sem_wait(runner_id_t executor, state::objid_t sem_id) + : transition(executor), sem_id(sem_id) {} + ~sem_wait() = default; + status modify(model::mutable_state& s) const override { + using namespace model::objects; + const semaphore* sem = s.get_state_of_object(sem_id); + if (sem->will_block()) { + return status::disabled; + } + s.add_state_for_obj(sem_id, new semaphore(sem->count() - 1)); + return status::exists; + } + state::objid_t get_id() const { return this->sem_id; } + std::string to_string() const override { + return "sem_wait(semaphore:" + std::to_string(sem_id) + ")"; + } +}; +} // namespace transitions +} // namespace model diff --git a/dmtcp/include/mcmini/model/transitions/thread/callbacks.hpp b/dmtcp/include/mcmini/model/transitions/thread/callbacks.hpp new file mode 100644 index 00000000..cc064624 --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/thread/callbacks.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "mcmini/coordinator/model_to_system_map.hpp" +#include "mcmini/model/state.hpp" +#include "mcmini/model/transitions/thread/thread_create.hpp" +#include "mcmini/model/transitions/thread/thread_exit.hpp" +#include "mcmini/model/transitions/thread/thread_join.hpp" +#include "mcmini/model/transitions/thread/thread_start.hpp" +#include "mcmini/real_world/mailbox/runner_mailbox.h" + +model::transition* thread_create_callback(runner_id_t, + const volatile runner_mailbox&, + model_to_system_map&); +model::transition* thread_exit_callback(runner_id_t, + const volatile runner_mailbox&, + model_to_system_map&); +model::transition* thread_join_callback(runner_id_t, + const volatile runner_mailbox&, + model_to_system_map&); diff --git a/dmtcp/include/mcmini/model/transitions/thread/thread_create.hpp b/dmtcp/include/mcmini/model/transitions/thread/thread_create.hpp new file mode 100644 index 00000000..52a29c61 --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/thread/thread_create.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "mcmini/model/objects/thread.hpp" +#include "mcmini/model/transition.hpp" + +namespace model { +namespace transitions { + +struct thread_create : public model::transition { + private: + state::runner_id_t target; + + public: + thread_create(state::runner_id_t executor, state::runner_id_t target) + : transition(executor), target(target) {} + ~thread_create() = default; + + status modify(model::mutable_state& s) const override { + using namespace model::objects; + s.add_state_for_runner(target, new thread(thread::running)); + return status::exists; + } + state::runner_id_t get_target() const { return target; } + std::string to_string() const override { + return "pthread_create(thread: " + std::to_string(target) + ")"; + } + + // MARK: DPOR Methods + bool depends(const model::transition* t) const { + return this->target == t->get_executor(); + } + bool coenabled_with(const model::transition *t) const { + return this->target != t->get_executor(); + } +}; + +} // namespace transitions +} // namespace model diff --git a/dmtcp/include/mcmini/model/transitions/thread/thread_exit.hpp b/dmtcp/include/mcmini/model/transitions/thread/thread_exit.hpp new file mode 100644 index 00000000..7509ff22 --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/thread/thread_exit.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "mcmini/defines.h" +#include "mcmini/model/objects/thread.hpp" +#include "mcmini/model/transition.hpp" + +namespace model { +namespace transitions { + +struct thread_exit : public model::transition { + public: + thread_exit(state::runner_id_t executor) : transition(executor) {} + ~thread_exit() = default; + + status modify(model::mutable_state& s) const override { + using namespace model::objects; + auto* thread_state = s.get_state_of_runner(executor); + if (!thread_state->is_running() || executor == RID_MAIN_THREAD) { + return status::disabled; + } + s.add_state_for_runner(executor, new thread(thread::exited)); + return status::exists; + } + + std::string to_string() const override { return "exits"; } +}; + +} // namespace transitions +} // namespace model diff --git a/dmtcp/include/mcmini/model/transitions/thread/thread_join.hpp b/dmtcp/include/mcmini/model/transitions/thread/thread_join.hpp new file mode 100644 index 00000000..4edd1d5a --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/thread/thread_join.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "mcmini/model/objects/thread.hpp" +#include "mcmini/model/transition.hpp" + +namespace model { +namespace transitions { + +struct thread_join : public model::transition { + private: + state::runner_id_t target; + + public: + thread_join(state::runner_id_t executor, state::runner_id_t target) + : transition(executor), target(target) {} + ~thread_join() = default; + + status modify(model::mutable_state& s) const override { + using namespace model::objects; + auto* target_state = s.get_state_of_runner(target); + return target_state->has_exited() ? status::exists : status::disabled; + } + state::runner_id_t get_target() const { return target; } + std::string to_string() const override { + return "pthread_join(thread: " + std::to_string(target) + ")"; + } + + // MARK: Dependencies + bool depends(const model::transition* t) const { + return this->target == t->get_executor(); + } + bool coenabled_with(const model::transition* t) const { + return this->target != t->get_executor(); + } +}; + +} // namespace transitions +} // namespace model diff --git a/dmtcp/include/mcmini/model/transitions/thread/thread_start.hpp b/dmtcp/include/mcmini/model/transitions/thread/thread_start.hpp new file mode 100644 index 00000000..db9821ea --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/thread/thread_start.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "mcmini/model/objects/thread.hpp" +#include "mcmini/model/transition.hpp" + +namespace model { +namespace transitions { + +struct thread_start : public model::transition { + public: + thread_start(state::runner_id_t executor) : transition(executor) {} + ~thread_start() = default; + + status modify(model::mutable_state& s) const override { + // No modification necessary: we simply move into the next state + using namespace model::objects; + auto* thread_state = s.get_state_of_runner(executor); + return thread_state->is_embryo() ? status::disabled : status::exists; + } + + std::string to_string() const override { return "starts"; } +}; + +} // namespace transitions +} // namespace model diff --git a/dmtcp/include/mcmini/model/transitions/transition_sequence.hpp b/dmtcp/include/mcmini/model/transitions/transition_sequence.hpp new file mode 100644 index 00000000..ad2c5228 --- /dev/null +++ b/dmtcp/include/mcmini/model/transitions/transition_sequence.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include + +#include "mcmini/model/transition.hpp" + +namespace model { + +/** + * @brief A sequence of transitions. + * + * A _transition sequence_ describes a sequence of consecutive executions. The + * order that transitions appear in the sequence corresponds to how those + * transitions were executed one after another in a program undergoing + * verification. However, a transition sequence is a sequence and _only_ a + * sequence of transitions; it is not guaranteed to represent a possible + * execution path of a (or any) program; that is, there are no constraints on + * the enabled-ness of transitions in the sequence. + */ +class transition_sequence final { + private: + std::vector contents; + + public: + using index = size_t; + transition_sequence() = default; + ~transition_sequence(); + + auto begin() -> decltype(contents.begin()) { return contents.begin(); } + auto end() -> decltype(contents.end()) { return contents.end(); } + auto begin() const -> decltype(contents.begin()) { return contents.begin(); } + auto end() const -> decltype(contents.end()) { return contents.end(); } + + bool empty() const { return contents.empty(); } + size_t count() const { return contents.size(); } + const transition* at(size_t i) const { return contents.at(i); } + const transition* back() const { return contents.back(); } + std::unique_ptr extract_at(size_t i); + void push(const transition* t) { contents.push_back(t); } + void consume_into_subsequence(uint32_t depth); +}; + +} // namespace model diff --git a/docs/design/include/mcmini/model/visible_object.hpp b/dmtcp/include/mcmini/model/visible_object.hpp similarity index 58% rename from docs/design/include/mcmini/model/visible_object.hpp rename to dmtcp/include/mcmini/model/visible_object.hpp index d2dd020a..df2ca5b6 100644 --- a/docs/design/include/mcmini/model/visible_object.hpp +++ b/dmtcp/include/mcmini/model/visible_object.hpp @@ -26,44 +26,56 @@ namespace model { */ class visible_object final { private: - std::vector> history; + std::vector history; /** - *@brief Construct a visible object with the given history _history_. + * @brief Construct a visible object with the given history _history_. */ - visible_object( - std::vector> history) + visible_object(std::vector history) : history(std::move(history)) {} visible_object(const visible_object &other, size_t num_states) { - *this = other.slice(num_states); + *this = other.copy_slice(num_states); } public: visible_object() = default; visible_object(visible_object &&) = default; visible_object &operator=(visible_object &&) = default; - visible_object(std::unique_ptr &&initial_state) { - push_state(std::move(initial_state)); + visible_object(const visible_object_state *initial_state) { + push_state(initial_state); } + visible_object(std::unique_ptr initial_state) + : visible_object(initial_state.release()) {} visible_object(const visible_object &other) { - *this = other.slice(other.get_num_states()); + *this = other.copy_slice(other.get_num_states()); } visible_object &operator=(const visible_object &other) { - return *this = *other.clone(); + this->history.reserve(other.get_num_states()); + for (size_t j = 0; j < other.get_num_states(); j++) + this->history.push_back(other.history.at(j)->clone().release()); + return *this; } + ~visible_object(); public: size_t get_num_states() const { return history.size(); } + size_t get_last_state_index() const { return history.size() - 1; } const visible_object_state *state_at(size_t i) const { - return this->history.at(i).get(); + return this->history.at(i); } const visible_object_state *get_current_state() const { - return this->history.back().get(); - } - void push_state(std::unique_ptr s) { - history.push_back(std::move(s)); + return this->history.back(); } + void push_state(const visible_object_state *s) { history.push_back(s); } + /** + * @brief Produces a visible object with the first `num_states` states of this + * visible object. + * + * The object will be left with the states `[0, index]` (inclusive). + */ + void slice(size_t index); + /** * @brief Produces a visible object with the first `num_states` states of this * visible object. @@ -73,28 +85,30 @@ class visible_object final { * @returns a visible object with identical states as this visible object for * the first `num_states` states. */ - visible_object slice(size_t num_states) const { - auto sliced_states = - std::vector>(); + visible_object copy_slice(size_t num_states) const { + std::vector sliced_states; sliced_states.reserve(num_states); - for (int j = 0; j < num_states; j++) { - sliced_states.push_back( - extensions::to_const_unique_ptr(history.at(j)->clone())); + for (size_t j = 0; j < num_states; j++) { + sliced_states.push_back(history.at(j)->clone().release()); } return visible_object(std::move(sliced_states)); } /// @brief Extracts the current state from this object. - /// @return a pointer to the current state of this object - std::unique_ptr consume_into_current_state() && { + /// @return a pointer to the current state of this object, or `nullptr` is the + /// object contains no states. After calling this method, the visible object + /// should be considered destroyed + std::unique_ptr consume_into_current_state() { if (history.empty()) { return nullptr; } - return std::move(history.back()); + auto result = std::unique_ptr(history.back()); + history.pop_back(); + return result; } std::unique_ptr clone() const { return extensions::make_unique(*this); } }; -} // namespace model \ No newline at end of file +} // namespace model diff --git a/docs/design/include/mcmini/model/visible_object_state.hpp b/dmtcp/include/mcmini/model/visible_object_state.hpp similarity index 74% rename from docs/design/include/mcmini/model/visible_object_state.hpp rename to dmtcp/include/mcmini/model/visible_object_state.hpp index 092e44f2..bb5919cf 100644 --- a/docs/design/include/mcmini/model/visible_object_state.hpp +++ b/dmtcp/include/mcmini/model/visible_object_state.hpp @@ -5,10 +5,6 @@ #include namespace model { - -/** - * @brief A capture of the state of a particular visible object. - */ class visible_object_state { public: virtual ~visible_object_state() = default; @@ -16,4 +12,4 @@ class visible_object_state { virtual std::string to_string() const = 0; }; -} // namespace model \ No newline at end of file +} // namespace model diff --git a/docs/design/include/mcmini/model_checking/algorithm.hpp b/dmtcp/include/mcmini/model_checking/algorithm.hpp similarity index 65% rename from docs/design/include/mcmini/model_checking/algorithm.hpp rename to dmtcp/include/mcmini/model_checking/algorithm.hpp index cbba328d..5fce879f 100644 --- a/docs/design/include/mcmini/model_checking/algorithm.hpp +++ b/dmtcp/include/mcmini/model_checking/algorithm.hpp @@ -1,7 +1,12 @@ #pragma once +#include + #include "mcmini/coordinator/coordinator.hpp" +#include "mcmini/model/exception.hpp" #include "mcmini/model/program.hpp" +#include "mcmini/model_checking/stats.hpp" +#include "mcmini/real_world/process.hpp" namespace model_checking { @@ -11,15 +16,23 @@ namespace model_checking { */ class algorithm { public: - // TODO: Eventually we may want to pass more information to the callbacks - // (e.g. the algorithm itself or the coordinator) to provide detailed printing - // information. - // TODO: struct callbacks { public: - virtual void encountered_deadlock_in(const model::program &) {} - virtual void encountered_crash_in(const model::program &) {} - virtual void encountered_data_race_in(const model::program &) {} + callbacks() = default; + std::function crash; + std::function deadlock; + std::function data_race; + std::function unknown_error; + std::function trace_completed; + std::function + abnormal_termination; + std::function + nonzero_exit_code; + std::function + undefined_behavior; }; /** @@ -59,4 +72,4 @@ class algorithm { } }; -}; // namespace model_checking \ No newline at end of file +}; // namespace model_checking diff --git a/dmtcp/include/mcmini/model_checking/algorithms/classic_dpor.hpp b/dmtcp/include/mcmini/model_checking/algorithms/classic_dpor.hpp new file mode 100644 index 00000000..931e0013 --- /dev/null +++ b/dmtcp/include/mcmini/model_checking/algorithms/classic_dpor.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include "mcmini/misc/ddt.hpp" +#include "mcmini/model_checking/algorithm.hpp" +#include "mcmini/model_checking/algorithms/classic_dpor/runner_item.hpp" +#include "mcmini/model_checking/algorithms/classic_dpor/stack_item.hpp" + +namespace model_checking { + +/** + * @brief A model-checking algorithm which performs verification using the + * algorithm of Flanagan and Godefroid (2005). + */ +class classic_dpor final : public algorithm { + public: + using dependency_relation_type = + double_dispatch_member_function_table; + + using coenabled_relation_type = + double_dispatch_member_function_table; + + void verify_using(coordinator &, const callbacks &) override; + void verify_using(coordinator &coordinator) { + callbacks no_callbacks; + this->verify_using(coordinator, no_callbacks); + } + static dependency_relation_type default_dependencies(); + static coenabled_relation_type default_coenabledness(); + + struct configuration { + dependency_relation_type dependency_relation = + classic_dpor::default_dependencies(); + coenabled_relation_type coenabled_relation = + classic_dpor::default_coenabledness(); + uint32_t maximum_total_execution_depth = 1500; + bool assumes_linear_program_flow = false; + }; + + classic_dpor() = default; + classic_dpor(configuration config) : config(std::move(config)) {} + + private: + configuration config; + + bool are_dependent(const model::transition &t1, + const model::transition &t2) const; + + bool are_coenabled(const model::transition &t1, + const model::transition &t2) const; + + bool are_independent(const model::transition &t1, + const model::transition &t2) const { + return !are_dependent(t1, t2); + } + + // Do not call these methods directly. They are implementation details of + // the DPOR algorithm and are called at specific points in time! + + struct dpor_context; + + bool happens_before(const dpor_context &, size_t i, size_t j) const; + bool happens_before_thread(const dpor_context &, size_t i, + runner_id_t p) const; + bool threads_race_after(const dpor_context &context, size_t i, runner_id_t q, + runner_id_t p) const; + + clock_vector accumulate_max_clock_vector_against(const model::transition &, + const dpor_context &) const; + + void continue_dpor_by_expanding_trace_with(runner_id_t p, dpor_context &); + void grow_stack_after_running(dpor_context &); + void dynamically_update_backtrack_sets(dpor_context &); + + bool dynamically_update_backtrack_sets_at_index( + const dpor_context &, const model::transition &S_i, + const model::transition &nextSP, stack_item &preSi, size_t i, + runner_id_t p); +}; + +} // namespace model_checking diff --git a/dmtcp/include/mcmini/model_checking/algorithms/classic_dpor/clock_vector.hpp b/dmtcp/include/mcmini/model_checking/algorithms/classic_dpor/clock_vector.hpp new file mode 100644 index 00000000..9c5ed2b0 --- /dev/null +++ b/dmtcp/include/mcmini/model_checking/algorithms/classic_dpor/clock_vector.hpp @@ -0,0 +1,115 @@ +#pragma once + +#include +#include +#include + +#include "mcmini/model/defines.hpp" +#include "mcmini/model/transitions/transition_sequence.hpp" + +namespace model_checking { +/** + * @brief A multi-dimensional vector used to keep track of "time" between + * dependencies in a model checking trace + * + * A clock vector is simply a vector of integer components. They are used in + * message-passing systems to determine if an event in a distributed system + * "causally-happens-before" another. But clock vectors are more generally + * applicable: they can be used to determine a "happens-before" partial order + * even in the absence of explicit "send" and "receive" messages. McMini, for + * example, uses clock vectors to determine the "happens-before" relation among + * transitions in a transition sequence + * + * A `clock_vector` implicitly maps a thread id it does not contain to a default + * value of `0`. Thus, a `clock_vector` is "lazy" in the sense that new threads + * are "automatically" mapped without needing to be explicitly added the clock + * vector when the thread is created + */ +struct clock_vector final { + private: + std::unordered_map contents; + + public: + using index = model::transition_sequence::index; + static constexpr index invalid = -1; + + clock_vector() = default; + clock_vector(const clock_vector &) = default; + clock_vector(clock_vector &&) = default; + clock_vector &operator=(const clock_vector &) = default; + clock_vector &operator=(clock_vector &&) = default; + + /** + * @brief The number of components in this clock vector + * + * The `clock_vector` is "lazy" in the sense that + * new components must be explicitly added to the + * clock vector: the clock vector is not of an + * initial, fixed size. Thus two `clock_vector`s may + * have different sizes + * + * @return the number of elements in the clock vector + */ + uint32_t size() const { return this->contents.size(); } + + index &operator[](runner_id_t tid) { + // NOTE: The `operator[]` overload of + // unordered_map will create a new key-value + // pair if `tid` does not exist and will use + // a _default_ value for the value (0 in this case) + // which is actually what we want here + return this->contents[tid]; + } + + /** + * @brief Retrieves the value for the thread if mapped by this clock vector + * + * @param tid the id of the thread to check for a mapping + * @return the value for the thread if mapped by this clock + * vector or `0` if the clock vector does not have an explicit mapping for the + * thread + */ + index value_for(runner_id_t tid) const { + const auto iter = this->contents.find(tid); + if (iter != this->contents.end()) return iter->second; + return 0; + } + + bool has_explicit_mapping_for(runner_id_t rid) const { + return this->contents.count(rid) != 0; + } + + bool has_implicit_mapping_for(runner_id_t rid) const { + return this->contents.count(rid) == 0; + } + + /** + * @brief Computes a clock vector whose components + * are larger than the components of both of + * the given clock vectors + * + * The maximum of two clock vectors is definied to be the clock vector whose + * components are the maxmimum of the corresponding components of the + * arguments. Since the `clock_vector` class is "lazy", the two clock vectors + * given as arguments may not be of the same size. The resultant clock vector + * has components as follows: + * + * 1. For each thread that each clock vector maps, the resulting clock vector + * maps that thread to the maxmimum of the values mapped to the thread by each + * clock vector + * + * 2. For each thread that only a single clock vector maps, the resulting + * clock vector maps that thread to the value mapped by the lone clock vector + * + * The scheme is equivalent to assuming that an unmapped thread by any one + * clock vector is implicitly mapped to zero + * + * @param cv1 the first clock vector + * @param cv2 the second clock vector + * @return clock_vector a clock vector whose components are at least as large + * as the corresponding components of each clock vector and whose size is + * large enough to contain the union of components of each clock vector + */ + static clock_vector max(const clock_vector &cv1, const clock_vector &cv2); +}; +} // namespace model_checking diff --git a/dmtcp/include/mcmini/model_checking/algorithms/classic_dpor/runner_item.hpp b/dmtcp/include/mcmini/model_checking/algorithms/classic_dpor/runner_item.hpp new file mode 100644 index 00000000..3284ca74 --- /dev/null +++ b/dmtcp/include/mcmini/model_checking/algorithms/classic_dpor/runner_item.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include +#include + +#include "mcmini/model_checking/algorithms/classic_dpor/clock_vector.hpp" + +namespace model_checking { +/** + * @brief A simple C-like struct that McMini associates + * with each thread created in the target process + */ +struct runner_item final { + private: + uint32_t executionDepth = 0u; + clock_vector cv; + + public: + runner_item() = default; + uint32_t get_execution_depth() const; + void increment_execution_depth() { executionDepth++; } + void decrement_execution_depth_if_possible() { + executionDepth = std::min(0u, executionDepth - 1); + } + const clock_vector& get_clock_vector() const { return cv; } + void set_clock_vector(const clock_vector& new_cv) { cv = new_cv; } +}; + +} // namespace model_checking diff --git a/dmtcp/include/mcmini/model_checking/algorithms/classic_dpor/stack_item.hpp b/dmtcp/include/mcmini/model_checking/algorithms/classic_dpor/stack_item.hpp new file mode 100644 index 00000000..8e41fb50 --- /dev/null +++ b/dmtcp/include/mcmini/model_checking/algorithms/classic_dpor/stack_item.hpp @@ -0,0 +1,251 @@ +#pragma once +#include +#include +#include + +#include "mcmini/defines.h" +#include "mcmini/model_checking/algorithms/classic_dpor/clock_vector.hpp" + +namespace model_checking { + +/** + * @brief Associates information relevant to DPOR about a state it has searched + * + * The `stack_item` is a light-weight representation of a state explored by + * McMini. McMini is a stateless model checker, meaning that is does not + * "remember" or cache the previous states of the visible objects it manages. + * However, DPOR requires that some information be persisted during the + * state-space search in order to function correctly. + * + * Each `stack_item` persists the state's backtracking set, done set, and sleep + * set. Each state explored by McMini has exactly one representative + * `stack_item`. McMini will adjust the state's trio of sets as it continues to + * explore the state space and execute more transitions + */ +struct stack_item final { + private: + /** + * @brief The clock vector associated with the transition _resulting_ in this + * state + * + * @note See line 14.5 of the original DPOR paper. This clock vector + * corresponds to the value set for |S'| : + * + * """" + * let C' = C[p := cv2, |S'| := cv2] + * """" + * + * cv here refers to the `cv2` value computed at some point in time prior to + * creating this stack item. This object merely stores it for later use + */ + const clock_vector cv; + + /** + * @brief The clock vector associated with each runner in this state + * + * @note See line 14.5 of the original DPOR paper. Here, instead of keeping + * around a single per-vector clock for the threads for _all_ state items, we + * instead store one _per_ state item. This eases DPOR model-checking state + * regeneration. + */ + std::array per_runner_clocks; + + /// @brief The transition which DPOR decided to schedule from this state. + /// + /// @note the item does not own this transition; it is instead owned by the + /// trace of the model which the DPOR algorithm manipulates. + const model::transition *out_transition = nullptr; + + /** + * @brief A collection of threads that are scheduled to be run from this state + * to continune to state-space search + * + * @invariant a thread is contained in exactly one of either the backtracking + * set, the done set, or the sleep set + */ + std::unordered_set backtrack_set; + + /** + * @brief A collection of threads that have executed from this state so far + * + * @invariant a thread is contained in exactly one of either the backtracking + * set, the done set, or the sleep set + */ + std::unordered_set done_set; + + /** + * @brief A collection of threads that do + * not need to be executed from this state + * + * The technical definition of a sleep set is + * a collection of transitions. However, we can + * exploit a clever observation + * + * @invariant a thread is contained in + * exactly one of either the backtracking set, + * the done set, or the sleep set + * + * @invariant for any given thread, at most + * one transition can be contained in the sleep + * set. If such a transition exists, it is the + * _next_ transition in this state that will be + * run by that thread + */ + std::unordered_set sleep_set; + + /** + * @brief A cache of threads that are enabled in this state + * + * DPOR needs to know which threads are enabled in an + * arbitrary state in the current DFS search. It is important + * that the state(s) of the visible object(s) McMini + * manages reflect what they were at a particular moment in + * time during the state-space search since a transition's + * status for being enabled can change between state changes + * (consider e.g. a mutex lock becoming disabled after a different + * thread acquires the mutex before this thread does). + * + * Since DPOR performs stateless model checking, McMini can only + * represent a *single* state at any given time. Thus, we can either + * + * A. Re-generate the past states of objects by reseting the current + * state and re-playing the transitions in the transition stack + * leading up to that state; or + * + * B. Compute which threads were enabled when the state was + * encountered and store them for later use + * + * The former is very expensive and would complicate the implementation even + * further. Thus, we opt for the latter choice. + */ + std::unordered_set enabled_runners; + + public: + stack_item() : stack_item(clock_vector()) {} + stack_item(clock_vector cv) : cv(std::move(cv)) {} + stack_item(clock_vector cv, std::unordered_set enabled_runners) + : cv(std::move(cv)), enabled_runners(std::move(enabled_runners)) {} + stack_item( + clock_vector cv, + std::array per_runner_clocks, + const model::transition *out_transition, + std::unordered_set enabled_runners) + : cv(std::move(cv)), + per_runner_clocks(std::move(per_runner_clocks)), + out_transition(out_transition), + enabled_runners(std::move(enabled_runners)) {} + + bool backtrack_set_empty() const { return backtrack_set.empty(); } + bool backtrack_set_contains(runner_id_t id) const { + return backtrack_set.count(id) != 0; + } + bool sleep_set_contains(runner_id_t rid) const { + return sleep_set.count(rid) != 0; + } + bool done_set_contains(runner_id_t rid) const { + return done_set.count(rid) != 0; + } + bool has_enabled_runners() const { return !this->enabled_runners.empty(); } + + /** + * @brief Places the given thread into the backtrack set. + * + * If the thread is already contained in the done set, the thread will + * not be re-added to the backtracking set and this method acts as a no-op. + * Otherwise the given thread will be added to the backtracking set associated + * with this state and will be scheduled to execute from this state. + * + * @param rid the thread to add to the backtracking set + * + * @note McMini will ensure that the given thread is running a transition + * that's enabled. This, however, is not enforced by this method; rather, + * threads added to the backtrack set using this method are simply cached for + * later use by McMini and DPOR + */ + void insert_into_backtrack_set_unless_completed(runner_id_t rid) { + if (!done_set_contains(rid)) backtrack_set.insert(rid); + } + void insert_into_sleep_set(runner_id_t rid) { sleep_set.insert(rid); } + + /** + * @brief Moves a thread into the done set + * + * When a thread is moved into the done set, the thread will not be scheduled + * to execute from this state in future depth-first state-space searches that + * include this state. + * + * @param rid the thread to move to the done set + */ + void mark_searched(runner_id_t rid) { + this->done_set.insert(rid); + this->backtrack_set.erase(rid); + } + + /** + * @brief Caches the threads in the given set + * for later use by DPOR when determining enabled + * threads in this state + * + * @param runners a collection of runners that are enabled in this state + * + * @note McMini will ensure that threads marked enabled in this state are + * indeed enabled. This, however, is not enforced by this method; rather, + * threads marked as enabled are simply cached for later use by McMini and + * DPOR + */ + void mark_enabled_in_state(const std::unordered_set &runners) { + for (const runner_id_t rid : runners) this->enabled_runners.insert(rid); + } + + runner_id_t backtrack_set_pop() { + if (backtrack_set_empty()) + throw std::runtime_error("There are no more threads to backtrack on"); + runner_id_t backtrack_thread = *this->backtrack_set.begin(); + this->mark_searched(backtrack_thread); + return backtrack_thread; + } + + runner_id_t backtrack_set_pop_first() { + if (backtrack_set_empty()) + throw std::runtime_error("There are no more threads to backtrack on"); + + // NOTE: We arbitrarily always pick the smallest thread to provide + // determinism + runner_id_t backtrack_thread = *this->backtrack_set.begin(); + for (const runner_id_t rid : this->backtrack_set) + backtrack_thread = std::min(rid, backtrack_thread); + + this->mark_searched(backtrack_thread); + return backtrack_thread; + } + + clock_vector get_clock_vector() const { return this->cv; } + const std::array + &get_per_runner_clocks() const { + return this->per_runner_clocks; + } + runner_id_t get_first_enabled_runner() const { + runner_id_t r = RUNNER_ID_MAX; + for (runner_id_t p : this->enabled_runners) { + r = std::min(r, p); + } + return r; + } + const std::unordered_set &get_enabled_runners() const { + return this->enabled_runners; + } + const std::unordered_set &get_sleep_set() const { + return this->sleep_set; + } + const std::unordered_set &get_backtrack_set() const { + return this->backtrack_set; + } + const model::transition *get_out_transition() const { + return this->out_transition; + } + void set_out_transition(const model::transition *out) { + this->out_transition = out; + } +}; + +} // namespace model_checking diff --git a/dmtcp/include/mcmini/model_checking/stats.hpp b/dmtcp/include/mcmini/model_checking/stats.hpp new file mode 100644 index 00000000..e599e033 --- /dev/null +++ b/dmtcp/include/mcmini/model_checking/stats.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include + +namespace model_checking { +struct stats { + uint32_t trace_id = 1; + uint32_t total_transitions = 0; +}; +} // namespace model_checking diff --git a/docs/design/include/mcmini/plugins.h b/dmtcp/include/mcmini/plugins.h similarity index 91% rename from docs/design/include/mcmini/plugins.h rename to dmtcp/include/mcmini/plugins.h index 8029ae2c..2edcb666 100644 --- a/docs/design/include/mcmini/plugins.h +++ b/dmtcp/include/mcmini/plugins.h @@ -6,4 +6,4 @@ * binaries. See the ld(2) man page for how McMini should be compiled (`-E` * and/or `--dynamic-list-cpp-typeinfo` and`--dynamic-list-cpp-new`) */ -void mcmini_plugin_launch(); \ No newline at end of file +void mcmini_plugin_launch(); diff --git a/docs/design/include/mcmini/plugins/loader.hpp b/dmtcp/include/mcmini/plugins/loader.hpp similarity index 93% rename from docs/design/include/mcmini/plugins/loader.hpp rename to dmtcp/include/mcmini/plugins/loader.hpp index 5995cbf1..b9252325 100644 --- a/docs/design/include/mcmini/plugins/loader.hpp +++ b/dmtcp/include/mcmini/plugins/loader.hpp @@ -12,4 +12,4 @@ namespace plugins { */ class loader final {}; -}; // namespace plugins \ No newline at end of file +}; // namespace plugins diff --git a/dmtcp/include/mcmini/real_world/dmtcp_target.hpp b/dmtcp/include/mcmini/real_world/dmtcp_target.hpp new file mode 100644 index 00000000..517e31f1 --- /dev/null +++ b/dmtcp/include/mcmini/real_world/dmtcp_target.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include + +#include "mcmini/real_world/target.hpp" + +namespace real_world { + +struct dmtcp_target : public target { + protected: + // Whether the environment should be propagated by `dmtcp_restart` + bool propagate_env_through_dmtcp = true; + + // The path to the checkpoint file + std::string ckpt_file_path; + + // A handle to the `dmtcp_env.txt` file containing each line of the evironment + // which will be propogated to the + std::fstream dmtcp_env_file; + + private: + /// @brief A cache variable which is used to determine whether new environment + /// variables have been set since the last time the file was written to + /// + /// To avoid writing to the environment file after each invocation, if the + /// environment hasn't changed since the last invocation, we can simply skip + /// the rewrite since the environment variables are as is. + std::unordered_map setenv_file_vars; + std::unordered_set unsetenv_file_vars; + + public: + dmtcp_target() = default; + explicit dmtcp_target(const std::string &target_program, + const std::vector &target_program_args) + : target(target_program, target_program_args) {} + explicit dmtcp_target(const std::string &target_program, + const std::string &ckpt_file_path) + : dmtcp_target(target_program, std::vector{}, + ckpt_file_path) {} + explicit dmtcp_target(const std::string &target_program, + const std::vector &target_program_args, + const std::string &ckpt_file_path) + : target(target_program, target_program_args) { + this->target_program_args.push_back(ckpt_file_path); + } + pid_t launch_dont_wait() override; +}; + +} // namespace real_world diff --git a/dmtcp/include/mcmini/real_world/fifo.hpp b/dmtcp/include/mcmini/real_world/fifo.hpp new file mode 100644 index 00000000..a3ae1b28 --- /dev/null +++ b/dmtcp/include/mcmini/real_world/fifo.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +namespace real_world { + +/** + * @brief A handle to FIFO/named pipe. + */ +struct fifo { + private: + int fd = -1; + std::string name; + + public: + fifo(const std::string&); + fifo(const fifo&) = delete; + fifo(fifo&&) = default; + fifo& operator=(const fifo&) = delete; + fifo& operator=(fifo&&) = default; + ~fifo(); + + size_t read(void* buf, size_t size) const; + size_t write(const void* buf, size_t size) const; + + template + size_t read(T* val) const { + return read(val, sizeof(T)); + } + template + size_t write(const T* val) const { + return write(val, sizeof(T)); + } +}; + +} // namespace real_world diff --git a/dmtcp/include/mcmini/real_world/mailbox/runner_mailbox.h b/dmtcp/include/mcmini/real_world/mailbox/runner_mailbox.h new file mode 100644 index 00000000..8f864b55 --- /dev/null +++ b/dmtcp/include/mcmini/real_world/mailbox/runner_mailbox.h @@ -0,0 +1,27 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +typedef struct { + sem_t model_side_sem; + sem_t child_side_sem; + uint32_t type; + uint8_t cnts[64]; // TODO: How much space should each thread have to write + // payloads? +} runner_mailbox, *runner_mailbox_ref; + +void mc_runner_mailbox_init(volatile runner_mailbox *); +void mc_runner_mailbox_destroy(volatile runner_mailbox *); +int mc_wait_for_thread(volatile runner_mailbox *); +int mc_wait_for_scheduler(volatile runner_mailbox *); +int mc_wake_thread(volatile runner_mailbox *); +int mc_wake_scheduler(volatile runner_mailbox *); + +#ifdef __cplusplus +} +#endif // extern "C" diff --git a/dmtcp/include/mcmini/real_world/process.hpp b/dmtcp/include/mcmini/real_world/process.hpp new file mode 100644 index 00000000..993120a4 --- /dev/null +++ b/dmtcp/include/mcmini/real_world/process.hpp @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include + +#include "mcmini/model/defines.hpp" +#include "mcmini/real_world/mailbox/runner_mailbox.h" + +namespace real_world { + +/** + * @brief An abstract reference to a process different than the one which owns + * this one. + */ +struct process_handle { + virtual ~process_handle() = default; + virtual pid_t get_pid() const = 0; +}; + +/** + * @brief A proxy for a process running on the CPU. + * + * A `real_world::process` is a proxy for running code. It represents a + * static snapshot of executable code at a given moment in time as it runs under + * McMini's. + * + * A process can be conceptualized as a "multi-dimensional" forward iterator; + * that is, a process can undergo changes by _executing_ operations which cannot + * be undone. Backwards progress is not possible for a + * `real_world::process`, which matches the forward execution of the + * instruction streams. + * + * A process is a collection of different _runners_ of execution. Each + * separate _runner_ in the proxied process is represented with a unique id. + * New runners may appear during the during execution is assigned a unique + * id (one for every thread creation call) and is tracked by the process. + */ +struct process : public process_handle { + public: + using runner_id_t = ::runner_id_t; + + public: + struct execution_error : public std::runtime_error { + explicit execution_error(const char *c) : std::runtime_error(c) {} + explicit execution_error(const std::string &s) : std::runtime_error(s) {} + }; + + struct termination_error : public std::runtime_error { + int signo; + runner_id_t culprit; + explicit termination_error(int signo, runner_id_t culprit, + const std::string &s) + : std::runtime_error(s), signo(signo), culprit(culprit) {} + }; + + struct nonzero_exit_code_error : public std::runtime_error { + int exit_code; + runner_id_t culprit; + explicit nonzero_exit_code_error(int exit_code, runner_id_t culprit, + const std::string &s) + : std::runtime_error(s), exit_code(exit_code), culprit(culprit) {} + }; + + /** + * @brief Schedule the given runner for execution. + * + * This method signals the proxy process to resume execution of the runner + * with id `mcmini_runner_id`. The method blocks until the runner reaches + * the next semantically interesting point of execution according to that + * runner. + * + * @note The process may not actually contain a runner with id + * `mcmini_runner_id`, or the runner with the id `mcmini_runner_id` may be + * blocked or asleep, or the runner itself may be in a deadlock. In such + * scenarios, the proxy process will not respond and the method will never + * return. It is up to the caller to ensure that scheduling runner + * `mcmini_runner_id` for execution will not block forever. + * + * @returns a mailbox containing the serialized response from the process + * represented by this proxy. The mailbox must contain as its first element a + * `model::transition_registry::rttid`. The McMini coordinator will + * use this identifier to invoke the appropriate callback function to + * transform the remaining contents of the stream into its model. + * @throws an `execution_error` is raised if the provided runner doesn't + * yet exist or if the runner exists but something went wrong during + * execution. + * TODO: We assume at the moment that the number of runners is fixed and that + * every call to `execute_runner` above is valid. Eventually, to support more + * complicated runners (e.g. entire processes, a runner representing multiple + * threads, etc.) the idea of "adding" a new slot for a runner dynamically + * might be needed. + */ + virtual volatile runner_mailbox *execute_runner(runner_id_t) = 0; + virtual ~process() = default; +}; + +} // namespace real_world diff --git a/dmtcp/include/mcmini/real_world/process/dmtcp_coordinator.hpp b/dmtcp/include/mcmini/real_world/process/dmtcp_coordinator.hpp new file mode 100644 index 00000000..00da6233 --- /dev/null +++ b/dmtcp/include/mcmini/real_world/process/dmtcp_coordinator.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "mcmini/real_world/dmtcp_target.hpp" + +namespace real_world { + +struct dmtcp_coordinator : public dmtcp_target { + public: + dmtcp_coordinator(); + + public: + void launch_and_wait() override; + uint32_t get_port() const { return this->port; } + + private: + uint32_t port = 0; +}; +} // namespace real_world diff --git a/dmtcp/include/mcmini/real_world/process/dmtcp_process_source.hpp b/dmtcp/include/mcmini/real_world/process/dmtcp_process_source.hpp new file mode 100644 index 00000000..7e334f75 --- /dev/null +++ b/dmtcp/include/mcmini/real_world/process/dmtcp_process_source.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include + +#include "mcmini/defines.h" +#include "mcmini/forwards.hpp" +#include "mcmini/real_world/process/dmtcp_coordinator.hpp" +#include "mcmini/real_world/process/local_linux_process.hpp" +#include "mcmini/real_world/process_source.hpp" +#include "mcmini/real_world/shm.hpp" +#include "mcmini/real_world/target.hpp" + +namespace real_world { + +/** + * @brief A factory which produces `real_world::local_linux_process` by forking + * a process stored from a checkpoint image via `dmtcp_restart` + * + * A `dmtcp_process_source` is responsible for creating new processes by forking + * and `exec()`-ing into a template process (`dmtcp_restart`) and then + * repeatedly forking the new process to create new processes. The processes + * that are vended are duplicates of the template process. + */ +class dmtcp_process_source : public process_source { + private: + std::string ckpt_file; + dmtcp_target dmtcp_restart_target; + dmtcp_coordinator coordinator_target; + + private: + pid_t make_new_branch(); + + public: + dmtcp_process_source(const std::string &ckpt_file); + virtual ~dmtcp_process_source(); + + public: + std::unique_ptr make_new_process() override; +}; + +}; // namespace real_world diff --git a/dmtcp/include/mcmini/real_world/process/fork_process_source.hpp b/dmtcp/include/mcmini/real_world/process/fork_process_source.hpp new file mode 100644 index 00000000..23ec7d2c --- /dev/null +++ b/dmtcp/include/mcmini/real_world/process/fork_process_source.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +#include "mcmini/defines.h" +#include "mcmini/forwards.hpp" +#include "mcmini/real_world/process_source.hpp" +#include "mcmini/real_world/shm.hpp" +#include "mcmini/real_world/target.hpp" + +namespace real_world { + +/** + * @brief A factory which produces `real_world::local_linux_process` by + * `fork()`-ing the templace process process named `target` with + * `libmcmini.so` preloaded. + * + * A `fork_process_source` is responsible for creating new processes by forking + * and `exec()`-ing into a template process and then repeatedly forking + * the new process to create new processes. The processes that are vended are + * duplicates of the template process. + */ +class fork_process_source : public process_source { + protected: + std::unique_ptr template_program; + std::unique_ptr template_process_handle; + + virtual void make_new_template_process(); + bool has_template_process_alive() const { + return template_process_handle != nullptr; + } + friend local_linux_process; + + public: + fork_process_source() = default; + fork_process_source(real_world::target&&); + fork_process_source(const real_world::target&); + std::unique_ptr make_new_process() override; +}; + +} // namespace real_world diff --git a/dmtcp/include/mcmini/real_world/process/local_linux_process.hpp b/dmtcp/include/mcmini/real_world/process/local_linux_process.hpp new file mode 100644 index 00000000..3feccfb5 --- /dev/null +++ b/dmtcp/include/mcmini/real_world/process/local_linux_process.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +#include "mcmini/real_world/process.hpp" +#include "mcmini/real_world/shm.hpp" + +namespace real_world { + +/** + * @brief A proxy for a process running under Linux. + * + * A `real_world::linux_process` is a local proxy for a process running + * on the same machine. Each instance manages a portion of shared memory that it + * uses to communicate with the proxied process. The proxied process is assumed + * to be under the control of `libmcmini.so`; otherwise, executing runners + * results in undefined behavior. + */ +class local_linux_process : public process { + private: + pid_t pid; + bool should_wait; /* Whether `wait(3)` should be called on destruction */ + + public: + pid_t get_pid() const override { return pid; } + local_linux_process() : local_linux_process(-1) {} + local_linux_process(pid_t pid) : local_linux_process(pid, true) {} + local_linux_process(pid_t pid, bool should_wait); + local_linux_process(const local_linux_process&) = delete; + local_linux_process(local_linux_process&&); + local_linux_process& operator=(const local_linux_process&) = delete; + local_linux_process& operator=(local_linux_process&&); + virtual ~local_linux_process(); + volatile runner_mailbox* execute_runner(runner_id_t) override; +}; +} // namespace real_world diff --git a/dmtcp/include/mcmini/real_world/process/multithreaded_fork_process_source.hpp b/dmtcp/include/mcmini/real_world/process/multithreaded_fork_process_source.hpp new file mode 100644 index 00000000..8bdea4a1 --- /dev/null +++ b/dmtcp/include/mcmini/real_world/process/multithreaded_fork_process_source.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include "mcmini/defines.h" +#include "mcmini/forwards.hpp" +#include "mcmini/real_world/process/dmtcp_coordinator.hpp" +#include "mcmini/real_world/process/fork_process_source.hpp" + +namespace real_world { + +/** + * @brief A factory which produces `real_world::local_linux_process` by + * `multithreaded-fork()`-ing a templace process process named `target` with + * `libmcmini.so` preloaded into it after restarting + * + * A `fork_process_source` is responsible for creating new processes by forking + * and `exec()`-ing into a template process and then repeatedly forking + * the new process to create new processes. The processes that are vended are + * duplicates of the template process. + */ +class multithreaded_fork_process_source : public fork_process_source { + private: + dmtcp_coordinator coordinator_target; + + public: + void make_new_template_process() override; + multithreaded_fork_process_source(const std::string &ckpt_file); +}; + +} // namespace real_world diff --git a/dmtcp/include/mcmini/real_world/process/resources.hpp b/dmtcp/include/mcmini/real_world/process/resources.hpp new file mode 100644 index 00000000..749a23a6 --- /dev/null +++ b/dmtcp/include/mcmini/real_world/process/resources.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include "mcmini/real_world/shm.hpp" + +namespace real_world { + +/** + * @brief A singleton holding shared resources between the McMini process and + * the other processes it interacts with. + */ +struct xpc_resources { + private: + xpc_resources(); + std::unique_ptr rw_region = nullptr; + + public: + /// @brief The special port used by the `dmtcp_coordinator` which exists + /// specifically for aiding McMini with creating new target branches + /// after restart. + static const int dmtcp_coordinator_port = 7778; + static xpc_resources &get_instance() { + static std::unique_ptr resources = nullptr; + if (!resources) { + resources = std::unique_ptr(new xpc_resources()); + } + return *resources; + } + shared_memory_region *get_rw_region() const { return this->rw_region.get(); } + void reset_binary_semaphores_for_new_branch(); +}; + +} // namespace real_world diff --git a/dmtcp/include/mcmini/real_world/process/template_process.h b/dmtcp/include/mcmini/real_world/process/template_process.h new file mode 100644 index 00000000..8c9fd853 --- /dev/null +++ b/dmtcp/include/mcmini/real_world/process/template_process.h @@ -0,0 +1,41 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#define TEMPLATE_FORK_FAILED (-2) // fork(2) failed in the template + +struct template_process_t { + /// Unused in most cases, but set in the event of an error after + /// calling a C function in the template process (e.g. after `fork(2)` fails). + int err; + + /// The process id of the latest `fork()` of the template process, or + /// `TEMPLATE_FORK_FAILED` in the event that `fork(2)` in the template process + /// failed. + pid_t cpid; + + /// If a `siginfo_t` was passed to the template process from its current + /// child, the template will populate this value. + siginfo_t sa_cinfo; + + /// A semaphore that the McMini process waits on and that + /// the template process signals after writing the newly spawned pid to shared + /// memory. + sem_t mcmini_process_sem; + + /// A semphore that `libmcmini.so` waits on and that the McMini process + /// signals when it wants to spawn a new process. + sem_t libmcmini_sem; + + // MARK: DMTCP ONLY +}; + +#ifdef __cplusplus +} +#endif // extern "C" diff --git a/dmtcp/include/mcmini/real_world/process/wait.h b/dmtcp/include/mcmini/real_world/process/wait.h new file mode 100644 index 00000000..0c8d2b82 --- /dev/null +++ b/dmtcp/include/mcmini/real_world/process/wait.h @@ -0,0 +1,20 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +// +struct wait_status { + pid_t cpid; +}; + +struct wait_status mc_wait(); +struct wait_status mc_waitpid(pid_t, int); + +#ifdef __cplusplus +} +#endif // extern "C" diff --git a/docs/design/include/mcmini/real_world/process_source.hpp b/dmtcp/include/mcmini/real_world/process_source.hpp similarity index 63% rename from docs/design/include/mcmini/real_world/process_source.hpp rename to dmtcp/include/mcmini/real_world/process_source.hpp index c0a78136..8498ae3a 100644 --- a/docs/design/include/mcmini/real_world/process_source.hpp +++ b/dmtcp/include/mcmini/real_world/process_source.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include "mcmini/real_world/process.hpp" namespace real_world { @@ -16,6 +18,13 @@ namespace real_world { */ class process_source { public: + struct process_creation_exception : public std::runtime_error { + explicit process_creation_exception(const char *c) + : std::runtime_error(c) {} + explicit process_creation_exception(const std::string &s) + : std::runtime_error(s) {} + }; + /** * @brief Spawn a new process starting from the the fixed point of this * process source. @@ -27,7 +36,20 @@ class process_source { * to tell us what the issue was and would make for better error outputs. */ virtual std::unique_ptr make_new_process() = 0; + + /** + * @brief Attempts to make a new process and raises a + * `process_soprocess_creation_exception` on failure + */ + std::unique_ptr force_new_process() { + auto p = make_new_process(); + if (!p) { + throw process_creation_exception("Process creation failed"); + } + return p; + } + virtual ~process_source() = default; }; -} // namespace real_world \ No newline at end of file +} // namespace real_world diff --git a/dmtcp/include/mcmini/real_world/remote_address.hpp b/dmtcp/include/mcmini/real_world/remote_address.hpp new file mode 100644 index 00000000..f56431fe --- /dev/null +++ b/dmtcp/include/mcmini/real_world/remote_address.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +namespace real_world { + +template +struct remote_address final { + private: + T* remote_addr = nullptr; + + T*& get_as_ref() { return remote_addr; } + + template + friend std::ostream& operator<<(std::ostream& os, remote_address& ra) { + return (os << static_cast(ra.get_as_ref())); + } + template + friend std::istream& operator>>(std::istream& is, remote_address& ra) { + return (is >> ra.get_as_ref()); + } + + public: + T* get() const { return remote_addr; } + remote_address() : remote_addr(nullptr) {} + remote_address(T* p) : remote_addr(p) {} + + bool operator==(const remote_address& o) const { + return o.remote_addr == remote_addr; + } +}; + +template +using remote_object = remote_address; + +} // namespace real_world + +template +struct std::hash> { + std::size_t operator()(const real_world::remote_address& o) const { + return std::hash{}(o.get()); + } +}; diff --git a/docs/design/include/mcmini/real_world/shm.hpp b/dmtcp/include/mcmini/real_world/shm.hpp similarity index 69% rename from docs/design/include/mcmini/real_world/shm.hpp rename to dmtcp/include/mcmini/real_world/shm.hpp index 8c4bb36b..111fd3d4 100644 --- a/docs/design/include/mcmini/real_world/shm.hpp +++ b/dmtcp/include/mcmini/real_world/shm.hpp @@ -14,26 +14,30 @@ namespace real_world { */ struct shared_memory_region { public: - /** - * @brief Create a new shared memory - */ shared_memory_region(const std::string& shm_file_name, size_t region_size); - virtual ~shared_memory_region(); - - // Ownership can only be passed -- prevent copying to prevent shared_memory_region(const shared_memory_region&) = delete; shared_memory_region(shared_memory_region&&) = default; shared_memory_region& operator=(const shared_memory_region&) = delete; shared_memory_region& operator=(shared_memory_region&&) = default; + ~shared_memory_region(); volatile void* get() const { return this->shm_mmap_region; } volatile void* contents() const { return get(); } - + volatile void* off(off64_t offset) const { + return static_cast( + static_cast(shm_mmap_region) + offset); + } template - volatile T* as_stream_of() const { + volatile T* as() const { return static_cast(shm_mmap_region); } - volatile char* byte_stream() const { return as_stream_of(); } + template + volatile T* as_array_of(off64_t off_out = 0, off64_t off_in = 0) const { + return static_cast(off(off_in)) + off_out; + } + volatile char* byte_array(off64_t off = 0) const { + return as_array_of(off); + } size_t size() const { return this->region_size; } private: @@ -42,4 +46,4 @@ struct shared_memory_region { size_t region_size; }; -}; // namespace real_world \ No newline at end of file +}; // namespace real_world diff --git a/dmtcp/include/mcmini/real_world/target.hpp b/dmtcp/include/mcmini/real_world/target.hpp new file mode 100644 index 00000000..2e317e4e --- /dev/null +++ b/dmtcp/include/mcmini/real_world/target.hpp @@ -0,0 +1,117 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace real_world { + +struct target { + protected: + bool quiet = false; + + // The name of the program which we should exec() into with libmcmini.so + // preloaded. + // NOTE: Favor std::filesystem::path if C++17 is eventually supported + // Alternatively, have McMini conditionally compile a std::filesystem::path + std::string target_program; + + /// @brief The list of arguments to pass to the target program. + std::vector target_program_args; + + /// @brief A list of environment variables to set prior to `fork()/exec()`. + std::unordered_map setenv_vars; + + /// @brief A list of environment variable names for which `unsetenv()` should + /// be invoked prior to a `fork()/exec()`. + std::unordered_set unsetenv_vars; + + public: + target() = default; + static void prepare_mcmini_targets(); + explicit target(const std::string &target_program) + : target(target_program, std::vector()) {} + + explicit target(const std::string &target_program, + const std::vector &target_program_args) + : target_program(target_program), + target_program_args(target_program_args) {} + + const std::string &name() const { return this->target_program; } + std::string invocation() const { + std::string invocation = this->target_program; + for (const auto &arg : this->target_program_args) invocation += " " + arg; + return invocation; + } + + // The semantics + void dont_unset_env(std::string name) { + unsetenv_vars.erase(std::move(name)); + } + void dont_set_env(std::string name) { setenv_vars.erase(std::move(name)); } + void unset_env(std::string name) { + unsetenv_vars.insert(name); + dont_set_env(std::move(name)); + } + void set_env(std::string name, std::string value) { + setenv_vars.insert({name, std::move(value)}); + dont_unset_env(std::move(name)); + } + + /// @brief A convenience method for setting the `LD_PRELOAD` environment + /// variable for `libmcmini.so`. + void set_preload_libmcmini() { + // NOTE: According to the man page `dirname(const char *path)` "may modify + // the contents of `path`...", so we use the storage of the local instead. + // We don't want to use `std::string` either since it doesn't expect its + // contents to be modified indirectly + std::vector target_program_mutable_name(this->name().begin(), + this->name().end()); + char buf[1000]; + buf[sizeof(buf) - 1] = '\0'; + snprintf(buf, sizeof buf, "%s:%s/libmcmini.so", + (getenv("LD_PRELOAD") ? getenv("LD_PRELOAD") : ""), + dirname(target_program_mutable_name.data())); + this->set_env("LD_PRELOAD", buf); + } + + /// @brief Whether `stdout` and `stderr` should be closed on fork() + void set_quiet(bool be_quiet) { this->quiet = be_quiet; } + + /// @brief A convenience method for setting the `MCMINI_TEMPLATE_LOOP` + /// environment variable for the child process + /// + /// A child process with `MCMINI_TEMPLATE_LOOP` set indicates that the child + /// will be used as a template process for McMini + void set_is_template() { this->set_env("MCMINI_TEMPLATE_LOOP", "1"); } + + /// @brief Creates a new process running this program + /// + /// @return the process id of the newly created process. + virtual pid_t launch_dont_wait(); + + /// @brief Executes the target program as a separate process + /// and waits for that process to finish execution. + /// + /// @throws a `process::execution_error` is raised if the + /// process exits unexpectedly + virtual void launch_and_wait(); + + /// @brief Turn this process into the target via `execvp()` + /// + /// @note this function only returns on failure of `execvp(2)` and sets + /// errno + void execvp() const; + + friend std::ostream &operator<<(std::ostream &os, const target &target) { + os << target.target_program; + for (const std::string &arg : target.target_program_args) os << " " << arg; + return os; + } +}; + +} // namespace real_world diff --git a/dmtcp/include/mcmini/signal.hpp b/dmtcp/include/mcmini/signal.hpp new file mode 100644 index 00000000..27f34915 --- /dev/null +++ b/dmtcp/include/mcmini/signal.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +extern "C" { +#include "mcmini/lib/sig.h" +} + +using signo_t = int; +extern const std::unordered_map sig_to_str; + +struct signal_tracker { + private: + static signal_tracker _instance; + constexpr signal_tracker() = default; + constexpr static size_t MAX_SIGNAL_TYPES = NSIG - 1; +#ifndef ATOMIC_INT_LOCK_FREE +#error "Signal tracking relies on `std::atomic` to be lock-free." +#endif + std::atomic_uint32_t flags[MAX_SIGNAL_TYPES] = {}; + + public: + static sem_t *current_sem; + struct interrupted_error; + static signal_tracker &instance(); + static bool is_bad_signal(int signo); + static void set_sem(sem_t *sem) { current_sem = sem; } + static int sig_semwait(sem_t *sem); + static void throw_if_received(int sig); + static void install_process_wide_signal_handlers(); + void set_signal(int sig); + bool has_signal(int sig) const; + bool try_consume_signal(int sig); +}; + +extern "C" void signal_tracker_sig_handler(int sig); diff --git a/dmtcp/include/mcmini/spy/checkpointing/alloc.h b/dmtcp/include/mcmini/spy/checkpointing/alloc.h new file mode 100644 index 00000000..71ba3341 --- /dev/null +++ b/dmtcp/include/mcmini/spy/checkpointing/alloc.h @@ -0,0 +1,3 @@ +#pragma once + +void mc_allocate_shared_memory_region(const char *); diff --git a/dmtcp/include/mcmini/spy/checkpointing/cv_status.h b/dmtcp/include/mcmini/spy/checkpointing/cv_status.h new file mode 100644 index 00000000..29b73da5 --- /dev/null +++ b/dmtcp/include/mcmini/spy/checkpointing/cv_status.h @@ -0,0 +1,50 @@ +#ifndef CV_STATUS_H +#define CV_STATUS_H +/* + Each state in condition_variable_state corresponds to a phase in the lifescycle + of a condition variable. The status of a condition variable can be one of the following: + - CV_UNINITIALIZED: The condition variable has not been initialized yet via mc_pthread_cond_init. + - CV_INITIALIZED: The condition variable has been initialized and ready for use (post mc_pthread_cond_init). + - CV_PREWAITING: A thread is releasing the mutex but hasn't fully entered the wait state yet + (mc_pthread_cond_wait in progress), hence entered outer waiting room. This prevents checkpointing during the + unsafe gap between mutex unlock and wait. + - CV_WAITING: The thread has successfully entered the wait state i.e, consumed the signal or successfully returned + from libpthread_cond_timed_wait. (mc_pthread_cond_wait). + - CV_SIGNALED: A signal/broadcast has been sent; the condition variable resumes operation (mc_pthread_cond_signal/broadcast). + - CV_DESTROYED: The condition variable has been destroyed (post mc_pthread_cond_destroy). + Note that the status of a condition variable can be CV_SIGNALED even when there are no threads waiting on it. + This is because a thread can signal a condition variable even when no threads are waiting on it (spurious wakeup). + CV_UNINITIALIZED + │ + │ mc_pthread_cond_init() + ▼ + CV_INITIALIZED ◄───┐ + │ │ + │ mc_pthread_cond_wait() (pre-wait phase) + ▼ │ + CV_PREWAITING │ + │ │ + │ pthread_cond_timedwait() success + ▼ │ + CV_WAITING ────┘ + │ + │ mc_pthread_cond_signal() + ▼ + CV_SIGNALED ──────┐ + ▲ │ + └────────────┘ (thread re-acquires mutex) + + CV_DESTROYED (via mc_pthread_cond_destroy) + +*/ +typedef enum condition_variable_status{ + CV_UNINITIALIZED, + CV_INITIALIZED, + CV_WAITING, + CV_SIGNALED, + CV_PREWAITING, + CV_DESTROYED + }condition_variable_status; + +#endif // CV_STATUS_H + \ No newline at end of file diff --git a/dmtcp/include/mcmini/spy/checkpointing/objects.h b/dmtcp/include/mcmini/spy/checkpointing/objects.h new file mode 100644 index 00000000..413224f4 --- /dev/null +++ b/dmtcp/include/mcmini/spy/checkpointing/objects.h @@ -0,0 +1,82 @@ +#pragma once + +#include + +#include "mcmini/defines.h" +#include "cv_status.h" +#include "mcmini/Thread_queue.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum visible_object_type { + UNKNOWN, + MUTEX, + SEMAPHORE, + CONDITION_VARIABLE, + THREAD, + CV_WAITERS_QUEUE +} visible_object_type; + +typedef enum mutex_state { + UNINITIALIZED, + UNLOCKED, + LOCKED, + DESTROYED +} mutex_state; + +typedef enum thread_status { + ALIVE, + EXITED, +} thread_status; + +typedef enum semaphore_status { + SEM_UNINITIALIZED, + SEM_INITIALIZED, + SEM_DESTROYED, +} semaphore_status; + +typedef struct semaphore_state { + unsigned count; + semaphore_status status; +} semaphore_state; + +typedef struct cv_waiters_queue_state{ + void *cv_location; + runner_id_t waiting_id; + condition_variable_status cv_state; +}cv_waiters_queue_state; + +typedef struct thread_state { + pthread_t pthread_desc; + runner_id_t id; + thread_status status; +} thread_state; + +typedef struct condition_variable_state{ + condition_variable_status status; + runner_id_t interacting_thread; // The thread that iscurrently interacting with this condition variable + pthread_mutex_t *associated_mutex; // The mutex that is associated with this condition variable + int count; // The number of threads waiting on this condition variable + thread_queue* waiting_threads; // The queue of threads waiting on this condition variable + int prev_waiting_count; + int lost_wakeups; +} condition_variable_state; + +typedef struct visible_object { + visible_object_type type; + void *location; + union{ + mutex_state mut_state; + semaphore_state sem_state; + condition_variable_state cond_state; + thread_state thrd_state; + cv_waiters_queue_state waiting_queue_state; + }; +} visible_object; + +extern visible_object empty_visible_obj; +#ifdef __cplusplus +} +#endif // extern "C" diff --git a/dmtcp/include/mcmini/spy/checkpointing/rec_list.h b/dmtcp/include/mcmini/spy/checkpointing/rec_list.h new file mode 100644 index 00000000..b62adf6c --- /dev/null +++ b/dmtcp/include/mcmini/spy/checkpointing/rec_list.h @@ -0,0 +1,17 @@ +#pragma once + +#include "mcmini/spy/checkpointing/objects.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct rec_list { + visible_object vo; + struct rec_list *next; +} rec_list; + + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/dmtcp/include/mcmini/spy/checkpointing/record.h b/dmtcp/include/mcmini/spy/checkpointing/record.h new file mode 100644 index 00000000..59388a26 --- /dev/null +++ b/dmtcp/include/mcmini/spy/checkpointing/record.h @@ -0,0 +1,211 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef __STDC_NO_ATOMICS__ +#error "C11 atomics are required to compile libmcmini.so" +#else +#include +#endif + +#ifndef ATOMIC_BOOL_LOCK_FREE +#error \ + "Atomic booleans must be lock free, but the compiler has indicated this is not the case" +#endif + +#ifndef ATOMIC_INT_LOCK_FREE +#error \ + "Atomic integers must be lock free, but the compiler has indicated this is not the case" +#endif + +#include +#include +#include + +/** + * @brief Describes the different behaviors that `libmcmini.so` should exhibit + * + * `libmcmini.so` is pre-loaded (i.e. with LD_PRELOAD) either directly by the + * `mcmini` process or by `dmtcp_launch` into an executable known as the + * _target_. The target is the program whose behavior we wish to verify. + * + * `libmcmini.so` however will appear within different processes running the + * target program and must behave differently based on how and when it was + * loaded. Below is a description on the different ways `libmcmini.so` can + * behave. + * + * I. Classic Model Checking (No Deep Debugging) + * + * TARGET_TEMPLATE: + * In this mode, `libmcmini.so` enters an infinite loop waiting for the signal + * from the `mcmini` process to produce a new branch via `fork()`. + * + * TARGET_BRANCH: + * In this mode, `libmcmini.so` will now behave as if under the control of the + * model checker in the `mcmini` process. + * + * II. Recording + * + * PRE_DMTCP_INIT: + * In this mode, the `mcmini` process has `exec()`-ed into `dmtcp_launch` + * with `libmcmini.so` as a DMTCP plugin. Here, DMTCP will preload mcmini. Prior + * to DMTCP sending the `DMTCP_EVENT_INIT` to `libmcmini.so`, wrapper functions + * simply forward calls to `libpthread.so` + * + * PRE_CHECKPOINT_THREAD: + * In this mode, DMTCP has sent the `DMTCP_EVENT_INIT` event but has not + * yet created the checkpoint thread. All wrappers (except `pthread_create`) + * behave as in `PRE_DMTCP_INIT`. For `pthread_create`, the call is _only_ + * forwarded into DMTCP instead -- the checkpoint thread is NOT recorded. + * + * CHECKPOINT_THREAD: + * In this mode, DMTCP has created the checkpoint thread. The checkpoint + * thread should not interact with McMini in any way. But the two interact + * because the functions `libmcmini.so` overrides are used by `dmtcp` + * extensively (e.g. `sem_wait()`). This mode, special to the library when + * executing from the perspective of the checkpoint thread, indicates that DMTCP + * has called directly into McMini. In most cases, this probably means + * forwarding the call to DMTCP's wrapper functions of to `libpthread.so`. + * + * RECORD: + * In this mode, `libmcmini.so` performs a light-weight recording of the + * primitives it encounters and keeps track of their state. Only after doing so + * do wrapper functions forward calls to the next available function found by + * `dlsym(3)` using `RTLD_NEXT`. + * + * PRE_CHECKPOINT: + * In this mode, `libmcmini.so` has been alerted about the end of the + * recording session. Wrapper functions again forward their calls to the next + * available function. Wrappers may eventually change their behavior from RECORD + * mode if necessary. + * + * II.i. Deep Debugging with `dmtcp_restart` + * + * DMTCP_RESTART_INTO_BRANCH: + * In this mode, the DMTCP plugin has been notified of the + * `DMTCP_EVENT_RESTART` event. The user-space threads will transition into a + * stable state before the template thread and the scheduler process proceed + * with model checking. The userspace threads assume that they are immediately + * under the control of the model checker. + * + * II.i. Deep Debugging with `dmtcp_restart` + * + * DMTCP_RESTART_INTO_TEMPLATE: + * In this mode, the DMTCP plugin has been notified of the + * `DMTCP_EVENT_RESTART` event. The user-space threads will transition into a + * stable state before the template thread and the scheduler process proceed + * with model checking. However, unlike `DMTCP_RESTART_INTO_BRANCH`, the + * userspace threads assume that they are in a template _process_. The template + * thread in this template process will repeatedly call `multithreaded_fork()`. + * + * TARGET_TEMPLATE_AFTER_RESTART: + * In this mode, `libmcmini.so` has been restored by `dmtcp_launch` from a + * checkpoint image. Here, `libmcmini.so` will first attempt to communicate + * with `mcmini` and transfer over the state information recorded during the + * RECORD phase. After transfer has completed, `libmcmini.so` will behave the + * same as `TARGET_TEMPLATE`, except this time the template thread will call + * `multithreaded_fork()` instead of `fork()`. Other userspace threads in the + * template process are blocked indefinitely and will not communicate with the + * model checker `mcmini`. Only after a `multithreaded_fork()` will the + * (duplicates of) these threads participate in model checking. + * + * III. Deep Debugging Under the Modeler + * + * TARGET_BRANCH_AFTER_RESTART: + * In this mode, `libmcmini.so` behaves exactly as in the classic case of + * `TARGET_BRANCH`. The only difference is that the userspace threads appear + * after a `dmtcp_restart` of a program in the middle of execution instead + * directly launching the program. Userspace threads assume they are under the + * control of the model checker. + * + */ +enum libmcmini_mode { + PRE_DMTCP_INIT, + PRE_CHECKPOINT_THREAD, + CHECKPOINT_THREAD, + RECORD, + PRE_CHECKPOINT, + DMTCP_RESTART_INTO_BRANCH, + DMTCP_RESTART_INTO_TEMPLATE, + TARGET_TEMPLATE_AFTER_RESTART, + TARGET_BRANCH_AFTER_RESTART, + + // Modes possible only when not using DMTCP + // i.e. classic McMini. + TARGET_TEMPLATE, + TARGET_BRANCH, +}; + +/** + * We use an `atomic_int` as opposed to `enum libmcmini_mode` directly + * to ensure that writes from the checkpoint thread (which is responsible + * for invoking our callback that in turn changes the mode) are visible to + * all user-space threads. + * + * @note we CANNOT use a lock (at least easily) as e.g. during the + * DMTCP_EVENT_RESTART event the user-space threads are blocked. This means that + * using a lock to synchronize accesses to `libmcmini_mode` is dangerous: it's + * possible that a user-space thread owned the lock at checkpoint-time (and + * hence is also the owner at restart-time). If the checkpoint thread then tried + * to acquire the lock, we'd have deadlock. + */ +extern volatile atomic_int libmcmini_mode; + +bool is_in_restart_mode(void); +enum libmcmini_mode get_current_mode(); +void set_current_mode(enum libmcmini_mode); + +extern pthread_t ckpt_pthread_descriptor; +extern volatile atomic_bool libmcmini_has_recorded_checkpoint_thread; +bool is_checkpoint_thread(void); + +typedef struct visible_object visible_object; +typedef struct rec_list rec_list; + +extern sem_t dmtcp_restart_sem; +extern pthread_mutex_t rec_list_lock; +extern pthread_mutex_t dmtcp_list_lock; +extern rec_list *head_record_mode; +// The VIRTUAL tid of the checkpoint thread +// as it appeared while recording. + +/// @brief Retrieves the stored state for the given object +/// @return a pointer to the node in the list formed by `head`, +/// or `NULL` if the object at address `addr` is not found +/// +/// @note you must acquire `rec_list_lock` before calling this function +rec_list *find_object(void *addr, rec_list *); +rec_list *find_thread_record_mode(pthread_t); +rec_list *find_object_record_mode(void *addr); +rec_list *find_dmtcp_object(void *addr); +bool is_dmtcp_object(void *addr); + +/// @brief Adds a new element to the list `head`. +/// +/// @note you must acquire `rec_list_lock` before calling this function +rec_list *add_rec_entry(const visible_object *, rec_list **, rec_list **); +rec_list *add_rec_entry_record_mode(const visible_object *); +void print_rec_list(const rec_list *); +rec_list *add_dmctp_object(const visible_object *); + +/** + * @brief Notifies the template thread spawned during DMTCP_RESTART + * that the current thread is now in a stable state and will + * await further instruction from the scheduler process + */ +void notify_template_thread(); + +/** + * @brief Either waits for the model checker `McMini` OR the template thread to + * be awoken. + */ +void thread_wait_after_dmtcp_restart(); + +// Spawns a new process with all threads of this process duplicated. +pid_t multithreaded_fork(void); + +#ifdef __cplusplus +} +#endif // extern "C" diff --git a/dmtcp/include/mcmini/spy/checkpointing/transitions.h b/dmtcp/include/mcmini/spy/checkpointing/transitions.h new file mode 100644 index 00000000..b03a712a --- /dev/null +++ b/dmtcp/include/mcmini/spy/checkpointing/transitions.h @@ -0,0 +1,84 @@ +#pragma once +#include + +#ifdef __cplusplus +extern "C" { +#endif +#include + +#include "mcmini/defines.h" +#include "mcmini/spy/checkpointing/objects.h" + +typedef enum transition_type { + INVISIBLE_OPERATION_TYPE, + MUTEX_INIT_TYPE, + MUTEX_LOCK_TYPE, + MUTEX_UNLOCK_TYPE, + THREAD_CREATE_TYPE, + THREAD_JOIN_TYPE, + THREAD_EXIT_TYPE, + PROCESS_EXIT_TYPE, + PROCESS_ABORT_TYPE, + COND_ENQUEUE_TYPE, + COND_INIT_TYPE, + COND_WAIT_TYPE, + COND_SIGNAL_TYPE, + COND_BROADCAST_TYPE, + COND_DESTROY_TYPE, + SEM_INIT_TYPE, + SEM_WAIT_TYPE, + SEM_POST_TYPE, + SEM_DESTROY_TYPE +} transition_type; + +typedef struct mutex_init { + visible_object *mut; +} mutex_init; + +typedef struct mutex_lock { + visible_object *mut; +} mutex_lock; + +typedef struct mutex_unlock { + visible_object *mut; +} mutex_unlock; + +typedef struct cond_init { + visible_object *cond; +} cond_init; + +typedef struct cond_wait { + visible_object *cond; + visible_object *mut; +} cond_wait; + +typedef struct cond_signal { + visible_object *cond; +} cond_signal; + +typedef struct cond_broadcast { + visible_object *cond; +} cond_broadcast; + +typedef struct cond_destroy { + visible_object *cond; +} cond_destroy; + +typedef struct transition { + pthread_t executor; + transition_type type; + union { + mutex_init init; + mutex_lock lock; + mutex_unlock unlock; + cond_init cv_init; + cond_wait cv_wait; + cond_signal cv_signal; + }; +} transition; + +transition invisible_operation_for_this_thread(void); + +#ifdef __cplusplus +} +#endif // extern "C" diff --git a/dmtcp/include/mcmini/spy/intercept/interception.h b/dmtcp/include/mcmini/spy/intercept/interception.h new file mode 100644 index 00000000..105f024f --- /dev/null +++ b/dmtcp/include/mcmini/spy/intercept/interception.h @@ -0,0 +1,67 @@ +#pragma once + +#define _GNU_SOURCE + +#include "mcmini/defines.h" +#include +#include +#include +#include + +void libmcmini_init(void); +void mc_load_intercepted_pthread_functions(void); + +int pthread_mutex_init(pthread_mutex_t *mutex, + const pthread_mutexattr_t *mutexattr); +int pthread_mutex_lock(pthread_mutex_t *mutex); +int pthread_mutex_unlock(pthread_mutex_t *mutex); +int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr); +int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mut); +int pthread_cond_signal(pthread_cond_t *cond); + +int pthread_create(pthread_t *thread, const pthread_attr_t *attr, + void *(*routine)(void *), void *arg); +int libpthread_pthread_create(pthread_t *thread, const pthread_attr_t *attr, + void *(*routine)(void *), void *arg); +int libdmtcp_pthread_create(pthread_t *thread, const pthread_attr_t *attr, + void *(*routine)(void *), void *arg); + +int pthread_join(pthread_t thread, void**); +int libpthread_pthread_join(pthread_t thread, void**); +int libdmtcp_pthread_join(pthread_t thread, void**); + +int libpthread_mutex_init(pthread_mutex_t *, const pthread_mutexattr_t *); +int libpthread_mutex_lock(pthread_mutex_t *); +int libpthread_mutex_trylock(pthread_mutex_t *); +int libpthread_mutex_timedlock(pthread_mutex_t *, struct timespec *); +int libpthread_mutex_unlock(pthread_mutex_t *); +int libpthread_mutex_destroy(pthread_mutex_t *); + +int libpthread_cond_init(pthread_cond_t*, const pthread_condattr_t*); +int libpthread_cond_wait(pthread_cond_t*, pthread_mutex_t*); +int libpthread_cond_timedwait(pthread_cond_t*, pthread_mutex_t*, const struct timespec*); +int libpthread_cond_signal(pthread_cond_t*); +int libpthread_cond_broadcast(pthread_cond_t*); +int libpthread_cond_destroy(pthread_cond_t*); + +int sem_init(sem_t*, int, unsigned); +int sem_post(sem_t*); +int sem_wait(sem_t*); +int sem_destroy(sem_t *); +int libpthread_sem_init(sem_t*, int, int); +int libpthread_sem_post(sem_t*); +int libpthread_sem_wait(sem_t*); +int libpthread_sem_wait_loop(sem_t*); +int libpthread_sem_destroy(sem_t *); +int libpthread_sem_timedwait(sem_t*, struct timespec *); + +unsigned sleep(unsigned); +unsigned libc_sleep(unsigned); + +void exit(int); +MCMINI_NO_RETURN void libc_exit(int); + +void abort(void); +MCMINI_NO_RETURN void libc_abort(void); + +pid_t libc_fork(void); diff --git a/dmtcp/include/mcmini/spy/intercept/wrappers.h b/dmtcp/include/mcmini/spy/intercept/wrappers.h new file mode 100644 index 00000000..88208668 --- /dev/null +++ b/dmtcp/include/mcmini/spy/intercept/wrappers.h @@ -0,0 +1,42 @@ +#pragma once + +#include + +#include "mcmini/lib/entry.h" +#include "mcmini/real_world/mailbox/runner_mailbox.h" + +void thread_await_scheduler(void); +void thread_wake_scheduler_and_wait(void); +void thread_awake_scheduler_for_thread_finish_transition(void); +void thread_handle_after_dmtcp_restart(void); +volatile runner_mailbox *thread_get_mailbox(void); + +int mc_pthread_mutex_init(pthread_mutex_t *mutex, + const pthread_mutexattr_t *mutexattr); +int mc_pthread_mutex_lock(pthread_mutex_t *mutex); +int mc_pthread_mutex_unlock(pthread_mutex_t *mutex); +int mc_pthread_create(pthread_t *thread, const pthread_attr_t *attr, + void *(*routine)(void *), void *arg); +int mc_sem_post(sem_t *); +int mc_sem_wait(sem_t *); +int mc_pthread_join(pthread_t, void**); +int mc_sem_init(sem_t*, int, unsigned); +int mc_sem_post(sem_t*); +int mc_sem_wait(sem_t*); +int mc_sem_destroy(sem_t *sem); +unsigned mc_sleep(unsigned); +int mc_pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr); +int mc_pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); +int mc_pthread_cond_signal(pthread_cond_t *cond); +int mc_pthread_cond_broadcast(pthread_cond_t *cond); +int mc_pthread_cond_destroy(pthread_cond_t *cond); + + +/* + An `atexit()` handler is installed in libmcmini.so with this function. + This ensures that if the main thread exits the model checker still maintains + control. +*/ +void mc_exit_main_thread_in_child(void); +MCMINI_NO_RETURN void mc_transparent_abort(void); +MCMINI_NO_RETURN void mc_transparent_exit(int status); diff --git a/docs/design/mcmini-architecture.md b/dmtcp/mcmini-architecture.md similarity index 99% rename from docs/design/mcmini-architecture.md rename to dmtcp/mcmini-architecture.md index f8ccd2a5..8c0d528c 100644 --- a/docs/design/mcmini-architecture.md +++ b/dmtcp/mcmini-architecture.md @@ -72,4 +72,4 @@ The role of the Coordinator/Tracer is thus not merely to monitor but to actively ### Naming Conventions -We follow the naming conventions of the CoreFoundation regarding memory management where possible [see here](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFMemoryMgmt/Concepts/Ownership.html#//apple_ref/doc/uid/20001148-CJBEJBHH) \ No newline at end of file +We follow the naming conventions of the CoreFoundation regarding memory management where possible [see here](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFMemoryMgmt/Concepts/Ownership.html#//apple_ref/doc/uid/20001148-CJBEJBHH) diff --git a/dmtcp/script/debug-mcmini-child-gdb.py b/dmtcp/script/debug-mcmini-child-gdb.py new file mode 100644 index 00000000..e69de29b diff --git a/dmtcp/script/debug-mcmini-child.py b/dmtcp/script/debug-mcmini-child.py new file mode 100644 index 00000000..7cc5bad9 --- /dev/null +++ b/dmtcp/script/debug-mcmini-child.py @@ -0,0 +1,20 @@ +import sys +import os +import subprocess as sp + +try: + pids = sp.check_output(["pgrep", "DMTCP"], text=True).strip().split("\n") +except subprocess.CalledProcessError: + # No processes found + print("No processes matching 'foo' were found.") + exit(1) + +# Loop through each PID and open a new GNOME terminal with gdb attached +for pid in pids: + sp.check_output([ + "gnome-terminal", '--tab', f'--title=\'McMini Child {pid}\'', + "--", + "bash", "-c", f"gdb -x gdbinit-attach-dmtcp-child -p {pid}" + ]) + +print(f"Opened GNOME terminals for PIDs: {', '.join(pids)}") diff --git a/dmtcp/script/debug-mcmini-dmtcp.py b/dmtcp/script/debug-mcmini-dmtcp.py new file mode 100644 index 00000000..7459c510 --- /dev/null +++ b/dmtcp/script/debug-mcmini-dmtcp.py @@ -0,0 +1,84 @@ +import os +import re +import sys +import subprocess as sp + +ckpt_file_regex = r"ckpt*" +restart_script_regex = r"dmtcp_restart_script*" +mcmini_shm_file_regex = r"mcmini*" +cwd = os.getcwd() + +def get_script_path(): + return os.path.dirname(os.path.realpath(sys.argv[0])) + +def clean_dir(root_dir, regex): + for filename in os.listdir(root_dir): + file_path = os.path.join(root_dir, filename) + try: + if re.match(regex, filename): + print(f'>>>> Deleting `{filename}` ...') + os.unlink(file_path) + except Exception as e: + print('Failed to delete %s. Reason: %s' % (file_path, e)) + +## Rerun make --> ensures there's no build artifacts left over +print(f'>> Calling `cmake` ...') +try: + sp.check_output(['cmake', '--build', cwd]) +except sp.CalledProcessError as e: + print(f'ERROR: {e}', file=sys.stderr) + exit(1) + +print(f'>> Removing checkpoints & restart scripts ...') +clean_dir(cwd, ckpt_file_regex) +clean_dir(cwd, restart_script_regex) + +print(f'>> Removing lingering shared memory files') +clean_dir('/dev/shm', mcmini_shm_file_regex) + +print(f'>> Removing the `mcmini` named pipe at `/tmp/mcmini` ...') +sp.run(['rm', '-f', '/tmp/mcmini-fifo']) + +print(f'>> Killing lingering `dmtcp_coordinator`s ...') +sp.run(['pkill', '-9', 'dmtcp_co']) + +# Run mcmini under checkpointing mode with a timeout. +# The timeout ensures that mcmini exits quickly +# but not too quickly +MCMINI_CHECKPOINT_DURATION = 2 +MCMINI_LIFETIME = 3 +try: + sp.run( + ['./mcmini', '-i', str(MCMINI_CHECKPOINT_DURATION), './hello-world'], + timeout=MCMINI_LIFETIME, capture_output=False, shell=False + ) +except Exception as e: + print('Generated a new checkpoint file') + +## At this point, a checkpoint file should be created +gdbinit_preamble = f'''# This file was autogenerated by `debug-mcmini-dmtcp.py` +set pagination off +set breakpoint pending on +source {os.path.join(get_script_path(), './debug-mcmini-child-gdb.py')} +''' + +gdbinit = gdbinit_preamble + ''' +r +''' + +gdbinit_attach_dmtcp_child = gdbinit_preamble + '''source ~/dmtcp/util/gdb-dmtcp-utils.py +load-symbols +info threads +''' + +with open(os.path.join(cwd, 'gdbinit-autogenerated'), 'w') as gdbinit_file: + gdbinit_file.write(gdbinit) + +with open(os.path.join(cwd, 'gdbinit-attach-dmtcp-child'), 'w') as gdbinit_file: + gdbinit_file.write(gdbinit_attach_dmtcp_child) + +sp.run( + [ + 'gdb', '-x', 'gdbinit-autogenerated', '--args', './mcmini', '--from-first-checkpoint', '-mtf' + ] +) diff --git a/docs/design/sketch/mcmini_program_evolution.png b/dmtcp/sketch/mcmini_program_evolution.png similarity index 100% rename from docs/design/sketch/mcmini_program_evolution.png rename to dmtcp/sketch/mcmini_program_evolution.png diff --git a/docs/design/sketch/state_sequence.png b/dmtcp/sketch/state_sequence.png similarity index 100% rename from docs/design/sketch/state_sequence.png rename to dmtcp/sketch/state_sequence.png diff --git a/dmtcp/src/common/exit.c b/dmtcp/src/common/exit.c new file mode 100644 index 00000000..d408106b --- /dev/null +++ b/dmtcp/src/common/exit.c @@ -0,0 +1,22 @@ +#include "mcmini/common/exit.h" + +#include +#include + +#include "mcmini/common/exit.h" + +void mc_exit(int status) { + // The exit() function is intercepted. Calling exit() directly + // results in a deadlock since the thread calling it will block + // forever (McMini does not let a process exit() during model + // checking). Keep this in mind before switching this call to + // a different exit function + _Exit(status); +} + +void mc_exit_mt(int status) { + static pthread_mutex_t exit_mut = PTHREAD_MUTEX_INITIALIZER; + pthread_mutex_lock(&exit_mut); + mc_exit(status); + pthread_mutex_unlock(&exit_mut); +} diff --git a/dmtcp/src/common/mem.c b/dmtcp/src/common/mem.c new file mode 100644 index 00000000..6e1b61aa --- /dev/null +++ b/dmtcp/src/common/mem.c @@ -0,0 +1,29 @@ +#include "mcmini/mem.h" + +volatile void *memset_v(volatile void *dst, int ch, size_t n) { + volatile unsigned char *dstc = dst; + while ((n--) > 0) dstc[n] = ch; + return dst; +} + +volatile void *memcpy_v(volatile void *dst, const volatile void *src, + size_t n) { + // From cppreference on the use of restrict pointers in the C language: + // + // | Restricted pointers can be assigned to unrestricted pointers freely, the + // | optimization opportunities remain in place as long as the compiler is + // | able to analyze the code: + // | + // | void f(int n, float * restrict r, float * restrict s) + // | { + // | float *p = r, *q = s; // OK + // | while (n-- > 0) + // | *p++ = *q++; // almost certainly optimized just like *r++ = *s++ + // | } + // + // See https://en.cppreference.com/w/c/language/restrict for details. + const volatile unsigned char *srcc = src; + volatile unsigned char *dstc = dst; + while ((n--) > 0) dstc[n] = srcc[n]; + return dst; +} diff --git a/dmtcp/src/common/multithreaded_fork.c b/dmtcp/src/common/multithreaded_fork.c new file mode 100644 index 00000000..0ff6fb3d --- /dev/null +++ b/dmtcp/src/common/multithreaded_fork.c @@ -0,0 +1,675 @@ +/***************************************************************************** + * Copyright (C) 2024 Gene Cooperman * + * * + * This program is free software: you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 3 of the * + * License, or (at your option) any later version. * + * * + * DMTCP is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with DMTCP. If not, see . * + *****************************************************************************/ + +// NOTE: This version of fork() does not support pthread_atfork(), +// and it doesn't do the weird stuff about cancellation points, +// or robust mutexes. + +// Remove this 'define' if using this with other software. +// #define STANDALONE + +// FIXME: the virtual_to_real logic is not yet working, when not using libdmtcp.so +// Define this if using this with DMTCP: +// #define DMTCP + +// FIXME: glibc-2.28 and earlier don't define _Fork(). Replace by syscall and test. +// FIXME: Check all SYS_gettid, SYS_tgkill, get_child_threads for whether +// they use virtual or real tid; Probably extract declarations +// from DMTCP, and then optionally test DMTCP.via dmtcp_is_enabled(), +// or simply convert tall tids to virtual. +// MODIFY: get_child_threads to return virtual tid. +// NOTE: syscall(SYS_tgkill, ...) takes a virtual tid. +// FIXME: This is arguably a bug in glibc: +// glibc returns EBUSY if pthread_join() is called and succeeds and pthread_tryjoin_np is then called. +// It's because pthread_join() sets tid to -1, and then pthread_tryjoin_np tests 'if (tid != 0) {EBUSY;}' +// It should return ESRCH rather than EBUSY if the child thread has already joined. +// FIXME: Change _Fork to syscall(SYS_fork/SYS_clone, ...) (support all libc's) +// and add comment about _Fork() for robust mutex if desired. +// FIXME: After ARCH_SET_FS, errno becomes 22. Why? +// FIXME: We should clean up the temporary child thread stack after doing setcontext. +// This code still has to: +// 1. add the current tid to the thread descriptor (pthread_t) +// DONE: See getTLSPointer()/getTLSPointer() +// 2. set restore the TLS: src/tls.cpp:TLSInfo_RestoreTLSPointer() +// DONE: See getTLSPointer(), setTLSPointer() +// 3. support pthread_sigmask +// TODO: See DMTCP:src/tls.cpp, near the call to setcontext() + +#define _GNU_SOURCE +#include +#include +#include +#include // man 2 open +#include +#include // For clone() +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dmtcp.h" + +#ifdef MC_SHARED_LIBRARY +#include "mcmini/real_world/process/template_process.h" +#include "mcmini/spy/checkpointing/record.h" +#include "mcmini/spy/intercept/interception.h" +#endif + +// These are for debugging, only. If segfault, do infinite loop, +// and later, we can attach with GDB. +#ifdef STANDALONE +static void SegvfaultHandler(int signum, siginfo_t *siginfo, void *context) { + while(1); +} +static int AddSegvHandler() { + struct sigaction act; + static struct sigaction old_act; + + act.sa_sigaction = &SegvfaultHandler; + act.sa_flags = SA_RESTART | SA_SIGINFO; + sigemptyset(&act.sa_mask); + if (sigaction(SIGSEGV, &act, &old_act)) { + perror("Failed to install segv handler"); + return -1; + } + return 0; +} +#endif + +// In DMTCP, 'struct threadinfo' is declared as DMTCP:src/threadinfo.h:ThreadTLSInfo +struct threadinfo { + pid_t origTid; // Used for debugging, and 'origTid == 0' if not a valid entry. + ucontext_t context; + // fs and gs only used in __x86_64__ + unsigned long fs; + unsigned long gs; + // tlsAddr only used in __aarch64__ and __riscv + // In fact, __riscv has the address in a normal register, restored w/ context. + unsigned long int tlsAddr; + // The kernel has a process-wide sigmask, and also a per-thread sigmask. + sigset_t thread_sigmask; + // glibc:pthread_create and pthread_self use this, but not the clone call: + pthread_t pthread_descriptor; +} childThread[1000]; + +volatile atomic_int threadIdx = 0; // threadInfo[threadIdx] is 'struct threadinfo' for next thread. + +#ifdef __x86_64__ +# include +# include + +static void getTLSPointer(struct threadinfo *localThreadInfo) { + + assert(syscall(SYS_arch_prctl, ARCH_GET_FS, &localThreadInfo->fs) == 0); + assert(syscall(SYS_arch_prctl, ARCH_GET_GS, &localThreadInfo->gs) == 0); +} + +static void setTLSPointer(struct threadinfo *localThreadInfo) { + assert(syscall(SYS_arch_prctl, 2, ARCH_SET_FS, localThreadInfo->fs) != 0); + assert(syscall(SYS_arch_prctl, 2, ARCH_SET_GS, localThreadInfo->gs) != 0); +} +#elif defined(__aarch64__) +// 1776 valid for glibc-2.17 and beyond +static void getTLSPointer(struct threadinfo *localThreadInfo) { + unsigned long int addr; + asm volatile ("mrs %0, tpidr_el0" : "=r" (addr)); + localThreadInfo->tlsAddr = addr - 1776; // sizeof(struct pthread) = 1776 +} +static void setTLSPointer(struct threadinfo *localThreadInfo) { + unsigned long int addr = localThreadInfo->tlsAddr + 1776; + asm volatile ("msr tpidr_el0, %[gs]" : :[gs] "r" (addr)); +} +#elif defined(__riscv) +void getTLSPointer(struct threadinfo *localThreadInfo) +{ + unsigned long int addr; + asm volatile ("addi %0, tp, 0" : "=r" (addr)); + localThreadInfo->tlsAddr = addr - 1856; // sizeof(struct pthread)=1856 +} +void setTLSPointer(struct threadinfo *localThreadInfo) +{ + unsigned long int addr = localThreadInfo->tlsAddr + 1856; + asm volatile("addi tp, %[gs], 0" : : [gs] "r" (addr)); +} +#else +void getTLSPointer(struct threadinfo *localThreadInfo) {} +void setTLSPointer(struct threadinfo *localThreadInfo) {} +#endif + +// SEE DMTCP:src/tls.cpp:TLSInfo_GetTidOffset() +// and DMTCP:src/tls.cpp:TLSInfo_RestoreTLSTidPid(Thread *thread) +// If constants are not right, run DMTCP:util/check-pthread-tid-offset.c -v +// Returns offset of tid in pthread_t. +int pthreadDescriptorTidOffset() { +#ifdef __x86_64__ + // Since glibc-2.11: + int offset = 720; // sizeof(tcbhead_t) + sizeof(list_t) +#elif defined(__aarch64__) + int offset = 208; +#elif defined(__riscv) + int offset = 208; +#endif + return offset; +} +pid_t get_tid_from_pthread_descriptor(pthread_t pthread_descriptor) { + int offset = pthreadDescriptorTidOffset(); + pid_t ctid = *(pid_t*)((char*)(pthread_descriptor) + offset); +#ifdef DMTCP + pid_t virttid = dmtcp_real_to_virtual_pid(ctid); + ctid = (virttid ? virttid : ctid); +#endif + return ctid; +} +static pid_t patchThreadDescriptor(pthread_t pthreadSelf) { + int offset = pthreadDescriptorTidOffset(); + pid_t oldtid = *(pid_t *)((char *)pthreadSelf + offset); + // Since glibc.2.25, tid, but not pid, is stored in pthread_t. + // gettid() supported only in glibc-2.30; So, we use syscall(). + *(pid_t *)((char *)pthreadSelf + offset) = syscall(SYS_gettid); + return oldtid; +} + +void restart_child_threads(); +void multithreaded_fork_child_handler(int sig); + +#define SIG_MULTITHREADED_FORK (SIGRTMIN+6) + +int get_child_threads(int child_threads[]) { + DIR *dir; + struct dirent *entry; + dir = opendir("/proc/self/task"); + if (dir == NULL) { + perror("opendir"); + return -1; + } + int i = 0; + while ((entry = readdir(dir)) != NULL) { + if (atoi(entry->d_name) != 0) { + pid_t nexttid = atoi(entry->d_name); +#ifdef DMTCP + pid_t virttid = dmtcp_real_to_virtual_pid(nexttid); + nexttid = (virttid ? virttid : nexttid); +#endif + child_threads[i++] = nexttid; + } + } + child_threads[i] = 0; + closedir(dir); + return i; +} + +// Optimization: could skip pthread_sigmask for targets not using it +// (We would need a wrapper function to test it.) +void saveThreadStateBeforeFork(struct threadinfo* threadInfo) { + threadInfo->origTid = syscall(SYS_gettid); + + // For verification only; not needed for functionality + pid_t oldTid = patchThreadDescriptor(pthread_self()); + if (oldTid != syscall(SYS_gettid)) { + fprintf(stderr, "PID %d: multithreaded_fork(): patchThreadDescriptor:" + "bad offset:\n Run: DMTCP:util/check-pthread-tid-offset.c\n", + getpid()); + libc_abort(); + } + threadInfo->pthread_descriptor = pthread_self(); + getTLSPointer(threadInfo); + + // FIXME: Add func fo get/set signals in child thread of child process. + // and restore thread sigmask sfter setcontext. + pthread_sigmask(SIG_BLOCK, NULL, &threadInfo->thread_sigmask); + sigset_t sigtest; + pthread_sigmask(SIG_BLOCK, NULL, &sigtest); + sigdelset(&sigtest, SIG_MULTITHREADED_FORK); + if (! sigisemptyset(&sigtest)) { + fprintf(stderr, "PID %d: multithreaded_fork() not yet implemented" + " for non-empty thread signaks\n", getpid()); + libc_abort(); + } +} + +void restoreThreadStateAfterFork(struct threadinfo* threadInfo) { + setTLSPointer(threadInfo); // Set the fs register (set thread-local address) +// FIXME: +// NOTE: glibc clone allocated a new pthread descriptor on the temporary stack used by child_setcontext +// But the tcb (Thread Control Block) always starts at %fs:0 (or tpidr_el0 for aarch64 or tp for riscv). +// And glibc finds the current pthread_descriptor as pointed to by %fs:0x10 +// Now that we have restored %fs, we can restore the pthread descriptor. +// Caveat: I hope that the %fs used in glibc clone didn't overwrite our restored %fs and +// then overwrite the tcb. If it did, we would have to restore the full tcb. +// QUESTION: Does glibc fork() keep the same %fs:0 address in child? +// NOTE: pthread_tryjoin_np() was signaling an error, until we did this. +// But pthread_join() has an error: errno 2: No such file or directory IN: +// rc1 error: perror("****** Child process: parent thread: pthread_join"); + patchThreadDescriptor(threadInfo->pthread_descriptor); // Update the tid +} + + +# define STRINGIFY2(x) #x +# define STRINGIFY(x) STRINGIFY2(x) +void signal_multithreaded_fork_handler() { + sigset_t emptyset; + sigemptyset(&emptyset); + struct sigaction new = {.sa_handler = multithreaded_fork_child_handler, + .sa_flags = SA_RESTART, .sa_mask = emptyset}; + struct sigaction old; + sigaction(SIG_MULTITHREADED_FORK, &new, &old); + if (old.sa_handler != SIG_DFL) { + fprintf(stderr, + "\n***************************************************************\n" + "* WARNING: There was a previous handler for signal %s\n" + "* And multithreaded_fork() is overwriting it.\n" + "* Maybe change SIG_MULTITHREADED_FORK in source code.\n" + "***************************************************************\n\n", + STRINGIFY(SIG_MULTITHREADED_FORK)); + } +} + +// Semaphore for parent thread. +// Used in the original process before fork, and child process after fork. +sem_t sem_fork_parent; +// Semaphore for child thread; Only used in child process. +sem_t sem_fork_child; + +pid_t multithreaded_fork() { + // WARNING: Doesn't yet support concurrency (currently calling + // multithreaded_fork()) Semaphore for parent thread. + atomic_store(&threadIdx, + 0); // Needed for multiple calls to multithreaded_fork() + static int initialized = 0; + if (!initialized) { + libpthread_sem_init(&sem_fork_child, 0, 0); + libpthread_sem_init(&sem_fork_parent, 0, 0); + } + + static int handler_is_declared = 0; + if (! handler_is_declared) { + signal_multithreaded_fork_handler(); + handler_is_declared = 1; + } + + int tids[1000]; + int num_threads = get_child_threads(tids); + const pid_t mytid = syscall(SYS_gettid); + +#ifdef MC_SHARED_LIBRARY + const pid_t ckpt_tid = get_tid_from_pthread_descriptor(ckpt_pthread_descriptor); +#endif + + for (int i = 0; i < num_threads; i++) { +#ifdef DMTCP + if (tids[i] != mytid && tids[i] != ckpt_tid) { +#else + if (tids[i] != mytid) { +#endif + int rc = syscall(SYS_tgkill, getpid(), tids[i], SIG_MULTITHREADED_FORK); + assert(rc == 0); + } + } + +#ifdef DMTCP + // NOTE: `(num_threads - 2) = # user space threads - this thread (1) - checkpoint thread (1) + int num_secondary_threads = num_threads - 2; +#else + // NOTE: `(num_threads - 1) = # user space threads - this thread (1) + int num_secondary_threads = num_threads - 1; +#endif + + for (int i = 0; i < num_secondary_threads; i++) { + libpthread_sem_wait_loop( + &sem_fork_parent); // Wait until children have initialized context. + } + + int ckptThreadIdx = atomic_fetch_add(&threadIdx, 1); + int templateThreadThreadIdx = atomic_fetch_add(&threadIdx, 1); + childThread[ckptThreadIdx].origTid = 0; + childThread[templateThreadThreadIdx].origTid = 0; + + /********************************************************************* + *NOTE: We want to skip the normal fork cleanup + * of child threads (e.g., reclaim_stack()). + * So, we call an internal version of fork(). + * + *__libc__fork() calls: + * + *../sysdeps/unix/sysv/linux/arch-fork.h:26 + *__libc_fork() calls: + *_Fork() { + * pid_t pid = arch_fork (&THREAD_SELF->tid); + * ... robust mutex support ... + *} + * + *../sysdeps/unix/sysv/linux/arch-fork.h:34 + *statinc inline pid_t arch_fork(void *ctid) { + * echo 'flags = CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD' + * ret = INLINE_SYSCALL_CALL (clone, flags, 0, NULL, ctid, 0); + *} + *********************************************************************/ +#if 1 + pid_t _Fork(); + int childpid = _Fork(); +#else + // NOT YET FULLY DEVELOPED: + int flags = CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD; + int childpid; +// syscall(SYS_clone, ...); +// stack must be NULL +// https://stackoverflow.com/questions/2898579/clone-equivalent-of-fork +// But that says to use only SIGCHLD for flags, and glibc uses the above. +// But it's okay, since we're setting ctid and tls to NULL. +// FIXME: If we're going to set the last 3 args to NULL, who cares in what order they're found! +# ifdef __x86_64__ + long clone(unsigned long flags, void *stack, + int *parent_tid, int *child_tid, + unsigned long tls); +# elif defined(__aarch64__) + long clone(unsigned long flags, void *stack, + int *parent_tid, unsigned long tls, + int *child_tid); +# elif defined(__riscv) +# error Unimplemented CPU architecture +https://github.com/bminor/glibc/blob/master/sysdeps/unix/sysv/linux/riscv/clone.S +int clone(int (*fn)(void *arg), void *child_stack, int flags, void *arg, + void *parent_tidptr, void *tls, void *child_tidptr) */ + /* The syscall expects the args to be in different slots. */ + mv a0,a2 + mv a2,a4 + mv a3,a5 + mv a4,a6 +# endif +#endif + initialized = 0; // Reset for next call to multithreaded_fork() + if (childpid == 0) { // child process + restart_child_threads(num_threads); + } + return childpid; +} + +// This could be optimized for performance: +// orig_pid could be static var, get/setTLSPointer could use assembly (cf MANA) +void multithreaded_fork_child_handler(int sig) { + if (sig == SIG_MULTITHREADED_FORK) { + // The handler is called before fork(). So, this will always be parent pid + // pid_t orig_pid = getpid(); + int origThreadIdx = atomic_fetch_add(&threadIdx, 1); + struct threadinfo* threadInfo = &childThread[origThreadIdx]; + memset(threadInfo, 0x0, sizeof(struct threadinfo)); + + saveThreadStateBeforeFork(threadInfo); + int rc = getcontext(&threadInfo->context); + assert(rc == 0); + // setcontext() returns to here after fork() and clone() of + // child thread (setcontext) and setTLSPointer() + + // NOTE: After the call to `sem_post(3)`, the parent thread + // may then call `_Fork()`. But this is OK: only the (forked) parent + // thread in the child process will initially exist, and this thread + // only uses the result of `getcontext()`. + libpthread_sem_post(&sem_fork_parent); + } +} + +#if 0 +#include +#include +#include +#endif +// 'int' return type required by 'clone()' +int child_setcontext(void *arg) { + struct threadinfo* threadInfo = arg; + // restoreThreadStateAfterFork(threadInfo); + setTLSPointer(threadInfo); + patchThreadDescriptor(threadInfo->pthread_descriptor); + setcontext(&(threadInfo->context)); + return 0; // not reached +} + +void restart_child_threads(int num_threads) { + for (int i = 0; i < num_threads; i++) { + if (childThread[i].origTid == 0) continue; + + // int clone(int (*fn)(void *), void *stack, int flags, void *arg, ... + // /* pid_t *parent_tid, void *tls, pid_t *child_tid */ ); + int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM + | CLONE_SIGHAND | CLONE_THREAD + | CLONE_SETTLS | CLONE_PARENT_SETTID + | CLONE_CHILD_CLEARTID + | 0); + // This is a temporary stack, to be replaced after setcontext. + // FIXME: This stack is a memory leak. We should free it later. + void *stack = malloc(0x10000) + 0x10000 - 128; // 64 KB + int offset = pthreadDescriptorTidOffset(); + pid_t *ctid = (pid_t*)((char*)childThread[i].pthread_descriptor + offset); + pid_t *ptid = ctid; + // For more insight, read 'man set_tid_address'. + clone(child_setcontext, + stack, + clone_flags, + (void *)&childThread[i], ptid, childThread[i].fs, ctid); + } +} + +// =========================================================================== +// This main routine and child() demonstrates the use of multithreaded_fork(). +#ifdef STANDALONE + +sem_t sem_parent; +sem_t sem_child; + +void fprintf_thread(FILE *stream, const char *format_string, int pid) { + char buf[500]; + snprintf(buf, sizeof(buf), format_string, pid); + // This isn't really thread-safe, but kernel usually finishes this + // syscall before starting a new syscall. + write(fileno(stream), buf, strlen(buf)); +} +void printf_thread(const char *format_string, int pid) { + fprintf_thread(stdout, format_string, pid); +} + +void *child_thread(void *dummy) { +printf("=============== 3. (char *)pthread_self()+720: %p\n", (char *)pthread_self()+720); + int orig_pid = getpid(); // We do this before forking; So, it's the parent. + pthread_t orig_pthread_self = pthread_self(); + sem_post(&sem_parent); + int rc = sem_wait(&sem_child); // Wait while parent does multithreaded fork() + // Parent has done multithreaded_fork(); we are in child or parent process + if (rc != 0) { + perror("sem_wait"); + } + // 'errno' is a thread-local variable. Test if thread-local was restored. + int fd = open("/FILE_DOES_NOT_EXIST", O_RDONLY); + if (getpid() != orig_pid) { + assert(fd == -1 && errno == ENOENT); + } + // We have now forked. If we are a child thread of the child process, do we + // still remember our orig_pthread_self (from child thread + // of parent process)? Test if it's the same. + if (getpid() != orig_pid) { + assert(pthread_equal(pthread_self(), orig_pthread_self)); + } +if (getpid() != orig_pid) { +printf("=============== 4. (char *)pthread_self()+720: %p\n", (char *)pthread_self()+720); +printf("=============== 4b. *(int *)((char *)pthread_self()+720): %d\n", *(int *)((char *)pthread_self()+720)); +printf("=============== 5. (char *)pthread_self()+720: %p\n", (char *)pthread_self()+720); +printf("=============== 5b. *(int *)((char *)pthread_self()+720): %d\n", *(int *)((char *)pthread_self()+720)); +} + if (getpid() != orig_pid) { + sleep(1); // Let's see if parent or child process joins with this thread. + } + char format[] = "Child thread %ld of %s process is exiting."; + char *return_string = malloc(50); + snprintf(return_string, 50, format, syscall(SYS_gettid), + (getpid() == orig_pid ? "parent" : "child")); + return return_string; +} + +#pragma GCC diagnostic ignored "-Wformat-security" +#ifndef NUM_THREADS +# define NUM_THREADS 3 +#endif +int main() { + sem_init(&sem_parent, 0, 0); + sem_init(&sem_child, 0, 0); +AddSegvHandler(); + pthread_t thread[NUM_THREADS-1]; + for (int i = 0; i < NUM_THREADS-1; i++) { + pthread_create(&thread[i], NULL, child_thread, NULL); + } + for (int i = 0; i < NUM_THREADS-1; i++) { + sem_wait(&sem_parent); + } +printf("=============== 2. (char *)thread[0]+720: %p\n", (char *)thread[0]+720); + printf("Forking ...\n"); + pid_t childpid = multithreaded_fork(); + for (int i = 0; i < NUM_THREADS-1; i++) { + sem_post(&sem_child); // Let child thread move on. + } + + if (childpid > 0) { // If parent process (parent thread) + // Wait and allow parent thread of child process to do pthread_join first. + int status; + assert(waitpid(childpid, &status, 0) == childpid); + printf("waitpid: child process has exited %s.\n", + (WIFEXITED(status) ? "normally" : "**ABNORMALLY**")); + if (WEXITSTATUS(status) != 0) { + printf("*** The child process had exited with return code %d\n", + WEXITSTATUS(status)); + } + if (WIFEXITED(status) == 0 && WIFSIGNALED(status)) { + printf("*** The child process had terminated with the signal %d\n", + WTERMSIG(status)); + } + } + +if (childpid == 0) { +printf("=============== 6. (char *)thread[0]+720: %p\n", (char *)thread[0]+720); +} + void *child_return_string; + for (int i = 0; i < NUM_THREADS-1; i++) { + assert(pthread_join(thread[i], &child_return_string) == 0); + printf("Child thread has joined %ld: %s\n", + syscall(SYS_gettid), (char *)child_return_string); + free(child_return_string); // Was malloc'ed in child thread + } + + if (childpid == 0) { // if child process (parent thread) + // NOTE: glibc:pthread_tryjoin_np has a bug. If pthread_join is called + // first, then the tid field of 'struct pthread' will be '-1', + // and so pthread_tryjoin_np returns EBUSY, instead of ESARCH. + int rc2 = pthread_tryjoin_np(thread[0], &child_return_string); + // non-posix: Returns EBUSY or 0 or thread_tid, below, == -1 + // But it never returns eSRCH (no such thread). + int offset = pthreadDescriptorTidOffset(); + pid_t thread_tid = *(int *)((char *)thread[0] + offset); + if (rc2 == EBUSY && thread_tid != -1) { + fprintf_thread(stderr, + "\n*** ERROR: PID %d: pthread_tryjoin_np detected" + " EXTRA thread\n*** (bug in pthread_t:tid?)\n" + "*** NOTE: The 'pthread_join' likely returned success.\n" + "*** without joining to any child thread.\n" + "*** Run: DMTCP:util/check-pthread-tid-offset.c\n\n", + getpid()); + fflush(stderr); + return 0; + } else if (rc2 == EINVAL) { + printf_thread("*** PID %d: pthread_tryjoin_np returned EINVAL\n", + getpid()); + } else { + printf_thread("PID %d: And pthread_tryjoin_np shows that there are" + " no more child threads.\n", getpid()); + } + } + + return 0; +} + +#endif + +// =========================================================================== +// DOCUMENTATION NOTES: +#if 0 +glibc uses a pthread descriptor (often noted as 'struct pthread *pd' in the code). +The user code sees this as 'pthread_t', which is the same as 'struct pthread *'. +Therefore, pthread_self() returns a pointer to the thread descriptor of the current thread. + +When we call clone() in glibc, glibc tries to place a newly allocated + thread descriptor at the top of the stack that was passed to clone. +The relevant codes in glibc are: + GLIBC/nptl/descr.h - defines 'struct pthread'. The first field is 'tcbhead_t header' + GLIBC/sysdeps/x86_64/nptl/tls.h - defines 'tcbhead_t', and also THREAD_SELF + GLIBC/sysdeps/x86_64/nptl/tcb-access.h - defines THREAD_GETMEN and THREAD_SETMEM + GLIBC/nptl/pthread_create.c - creates new thread descriptor, and calls clone() + +The first three fields of tcbhead_t are: + void *tcb; /* Pointer to the TCB. Not necessarily the + thread descriptor used by libpthread. */ + dtv_t *dtv; + void *self; /* Pointer to the thread descriptor. */ + +THREAD_SELF is defined as: + # define THREAD_SELF \ + (*(struct pthread *__seg_fs *) offsetof (struct pthread, header.self)) +where the macro is referring to the 'header' field at the beginning of 'struct pthread'. +In fact, pthread_self(), internally, is simply returning THREAD_SELF. +NOTE ON THREAD_SELF: Apparently, the notation '*__seg_fs' tells the compiler to use the + %fs segment register. So, pthread_self() executes 'mov %fs:0x10,%rax' and returns %rax. + where %fs:0 is pointing to header (tcbhead_t field of pthread descriptor). + Note that 0x10 is the offset of 'self' in tcbhead_t, thus confirming our analysis. + So, in conclusion, at assembly level, THREAD_SELF will alwasy translate to %fs:0x10, + which is always a pointer to the pthread_t thread descriptor. + Thus, if we wanted to change the thread to point to a different thread descriptor, it is as simple as: + void *tmp[] = (void **)pthread_self(); tmp[2] = address_of_new_thread_descriptor; + (But only after verifying that your fs register is correct.) + +The dtv field is a data structure to access variables across multiple dynamic libraries. +It is referred to here in case there are thread-local variables declared in dynamic variables. +We are not concerned with it here, but dtv stands for "Dynamic Thread Vector", +and there is a good description here: + https://chao-tic.github.io/blog/2018/12/25/tls + "A Deep dive into (implicit) Thread Local Storage" + +The glibc internal code does not access the thread descriptor fields directly. +Instead, it uses THREAD_GETMEN and THREAD_SETMEM. Almost always, it is invoked as: + THREAD_GETMEN(THREAD_SELF, name_of_descriptor_field) + THREAD_SETMEN(THREAD_SELF, name_of_descriptor_field, new_value) + +QUESTION: +clone() with CLONE_SETTLS -- It says this can be used to set %fs register. True? + +COMMENT: + GLIBC/sysdeps/x86_64/nptl/tls.h has a comment: + /* Must be kept even if it is no longer used by glibc since programs, + like AddressSanitizer, depend on the size of tcbhead_t. */ + Hence, GLIBC is unlikely to change the size of tcbhead_t in the future. + This implies, luckily for us, that the offset of tid in pthread_t is also + not likely to change, in the future. + +NOTE: + 'man set_tid_address' has important information about CLONE_CHILD_CLEARTID. + In particular, the kernel keeps a clear_child_tid address associated with + the child thread created by clone(). This points to the 'tid' field + of the 'struct pthread' (of the struct pointed to by 'pthread_t'). + When pthread_join wakes from FUTEX_WAKE, it sets 'tid' to -1. +#endif diff --git a/dmtcp/src/common/runner_mailbox.c b/dmtcp/src/common/runner_mailbox.c new file mode 100644 index 00000000..e11af991 --- /dev/null +++ b/dmtcp/src/common/runner_mailbox.c @@ -0,0 +1,68 @@ +#include "mcmini/real_world/mailbox/runner_mailbox.h" + +#include +#include + +#include "mcmini/lib/log.h" +#include "mcmini/defines.h" +#include "mcmini/spy/intercept/interception.h" +#include "string.h" + +void mc_runner_mailbox_init(volatile runner_mailbox* r) { + runner_mailbox_ref ref = (runner_mailbox_ref)(r); +#ifdef MC_SHARED_LIBRARY + libpthread_sem_init(&ref->model_side_sem, SEM_FLAG_SHARED, 0); + libpthread_sem_init(&ref->child_side_sem, SEM_FLAG_SHARED, 0); +#else + sem_init(&ref->model_side_sem, SEM_FLAG_SHARED, 0); + sem_init(&ref->child_side_sem, SEM_FLAG_SHARED, 0); +#endif + memset(ref->cnts, 0, sizeof(ref->cnts)); +} + +void mc_runner_mailbox_destroy(volatile runner_mailbox* r) { + runner_mailbox_ref ref = (runner_mailbox_ref)(r); +#ifdef MC_SHARED_LIBRARY + libpthread_sem_destroy(&ref->model_side_sem); + libpthread_sem_destroy(&ref->child_side_sem); +#else + sem_destroy(&ref->model_side_sem); + sem_destroy(&ref->child_side_sem); +#endif +} + +int mc_wait_for_thread(volatile runner_mailbox* r) { + runner_mailbox_ref ref = (runner_mailbox_ref)(r); +#ifdef MC_SHARED_LIBRARY + return libpthread_sem_wait(&ref->model_side_sem); +#else + return sem_wait(&ref->model_side_sem); +#endif +} + +int mc_wait_for_scheduler(volatile runner_mailbox* r) { + runner_mailbox_ref ref = (runner_mailbox_ref)(r); +#ifdef MC_SHARED_LIBRARY + return libpthread_sem_wait(&ref->child_side_sem); +#else + return sem_wait(&ref->child_side_sem); +#endif +} + +int mc_wake_thread(volatile runner_mailbox* r) { + runner_mailbox_ref ref = (runner_mailbox_ref)(r); +#ifdef MC_SHARED_LIBRARY + return libpthread_sem_post(&ref->child_side_sem); +#else + return sem_post(&ref->child_side_sem); +#endif +} + +int mc_wake_scheduler(volatile runner_mailbox* r) { + runner_mailbox_ref ref = (runner_mailbox_ref)(r); +#ifdef MC_SHARED_LIBRARY + return libpthread_sem_post(&ref->model_side_sem); +#else + return sem_post(&ref->model_side_sem); +#endif +} diff --git a/dmtcp/src/common/shm_config.c b/dmtcp/src/common/shm_config.c new file mode 100644 index 00000000..6dd53157 --- /dev/null +++ b/dmtcp/src/common/shm_config.c @@ -0,0 +1,16 @@ +#include "mcmini/common/shm_config.h" + +#include +#include +#include +#include +#include + +#include "mcmini/real_world/mailbox/runner_mailbox.h" + +const size_t shm_size = sizeof(struct mcmini_shm_file); + +void mc_get_shm_handle_name(char *dst, size_t sz) { + snprintf(dst, sz, "/mcmini-%s-%lu", getenv("USER"), (long)getppid()); + dst[sz - 1] = '\0'; +} diff --git a/dmtcp/src/examples/CMakeLists.txt b/dmtcp/src/examples/CMakeLists.txt new file mode 100644 index 00000000..5c056b14 --- /dev/null +++ b/dmtcp/src/examples/CMakeLists.txt @@ -0,0 +1,11 @@ +add_executable(hello-world hello-world.c) +add_executable(cv-hello-world cv-hello-world.c) +add_executable(cv-test cv-test.c) +add_executable(deadly-embrace deadly-embrace.c) +add_executable(fifo-example fifo.cpp) +add_executable(producer-consumer producer-consumer.c) +target_link_libraries(hello-world PUBLIC -pthread) +target_link_libraries(cv-hello-world PUBLIC -pthread) +target_link_libraries(cv-test PUBLIC -pthread) +target_link_libraries(deadly-embrace PUBLIC -pthread) +target_link_libraries(producer-consumer PUBLIC -pthread) diff --git a/dmtcp/src/examples/cv-hello-world b/dmtcp/src/examples/cv-hello-world new file mode 100755 index 00000000..22c824b5 Binary files /dev/null and b/dmtcp/src/examples/cv-hello-world differ diff --git a/dmtcp/src/examples/cv-hello-world.c b/dmtcp/src/examples/cv-hello-world.c new file mode 100644 index 00000000..6cec8886 --- /dev/null +++ b/dmtcp/src/examples/cv-hello-world.c @@ -0,0 +1,129 @@ +// NOTE: You can choose WRITER_PREFERRED, READER_PREFERRED, or default +// NOTE: The bug still exists if you change the reader function to do 1 task: +// for (i = 0; i < 1; i++) { ... } +// However, with 2 reader tasks, running will show the deadlock. +// NOTE: With 1 reader task, running will not show the deadlock. +// and McMini has a bug, and fails to catch the reader-writer bug. +// So, I recommend running as is (without McMini) in this case. + +// TO COMPILE: gcc -g3 -O0 THIS_FILE -pthread +#include +#include +#include +#include +#include + + +// Uncomment preferred policy, or accept default (balanced) policy +//#define WRITER_PREFERRED +#define READER_PREFERRED + +int num_active_readers = 0; +int num_active_writers = 0; +int num_waiting_readers = 0; +int num_waiting_writers = 0; +pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER; +pthread_cond_t cond; +int quiet = 0; + +void *reader(void *arg) { + int *reader_num = arg; + int i; + for (i = 0; i < 3; i++) { + // Acquire permission + sleep(6); + pthread_mutex_lock(&mut); +#ifdef WRITER_PREFERRED + while (num_active_writers > 0 || num_waiting_writers > 0) /*Prefer Writer*/ +#elif defined(READER_PREFERRED) + while (num_active_writers > 0) /* Reader Preferred */ +#else + while (num_active_writers > 0) /* Default */ +#endif + { + num_waiting_readers++; + pthread_cond_wait(&cond, &mut); + num_waiting_readers--; + } + num_active_readers++; + pthread_mutex_unlock(&mut); + // Do task + if (! quiet) { + printf("reader %d did task %d.\n", *reader_num, i+1); + } + // Release permission + pthread_mutex_lock(&mut); + num_active_readers--; + pthread_cond_signal(&cond); + pthread_mutex_unlock(&mut); + } + return NULL; +} + +void *writer(void *arg) { + int *writer_num = arg; + int i; + for (i = 0; i < 2; i++) { + // Acquire permission + sleep(6); + pthread_mutex_lock(&mut); + +#ifdef WRITER_PREFERRED + while (num_active_readers > 0 || num_active_writers > 0) /* Prefer Writer */ +#elif defined(READER_PREFERRED) + while (num_active_readers > 0 || num_active_writers > 0 || + num_waiting_readers > 0) /* Reader Preferred */ +#else + while (num_active_readers > 0 || num_active_writers > 0) /* Default */ +#endif + { + num_waiting_writers++; + pthread_cond_wait(&cond, &mut); + num_waiting_writers--; + } + num_active_writers++; + pthread_mutex_unlock(&mut); + + // Do task + if (! quiet) { + printf("writer %d did task %d.\n", *writer_num, i+1); + } + // Release permission + pthread_mutex_lock(&mut); + num_active_writers--; + pthread_cond_signal(&cond); + pthread_mutex_unlock(&mut); + } + return NULL; +} + +int main(int argc, char *argv[]) { + int num_readers = 2; + int num_writers = 2; + int i; + int thread_number[3] = {1, 2, 3}; + if (argc == 2 && strcmp(argv[1], "--quiet") == 0) { + quiet = 1; + } + // This next stmt not needed, but McMini doesn't yet detect const. initializer + pthread_mutex_init(&mut, NULL); + pthread_t reader_thread[3]; // Create 3 pthread_t even if we don't use it all + pthread_t writer_thread[3]; // Create 3 pthread_t even if we don't use it all + pthread_cond_init(&cond, NULL); + for (i = 0; i < num_readers; i++) { + // Pass in argument 'i' + pthread_create(&reader_thread[i], NULL, reader, &thread_number[i]); + } + for (i = 0; i < num_writers; i++) { + // Pass in argument 'i' + pthread_create(&writer_thread[i], NULL, writer, &thread_number[i]); + } + + for (i = 0; i < num_readers; i++) { + pthread_join(reader_thread[i], NULL); + } + for (i = 0; i < num_writers; i++) { + pthread_join(writer_thread[i], NULL); + } + return 0; +} diff --git a/dmtcp/src/examples/cv-test.c b/dmtcp/src/examples/cv-test.c new file mode 100644 index 00000000..1b67daae --- /dev/null +++ b/dmtcp/src/examples/cv-test.c @@ -0,0 +1,118 @@ +#include +#include +#include +#include + +#define NUM_CONSUMERS 3 + +pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; +pthread_cond_t cond = PTHREAD_COND_INITIALIZER; + +int data_ready = 0; +int stop_flag = 0; +int destroy_cv = 0; // Flag to control CV destruction + +// Consumer threads wait for data +void* consumer(void* arg) { + int id = *(int*)arg; + + printf("Consumer %d: Started\n", id); + sleep(5); // Give time for checkpoint + + pthread_mutex_lock(&mutex); + + // Wait for data to be ready + while (!data_ready && !stop_flag) { + printf("Consumer %d: Waiting for data...\n", id); + pthread_cond_wait(&cond, &mutex); + printf("Consumer %d: Woke up\n", id); + } + + if (data_ready) { + printf("Consumer %d: Processing data\n", id); + sleep(1); // Simulate processing + } else { + printf("Consumer %d: Stopped without data\n", id); + } + + pthread_mutex_unlock(&mutex); + printf("Consumer %d: Finished\n", id); + return NULL; +} + +// Producer broadcasts to all consumers +void* producer(void* arg) { + printf("Producer: Started\n"); + sleep(5); // Give time for consumers to start waiting + + pthread_mutex_lock(&mutex); + data_ready = 1; + printf("Producer: Data is ready, broadcasting to all consumers\n"); + pthread_cond_broadcast(&cond); // Wake all consumers + pthread_mutex_unlock(&mutex); + + printf("Producer: Finished\n"); + return NULL; +} + +// Cleanup thread might destroy CV prematurely +void* cleanup(void* arg) { + printf("Cleanup: Started\n"); + sleep(5); // Sleep to allow consumers to start waiting + + if (destroy_cv) { + // Potential bug: Destroy CV while threads are waiting + printf("Cleanup: Destroying condition variable prematurely!\n"); + pthread_cond_destroy(&cond); // Problematic if threads are waiting + printf("Cleanup: Destroyed\n"); + } else { + printf("Cleanup: Decided not to destroy CV\n"); + } + + return NULL; +} + +int main(int argc, char* argv[]) { + pthread_t consumers[NUM_CONSUMERS]; + pthread_t prod, clean; + int ids[NUM_CONSUMERS]; + + // Allow command-line control over deadlock scenario + if (argc > 1 && atoi(argv[1]) != 0) { + destroy_cv = 1; + printf("Running with CV destruction (deadlock possible)\n"); + } else { + printf("Running without CV destruction (normal operation)\n"); + } + + // Create consumer threads + for (int i = 0; i < NUM_CONSUMERS; i++) { + ids[i] = i + 1; + pthread_create(&consumers[i], NULL, consumer, &ids[i]); + } + + // Create producer and cleanup threads + pthread_create(&prod, NULL, producer, NULL); + pthread_create(&clean, NULL, cleanup, NULL); + + // Allow time for checkpoint + printf("Main: Sleeping to allow operations to proceed\n"); + sleep(5); + + // Join all threads + for (int i = 0; i < NUM_CONSUMERS; i++) { + pthread_join(consumers[i], NULL); + } + pthread_join(prod, NULL); + pthread_join(clean, NULL); + + sleep(5); + // Clean up if needed + if (!destroy_cv) { + pthread_cond_destroy(&cond); + } + pthread_mutex_destroy(&mutex); + + printf("Main: Program completed\n"); + return 0; +} \ No newline at end of file diff --git a/dmtcp/src/examples/deadly-embrace.c b/dmtcp/src/examples/deadly-embrace.c new file mode 100644 index 00000000..91d1b0ff --- /dev/null +++ b/dmtcp/src/examples/deadly-embrace.c @@ -0,0 +1,34 @@ +// Naive dining philosophers solution, which leads to deadlock. + +#include +#include +#include +#include + +#define NUM_THREADS 2 + +pthread_mutex_t mut1; +pthread_mutex_t mut2; + +void *philosopher_doit(void *unused) { + sleep(4); + pthread_mutex_lock(&mut2); + pthread_mutex_lock(&mut1); + pthread_mutex_unlock(&mut1); + pthread_mutex_unlock(&mut2); + return NULL; +} + +int main(int argc, char *argv[]) { + pthread_t child; + pthread_mutex_t mutex_resource[NUM_THREADS]; + pthread_mutex_init(&mut1, NULL); + pthread_mutex_init(&mut2, NULL); + pthread_create(&child, NULL, &philosopher_doit, NULL); + sleep(4); + pthread_mutex_lock(&mut1); + pthread_mutex_lock(&mut2); + pthread_mutex_unlock(&mut1); + pthread_mutex_unlock(&mut2); + return 0; +} diff --git a/dmtcp/src/examples/fifo.cpp b/dmtcp/src/examples/fifo.cpp new file mode 100644 index 00000000..425c8b7a --- /dev/null +++ b/dmtcp/src/examples/fifo.cpp @@ -0,0 +1,69 @@ +#include +#include +#include +#include + +#include +#include +#include + +typedef enum visible_object_type { + MUTEX, + SEMAPHORE, + CONDITION_VARIABLE, +} visible_object_type; + +typedef enum mutex_state { + UNINITIALIZED, + UNLOCKED, + LOCKED, + DESTROYED +} mutex_state; + +typedef struct visible_object { + visible_object_type type; + void *location; + union { + mutex_state mut_state; + }; +} visible_object; + +static const char *fifo_name = "/tmp/our_fifo"; + +void reads_the_fifo() { + int fd = open(fifo_name, O_RDONLY); + visible_object received_value; + while (read(fd, &received_value, sizeof(visible_object))) { + std::cout << "Received: " << received_value.location << std::endl; + } + close(fd); +} + +void writes_to_the_fifo() { + int fd = open(fifo_name, O_WRONLY); + + visible_object recorded_objects[10]; + recorded_objects[4].location = (void *)0x400; + recorded_objects[7].location = (void *)0x900; + for (int i = 0; i < 10; i++) { + write(fd, &recorded_objects[i], sizeof(visible_object)); + } + close(fd); +} + +int main(int argc, const char **argv) { + int fifo_fd = -1; + if ((fifo_fd = mkfifo(fifo_name, S_IRUSR | S_IWUSR)) != 0 && + errno != EEXIST) { + std::cerr << "Error (mkfifo failed): " << strerror(errno) << std::endl; + return EXIT_FAILURE; + } + + std::thread reader_thread(&reads_the_fifo); + std::thread writer_thread(&writes_to_the_fifo); + reader_thread.join(); + writer_thread.join(); + + if (fifo_fd != -1) close(fifo_fd); + return 0; +} diff --git a/dmtcp/src/examples/hello-world.c b/dmtcp/src/examples/hello-world.c new file mode 100644 index 00000000..890da814 --- /dev/null +++ b/dmtcp/src/examples/hello-world.c @@ -0,0 +1,59 @@ +// Naive dining philosophers solution, which leads to deadlock. + +#include +#include +#include +#include +#include + +int DEBUG = 0; + +struct forks { + int philosopher; + pthread_mutex_t *left_fork; + pthread_mutex_t *right_fork; +} *forks; + +void *philosopher_doit(void *forks_arg) { + struct forks *forks = forks_arg; + sleep(4); + pthread_mutex_lock(forks->left_fork); + sleep(1); + pthread_mutex_lock(forks->right_fork); + sleep(3); + + if (DEBUG) printf("Philosopher %d just ate.\n", forks->philosopher); + + pthread_mutex_unlock(forks->left_fork); + sleep(1); + pthread_mutex_unlock(forks->right_fork); + return NULL; +} + +int main(int argc, char *argv[]) { + int NUM_THREADS = 3; + DEBUG = 1; + + pthread_t thread[NUM_THREADS]; + pthread_mutex_t mutex_resource[NUM_THREADS]; + + forks = malloc(NUM_THREADS * sizeof(struct forks)); + + int i; + for (i = 0; i < NUM_THREADS; i++) { + pthread_mutex_init(&mutex_resource[i], NULL); + forks[i] = (struct forks){i, &mutex_resource[i], + &mutex_resource[(i + 1) % NUM_THREADS]}; + } + + for (i = 0; i < NUM_THREADS; i++) { + pthread_create(&thread[i], NULL, &philosopher_doit, &forks[i]); + } + + for (i = 0; i < NUM_THREADS; i++) { + pthread_join(thread[i], NULL); + } + + free(forks); + return 0; +} diff --git a/dmtcp/src/examples/producer-consumer.c b/dmtcp/src/examples/producer-consumer.c new file mode 100644 index 00000000..43034fee --- /dev/null +++ b/dmtcp/src/examples/producer-consumer.c @@ -0,0 +1,88 @@ +#include +#include +#include +#include +#include +#include + +#define MaxItems 1 +#define BufferSize 2 + +sem_t empty; +sem_t full; +int in = 0; +int out = 0; +int buffer[BufferSize]; +pthread_mutex_t mutex; +int DEBUG; + +void *producer(void *pno) +{ + int item; + for(int i = 0; i < MaxItems; i++) { + sleep(3); + item = rand(); // Produce an random item + sem_wait(&empty); + pthread_mutex_lock(&mutex); + buffer[in] = item; + if (DEBUG) { + printf("Producer %d: Insert Item %d at %d\n", + *((int *)pno),buffer[in],in); + } + in = (in+1)%BufferSize; + pthread_mutex_unlock(&mutex); + sem_post(&full); + } + return NULL; +} + +void *consumer(void *cno) +{ + for(int i = 0; i < MaxItems; i++) { + sleep(3); + sem_wait(&full); + pthread_mutex_lock(&mutex); + int item = buffer[out]; + if (DEBUG) { + printf("Consumer %d: Remove Item %d from %d\n", + *((int *)cno),item, out); + } + out = (out+1)%BufferSize; + pthread_mutex_unlock(&mutex); + sem_post(&empty); + } + return NULL; +} + +int main(int argc, char* argv[]) +{ + int NUM_PRODUCERS = 1; + int NUM_CONSUMERS = 1; + DEBUG = 1; + + pthread_t pro[NUM_PRODUCERS],con[NUM_CONSUMERS]; + + pthread_mutex_init(&mutex, NULL); + sem_init(&empty,0,BufferSize); + sem_init(&full,0,0); + + int a[NUM_PRODUCERS > NUM_CONSUMERS ? NUM_PRODUCERS : NUM_CONSUMERS]; + + for(int i = 0; i < NUM_PRODUCERS; i++) { + a[i] = i+1; + pthread_create(&pro[i], NULL, producer, (void *)&a[i]); + } + for(int i = 0; i < NUM_CONSUMERS; i++) { + a[i] = i+1; + pthread_create(&con[i], NULL, consumer, (void *)&a[i]); + } + + for(int i = 0; i < NUM_PRODUCERS; i++) { + pthread_join(pro[i], NULL); + } + for(int i = 0; i < NUM_CONSUMERS; i++) { + pthread_join(con[i], NULL); + } + + return 0; +} diff --git a/dmtcp/src/examples/simple_cond_deadlock b/dmtcp/src/examples/simple_cond_deadlock new file mode 100755 index 00000000..22c824b5 Binary files /dev/null and b/dmtcp/src/examples/simple_cond_deadlock differ diff --git a/dmtcp/src/lib/dmtcp-callback.c b/dmtcp/src/lib/dmtcp-callback.c new file mode 100644 index 00000000..9ed8c7ff --- /dev/null +++ b/dmtcp/src/lib/dmtcp-callback.c @@ -0,0 +1,649 @@ +#define _GNU_SOURCE +#include +#include +#include +#include // man 2 open +#include +#include // For clone() +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dmtcp.h" +#include "mcmini/mcmini.h" + +#define SIG_MULTITHREADED_FORK (SIGRTMIN+6) + +// We probably won't need the '#undef', but just in case a .h file defined it: +#undef dmtcp_mcmini_is_loaded +int dmtcp_mcmini_is_loaded(void) { return 1; } + +pthread_t ckpt_pthread_descriptor; +volatile atomic_bool libmcmini_has_recorded_checkpoint_thread; +static sem_t template_thread_sem; +static pthread_cond_t template_thread_cond = PTHREAD_COND_INITIALIZER; +static pthread_mutex_t template_thread_mut = PTHREAD_MUTEX_INITIALIZER; + +struct threadinfo { + pid_t origTid; // Used for debugging, and 'origTid == 0' if not a valid entry. + ucontext_t context; + // fs and gs only used in __x86_64__ + unsigned long fs; + unsigned long gs; + // tlsAddr only used in __aarch64__ and __riscv + // In fact, __riscv has the address in a normal register, restored w/ context. + unsigned long int tlsAddr; + // The kernel has a process-wide sigmask, and also a per-thread sigmask. + sigset_t thread_sigmask; + // glibc:pthread_create and pthread_self use this, but not the clone call: + pthread_t pthread_descriptor; +} threadInfos[1000]; + +static volatile atomic_int threadIdx = 0; // threadInfo[threadIdx] is 'struct threadinfo' for next thread. + +#ifdef __x86_64__ +# include +# include + +void getTLSPointer(struct threadinfo *localThreadInfo) { + assert(syscall(SYS_arch_prctl, ARCH_GET_FS, &localThreadInfo->fs) == 0); + assert(syscall(SYS_arch_prctl, ARCH_GET_GS, &localThreadInfo->gs) == 0); +} + +void setTLSPointer(struct threadinfo *localThreadInfo) { + assert(syscall(SYS_arch_prctl, 2, ARCH_SET_FS, localThreadInfo->fs) != 0); + assert(syscall(SYS_arch_prctl, 2, ARCH_SET_GS, localThreadInfo->gs) != 0); +} +#elif defined(__aarch64__) +// 1776 valid for glibc-2.17 and beyond +static void getTLSPointer(struct threadinfo *localThreadInfo) { + unsigned long int addr; + asm volatile ("mrs %0, tpidr_el0" : "=r" (addr)); + localThreadInfo->tlsAddr = addr - 1776; // sizeof(struct pthread) = 1776 +} +static void setTLSPointer(struct threadinfo *localThreadInfo) { + unsigned long int addr = localThreadInfo->tlsAddr + 1776; + asm volatile ("msr tpidr_el0, %[gs]" : :[gs] "r" (addr)); +} +#elif defined(__riscv) +void getTLSPointer(struct threadinfo *localThreadInfo) +{ + unsigned long int addr; + asm volatile ("addi %0, tp, 0" : "=r" (addr)); + localThreadInfo->tlsAddr = addr - 1856; // sizeof(struct pthread)=1856 +} +void setTLSPointer(struct threadinfo *localThreadInfo) +{ + unsigned long int addr = localThreadInfo->tlsAddr + 1856; + asm volatile("addi tp, %[gs], 0" : : [gs] "r" (addr)); +} +#else +#error "getTLSPointer()/setTLSPointer() unimplemented on this architecture" +#endif + +static inline int pthreadDescriptorTidOffset() { +#ifdef __x86_64__ + // Since glibc-2.11: + int offset = 720; // sizeof(tcbhead_t) + sizeof(list_t) +#elif defined(__aarch64__) + int offset = 208; +#elif defined(__riscv) + int offset = 208; +#endif + return offset; +} + +static inline pid_t patchThreadDescriptor(pthread_t pthreadSelf) { + int offset = pthreadDescriptorTidOffset(); + pid_t oldtid = *(pid_t *)((char *)pthreadSelf + offset); + // Since glibc.2.25, tid, but not pid, is stored in pthread_t. + // gettid() supported only in glibc-2.30; So, we use syscall(). + *(pid_t *)((char *)pthreadSelf + offset) = syscall(SYS_gettid); + return oldtid; +} + +static void saveThreadStateBeforeFork(struct threadinfo* threadInfo) { + threadInfo->origTid = syscall(SYS_gettid); + + // For verification only; not needed for functionality + pid_t oldTid = patchThreadDescriptor(pthread_self()); + if (oldTid != syscall(SYS_gettid)) { + fprintf(stderr, "PID %d: multithreaded_fork(): patchThreadDescriptor:" + "bad offset:\n Run: DMTCP:util/check-pthread-tid-offset.c\n", + getpid()); + libc_abort(); + } + threadInfo->pthread_descriptor = pthread_self(); + getTLSPointer(threadInfo); + + // FIXME: Add func fo get/set signals in child thread of child process. + // and restore thread sigmask sfter setcontext. + pthread_sigmask(SIG_BLOCK, NULL, &threadInfo->thread_sigmask); + sigset_t sigtest; + pthread_sigmask(SIG_BLOCK, NULL, &sigtest); + sigdelset(&sigtest, SIG_MULTITHREADED_FORK); + if (! sigisemptyset(&sigtest)) { + fprintf(stderr, "PID %d: multithreaded_fork() not yet implemented" + " for non-empty thread signaks\n", getpid()); + libc_abort(); + } +} + +static int child_setcontext_fast(void *arg) { + struct threadinfo* threadInfo = arg; + setTLSPointer(threadInfo); + patchThreadDescriptor(threadInfo->pthread_descriptor); + setcontext(&(threadInfo->context)); + return 0; // not reached +} + +void restart_child_threads_fast(void) { + int maxThreadIdx = atomic_load(&threadIdx); + for (int i = 0; i < maxThreadIdx; i++) { + if (i == 0) break; + // int clone(int (*fn)(void *), void *stack, int flags, void *arg, ... + // /* pid_t *parent_tid, void *tls, pid_t *child_tid */ ); + int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM + | CLONE_SIGHAND | CLONE_THREAD + | CLONE_SETTLS | CLONE_PARENT_SETTID + | CLONE_CHILD_CLEARTID + | 0); + // This is a temporary stack, to be replaced after setcontext. + // FIXME: This stack is a memory leak. We should free it later. + void *stack = malloc(0x10000) + 0x10000 - 128; // 64 KB + int offset = pthreadDescriptorTidOffset(); + pid_t *ctid = (pid_t*)((char*)threadInfos[i].pthread_descriptor + offset); + pid_t *ptid = ctid; + // For more insight, read 'man set_tid_address'. + clone(child_setcontext_fast, + stack, + clone_flags, + (void *)&threadInfos[i], ptid, threadInfos[i].fs, ctid); + } +} + +pid_t fast_multithreaded_fork(void) { + /********************************************************************* + *NOTE: We want to skip the normal fork cleanup + * of child threads (e.g., reclaim_stack()). + * So, we call an internal version of fork(). + * + *__libc__fork() calls: + * + *../sysdeps/unix/sysv/linux/arch-fork.h:26 + *__libc_fork() calls: + *_Fork() { + * pid_t pid = arch_fork (&THREAD_SELF->tid); + * ... robust mutex support ... + *} + * + *../sysdeps/unix/sysv/linux/arch-fork.h:34 + *statinc inline pid_t arch_fork(void *ctid) { + * echo 'flags = CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD' + * ret = INLINE_SYSCALL_CALL (clone, flags, 0, NULL, ctid, 0); + *} + *********************************************************************/ +#if 1 + pid_t _Fork(); + int childpid = _Fork(); +#else + // NOT YET FULLY DEVELOPED: + int flags = CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD; + int childpid; +// syscall(SYS_clone, ...); +// stack must be NULL +// https://stackoverflow.com/questions/2898579/clone-equivalent-of-fork +// But that says to use only SIGCHLD for flags, and glibc uses the above. +// But it's okay, since we're setting ctid and tls to NULL. +// FIXME: If we're going to set the last 3 args to NULL, who cares in what order they're found! +# ifdef __x86_64__ + long clone(unsigned long flags, void *stack, + int *parent_tid, int *child_tid, + unsigned long tls); +# elif defined(__aarch64__) + long clone(unsigned long flags, void *stack, + int *parent_tid, unsigned long tls, + int *child_tid); +# elif defined(__riscv) +# error Unimplemented CPU architecture +https://github.com/bminor/glibc/blob/master/sysdeps/unix/sysv/linux/riscv/clone.S +int clone(int (*fn)(void *arg), void *child_stack, int flags, void *arg, + void *parent_tidptr, void *tls, void *child_tidptr) */ + /* The syscall expects the args to be in different slots. */ + mv a0,a2 + mv a2,a4 + mv a3,a5 + mv a4,a6 +# endif +#endif + if (childpid == 0) { // child process + restart_child_threads_fast(); + } + return childpid; +} + +bool is_checkpoint_thread(void) { + return pthread_equal(pthread_self(), ckpt_pthread_descriptor); +} + +void thread_handle_after_dmtcp_restart(void) { + // IMPORTANT: There's a potential race between + // notifying the template thread and accessing + // the new current mode. We care about how + // we're supposed to react after restarting. The template + // thread, once signaled by all userspace threads, will + // change its mode to `TARGET_TEMPLATE_AFTER_RESTART`. + enum libmcmini_mode mode_on_entry = get_current_mode(); + const pid_t origPid = mcmini_real_pid(getpid()); + const int origThreadIdx = atomic_fetch_add(&threadIdx, 1); + struct threadinfo* threadInfo = &threadInfos[origThreadIdx]; + memset(threadInfo, 0x0, sizeof(struct threadinfo)); + + saveThreadStateBeforeFork(threadInfo); + int rc = getcontext(&threadInfo->context); + assert(rc == 0); + + if (mcmini_real_pid(getpid()) == origPid) { + // Inside the template process + notify_template_thread(); + } + else { + // Returned from `getcontext()` in the forked child + } + + switch (mode_on_entry) { + case DMTCP_RESTART_INTO_TEMPLATE: { + log_debug("Restarting into template (DMTCP_RESTART_INTO_TEMPLATE)\n"); + // For userspace threads in the template process, the threads + // must wait _forever_. Since these userspace threads will eventually + // become active and start listening to the model checker in the + // multithreaded fork process, we need to be able to control + // when these threads should begin listening to the model checker. + // This is accomplished by using the current "mode" of libmcmini: + // as long as we're not in the `TARGET_BRANCH_AFTER_RESTART` case, we + // simply ignore any wakeups + libpthread_mutex_lock(&template_thread_mut); + while (get_current_mode() != TARGET_BRANCH_AFTER_RESTART) { + libpthread_cond_wait(&template_thread_cond, &template_thread_mut); + } + libpthread_mutex_unlock(&template_thread_mut); + break; + } + case DMTCP_RESTART_INTO_BRANCH: { + // In the case of calling `dmtcp_restart` fpr each branch, we + // are expected to immediately talk to the model checker. + log_debug("Restarting into branch (DMTCP_RESTART_INTO_BRANCH)\n"); + break; + } + default: { + // If we're in any other mode other than restarting at this point, + // this is a bug. Note the potential race highlighted above that is + // mitigated with the local variable. + // + // However, this solution assumes that the template thread indeed waits + // until all userspace threads have notified it. Make sure that the logic + // for the template thread matches this. + libc_abort(); + } + } + + // Finally, we can communicate directly with the model checker `mcmini`. + thread_await_scheduler(); +} + +void mc_template_thread_loop_forever(void); +static void *template_thread(void *unused) { + // Phase 1. The template thread is created at record-time + // _prior_ to checkpoint. It must not intervene until exclusively + // after a `dmtcp_restart`. + // + // This is accomplished by waiting on a semaphore that is only incremented + // once the `DMTCP_EVENT_RESTART` is sent to `libmcmini.so`. + libpthread_sem_wait_loop(&template_thread_sem); + + // Phase 2. Wait for all userspace threads to move into a stable state + // + // After the `DMTCP_EVENT_RESTART` event is send to `libmcmini.so`, all + // userspace threads will resume execution. These userspace threads , + // + // TODO: Even in the event in which we don't need to transfer the records + // during the RECORD phase, we still wait for the userspace threads to go into + // the "stable" state. This is probably not needed. Indeed, for the + // `DMTCP_RESTART_INTO_BRANCH` case, waiting after each `dmtcp_restart` call + // to ensure a stable recorded state is pointless: we're not going to read it + // anyway! This is an only a potential optimization for later though. + + int thread_count = 0; + struct dirent *entry; + DIR *dp = opendir("/proc/self/task"); + if (dp == NULL) { + perror("opendir"); + mc_exit(EXIT_FAILURE); + } + + while ((entry = readdir(dp))) + if (strcmp(entry->d_name, ".") != 0 && strcmp(entry->d_name, "..") != 0) + thread_count++; + + // We don't want to count the template thread nor + // the checkpoint thread, but these will appear in + // `/proc/self/tasks` + thread_count -= 2; + closedir(dp); + log_debug( + "There are %d threads... waiting for them to get into a consistent " + "state...\n", + thread_count); + for (int i = 0; i < thread_count; i++) { + libpthread_sem_wait_loop(&dmtcp_restart_sem); + } + log_debug("The threads are now in a consistent state\n"); + + if (get_current_mode() == DMTCP_RESTART_INTO_BRANCH) { + atexit(&mc_exit_main_thread_in_child); + set_current_mode(TARGET_BRANCH_AFTER_RESTART); + } + + volatile struct mcmini_shm_file *shm_file = global_shm_start; + volatile struct template_process_t *tpt = &shm_file->tpt; + pid_t target_branch_pid = mcmini_real_pid(getpid()); + tpt->cpid = target_branch_pid; + libpthread_sem_post((sem_t *)&tpt->mcmini_process_sem); + + if (get_current_mode() == DMTCP_RESTART_INTO_TEMPLATE) { + log_debug("Restarting into template (DMTCP_RESTART_INTO_TEMPLATE)\n"); + mc_template_thread_loop_forever(); + + // Reaching this point means that we're in the branch: the + // parent process (aka the template) will never exit + // the above call to `mc_template_process_loop_forever()`. + set_current_mode(TARGET_BRANCH_AFTER_RESTART); + + // Recall that the userspace threads in the template process + // were idling/doing nothing. Indeed, those threads exist ONLY + // to ensure that `multithreaded_fork()` clones them. + // + // Now that we're finally in the branch, we can signal each + // of these userspace threads that checkpointing has begun. + libpthread_mutex_lock(&template_thread_mut); + libpthread_cond_broadcast(&template_thread_cond); + libpthread_mutex_unlock(&template_thread_mut); + } + + // Phase 3. Once in a stable state, check if `mcmini` needs to construct + // a model of what we've recorded. + if (getenv("MCMINI_NEEDS_STATE")) { + log_debug("The template thread is transferring state"); + + // FIXME: There appears to be an issue with opening the FIFO + // here. If it already exists most likely it should be replaced, + // but we seem to block on both sides (the `McMini` process side and here) + // or else exit with `No file or directory`. It's probably a race. + // + // The current work around is to simply remove the named FIFO manually + // and run a few exections until the race "resolves" itself (just hope that + // they don't block). + int rc = mkfifo("/tmp/mcmini-fifo", S_IRUSR | S_IWUSR); + if (rc != 0) { + perror("mkfifo"); + } + int fd = open("/tmp/mcmini-fifo", O_WRONLY); + if (fd == -1) { + perror("open"); + mc_exit(EXIT_FAILURE); + } + for (rec_list *entry = head_record_mode; entry != NULL; + entry = entry->next) { + if (entry->vo.type == MUTEX) { + log_verbose("Writing mutex entry %p (state %d)\n", entry->vo.location, + entry->vo.mut_state); + } else if (entry->vo.type == THREAD) { + log_verbose("Writing thread entry %p (id %d, status: %d)\n", + (void *)entry->vo.thrd_state.pthread_desc, + entry->vo.thrd_state.id, entry->vo.thrd_state.status); + } else if (entry->vo.type == CONDITION_VARIABLE) { + log_verbose("Writing condition variable entry %p (status %d) (count %d) (waiting_queue %p)\n", + entry->vo.location, entry->vo.cond_state.status, entry->vo.cond_state.waiting_threads->size, + entry->vo.cond_state.waiting_threads); + } else if (entry->vo.type == SEMAPHORE) { + log_verbose("Writing semaphore entry %p (count %d, status: %d)\n", + (void *)entry->vo.location, + entry->vo.sem_state.count, entry->vo.sem_state.status); + } else { + libc_abort(); + } + if (entry->vo.type == CONDITION_VARIABLE) { + // Here we are trying to modify the variables that are a part of union, + // so doing so is not ideal because it will change other varibale as memory is overlapped. + thread_queue_node* current = entry->vo.cond_state.waiting_threads->front; + while(current != NULL){ + visible_object waiting_queue_vo = { + .type = CV_WAITERS_QUEUE, + .location = current, + .waiting_queue_state.cv_location = entry->vo.location, + .waiting_queue_state.waiting_id = current->thread, + .waiting_queue_state.cv_state = current->thread_cv_state + }; + int sz = write(fd, &waiting_queue_vo, sizeof(visible_object)); + assert(sz == sizeof(visible_object)); + current = current->next; + } + int sz = write(fd, &entry->vo, sizeof(visible_object)); + assert(sz == sizeof(visible_object)); + } + else { + int sz = write(fd, &entry->vo, sizeof(visible_object)); + assert(sz == sizeof(visible_object)); + } + } + int sz = write(fd, &empty_visible_obj, sizeof(empty_visible_obj)); + assert(sz == sizeof(visible_object)); + fsync(fd); + fsync(0); + close(fd); + } + + // Exiting from the template thread is fine: + // once we're in the target branch, we no longer care + // about it anyways. + // + // NOTE: This is true for both repeated `dmtcp_restart` AND for multithreaded + // forking. + log_debug("The template thread has completed: exiting..."); + return NULL; +} +// static void SegvfaultHandler(int signum, siginfo_t *siginfo, void *context) { +// while(1); +// } +// static int AddSegvHandler() { +// struct sigaction act; +// static struct sigaction old_act; + +// act.sa_sigaction = &SegvfaultHandler; +// act.sa_flags = SA_RESTART | SA_SIGINFO; +// sigemptyset(&act.sa_mask); +// if (sigaction(SIGSEGV, &act, &old_act)) { +// perror("Failed to install segv handler"); +// return -1; +// } +// if (sigaction(SIGABRT, &act, &old_act)) { +// perror("Failed to install segv handler"); +// return -1; +// } +// return 0; +// } + +__attribute__((constructor)) void libmcmini_event_late_init() { + // AddSegvHandler(); + if (!dmtcp_is_enabled()) { + return; + } + + // Initialization should NOT happen in any of the + // `pthread*` wrapper functions, as they may be called + // in unexpected ways prior to the `DMTCP_EVENT_INIT`. + // + // For example, `libatomic.so` on aarch64 calls `pthread_mutex_lock` + // as part of its implementation (silly but so it is). This calls + // `libmcmini.so`'s `pthread_mutex_lock` since McMini comes first. + // If we called `libmcmini_init()` then, it's possible to end back + // up in DMTCP (via `dlopen(3)` which DMTCP intercepts) and subsequently + // call `pthread_mutex_lock` through `libatomic.so` _all by the same thread_. + // Since we use `pthread_once()`, this causes a deadlock. + // + // Hence, we initialization ONLY NOW, and there is not danger that the + // wrapper functions will unexpectedly recurse on themselves. + libmcmini_init(); + + // We also initialize the semaphore used by the wrapper functions + // AFTER DMTCP restart. This ensures that the semaphore is properly + // initialized at restart time. + libpthread_sem_init(&dmtcp_restart_sem, 0, 0); + + // We would prefer to create the template thread + // only during restart. However, the restart event + // is handled by the checkpoint thread. The DMTCP + // checkpoint thread does not expect to create an + // extra user thread as a child of the checkpoint + // thread. So we create the template thread here. + // + // The problem with `pthread_create` is that both + // DMTCP, libpthread.so, and libmcmini.so define it. + // We don't want to use `libmcmini's` `pthread_create`, + // because the template thread is a `mcmini` helper thread + // and not a user thread. We also don't want to use + // `libpthread.so` directly, or else DMTCP won't + // know about the thread. So we're left with DMTCP's + // `pthread_create`. + // + // However, we _cannot_ create the template thread + // upon receiving the `DMTCP_EVENT_INIT` because we need + // to ensure that any DMTCP resources used by DMTCP's own + // internal plugins are initialized before using the + // DMTCP resources. DMTCP's `pthread_create` essentially + // assumes that the DMTCP resources are already initialized. + pthread_t template_thread_id; + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + libpthread_sem_init(&template_thread_sem, 0, 0); + libdmtcp_pthread_create(&template_thread_id, &attr, &template_thread, NULL); + pthread_attr_destroy(&attr); +} + +static void presuspend_eventHook(DmtcpEvent_t event, DmtcpEventData_t *data) { + switch (event) { + case DMTCP_EVENT_INIT: { + // By default, `libmcmini_mode` is set to `PRE_DMTCP_INIT` + // to indicate that DMTCP has not yet sent the + // DMTCP_EVENT_INIT to `libmcmini.so`. This ensures that + // wrapper functions simply forward their calls to the + // next appropriate function in line. In most cases, this means + // calling the equivalent function in `libphread.so`. + // + // The checkpoint thread will be created immediately after + // DMTCP sends the `DMTCP_EVENT_INIT` but it has _not_ yet been + // created. This means that is it not safe to move into `RECORD` mode + // yet: we have to wait until the checkpoint thread has been created + // before moving into `RECORD` mode. + // + // Why? If we set + set_current_mode(PRE_CHECKPOINT_THREAD); + log_verbose("DMTCP_EVENT_INIT"); + break; + } + case DMTCP_EVENT_PRESUSPEND: + log_verbose("DMTCP_EVENT_PRESUSPEND"); + break; + case DMTCP_EVENT_PRECHECKPOINT: { + set_current_mode(PRE_CHECKPOINT); + log_verbose("DMTCP_EVENT_PRECHECKPOINT"); + break; + } + case DMTCP_EVENT_RESUME: + log_verbose("DMTCP_EVENT_RESUME"); + break; + case DMTCP_EVENT_RESTART: { + log_verbose("DMTCP_EVENT_RESTART callback"); + if (getenv("MCMINI_TEMPLATE_LOOP")) { + set_current_mode(DMTCP_RESTART_INTO_TEMPLATE); + log_debug("`MCMINI_TEMPLATE_LOOP` was set at restart-time\n"); + } else { + set_current_mode(DMTCP_RESTART_INTO_BRANCH); + log_debug("`MCMINI_TEMPLATE_LOOP` was not set at restart-time\n"); + } + + // During record mode, the shared memory + // used by the `mcmini` process to control + // the userspace threads in this process + // is not allocated. At restart time, + // the userspace threads may be in the middle + // of executing wrapper functions. Once they + // notice that the `DMTCP_EVENT_RESTART` event has + // been sent, they will want to access this + // region. Hence, we need to allocate it prior + // to returning from the checkpoint thread + // + // NOTE: `mcmini` ensures that the shared memory region + // is properly _initialized_, just as with classic + // model checking. + char shm_name[100]; + snprintf(shm_name, sizeof(shm_name), "/mcmini-%s-%lu", getenv("USER"), + (long)mcmini_real_pid(getppid())); + shm_name[sizeof(shm_name) - 1] = '\0'; + mc_allocate_shared_memory_region(shm_name); + + // A target program may install a signal handler for signals such + // as SIGSEGV. A common paradigm with such a signal handler is + // to block forever to allow a user to attach a debugger to + // the process for debugging. However, McMini relies on receiving + // the SIGCHLD signal from processes which contain a segfault. + // Therefore, on restart, we disable any handlers for SIGSEGV + struct sigaction action; + action.sa_handler = SIG_DFL; + sigemptyset(&action.sa_mask); + sigaction(SIGSEGV, &action, NULL); + + // Moreover, to ensure that the template thread is solely + // responsible for handling SIGCHLD, by default we block + // SIGCHLD. Blocking only in the checkpoint thread and unblocking + // in the template thread works because the man page for + //`pthread_sigmask(3)` reads: + // + // """ + // A new thread inherits a copy of its creator's signal mask. + // """ + sigset_t sigchld; + sigemptyset(&sigchld); + sigaddset(&sigchld, SIGCHLD); + pthread_sigmask(SIG_BLOCK, &sigchld, NULL); + + // NOTE: The template thread has been sleeping + // on `template_thread_sem` during the entire + // record phase. Only at restart time do we + // actually wake it up to take control of the + // other userspace threads to prepare for model checking. + libpthread_sem_post(&template_thread_sem); + break; + } + default: + break; + } +} + +DmtcpPluginDescriptor_t presuspend_plugin = { + DMTCP_PLUGIN_API_VERSION, DMTCP_PACKAGE_VERSION, + "McMini DMTCP Plugin", "McMini", + "dmtcp@ccs.neu.edu", "McMini deep debugging plugin", + presuspend_eventHook}; + +DMTCP_DECL_PLUGIN(presuspend_plugin); diff --git a/dmtcp/src/lib/entry.c b/dmtcp/src/lib/entry.c new file mode 100644 index 00000000..d51bf306 --- /dev/null +++ b/dmtcp/src/lib/entry.c @@ -0,0 +1,19 @@ +#include +#include + +#include "mcmini/mcmini.h" + +void mc_prevent_addr_randomization(void) { + if (personality(ADDR_NO_RANDOMIZE) == -1) { + perror("personality"); + mc_exit(EXIT_FAILURE); + } +} + +void mc_install_sig_handlers(void) { + // TODO: Install SIGCHLD handler to pass to the parent process. + // struct sigaction action; + // action.sa_handler = &; + // sigemptyset(&action.sa_mask); + // sigaction(SIGINT, &action, NULL); +} diff --git a/dmtcp/src/lib/interception.c b/dmtcp/src/lib/interception.c new file mode 100644 index 00000000..6d0b66f6 --- /dev/null +++ b/dmtcp/src/lib/interception.c @@ -0,0 +1,303 @@ +#define _GNU_SOURCE +#include "mcmini/spy/intercept/interception.h" + +#include +#include +#include +#include +#include +#include + +#include "mcmini/spy/intercept/wrappers.h" + +pthread_once_t libmcini_init = PTHREAD_ONCE_INIT; + +typeof(&pthread_create) libpthread_pthread_create_ptr; +typeof(&pthread_create) libdmtcp_pthread_create_ptr; +typeof(&pthread_join) libpthread_pthread_join_ptr; +typeof(&pthread_join) libdmtcp_pthread_join_ptr; +typeof(&pthread_mutex_init) pthread_mutex_init_ptr; +typeof(&pthread_mutex_lock) pthread_mutex_lock_ptr; +typeof(&pthread_mutex_trylock) pthread_mutex_trylock_ptr; +typeof(&pthread_mutex_timedlock) pthread_mutex_timedlock_ptr; +typeof(&pthread_mutex_unlock) pthread_mutex_unlock_ptr; +typeof(&pthread_mutex_destroy) pthread_mutex_destroy_ptr; +typeof(&sem_wait) sem_wait_ptr; +typeof(&sem_timedwait) sem_timedwait_ptr; +typeof(&sem_post) sem_post_ptr; +typeof(&sem_init) sem_init_ptr; +typeof(&sem_destroy) sem_destroy_ptr; +typeof(&pthread_cond_init) pthread_cond_init_ptr; +typeof(&pthread_cond_wait) pthread_cond_wait_ptr; +typeof(&pthread_cond_timedwait) pthread_cond_timedwait_ptr; +typeof(&pthread_cond_signal) pthread_cond_signal_ptr; +typeof(&pthread_cond_broadcast) pthread_cond_broadcast_ptr; +typeof(&pthread_cond_destroy) pthread_cond_destroy_ptr; +typeof(&sleep) sleep_ptr; +__attribute__((__noreturn__)) typeof(&exit) exit_ptr; +__attribute__((__noreturn__)) typeof(&abort) abort_ptr; +typeof(&fork) fork_ptr; + +void libmcmini_init(void) { + pthread_once(&libmcini_init, &mc_load_intercepted_pthread_functions); +} + +void mc_load_intercepted_pthread_functions(void) { + void *libpthread_handle = + dlopen("libpthread.so", RTLD_LAZY); + + if (!libpthread_handle) { + libpthread_handle = dlopen("libpthread.so.0", RTLD_LAZY); + } + + if (!libpthread_handle) { + fprintf(stderr, "dlopen(3) couldn't find `libpthread`: %s\n", dlerror()); + fflush(stderr); + libc_abort(); + } + + void *libc_handle = dlopen("libc.so", RTLD_LAZY); + if (!libc_handle) { + libc_handle = dlopen("libc.so.6", RTLD_LAZY); + } + + if (!libc_handle) { + fprintf(stderr, "dlopen(3) couldn't find `libc`: %s\n", dlerror()); + fflush(stderr); + libc_abort(); + } + + libpthread_pthread_create_ptr = dlsym(libpthread_handle, "pthread_create"); + libpthread_pthread_join_ptr = dlsym(libpthread_handle, "pthread_join"); + pthread_mutex_init_ptr = dlsym(libpthread_handle, "pthread_mutex_init"); + pthread_mutex_lock_ptr = dlsym(libpthread_handle, "pthread_mutex_lock"); + pthread_mutex_trylock_ptr = dlsym(libpthread_handle, "pthread_mutex_trylock"); + pthread_mutex_timedlock_ptr = dlsym(libpthread_handle, "pthread_mutex_timedlock"); + pthread_mutex_unlock_ptr = dlsym(libpthread_handle, "pthread_mutex_unlock"); + pthread_mutex_destroy_ptr = dlsym(libpthread_handle, "pthread_mutex_destroy"); + sem_timedwait_ptr = dlsym(libpthread_handle, "sem_timedwait"); + sem_wait_ptr = dlsym(libpthread_handle, "sem_wait"); + sem_timedwait_ptr = dlsym(libpthread_handle, "sem_timedwait"); + sem_post_ptr = dlsym(libpthread_handle, "sem_post"); + sem_init_ptr = dlsym(libpthread_handle, "sem_init"); + sem_destroy_ptr = dlsym(libpthread_handle, "sem_destroy"); + pthread_cond_init_ptr = dlsym(libpthread_handle, "pthread_cond_init"); + pthread_cond_wait_ptr = dlsym(libpthread_handle, "pthread_cond_wait"); + pthread_cond_timedwait_ptr = dlsym(libpthread_handle, "pthread_cond_timedwait"); + pthread_cond_signal_ptr = dlsym(libpthread_handle, "pthread_cond_signal"); + pthread_cond_broadcast_ptr = dlsym(libpthread_handle, "pthread_cond_broadcast"); + pthread_cond_destroy_ptr = dlsym(libpthread_handle, "pthread_cond_destroy"); + sleep_ptr = dlsym(libc_handle, "sleep"); + exit_ptr = dlsym(libc_handle, "exit"); + abort_ptr = dlsym(libc_handle, "abort"); + fork_ptr = dlsym(libc_handle, "fork"); + dlclose(libpthread_handle); + dlclose(libc_handle); + + if (dmtcp_is_enabled()) { + // TODO: This shouldn't refer to the absolute path to dmtcp.so + // We need to --> + void *libdmtcp_handle = dlopen("libdmtcp.so", RTLD_LAZY); + if (!libdmtcp_handle) { + fprintf(stderr, "dlopen(3) couldn't find `libdmtcp`: %s\n", dlerror()); + fflush(stderr); + libc_abort(); + } + libdmtcp_pthread_create_ptr = dlsym(libdmtcp_handle, "pthread_create"); + libdmtcp_pthread_join_ptr = dlsym(libdmtcp_handle, "pthread_join"); + dlclose(libdmtcp_handle); + } +} + +int pthread_mutex_init(pthread_mutex_t *mutex, + const pthread_mutexattr_t *mutexattr) { + return mc_pthread_mutex_init(mutex, mutexattr); +} + +int libpthread_mutex_init(pthread_mutex_t *mutex, + const pthread_mutexattr_t *attr) { + libmcmini_init(); + return (*pthread_mutex_init_ptr)(mutex, attr); +} + +int pthread_mutex_lock(pthread_mutex_t *mutex) { + return mc_pthread_mutex_lock(mutex); +} + +int libpthread_mutex_lock(pthread_mutex_t *mut) { + libmcmini_init(); + return (*pthread_mutex_lock_ptr)(mut); +} + +int libpthread_mutex_trylock(pthread_mutex_t *mut) { + libmcmini_init(); + return (*pthread_mutex_trylock_ptr)(mut); +} + +int libpthread_mutex_timedlock(pthread_mutex_t *mut, struct timespec *t) { + libmcmini_init(); + return (*pthread_mutex_timedlock_ptr)(mut, t); +} + +int pthread_mutex_unlock(pthread_mutex_t *mutex) { + return mc_pthread_mutex_unlock(mutex); +} + +int libpthread_mutex_unlock(pthread_mutex_t *mut) { + libmcmini_init(); + return (*pthread_mutex_unlock_ptr)(mut); +} + +int libpthread_mutex_destroy(pthread_mutex_t *mut) { + libmcmini_init(); + return (*pthread_mutex_destroy_ptr)(mut); +} + +int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr) { + return mc_pthread_cond_init(cond, attr); +} + +int libpthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr) { + libmcmini_init(); + return (*pthread_cond_init_ptr)(cond, attr); +} + +int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mut) { + return mc_pthread_cond_wait(cond, mut); +} + +int libpthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mut) { + libmcmini_init(); + return (*pthread_cond_wait_ptr)(cond, mut); +} + +int libpthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mut, + const struct timespec *abstime) { + libmcmini_init(); + return (*pthread_cond_timedwait_ptr)(cond, mut, abstime); +} + +int pthread_cond_signal(pthread_cond_t *cond) { + return mc_pthread_cond_signal(cond); +} + +int libpthread_cond_signal(pthread_cond_t *cond) { + libmcmini_init(); + return (*pthread_cond_signal_ptr)(cond); +} + +int pthread_cond_broadcast(pthread_cond_t *cond) { + return mc_pthread_cond_broadcast(cond); +} + +int libpthread_cond_broadcast(pthread_cond_t *cond) { + libmcmini_init(); + return (*pthread_cond_broadcast_ptr)(cond); +} + +int pthread_cond_destroy(pthread_cond_t *cond) { + return mc_pthread_cond_destroy(cond); +} + +int libpthread_cond_destroy(pthread_cond_t *cond) { + libmcmini_init(); + return (*pthread_cond_destroy_ptr)(cond); +} + +int pthread_create(pthread_t *thread, const pthread_attr_t *attr, + void *(*routine)(void *), void *arg) { + return mc_pthread_create(thread, attr, routine, arg); +} + +int libpthread_pthread_create(pthread_t *thread, const pthread_attr_t *attr, + void *(*routine)(void *), void *arg) { + libmcmini_init(); + return (*libpthread_pthread_create_ptr)(thread, attr, routine, arg); +} + +int libdmtcp_pthread_create(pthread_t *thread, const pthread_attr_t *attr, + void *(*routine)(void *), void *arg) { + libmcmini_init(); + return (*libdmtcp_pthread_create_ptr)(thread, attr, routine, arg); +} + +int pthread_join(pthread_t thread, void **rv) { + return mc_pthread_join(thread, rv); +} +int libpthread_pthread_join(pthread_t thread, void **rv) { + libmcmini_init(); + return (*libpthread_pthread_join_ptr)(thread, rv); +} +int libdmtcp_pthread_join(pthread_t thread, void **rv) { + libmcmini_init(); + return (*libdmtcp_pthread_join_ptr)(thread, rv); +} + +void exit(int status) { + mc_transparent_exit(status); +} + +void abort(void) { + mc_transparent_abort(); +} + +unsigned sleep(unsigned duration) { + return mc_sleep(duration); +} + +unsigned libc_sleep(unsigned duration) { + libmcmini_init(); + return (*sleep_ptr)(duration); +} + +// Forwarding methods to the underlying libraries +MCMINI_NO_RETURN void libc_abort(void) { + libmcmini_init(); + (*abort_ptr)(); +} +MCMINI_NO_RETURN void libc_exit(int status) { + libmcmini_init(); + (*exit_ptr)(status); +} +pid_t libc_fork(void) { + libmcmini_init(); + return (*fork_ptr)(); +} + +int sem_init(sem_t*sem, int p, unsigned count) { + return mc_sem_init(sem, p, count); +} +int sem_destroy(sem_t *sem) { return mc_sem_destroy(sem); } +int libpthread_sem_init(sem_t *sem, int pshared, int value) { + libmcmini_init(); + return (*sem_init_ptr)(sem, pshared, value); +} +int libpthread_sem_destroy(sem_t *sem) { + libmcmini_init(); + return (*sem_destroy_ptr)(sem); +} +int sem_post(sem_t* sem) { + return mc_sem_post(sem); +} +int libpthread_sem_post(sem_t *sem) { + libmcmini_init(); + return (*sem_post_ptr)(sem); +} +int sem_wait(sem_t *sem) { + return mc_sem_wait(sem); +} +int libpthread_sem_wait(sem_t *sem) { + libmcmini_init(); + return (*sem_wait_ptr)(sem); +} +int libpthread_sem_timedwait(sem_t *sem, struct timespec *ts) { + libmcmini_init(); + return (*sem_timedwait_ptr)(sem, ts); +} +int libpthread_sem_wait_loop(sem_t *sem) { + // retry on interruption + int rc = libpthread_sem_wait(sem); + while (rc == -1 && errno == EINTR) + rc = libpthread_sem_wait(sem); + return rc; +} diff --git a/dmtcp/src/lib/log.c b/dmtcp/src/lib/log.c new file mode 100644 index 00000000..3cc30140 --- /dev/null +++ b/dmtcp/src/lib/log.c @@ -0,0 +1,60 @@ +#include "mcmini/lib/log.h" +#include "mcmini/spy/intercept/interception.h" + +#include +#include +#include +#include "dmtcp.h" + +static int global_log_level = MCMINI_LOG_MINIMUM_LEVEL; +static const char *log_level_strs[] = { + "VERBOSE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL", "DISABLE" +}; + +typedef struct log_record { + int level; + int line; + const char *file; + const char *format; + struct tm *time; + va_list var_args; + FILE *target; +} log_record; + +void mcmini_log_set_level(int level) { + static pthread_mutex_t level_mut = PTHREAD_MUTEX_INITIALIZER; + libpthread_mutex_lock(&level_mut); + global_log_level = level; + libpthread_mutex_unlock(&level_mut); +} + +void mcmini_log_toggle(bool enable) { + mcmini_log_set_level(MCMINI_LOG_DISABLE); +} + +void mcmini_log(int level, const char *file, int line, const char *fmt, ...) { + if (level < global_log_level) { + return; + } + log_record rc; + time_t t = time(NULL); + rc.format = fmt; + rc.file = file; + rc.line = line; + rc.level = level; + rc.target = stdout; + rc.time = localtime(&t); + va_start(rc.var_args, fmt); + char buf[20]; + buf[strftime(buf, sizeof(buf), "%H:%M:%S", rc.time)] = '\0'; + const pid_t pid = mcmini_real_pid(getpid()); + fprintf( + rc.target, + "[%u] %s %-5s %s:%d: ", pid, + buf, log_level_strs[rc.level], rc.file, rc.line + ); + vfprintf(rc.target, rc.format, rc.var_args); + fprintf(rc.target, "\n"); + fflush(rc.target); + va_end(rc.var_args); +} diff --git a/dmtcp/src/lib/main.c b/dmtcp/src/lib/main.c new file mode 100644 index 00000000..3909c0c8 --- /dev/null +++ b/dmtcp/src/lib/main.c @@ -0,0 +1,126 @@ +#define _POSIX_C_SOURCE +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dmtcp.h" +#include "mcmini/mcmini.h" + +volatile void *global_shm_start = NULL; + +void mc_allocate_shared_memory_region(const char *shm_name) { + int fd = shm_open(shm_name, O_RDWR, S_IRUSR | S_IWUSR); + if (fd == -1) { + if (errno == EACCES) { + fprintf(stderr, "Shared memory region '%s' not owned by this process\n", + shm_name); + } else { + perror("shm_open"); + } + mc_exit(EXIT_FAILURE); + } + int rc = ftruncate(fd, shm_size); + if (rc == -1) { + perror("ftruncate"); + mc_exit(EXIT_FAILURE); + } + + void *gshms = mmap(NULL, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (gshms == MAP_FAILED) { + perror("mmap"); + mc_exit(EXIT_FAILURE); + } + fsync(fd); + close(fd); + global_shm_start = gshms; +} + +void mc_deallocate_shared_memory_region(void) { + char shm_file_name[100]; + mc_get_shm_handle_name(shm_file_name, sizeof(shm_file_name)); + if (global_shm_start) { + int rc = munmap((void *)global_shm_start, shm_size); + if (rc == -1) { + perror("munmap"); + mc_exit(EXIT_FAILURE); + } + } +} + +int dmtcp_mcmini_plugin_is_loaded(void) __attribute((weak)); +#define dmtcp_mcmini_plugin_is_loaded() \ + (dmtcp_mcmini_plugin_is_loaded ? dmtcp_mcmini_plugin_is_loaded() : 0) + +// static void SegvfaultHandler(int signum, siginfo_t *siginfo, void *context) { +// while(1); +// } +// static int AddSegvHandler() { +// struct sigaction act; +// static struct sigaction old_act; + +// act.sa_sigaction = &SegvfaultHandler; +// act.sa_flags = SA_RESTART | SA_SIGINFO; +// sigemptyset(&act.sa_mask); +// if (sigaction(SIGSEGV, &act, &old_act)) { +// perror("Failed to install segv handler"); +// return -1; +// } +// return 0; +// } + +__attribute__((constructor)) void libmcmini_main() { + // AddSegvHandler(); + // In recording mode, the constructor should be ignored and + // the DMTCP callback should instead be used to determine when + // `libmcmini.so` wrappers should begin recording. + + // TODD: Why doesn't this work as expected?? + // if (dmtcp_mcmini_plugin_is_loaded()) { + // // The libmcmini plugin of DMTCP has been loaded. + // // We must be in recording mode. Don't do model checking yet. + // return; + // } + + if (dmtcp_is_enabled()) { + // The libmcmini plugin of DMTCP has been loaded. + // We must be in recording mode. Don't do model checking yet. + return; + } + // ************************************************ + // STANDARD MODEL CHECKING (NO DEEP DEBUGGING) + // ************************************************ + mc_prevent_addr_randomization(); + mc_install_sig_handlers(); + mc_register_this_thread(); + + char shm_name[100]; + mc_get_shm_handle_name(shm_name, sizeof(shm_name)); + mc_allocate_shared_memory_region(shm_name); + atexit(&mc_deallocate_shared_memory_region); + + if (getenv("MCMINI_TEMPLATE_LOOP")) { + set_current_mode(TARGET_TEMPLATE); + + // In classic model checking, verification begins at program start. + // We can use traditional `fork(3)` instead of `multithreaded_fork()` + // because at program launch there's only a single thread. + mc_template_process_loop_forever(&fork); + + // Reaching this point means that we're in the branch: the + // parent process (aka the template) will never exit + // the above call to `mc_template_process_loop_forever()`. + } + set_current_mode(TARGET_BRANCH); + thread_await_scheduler(); +} diff --git a/dmtcp/src/lib/record.c b/dmtcp/src/lib/record.c new file mode 100644 index 00000000..2023f7cc --- /dev/null +++ b/dmtcp/src/lib/record.c @@ -0,0 +1,168 @@ +#include "mcmini/spy/checkpointing/record.h" +#include "mcmini/spy/checkpointing/rec_list.h" +#include "mcmini/spy/checkpointing/objects.h" +#include "mcmini/spy/checkpointing/transitions.h" +#include "mcmini/spy/intercept/interception.h" + +#include +#include +#include + +sem_t dmtcp_restart_sem; +pthread_mutex_t rec_list_lock = PTHREAD_MUTEX_INITIALIZER; +pthread_mutex_t pending_op_lock = PTHREAD_MUTEX_INITIALIZER; +volatile atomic_int libmcmini_mode = PRE_DMTCP_INIT; +visible_object empty_visible_obj = {.type = UNKNOWN, .location = NULL}; +rec_list *head_record_mode = NULL; +rec_list *current_record_mode = NULL; + +transition invisible_operation_for_this_thread(void) { + transition t = {.type = INVISIBLE_OPERATION_TYPE, .executor = pthread_self()}; + return t; +} + +rec_list *find_object(void *addr, rec_list *head) { + for (rec_list *node = head; node != NULL; node = node->next) { + if (node->vo.location == addr) return node; + } + return NULL; +} + +rec_list *find_thread_record_mode(pthread_t thrd) { + for (rec_list *node = head_record_mode; node != NULL; node = node->next) { + if (node->vo.type == THREAD && + pthread_equal(thrd, node->vo.thrd_state.pthread_desc)) + return node; + } + return NULL; +} + +rec_list *find_object_record_mode(void *addr) { + return find_object(addr, head_record_mode); +} + +rec_list *add_rec_entry(const visible_object *vo, rec_list **head, rec_list **current) { + rec_list *new_node = (rec_list *)malloc(sizeof(rec_list)); + if (new_node == NULL) { + perror("malloc"); + exit(EXIT_FAILURE); + } + new_node->vo = *vo; + if (*head == NULL) { + *head = new_node; + *current = new_node; + } + else { + (*current)->next = new_node; + *current = new_node; + } + return new_node; +} + +rec_list *add_rec_entry_record_mode(const visible_object *vo) { + rec_list *new_node = (rec_list *)malloc(sizeof(rec_list)); + if (new_node == NULL) { + perror("malloc"); + exit(EXIT_FAILURE); + } + new_node->vo = *vo; + new_node->next = NULL; + if (head_record_mode == NULL) { + head_record_mode = new_node; + current_record_mode = new_node; + } + else { + current_record_mode->next = new_node; + current_record_mode = new_node; + } + return new_node; +} + +//debugging puropses, will remove later +// void print_rec_list(const rec_list *head) { +// const rec_list *current = head; +// while (current != NULL) { +// const visible_object *vo = ¤t->vo; + +// printf("Record:\n"); +// printf(" Type: "); +// switch (vo->type) { +// case UNKNOWN: +// printf("UNKNOWN\n"); +// break; +// case MUTEX: +// printf("MUTEX\n"); +// printf(" Location: %p\n", vo->location); +// printf(" Mutex State: "); +// switch (vo->mut_state) { +// case UNINITIALIZED: printf("UNINITIALIZED\n"); break; +// case UNLOCKED: printf("UNLOCKED\n"); break; +// case LOCKED: printf("LOCKED\n"); break; +// case DESTROYED: printf("DESTROYED\n"); break; +// default: printf("UNKNOWN STATE\n"); break; +// } +// break; +// case SEMAPHORE: +// printf("SEMAPHORE\n"); +// printf(" Location: %p\n", vo->location); +// printf(" Count: %d\n", vo->sem_state.count); +// break; +// case CONDITION_VARIABLE: +// printf("CONDITION VARIABLE\n"); +// printf(" Location: %p\n", vo->location); +// printf(" Status: "); +// switch (vo->cond_state.status) { +// case CV_UNINITIALIZED: printf("UNINITIALIZED\n"); break; +// case CV_INITIALIZED: printf("INITIALIZED\n"); break; +// case CV_WAITING: printf("WAITING\n"); break; +// case CV_SIGNALED: printf("SIGNALED\n"); break; +// case CV_PREWAITING: printf("TRANSITIONAL\n"); break; +// default: printf("UNKNOWN STATUS\n"); break; +// } +// printf(" Waiting Thread: %p\n", (void *)vo->cond_state.interacting_thread); +// printf(" Associated Mutex: %p\n", (void *)vo->cond_state.associated_mutex); +// printf(" Waiting Count: %d\n", vo->cond_state.count); +// break; +// case THREAD: +// printf("THREAD\n"); +// printf(" Thread Descriptor: %p\n", (void *)vo->thrd_state.pthread_desc); +// printf(" Runner ID: %u\n", vo->thrd_state.id); +// printf(" Status: "); +// switch (vo->thrd_state.status) { +// case ALIVE: printf("ALIVE\n"); break; +// case EXITED: printf("EXITED\n"); break; +// default: printf("UNKNOWN STATUS\n"); break; +// } +// break; +// default: +// printf("INVALID TYPE\n"); +// break; +// } + +// current = current->next; +// } +// } + +void notify_template_thread() { libpthread_sem_post(&dmtcp_restart_sem); } + +bool is_in_restart_mode(void) { + enum libmcmini_mode mode = get_current_mode(); + return mode == DMTCP_RESTART_INTO_BRANCH || + mode == DMTCP_RESTART_INTO_TEMPLATE; +} + +enum libmcmini_mode get_current_mode() { + // INVARIANT (imposed by the code in `mc_pthread_create()`): + // The checkpoint thread is guaranteed to reach the line + // "is_checkpoint_thread()", because the only time in which the atomic is + // stored is BEFORE the checkpoint thread has executed DMTCP code. + if (atomic_load(&libmcmini_has_recorded_checkpoint_thread)) { + if (is_checkpoint_thread()) { + return CHECKPOINT_THREAD; + } + } + return atomic_load(&libmcmini_mode); +} +void set_current_mode(enum libmcmini_mode new_mode) { + atomic_store(&libmcmini_mode, new_mode); +} diff --git a/dmtcp/src/lib/sem-wrappers.c b/dmtcp/src/lib/sem-wrappers.c new file mode 100644 index 00000000..6ac27820 --- /dev/null +++ b/dmtcp/src/lib/sem-wrappers.c @@ -0,0 +1,250 @@ +#define _GNU_SOURCE +#include +#include +#include + +#include "mcmini/mcmini.h" + +int mc_sem_init(sem_t *sem, int p, unsigned count) { + // TODO: Does not handle interprocess semaphores + assert(p == 0); + log_debug("mc_sem_init"); + + switch (get_current_mode()) { + case PRE_DMTCP_INIT: + case PRE_CHECKPOINT_THREAD: + case CHECKPOINT_THREAD: { + return libpthread_sem_init(sem, p, count); + } + case RECORD: + case PRE_CHECKPOINT: { + libpthread_mutex_lock(&rec_list_lock); + rec_list *sem_record = find_object_record_mode(sem); + if (sem_record == NULL) { + visible_object vo = {.type = SEMAPHORE, + .location = sem, + .sem_state.status = SEM_UNINITIALIZED}; + sem_record = add_rec_entry_record_mode(&vo); + } + libpthread_mutex_unlock(&rec_list_lock); + + int rc = libpthread_sem_init(sem, p, count); + if (rc == 0) { + libpthread_mutex_lock(&rec_list_lock); + rec_list *sem_record = find_object_record_mode(sem); + assert(sem_record != NULL); + visible_object vo = {.type = SEMAPHORE, + .location = sem, + .sem_state.status = SEM_INITIALIZED, + .sem_state.count = count}; + sem_record->vo = vo; + libpthread_mutex_unlock(&rec_list_lock); + } + return rc; + } + case TARGET_BRANCH: + case TARGET_BRANCH_AFTER_RESTART: + case DMTCP_RESTART_INTO_BRANCH: + case DMTCP_RESTART_INTO_TEMPLATE: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = SEM_INIT_TYPE; + memcpy_v(mb->cnts, &sem, sizeof(sem)); + memcpy_v(mb->cnts + sizeof(sem), &count, sizeof(count)); + is_in_restart_mode() ? thread_handle_after_dmtcp_restart() : thread_wake_scheduler_and_wait(); + return libpthread_sem_init(sem, p, count); + } + default: { + // Wrapper functions should not be executing + // inside the template! If we reach this point, it + // means that this is a template process. This + // method must have been directly called + // erroneously. + fprintf(stderr, "mcmini internal error: %s:%d\n", __FILE__, __LINE__); + libc_abort(); + } + } +} + +int mc_sem_destroy(sem_t *sem) { + switch (get_current_mode()) { + case PRE_DMTCP_INIT: + case PRE_CHECKPOINT_THREAD: + case CHECKPOINT_THREAD: { + return libpthread_sem_destroy(sem); + } + case RECORD: + case PRE_CHECKPOINT: { + libpthread_mutex_lock(&rec_list_lock); + rec_list *sem_record = find_object_record_mode(sem); + if (sem_record == NULL) { + visible_object vo = {.type = SEMAPHORE, + .location = sem, + .sem_state.status = SEM_UNINITIALIZED}; + sem_record = add_rec_entry_record_mode(&vo); + } + libpthread_mutex_unlock(&rec_list_lock); + + int rc = libpthread_sem_destroy(sem); + if (rc == 0) { + libpthread_mutex_lock(&rec_list_lock); + rec_list *sem_record = find_object_record_mode(sem); + assert(sem_record != NULL); + visible_object vo = {.type = SEMAPHORE, + .location = sem, + .sem_state.status = SEM_DESTROYED}; + sem_record->vo = vo; + libpthread_mutex_unlock(&rec_list_lock); + } + return rc; + } + case TARGET_BRANCH: + case TARGET_BRANCH_AFTER_RESTART: + case DMTCP_RESTART_INTO_BRANCH: + case DMTCP_RESTART_INTO_TEMPLATE: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = SEM_DESTROY_TYPE; + memcpy_v(mb->cnts, &sem, sizeof(sem)); + is_in_restart_mode() ? thread_handle_after_dmtcp_restart() + : thread_wake_scheduler_and_wait(); + return libpthread_sem_destroy(sem); + } + default: { + // Wrapper functions should not be executing + // inside the template! If we reach this point, it + // means that this is a template process. This + // method must have been directly called + // erroneously. + fprintf(stderr, "mcmini internal error: %s:%d\n", __FILE__, __LINE__); + libc_abort(); + } + } +} + +int +mc_sem_post(sem_t *sem) { + switch (get_current_mode()) { + case PRE_DMTCP_INIT: + case PRE_CHECKPOINT_THREAD: + case CHECKPOINT_THREAD: { + return libpthread_sem_post(sem); + } + case RECORD: + case PRE_CHECKPOINT: { + libpthread_mutex_lock(&rec_list_lock); + rec_list *sem_record = find_object_record_mode(sem); + if (sem_record == NULL) { + // FIXME: We only change into record mode once the + // checkpoint thread has + int count = 0; + sem_getvalue(sem, &count); + visible_object vo = {.type = SEMAPHORE, + .location = sem, + .sem_state.status = SEM_INITIALIZED, + .sem_state.count = count}; + sem_record = add_rec_entry_record_mode(&vo); + } + libpthread_mutex_unlock(&rec_list_lock); + int rc = libpthread_sem_post(sem); + if (rc == 0) { // Post succeeded + libpthread_mutex_lock(&rec_list_lock); + sem_record->vo.sem_state.count++; + libpthread_mutex_unlock(&rec_list_lock); + } + return rc; + } + case TARGET_BRANCH: + case TARGET_BRANCH_AFTER_RESTART: + case DMTCP_RESTART_INTO_BRANCH: + case DMTCP_RESTART_INTO_TEMPLATE: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = SEM_POST_TYPE; + memcpy_v(mb->cnts, &sem, sizeof(sem)); + is_in_restart_mode() ? thread_handle_after_dmtcp_restart() : thread_wake_scheduler_and_wait(); + return libpthread_sem_post(sem); + } + default: { + // Wrapper functions should not be executing + // inside the template! If we reach this point, it + // means that this is a template process. This + // method must have been directly called + // erroneously. + fprintf(stderr, "mcmini internal error: %s:%d\n", __FILE__, __LINE__); + libc_abort(); + } + } +} + +int mc_sem_wait(sem_t *sem) { + switch (get_current_mode()) { + case PRE_DMTCP_INIT: + case PRE_CHECKPOINT_THREAD: + case CHECKPOINT_THREAD: { + return libpthread_sem_wait(sem); + } + case RECORD: + case PRE_CHECKPOINT: { + libpthread_mutex_lock(&rec_list_lock); + rec_list *sem_record = find_object_record_mode(sem); + if (sem_record == NULL) { + int count = 0; + sem_getvalue(sem, &count); + visible_object vo = {.type = SEMAPHORE, + .location = sem, + .sem_state.status = SEM_INITIALIZED, + .sem_state.count = count}; + sem_record = add_rec_entry_record_mode(&vo); + } + libpthread_mutex_unlock(&rec_list_lock); + + struct timespec ts; + while (1) { + // Do sem_timedwait for one second ... + clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec++; + int rc = libpthread_sem_timedwait(sem, &ts); + if (rc == 0) { // Wait succeeded + libpthread_mutex_lock(&rec_list_lock); + sem_record->vo.sem_state.count--; + libpthread_mutex_unlock(&rec_list_lock); + return rc; + } else if (rc == ETIMEDOUT) { // If the lock failed. + // Here, the user-space thread did not manage to wait on the + // the lock. However, we do NOT want threads to block during + // the recording phase to ensure that each user-space thread + // can be put back under the control of the model checker. + // + // For those threads which have not managed to enter, + // we want to ensure that they can eventually escape from this loop. + // After the DMTCP_EVENT_RESTART event, other threads must notice that + // the record phase has ended or else they could loop forever. + if (is_in_restart_mode()) { + break; + } + } else if (rc != 0 && rc != ETIMEDOUT) { + // A "true" error: something went wrong with locking + // and we pass this on to the end user + return rc; + } + } + // Explicit fallthrough + } + case TARGET_BRANCH: + case TARGET_BRANCH_AFTER_RESTART: + case DMTCP_RESTART_INTO_BRANCH: + case DMTCP_RESTART_INTO_TEMPLATE: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = SEM_WAIT_TYPE; + memcpy_v(mb->cnts, &sem, sizeof(sem)); + is_in_restart_mode() ? thread_handle_after_dmtcp_restart() : thread_wake_scheduler_and_wait(); + return libpthread_sem_post(sem); + } + default: { + // Wrapper functions should not be executing + // inside the template! If we reach this point, it + // means that this is a template process. This + // method must have been directly called + // erroneously. + fprintf(stderr, "mcmini internal error: %s:%d\n", __FILE__, __LINE__); + libc_abort(); + } + } +} diff --git a/dmtcp/src/lib/template/loop.c b/dmtcp/src/lib/template/loop.c new file mode 100644 index 00000000..de585a3e --- /dev/null +++ b/dmtcp/src/lib/template/loop.c @@ -0,0 +1,241 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mcmini/lib/sig.h" +#include "mcmini/common/exit.h" +#include "mcmini/common/shm_config.h" +#include "mcmini/defines.h" +#include "mcmini/lib/entry.h" +#include "mcmini/lib/log.h" +#include "mcmini/lib/template.h" +#include "mcmini/spy/checkpointing/record.h" +#include "mcmini/spy/intercept/interception.h" + +pid_t fast_multithreaded_fork(void); + +void mc_prepare_new_child_process(pid_t template_pid, pid_t model_checker_pid) { + // IMPORTANT: If the THREAD in the template process ever exits, this will + // prove problematic as it is when the THREAD which called `fork()` exits that + // the signal will be delivered to this process (and not when the process as a + // whole exits) + if (prctl(PR_SET_PDEATHSIG, SIGTERM) == -1) { + perror("prctl"); + libc_abort(); + } + + // HANDLE RACE CONDITION! It's possible that the parent exited BEFORE the call + // to `prctl()` above. To test if this is the case, we check whether the + // current parent process id differs from the process id of the parent which + // fork()-ed this process. If they don't match, we know that the parent exited + // before prctl() + if (mcmini_real_pid(getppid()) != template_pid) mc_exit(EXIT_FAILURE); + + // This is important to handle the case when the + // main thread exits `in main()`; in that case, we + // keep the process alive to allow the model checker to + // continue working + // + // NOTE: `atexit()`-handlers can be invoked when a dynamic + // library is unloaded. + atexit(&mc_exit_main_thread_in_child); + + // FIXME: Make sure to restore the old signal handler + // for SIGCHLD replaced in the template process in favor + // of any SIGCHLD handler previously installed. + // + // This is related to a future optimization in which the + // branch process directly signals the McMini process. + // The branch process would have to install a custom handler + // to handle the case where the branch process aborts + // unexpectedly. + struct sigaction action; + action.sa_handler = SIG_DFL; + sigemptyset(&action.sa_mask); + sigaction(SIGCHLD, &action, NULL); +} + +#define NO_DEFINED_MCMINI_PID (-1) +static sem_t sigchld_sem; +static volatile sig_atomic_t global_model_checker_pid = NO_DEFINED_MCMINI_PID; + +int rt_sigqueueinfo(pid_t tgid, int sig, siginfo_t *info) { + return syscall(SYS_rt_sigqueueinfo, tgid, sig, info); +} + +void mc_template_receive_sigchld(int sig, siginfo_t *info, void *) { + assert(global_model_checker_pid != NO_DEFINED_MCMINI_PID); + printf("signalling!!\n"); + fsync(STDOUT_FILENO); + int status; + bool signal_mcmini = false; + int rc = waitpid(-1, &status, 0); + if (rc == -1) { + // Error with waitpid. Signal McMini? + perror("waitpid"); + return; + } + if (WIFEXITED(status)) { + // int exit_code = WEXITSTATUS(status); + // printf("exited %d", status); + signal_mcmini = true; + } + else if (WIFSIGNALED(status)) { + int signo = WTERMSIG(status); + printf("signaled %d", status); + fsync(STDOUT_FILENO); + signal_mcmini = is_bad_signal(signo); + } + if (signal_mcmini) { + printf("signalling McMini!!\n"); + fsync(STDOUT_FILENO); + // TODO: We can use `sigqueue(3)` to pass the exit status of + // the child to the McMini process + // + // e.g. `sigqueue(global_model_checker_pid, SIG... ...)` + // + // Alternatively, the syscall `rt_sigqueueinfo(2)` can be used + // to deliver the `siginfo_t` directly. + // + // See https://man7.org/linux/man-pages/man2/rt_sigqueueinfo.2.html + // rt_sigqueueinfo(global_model_checker_pid, SIGCHLD, info); + kill(global_model_checker_pid, SIGCHLD); + } + libpthread_sem_post(&sigchld_sem); +} + +void mc_template_process_loop_forever(pid_t (*make_new_process)(void)) { + volatile struct mcmini_shm_file *shm_file = global_shm_start; + volatile struct template_process_t *tpt = &shm_file->tpt; + const pid_t model_checker_pid = getppid(); + const pid_t ppid_before_fork = getpid(); + global_model_checker_pid = model_checker_pid; + + libpthread_sem_init(&sigchld_sem, 0, 0); + + struct sigaction action = {0}; + sigemptyset(&action.sa_mask); + action.sa_flags = 0; + action.sa_sigaction = &mc_template_receive_sigchld; + action.sa_flags |= SA_NOCLDSTOP; + sigaction(SIGCHLD, &action, NULL); + + while (1) { + libpthread_sem_wait_loop((sem_t *)&tpt->libmcmini_sem); + const pid_t cpid = make_new_process(); + if (cpid == -1) { + // `fork()` failed + tpt->err = errno; + tpt->cpid = TEMPLATE_FORK_FAILED; + } else if (cpid == 0) { + // Child case: Simply return and escape into the child process + mc_prepare_new_child_process(ppid_before_fork, model_checker_pid); + return; + } + // `libmcmini.so` acting as a template process. + tpt->cpid = cpid; + libpthread_sem_post((sem_t *)&tpt->mcmini_process_sem); + + log_debug("Waiting for the child `%d` to exit... \n", cpid); + libpthread_sem_wait_loop(&sigchld_sem); + log_debug("The child exited! Circling back... %d\n", cpid); + } +} + +void mc_template_thread_loop_forever(void) { + bool has_transferred_state = false; + + volatile struct mcmini_shm_file *shm_file = global_shm_start; + volatile struct template_process_t *tpt = &shm_file->tpt; + const pid_t model_checker_pid = mcmini_real_pid(getppid()); + const pid_t template_pid = mcmini_real_pid(getpid()); + global_model_checker_pid = model_checker_pid; + + // Prior to creating new child processes, the template thread + // unblocks SIGCHLD and installs a signal handler. When the child + // process dies, the template delivers a SIGCHLD to the parent process + // in its place via `kill(2)` (which is async-signal-safe) + // + // See the Linux ma page for `sigaction(2)`: + // + // """ + // SA_NOCLDSTOP: + // + // If signum is SIGCHLD, do not receive notification when child processes stop + // (i.e., when they receive one of SIGSTOP, SIGTSTP, SIGTTIN, or SIGTTOU) + // or resume (i.e., they receive SIGCONT) (see wait(2)). This flag is + // meaningful only when establishing a handler for SIGCHLD. + // """ + // + // NOTE: We could add SIGUSR1 to the `sa_mask`. This would ensure that if + // the McMini process delivered a SIGUSR1 while the template were inside its + // SIGCHLD handler, the McMini process would still receive the (forwarded) + // SIGCHLD. It's unclear if this would be useful at the time of writing. + // + // NOTE: We need to wait until the `SIGCHLD` is delivered before we continue + // execution. Although we could also use `sigwait()`, the disadvantage with using + // `sigwait()` is that the SA_NOCLDSTP flag has no equivalent with `sigwait()`. + // Instead, we rely on the fact that `sem_post(3)` is async-signal-safe + // + // TODO: A future optimization could be to ignore the SIGCHLD altogether in + // the template process. + // + libpthread_sem_init(&sigchld_sem, 0, 0); + + struct sigaction action = {0}; + sigemptyset(&action.sa_mask); + action.sa_flags = 0; + action.sa_sigaction = &mc_template_receive_sigchld; + action.sa_flags |= SA_NOCLDSTOP; + sigaction(SIGCHLD, &action, NULL); + + sigset_t sigchld; + sigemptyset(&sigchld); + sigaddset(&sigchld, SIGCHLD); + pthread_sigmask(SIG_UNBLOCK, &sigchld, NULL); + + while (1) { + log_debug("Waiting for `mcmini` to signal a fork"); + libpthread_sem_wait((sem_t *)&tpt->libmcmini_sem); + log_debug("`mcmini` signaled a fork!"); + + const pid_t cpid = multithreaded_fork(); + + if (cpid == -1) { + // `multithreaded_fork()` failed + log_debug("The template process failed to create a new child%d\n"); + tpt->err = errno; + tpt->cpid = TEMPLATE_FORK_FAILED; + } else if (cpid == 0) { + // Child case: Simply return and escape into the child process. + mc_prepare_new_child_process(template_pid, model_checker_pid); + return; + } + else { + // Successful parent case + log_debug("The template process created child with pid %d\n", cpid); + tpt->cpid = cpid; + } + log_debug("Signaling `mcmini` about child status\n"); + libpthread_sem_post((sem_t *)&tpt->mcmini_process_sem); + + if (!has_transferred_state) { + has_transferred_state = true; + unsetenv("MCMINI_NEEDS_STATE"); + } + + log_debug("Waiting for the child `%d` to exit... \n", cpid); + libpthread_sem_wait_loop(&sigchld_sem); + log_debug("The child exited! Circling back... %d\n", cpid); + } +} diff --git a/dmtcp/src/lib/template/sig.c b/dmtcp/src/lib/template/sig.c new file mode 100644 index 00000000..351548f0 --- /dev/null +++ b/dmtcp/src/lib/template/sig.c @@ -0,0 +1,17 @@ +#include "mcmini/lib/sig.h" + +bool is_bad_signal(int signo) { + switch (signo) { + case SIGILL: + case SIGABRT: + case SIGBUS: + case SIGFPE: + case SIGKILL: + case SIGSEGV: + case SIGPIPE: + case SIGSYS: + return true; + default: + return false; + } +} diff --git a/dmtcp/src/lib/wrappers.c b/dmtcp/src/lib/wrappers.c new file mode 100644 index 00000000..a385c012 --- /dev/null +++ b/dmtcp/src/lib/wrappers.c @@ -0,0 +1,1198 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mcmini/common/exit.h" +#include "mcmini/Thread_queue.h" +#include "mcmini/mcmini.h" + +typedef struct pthread_map { + pthread_t thread; + runner_id_t value; + struct pthread_map *next; +} pthread_map_t; + +static pthread_rwlock_t pthread_map_lock = PTHREAD_RWLOCK_INITIALIZER; +static pthread_map_t *head = NULL; + +void insert_pthread_map(pthread_t t, runner_id_t v) { + pthread_rwlock_wrlock(&pthread_map_lock); + pthread_map_t *n = malloc(sizeof *n); + n->thread = t; + n->value = v; + n->next = head; + head = n; + pthread_rwlock_unlock(&pthread_map_lock); +} + +runner_id_t search_pthread_map(pthread_t t) { + pthread_rwlock_rdlock(&pthread_map_lock); + pthread_map_t *cur = head; + while (cur) { + if (pthread_equal(cur->thread, t)) { + return cur->value; + } + cur = cur->next; + } + pthread_rwlock_unlock(&pthread_map_lock); + return RID_INVALID; +} + + +MCMINI_THREAD_LOCAL runner_id_t tid_self = RID_INVALID; + +runner_id_t mc_register_this_thread(void) { + static pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER; + static runner_id_t tid_next = 0; + + libpthread_mutex_lock(&mut); + tid_self = tid_next++; + insert_pthread_map(pthread_self(), tid_self); + libpthread_mutex_unlock(&mut); + return tid_self; +} + +volatile runner_mailbox *thread_get_mailbox() { + assert(!is_checkpoint_thread()); + assert(tid_self != RID_INVALID); + assert(tid_self != RID_CHECKPOINT_THREAD); + return &((volatile struct mcmini_shm_file *)(global_shm_start)) + ->mailboxes[tid_self]; +} + +void thread_wake_scheduler_and_wait(void) { + log_verbose("thread_wake_scheduler_and_wait\n"); + fflush(stdout); + assert(tid_self != RID_INVALID); + volatile runner_mailbox *thread_mailbox = thread_get_mailbox(); + errno = 0; + int wake_rc = mc_wake_scheduler(thread_mailbox); + assert(wake_rc == 0); + assert(errno == 0); + + errno = 0; + int rc = mc_wait_for_scheduler(thread_mailbox); + while (rc != 0 && errno == EINTR) { + rc = mc_wait_for_scheduler(thread_mailbox); + } + assert(errno != EINVAL); +} + +void thread_await_scheduler(void) { + assert(tid_self != RID_INVALID); + volatile runner_mailbox *thread_mailbox = thread_get_mailbox(); + + errno = 0; + int rc = mc_wait_for_scheduler(thread_mailbox); + while (rc != 0 && errno == EINTR) { + rc = mc_wait_for_scheduler(thread_mailbox); + } + assert(errno != EINVAL); +} + +void thread_awake_scheduler_for_thread_finish_transition(void) { + assert(tid_self != RID_INVALID); + mc_wake_scheduler(thread_get_mailbox()); +} + +void thread_block_indefinitely(void) { + while (1) { + pause(); + } +} + +int mc_pthread_mutex_init(pthread_mutex_t *mutex, + const pthread_mutexattr_t *attr) { + // FIXME: Only handles NORMAL mutexes + if (attr != NULL) { + int type; + pthread_mutexattr_gettype(attr, &type); + assert(type == PTHREAD_MUTEX_NORMAL); + } + + switch (get_current_mode()) { + case PRE_DMTCP_INIT: + case PRE_CHECKPOINT_THREAD: + case CHECKPOINT_THREAD: { + return libpthread_mutex_init(mutex, attr); + } + case RECORD: + case PRE_CHECKPOINT: { + // NOTE: This is subtle: at this point, the possible modes are + // RECORD ***AND*** DMTCP_RESTART. The latter is possible if + // checkpointing occurs anywhere AFTER the switch statement above. + libpthread_mutex_lock(&rec_list_lock); + rec_list *mutex_record = find_object_record_mode(mutex); + if (mutex_record == NULL) { + // FIXME: We assume that this is a normal mutex. For other mutex + // types, we'd need to behave differently + visible_object vo = { + .type = MUTEX, .location = mutex, .mut_state = UNINITIALIZED}; + mutex_record = add_rec_entry_record_mode(&vo); + } + libpthread_mutex_unlock(&rec_list_lock); + + int rc = libpthread_mutex_init(mutex, attr); + if (rc == 0) { // Init + libpthread_mutex_lock(&rec_list_lock); + mutex_record->vo.mut_state = UNLOCKED; + libpthread_mutex_unlock(&rec_list_lock); + } + return rc; + } + case DMTCP_RESTART_INTO_BRANCH: + case DMTCP_RESTART_INTO_TEMPLATE: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = MUTEX_INIT_TYPE; + memcpy_v(mb->cnts, &mutex, sizeof(mutex)); + thread_handle_after_dmtcp_restart(); + return libpthread_mutex_init(mutex, attr); + } + case TARGET_BRANCH: + case TARGET_BRANCH_AFTER_RESTART: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = MUTEX_INIT_TYPE; + memcpy_v(mb->cnts, &mutex, sizeof(mutex)); + thread_wake_scheduler_and_wait(); + return libpthread_mutex_init(mutex, attr); + } + default: { + // Wrapper functions should not be executing + // inside the template! If we reach this point, it + // means that this is a template process. This + // method must have been directly called + // erroneously. + libc_abort(); + } + } +} + +int mc_pthread_mutex_lock(pthread_mutex_t *mutex) { + // On entry, there are several cases: + // + // 1. The thread was executing before dmtcp has had a chance + // to notify us of the DMTCP_EVENT_INIT. Here we forward + // the call to `libpthread.so` directly to avoid recording + // the system calls dmtcp makes in order to set up + // restart (i.e. prior to invoking our callback) + // + // 2. The thread was executing invisible operations during + // restart and has since reached the next visible operation. + // In this case, before shifting into model checking mode, we + // need to notify the template thread that the thread has + // completed all its recording. + // + // 3. The thread was executing a visible operation and has since + // reached its next visible operation as a consequence of + // control by the scheduler. Direct transfer to model checking + // mode suffices. + // + // 4. The thread is executing in RECORD mode and should do state + // tracking. + switch (get_current_mode()) { + case PRE_DMTCP_INIT: + case PRE_CHECKPOINT_THREAD: + case CHECKPOINT_THREAD: { + return libpthread_mutex_lock(mutex); + } + case RECORD: + case PRE_CHECKPOINT: { + // NOTE: This is subtle: at this point, the possible modes are + // RECORD ***AND*** DMTCP_RESTART. The latter is possible if + // checkpointing occurs anywhere AFTER the switch statement above. + libpthread_mutex_lock(&rec_list_lock); + rec_list *mutex_record = find_object_record_mode(mutex); + if (mutex_record == NULL) { + visible_object vo = { + .type = MUTEX, .location = mutex, .mut_state = UNINITIALIZED}; + mutex_record = add_rec_entry_record_mode(&vo); + } + libpthread_mutex_unlock(&rec_list_lock); + + struct timespec time = {.tv_sec = 2}; + while (1) { + int rc = libpthread_mutex_timedlock(mutex, &time); + if (rc == 0) { // Lock succeeded + libpthread_mutex_lock(&rec_list_lock); + mutex_record->vo.mut_state = LOCKED; + libpthread_mutex_unlock(&rec_list_lock); + return rc; + } else if (rc == ETIMEDOUT) { // If the lock failed. + // Here, the user-space thread did not manage to acquire + // the lock. However, we do NOT want threads to block during + // the recording phase to ensure that each user-space thread + // can be put back under the control of the model checker. + // + // For those threads which have not managed to acquire the lock, + // we want to ensure that they can eventually escape from this loop. + // After the DMTCP_EVENT_RESTART event, exactly one thread will + // successfully acquire the lock. Other threads must notice that + // the record phase has ended or else they could loop forever. + if (is_in_restart_mode()) { + break; + } + } else if (rc != 0 && rc != ETIMEDOUT) { + // A "true" error: something went wrong with locking + // and we pass this on to the end user + return rc; + } + } + // Explicit fallthrough + } + case DMTCP_RESTART_INTO_BRANCH: + case DMTCP_RESTART_INTO_TEMPLATE: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = MUTEX_LOCK_TYPE; + memcpy_v(mb->cnts, &mutex, sizeof(mutex)); + thread_handle_after_dmtcp_restart(); + return libpthread_mutex_lock(mutex); + } + case TARGET_BRANCH: + case TARGET_BRANCH_AFTER_RESTART: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = MUTEX_LOCK_TYPE; + memcpy_v(mb->cnts, &mutex, sizeof(mutex)); + thread_wake_scheduler_and_wait(); + return libpthread_mutex_lock(mutex); + } + default: { + // Wrapper functions should not be executing + // inside the template! If we reach this point, it + // means that this is a template process. This + // method must have been directly called + // erroneously. + libc_abort(); + } + } +} + +int mc_pthread_mutex_unlock(pthread_mutex_t *mutex) { + switch (get_current_mode()) { + case PRE_DMTCP_INIT: + case PRE_CHECKPOINT_THREAD: + case CHECKPOINT_THREAD: { + return libpthread_mutex_unlock(mutex); + } + case RECORD: + case PRE_CHECKPOINT: { + libpthread_mutex_lock(&rec_list_lock); + rec_list *mutex_record = find_object_record_mode(mutex); + if (mutex_record == NULL) { + // FIXME: We assume that this is a normal mutex. For other mutex + // types, we'd need to behave differently + fprintf(stderr, + "Undefined behavior: attempting to unlock an uninitialized " + "mutex %p", + mutex); + libc_abort(); + } + libpthread_mutex_unlock(&rec_list_lock); + int rc = libpthread_mutex_unlock(mutex); + if (rc == 0) { // Unlock succeeded + libpthread_mutex_lock(&rec_list_lock); + mutex_record->vo.mut_state = UNLOCKED; + libpthread_mutex_unlock(&rec_list_lock); + } + return rc; + } + case DMTCP_RESTART_INTO_BRANCH: + case DMTCP_RESTART_INTO_TEMPLATE: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = MUTEX_UNLOCK_TYPE; + memcpy_v(mb->cnts, &mutex, sizeof(mutex)); + thread_handle_after_dmtcp_restart(); + return libpthread_mutex_unlock(mutex); + } + case TARGET_BRANCH: + case TARGET_BRANCH_AFTER_RESTART: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = MUTEX_UNLOCK_TYPE; + memcpy_v(mb->cnts, &mutex, sizeof(mutex)); + thread_wake_scheduler_and_wait(); + return libpthread_mutex_unlock(mutex); + } + default: { + // Wrapper functions should not be executing + // inside the template! If we reach this point, it + // means that this is a template process. This + // method must have been directly called + // erroneously. + libc_abort(); + } + } +} + +void mc_exit_thread_in_child(void) { + thread_get_mailbox()->type = THREAD_EXIT_TYPE; + thread_wake_scheduler_and_wait(); + thread_awake_scheduler_for_thread_finish_transition(); + thread_block_indefinitely(); +} + +void mc_exit_main_thread_in_child(void) { + if (tid_self != RID_MAIN_THREAD) libc_abort(); + // IMPORTANT: This is NOT a typo! + // 1. `thread_wake_scheduler_and_wait()` is called when the + // main thread is known to be _alive_ to the model + // 2. `thread_awake_scheduler_for_thread_finish_transition()` + // is called to simulate the thread having "exited" + // 3. `thread_block_indefinitely()` ensures that the + // main thread never actually exits + // + // However, unlike a normal thread exiting, we want to ensure that + // the process doesn't terminate; hence, we prevent the main thread + // from ever escaping this function. + thread_get_mailbox()->type = THREAD_EXIT_TYPE; + thread_wake_scheduler_and_wait(); + + thread_get_mailbox()->type = THREAD_EXIT_TYPE; + thread_awake_scheduler_for_thread_finish_transition(); + thread_block_indefinitely(); +} + +MCMINI_NO_RETURN void mc_transparent_exit(int status) { + // No checkpoints within the true scope. Since we want to perform + // the true `exit(2)` call even in the case of recording, + // we need to ensure that a restarted process can't initiate + // a checkpoint in the call to `libc_exit()`; otherwise the + // checkpoint image would be "permanently" bad and always exit + // before we have a chance to restore it and explore the (short) + // branch leading to the exit. The same logic applies for `abort(2)` + + switch (get_current_mode()) { + case RECORD: + case PRE_DMTCP_INIT: + case PRE_CHECKPOINT_THREAD: + case PRE_CHECKPOINT: { + libc_exit(status); + } + case DMTCP_RESTART_INTO_BRANCH: + case DMTCP_RESTART_INTO_TEMPLATE: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = PROCESS_EXIT_TYPE; + memcpy_v(mb->cnts, &status, sizeof(status)); + thread_handle_after_dmtcp_restart(); + + // Fallthrough + } + case TARGET_BRANCH: + case TARGET_BRANCH_AFTER_RESTART: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = PROCESS_EXIT_TYPE; + memcpy_v(mb->cnts, &status, sizeof(status)); + thread_await_scheduler(); + + // After "exiting", don't actually exit yet: + // the model checker will prevent the process + // from continuing execution and will note this + // branch as "useless" since at this point + mb->type = PROCESS_EXIT_TYPE; + memcpy_v(mb->cnts, &status, sizeof(status)); + thread_await_scheduler(); + } + default: { + libc_exit(status); + } + } +} + +MCMINI_NO_RETURN void mc_transparent_abort(void) { + // No checkpoints within the true scope. Since we want to perform + // the true `abort(2)` call even in the case of recording, + // we need to ensure that a restarted process can't initiate + // a checkpoint in the call to `libc_exit()`; otherwise the + // checkpoint image would be "permanently" bad and always exit + // before we have a chance to restore it and explore the (short) + // branch leading to the exit. The same logic applies for `abort(2)` + // dmtcp_disable_ckpt(); + + switch (get_current_mode()) { + case RECORD: + case PRE_DMTCP_INIT: + case PRE_CHECKPOINT_THREAD: + case PRE_CHECKPOINT: { + libc_abort(); + } + case DMTCP_RESTART_INTO_BRANCH: + case DMTCP_RESTART_INTO_TEMPLATE: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = PROCESS_ABORT_TYPE; + thread_handle_after_dmtcp_restart(); + + // Explicit fallthrough + } + case TARGET_BRANCH: + case TARGET_BRANCH_AFTER_RESTART: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = PROCESS_ABORT_TYPE; + thread_wake_scheduler_and_wait(); + + // After "aborting", don't actually abort yet: + // the model checker will prevent the process + // from continuing execution and will note this + // branch as "useless" since at this point + mb->type = PROCESS_ABORT_TYPE; + thread_wake_scheduler_and_wait(); + libc_abort(); + } + default: { + libc_abort(); + } + } +} + +struct mc_thread_routine_arg { + void *arg; + thread_routine routine; + sem_t mc_pthread_create_binary_sem; +}; + +void *mc_thread_routine_wrapper(void *arg) { + runner_id_t rid = mc_register_this_thread(); + struct mc_thread_routine_arg *unwrapped_arg = arg; + switch (get_current_mode()) { + case PRE_DMTCP_INIT: + case PRE_CHECKPOINT_THREAD: + case CHECKPOINT_THREAD: { + fprintf(stderr, + "In `PRE_DMTCP_INIT` mode, `mc_pthread_create` always directly calls DMTCP." + "Reaching this point would be an error.\n"); + libc_abort(); + } + case RECORD: + case PRE_CHECKPOINT: + case DMTCP_RESTART_INTO_BRANCH: + case DMTCP_RESTART_INTO_TEMPLATE: { + // If we've noticed we're executing after a `DMTCP_EVENT_RESTART`, we + // simply let the thread continue executing until one of two things + // happens: + // + // 1. The thread routine itself calls another wrapper function (i.e. + // visible operation) + // 2. The thread routine exits before calling another wrapper function. + // + // The same applies to RECORD mode: we simply do the recording and let the + // thread routine call the next wrapper function. + pthread_t this_thread = pthread_self(); + libpthread_mutex_lock(&rec_list_lock); + rec_list *thread_record = find_thread_record_mode(this_thread); + assert(thread_record == NULL); + visible_object vo = {.type = THREAD, + .location = NULL, + .thrd_state.pthread_desc = this_thread, + .thrd_state.status = ALIVE, + .thrd_state.id = rid}; + thread_record = add_rec_entry_record_mode(&vo); + libpthread_mutex_unlock(&rec_list_lock); + libpthread_sem_post(&unwrapped_arg->mc_pthread_create_binary_sem); + break; + } + case TARGET_BRANCH: + case TARGET_BRANCH_AFTER_RESTART: { + libpthread_sem_post(&unwrapped_arg->mc_pthread_create_binary_sem); + // Simulates THREAD_START for this thread NOTE: + // We don't need to write into shared memory here. The + // scheduler already knows how to handle the case + // of thread creation + thread_await_scheduler(); + break; + } + default: { + libc_abort(); + } + } + void *rv = unwrapped_arg->routine(unwrapped_arg->arg); + free(arg); + + switch (get_current_mode()) { + case RECORD: + case PRE_CHECKPOINT: { + libpthread_mutex_lock(&rec_list_lock); + rec_list *thread_record = find_thread_record_mode(pthread_self()); + assert(thread_record != NULL); + thread_record->vo.thrd_state.status = EXITED; + fprintf(stdout, "\n\nexited\n\n"); fflush(stdout); + libpthread_mutex_unlock(&rec_list_lock); + return rv; + } + case DMTCP_RESTART_INTO_BRANCH: + case DMTCP_RESTART_INTO_TEMPLATE: { + thread_get_mailbox()->type = THREAD_EXIT_TYPE; + thread_handle_after_dmtcp_restart(); + + thread_get_mailbox()->type = THREAD_EXIT_TYPE; + thread_await_scheduler(); + break; + } + case TARGET_BRANCH: + case TARGET_BRANCH_AFTER_RESTART: { + mc_exit_thread_in_child(); + break; + } + default: { + libc_abort(); + } + } + return rv; +} + +void record_main_thread(void) { + pthread_t main_thread = pthread_self(); + runner_id_t main_tid = mc_register_this_thread(); + assert(main_tid == 0); + libpthread_mutex_lock(&rec_list_lock); + rec_list *thread_record = find_thread_record_mode(main_thread); + assert(thread_record == NULL); + visible_object vo = {.type = THREAD, + .location = NULL, + .thrd_state.id = main_tid, + .thrd_state.pthread_desc = main_thread, + .thrd_state.status = ALIVE}; + thread_record = add_rec_entry_record_mode(&vo); + libpthread_mutex_unlock(&rec_list_lock); +} + +void record_checkpoint_thread(void) { + tid_self = RID_CHECKPOINT_THREAD; + ckpt_pthread_descriptor = pthread_self(); + atomic_store(&libmcmini_has_recorded_checkpoint_thread, true); + + // Recording the presence of the checkpoint thread means that the main + // thread has made its first call to `pthread_create`. The assumption + // is that DMTCP (executing in the main thread before the `main` routine) + // makes the first call to `pthread_create` to create the checkpoint thread. + // Since the checkpoint thread is about to be created, it is safe to begin + // recording. + set_current_mode(RECORD); +} + +void *dmtcp_create_checkpoint_thread_wrapper(void *arg) { + record_checkpoint_thread(); + struct mc_thread_routine_arg *unwrapped_arg = arg; + libpthread_sem_post(&unwrapped_arg->mc_pthread_create_binary_sem); + void *rv = unwrapped_arg->routine(unwrapped_arg->arg); + free(arg); + return rv; +} + +int mc_pthread_create(pthread_t *thread, const pthread_attr_t *attr, + void *(*routine)(void *), void *arg) { + // NOTE: We're assuming that DMTCP creates only the checkpoint thread + // immediately after sending the `DMTCP_EVENT_INIT` to `libmcmini.so` + // and creates no other threads during execution + static pthread_once_t main_thread_once = PTHREAD_ONCE_INIT; + + // TODO: Reduce code duplication here! + switch (get_current_mode()) { + case PRE_DMTCP_INIT: { + // This case implies that DMTCP attempted to create + // a thread BEFORE the `DMTCP_EVENT_INIT` was delivered + // to `libmcmini`'s callback. This + // NOTE: Explicit fallthrough intended + assert(0); + } + case PRE_CHECKPOINT_THREAD: { + pthread_once(&main_thread_once, &record_main_thread); + + // We must be able to at runtime determine which thread is the checkpoint + // thread. Here we record the `pthread_t` struct assigned to the + // checkpoint thread and later compare it with the value returned by + // `pthread_create()`. + // + // NOTE: The semaphore is necessary here as the child thread in this + // instance will be the checkpoint thread and will hence call into DMTCP. + // Since the goal of detecting if the caller is the checkpoint thread + // inside of wrappers is to prevent the checkpoint thread from interacting + // with McMini wrappers, and since we write the checkpoint thread's + // `pthread_t` into a globally accessible location, we must synchronize + // with the checkpoint thread. + struct mc_thread_routine_arg *wrapped_arg = + malloc(sizeof(struct mc_thread_routine_arg)); + wrapped_arg->arg = arg; + wrapped_arg->routine = routine; + libpthread_sem_init(&wrapped_arg->mc_pthread_create_binary_sem, 0, 0); + int rc = libdmtcp_pthread_create( + thread, attr, &dmtcp_create_checkpoint_thread_wrapper, wrapped_arg); + libpthread_sem_wait(&wrapped_arg->mc_pthread_create_binary_sem); + return rc; + } + case CHECKPOINT_THREAD: { + log_warn( + "The checkpoint thread is creating another thread. Calls from this " + "thread should probably be ignored by the McMini library, as " + "creating a thread indicates the checkpoint thread is doing " + "background work. The DMTCP library and McMini must not interefere. " + "However, the current implementation " + "only prevents calls by the checkpoint thread from entering wrapper " + "functions."); + return libdmtcp_pthread_create(thread, attr, routine, arg); + } + case RECORD: + case PRE_CHECKPOINT: + case DMTCP_RESTART_INTO_BRANCH: + case DMTCP_RESTART_INTO_TEMPLATE: { + // TODO: add support for thread attributes + struct mc_thread_routine_arg *libmcmini_controlled_thread_arg = + malloc(sizeof(struct mc_thread_routine_arg)); + libmcmini_controlled_thread_arg->arg = arg; + libmcmini_controlled_thread_arg->routine = routine; + // TODO: Handle the errors that can occur when + // pthread_create is called. They are unlikely to + // occur in practice, but should be handled + libpthread_sem_init( + &libmcmini_controlled_thread_arg->mc_pthread_create_binary_sem, 0, + 0); + const int rc = + libdmtcp_pthread_create(thread, attr, &mc_thread_routine_wrapper, + libmcmini_controlled_thread_arg); + // IMPORTANT: We need to ensure that the child thread is recorded + // before exiting; otherwise there are potential race conditions + // in the record code (e.g. with `pthread_join()`) which expect the + // child thread to have been recorded when it may not have been yet. + libpthread_sem_wait( + &libmcmini_controlled_thread_arg->mc_pthread_create_binary_sem); + return rc; + } + case TARGET_BRANCH: + case TARGET_BRANCH_AFTER_RESTART: { + // TODO: add support for thread attributes + struct mc_thread_routine_arg *libmcmini_controlled_thread_arg = + malloc(sizeof(struct mc_thread_routine_arg)); + libmcmini_controlled_thread_arg->arg = arg; + libmcmini_controlled_thread_arg->routine = routine; + libpthread_sem_init( + &libmcmini_controlled_thread_arg->mc_pthread_create_binary_sem, 0, + 0); + + // NOTE: This code is reached when the model checker has control. + // Since this process represents an(emphemeral) branch of state space, + // unless we later want to checkpoint this branch, we don't need to + // inform DMTCP of these new threads. Indeed, in classic model checking + // mode, `libdmtcp.so` is not even loaded (so we'd have to check first anyway). + // Calling `libpthread_pthread_create` simplifies all this. + const int rv = + libpthread_pthread_create(thread, attr, &mc_thread_routine_wrapper, + libmcmini_controlled_thread_arg); + + // IMPORTANT: We need to ensure that the thread that is + // created has been assigned an id; otherwise, there is a race condition + // in which two thread creates in the child might + // not be scheduled to run until *two* steps of the scheduler + libpthread_sem_wait( + &libmcmini_controlled_thread_arg->mc_pthread_create_binary_sem); + + memcpy_v(thread_get_mailbox()->cnts, thread, sizeof(pthread_t)); + thread_get_mailbox()->type = THREAD_CREATE_TYPE; + thread_wake_scheduler_and_wait(); + return rv; + } + default: { + libc_abort(); + } + } +} + +int mc_pthread_join(pthread_t t, void **rv) { + switch (get_current_mode()) { + case PRE_DMTCP_INIT: { + // This case implies that DMTCP attempted to join + // a thread BEFORE the `DMTCP_EVENT_INIT` was delivered + // to `libmcmini`'s callback + // NOTE: Explicit fallthrough intended + assert(0); + } + case PRE_CHECKPOINT_THREAD: + case CHECKPOINT_THREAD: { + return libdmtcp_pthread_join(t, rv); + } + case RECORD: + case PRE_CHECKPOINT: { + // NOTE: This is subtle: at this point, the possible modes are + // RECORD ***AND*** DMTCP_RESTART. The latter is possible if + // checkpointing occurs anywhere AFTER the switch statement above. + libpthread_mutex_lock(&rec_list_lock); + rec_list *thread_record = find_thread_record_mode(t); + assert(thread_record != NULL); + libpthread_mutex_unlock(&rec_list_lock); + + struct timespec time = {.tv_sec = 2, .tv_nsec = 0}; + while (1) { + int rc = pthread_timedjoin_np(t, rv, &time); + if (rc == 0) { // Join succeeded + libpthread_mutex_lock(&rec_list_lock); + thread_record->vo.thrd_state.status = EXITED; + libpthread_mutex_unlock(&rec_list_lock); + return rc; + } else if (rc == ETIMEDOUT) { + // If the join failed. + // Here, the user-space thread did not manage to join on + // the thread. However, we do NOT want threads to block during + // the recording phase to ensure that each user-space thread + // can be put back under the control of the model checker. + // + // For those threads which have not managed to join on the thread, + // we want to ensure that they can eventually escape from this loop. + // After the DMTCP_EVENT_RESTART event, exactly one thread will + // successfully acquire the lock. Other threads must notice that + // the record phase has ended or else they would loop forever. + if (is_in_restart_mode()) { + break; + } + } else if (rc != 0 && rc != ETIMEDOUT) { + // A "true" error: something went wrong with locking + // and we pass this on to the end user + return rc; + } + } + } + // Explicit fallthrough here + case DMTCP_RESTART_INTO_BRANCH: + case DMTCP_RESTART_INTO_TEMPLATE: { + runner_id_t rid = search_pthread_map(t); + memcpy_v(thread_get_mailbox()->cnts, &rid, sizeof(runner_id_t)); + thread_get_mailbox()->type = THREAD_JOIN_TYPE; + thread_handle_after_dmtcp_restart(); + return 0; + } + case TARGET_BRANCH: + case TARGET_BRANCH_AFTER_RESTART: { + runner_id_t rid = search_pthread_map(t); + memcpy_v(thread_get_mailbox()->cnts, &rid, sizeof(runner_id_t)); + thread_get_mailbox()->type = THREAD_JOIN_TYPE; + thread_wake_scheduler_and_wait(); + return 0; + } + default: { + libc_abort(); + } + } + +} + +unsigned mc_sleep(unsigned duration) { + switch (get_current_mode()) { + case TARGET_BRANCH: + case DMTCP_RESTART_INTO_BRANCH: + case DMTCP_RESTART_INTO_TEMPLATE: + case TARGET_BRANCH_AFTER_RESTART: { + // Ignore actually putting this thread to sleep: + // it doesn't affect correctness neither for model + // checking nor for state regenetation. + return 0; + } + default: + return libc_sleep(duration); + } +} + +int mc_pthread_cond_init(pthread_cond_t *cond, + const pthread_condattr_t *attr) { + switch (get_current_mode()) { + case PRE_DMTCP_INIT: + case PRE_CHECKPOINT_THREAD: { + return libpthread_cond_init(cond, attr); + } + case RECORD: + case PRE_CHECKPOINT: { + libpthread_mutex_lock(&rec_list_lock); + rec_list *cond_record = find_object_record_mode(cond); + if (cond_record == NULL) { + //Initialize the condition variable + visible_object vo = { + .type = CONDITION_VARIABLE, .location = cond, + .cond_state = { .status = CV_UNINITIALIZED, .interacting_thread = 0, .associated_mutex = NULL, + .count = 0 } + }; + cond_record = add_rec_entry_record_mode(&vo); + } + libpthread_mutex_unlock(&rec_list_lock); + + int rc = libpthread_cond_init(cond, attr); + if (rc == 0) { + libpthread_mutex_lock(&rec_list_lock); + cond_record->vo.cond_state.status = CV_INITIALIZED; + cond_record->vo.cond_state.interacting_thread = 0; + //we typically don't know which mutex will be associated with the + //condition variable until a thread actually waits on it. + cond_record->vo.cond_state.associated_mutex = NULL; + cond_record->vo.cond_state.waiting_threads = create_thread_queue(); + cond_record->vo.cond_state.count = 0; + libpthread_mutex_unlock(&rec_list_lock); + } + return rc; + } + case DMTCP_RESTART_INTO_BRANCH: + case DMTCP_RESTART_INTO_TEMPLATE: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = COND_INIT_TYPE; + memcpy_v(mb->cnts, &cond, sizeof(cond)); + // notify_template_thread(); + // thread_await_scheduler(); + thread_handle_after_dmtcp_restart(); + return libpthread_cond_init(cond, attr); + } + case TARGET_BRANCH: + case TARGET_BRANCH_AFTER_RESTART: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = COND_INIT_TYPE; + memcpy_v(mb->cnts, &cond, sizeof(cond)); + thread_wake_scheduler_and_wait(); + return libpthread_cond_init(cond, attr); + } + default: { + libc_abort(); + } + } +} + +int mc_pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) { + switch (get_current_mode()){ + case PRE_DMTCP_INIT: + case PRE_CHECKPOINT_THREAD: { + return libpthread_cond_wait(cond, mutex); + } + case RECORD: + case PRE_CHECKPOINT: { + pthread_t this_thread = pthread_self(); + libpthread_mutex_lock(&rec_list_lock); + rec_list *thrd_record = find_thread_record_mode(this_thread); + rec_list *cond_record = find_object_record_mode(cond); + runner_id_t tmp = thrd_record->vo.thrd_state.id; + if (cond_record == NULL) { + //Initialize the condition variable + visible_object vo = { + .type = CONDITION_VARIABLE, .location = cond, .cond_state = { .status= CV_INITIALIZED, .interacting_thread = tmp, + .associated_mutex = mutex, .count = 0, .waiting_threads = create_thread_queue() } + }; + cond_record = add_rec_entry_record_mode(&vo); + } + // The thread will enter in the outer waiting room first. Here its state will be + // CV_PREWAITING. It is done to avoid race condition that might occur due to checkpointing + // between releasing the mutex and actually getting into wait state. + cond_record->vo.cond_state.interacting_thread = tmp; + //check if thread is not already in the waiting room + if (!is_in_thread_queue(cond_record->vo.cond_state.waiting_threads, tmp)) { + //add the thread to the waiting room + enqueue_thread(cond_record->vo.cond_state.waiting_threads,tmp,CV_PREWAITING); + } + cond_record->vo.cond_state.associated_mutex = mutex; + cond_record->vo.cond_state.count++; + libpthread_mutex_unlock(&rec_list_lock); + + struct timespec wait_time = {.tv_sec = 2, .tv_nsec = 0}; + int rc; + while (1) { + rc = libpthread_cond_timedwait(cond, mutex, &wait_time); + if (rc == 0) { + // The thread has successfully entered the waiting state. + libpthread_mutex_lock(&rec_list_lock); + thrd_record = find_thread_record_mode(pthread_self()); + + //Check if this thread was signaled (CV_SIGNALED state) + condition_variable_status cv_state = get_thread_cv_state(cond_record->vo.cond_state.waiting_threads, thrd_record->vo.thrd_state.id); + + if (cv_state == CV_SIGNALED) { + // Remove this thread from the queue + remove_thread_from_queue(cond_record->vo.cond_state.waiting_threads, thrd_record->vo.thrd_state.id); + cond_record->vo.cond_state.count--; + } + else { + update_thread_cv_state(cond_record->vo.cond_state.waiting_threads,thrd_record->vo.thrd_state.id,CV_WAITING); + } + libpthread_mutex_unlock(&rec_list_lock); + return rc; + } + else if (rc == ETIMEDOUT) { + // Timeout case: The thread did not manage to enter the wait state + // within the given time frame. In a regular run, this could simply + // mean the condition was not signaled, but here in model checking + // mode, it serves a critical purpose: + // + // During recording, we do NOT want threads to block indefinitely. + // Each thread must complete its current visible operation and yield + // control back to the model checker. Therefore, any thread that times + // out here (failing to enter the wait state) will check if the model + // checker requires it to retry. The timeout gives us a way to ensure + // that threads are not permanently stuck in transition due to an + // unforeseen checkpoint. + // + // After a DMTCP_EVENT_RESTART event, exactly one thread will ultimately + // succeed in fully acquiring the condition and transitioning to the + // wait state. Other threads should detect this change and eventually + // escape from this loop to avoid unnecessary blocking. + + // The purpose of CV_PREWAITING state and Inner/Outer Waiting Room: + // + // By setting the state to CV_PREWAITING, we create a two-part "waiting room" mechanism: + // - The "outer waiting room" corresponds to the transition period right after the thread + // has released the mutex but has not fully entered the wait state. + // - Once the thread enters the full wait state (pthread_cond_wait), it moves into the "inner + // waiting room," and its state is set to CV_WAITING. This distinction is critical for + // checkpoint safety: + // - If a checkpoint occurs while the thread is still in the CV_PREWAITING (outer waiting room), + // we know it has not fully transitioned to a waiting state and can handle it accordingly. + // - If the thread is in CV_WAITING (inner waiting room), we know it has entered a stable wait + // state, ensuring the mutex-conditional interaction is checkpoint-safe. + if (is_in_restart_mode()) { + break; + } + } else if (rc != 0 && rc != ETIMEDOUT) { + // A "true" error: something went wrong with locking + // and we pass this on to the end user + return rc; + } + } + } + case DMTCP_RESTART_INTO_BRANCH: + case DMTCP_RESTART_INTO_TEMPLATE:{ + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = COND_ENQUEUE_TYPE; + memcpy_v(mb->cnts, &cond, sizeof(cond)); + memcpy_v(mb->cnts + sizeof(cond), &mutex, sizeof(mutex)); + thread_handle_after_dmtcp_restart(); + libpthread_mutex_unlock(mutex); + mb->type = COND_WAIT_TYPE; + memcpy_v(mb->cnts, &cond, sizeof(cond)); + memcpy_v(mb->cnts + sizeof(cond), &mutex, sizeof(mutex)); + thread_handle_after_dmtcp_restart(); + libpthread_mutex_lock(mutex); + return 0; + } + case TARGET_BRANCH: + case TARGET_BRANCH_AFTER_RESTART: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = COND_ENQUEUE_TYPE; + memcpy_v(mb->cnts, &cond, sizeof(cond)); + memcpy_v(mb->cnts + sizeof(cond), &mutex, sizeof(mutex)); + thread_wake_scheduler_and_wait(); + libpthread_mutex_unlock(mutex); + mb->type = COND_WAIT_TYPE; + memcpy_v(mb->cnts, &cond, sizeof(cond)); + memcpy_v(mb->cnts + sizeof(cond), &mutex, sizeof(mutex)); + thread_wake_scheduler_and_wait(); + libpthread_mutex_lock(mutex); + return 0; + } + default: { + // Wrapper functions should not be executing + // inside the template! If we reach this point, it + // means that this is a template process. This + // method must have been directly called + // erroneously. + libc_abort(); + } + } +} + +int mc_pthread_cond_signal(pthread_cond_t *cond) { + switch (get_current_mode()) { + case PRE_DMTCP_INIT: + case PRE_CHECKPOINT_THREAD: { + return libpthread_cond_signal(cond); + } + case RECORD: + case PRE_CHECKPOINT: { + libpthread_mutex_lock(&rec_list_lock); + rec_list *cond_record = find_object_record_mode(cond); + if (cond_record == NULL) { + fprintf(stderr, + "Undefined behavior: attempting to signal an uninitialized" + "condition variable %p", + cond); + libc_abort(); + } + // Store pre-signal waiting count (only count CV_WAITING threads) + int cv_waiting_count = 0; + thread_queue_node* current = cond_record->vo.cond_state.waiting_threads->front; + while (current != NULL) { + if (current->thread_cv_state == CV_WAITING) { + cv_waiting_count++; + } + current = current->next; + } + cond_record->vo.cond_state.prev_waiting_count = cv_waiting_count; + libpthread_mutex_unlock(&rec_list_lock); + + int rc = libpthread_cond_signal(cond); + if (rc == 0) { + libpthread_mutex_lock(&rec_list_lock); + runner_id_t waiting_thread = get_waiting_thread_node(cond_record->vo.cond_state.waiting_threads); + if (!is_queue_empty(cond_record->vo.cond_state.waiting_threads)) { + // Find first thread in CV_WAITING state + if (waiting_thread != RID_INVALID) { + update_thread_cv_state(cond_record->vo.cond_state.waiting_threads, waiting_thread, CV_SIGNALED); + } + } + // After signaling, check if any thread was marked as signaled + bool any_thread_signaled = false; + current = cond_record->vo.cond_state.waiting_threads->front; + while (current != NULL) { + if (current->thread_cv_state == CV_SIGNALED) { + any_thread_signaled = true; + break; + } + current = current->next; + } + + // If no thread was signaled and there were waiting threads, it's a lost wakeup + if (!any_thread_signaled && cond_record->vo.cond_state.prev_waiting_count > 0) { + cond_record->vo.cond_state.lost_wakeups++; + fprintf(stderr, "WARNING: Lost wakeup detected on condition variable %p\n", cond); + } + + libpthread_mutex_unlock(&rec_list_lock); + } + return rc; + } + case DMTCP_RESTART_INTO_BRANCH: + case DMTCP_RESTART_INTO_TEMPLATE: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = COND_SIGNAL_TYPE; + memcpy_v(mb->cnts, &cond, sizeof(cond)); + // notify_template_thread(); + // thread_await_scheduler(); + thread_handle_after_dmtcp_restart(); + return libpthread_cond_signal(cond); + } + case TARGET_BRANCH: + case TARGET_BRANCH_AFTER_RESTART: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = COND_SIGNAL_TYPE; + memcpy_v(mb->cnts, &cond, sizeof(cond)); + thread_wake_scheduler_and_wait(); + return libpthread_cond_signal(cond); + } + default: { + // Wrapper functions should not be executing + // inside the template! If we reach this point, it + // means that this is a template process. This + // method must have been directly called + // erroneously. + libc_abort(); + } + } +} + +int mc_pthread_cond_broadcast(pthread_cond_t *cond) { + switch (get_current_mode()) { + case PRE_DMTCP_INIT: + case PRE_CHECKPOINT_THREAD: { + return libpthread_cond_broadcast(cond); + } + case RECORD: + case PRE_CHECKPOINT: { + libpthread_mutex_lock(&rec_list_lock); + rec_list *cond_record = find_object_record_mode(cond); + if (cond_record == NULL) { + fprintf(stderr, + "Undefined behavior: attempting to broadcast an uninitialized" + "condition variable %p", + cond); + libc_abort(); + } + libpthread_mutex_unlock(&rec_list_lock); + int rc = libpthread_cond_broadcast(cond); + if (rc == 0) { + libpthread_mutex_lock(&rec_list_lock); + // Mark all waiting threads as signaled + thread_queue_node* current = cond_record->vo.cond_state.waiting_threads->front; + while (current != NULL) { + // Only mark CV_WAITING threads as signaled (not transitional) + if (current->thread_cv_state == CV_WAITING) { + update_thread_cv_state(cond_record->vo.cond_state.waiting_threads, + current->thread, CV_SIGNALED); + } + current = current->next; + } + libpthread_mutex_unlock(&rec_list_lock); + } + return rc; + } + case DMTCP_RESTART_INTO_BRANCH: + case DMTCP_RESTART_INTO_TEMPLATE: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = COND_BROADCAST_TYPE; + memcpy_v(mb->cnts, &cond, sizeof(cond)); + thread_handle_after_dmtcp_restart(); + return libpthread_cond_broadcast(cond); + } + case TARGET_BRANCH: + case TARGET_BRANCH_AFTER_RESTART: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = COND_BROADCAST_TYPE; + memcpy_v(mb->cnts, &cond, sizeof(cond)); + thread_wake_scheduler_and_wait(); + return libpthread_cond_broadcast(cond); + } + default: { + libc_abort(); + } + } +} + +int mc_pthread_cond_destroy(pthread_cond_t *cond) { + switch (get_current_mode()) { + case PRE_DMTCP_INIT: + case PRE_CHECKPOINT_THREAD: { + return libpthread_cond_destroy(cond); + } + case RECORD: + case PRE_CHECKPOINT: { + libpthread_mutex_lock(&rec_list_lock); + rec_list *cond_record = find_object_record_mode(cond); + if (cond_record == NULL) { + fprintf(stderr, + "Undefined behavior: attempting to destroy an uninitialized" + "condition variable %p", + cond); + libc_abort(); + } + + // Check if any thread is waiting on this condition variable + if (!is_queue_empty(cond_record->vo.cond_state.waiting_threads)) { + fprintf(stderr, + "Undefined behavior: attempting to destroy a condition variable" + " that has threads waiting on it %p", + cond); + libc_abort(); + } + + libpthread_mutex_unlock(&rec_list_lock); + int rc = libpthread_cond_destroy(cond); + if (rc == 0) { + libpthread_mutex_lock(&rec_list_lock); + cond_record->vo.cond_state.status = CV_DESTROYED; + libpthread_mutex_unlock(&rec_list_lock); + } + return rc; + } + case DMTCP_RESTART_INTO_BRANCH: + case DMTCP_RESTART_INTO_TEMPLATE: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = COND_DESTROY_TYPE; + memcpy_v(mb->cnts, &cond, sizeof(cond)); + thread_handle_after_dmtcp_restart(); + return libpthread_cond_destroy(cond); + } + case TARGET_BRANCH: + case TARGET_BRANCH_AFTER_RESTART: { + volatile runner_mailbox *mb = thread_get_mailbox(); + mb->type = COND_DESTROY_TYPE; + memcpy_v(mb->cnts, &cond, sizeof(cond)); + thread_wake_scheduler_and_wait(); + return libpthread_cond_destroy(cond); + } + default: { + libc_abort(); + } + } +} diff --git a/dmtcp/src/mcmini/Thread_queue.c b/dmtcp/src/mcmini/Thread_queue.c new file mode 100644 index 00000000..778d37f2 --- /dev/null +++ b/dmtcp/src/mcmini/Thread_queue.c @@ -0,0 +1,184 @@ +#include "mcmini/Thread_queue.h" + +#include +#include + +thread_queue* create_thread_queue() { + thread_queue *queue = malloc(sizeof(thread_queue)); + if (!queue) { + perror("Failed to allocate memory for thread queue"); + exit(EXIT_FAILURE); + } + queue->front = queue->rear = NULL; + return queue; +} + +thread_queue_node* create_thread_queue_node() { + thread_queue_node *node = malloc(sizeof(thread_queue_node)); + if (!node) { + perror("Failed to allocate memory for thread queue node"); + exit(EXIT_FAILURE); + } + node->next = NULL; + return node; +} + +void enqueue_thread(thread_queue *queue, runner_id_t thread, condition_variable_status cv_state) { + thread_queue_node *node = create_thread_queue_node(); + node->thread = thread; + node->thread_cv_state = cv_state; + if (queue->rear == NULL) { + queue->front = queue->rear = node; + } else { + queue->rear->next = node; + queue->rear = node; + } + queue->size++; +} + +runner_id_t dequeue_thread(thread_queue *queue) { + if (queue->front == NULL) { + fprintf(stderr, "Error: Attempt to dequeue from an empty queue\n"); + exit(EXIT_FAILURE); + } + while(queue->front != NULL){ + if (queue->front->thread_cv_state == CV_WAITING) { + thread_queue_node *temp = queue->front; + pthread_t thread = temp->thread; + + queue->front = queue->front->next; + if (!queue->front) { + queue->rear = NULL; + } + + free(temp); + queue->size--; + return thread; + } + queue->front = queue->front->next; // Move front pointer to the next node + } + // If we reach here, it means there are no threads in the queue with CV_WAITING state + return -1; +} + +runner_id_t peek_thread(thread_queue *queue) { + if (queue->front == NULL) { + fprintf(stderr, "Error: Attempt to peek at an empty queue\n"); + exit(EXIT_FAILURE); + } + return queue->front->thread; +} + +bool is_in_thread_queue(thread_queue *queue, runner_id_t thread) { + thread_queue_node *current = queue->front; + while (current != NULL) { + if (current->thread == thread) { + return true; + } + current = current->next; + } + return false; +} + +bool is_queue_empty(thread_queue *queue) { + return queue->front == NULL; +} + + // Added for debugging purpose: + void print_thread_queue(const thread_queue *queue) { + if (queue->front == NULL) { + fprintf(stdout,"Thread queue is empty.\n");fflush(stdout); + return; + } + + fprintf(stdout,"Thread queue (size: %d): ", queue->size);fflush(stdout); + thread_queue_node *current = queue->front; + while (current) { + fprintf(stdout,"%ld (%d) -> ", (long)current->thread,current->thread_cv_state);fflush(stdout); + current = current->next; + } + fprintf(stdout,"NULL\n");fflush(stdout); +} + +condition_variable_status get_thread_cv_state(thread_queue *queue, runner_id_t thread) { + thread_queue_node *current = queue->front; + while (current != NULL) { + if (current->thread == thread) { + return current->thread_cv_state; + } + current = current->next; + } + fprintf(stderr, "Error get_thread_cv: Thread %d not found in the queue\n", (int)thread); + exit(EXIT_FAILURE); +} + +void update_thread_cv_state(thread_queue *queue, runner_id_t thread, condition_variable_status new_state) { + thread_queue_node *current = queue->front; + while (current != NULL) { + if (current->thread == thread) { + current->thread_cv_state = new_state; + return; // Successfully updated + } + current = current->next; + } + fprintf(stderr, "Error update_thread_cv_state: Thread %d not found in the queue\n", (int)thread); + exit(EXIT_FAILURE); +} + +runner_id_t get_waiting_thread_node(thread_queue *queue) { + thread_queue_node *current = queue->front; + while (current != NULL) { + // Return the first node in the queue + // This can be used to get the first waiting thread + if (current->thread_cv_state == CV_WAITING) { + return current->thread; // Found a waiting thread + } + current = current->next; + } + // If no waiting thread found, return NULL + return RID_INVALID; // No waiting thread found +} + +int remove_thread_from_queue(thread_queue* queue, runner_id_t tid) { + if (queue == NULL || queue->front == NULL) { + return -1; // Empty queue or invalid queue + } + + // Special case: removing the first node + if (queue->front->thread == tid) { + thread_queue_node* to_remove = queue->front; + queue->front = queue->front->next; + + // If we removed the last node, update rear pointer + if (queue->front == NULL) { + queue->rear = NULL; + } + + free(to_remove); + queue->size--; + return 0; + } + + // Search through the list for the node to remove + thread_queue_node* current = queue->front; + while (current->next != NULL) { + if (current->next->thread == tid) { + thread_queue_node* to_remove = current->next; + + // Update the next pointer to skip the node being removed + current->next = to_remove->next; + + // If we're removing the last node, update rear pointer + if (to_remove == queue->rear) { + queue->rear = current; + } + + free(to_remove); + queue->size--; + return 0; + } + current = current->next; + } + + return -1; // Thread ID not found in queue +} \ No newline at end of file diff --git a/dmtcp/src/mcmini/constants.cpp b/dmtcp/src/mcmini/constants.cpp new file mode 100644 index 00000000..0c553971 --- /dev/null +++ b/dmtcp/src/mcmini/constants.cpp @@ -0,0 +1,21 @@ +#include "mcmini/constants.hpp" + +#include +#include + +pid_t constants::getpid() { + static std::once_flag once_flag; + static pid_t pid = -1; + std::call_once(once_flag, []() { pid = ::getpid(); }); + return pid; +} + +size_t constants::subsystem_log_size() { + // static std::once_flag once_flag; + // static uint32_t sz = -1; + // std::call_once(once_flag, + // []() { sz = std::getenv("MCMINI_SUBSYS_LOG_CHARS") ? ; }); + // return pid; + return 5; +} +size_t constants::file_log_size() { return 20; } diff --git a/dmtcp/src/mcmini/coordinator/coordinator.cpp b/dmtcp/src/mcmini/coordinator/coordinator.cpp new file mode 100644 index 00000000..2b8146d3 --- /dev/null +++ b/dmtcp/src/mcmini/coordinator/coordinator.cpp @@ -0,0 +1,185 @@ +#include "mcmini/coordinator/coordinator.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mcmini/coordinator/model_to_system_map.hpp" +#include "mcmini/model/program.hpp" +#include "mcmini/model/state.hpp" +#include "mcmini/model/transition.hpp" +#include "mcmini/model/transition_registry.hpp" +#include "mcmini/model/visible_object_state.hpp" +#include "mcmini/real_world/mailbox/runner_mailbox.h" +#include "mcmini/real_world/process.hpp" +#include "mcmini/real_world/process_source.hpp" +#include "mcmini/real_world/remote_address.hpp" + +using namespace real_world; + +coordinator::coordinator( + model::program &&initial_state, + model::transition_registry runtime_transition_mapping, + std::unique_ptr &&process_source) + : current_program_model(std::move(initial_state)), + runtime_transition_mapping(std::move(runtime_transition_mapping)), + process_source(std::move(process_source)) { + this->assign_new_process_handle(); +} + +void coordinator::execute_runner(process::runner_id_t runner_id) { + if (!current_process_handle) { + throw real_world::process::execution_error( + "Failed to execute runner with id \"" + std::to_string(runner_id) + + "\": the process is not alive"); + } + log_debug(logger) << "Scheduling `" << runner_id << "` for execution." + << logging::severity_level::verbose + << "The next transition is `" + << this->current_program_model + .get_pending_transition_for(runner_id) + ->debug_string() + << "`"; + volatile runner_mailbox *mb = + this->current_process_handle->execute_runner(runner_id); + model::transition_registry::runtime_type_id const rttid = mb->type; + + model::transition_registry::transition_discovery_callback callback_function = + runtime_transition_mapping.get_callback_for(rttid); + log_debug(logger) << "Grabbing callback function for rttid `" << rttid << "`"; + if (!callback_function) { + throw real_world::process::execution_error( + "Execution resulted in a runner scheduled to execute the transition " + "type with the RTTID '" + + std::to_string(rttid) + + "' but this identifier has not been registered before model checking " + "began. Double check that the coordinator was properly configured " + "before launch; otherwise, please report this as a bug in " + "libmcmini.so with this message."); + } + model_to_system_map remote_address_mapping = model_to_system_map(*this); + model::transition *pending_operation = + callback_function(runner_id, *mb, remote_address_mapping); + if (!pending_operation) { + throw real_world::process::execution_error( + "Failed to translate the data written into the mailbox of runner " + + std::to_string(runner_id)); + } + log_debug(logger) << "The next pending operation is `" + << pending_operation->debug_string() << "`"; + this->current_program_model.model_execution_of(runner_id, pending_operation); + + log_verbose(logger) + << "\n\n**************** AFTER MODEL EXEC *********************\n\n\n" + << current_program_model.get_trace().back()->debug_string() + << " just executed\n" + << pending_operation->debug_string() << " will execute next " + << "\n\n" + << this->current_program_model.get_state_sequence().back().debug_string() + << "\n\n**************** AFTER MODEL EXEC *********************\n\n"; +} + +void coordinator::return_to_depth(uint32_t n) { + this->assign_new_process_handle(); + this->current_program_model.restore_model_at_depth(n); + + // Now regenerate the process from scratch. The new process handle has a state + // represented in the model as the state at depth `n = 0` in the state + // sequence; hence to ensure the model and the real world correspond, we must + // re-execute the threads in the order specified in the transition sequence. + assert(this->current_program_model.get_trace().count() == n); + for (const model::transition *t : this->current_program_model.get_trace()) { + this->current_process_handle->execute_runner(t->get_executor()); + } + log_verbose(logger) + << "**************** AFTER RESTORATION *********************\n\n" + << get_current_program_model() + << "\n\n**************** AFTER RESTORATION *********************"; +} + +model::state::runner_id_t model_to_system_map::get_model_of_runner( + remote_address handle) const { + model::state::objid_t objid = get_model_of_object(handle); + if (objid != model::invalid_objid) { + return _coordinator.get_current_program_model() + .get_state_sequence() + .get_runner_id_for_obj(objid); + } + return model::invalid_rid; +} + +model::state::objid_t model_to_system_map::get_model_of_object( + remote_address handle) const { + if (_coordinator.system_address_mapping.count(handle) > 0) { + return _coordinator.system_address_mapping[handle]; + } + return model::invalid_objid; +} + +model::state::objid_t model_to_system_map::observe_object( + real_world::remote_address rp_vobj_handle, + const model::visible_object_state *vobs) { + if (contains(rp_vobj_handle)) { + throw std::runtime_error( + "Attempting to rebind a remote address to an object that already " + "exists in the model. Did you check that the object doesn't already " + "exist?"); + } + model::state::objid_t const new_objid = + _coordinator.current_program_model.get_current_state().add_object(vobs); + _coordinator.system_address_mapping.insert({rp_vobj_handle, new_objid}); + return new_objid; +} + +model::state::runner_id_t model_to_system_map::observe_runner( + real_world::remote_address rp_vobj_handle, + const model::runner_state *vobs, const model::transition *t) { + return observe_runner(std::move(rp_vobj_handle), vobs, + [t](runner_id_t id) { return t; }); +} + +model::state::runner_id_t model_to_system_map::observe_runner( + real_world::remote_address rp_vobj_handle, + const model::runner_state *vobs, runner_generation_function f) { + if (contains(rp_vobj_handle)) { + throw std::runtime_error( + "Attempting to rebind a remote address to an object that already " + "exists in the model. Did you check that the object doesn't already " + "exist?"); + } + model::state::runner_id_t const new_runner_id = + _coordinator.current_program_model.discover_runner(vobs, std::move(f)); + model::state::objid_t const new_objid = + _coordinator.get_current_program_model() + .get_state_sequence() + .get_objid_for_runner(new_runner_id); + _coordinator.system_address_mapping.insert({rp_vobj_handle, new_objid}); + return new_runner_id; +} + +void model_to_system_map::observe_runner_transition( + const model::transition *t) { + _coordinator.current_program_model.get_pending_transitions().set_transition( + t); +} + +model::state::runner_id_t model_to_system_map::observe_runner( + real_world::remote_address rp_vobj_handle, + const model::runner_state *rs) { + // TODO: Remove code duplication here. Also there's a better way to do all of + // this I think (instead of e.g. needing to add similar methods onto + // `model::program`) + model::state::runner_id_t const new_runner_id = + _coordinator.current_program_model.get_current_state().add_runner(rs); + model::state::objid_t const new_objid = + _coordinator.get_current_program_model() + .get_state_sequence() + .get_objid_for_runner(new_runner_id); + _coordinator.system_address_mapping.insert({rp_vobj_handle, new_objid}); + return new_runner_id; +} diff --git a/dmtcp/src/mcmini/coordinator/restore-objects.cpp b/dmtcp/src/mcmini/coordinator/restore-objects.cpp new file mode 100644 index 00000000..7b7deda3 --- /dev/null +++ b/dmtcp/src/mcmini/coordinator/restore-objects.cpp @@ -0,0 +1,3 @@ +#include "mcmini/coordinator/restore-objects.hpp" + +// ... diff --git a/dmtcp/src/mcmini/log/filter.cpp b/dmtcp/src/mcmini/log/filter.cpp new file mode 100644 index 00000000..f78762fb --- /dev/null +++ b/dmtcp/src/mcmini/log/filter.cpp @@ -0,0 +1,53 @@ +#include "mcmini/log/filter.hpp" + +#include "mcmini/log/severity_level_parser.hpp" +#include "mcmini/misc/ini-parser.hpp" + +using namespace logging; + +bool whitelist_filter::apply(const std::string &subsystem, + severity_level log_record_severity) const { + if (registered_subsystems.count(subsystem) != 0) { + return log_record_severity <= registered_subsystems.at(subsystem); + } + severity_level most_permissive = severity_level::nothing; + for (const auto &pair : registered_regexes) { + if (std::regex_match(subsystem, pair.first)) { + most_permissive = std::max(most_permissive, pair.second); + } + } + return log_record_severity <= most_permissive; +} + +bool blacklist_filter::apply(const std::string &subsystem, + severity_level log_record_severity) const { + if (registered_subsystems.count(subsystem) != 0) { + return log_record_severity > registered_subsystems.at(subsystem); + } + severity_level most_restrictive = severity_level::nothing; + for (const auto &pair : registered_regexes) { + if (std::regex_match(subsystem, pair.first)) { + most_restrictive = std::max(most_restrictive, pair.second); + } + } + return log_record_severity > most_restrictive; +} + +composite_filter::composite_filter(const std::string &ini_file_path) { + // IniParser ini_contents(ini_file_path); + + // for (const auto §ion : ini_contents) { + // std::string lowerSection = section.first; + // std::transform(lowerSection.begin(), lowerSection.end(), + // lowerSection.begin(), ::tolower); + + // if (lowerSection == "blacklist") { + // for (const auto &item : section.second) + // mapping[item.first] = parse_severity(item.second); + // } + // else if (lowerSection == "whitelist") { + + // } + // } + // return mapping; +} diff --git a/dmtcp/src/mcmini/log/logger.cpp b/dmtcp/src/mcmini/log/logger.cpp new file mode 100644 index 00000000..b41434e5 --- /dev/null +++ b/dmtcp/src/mcmini/log/logger.cpp @@ -0,0 +1,95 @@ +#include "mcmini/log/logger.hpp" + +#include +#include + +#include "mcmini/log/log_control.hpp" +#include "mcmini/log/severity_level.hpp" + +using namespace logging; + +static const char *log_level_strs[] = { + "NOTHING", "VERY_VERBOSE", "VERBOSE", "DEBUG", "INFO", + "UNEXPECTED", "ERROR", "CRITICAL", "ABORT", "EVERYTHING"}; + +void log_control::allow_everything() { + this->set_filter(new permissive_filter()); +} + +void log_control::allow_everything_over(severity_level level) { + this->set_filter(new severity_filter(level)); +} + +void log_control::blacklist(blacklist_filter bl) { + this->set_filter(new blacklist_filter(std::move(bl))); +} + +void log_control::set_filter(filter *filt) { + RWLock::WriteGuard guard(this->filter_lock); + this->active_filter.reset(filt); +} + +static std::string filename_from_path(const char *path) { + std::string p(path); + auto pos = p.find_last_of("/\\"); + std::string basename = (pos == std::string::npos) ? p : p.substr(pos + 1); + return basename; +} + +static uint32_t digits(uint32_t x) { + if (x == 0) return 1; + return static_cast(std::ceil(std::log10(x + 1))); +} + +void log_control::log_raw(const std::string &instance, + const std::string &subsystem, + const std::string &message, + const severity_level severity, const char *file, + int line) { + std::string subsystem_str = subsystem != "" ? subsystem : "global"; + if (active_filter) { + RWLock::ReadGuard guard(this->filter_lock); + if (!active_filter->apply(subsystem_str, severity)) { + return; + } + } + + tm tm; + time_t t = std::time(nullptr); + localtime_r(&t, &tm); + char buf[100]; + buf[std::strftime(buf, sizeof(buf), "%H:%M:%S", &tm)] = '\0'; + const std::string instance_str = + instance != "" ? (" (" + instance + ") ") : ""; + std::string file_str = filename_from_path(file) + ":" + std::to_string(line); + subsystem_str.resize(constants::subsystem_log_size(), ' '); + file_str.resize(constants::file_log_size(), ' '); + + std::stringstream ss; + ss << "[" << constants::getpid() << "] " << subsystem_str << " " + << instance_str << buf << " " << std::left << std::setw(5) + << log_level_strs[static_cast(severity)] << " " << file_str + << ": "; + const std::string prefix = ss.str(); + + std::vector lines; + std::istringstream ss_msg(message); + for (std::string line; std::getline(ss_msg, line);) { + lines.push_back(line); + } + + if (lines.size() > 1) { + uint32_t lineno = 0; + const uint32_t num_lines = lines.size(); + const uint32_t padding = digits(num_lines); + for (const auto &line : lines) { + std::clog << prefix << "(lineno: " << std::left + << std::setw(padding - digits(lineno) + 1) << lineno + << std::left << std::setw(1) << ") " << line << "\n"; + lineno++; + } + + } else if (!lines.empty()) { + std::clog << prefix << lines[0] << "\n"; + } +} diff --git a/dmtcp/src/mcmini/mcmini.cpp b/dmtcp/src/mcmini/mcmini.cpp new file mode 100644 index 00000000..025c28e3 --- /dev/null +++ b/dmtcp/src/mcmini/mcmini.cpp @@ -0,0 +1,516 @@ +#include "mcmini/Thread_queue.h" +#include "mcmini/common/shm_config.h" +#include "mcmini/coordinator/coordinator.hpp" +#include "mcmini/coordinator/restore-objects.hpp" +#include "mcmini/log/severity_level_parser.hpp" +#include "mcmini/model/config.hpp" +#include "mcmini/model/objects/condition_variables.hpp" +#include "mcmini/model/objects/mutex.hpp" +#include "mcmini/model/objects/semaphore.hpp" +#include "mcmini/model/objects/thread.hpp" +#include "mcmini/model/transitions/thread/thread_exit.hpp" +#include "mcmini/model_checking/algorithm.hpp" +#include "mcmini/model_checking/algorithms/classic_dpor.hpp" +#include "mcmini/real_world/fifo.hpp" +#include "mcmini/real_world/process/dmtcp_process_source.hpp" +#include "mcmini/real_world/process/multithreaded_fork_process_source.hpp" +#include "mcmini/real_world/process/resources.hpp" +#include "mcmini/signal.hpp" + +#define _XOPEN_SOURCE_EXTENDED 1 + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace extensions; +using namespace model; +using namespace model_checking; +using namespace objects; +using namespace real_world; + +visible_object_state* translate_recorded_object_to_model( + const ::visible_object& recorded_object, + const std::unordered_map< + void*, std::vector>> + cv_waiting_threads) { + // TODO: A function table would be slightly better, but this works perfectly + // fine too. + switch (recorded_object.type) { + case MUTEX: { + auto mutex_state = + static_cast(recorded_object.mut_state); + pthread_mutex_t* mutex_location = + (pthread_mutex_t*)recorded_object.location; + return new objects::mutex(mutex_state, mutex_location); + } + case CONDITION_VARIABLE: { + // Create the condition variable model object with full state information + auto cv_state = static_cast( + recorded_object.cond_state.status); + + runner_id_t interacting_thread = + recorded_object.cond_state.interacting_thread; + pthread_mutex_t* associated_mutex = + recorded_object.cond_state.associated_mutex; + int count = recorded_object.cond_state.count; + // get waiting threads from the map + auto it = cv_waiting_threads.find(recorded_object.location); + std::vector> + waiters_with_state = + (it != cv_waiting_threads.end()) + ? it->second + : std::vector< + std::pair>(); + return new objects::condition_variable(cv_state, interacting_thread, + associated_mutex, count, + waiters_with_state); + } + case SEMAPHORE: { + return new objects::semaphore(static_cast( + recorded_object.sem_state.status), + recorded_object.sem_state.count); + } + // Other objects here + // case ... { } + // ... + default: { + std::cerr << "The new object type" << recorded_object.type + << "hasn't been implemented yet\n"; + std::abort(); + } + } +} + +runner_state* translate_recorded_runner_to_model( + const ::visible_object& recorded_object) { + switch (recorded_object.type) { + case THREAD: { + return new objects::thread(recorded_object.thrd_state.status); + } + default: { + std::abort(); + } + } +} + +void finished_trace_classic_dpor(const coordinator& c, const stats& stats) { + std::stringstream ss; + const auto& program_model = c.get_current_program_model(); + ss << "TRACE " << stats.trace_id << "\n"; + for (const auto& t : program_model.get_trace()) { + ss << "thread " << t->get_executor() << ": " << t->to_string() << "\n"; + } + ss << "\nNEXT THREAD OPERATIONS\n"; + for (const auto& tpair : program_model.get_pending_transitions()) { + ss << "thread " << tpair.first << ": " << tpair.second->to_string() << "\n"; + } + std::cout << ss.str(); + std::cout.flush(); +} + +void found_undefined_behavior(const coordinator& c, const stats& stats, + const undefined_behavior_exception& ub) { + std::cerr << "UNDEFINED BEHAVIOR:\n" << ub.what() << std::endl; + finished_trace_classic_dpor(c, stats); +} + +void found_abnormal_termination( + const coordinator& c, const stats& stats, + const real_world::process::termination_error& ub) { + std::cerr << "Abnormally Termination (signo: " << ub.signo + << ", signal: " << sig_to_str.at(ub.signo) << "):\n" + << ub.what() << std::endl; + + std::stringstream ss; + const auto& program_model = c.get_current_program_model(); + ss << "TRACE " << stats.trace_id << "\n"; + for (const auto& t : program_model.get_trace()) { + ss << "thread " << t->get_executor() << ": " << t->to_string() << "\n"; + } + const transition* terminator = + program_model.get_pending_transition_for(ub.culprit); + ss << "thread " << terminator->get_executor() << ": " + << terminator->to_string() << "\n"; + + ss << "\nNEXT THREAD OPERATIONS\n"; + for (const auto& tpair : program_model.get_pending_transitions()) { + if (tpair.first == terminator->get_executor()) { + ss << "thread " << tpair.first << ": executing" + << "\n"; + } else { + ss << "thread " << tpair.first << ": " << tpair.second->to_string() + << "\n"; + } + } + ss << stats.total_transitions + 1 << " total transitions executed" + << "\n"; + std::cout << ss.str(); + std::cout.flush(); +} + +void found_deadlock(const coordinator& c, const stats& stats) { + std::cerr << "DEADLOCK" << std::endl; + std::stringstream ss; + const auto& program_model = c.get_current_program_model(); + for (const auto& t : program_model.get_trace()) { + ss << "thread " << t->get_executor() << ": " << t->to_string() << "\n"; + } + ss << "\nNEXT THREAD OPERATIONS\n"; + for (const auto& tpair : program_model.get_pending_transitions()) { + ss << "thread " << tpair.first << ": " << tpair.second->to_string() << "\n"; + } + std::cout << ss.str(); + std::cout.flush(); +} + +void do_model_checking(const config& config) { + algorithm::callbacks c; + target target_program(config.target_executable, + config.target_executable_args); + coordinator coordinator(program::starting_from_main(), + transition_registry::default_registry(), + make_unique(target_program)); + std::cerr << "\n\n**************** INTIAL STATE *********************\n\n"; + coordinator.get_current_program_model().dump_state(std::cerr); + std::cerr << "\n\n**************** INTIAL STATE *********************\n\n"; + std::cerr.flush(); + + model_checking::classic_dpor classic_dpor_checker; + c.trace_completed = &finished_trace_classic_dpor; + c.deadlock = &found_deadlock; + c.undefined_behavior = &found_undefined_behavior; + c.abnormal_termination = &found_abnormal_termination; + classic_dpor_checker.verify_using(coordinator, c); + std::cout << "Model checking completed!" << std::endl; +} + +void do_model_checking_from_dmtcp_ckpt_file(const config& config) { + volatile mcmini_shm_file* rw_region = + xpc_resources::get_instance().get_rw_region()->as(); + + std::unique_ptr dmtcp_template_handle; + + if (config.use_multithreaded_fork) { + dmtcp_template_handle = + extensions::make_unique( + config.checkpoint_file); + } else { + dmtcp_template_handle = + extensions::make_unique(config.checkpoint_file); + } + + // Make sure that `dmtcp_restart` has executed and that the template + // process is ready for execution; otherwise, the state restoration will not + // work as expected. + algorithm::callbacks c; + transition_registry tr = transition_registry::default_registry(); + coordinator coordinator(program(), tr, std::move(dmtcp_template_handle)); + { + model_to_system_map recorder(coordinator); + + fifo fifo("/tmp/mcmini-fifo"); + ::visible_object current_obj; + std::vector<::visible_object> recorded_threads; + std::unordered_map< + void*, std::vector>> + cv_waiting_threads; + while (fifo.read(¤t_obj) && current_obj.type != UNKNOWN) { + if (current_obj.type == THREAD) { + recorded_threads.emplace_back(std::move(current_obj)); + } else if (current_obj.type == CV_WAITERS_QUEUE) { + // Capture both thread ID and state + cv_waiting_threads[current_obj.waiting_queue_state.cv_location] + .push_back( + std::make_pair(current_obj.waiting_queue_state.waiting_id, + current_obj.waiting_queue_state.cv_state)); + } else { + recorder.observe_object(current_obj.location, + translate_recorded_object_to_model( + current_obj, cv_waiting_threads)); + } + } + + std::sort(recorded_threads.begin(), recorded_threads.end(), + [](const ::visible_object& lhs, const ::visible_object& rhs) { + return lhs.thrd_state.id < rhs.thrd_state.id; + }); + + for (const ::visible_object& recorded_thread : recorded_threads) { + recorder.observe_runner( + (void*)recorded_thread.thrd_state.pthread_desc, + translate_recorded_runner_to_model(recorded_thread)); + } + + for (const ::visible_object& recorded_thread : recorded_threads) { + // Translates from what each user space thread recorded as its next + // transition. This happens _after_ DMTCP has restarted the checkpoint + // image but _before_ the template thread told the McMini process (i.e. + // this one) about the recorded objects. In other words, each user space + // thread has "recorded" (in the sense of "marked" and not in the sense + // of "during the RECORD phase of `libmcmini.so`") the next transition + // it would have run had McMini not just now intervened. + runner_id_t recorded_id = recorded_thread.thrd_state.id; + const transition* next_transition = nullptr; + + switch (recorded_thread.thrd_state.status) { + case ALIVE: { + volatile runner_mailbox* mb = &rw_region->mailboxes[recorded_id]; + transition_registry::transition_discovery_callback callback = + tr.get_callback_for(mb->type); + if (!callback) { + throw std::runtime_error("Expected a callback for " + + std::to_string(mb->type)); + } + next_transition = callback(recorded_id, *mb, recorder); + break; + } + case EXITED: { + next_transition = new transitions::thread_exit(recorded_id); + } + } + + const size_t num_objects_before = + coordinator.get_current_program_model().get_state_sequence().count(); + recorder.observe_runner_transition(next_transition); + const size_t num_objects_after = + coordinator.get_current_program_model().get_state_sequence().count(); + + // Ensures that no objects were added during `callback`. + // + // Callbacks insert objects into the model when they notice + // that the model is missing an association for a particular + // real world address. Since we already translated all recorded + // objects from `libmcmini.so`, if an object is ever added + // when determining the pending operation of `recorded_thread`, + // this means that the object was not added to the model and + // hence was not sent to the McMini proces by `libmcmini.so`. + // + // In short, this ensures that recording worked as expected. + if (num_objects_before != num_objects_after) { + throw std::runtime_error( + "A callback inserted an object into the model, but all objects " + "should have been available to it during record."); + } + } + } + + std::cerr << "\n\n**************** INTIAL STATE *********************\n\n"; + coordinator.get_current_program_model().dump_state(std::cerr); + std::cerr << "\n\n**************** INTIAL STATE *********************\n\n"; + std::cerr.flush(); + + model_checking::classic_dpor classic_dpor_checker; + c.trace_completed = &finished_trace_classic_dpor; + c.undefined_behavior = &found_undefined_behavior; + c.deadlock = &found_deadlock; + c.abnormal_termination = &found_abnormal_termination; + classic_dpor_checker.verify_using(coordinator, c); + std::cerr << "Deep debugging completed!" << std::endl; +} + +#include "mcmini/log/logger.hpp" + +void do_recording(const config& config) { + char dir[PATH_MAX]; + // FIXME: This depends on mcmini starting in root dir of git repo. + std::string libmcini_dir = getcwd(dir, sizeof(dir)) ? dir : "PATH_TOO_LONG"; + std::string libmcmini_path = libmcini_dir + "/libmcmini.so"; + std::vector dmtcp_launch_args; + dmtcp_launch_args.push_back("--disable-alloc-plugin"); + dmtcp_launch_args.push_back("-i"); + dmtcp_launch_args.push_back(std::to_string(config.checkpoint_period.count())); + dmtcp_launch_args.push_back("--with-plugin"); + dmtcp_launch_args.push_back(libmcmini_path); + dmtcp_launch_args.push_back("--modify-env"); + dmtcp_launch_args.push_back(config.target_executable); + for (const std::string& target_arg : config.target_executable_args) + dmtcp_launch_args.push_back(target_arg); + real_world::target target_program("dmtcp_launch", dmtcp_launch_args); + std::cout << "Recording: " << target_program << std::endl; + target_program.execvp(); +} + +std::string find_first_ckpt_file_in_cwd() { + try { + // Open the current directory + DIR* dir = opendir("."); + if (dir == nullptr) { + perror("opendir"); + return ""; + } + + struct dirent* entry; + while ((entry = readdir(dir)) != nullptr) { + // Check if the entry is a regular file and has the .foo extension + if (entry->d_type == DT_REG) { // DT_REG indicates a regular file + std::string filename(entry->d_name); + if (filename.size() >= 6 && + filename.substr(filename.size() - 6) == ".dmtcp") { + std::cout << "Found checkpoint file: " << filename << std::endl; + closedir(dir); + return filename; + } + } + } + std::cout + << "No file with the `.dmtcp` extension found in the current directory." + << std::endl; + closedir(dir); + return ""; + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + return ""; + } +} + +int main_cpp(int argc, const char** argv) { + model::config mcmini_config; + + if (const char* env_p = std::getenv("MCMINI_LOG_LEVEL")) + mcmini_config.global_severity_level = logging::parse_severity(env_p); + + const char** cur_arg = &argv[1]; + if (argc == 1) { + cur_arg[0] = "--help"; + cur_arg[1] = NULL; + } + + // TODO: Use argp.h instead (more options, better descriptions, etc) + while (cur_arg[0] != NULL && cur_arg[0][0] == '-') { + if (strcmp(cur_arg[0], "--max-depth-per-thread") == 0 || + strcmp(cur_arg[0], "-m") == 0) { + mcmini_config.max_thread_execution_depth = + strtoul(cur_arg[1], nullptr, 10); + + char* endptr; + if (strtol(cur_arg[1], &endptr, 10) == 0 || endptr[0] != '\0') { + fprintf(stderr, "%s: illegal value\n", "--max-depth-per-thread"); + exit(1); + } + cur_arg += 2; + } else if (strcmp(cur_arg[0], "--interval") == 0 || + strcmp(cur_arg[0], "-i") == 0) { + mcmini_config.record_target_executable_only = true; + mcmini_config.checkpoint_period = + std::chrono::seconds(strtoul(cur_arg[1], nullptr, 10)); + cur_arg += 2; + } else if (strcmp(cur_arg[0], "--from-checkpoint") == 0 || + strcmp(cur_arg[0], "-ckpt") == 0) { + mcmini_config.checkpoint_file = cur_arg[1]; + cur_arg += 2; + } else if (strcmp(cur_arg[0], "--log-level") == 0 || + strcmp(cur_arg[0], "-log") == 0) { + mcmini_config.global_severity_level = logging::parse_severity(cur_arg[1]); + cur_arg += 2; + } else if (strcmp(cur_arg[0], "--from-first-checkpoint") == 0) { + mcmini_config.checkpoint_file = find_first_ckpt_file_in_cwd(); + cur_arg += 1; + } else if (strcmp(cur_arg[0], "--multithreaded-fork") == 0 || + strcmp(cur_arg[0], "-mtf") == 0) { + mcmini_config.use_multithreaded_fork = true; + cur_arg++; + } else if (cur_arg[0][1] == 'm' && isdigit(cur_arg[0][2])) { + mcmini_config.max_thread_execution_depth = + strtoul(cur_arg[1], nullptr, 10); + cur_arg++; + } else if (strcmp(cur_arg[0], "--first-deadlock") == 0 || + strcmp(cur_arg[0], "--first") == 0 || + strcmp(cur_arg[0], "-f") == 0) { + mcmini_config.stop_at_first_deadlock = true; + cur_arg++; + } else if (strcmp(cur_arg[0], "--print-at-traceId") == 0 || + strcmp(cur_arg[0], "-p") == 0) { + mcmini_config.target_trace_id = strtoul(cur_arg[1], nullptr, 10); + char* endptr; + if (strtol(cur_arg[1], &endptr, 10) == 0 || endptr[0] != '\0') { + fprintf(stderr, "%s: illegal value\n", "--print-at-traceId"); + exit(1); + } + cur_arg += 2; + } else if (cur_arg[0][1] == 'p' && isdigit(cur_arg[0][2])) { + mcmini_config.target_trace_id = strtoul(cur_arg[2], nullptr, 10); + cur_arg++; + } else if (strcmp(cur_arg[0], "--help") == 0 || + strcmp(cur_arg[0], "-h") == 0) { + fprintf( + stderr, + "Usage: mcmini (experimental)\n" + " [--record|-r ] \n" + " [--from-checkpoint ] [--multithreaded-fork] \n" + " [--max-depth-per-thread|-m ]\n" + " [--first-deadlock|--first|-f]\n" + " [--log-level|-log ]\n" + " [--help|-h]\n" + " target_executable\n"); + exit(1); + } else { + printf("mcmini: unrecognized option: %s\n", cur_arg[0]); + exit(1); + } + } + + if (mcmini_config.checkpoint_file == "") { + struct stat stat_buf; + if (cur_arg[0] == NULL || stat(cur_arg[0], &stat_buf) == -1) { + fprintf(stderr, "*** Missing target_executable or no such file.\n\n"); + exit(1); + } + + assert(cur_arg[0][strlen(cur_arg[0])] == '\0'); + char idx = strlen(cur_arg[0]) - strlen("mcmini") - 1 >= 0 + ? strlen(cur_arg[0]) - strlen("mcmini") - 1 + : strlen(cur_arg[0]); + // idx points to 'X' when cur_arg[0] == "...Xmcmini" + if (strcmp(cur_arg[0], "mcmini") == 0 || + strcmp(cur_arg[0] + idx, "/mcmini") == 0) { + fprintf(stderr, + "\n*** McMini being called on 'mcmini'. This doesn't work.\n"); + exit(1); + } + mcmini_config.target_executable = std::string(cur_arg[0]); + // Collect all remaining arguments as target executable arguments + cur_arg++; + while (cur_arg[0] != NULL) { + mcmini_config.target_executable_args.push_back(std::string(cur_arg[0])); + cur_arg++; + } + } + signal_tracker::install_process_wide_signal_handlers(); + logging::log_control::instance().allow_everything_over( + mcmini_config.global_severity_level); + if (mcmini_config.record_target_executable_only) { + do_recording(mcmini_config); + } else if (mcmini_config.checkpoint_file != "") { + do_model_checking_from_dmtcp_ckpt_file(mcmini_config); + } else { + do_model_checking(mcmini_config); + } + return EXIT_SUCCESS; +} + +int main(int argc, const char** argv) { + try { + return main_cpp(argc, argv); + } catch (const std::exception& e) { + std::cerr << "ERROR: " << e.what() << std::endl; + return EXIT_FAILURE; + } catch (...) { + std::cerr << "ERROR: Unknown error occurred" << std::endl; + return EXIT_FAILURE; + } +} diff --git a/dmtcp/src/mcmini/model/cond_var_arbitrary_policy.cpp b/dmtcp/src/mcmini/model/cond_var_arbitrary_policy.cpp new file mode 100644 index 00000000..f4f99f3a --- /dev/null +++ b/dmtcp/src/mcmini/model/cond_var_arbitrary_policy.cpp @@ -0,0 +1,56 @@ +#include "mcmini/misc/cond/cond_var_arbitrary_policy.hpp" +#include +#include + +ConditionVariablePolicy* +ConditionVariableArbitraryPolicy::clone() const +{ + return new ConditionVariableArbitraryPolicy(*this); +} + +void +ConditionVariableArbitraryPolicy::receive_signal_message() +{ + if (!this->wait_queue.empty()) { + this->wake_groups.push_back( + WakeGroup(this->wait_queue.begin(), this->wait_queue.end())); + } +} + +bool +ConditionVariableArbitraryPolicy::has_waiters() const +{ + return ! this->wait_queue.empty(); +} + +std::deque ConditionVariableArbitraryPolicy::return_wait_queue() const { + return this->wait_queue; +} + +std::vector ConditionVariableArbitraryPolicy::return_wake_groups() const { + return this->wake_groups; +} + +void ConditionVariableArbitraryPolicy::add_to_wake_groups(const std::vector& threads) { + if (!threads.empty()) { + this->wake_groups.push_back(WakeGroup(threads.begin(), threads.end())); + } +} + +// Add thread with specific state +void ConditionVariableArbitraryPolicy::add_waiter_with_state(runner_id_t tid, condition_variable_status state) { + add_waiter(tid); + this->threads_with_states[tid] = state; +} + +// Get thread's current state +condition_variable_status ConditionVariableArbitraryPolicy::get_thread_cv_state(runner_id_t tid) { + auto it = this->threads_with_states.find(tid); + return (it != this->threads_with_states.end()) ? it->second : CV_UNINITIALIZED; +} + +void ConditionVariableArbitraryPolicy::update_thread_cv_state(runner_id_t tid, condition_variable_status state) { + this->threads_with_states[tid] = state; +} + + diff --git a/dmtcp/src/mcmini/model/cond_var_default_policy.cpp b/dmtcp/src/mcmini/model/cond_var_default_policy.cpp new file mode 100644 index 00000000..fb064722 --- /dev/null +++ b/dmtcp/src/mcmini/model/cond_var_default_policy.cpp @@ -0,0 +1,78 @@ +/* File inspired by classic mcmini*/ +#include "mcmini/misc/cond/cond_var_default_policy.hpp" +#include +#include +#include + +void +ConditionVariableDefaultPolicy::receive_broadcast_message() +{ + // Move everyone into the get-out-of-jail free place + // from the wake group list. + for (const WakeGroup &wg : this->wake_groups) { + for (const runner_id_t signaled_thread : wg) { + this->broadcast_eligible_threads.insert(signaled_thread); + } + } + this->wake_groups.clear(); +} + +bool +ConditionVariableDefaultPolicy::has_waiters() const +{ + // broadcast_eligiblle_threads are those threads that were + // blocked, but were around during the last broadcast. + // wake_groups are the groups that are available to wake. + return ! this->broadcast_eligible_threads.empty() || + ! this->wake_groups.empty(); +} + +bool +ConditionVariableDefaultPolicy::thread_can_exit(runner_id_t tid) const +{ + // Either you're eligible to wake up because: + + // 1. You were around during a broadcast message + if (this->broadcast_eligible_threads.count(tid) > 0) { + return true; + } + + // 2. OR you can now consume a signal + return std::any_of( + wake_groups.begin(), wake_groups.end(), + [=](const WakeGroup &wg) { return wg.contains(tid); }); + +} + +void +ConditionVariableDefaultPolicy::wake_thread(runner_id_t tid) +{ + // To correctly match the semantics of condition variables, + // if a thread was present in the condition variable during a + // broadcast, we DO NOT want it to consume signals which arrive + // later, since we want to treat the thread as no longer waiting + // on the condition variable + if (this->broadcast_eligible_threads.count(tid) > 0) { + this->broadcast_eligible_threads.erase(tid); + } else { + // Otherwise, we should consume the FIRST signal + // we're located in: we want to ensure we allow + // the most possible threads to + const auto signal_to_consume = std::find_if( + wake_groups.begin(), wake_groups.end(), + [=](const WakeGroup &wg) { return wg.contains(tid); }); + + // If 'signal_to_consume == wake_groups.end()', we are attempting + // to wake a thread which can neither consume a signal nor has + // been woken from a prior broadcast. + assert(signal_to_consume != wake_groups.end()); + wake_groups.erase(signal_to_consume); + } + + //Additionally, remove this thread from any other + // wake groups it may be in, so that it does'nt attempt + // to consume a signal in the future + for (WakeGroup &wg : this->wake_groups) { + wg.remove_candidate_thread(tid); + } +} diff --git a/dmtcp/src/mcmini/model/cond_var_single_grp_policy.cpp b/dmtcp/src/mcmini/model/cond_var_single_grp_policy.cpp new file mode 100644 index 00000000..c25cac70 --- /dev/null +++ b/dmtcp/src/mcmini/model/cond_var_single_grp_policy.cpp @@ -0,0 +1,46 @@ +/* This file is inspired by classic mcmini */ +#include "mcmini/misc/cond/cond_var_single_grp_policy.hpp" + +#include + +void +ConditionVariableSingleGroupPolicy::receive_broadcast_message() +{ + // Empty BOTH the sleep queue and the wake group list: + // at this point everyone is allowed to wake up + ConditionVariableDefaultPolicy::receive_broadcast_message(); + + for (const runner_id_t waiting_thread : this->wait_queue) { + this->broadcast_eligible_threads.insert(waiting_thread); + } + this->wait_queue.clear(); +} + +void +ConditionVariableSingleGroupPolicy::wake_thread(runner_id_t tid) +{ + ConditionVariableDefaultPolicy::wake_thread(tid); + + // Remove the thread from the sleep queue if it is still + // contained in there. Some policies may decide to keep + // the thread in the queue as part of their implementation, + // but they should always be removed when a thread is woken + const auto waiting_thread = + std::find(wait_queue.begin(), wait_queue.end(), tid); + if (waiting_thread != wait_queue.end()) { + wait_queue.erase(waiting_thread); + } +} + +void +ConditionVariableSingleGroupPolicy::add_waiter(runner_id_t tid) +{ + this->wait_queue.push_back(tid); + +} + +bool +ConditionVariableSingleGroupPolicy::has_waiters() const +{ + return ! this->wait_queue.empty(); +} diff --git a/dmtcp/src/mcmini/model/cond_var_wakegroup.cpp b/dmtcp/src/mcmini/model/cond_var_wakegroup.cpp new file mode 100644 index 00000000..d1adea00 --- /dev/null +++ b/dmtcp/src/mcmini/model/cond_var_wakegroup.cpp @@ -0,0 +1,11 @@ +#include "mcmini/misc/cond/cond_var_wakegroup.hpp" +#include + + + bool WakeGroup::contains(runner_id_t tid) const { + return std::find(begin(), end(), tid) != end(); + } + + void WakeGroup::remove_candidate_thread(runner_id_t tid) { + erase(std::remove(begin(), end(), tid), end()); + } diff --git a/dmtcp/src/mcmini/model/detached_state.cpp b/dmtcp/src/mcmini/model/detached_state.cpp new file mode 100644 index 00000000..83aa992e --- /dev/null +++ b/dmtcp/src/mcmini/model/detached_state.cpp @@ -0,0 +1,86 @@ +#include "mcmini/model/state/detached_state.hpp" + +#include +#include +#include +#include + +#include "mcmini/misc/append-only.hpp" +#include "mcmini/misc/extensions/unique_ptr.hpp" +#include "mcmini/model/state.hpp" +#include "mcmini/model/visible_object.hpp" +#include "mcmini/model/visible_object_state.hpp" + +using namespace model; + +detached_state::detached_state(std::vector &&objs) + : visible_objects(std::move(objs)) {} +detached_state::detached_state(append_only &&objs) + : visible_objects(std::move(objs)) {} + +bool detached_state::contains_object_with_id(state::objid_t id) const { + return id < this->visible_objects.size(); +} + +state::objid_t detached_state::get_objid_for_runner(runner_id_t id) const { + return this->contains_runner_with_id(id) ? runner_to_obj_map.at(id) + : model::invalid_objid; +} + +runner_id_t detached_state::get_runner_id_for_obj(objid_t id) const { + return this->is_runner(id) ? this->runner_to_obj_map.range_at(id) + : model::invalid_rid; +} + +bool detached_state::contains_runner_with_id(runner_id_t id) const { + return this->runner_to_obj_map.count_domain(id) > 0; +} + +bool detached_state::is_runner(objid_t id) const { + return this->runner_to_obj_map.count_range(id) > 0; +} + +const visible_object_state *detached_state::get_state_of_object( + objid_t id) const { + return this->visible_objects.at(id).get_current_state(); +} + +const runner_state *detached_state::get_state_of_runner(runner_id_t id) const { + return static_cast( + this->get_state_of_object(this->get_objid_for_runner(id))); +} + +state::objid_t detached_state::add_object( + const visible_object_state *initial_state) { + // INVARIANT: The current element needs to update at index `id` to reflect + // this new object, as this element effectively represents this state + visible_objects.push_back(initial_state); + return visible_objects.size() - 1; +} + +state::runner_id_t detached_state::add_runner(const runner_state *new_state) { + const objid_t runner_objid = this->add_object(new_state); + const runner_id_t next_runner_id = this->runner_to_obj_map.size(); + this->runner_to_obj_map.insert({next_runner_id, runner_objid}); + return next_runner_id; +} + +void detached_state::add_state_for_obj(objid_t id, + const visible_object_state *new_state) { + if (id == invalid_objid) { + throw std::runtime_error( + "Attempted to insert a state for an invalid object id"); + } + // INVARIANT: The current element needs to update at index `id` to reflect + // this new state, as this element effectively represents this state + this->visible_objects.at(id).push_state(new_state); +} + +void detached_state::add_state_for_runner(runner_id_t id, + const runner_state *new_state) { + return this->add_state_for_obj(this->get_objid_for_runner(id), new_state); +} + +std::unique_ptr detached_state::mutable_clone() const { + return extensions::make_unique(*this); +} diff --git a/dmtcp/src/mcmini/model/diff_state.cpp b/dmtcp/src/mcmini/model/diff_state.cpp new file mode 100644 index 00000000..5213d051 --- /dev/null +++ b/dmtcp/src/mcmini/model/diff_state.cpp @@ -0,0 +1,95 @@ +#include "mcmini/model/state/diff_state.hpp" + +#include +#include + +#include "mcmini/misc/extensions/unique_ptr.hpp" +#include "mcmini/model/state.hpp" +#include "mcmini/model/visible_object.hpp" +#include "mcmini/model/visible_object_state.hpp" + +using namespace model; + +state::objid_t diff_state::get_objid_for_runner(runner_id_t id) const { + if (this->new_runners.count_domain(id) > 0) { + return this->new_runners.at(id); + } + return this->base_state.get_objid_for_runner(id); +} + +runner_id_t diff_state::get_runner_id_for_obj(objid_t id) const { + if (this->new_runners.count_range(id) > 0) { + return this->new_runners.range_at(id); + } + return this->base_state.get_runner_id_for_obj(id); +} + +bool diff_state::is_runner(objid_t id) const { + return this->new_runners.count_range(id) > 0 || + this->base_state.is_runner(id); +} + +bool diff_state::contains_object_with_id(objid_t id) const { + return this->new_object_states.count(id) > 0 || + this->base_state.contains_object_with_id(id); +} + +bool diff_state::contains_runner_with_id(runner_id_t id) const { + return this->new_runners.count_domain(id) > 0 || + this->base_state.contains_runner_with_id(id); +} + +const visible_object_state *diff_state::get_state_of_object(objid_t id) const { + if (this->new_object_states.count(id) > 0) { + return this->new_object_states.at(id).get_current_state(); + } + return this->base_state.get_state_of_object(id); +} + +const runner_state *diff_state::get_state_of_runner(runner_id_t id) const { + if (this->new_runners.count_domain(id) > 0) { + return static_cast( + this->get_state_of_object(this->new_runners.at(id))); + } + return this->base_state.get_state_of_runner(id); +} + +state::objid_t diff_state::add_object( + const visible_object_state *initial_state) { + // The next id that would be assigned is one more than + // the largest id available. The last id of the base it `size() - 1` and + // we are `new_object_state.size()` elements in + state::objid_t const next_id = base_state.count() + new_object_states.size(); + new_object_states[next_id] = initial_state; + return next_id; +} + +state::runner_id_t diff_state::add_runner(const runner_state *initial_state) { + objid_t const objid = this->add_object(initial_state); + + // The next runner id would be the current size. + state::objid_t const next_runner_id = runner_count(); + this->new_runners.insert({next_runner_id, objid}); + return next_runner_id; +} + +void diff_state::add_state_for_obj(objid_t id, + const visible_object_state *new_state) { + if (id == invalid_objid) { + throw std::runtime_error( + "Attempted to insert a state for an invalid object id"); + } + // Here we seek to insert all new states into the local cache instead of + // forwarding them onto the underlying base state. + visible_object &vobj = new_object_states[id]; + vobj.push_state(new_state); +} + +void diff_state::add_state_for_runner(runner_id_t id, + const runner_state *new_state) { + this->add_state_for_obj(this->get_objid_for_runner(id), new_state); +} + +std::unique_ptr diff_state::mutable_clone() const { + throw std::runtime_error("TODO: Implement diff_state cloning"); +} diff --git a/dmtcp/src/mcmini/model/program.cpp b/dmtcp/src/mcmini/model/program.cpp new file mode 100644 index 00000000..bb594ef1 --- /dev/null +++ b/dmtcp/src/mcmini/model/program.cpp @@ -0,0 +1,159 @@ +#include "mcmini/model/program.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "mcmini/model/objects/thread.hpp" +#include "mcmini/model/pending_transitions.hpp" +#include "mcmini/model/state.hpp" +#include "mcmini/model/state/detached_state.hpp" +#include "mcmini/model/transition.hpp" +#include "mcmini/model/transitions/thread/thread_start.hpp" +#include "mcmini/model/visible_object_state.hpp" + +using namespace model; + +program::program() : program(detached_state(), pending_transitions()) {} + +program::program(const state &initial_state, + pending_transitions &&initial_first_steps) + : state_seq(initial_state), next_steps(std::move(initial_first_steps)) {} + +program program::starting_from_main() { + detached_state state_of_program_at_main; + pending_transitions initial_first_steps; + const state::runner_id_t main_thread_id = state_of_program_at_main.add_runner( + new objects::thread(objects::thread::state::running)); + initial_first_steps.set_transition( + new transitions::thread_start(main_thread_id)); + return program(state_of_program_at_main, std::move(initial_first_steps)); +} + +std::unordered_set program::get_enabled_runners() const { + std::unordered_set enabled_runners; + for (const auto &runner_and_t : this->next_steps) { + if (runner_and_t.second->is_enabled_in(state_seq)) { + enabled_runners.insert(runner_and_t.first); + } + } + return enabled_runners; +} + +void program::restore_model_at_depth(uint32_t n) { + /* + * Fill in the set of next transitions by + * following the trace from the top to `n` since the trace implicitly holds + * what each thread *was* doing next at each point in time. + * + * We can simply keep track of the _smallest_ index in the trace *greater than + * `n`* for each thread. This must be the most recent transition + * that that was pending for the thread at depth `n` + * + * For threads that didn't run after depth _depth_, the latest transition in + * `next_steps` is already correct and we don't need to do anything. + * + * @note It's possible that some of these threads will be + * in the embryo state at depth `n`; those threads should consequently be + * executing the `thread_start()` transition, but this will be in the trace + */ + std::unordered_map runner_to_index_from_top; + for (uint32_t i = (trace.count() - 1); + i > (uint32_t)(n - 1) && !trace.empty(); i--) + runner_to_index_from_top[trace.at(i)->get_executor()] = i; + + for (const std::pair e : runner_to_index_from_top) + next_steps.set_transition(trace.extract_at(e.second).release()); + + trace.consume_into_subsequence(n); + state_seq.consume_into_subsequence(n + 1); +} + +void program::model_execution_of(runner_id_t p, const transition *npo) { + if (p != npo->get_executor()) { + throw std::runtime_error( + "The next incoming transition replacing `next_s_p` in the model must " + "be run by the same runner (" + + std::to_string(p) + " != " + std::to_string(npo->get_executor()) + ")"); + } + + const transition *next_s_p = next_steps.get_transition_for_runner(p); + if (!next_s_p) { + // This is the first time we've seen `p`; treat `new_transition` as the + // first `next_s_p`. + throw std::runtime_error( + "Attempting to execute a runner whose next transition is unknown to " + "the model. If this runner was dynamically created in the model (e.g. " + "in the callback routine for handling `pthread_create()`), ensure that " + "the "); + } + + transition::status const status = this->state_seq.follow(*next_s_p); + if (status == transition::status::disabled) { + throw std::runtime_error( + "Attempted to model the execution of a disabled transition(" + + next_s_p->debug_string() + ")"); + } + if (status == transition::status::undefined) { + throw std::runtime_error( + "Attempted to model the execution of a undefined transition(" + + next_s_p->debug_string() + ")"); + } + + trace.push(next_steps.replace_unowned(npo)); +} + +state::objid_t program::discover_object( + const visible_object_state *initial_state) { + return this->state_seq.add_object(initial_state); +} + +state::runner_id_t program::discover_runner(const runner_state *initial_state, + runner_generation_function f) { + state::runner_id_t const id = this->state_seq.add_runner(initial_state); + this->next_steps.set_transition(f(id)); + return id; +} + +state::runner_id_t program::discover_runner(const runner_state *initial_state, + const transition *next_transition) { + state::runner_id_t const id = this->state_seq.add_runner(initial_state); + this->next_steps.set_transition(next_transition); + return id; +} + +bool program::is_in_deadlock() const { + // If any transition is enabled, we are not in deadlock + // + // Furthermore, if there are NO enabled transitions, we need to check if this + // is because all runners have exited, i.e. whether they even have more + // transitions to execute. If they do, then then it would be a deadlock in + // the traditional sense since there are threads which haven't completed but + // are blocked. If they've all exited, we shouldn't say this is a deadlock: + // the program trivially can't do anything else. + for (const auto &pair : this->get_pending_transitions()) { + if (pair.second->is_enabled_in(this->state_seq) || + this->state_seq.get_state_of_runner(pair.first)->has_exited()) + return false; + } + return true; +} + +std::ostream &program::dump_state(std::ostream &os) const { + os << this->get_state_sequence().back().debug_string(); + std::stringstream ss; + ss << "\nTHREAD TRACE\n"; + for (const auto &t : get_trace()) { + ss << "thread " << t->get_executor() << ": " << t->to_string() << "\n"; + } + ss << "\nNEXT THREAD OPERATIONS\n"; + for (const auto &tpair : get_pending_transitions()) { + ss << "thread " << tpair.first << ": " << tpair.second->to_string() << "\n"; + } + os << ss.str(); + return os; +} diff --git a/dmtcp/src/mcmini/model/state_sequence.cpp b/dmtcp/src/mcmini/model/state_sequence.cpp new file mode 100644 index 00000000..65877f13 --- /dev/null +++ b/dmtcp/src/mcmini/model/state_sequence.cpp @@ -0,0 +1,330 @@ +#include "mcmini/model/state/state_sequence.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mcmini/misc/append-only.hpp" +#include "mcmini/misc/extensions/memory.hpp" +#include "mcmini/misc/extensions/unique_ptr.hpp" +#include "mcmini/model/state.hpp" +#include "mcmini/model/state/detached_state.hpp" +#include "mcmini/model/transition.hpp" +#include "mcmini/model/visible_object.hpp" +#include "mcmini/model/visible_object_state.hpp" + +using namespace model; + +/** + * @brief An element of a `model::state_sequence` + * + * The `element` and `state_sequence` are tightly intertwined. We allow them + * to work in tandem with one another as an implementation detail to permit + * "views" of the objects the state sequence maintains as new states are added + * to the sequence via transitions + */ +class state_sequence::element : public state { + private: + const state_sequence *owner; + + /// @brief A collection of references to states in the sequence + /// _owning_sequence_ to which this element belongs. + /// + /// Each state is a view into a `visible_object` owned by `owner`. + // INTERPRETATION: For an object with id `id`, the value `n` it is mapped to + // represents the `n`th state of the visible object in the owner that this + // element represents. In other words, this element represents object `id` in + // state `n`. + std::unordered_map visible_object_indices; + + // The largest index into the owning state_sequence's runner mapping. That + // this element is aware of. + runner_id_t max_visible_runner_id; + + element(const state_sequence *owner); + friend state_sequence; + + public: + element() = default; + void record_new_runner() { max_visible_runner_id++; } + void point_to_next_state_for(objid_t id) { + if (this->visible_object_indices.count(id)) + this->visible_object_indices[id]++; + else + this->visible_object_indices[id] = 0; + } + size_t count() const override { return visible_object_indices.size(); } + size_t runner_count() const override { return max_visible_runner_id; } + + objid_t get_objid_for_runner(runner_id_t id) const override; + runner_id_t get_runner_id_for_obj(objid_t id) const override; + bool is_runner(objid_t id) const override; + bool contains_object_with_id(state::objid_t id) const override; + bool contains_runner_with_id(runner_id_t id) const override; + const visible_object_state *get_state_of_object(objid_t id) const override; + const runner_state *get_state_of_runner(runner_id_t id) const override; + std::unique_ptr mutable_clone() const override; + std::string debug_string() const override; +}; + +state_sequence::state_sequence() { this->push_state_snapshot(); } + +state_sequence::state_sequence(const state &s) { + this->push_state_snapshot(); + const size_t num_objs = s.count(); + for (objid_t i = 0; i < (objid_t)num_objs; i++) + this->add_object(s.get_state_of_object(i)->clone().release()); + + const size_t num_runners = s.runner_count(); + for (runner_id_t p = 0; p < (runner_id_t)num_runners; p++) { + this->get_representative_state().record_new_runner(); + this->runner_to_obj_map.insert({p, s.get_objid_for_runner(p)}); + } +} + +state_sequence::state_sequence(state_sequence &&s) { + // For the std::vector<> move constructor: + // """ + // Constructs the container with the contents of + // other using move semantics. Allocator is obtained by move-construction + // from the allocator belonging to other. After the move, other is guaranteed + // to be empty(). + // """ + // See https://en.cppreference.com/w/cpp/container/vector/vector + this->states_in_sequence = std::move(s.states_in_sequence); + this->visible_objects = std::move(s.visible_objects); + this->runner_to_obj_map = std::move(s.runner_to_obj_map); + + // The elements currently point to `s` as their owner. Since we + // are now transferring ownership to _this_ state object, we need to inform + // each element. Note that the states they refer to are still valid however as + // we also acquire the visible objects they refer to viz. `s.visible_objects` + for (element *s : this->states_in_sequence) s->owner = this; +} + +state_sequence::state_sequence(std::vector &&initial_objects) + : visible_objects(std::move(initial_objects)) { + this->push_state_snapshot(); +} + +state_sequence::state_sequence(append_only &&ao) + : visible_objects(std::move(ao)) { + this->push_state_snapshot(); +} + +void state_sequence::push_state_snapshot() { + this->states_in_sequence.push_back(new element(this)); +} + +bool state_sequence::is_runner(objid_t id) const { + return this->get_representative_state().is_runner(id); +} + +state::objid_t state_sequence::get_objid_for_runner(runner_id_t id) const { + return this->get_representative_state().get_objid_for_runner(id); +} + +runner_id_t state_sequence::get_runner_id_for_obj(objid_t id) const { + return this->get_representative_state().get_runner_id_for_obj(id); +} + +bool state_sequence::contains_object_with_id(objid_t id) const { + return this->get_representative_state().contains_object_with_id(id); +} + +bool state_sequence::contains_runner_with_id(runner_id_t id) const { + return this->get_representative_state().contains_runner_with_id(id); +} + +const visible_object_state *state_sequence::get_state_of_object( + objid_t id) const { + return this->get_representative_state().get_state_of_object(id); +} + +const runner_state *state_sequence::get_state_of_runner(runner_id_t id) const { + return this->get_representative_state().get_state_of_runner(id); +} + +state::objid_t state_sequence::add_object(const visible_object_state *s) { + // INVARIANT: The current element needs to update at index `id` to reflect + // this new object, as this element effectively represents this state + objid_t const id = visible_objects.size(); + this->get_representative_state().point_to_next_state_for(id); + visible_objects.push_back(s); + return id; +} + +state::runner_id_t state_sequence::add_runner(const runner_state *s) { + const objid_t runner_objid = this->add_object(s); + const runner_id_t next_runner_id = this->runner_to_obj_map.size(); + this->runner_to_obj_map.insert({next_runner_id, runner_objid}); + this->get_representative_state().record_new_runner(); + return next_runner_id; +} + +void state_sequence::add_state_for_obj(objid_t id, + const visible_object_state *new_state) { + if (id == invalid_objid) { + throw std::runtime_error( + "Attempted to insert a state for an invalid object id"); + } + // INVARIANT: The current element needs to update at index `id` to reflect + // this new state, as this element effectively represents this state + this->get_representative_state().point_to_next_state_for(id); + this->visible_objects.at(id).push_state(new_state); +} + +void state_sequence::add_state_for_runner(runner_id_t id, + const runner_state *new_state) { + objid_t const objid = this->get_objid_for_runner(id); + if (objid == invalid_objid) { + throw std::runtime_error( + "Attempted to insert a state for a runner that does not exist in " + "this state."); + } + this->add_state_for_obj(objid, new_state); +} + +void state_sequence::consume_into_subsequence(size_t num_states) { + if (num_states >= this->states_in_sequence.size()) return; + extensions::delete_all(this->states_in_sequence.begin() + num_states, + this->states_in_sequence.end()); + this->states_in_sequence.erase(this->states_in_sequence.begin() + num_states, + this->states_in_sequence.end()); + assert(this->states_in_sequence.size() == num_states); + + const element ¤t_state = this->get_representative_state(); + for (objid_t i = 0; i < this->visible_objects.size(); i++) { + if (current_state.contains_object_with_id(i)) { + const size_t idx = current_state.visible_object_indices.at(i); + this->visible_objects.at(i).slice(idx); + } else { + this->visible_objects.at(i).slice(0); + } + } +} + +size_t state_sequence::count() const { + return this->get_representative_state().count(); +} + +size_t state_sequence::runner_count() const { + return this->get_representative_state().runner_count(); +} + +const state &state_sequence::state_at(size_t i) const { + return *this->states_in_sequence.at(i); +} + +const state &state_sequence::front() const { + return *this->states_in_sequence.at(0); +} + +const state &state_sequence::back() const { + return *this->states_in_sequence.back(); +} + +size_t state_sequence::get_num_states_in_sequence() const { + return this->states_in_sequence.size(); +} + +state_sequence::~state_sequence() { + // The elements have been dynamically allocated + for (const element *element : this->states_in_sequence) delete element; +} + +//////// state_sequence::element /////// + +state_sequence::element::element(const state_sequence *owner) : owner(owner) { + for (objid_t i = 0; i < owner->visible_objects.size(); i++) { + const size_t idx = owner->visible_objects.at(i).get_last_state_index(); + this->visible_object_indices[i] = idx; + } + + this->max_visible_runner_id = owner->runner_to_obj_map.size(); +} + +runner_id_t state_sequence::element::get_runner_id_for_obj(objid_t id) const { + return this->is_runner(id) ? owner->runner_to_obj_map.range_at(id) + : model::invalid_rid; +} + +bool state_sequence::element::is_runner(objid_t id) const { + return this->contains_object_with_id(id) && + owner->runner_to_obj_map.count_range(id) > 0; +} + +state::objid_t state_sequence::element::get_objid_for_runner( + runner_id_t id) const { + return this->contains_runner_with_id(id) ? owner->runner_to_obj_map.at(id) + : model::invalid_objid; +} + +bool state_sequence::element::contains_object_with_id(state::objid_t id) const { + return id < this->visible_object_indices.size(); +} + +bool state_sequence::element::contains_runner_with_id( + state::runner_id_t id) const { + return id < max_visible_runner_id && + owner->runner_to_obj_map.count_domain(id) > 0; +} + +const visible_object_state *state_sequence::element::get_state_of_object( + state::objid_t id) const { + return owner->visible_objects.at(id).state_at( + this->visible_object_indices.at(id)); +} + +const runner_state *state_sequence::element::get_state_of_runner( + runner_id_t id) const { + return static_cast( + this->get_state_of_object(this->get_objid_for_runner(id))); +} + +std::unique_ptr state_sequence::element::mutable_clone() const { + throw std::runtime_error( + "TODO: Implement state cloning for state_sequence::element"); +} + +std::string state_sequence::element::debug_string() const { + std::ostringstream ss; + + std::map vobjs(this->visible_object_indices.begin(), + this->visible_object_indices.end()); + + for (const auto &os : vobjs) { + ss << "Object " << std::to_string(os.first); + if (is_runner(os.first)) { + ss << " (runner [" << std::boolalpha + << this->get_state_of_runner(this->get_runner_id_for_obj(os.first)) + ->is_active() + << std::noboolalpha << "])"; + } + ss << ": " + << owner->visible_objects.at(os.first).state_at(os.second)->to_string() + << "\n"; + } + return ss.str(); +} + +std::unique_ptr state_sequence::mutable_clone() const { + throw std::runtime_error("TODO: Implement state cloning for state_sequence"); +} + +transition::status state_sequence::follow(const transition &t) { + auto result = t.apply_to(*this); + if (result.second == model::transition::status::exists) { + for (auto &new_state : result.first.new_object_states) + this->visible_objects[new_state.first].push_state( + new_state.second.consume_into_current_state().release()); + this->push_state_snapshot(); + } + return result.second; +} diff --git a/dmtcp/src/mcmini/model/transition_registry.cpp b/dmtcp/src/mcmini/model/transition_registry.cpp new file mode 100644 index 00000000..4e02687d --- /dev/null +++ b/dmtcp/src/mcmini/model/transition_registry.cpp @@ -0,0 +1,34 @@ +#include "mcmini/model/transition_registry.hpp" + +#include "mcmini/model/transitions/condition_variables/callbacks.hpp" +#include "mcmini/model/transitions/mutex/callbacks.hpp" +#include "mcmini/model/transitions/process/callbacks.hpp" +#include "mcmini/model/transitions/semaphore/callbacks.hpp" +#include "mcmini/model/transitions/thread/callbacks.hpp" +#include "mcmini/spy/checkpointing/transitions.h" + +using namespace model; + +transition_registry transition_registry::default_registry() { + transition_registry tr; + tr.register_transition(MUTEX_INIT_TYPE, &mutex_init_callback); + tr.register_transition(MUTEX_LOCK_TYPE, &mutex_lock_callback); + tr.register_transition(MUTEX_UNLOCK_TYPE, &mutex_unlock_callback); + tr.register_transition(THREAD_CREATE_TYPE, &thread_create_callback); + tr.register_transition(THREAD_EXIT_TYPE, &thread_exit_callback); + tr.register_transition(THREAD_JOIN_TYPE, &thread_join_callback); + tr.register_transition(PROCESS_EXIT_TYPE, &process_exit_callback); + tr.register_transition(PROCESS_ABORT_TYPE, &process_abort_callback); + tr.register_transition(COND_INIT_TYPE, &cond_init_callback); + tr.register_transition(COND_ENQUEUE_TYPE, + &cond_waiting_thread_enqueue_callback); + tr.register_transition(COND_WAIT_TYPE, &cond_wait_callback); + tr.register_transition(COND_SIGNAL_TYPE, &cond_signal_callback); + tr.register_transition(COND_BROADCAST_TYPE, &cond_broadcast_callback); + tr.register_transition(COND_DESTROY_TYPE, &cond_destroy_callback); + tr.register_transition(SEM_INIT_TYPE, &sem_init_callback); + tr.register_transition(SEM_DESTROY_TYPE, &sem_destroy_callback); + tr.register_transition(SEM_POST_TYPE, &sem_post_callback); + tr.register_transition(SEM_WAIT_TYPE, &sem_wait_callback); + return tr; +} diff --git a/dmtcp/src/mcmini/model/transition_sequence.cpp b/dmtcp/src/mcmini/model/transition_sequence.cpp new file mode 100644 index 00000000..72ccbbfc --- /dev/null +++ b/dmtcp/src/mcmini/model/transition_sequence.cpp @@ -0,0 +1,29 @@ +#include "mcmini/model/transitions/transition_sequence.hpp" + +#include +#include +#include + +#include "mcmini/misc/extensions/memory.hpp" +#include "mcmini/model/transition.hpp" + +using namespace model; + +void transition_sequence::consume_into_subsequence(uint32_t depth) { + // For depths greater than the size of the sequence, this method has no + // effect. + if (depth <= contents.size()) { + extensions::delete_all(contents.begin() + depth, contents.end()); + contents.erase(contents.begin() + depth, contents.end()); + } +} + +std::unique_ptr transition_sequence::extract_at(size_t i) { + auto result = std::unique_ptr(this->contents.at(i)); + this->contents.at(i) = nullptr; + return result; +} + +transition_sequence::~transition_sequence() { + extensions::delete_all(contents.begin(), contents.end()); +} diff --git a/dmtcp/src/mcmini/model/transitions/condition_variables.cpp b/dmtcp/src/mcmini/model/transitions/condition_variables.cpp new file mode 100644 index 00000000..c5af6d7a --- /dev/null +++ b/dmtcp/src/mcmini/model/transitions/condition_variables.cpp @@ -0,0 +1,118 @@ +#include "mcmini/mem.h" +#include "mcmini/model/exception.hpp" +#include "mcmini/model/transitions/condition_variables/callbacks.hpp" +#include "../include/mcmini/misc/cond/cond_var_arbitrary_policy.hpp" + +using namespace model; +using namespace objects; + +model::transition* cond_init_callback(runner_id_t p, + const volatile runner_mailbox& rmb, + model_to_system_map& m) { + // Fetch the remote object + pthread_cond_t* remote_cond; + memcpy_v(&remote_cond, (volatile void*)rmb.cnts, sizeof(pthread_cond_t*)); + + // Locate the corresponding model of this object + if (!m.contains(remote_cond)) { + // FIXME: Allow dynamic selection of wakeup policies. + // For now, we hard-code it here. Not great, but at least + // we can change it relatively easily still + ConditionVariablePolicy* policy = new ConditionVariableArbitraryPolicy(); + m.observe_object(remote_cond, + new condition_variable( + condition_variable::state::cv_initialized, policy)); + } + + state::objid_t const cond = m.get_model_of_object(remote_cond); + return new transitions::condition_variable_init(p, cond); +} + +model::transition* cond_waiting_thread_enqueue_callback(runner_id_t p, + const volatile runner_mailbox& rmb, + model_to_system_map& m){ + pthread_cond_t* remote_cond; + pthread_mutex_t* remote_mut; + memcpy_v(&remote_cond, (volatile void*)rmb.cnts, sizeof(pthread_cond_t*)); + memcpy_v(&remote_mut, (volatile void*)(rmb.cnts + sizeof(pthread_cond_t*)), sizeof(pthread_mutex_t*)); + + if (!m.contains(remote_cond)) + throw undefined_behavior_exception( + "Attempting to wait on an uninitialized condition variable"); + + if (!m.contains(remote_mut)) + throw undefined_behavior_exception( + "Attempting to wait on a condition variable with an uninitialized mutex"); + + state::objid_t const cond = m.get_model_of_object(remote_cond); + state::objid_t const mut = m.get_model_of_object(remote_mut); + return new transitions::condition_variable_enqueue_thread(p, cond, mut); +} + +model::transition* cond_wait_callback(runner_id_t p, + const volatile runner_mailbox& rmb, + model_to_system_map& m) { + pthread_cond_t* remote_cond; + pthread_mutex_t* remote_mut; + memcpy_v(&remote_cond, (volatile void*)rmb.cnts, sizeof(pthread_cond_t*)); + memcpy_v(&remote_mut, (volatile void*)(rmb.cnts + sizeof(pthread_cond_t*)), sizeof(pthread_mutex_t*)); + + // Locate the corresponding model of this object + if (!m.contains(remote_cond)) + throw undefined_behavior_exception( + "Attempting to wait on an uninitialized condition variable"); + + if (!m.contains(remote_mut)) + throw undefined_behavior_exception( + "Attempting to wait on a condition variable with an uninitialized mutex"); + + + state::objid_t const cond = m.get_model_of_object(remote_cond); + state::objid_t const mut = m.get_model_of_object(remote_mut); + return new transitions::condition_variable_wait(p, cond, mut); +} + +model::transition* cond_signal_callback(runner_id_t p, + const volatile runner_mailbox& rmb, + model_to_system_map& m) { + pthread_cond_t* remote_cond; + memcpy_v(&remote_cond, (volatile void*)rmb.cnts, sizeof(pthread_cond_t*)); + + // Locate the corresponding model of this object + if (!m.contains(remote_cond)) + throw undefined_behavior_exception( + "Attempting to signal an uninitialized condition variable"); + + state::objid_t const cond = m.get_model_of_object(remote_cond); + return new transitions::condition_variable_signal(p, cond); +} + +model::transition* cond_broadcast_callback(runner_id_t p, + const volatile runner_mailbox& rmb, + model_to_system_map& m) { + pthread_cond_t* remote_cond; + memcpy_v(&remote_cond, (volatile void*)rmb.cnts, sizeof(pthread_cond_t*)); + + // Locate the corresponding model of this object + if (!m.contains(remote_cond)) + throw undefined_behavior_exception( + "Attempting to broadcast on an uninitialized condition variable"); + + state::objid_t const cond = m.get_model_of_object(remote_cond); + return new transitions::condition_variable_broadcast(p, cond); +} + +model::transition* cond_destroy_callback(runner_id_t p, + const volatile runner_mailbox& rmb, + model_to_system_map& m) { + pthread_cond_t* remote_cond; + memcpy_v(&remote_cond, (volatile void*)rmb.cnts, sizeof(pthread_cond_t*)); + + // Locate the corresponding model of this object + if (!m.contains(remote_cond)) + throw undefined_behavior_exception( + "Attempting to destroy an uninitialized condition variable"); + + state::objid_t const cond = m.get_model_of_object(remote_cond); + return new transitions::condition_variable_destroy(p, cond); +} diff --git a/dmtcp/src/mcmini/model/transitions/mutex.cpp b/dmtcp/src/mcmini/model/transitions/mutex.cpp new file mode 100644 index 00000000..1757a7e7 --- /dev/null +++ b/dmtcp/src/mcmini/model/transitions/mutex.cpp @@ -0,0 +1,51 @@ +#include "mcmini/mem.h" +#include "mcmini/model/exception.hpp" +#include "mcmini/model/transitions/mutex/callbacks.hpp" + +using namespace model; +using namespace objects; + +model::transition* mutex_init_callback(runner_id_t p, + const volatile runner_mailbox& rmb, + model_to_system_map& m) { + // Fetch the remote object + pthread_mutex_t* remote_mut; + memcpy_v(&remote_mut, (volatile void*)rmb.cnts, sizeof(pthread_mutex_t*)); + + // Locate the corresponding model of this object + if (!m.contains(remote_mut)) + m.observe_object(remote_mut, new mutex(mutex::state::uninitialized)); + + state::objid_t const mut = m.get_model_of_object(remote_mut); + return new transitions::mutex_init(p, mut); +} + +model::transition* mutex_lock_callback(runner_id_t p, + const volatile runner_mailbox& rmb, + model_to_system_map& m) { + pthread_mutex_t* remote_mut; + memcpy_v(&remote_mut, (volatile void*)rmb.cnts, sizeof(pthread_mutex_t*)); + + // TODO: add code from Gene's PR here + if (!m.contains(remote_mut)) + throw undefined_behavior_exception( + "Attempting to lock an uninitialized mutex"); + + state::objid_t const mut = m.get_model_of_object(remote_mut); + return new transitions::mutex_lock(p, mut); +} + +model::transition* mutex_unlock_callback(runner_id_t p, + const volatile runner_mailbox& rmb, + model_to_system_map& m) { + pthread_mutex_t* remote_mut; + memcpy_v(&remote_mut, (volatile void*)rmb.cnts, sizeof(pthread_mutex_t*)); + + // TODO: add code from Gene's PR here + if (!m.contains(remote_mut)) + throw undefined_behavior_exception( + "Attempting to lock an uninitialized mutex"); + + state::objid_t const mut = m.get_model_of_object(remote_mut); + return new transitions::mutex_unlock(p, mut); +} diff --git a/dmtcp/src/mcmini/model/transitions/process.cpp b/dmtcp/src/mcmini/model/transitions/process.cpp new file mode 100644 index 00000000..9f973492 --- /dev/null +++ b/dmtcp/src/mcmini/model/transitions/process.cpp @@ -0,0 +1,20 @@ +#include "mcmini/mem.h" +#include "mcmini/model/transitions/process/abort.hpp" +#include "mcmini/model/transitions/process/callbacks.hpp" +#include "mcmini/model/transitions/process/exit.hpp" + +using namespace model; + +transition* process_exit_callback(runner_id_t id, + const volatile runner_mailbox& mb, + model_to_system_map& map) { + int exit_code = -1; + memcpy_v(&exit_code, (volatile void*)mb.cnts, sizeof(exit_code)); + return new transitions::process_exit(id, exit_code); +} + +transition* process_abort_callback(runner_id_t id, + const volatile runner_mailbox& mb, + model_to_system_map& map) { + return new transitions::process_abort(id); +} diff --git a/dmtcp/src/mcmini/model/transitions/semaphore.cpp b/dmtcp/src/mcmini/model/transitions/semaphore.cpp new file mode 100644 index 00000000..aa509d67 --- /dev/null +++ b/dmtcp/src/mcmini/model/transitions/semaphore.cpp @@ -0,0 +1,70 @@ +#include "mcmini/mem.h" +#include "mcmini/model/exception.hpp" +#include "mcmini/model/transitions/semaphore/callbacks.hpp" +#include "mcmini/model/transitions/semaphore/sem_init.hpp" +#include "mcmini/model/transitions/semaphore/sem_post.hpp" +#include "mcmini/model/transitions/semaphore/sem_wait.hpp" + +using namespace model; +using namespace objects; + +model::transition* sem_init_callback(runner_id_t p, + const volatile runner_mailbox& rmb, + model_to_system_map& m) { + // Fetch the remote object + int count; + sem_t* remote_sem; + memcpy_v(&remote_sem, (volatile void*)rmb.cnts, sizeof(sem_t*)); + memcpy_v(&count, (volatile void*)(rmb.cnts + sizeof(sem_t*)), sizeof(int)); + + // Locate the corresponding model of this object + if (!m.contains(remote_sem)) m.observe_object(remote_sem, new semaphore()); + + const state::objid_t model_sem = m.get_model_of_object(remote_sem); + return new transitions::sem_init(p, model_sem, count); +} + +model::transition* sem_post_callback(runner_id_t p, + const volatile runner_mailbox& rmb, + model_to_system_map& m) { + sem_t* remote_sem; + memcpy_v(&remote_sem, (volatile void*)rmb.cnts, sizeof(sem_t*)); + + // TODO: Add code from Gene's PR here to check for initialized semaphores + if (!m.contains(remote_sem)) + throw undefined_behavior_exception( + "Attempting to post to an uninitialized semaphore"); + + const state::objid_t model_sem = m.get_model_of_object(remote_sem); + return new transitions::sem_post(p, model_sem); +} + +model::transition* sem_wait_callback(runner_id_t p, + const volatile runner_mailbox& rmb, + model_to_system_map& m) { + sem_t* remote_sem; + memcpy_v(&remote_sem, (volatile void*)rmb.cnts, sizeof(sem_t*)); + + // TODO: Add code from Gene's PR here to check for initialized semaphores + if (!m.contains(remote_sem)) + throw undefined_behavior_exception( + "Attempting to wait on an uninitialized semaphore"); + + const state::objid_t model_sem = m.get_model_of_object(remote_sem); + return new transitions::sem_wait(p, model_sem); +} + +model::transition* sem_destroy_callback(runner_id_t p, + const volatile runner_mailbox& rmb, + model_to_system_map& m) { + sem_t* remote_sem; + memcpy_v(&remote_sem, (volatile void*)rmb.cnts, sizeof(sem_t*)); + + if (!m.contains(remote_sem)) + throw undefined_behavior_exception( + "Attempting to wait on an uninitialized semaphore"); + + // const state::objid_t model_sem = m.get_model_of_object(remote_sem); + // return new transitions::sem_destroy(p, model_sem); + return nullptr; // TODO: Implement sem_destroy in the model +} diff --git a/dmtcp/src/mcmini/model/transitions/thread.cpp b/dmtcp/src/mcmini/model/transitions/thread.cpp new file mode 100644 index 00000000..91783f3a --- /dev/null +++ b/dmtcp/src/mcmini/model/transitions/thread.cpp @@ -0,0 +1,42 @@ +#include "mcmini/mem.h" +#include "mcmini/model/exception.hpp" +#include "mcmini/model/transitions/thread/callbacks.hpp" + +using namespace model; +using namespace objects; + +transition* thread_create_callback(runner_id_t p, + const volatile runner_mailbox& rmb, + model_to_system_map& m) { + pthread_t new_thread; + memcpy_v(&new_thread, static_cast(&rmb.cnts), + sizeof(pthread_t)); + if (!m.contains_runner((void*)new_thread)) + m.observe_runner( + (void*)new_thread, new thread(thread::embryo), + [](runner_id_t id) { return new transitions::thread_start(id); }); + const runner_id_t new_thread_id = m.get_model_of_runner((void*)new_thread); + return new transitions::thread_create(p, new_thread_id); +} + +transition* thread_exit_callback(runner_id_t p, + const volatile runner_mailbox& rmb, + model_to_system_map& m) { + return new transitions::thread_exit(p); +} + +transition* thread_join_callback(runner_id_t p, + const volatile runner_mailbox& rmb, + model_to_system_map& m) { + runner_id_t target_id; + memcpy_v(&target_id, static_cast(&rmb.cnts), + sizeof(runner_id_t)); + // const state::runner_id_t target_id = m.get_model_of_runner((void*)target); + if (target_id == model::invalid_rid) { + std::stringstream ss; + ss << "Attemping to join on an unmodeled thread (" << std::hex << target_id + << ") unrecognized"; + throw std::runtime_error(ss.str()); + } + return new transitions::thread_join(p, target_id); +} diff --git a/dmtcp/src/mcmini/model_checking/algorithms/classic_dpor.cpp b/dmtcp/src/mcmini/model_checking/algorithms/classic_dpor.cpp new file mode 100644 index 00000000..2504f213 --- /dev/null +++ b/dmtcp/src/mcmini/model_checking/algorithms/classic_dpor.cpp @@ -0,0 +1,486 @@ +#include "mcmini/model_checking/algorithms/classic_dpor.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mcmini/coordinator/coordinator.hpp" +#include "mcmini/defines.h" +#include "mcmini/log/logger.hpp" +#include "mcmini/model/exception.hpp" +#include "mcmini/model/program.hpp" +#include "mcmini/model/transition.hpp" +#include "mcmini/model/transitions/condition_variables/callbacks.hpp" +#include "mcmini/model/transitions/mutex/callbacks.hpp" +#include "mcmini/model/transitions/mutex/mutex_init.hpp" +#include "mcmini/model/transitions/semaphore/callbacks.hpp" +#include "mcmini/model/transitions/thread/callbacks.hpp" +#include "mcmini/model_checking/algorithms/classic_dpor/clock_vector.hpp" +#include "mcmini/model_checking/algorithms/classic_dpor/runner_item.hpp" +#include "mcmini/model_checking/algorithms/classic_dpor/stack_item.hpp" +#include "mcmini/real_world/process.hpp" + +using namespace model; +using namespace logging; +using namespace model_checking; + +logger dpor_logger("dpor"); + +struct classic_dpor::dpor_context { + ::coordinator &coordinator; + std::vector stack; + + dpor_context(::coordinator &c) : coordinator(c) {} + + const transition *get_transition(int i) const { + return stack.at(i).get_out_transition(); + } +}; + +clock_vector classic_dpor::accumulate_max_clock_vector_against( + const model::transition &t, const dpor_context &context) const { + // The last state in the stack does NOT have an out transition, hence the + // `nullptr` check. Note that `s_i.get_out_transition()` refers to `S_i` + // (case-sensitive) in the paper, viz. the transition between states `s_i` and + // `s_{i+1}`. + clock_vector result; + for (const stack_item &s_i : context.stack) { + if (s_i.get_out_transition() != nullptr && + this->are_dependent(*s_i.get_out_transition(), t)) { + result = clock_vector::max(result, s_i.get_clock_vector()); + } + } + return result; +} + +bool classic_dpor::are_coenabled(const model::transition &t1, + const model::transition &t2) const { + return t1.get_executor() != t2.get_executor() && + this->config.coenabled_relation.call_or(true, &t1, &t2); +} + +bool classic_dpor::are_dependent(const model::transition &t1, + const model::transition &t2) const { + return t1.get_executor() == t2.get_executor() || + this->config.dependency_relation.call_or(true, &t1, &t2); +} + +bool classic_dpor::happens_before(const dpor_context &context, size_t i, + size_t j) const { + const runner_id_t rid = + context.stack.at(i).get_out_transition()->get_executor(); + const clock_vector &cv = context.stack.at(j).get_clock_vector(); + return i <= cv.value_for(rid); +} + +bool classic_dpor::happens_before_thread(const dpor_context &context, size_t i, + runner_id_t p) const { + const runner_id_t rid = context.get_transition(i)->get_executor(); + const clock_vector &cv = + context.stack.back().get_per_runner_clocks()[p].get_clock_vector(); + return i <= cv.value_for(rid); +} + +bool classic_dpor::threads_race_after(const dpor_context &context, size_t i, + runner_id_t q, runner_id_t p) const { + const size_t transition_stack_height = context.stack.size() - 1; + for (size_t j = i + 1; j < transition_stack_height; j++) { + if (q == context.get_transition(j)->get_executor() && + this->happens_before_thread(context, j, p)) + return true; + } + return false; +} + +void classic_dpor::verify_using(coordinator &coordinator, + const callbacks &callbacks) { + // The code below is an implementation of the model-checking algorithm of + // Flanagan and Godefroid from 2015. + + // 1. Data structure set up + + /// @invariant: The number of items in the DPOR-specific stack is the same + /// size as the number of transitions in the current trace plus one. + /// + /// The initial entry into the stack represents the information DPOR tracks + /// for state `s_0`. + log_debug(dpor_logger) << "Initializing the DPOR stack"; + stats model_checking_stats; + dpor_context context(coordinator); + auto &dpor_stack = context.stack; + dpor_stack.emplace_back( + clock_vector(), + coordinator.get_current_program_model().get_enabled_runners()); + log_very_verbose(dpor_logger) + << "Initial enabled runners: " + << coordinator.get_current_program_model().get_enabled_runners(); + + while (!dpor_stack.empty()) { + // 2. Exploration phase + while (dpor_stack.back().has_enabled_runners()) { + if (dpor_stack.size() >= this->config.maximum_total_execution_depth) { + throw std::runtime_error( + "*** Execution Limit Reached! ***\n\n" + "McMini ran a trace with" + + std::to_string(dpor_stack.size()) + + " transitions which is\n" + "the more than McMini was configured to handle in any one trace (" + + std::to_string(this->config.maximum_total_execution_depth) + + "). Try running mcmini with the \"--max-depth-per-thread\" flag\n" + "to limit how far into a trace McMini can go\n"); + } + // NOTE: For deterministic results, always choose the "first" enabled + // runner. A runner precedes another runner in being enabled iff it has a + // smaller id. + try { + const runner_id_t rid = dpor_stack.back().get_first_enabled_runner(); + this->continue_dpor_by_expanding_trace_with(rid, context); + model_checking_stats.total_transitions++; + + // Now ask the question: will the next operation of this thread + // cause the program to exit or abort abnormally? + // + // If so, stop expanding the trace and backtrack. The rationale is that + // any extension of this trace will eventually show the same bug anyway + // (the transition claims to cause the program to abort), and smaller + // traces are more understandable. + const transition *rtransition = + coordinator.get_current_program_model().get_pending_transition_for( + rid); + if (rtransition->aborts_program_execution()) { + throw real_world::process::termination_error(SIGABRT, rid, + "The program aborted"); + } else if (rtransition->program_exit_code() > 0) { + throw real_world::process::nonzero_exit_code_error( + rtransition->program_exit_code(), rid, + "The program exited abnormally"); + } + } catch (const model::undefined_behavior_exception &ube) { + if (callbacks.undefined_behavior) + callbacks.undefined_behavior(coordinator, model_checking_stats, ube); + return; + } catch (const real_world::process::termination_error &te) { + if (callbacks.abnormal_termination) + callbacks.abnormal_termination(coordinator, model_checking_stats, te); + return; + } catch (const real_world::process::nonzero_exit_code_error &nzec) { + if (callbacks.nonzero_exit_code) + callbacks.nonzero_exit_code(coordinator, model_checking_stats, nzec); + return; + } + } + + if (callbacks.trace_completed) + callbacks.trace_completed(coordinator, model_checking_stats); + + if (callbacks.deadlock && + coordinator.get_current_program_model().is_in_deadlock()) + callbacks.deadlock(coordinator, model_checking_stats); + + // 3. Backtrack phase + while (!dpor_stack.empty() && dpor_stack.back().backtrack_set_empty()) + dpor_stack.pop_back(); + + if (!dpor_stack.empty()) { + // At this point, the model checker's data structures are valid for + // `dpor_stack.size()` states; however, the model and the associated + // process(es) that the model represent do not yet correspond after + // backtracking. + log_debug(dpor_logger) + << "Backtracking to depth `" << (dpor_stack.size() - 1) << "`"; + coordinator.return_to_depth(dpor_stack.size() - 1); + log_debug(dpor_logger) << "Finished backtracking to depth `" + << (dpor_stack.size() - 1) << "`"; + model_checking_stats.trace_id++; + + // The first step of the NEXT exploration phase begins with following + // one of the backtrack threads. Select one thread to backtrack upon and + // follow it before continuing onto the exploration phase. + try { + this->continue_dpor_by_expanding_trace_with( + dpor_stack.back().backtrack_set_pop_first(), context); + } catch (const model::undefined_behavior_exception &ube) { + if (callbacks.undefined_behavior) + callbacks.undefined_behavior(coordinator, model_checking_stats, ube); + return; + } + } + } +} + +void classic_dpor::continue_dpor_by_expanding_trace_with( + runner_id_t p, dpor_context &context) { + log_debug(dpor_logger) << "DPOR selected `" << p << "` to explore"; + context.coordinator.execute_runner(p); + log_debug(dpor_logger) << "DPOR expanded following `" << p << "`"; + this->grow_stack_after_running(context); + this->dynamically_update_backtrack_sets(context); +} + +void classic_dpor::grow_stack_after_running(dpor_context &context) { + // In this method, the following invariants are assumed to hold: + // + // 1. `n` := `stack.size()`. + // 2. `t_n` is the `n`th transition executed in the model of `coordinator`. + // This transition *has already executed in the model.* THIS IS VERY + // IMPORTANT as the DPOR information tracking below relies on this heavily. + // + // After this method is executed, the stack will have size `n + 1`. Each entry + // will correspond to the information DPOR cares about for each state in the + // `coordinator`'s state sequence. + const coordinator &coordinator = context.coordinator; + assert(coordinator.get_depth_into_program() == context.stack.size()); + const model::transition *t_n = + coordinator.get_current_program_model().get_trace().back(); + + // NOTE: `cv` corresponds to line 14.3 of figure 4 in the DPOR paper. + clock_vector cv = accumulate_max_clock_vector_against(*t_n, context); + + // NOTE: The assignment corresponds to line 14.4 of figure 4. Here `S'` + // represents the transition sequence _after_ `t_n` has executed. Since the + // `coordinator` already contains this transition (recall the invariants in + // the comment at the start of this function), `S' == + // `coordinator.get_current_program_model().get_trace()` and thus `|S'| == + // corodinator.get_depth_into_program(). + cv[t_n->get_executor()] = coordinator.get_depth_into_program(); + + // NOTE: This line corresponds to line 14.5 of figure 4. Here, C' is + // conceptually captured through the DPOR stack and the per-thread DPOR data. + // The former contains the per-state clock vectors while the latter the + // per-thread clock vectors (among other data). + auto s_n_per_runner_clocks = context.stack.back().get_per_runner_clocks(); + s_n_per_runner_clocks[t_n->get_executor()].set_clock_vector(cv); + context.stack.emplace_back( + cv, std::move(s_n_per_runner_clocks), t_n, + coordinator.get_current_program_model().get_enabled_runners()); + stack_item &s_n = context.stack.at(context.stack.size() - 2); + stack_item &s_n_plus_1 = context.stack.back(); + + // INVARIANT: For each thread `p`, if such a thread is contained + // in the sleep set of `s_n`, then `next(s_n, p)` MUST be the transition + // that would be contained in that sleep set. + for (const runner_id_t &rid : s_n.get_sleep_set()) { + const model::transition *rid_next = + coordinator.get_current_program_model().get_pending_transition_for(rid); + if (this->are_independent(*rid_next, *t_n)) + s_n_plus_1.insert_into_sleep_set(rid); + } + + // `t_n` is inserted into the sleep set AFTER execution. This is how sleep + // sets work (see papers etc.) + s_n.set_out_transition(t_n); + s_n.insert_into_sleep_set(t_n->get_executor()); + s_n.mark_searched(t_n->get_executor()); +} + +void classic_dpor::dynamically_update_backtrack_sets(dpor_context &context) { + /* + * Updating the backtrack sets is accomplished as follows + * (under the given assumptions) + * + * ASSUMPTIONS + * + * 1. The state reflects last(S) for the transition stack + * + * 2. The thread that ran last is at the top of the transition + * stack (this should always be true) + * + * 3. The next transition for the thread that ran the most + * recent transition in the transition stack (the transition at the + * top of the stack) has been properly updated to reflect what that + * thread will do next + * + * WLOG, assume there are `n` transitions in the transition stack + * and `k` threads that are known to exist at the time of updating + * the backtrack sets. Note this implies that there are `n+1` items + * in the state stack (since there is always the initial state + 1 + * for every subsequent transition thereafter) + * + * Let + * S_i = ith backtracking state item + * T_i = ith transition + * N_p = the next transition for thread p (next(s, p)) + * + * ALGORITHM: + * + * 1. First, get a reference to the transition at the top + * of the transition stack (i.e. the most recent transition) + * as well as the thread that ran that transition. WLOG suppose that + * thread has a thread id `i`. + * + * This transition will be used to test against the transitions + * queued as running "next" for all of the **other** threads + * that exist + * + * 2. Test whether a backtrack point is needed at state + * S_n for the other threads by comparing N_p, for all p != i. + * + * 3. Get a reference to N_i and traverse the transition stack + * to determine if a backtrack point is needed anywhere for + * thread `i` + */ + const coordinator &coordinator = context.coordinator; + const size_t num_threads = + coordinator.get_current_program_model().get_num_runners(); + + std::set thread_ids; + for (runner_id_t i = 0; i < num_threads; i++) thread_ids.insert(i); + + const ssize_t t_stack_top = (ssize_t)(context.stack.size()) - 2; + const runner_id_t last_runner_to_execute = + coordinator.get_current_program_model() + .get_trace() + .back() + ->get_executor(); + thread_ids.erase(last_runner_to_execute); + + // O(# threads) + { + const model::transition &S_n = + *coordinator.get_current_program_model().get_trace().back(); + + for (runner_id_t rid : thread_ids) { + const model::transition &next_sp = + *coordinator.get_current_program_model() + .get_pending_transitions() + .get_transition_for_runner(rid); + dynamically_update_backtrack_sets_at_index(context, S_n, next_sp, + context.stack.at(t_stack_top), + t_stack_top, rid); + } + } + + // O(transition stack size) + { + const model::transition &next_s_p_for_latest_runner = + *coordinator.get_current_program_model() + .get_pending_transitions() + .get_transition_for_runner(last_runner_to_execute); + + // It only remains to add backtrack points at the necessary + // points for thread `last_runner_to_execute`. We start at one step elow the + // top since we know that transition to not be co-enabled (since it was, by + // assumption, run by `last_runner_to_execute`) + for (int i = t_stack_top - 1; i >= 0; i--) { + const model::transition &S_i = + *coordinator.get_current_program_model().get_trace().at(i); + const bool should_stop = dynamically_update_backtrack_sets_at_index( + context, S_i, next_s_p_for_latest_runner, context.stack.at(i), i, + last_runner_to_execute); + /* + * Stop when we find the _first_ such i; this + * will be the maxmimum `i` since we're searching + * backwards + */ + if (should_stop) break; + } + } +} + +bool classic_dpor::dynamically_update_backtrack_sets_at_index( + const dpor_context &context, const model::transition &S_i, + const model::transition &next_sp, stack_item &pre_si, size_t i, + runner_id_t p) { + // TODO: add in co-enabled conditions + const bool has_reversible_race = this->are_dependent(next_sp, S_i) && + this->are_coenabled(next_sp, S_i) && + !this->happens_before_thread(context, i, p); + + // If there exists i such that ... + if (has_reversible_race) { + std::set e; + + for (runner_id_t const q : pre_si.get_enabled_runners()) { + const bool in_e = q == p || this->threads_race_after(context, i, q, p); + + // If E != empty set + if (in_e && !pre_si.sleep_set_contains(q)) e.insert(q); + } + + if (e.empty()) { + // E is the empty set -> add every enabled thread at pre(S, i) + for (runner_id_t const q : pre_si.get_enabled_runners()) + if (!pre_si.sleep_set_contains(q)) + pre_si.insert_into_backtrack_set_unless_completed(q); + } else { + for (runner_id_t const q : e) { + // If there is a thread in preSi that we + // are already backtracking AND which is contained + // in the set E, chose that thread to backtrack + // on. This is equivalent to not having to do + // anything + if (pre_si.backtrack_set_contains(q)) return true; + } + + // Otherwise select an arbitrary thread to backtrack upon. + pre_si.insert_into_backtrack_set_unless_completed(*e.begin()); + } + } + return has_reversible_race; +} + +classic_dpor::dependency_relation_type classic_dpor::default_dependencies() { + classic_dpor::dependency_relation_type dr; + dr.register_dd_entry( + &transitions::thread_create::depends); + dr.register_dd_entry( + &transitions::thread_join::depends); + dr.register_dd_entry( + &transitions::mutex_lock::depends); + dr.register_dd_entry( + &transitions::mutex_lock::depends); + dr.register_dd_entry( + &transitions::condition_variable_wait::depends); + dr.register_dd_entry( + &transitions::condition_variable_wait::depends); + dr.register_dd_entry( + &transitions::condition_variable_signal::depends); + dr.register_dd_entry( + &transitions::condition_variable_signal::depends); + return dr; +} + +classic_dpor::coenabled_relation_type classic_dpor::default_coenabledness() { + classic_dpor::dependency_relation_type cr; + cr.register_dd_entry( + &transitions::thread_create::coenabled_with); + cr.register_dd_entry( + &transitions::thread_join::coenabled_with); + cr.register_dd_entry( + &transitions::mutex_lock::coenabled_with); + cr.register_dd_entry( + &transitions::condition_variable_signal::coenabled_with); + cr.register_dd_entry( + &transitions::condition_variable_signal::coenabled_with); + cr.register_dd_entry( + &transitions::condition_variable_broadcast::coenabled_with); + cr.register_dd_entry( + &transitions::condition_variable_broadcast::coenabled_with); + cr.register_dd_entry( + &transitions::condition_variable_destroy::coenabled_with); + cr.register_dd_entry( + &transitions::condition_variable_destroy::coenabled_with); + return cr; +} diff --git a/dmtcp/src/mcmini/model_checking/algorithms/clock_vector.cpp b/dmtcp/src/mcmini/model_checking/algorithms/clock_vector.cpp new file mode 100644 index 00000000..84a72eb6 --- /dev/null +++ b/dmtcp/src/mcmini/model_checking/algorithms/clock_vector.cpp @@ -0,0 +1,17 @@ +#include "mcmini/model_checking/algorithms/classic_dpor/clock_vector.hpp" + +#include + +using namespace model_checking; + +using namespace std; + +clock_vector clock_vector::max(const clock_vector &cv1, + const clock_vector &cv2) { + clock_vector max_cv; + for (const auto &e : cv2.contents) + max_cv[e.first] = std::max(cv1.value_for(e.first), e.second); + for (const auto &e : cv1.contents) + max_cv[e.first] = std::max(cv2.value_for(e.first), e.second); + return max_cv; +} diff --git a/dmtcp/src/mcmini/real_world/dmtcp_process_source.cpp b/dmtcp/src/mcmini/real_world/dmtcp_process_source.cpp new file mode 100644 index 00000000..89b79dff --- /dev/null +++ b/dmtcp/src/mcmini/real_world/dmtcp_process_source.cpp @@ -0,0 +1,76 @@ +#include "mcmini/real_world/process/dmtcp_process_source.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "mcmini/common/shm_config.h" +#include "mcmini/misc/extensions/unique_ptr.hpp" +#include "mcmini/real_world/process/local_linux_process.hpp" +#include "mcmini/real_world/process/resources.hpp" + +using namespace real_world; +using namespace extensions; + +dmtcp_process_source::dmtcp_process_source(const std::string& ckpt_file) + : ckpt_file(ckpt_file) { + this->dmtcp_restart_target = dmtcp_target( + "dmtcp_restart", {"--new-coordinator", "--port", "0"}, this->ckpt_file); + this->dmtcp_restart_target.set_env("MCMINI_NEEDS_STATE", "1"); +} + +pid_t dmtcp_process_source::make_new_branch() { + return dmtcp_restart_target.launch_dont_wait(); +} + +std::unique_ptr dmtcp_process_source::make_new_process() { + // IMPORTANT: Here, resetting the semaphores for the userspace + // threads BEFORE creating the new branch process is very important. + // Otherwise the threads from the restarted checkpoint image would + // read from the semaphores which the McMini process would overwrite. + shared_memory_region* rw_region = + xpc_resources::get_instance().get_rw_region(); + xpc_resources::get_instance().reset_binary_semaphores_for_new_branch(); + pid_t target_branch_pid = make_new_branch(); + this->dmtcp_restart_target.unset_env("MCMINI_NEEDS_STATE"); + + const volatile template_process_t* tstruct = + &(rw_region->as()->tpt); + + if (sem_wait((sem_t*)&tstruct->mcmini_process_sem) != 0) { + throw process_source::process_creation_exception( + "The template thread (in process with PID " + + std::to_string(target_branch_pid) + + ") did not synchronize correctly with the McMini process: " + + std::string(strerror(errno))); + } + + // Since there is no template process intermediary, + // the PID of the child process created with `dmtcp_restart` + // is the PID of the branch. The chain of events is + // + // `mcmini process` --> fork()/exec() --> `dmtcp_restart` + // --> exec() --> `mtcp_restart` + // + // The key detail is that `dmtcp_restart` calls `exec()` only. + // So its PID is preserved. + // assert(tstruct->cpid == target_branch_pid); + return extensions::make_unique(target_branch_pid); +} + +dmtcp_process_source::~dmtcp_process_source() { + target dmtcp_cleanup( + "dmtcp_command", + {"-q", "--port", std::to_string(this->coordinator_target.get_port())}); + dmtcp_cleanup.set_quiet(true); + dmtcp_cleanup.launch_and_wait(); +} diff --git a/dmtcp/src/mcmini/real_world/fifo.cpp b/dmtcp/src/mcmini/real_world/fifo.cpp new file mode 100644 index 00000000..c52264ba --- /dev/null +++ b/dmtcp/src/mcmini/real_world/fifo.cpp @@ -0,0 +1,41 @@ +#include "mcmini/real_world/fifo.hpp" + +#include +#include +#include +#include +#include + +#include + +using namespace real_world; + +fifo::fifo(const std::string& name) : name(name) { + if (mkfifo(name.c_str(), S_IRUSR | S_IWUSR) != 0) { + if (errno == EEXIST) { + std::cerr << "The FIFO `" << name + << "` already exists. We'll use the existing one"; + } else { + std::perror("mkfifo"); + std::exit(EXIT_FAILURE); + } + } + fd = open(name.c_str(), O_RDONLY); + if (fd == -1) { + std::perror("open"); + std::exit(EXIT_FAILURE); + } +} + +size_t fifo::read(void* buf, size_t size) const { + return ::read(fd, buf, size); +} + +size_t fifo::write(const void* buf, size_t size) const { + return ::write(fd, buf, size); +} + +fifo::~fifo() { + if (fd != -1) close(fd); + unlink(name.c_str()); +} diff --git a/dmtcp/src/mcmini/real_world/fork_process_source.cpp b/dmtcp/src/mcmini/real_world/fork_process_source.cpp new file mode 100644 index 00000000..7ef730fb --- /dev/null +++ b/dmtcp/src/mcmini/real_world/fork_process_source.cpp @@ -0,0 +1,171 @@ +#include "mcmini/real_world/process/fork_process_source.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mcmini/common/shm_config.h" +#include "mcmini/defines.h" +#include "mcmini/log/logger.hpp" +#include "mcmini/misc/extensions/unique_ptr.hpp" +#include "mcmini/real_world/mailbox/runner_mailbox.h" +#include "mcmini/real_world/process.hpp" +#include "mcmini/real_world/process/local_linux_process.hpp" +#include "mcmini/real_world/process/multithreaded_fork_process_source.hpp" +#include "mcmini/real_world/process/resources.hpp" +#include "mcmini/real_world/process/template_process.h" +#include "mcmini/real_world/process_source.hpp" +#include "mcmini/real_world/shm.hpp" +#include "mcmini/signal.hpp" + +using namespace logging; +using namespace real_world; +using namespace extensions; + +logger fps("fork"); +logger mfps("mtf"); + +fork_process_source::fork_process_source(real_world::target&& tp) + : fork_process_source(tp) {} + +fork_process_source::fork_process_source( + const real_world::target& target_program) + : template_program(extensions::make_unique(target_program)) { + this->template_program->set_preload_libmcmini(); +} + +multithreaded_fork_process_source::multithreaded_fork_process_source( + const std::string& ckpt_file) { + log_debug(mfps) << "Initialized mtf source with checkpoint file `" + << ckpt_file << "`"; + this->template_program = extensions::make_unique( + std::string("dmtcp_restart"), + std::vector{"--new-coordinator", "--port", "0"}, ckpt_file); + this->template_program->set_env("MCMINI_NEEDS_STATE", "1"); + + // Possibly a dmtcp program... + + // Here `libmcmini.so` doesn't need to be preloaded: it is assumed that + // `mcmini` is contained in the checkpoint image that is restored by + // `dmtcp_restart`. Hence we can omit + // ``` + // this->target_program.set_preload_libmcmini(); + // ``` +} + +std::unique_ptr fork_process_source::make_new_process() { + shared_memory_region* rw_region = + xpc_resources::get_instance().get_rw_region(); + + // 1. Set up phase (LD_PRELOAD, binary sempahores, template process creation) + xpc_resources::get_instance().reset_binary_semaphores_for_new_branch(); + if (!has_template_process_alive()) make_new_template_process(); + + // 2. Check if the current template process has previously exited; if so, it + // would have delivered a `SIGCHLD` to this process. By default this signal is + // ignored, but McMini explicitly captures it (see `signal_tracker`). + if (signal_tracker::instance().try_consume_signal(SIGCHLD)) { + this->template_process_handle = nullptr; + throw process_source::process_creation_exception( + "Failed to create a new process (template process died). Consider " + "using a previous checkpoint image"); + } + + // 3. If the current template process is alive, tell it to spawn a new + // process and then wait for it to successfully call `fork(2)` to tell us + // about its new child. + const volatile template_process_t* tstruct = + &(rw_region->as()->tpt); + + signal_tracker::set_sem((sem_t*)&tstruct->mcmini_process_sem); + if (sem_post((sem_t*)&tstruct->libmcmini_sem) != 0) { + throw process_source::process_creation_exception( + "The template process (" + + std::to_string(this->template_process_handle->get_pid()) + + ") was not synchronized with correctly: " + + std::string(strerror(errno))); + } + int rc = signal_tracker::sig_semwait((sem_t*)&tstruct->mcmini_process_sem); + if (rc != 0) { + throw process_source::process_creation_exception( + "The template process (" + + std::to_string(this->template_process_handle->get_pid()) + + ") was not synchronized with correctly: " + + std::string(strerror(errno))); + } + + if (signal_tracker::instance().try_consume_signal(SIGCHLD)) { + // TODO: At this point, McMini may have multiple child processes. + // Calling `prctl(PR_SETSUBREAPER)` in the branch processes means + // that McMini receives a `SIGCHLD` when both the branch process and the + // template process exit unexpectedly. If a `SIGCHLD` is delivered by + // _either_ process, we would reach this error branch. To distinguish which + // process exited unexpectedly under our feet, we would need to use + // `waitpid(-1, &status, WNOHANG)` and check the status for _each_ child. + this->template_process_handle = nullptr; + throw process_source::process_creation_exception( + "Failed to create a new process (template or [possibly the branch] " + "process died)"); + } + + if (tstruct->cpid == TEMPLATE_FORK_FAILED) { + throw process_source::process_creation_exception( + "The `fork(2)` call in the template process failed unexpectedly " + "(errno " + + std::to_string(tstruct->err) + "): " + strerror(tstruct->err)); + } + return extensions::make_unique(tstruct->cpid, false); +} + +void fork_process_source::make_new_template_process() { + // Reset first. If an exception is raised in subsequent steps, we don't want + // to erroneously think that there is a template process when indeed there + // isn't one. + this->template_program->set_is_template(); + log_verbose(fps) << "Resetting template handle"; + this->template_process_handle = nullptr; + log_verbose(fps) << "Template handle reset. Launching template program"; + this->template_process_handle = extensions::make_unique( + this->template_program->launch_dont_wait(), false); + log_verbose(fps) << "Template process `fork()`-ed"; +} + +void multithreaded_fork_process_source::make_new_template_process() { + // Here we need, in addition, to wait for the template thread + // to have heard back from all userspace threads before declaing the template + // process is ready. + shared_memory_region* rw_region = + xpc_resources::get_instance().get_rw_region(); + const volatile template_process_t* tstruct = + &(rw_region->as()->tpt); + + signal_tracker::set_sem((sem_t*)&tstruct->mcmini_process_sem); + + fork_process_source::make_new_template_process(); + log_verbose(mfps) << "Waiting for the template thread to stabilize"; + int rc = signal_tracker::sig_semwait((sem_t*)&tstruct->mcmini_process_sem); + if (rc != 0) { + throw process_source::process_creation_exception( + "The template process (" + + std::to_string(this->template_process_handle->get_pid()) + + ") was not synchronized with correctly: " + + std::string(strerror(errno))); + } +} diff --git a/dmtcp/src/mcmini/real_world/local_linux_process.cpp b/dmtcp/src/mcmini/real_world/local_linux_process.cpp new file mode 100644 index 00000000..75c46036 --- /dev/null +++ b/dmtcp/src/mcmini/real_world/local_linux_process.cpp @@ -0,0 +1,122 @@ +#include "mcmini/real_world/process/local_linux_process.hpp" + +#include +#include + +#include +#include +#include + +#include "mcmini/common/shm_config.h" +#include "mcmini/log/logger.hpp" +#include "mcmini/real_world/mailbox/runner_mailbox.h" +#include "mcmini/real_world/process/fork_process_source.hpp" +#include "mcmini/real_world/process/resources.hpp" +#include "mcmini/real_world/shm.hpp" +#include "mcmini/signal.hpp" + +using namespace logging; +using namespace real_world; + +logger process_logger("processes"); + +local_linux_process::local_linux_process(pid_t pid, bool should_wait) + : pid(pid), should_wait(should_wait) {} + +local_linux_process::local_linux_process(local_linux_process &&other) + : local_linux_process(other.pid) { + other.pid = -1; +} + +local_linux_process &local_linux_process::operator=( + local_linux_process &&other) { + this->pid = other.pid; + other.pid = -1; + return *this; +} + +local_linux_process::~local_linux_process() { + if (pid <= 0) { + return; + } + if (kill(pid, SIGUSR1) == -1) { + log_error(process_logger) + << "Error sending SIGUSR1 to `" << (pid) << "`: " << strerror(errno); + } + int status; + if (should_wait) { + if (waitpid(pid, &status, 0) == -1) { + if (errno != ECHILD) { + log_error(process_logger) << "Error waiting for process (waitpid) `" + << pid << "`: " << strerror(errno); + } else { + log_error(process_logger) << "Error: " << strerror(errno); + } + } + } +} + +volatile runner_mailbox *local_linux_process::execute_runner(runner_id_t id) { + shared_memory_region *shm_slice = + xpc_resources::get_instance().get_rw_region(); + volatile runner_mailbox *rmb = + &(shm_slice->as_array_of()->mailboxes[id]); + + signal_tracker::set_sem((sem_t *)&rmb->model_side_sem); + + // NOTE: At the moment, each process has the entire view of the + // shared memory region at its disposal. If desired, an extra layer could be + // added on top which manages allocating slices of a `shared_memory_region` + // and "allocates" them to different processes. This would look similar to + // `malloc()/free()`, where the `free()` would be triggered by the destructor + // of the slice. This is overly complicated at the moment, and we simply + // restrict the number of proxy processes to one. + mc_wake_thread(rmb); + + // There is a potential race if the child dies and issues a SIGCHLD + // just before we call `mc_wait_for_thread()`. This doesn't happen + // because the signal handler for SIGCHLD will call `mc_wake_scheduler()` + // which issues a `sem_post(3)` to cancel the `sem_wait(3)` in + // `mc_wait_for_thread()`. The signal handler also sets `sigchld_set`, + // allowing us to determine that a SIGCHLD was received. + // + // TODO: The template process will also send a SIGCHLD if it dies + // unexpectedly. Because we don't expect the template process to die, this is + // OK for now, but should be handled in the future. + errno = 0; + signal_tracker::sig_semwait((sem_t *)&rmb->model_side_sem); + if (signal_tracker::instance().try_consume_signal(SIGCHLD)) { + // TODO: Get the true failure status from the template process via + // e.g. shared memory. + throw process::termination_error(SIGTERM, id, + "Process terminated abnormally."); + // // TODO: Double check that this + // // is the correct process that sent + // // the SIGCHILD using WNOHANG. + + // // `PR_SET_CHILD_SUBREAPER` enables us to wait on + // // this grandchild. + // int status; + // int rc = waitpid(this->pid, &status, 0); + // if (rc == -1) { + // throw process::execution_error( + // "Error attempting to determine the failure causing the child + // process " "to abnormally exit (or possibly an internal error of + // McMini)." + std::string(strerror(errno))); + // } else { + // if (WIFEXITED(status)) { + // int exit_code = WEXITSTATUS(status); + // throw process::nonzero_exit_code_error( + // exit_code, "Process terminated with a non-zero exit code."); + // } else if (WIFSIGNALED(status)) { + // int signo = WTERMSIG(status); + // throw process::termination_error(signo, + // "Process terminated abnormally."); + // } else { + // throw process::execution_error( + // "SIGSTOP/SIGCONT in branch processes is not yet supported."); + // } + // } + } + return rmb; +} diff --git a/dmtcp/src/mcmini/real_world/resources.cpp b/dmtcp/src/mcmini/real_world/resources.cpp new file mode 100644 index 00000000..5b19d1ea --- /dev/null +++ b/dmtcp/src/mcmini/real_world/resources.cpp @@ -0,0 +1,55 @@ +#include "mcmini/real_world/process/resources.hpp" + +#include +#include +#include + +#include "mcmini/common/shm_config.h" +#include "mcmini/log/logger.hpp" +#include "mcmini/misc/extensions/unique_ptr.hpp" + +using namespace logging; +using namespace real_world; +using namespace extensions; + +logger xpc_logger("xpc"); + +xpc_resources::xpc_resources() { + const std::string shm_file_name = "/mcmini-" + std::string(getenv("USER")) + + "-" + std::to_string((long)getpid()); + log_debug(xpc_logger) << "Creating shared memory file `" << shm_file_name + << "`"; + this->rw_region = make_unique(shm_file_name, shm_size); + + volatile mcmini_shm_file* shm_file = rw_region->as(); + volatile template_process_t* tstruct = &shm_file->tpt; + + log_debug(xpc_logger) << "Initializing shared memory semaphores"; + sem_init((sem_t*)&tstruct->mcmini_process_sem, SEM_FLAG_SHARED, 0); + sem_init((sem_t*)&tstruct->libmcmini_sem, SEM_FLAG_SHARED, 0); + + // TODO: The `MAX_TOTAL_THREADS_IN_PROGRAM` should be a configurable + // parameter... + const int max_total_threads = MAX_TOTAL_THREADS_IN_PROGRAM; + for (int i = 0; i < max_total_threads; i++) + mc_runner_mailbox_init(&shm_file->mailboxes[i]); + log_debug(xpc_logger) << "xpc resource initialization complete"; +} + +void xpc_resources::reset_binary_semaphores_for_new_branch() { + // Reinitialize the region for the new process, as the contents of the + // memory are dirtied from the last process which used the same memory and + // exited arbitrarily (i.e. in such a way as to leave data in the shared + // memory). + // + // INVARIANT: Only one `local_linux_process` is in existence at any given + // time. + log_very_verbose(xpc_logger) << "Destroying shared memory semaphores"; + volatile runner_mailbox* mbp = (rw_region->as()->mailboxes); + const int max_total_threads = MAX_TOTAL_THREADS_IN_PROGRAM; + for (int i = 0; i < max_total_threads; i++) { + mc_runner_mailbox_destroy(mbp + i); + mc_runner_mailbox_init(mbp + i); + } + log_very_verbose(xpc_logger) << "Shared memory semaphores reinitialized"; +} diff --git a/docs/design/src/mcmini/real_world/shm.cpp b/dmtcp/src/mcmini/real_world/shm.cpp similarity index 74% rename from docs/design/src/mcmini/real_world/shm.cpp rename to dmtcp/src/mcmini/real_world/shm.cpp index 6bb755f4..9a9fa9ca 100644 --- a/docs/design/src/mcmini/real_world/shm.cpp +++ b/dmtcp/src/mcmini/real_world/shm.cpp @@ -3,22 +3,21 @@ #include #include #include -#include #include +#include +#include #include +#include using namespace real_world; shared_memory_region::shared_memory_region(const std::string &shm_file_name, size_t region_size) : shm_file_name(shm_file_name), region_size(region_size) { - // TODO: We should figure out how to handle errors on failure besides simply - // exiting. Probably we should have a factory that can return a result of - // either the region or some sort of failure condition. - // This creates a file in /dev/shm/ - int fd = shm_open(shm_file_name.c_str(), O_CREAT | O_RDWR, S_IRUSR | S_IWUSR); + int const fd = + shm_open(shm_file_name.c_str(), O_CREAT | O_RDWR, S_IRUSR | S_IWUSR); if (fd == -1) { if (errno == EACCES) { std::fprintf(stderr, @@ -27,18 +26,18 @@ shared_memory_region::shared_memory_region(const std::string &shm_file_name, } else { std::perror("shm_open"); } - std::exit(EXIT_FAILURE); + return; } - int rc = ftruncate(fd, size()); + int const rc = ftruncate(fd, size()); if (rc == -1) { std::perror("ftruncate"); - std::exit(EXIT_FAILURE); + return; } void *handle = mmap(nullptr, size(), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (handle == MAP_FAILED) { std::perror("mmap"); - std::exit(EXIT_FAILURE); + return; } fsync(fd); close(fd); @@ -52,12 +51,7 @@ shared_memory_region::~shared_memory_region() { // only concerned with the actual address value and will not attempt to access // the _contents_ of `shm_mmap_region` without the volatile qualification // (such an access is undefined behavior) - int rc = munmap(const_cast(shm_mmap_region), size()); - if (rc == -1) { - std::perror("munmap"); - std::exit(EXIT_FAILURE); - } - rc = shm_unlink(shm_file_name.c_str()); + int rc = shm_unlink(shm_file_name.c_str()); if (rc == -1) { if (errno == EACCES) { std::fprintf(stderr, @@ -67,6 +61,9 @@ shared_memory_region::~shared_memory_region() { } else { std::perror("shm_unlink"); } - std::exit(EXIT_FAILURE); + } + rc = munmap(const_cast(shm_mmap_region), size()); + if (rc == -1) { + std::perror("munmap"); } } diff --git a/dmtcp/src/mcmini/real_world/target.cpp b/dmtcp/src/mcmini/real_world/target.cpp new file mode 100644 index 00000000..5a0bfb64 --- /dev/null +++ b/dmtcp/src/mcmini/real_world/target.cpp @@ -0,0 +1,279 @@ +#include "mcmini/real_world/target.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "mcmini/log/logger.hpp" +#include "mcmini/real_world/dmtcp_target.hpp" +#include "mcmini/real_world/process/dmtcp_coordinator.hpp" +#include "mcmini/real_world/process_source.hpp" +#include "mcmini/signal.hpp" + +using namespace real_world; +using namespace logging; + +logger targer_logger("targets"); + +void target::prepare_mcmini_targets() { + // To enable the model checker process to handle assertion failures, + // SEGFAULTs, and other unexpected errors during execution, we + // make the branch process a direct child of the McMini process (i.e. + // the process containing the model checker). + // + // `PR_SET_CHILD_SUBREAPER` must be set + if (prctl(PR_SET_CHILD_SUBREAPER, 1) == -1) { + throw std::runtime_error("Failed to set prctl"); + } +} + +void target::execvp() const { + // `const_cast<>` is needed to call the C-functions here. A new/delete + // or malloc/free _could be_ needed, we'd need to check the man page. As + // long as the char * is not actually modified, this is OK and the best way + // to interface with the C library routines + std::vector args; + args.reserve(this->target_program_args.size()); + args.push_back(const_cast(this->target_program.c_str())); + for (const std::string &arg : this->target_program_args) + args.push_back(const_cast(arg.c_str())); + args.push_back(NULL); + + // Ensures that addresses in the template process remain "stable" + personality(ADDR_NO_RANDOMIZE); + + // Ensures that the template process is sent a SIGTERM if THIS THREAD ever + // exits. Since McMini is currently single-threaded, this is equivalent to + // saying if McMini ever exits. Note that this `prctl(2)` persists across + // `execvp(2)`. + prctl(PR_SET_PDEATHSIG, SIGTERM); + + // Ensures that the child will accept the reception of all signals (see + // `install_process_wide_signal_handlers()` where we explicitly block the + // reception of signals from the main thread i.e. this thread which is + // calling `fork()`) + // + // The man page for `sigprocmask(2)` reads: + // + // "A child created via fork(2) inherits a copy of its parent's signal mask; + // the signal mask is preserved across execve(2)." + // + // hence why we clear it about before `execvp()` + sigset_t empty_set; + sigemptyset(&empty_set); + sigprocmask(SIG_SETMASK, &empty_set, NULL); + ::execvp(this->target_program.c_str(), args.data()); +} + +pid_t target::launch_dont_wait() { + int pipefd[2]; + if (pipe(pipefd) == -1) { + throw std::runtime_error("Failed to open pipe(2): " + + std::string(strerror(errno))); + } + errno = 0; + const pid_t child_pid = ::fork(); + if (child_pid == -1) { + // fork(2) failed + throw process_source::process_creation_exception( + "Failed to create a new process (fork(2) failed): " + + std::string(strerror(errno))); + } + if (child_pid == 0) { + // ****************** + // Child process case + // ****************** + close(pipefd[0]); + fcntl(pipefd[1], F_SETFD, FD_CLOEXEC); + + for (const auto &env_pair : this->setenv_vars) + setenv(env_pair.first.c_str(), env_pair.second.c_str(), 1); + + for (const auto &unsetenv_var : this->unsetenv_vars) + unsetenv(unsetenv_var.c_str()); + + if (quiet) { + int devnull = open("/dev/null", O_WRONLY); + if (devnull >= 0) { + dup2(devnull, STDOUT_FILENO); + dup2(devnull, STDERR_FILENO); + close(devnull); + } + } + + execvp(); + + // If `execvp()` fails, we signal the error to the parent process by writing + // into the pipe. + int err = errno; + write(pipefd[1], &err, sizeof(err)); + fsync(pipefd[1]); + close(pipefd[1]); + + // @note: We invoke `quick_exit()` here to ensure that C++ static + // objects are NOT destroyed. `std::exit()` will invoke the destructors + // of such static objects, among other cleanup. This is only intended to + // happen exactly once however; bad things likely would happen to a program + // which called the destructor on an object that already cleaned up its + // resources. + // + // We must remember that this child is in a completely separate process with + // a completely separate address space, but the shared resources that the + // McMini process holds onto will also (inadvertantly) be shared with the + // child. We want the resources to be destroyed in the MCMINI process, NOT + // this (failed) child fork(). To get C++ to play nicely, this is how we do + // it. + std::quick_exit(EXIT_FAILURE); + // ****************** + // Child process case + // ****************** + + // Should never be reached --> implies quick_exit returned + std::abort(); + } else { + // ******************* + // Parent process case + // ******************* + close(pipefd[1]); // Close write end + + int err = 0; + if (read(pipefd[0], &err, sizeof(err)) > 0) { + // waitpid() ensures that the child's resources are properly reacquired. + if (waitpid(child_pid, nullptr, 0) == -1) { + throw process_source::process_creation_exception( + "Failed to create a cleanup zombied child process (waitpid(2) " + "returned -1 after fork/exec): " + + std::string(strerror(errno))); + } + throw process_source::process_creation_exception( + "Failed to create a new process of '" + this->target_program + "'" + + " (execvp(2) failed with error code '" + std::to_string(errno) + + "'):" + std::string(strerror(err))); + } + close(pipefd[0]); + + // ******************* + // Parent process case + // ******************* + return child_pid; + } +} + +void target::launch_and_wait() { + int status = 0; + pid_t child = launch_dont_wait(); + if (waitpid(child, &status, 0) == -1) { + throw process::execution_error( + "Failed to create a cleanup zombied child process `" + + std::to_string(child) + + "` (waitpid(2) returned -1): " + std::string(strerror(errno))); + } + if (WIFEXITED(status)) { + log_error(targer_logger) << "`" << invocation() << "` [" << child + << "] exited with status " << WEXITSTATUS(status); + } else if (WIFSIGNALED(status)) { + throw process::execution_error( + "The child process was signaled (received :" + + std::to_string(WTERMSIG(status)) + ")"); + } else { + throw process::execution_error("The child process exited abnormally"); + } + + // sigwait... + + // A SIGCHLD was delivered to McMini at this point. To prevent it from being + // considered in subsequent launches, consume one of them. + signal_tracker::instance().try_consume_signal(SIGCHLD); + log_verbose(targer_logger) << "Consumed the SIGCHLD of " << child; +} + +pid_t dmtcp_target::launch_dont_wait() { + // From the DMTCP doc: + //""" + // DMTCP preserves the original pre-checkpoint environment variables when + // it restarts. Usually, this is the desired behavior. In a few cases, + // one wishes to update the environment variables based on their values + // within the parent process of the restarted process. These updates + // can be specified in a file: + // dmtcp_env.txt + // The file dmtcp_env.txt must be in the same checkpoint directory as + // the checkpoint image files ( ckpt_*.dmtcp ). + //""" + if (propagate_env_through_dmtcp && + ((this->unsetenv_vars != this->unsetenv_file_vars) || + (this->setenv_vars != this->setenv_file_vars))) { + std::string ckpt_dir; + size_t pos = this->ckpt_file_path.find_last_of("/"); + if (std::string::npos != pos) { + ckpt_dir = this->ckpt_file_path.substr(0, pos); + } else { + // Fallback to the current directory if there are no slashes + ckpt_dir = "."; + } + std::string dmtcp_env_file_path = std::move(ckpt_dir) + "/dmtcp_env.txt"; + this->dmtcp_env_file = std::fstream( + dmtcp_env_file_path, std::ios_base::trunc | std::fstream::out); + + // Write the environment vars out to the file. The format is "VAR=VALUE" + // according to the docs: + // """ + // Provide 'name=value' pairs in a format similar to that of putenv(). + // The 'name=value' will be added to the environment of the restarted + // process. + // + // A 'name' with no '=' indicates to unset 'name' in the restarted + // process. + // """ + this->dmtcp_env_file << "# This file was autogenerated by `mcmini`\n"; + for (const auto &pair : this->setenv_vars) + this->dmtcp_env_file << pair.first << "=" + << "\"" << pair.second << "\"\n"; + for (const auto &name : this->unsetenv_vars) + this->dmtcp_env_file << name << "\n"; + this->dmtcp_env_file.flush(); + + // Cache the current variables just written to the output. + this->setenv_file_vars = this->setenv_vars; + this->unsetenv_file_vars = this->unsetenv_vars; + } + return target::launch_dont_wait(); +} + +dmtcp_coordinator::dmtcp_coordinator() + : dmtcp_target("dmtcp_coordinator", {"--port", "0", "--port-file", + "mcmini_dmtcp_coordinator_port"}) { + this->propagate_env_through_dmtcp = false; + set_quiet(true); +} + +void dmtcp_coordinator::launch_and_wait() { + // For the DMTCP coordinator, we DON't need to wait until the process fully + // exits. The coordinator is not a daemon: it dies when McMini dies. + // + // However, returning from the call to `fork()` does _NOT_ guarantee + // that the forked child process has completed its `execvp()`. + // We need to wait for the coordinator to be ready before creating a + // `dmtcp_restart` child process. + // + // NOTE: We assume that the `dmtcp_coordinator` is ready once the port file + // has been written. + dmtcp_target::launch_dont_wait(); + + std::ifstream dmtcp_coord_port_stream("mcmini_dmtcp_coordinator_port"); + + // TODO: Remove the busy wait here + while (!dmtcp_coord_port_stream.is_open()) { + usleep(100000); + dmtcp_coord_port_stream.open("mcmini_dmtcp_coordinator_port"); + } + dmtcp_coord_port_stream >> this->port; + unlink("mcmini_dmtcp_coordinator_port"); +} diff --git a/dmtcp/src/mcmini/signal.cpp b/dmtcp/src/mcmini/signal.cpp new file mode 100644 index 00000000..f7bcbd6e --- /dev/null +++ b/dmtcp/src/mcmini/signal.cpp @@ -0,0 +1,262 @@ +#include "mcmini/signal.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +const std::unordered_map sig_to_str = { + {SIGHUP, "SIGHUP"}, {SIGINT, "SIGINT"}, {SIGQUIT, "SIGQUIT"}, + {SIGILL, "SIGILL"}, {SIGTRAP, "SIGTRAP"}, {SIGABRT, "SIGABRT"}, + {SIGBUS, "SIGBUS"}, {SIGFPE, "SIGFPE"}, {SIGKILL, "SIGKILL"}, + {SIGUSR1, "SIGUSR1"}, {SIGSEGV, "SIGSEGV"}, {SIGUSR2, "SIGUSR2"}, + {SIGPIPE, "SIGPIPE"}, {SIGALRM, "SIGALRM"}, {SIGTERM, "SIGTERM"}, + {SIGSTKFLT, "SIGSTKFLT"}, {SIGCHLD, "SIGCHLD"}, {SIGCONT, "SIGCONT"}, + {SIGSTOP, "SIGSTOP"}, {SIGTSTP, "SIGTSTP"}, {SIGTTIN, "SIGTTIN"}, + {SIGTTOU, "SIGTTOU"}, {SIGURG, "SIGURG"}, {SIGXCPU, "SIGXCPU"}, + {SIGXFSZ, "SIGXFSZ"}, {SIGVTALRM, "SIGVTALRM"}, {SIGPROF, "SIGPROF"}, + {SIGWINCH, "SIGWINCH"}, {SIGIO, "SIGIO"}, +#ifdef SIGPOLL + {SIGPOLL, "SIGPOLL"}, +#endif + {SIGPWR, "SIGPWR"}, {SIGSYS, "SIGSYS"}, +}; + +sem_t *signal_tracker::current_sem = nullptr; + +void signal_tracker_sig_handler(int sig, siginfo_t *, void *) { + // TODO: If multiple SIGCHLDs are received before the main thread has a chance + // to unset the `current_sem`, the `current_sem` may receive _multiple` + // `sem_post(2)` calls. We should handle this in the future if we want to + // retry creating the template process, but for now since we simply fail + // completely if the template/branch process fails unexpectedly, calling + // `sem_post(2)` more than once is OK (since we'll just error out anyway). + signal_tracker::instance().set_signal(sig); + sem_t *set_sem = signal_tracker::instance().current_sem; + if (set_sem) { + // Reset the global value BEFORE posting. We assume only TWO threads inside + // of McMini at this point. + sem_post(set_sem); + } +} + +bool signal_tracker::is_bad_signal(int signo) { + switch (signo) { + case SIGILL: + case SIGABRT: + case SIGBUS: + case SIGFPE: + case SIGKILL: + case SIGSEGV: + case SIGPIPE: + case SIGSYS: + return true; + default: + return false; + } +} + +int signal_tracker::sig_semwait(sem_t *sem) { + // INVARIANT: Assumes a single thread calls this at once. + // The McMini process contains a dedicated background thread to receive + // signals and post to the given semaphore. + int rc = sem_wait(sem); + while (rc != 0 && errno == EINTR) { + rc = sem_wait(sem); + } + return rc; +} + +static bool is_likely_debugging() { + // The Linux man page says that `proc/[pid]/status` contains a `TracerPid: + // field which indicates the process ID tracing this one or `0` if no + // tracer is present. We use this as a heuristic to detect is a debugger is + // present to allow SIGINT to enter the debugger. + std::ifstream status_file("/proc/self/status"); + if (!status_file.is_open()) { + return false; // Cannot open, assume no debugger + } + + std::string line; + while (std::getline(status_file, line)) { + std::istringstream iss(line); + std::string key; + iss >> key; + if (key == "TracerPid:") { + pid_t tracer_pid = 0; + iss >> tracer_pid; + return tracer_pid != 0; + } + } + return false; // Default: no debugger +} + +static void handle_incoming_signals(sem_t *rendez_vous) { + sigset_t no_signals; + sigemptyset(&no_signals); + pthread_sigmask(SIG_SETMASK, &no_signals, NULL); + pthread_setname_np(pthread_self(), "McMini Signal Listener"); + sem_post(rendez_vous); + + sigset_t all_signals; + sigfillset(&all_signals); + + // SIGCHLD is handled in a dedicated handler. + // "Bad" signals (e.g. SIGSEGV) shouldn't be captured by `sigwait()` and + // should instead lead to default program exiting behaviors + sigdelset(&all_signals, SIGCHLD); + sigdelset(&all_signals, SIGTSTP); + for (const auto sig_pair : sig_to_str) { + if (signal_tracker::is_bad_signal(sig_pair.first)) { + sigdelset(&all_signals, sig_pair.first); + } + } + + while (true) { + // According to the `sigwait()` man page: + // """ + // The sigwait() function suspends execution of the calling thread + // until one of the signals specified in the signal set set becomes pending. + // The function accepts the signal (removes it from the pending list of + // signals), and returns the signal number in sig + // """ + int sig; + sigwait(&all_signals, &sig); + std::cout << "Received signal: " << sig_to_str.at(sig) << std::endl; + if (signal_tracker::is_bad_signal(sig)) { + std::terminate(); + } else if (sig == SIGINT) { + if (is_likely_debugging()) { + std::raise(SIGSTOP); + } else { + std::exit(EXIT_FAILURE); + } + } + } +} + +void signal_tracker::install_process_wide_signal_handlers() { + // From + // "https://wiki.sei.cmu.edu/confluence/display/cplusplus/MSC54-CPP.+A+signal+handler+must+be+a+plain+old+function" + // + // """ + // The common subset of the C and C++ languages consists of all + // declarations, definitions, and expressions that may appear in a + // well-formed C++ program and also in a conforming C program. A POF + // (“plain old function”) is a function that uses only features from this + // common subset, and that does not directly or indirectly use any + // function that is not a POF, __except that it may use plain lock-free + // atomic operations__. A plain lock-free atomic operation is an invocation + // of a function f from Clause 29, such that f is not a member function, + // and either f is the function atomic_is_lock_free, or for every atomic + // argument A passed to f, atomic_is_lock_free(A) yields true. All signal + // handlers shall have C linkage. The behavior of any function other than + // a POF used as a signal handler in a C++ program is + // implementation-defined. + // """ + // + // Moreover, only async-signal-safe C functions may be called from handlers. + // Included in this list (according to the `signal-safety(7)` man page) is + // `sem_post(2)` which is used to unblock the main thread wainting on a child + // process (e.g. the template process) + struct sigaction action = {0}; + sigemptyset(&action.sa_mask); + action.sa_flags = 0; + action.sa_sigaction = signal_tracker_sig_handler; + + // See the Linux ma page for `sigaction(2)`: + // + // """ + // SA_NOCLDSTOP: + // + // If signum is SIGCHLD, do not receive notification when child processes stop + // (i.e., when they receive one of SIGSTOP, SIGTSTP, SIGTTIN, or SIGTTOU) + // or resume (i.e., they receive SIGCONT) (see wait(2)). This flag is + // meaningful only when establishing a handler for SIGCHLD. + // """ + action.sa_flags |= SA_NOCLDSTOP; + sigaction(SIGCHLD, &action, nullptr); + + // IMPORTANT: A SIGCHLD signal will be set to the PENDING status if there are + // no available threads to receive the signal. One such condition blocking + // signal delivery is "waiting on a blocking system call", e.g. `sem_wait(2)`. + // McMini has traditionally been a single-threaded process, but because McMini + // regularly communicates with child processes that can fail, we need to be + // able to unblock McMini in case these child processes exit. + static sem_t rendez_vous; + sigset_t all_signals; + sigfillset(&all_signals); + pthread_sigmask(SIG_SETMASK, &all_signals, nullptr); + + // The `rendez_vous` semaphore ensures that, before continuing execution, the + // signal thread has been configured to accept all signals. + sem_init(&rendez_vous, 0, 0); + std::thread(&handle_incoming_signals, &rendez_vous).detach(); + sem_wait(&rendez_vous); +} + +signal_tracker &signal_tracker::instance() { + // C++ guarantees that `instance` is instantiated in a thread-safe manner + // prior to `main()`. Inside the McMini process, this behavior is perfectly + // safe. Dancing around static instance initialization is precisely why + // `libmcmini.so` is written in C however... + static signal_tracker instance; + return instance; +} + +void signal_tracker::set_signal(int sig) { flags[sig].fetch_add(1); } + +bool signal_tracker::has_signal(int sig) const { return flags[sig].load() > 0; } + +bool signal_tracker::try_consume_signal(int sig) { + // TODO: More relaxed semantics may be possible here, but sequential + // consistency is the "easiest" + uint32_t current = flags[sig].load(); + while (current > 0) { + if (flags[sig].compare_exchange_weak(current, current - 1, + std::memory_order_seq_cst, + std::memory_order_seq_cst)) { + return true; + } + } + return false; // No signal to consume +} + +//*** Interrupted Error ***// + +struct signal_tracker::interrupted_error : public std::exception { + private: + std::string msg; + + public: + explicit interrupted_error(signo_t sig) { + msg = "Interrupted with signal " + std::to_string(sig); + if (sig_to_str.count(sig) != 0) { + msg += " ("; + msg += sig_to_str.find(sig)->second; + msg += ")"; + } else { + msg += " (unknown signal name)"; + } + } + const char *what() const noexcept override { return msg.c_str(); } +}; + +void signal_tracker::throw_if_received(int sig) { + if (instance().try_consume_signal(sig)) { + throw interrupted_error(sig); + } +} diff --git a/dmtcp/src/mcmini/visible_object.cpp b/dmtcp/src/mcmini/visible_object.cpp new file mode 100644 index 00000000..b47df4cd --- /dev/null +++ b/dmtcp/src/mcmini/visible_object.cpp @@ -0,0 +1,15 @@ +#include "mcmini/model/visible_object.hpp" + +#include "mcmini/misc/extensions/memory.hpp" + +using namespace model; + +void visible_object::slice(size_t index) { + extensions::delete_all(this->history.begin() + index + 1, + this->history.end()); + this->history.erase(this->history.begin() + index + 1, this->history.end()); +} + +visible_object::~visible_object() { + for (const visible_object_state* s : history) delete s; +} diff --git a/dmtcp/test/CMakeLists.txt b/dmtcp/test/CMakeLists.txt new file mode 100644 index 00000000..dfdbbd4a --- /dev/null +++ b/dmtcp/test/CMakeLists.txt @@ -0,0 +1 @@ +## TODO: Add Ctest/scripting capabilities to quickly run tests diff --git a/dmtcp/test/assertions/assertion-failure.c b/dmtcp/test/assertions/assertion-failure.c new file mode 100644 index 00000000..f2c2b067 --- /dev/null +++ b/dmtcp/test/assertions/assertion-failure.c @@ -0,0 +1,95 @@ +#include +#include +#include +#include // For sleep + +// Global variables to be set by each thread +int global_var_thread1 = 0; +int global_var_thread2 = 0; +pthread_mutex_t lock_global_variables; + +// Function executed by thread 1 +void *thread_function1(void *arg) { + printf("Thread 1: Starting...\n"); + + // Introduce a small delay to increase the chance of the bug appearing + usleep(100); // Sleep for 100 microseconds + + // BUG: Check without proper synchronization + pthread_mutex_lock(&lock_global_variables); + int var_thread2 = global_var_thread2; + pthread_mutex_unlock(&lock_global_variables); + + if (!var_thread2) { + printf("Thread 1: global_var_thread2 is 0, setting global_var_thread1 to 1.\n"); + pthread_mutex_lock(&lock_global_variables); + global_var_thread1 = 1; + pthread_mutex_unlock(&lock_global_variables); + } else { + printf("Thread 1: global_var_thread2 is already 1.\n"); + } + + printf("Thread 1: Finished. global_var_thread1 = %s\n", global_var_thread1 ? "1" : "0"); + return NULL; +} + +// Function executed by thread 2 +void *thread_function2(void *arg) { + printf("Thread 2: Starting...\n"); + + // Introduce a small delay to increase the chance of the bug appearing + usleep(100); // Sleep for 100 microseconds + + // BUG: Check without proper synchronization + pthread_mutex_lock(&lock_global_variables); + int var_thread1 = global_var_thread1; + pthread_mutex_unlock(&lock_global_variables); + + if (!var_thread1) { + printf("Thread 2: global_var_thread1 is 0, setting global_var_thread2 to 1.\n"); + pthread_mutex_lock(&lock_global_variables); + global_var_thread2 = 1; + pthread_mutex_unlock(&lock_global_variables); + } else { + printf("Thread 2: global_var_thread1 is already 1.\n"); + } + + printf("Thread 2: Finished. global_var_thread2 = %s\n", global_var_thread2 ? "1" : "0"); + return NULL; +} + +int main() { + pthread_t tid1, tid2; + + printf("Main: Creating threads...\n"); + + // Create thread 1 + if (pthread_create(&tid1, NULL, thread_function1, NULL) != 0) { + perror("Failed to create thread 1"); + return 1; + } + + // Create thread 2 + if (pthread_create(&tid2, NULL, thread_function2, NULL) != 0) { + perror("Failed to create thread 2"); + return 1; + } + + // Wait for threads to finish + if (pthread_join(tid1, NULL) != 0) { + perror("Failed to join thread 1"); + return 1; + } + if (pthread_join(tid2, NULL) != 0) { + perror("Failed to join thread 2"); + return 1; + } + + assert(global_var_thread1 ^ global_var_thread2); + + printf("\nMain: Both threads finished.\n"); + printf("Final global_var_thread1: %s\n", global_var_thread1 ? "1" : "0"); + printf("Final global_var_thread2: %s\n", global_var_thread2 ? "1" : "0"); + + return 0; +} diff --git a/dmtcp/test/segfaults/philosophers_branch_fails.cpp b/dmtcp/test/segfaults/philosophers_branch_fails.cpp new file mode 100644 index 00000000..acfdde8e --- /dev/null +++ b/dmtcp/test/segfaults/philosophers_branch_fails.cpp @@ -0,0 +1,63 @@ +// Naive dining philosophers solution, which leads to deadlock. + +#include +#include +#include +#include +#include + +int DEBUG = 0; + +struct forks { + int philosopher; + pthread_mutex_t *left_fork; + pthread_mutex_t *right_fork; +} * forks; + +void *philosopher_doit(void *forks_arg) { + struct forks *forks = forks_arg; + sleep(4); + pthread_mutex_lock(forks->left_fork); + sleep(1); + pthread_mutex_lock(forks->right_fork); + sleep(3); + + if (DEBUG) printf("Philosopher %d just ate.\n", forks->philosopher); + + pthread_mutex_unlock(forks->left_fork); + sleep(1); + pthread_mutex_unlock(forks->right_fork); + return NULL; +} + +int main(int argc, char *argv[]) { + int NUM_THREADS = 3; + DEBUG = 1; + + sleep(10); + + pthread_t thread[NUM_THREADS]; + pthread_mutex_t mutex_resource[NUM_THREADS]; + + forks = malloc(NUM_THREADS * sizeof(struct forks)); + + int i; + for (i = 0; i < NUM_THREADS; i++) { + pthread_mutex_init(&mutex_resource[i], NULL); + forks[i] = (struct forks){i, &mutex_resource[i], + &mutex_resource[(i + 1) % NUM_THREADS]}; + } + + raise(SIGSEGV); + + for (i = 0; i < NUM_THREADS; i++) { + pthread_create(&thread[i], NULL, &philosopher_doit, &forks[i]); + } + + for (i = 0; i < NUM_THREADS; i++) { + pthread_join(thread[i], NULL); + } + + free(forks); + return 0; +} diff --git a/dmtcp/test/segfaults/philosophers_template_fails.cpp b/dmtcp/test/segfaults/philosophers_template_fails.cpp new file mode 100644 index 00000000..d26df080 --- /dev/null +++ b/dmtcp/test/segfaults/philosophers_template_fails.cpp @@ -0,0 +1,62 @@ +// Naive dining philosophers solution, which leads to deadlock. + +#include +#include +#include +#include +#include + +int DEBUG = 0; + +struct forks { + int philosopher; + pthread_mutex_t *left_fork; + pthread_mutex_t *right_fork; +} * forks; + +void *philosopher_doit(void *forks_arg) { + struct forks *forks = forks_arg; + sleep(4); + pthread_mutex_lock(forks->left_fork); + sleep(1); + pthread_mutex_lock(forks->right_fork); + sleep(3); + + if (DEBUG) printf("Philosopher %d just ate.\n", forks->philosopher); + + pthread_mutex_unlock(forks->left_fork); + sleep(1); + pthread_mutex_unlock(forks->right_fork); + return NULL; +} + +int main(int argc, char *argv[]) { + int NUM_THREADS = 3; + DEBUG = 1; + + sleep(10); + raise(SIGSEGV); + + pthread_t thread[NUM_THREADS]; + pthread_mutex_t mutex_resource[NUM_THREADS]; + + forks = malloc(NUM_THREADS * sizeof(struct forks)); + + int i; + for (i = 0; i < NUM_THREADS; i++) { + pthread_mutex_init(&mutex_resource[i], NULL); + forks[i] = (struct forks){i, &mutex_resource[i], + &mutex_resource[(i + 1) % NUM_THREADS]}; + } + + for (i = 0; i < NUM_THREADS; i++) { + pthread_create(&thread[i], NULL, &philosopher_doit, &forks[i]); + } + + for (i = 0; i < NUM_THREADS; i++) { + pthread_join(thread[i], NULL); + } + + free(forks); + return 0; +} diff --git a/docs/design/CMakeLists.txt b/docs/design/CMakeLists.txt deleted file mode 100644 index de7598cc..00000000 --- a/docs/design/CMakeLists.txt +++ /dev/null @@ -1,70 +0,0 @@ -cmake_minimum_required(VERSION 3.10) -project(McMini-Revamp - VERSION 1.0.0 - DESCRIPTION "A bite-sized model checker" - LANGUAGES C CXX) - -# Require C11 and C++11 -set(CMAKE_C_STANDARD 11) -set(CMAKE_CXX_STANDARD 11) -set(CMAKE_C_STANDARD_REQUIRED TRUE) -set(CMAKE_CXX_STANDARD_REQUIRED TRUE) - -# Project configuration -option(BUILD_TESTS OFF) -option(VERBOSE_TESTING OFF) -set(MCMINI_DIR "${CMAKE_SOURCE_DIR}") -set(MCMINI_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/include") -set(MCMINI_CMAKE_MODULE_DIR "${CMAKE_SOURCE_DIR}/cmake") - -# Project source files -set(MCMINI_C_SRC ) -set(MCMINI_CPP_SRC - src/mcmini/mcmini.cpp - src/mcmini/visible_object.cpp - src/mcmini/coordinator/coordinator.cpp - src/mcmini/model/detached_state.cpp - src/mcmini/model/program.cpp - src/mcmini/model/state_sequence.cpp - src/mcmini/model_checking/algorithms/classic_dpor.cpp - src/mcmini/real_world/fork_process_source.cpp - src/mcmini/real_world/local_linux_process.cpp - src/mcmini/real_world/shm.cpp -) -set(LIBMCMINI_C_SRC - src/libmcmini/entry.c - src/libmcmini/wrappers.c -) -set(LIBMCMINI_CPP_SRC) - -# -Wall -> be strict with warnings -set(LIBMCMINI_EXTRA_COMPILER_FLAGS -Wall -Werror) -set(LIBMCMINI_EXTRA_COMPILER_DEFINITIONS MC_SHARED_LIBRARY=1) - -# -lrt -> shared memory -# -pthread -> libpthread.so -# -lm -> math library -# -ldl -> dlsym etc. -set(LIBMCMINI_EXTRA_LINK_FLAGS -lrt -pthread -lm -ldl) - -# libmcmini.so -> the dylib which is loaded -add_library(libmcmini SHARED "${LIBMCMINI_CPP_SRC}""${LIBMCMINI_C_SRC}") -add_library(McMini::Dylib ALIAS libmcmini) -set_target_properties(libmcmini PROPERTIES OUTPUT_NAME "mcmini") - -target_include_directories(libmcmini PUBLIC "${MCMINI_INCLUDE_DIR}") -target_compile_definitions(libmcmini - PUBLIC - "${LIBMCMINI_EXTRA_COMPILER_DEFINITIONS}") -target_compile_options(libmcmini - PRIVATE - "${LIBMCMINI_EXTRA_COMPILER_FLAGS}") -target_link_libraries(libmcmini - PUBLIC - "${LIBMCMINI_EXTRA_LINK_FLAGS}") - -add_executable(mcmini "${MCMINI_CPP_SRC}""${MCMINI_C_SRC}") -target_include_directories(mcmini PUBLIC "${MCMINI_INCLUDE_DIR}") - -# Compile examples -add_subdirectory(src/examples) diff --git a/docs/design/include/mcmini/MCSharedLibraryWrappers.h b/docs/design/include/mcmini/MCSharedLibraryWrappers.h deleted file mode 100644 index 693fcaf4..00000000 --- a/docs/design/include/mcmini/MCSharedLibraryWrappers.h +++ /dev/null @@ -1,75 +0,0 @@ -#ifndef INCLUDE_TRANSITION_WRAPPERS_H -#define INCLUDE_TRANSITION_WRAPPERS_H - -#include -#include -#include -#include -#include - -extern typeof(&pthread_create) pthread_create_ptr; -extern typeof(&pthread_join) pthread_join_ptr; -extern typeof(&pthread_mutex_init) pthread_mutex_init_ptr; -extern typeof(&pthread_mutex_lock) pthread_mutex_lock_ptr; -extern typeof(&pthread_mutex_unlock) pthread_mutex_unlock_ptr; -extern typeof(&sem_wait) sem_wait_ptr; -extern typeof(&sem_post) sem_post_ptr; -extern typeof(&sem_init) sem_init_ptr; -extern typeof(&sem_destroy) sem_destroy_ptr; -extern __attribute__((__noreturn__)) typeof(&exit) exit_ptr; -extern __attribute__((__noreturn__)) typeof(&abort) abort_ptr; -//extern typeof(&pthread_barrier_init) pthread_barrier_init_ptr; -//extern typeof(&pthread_barrier_wait) pthread_barrier_wait_ptr; -extern typeof(&pthread_cond_init) pthread_cond_init_ptr; -extern typeof(&pthread_cond_wait) pthread_cond_wait_ptr; -extern typeof(&pthread_cond_signal) pthread_cond_signal_ptr; -extern typeof(&pthread_cond_broadcast) pthread_cond_broadcast_ptr; -//extern typeof(&pthread_rwlock_init) pthread_rwlock_init_ptr; -//extern typeof(&pthread_rwlock_rdlock) pthread_rwlock_rdlock_ptr; -//extern typeof(&pthread_rwlock_wrlock) pthread_rwlock_wrlock_ptr; -//extern typeof(&pthread_rwlock_unlock) pthread_rwlock_unlock_ptr; -extern typeof(&sleep) sleep_ptr; - -#define __real_pthread_create (*pthread_create_ptr) -#define __real_pthread_join (*pthread_join_ptr) -#define __real_pthread_mutex_init (*pthread_mutex_init_ptr) -#define __real_pthread_mutex_lock (*pthread_mutex_lock_ptr) -#define __real_pthread_mutex_unlock (*pthread_mutex_unlock_ptr) -#define __real_sem_wait (*sem_wait_ptr) -#define __real_sem_post (*sem_post_ptr) -#define __real_sem_init (*sem_init_ptr) -#define __real_sem_destroy (*sem_destroy_ptr) -#define __real_exit (*exit_ptr) -#define __real_abort (*abort_ptr) -#define __real_pthread_barrier_init (*pthread_barrier_init_ptr) -#define __real_pthread_barrier_wait (*pthread_barrier_wait_ptr) -#define __real_pthread_cond_init (*pthread_cond_init_ptr) -#define __real_pthread_cond_wait (*pthread_cond_wait_ptr) -#define __real_pthread_cond_signal (*pthread_cond_signal_ptr) -#define __real_pthread_cond_broadcast (*pthread_cond_broadcast_ptr) -#define __real_pthread_rwlock_init (*pthread_rwlock_init_ptr) -#define __real_pthread_rwlock_rdlock (*pthread_rwlock_rdlock_ptr) -#define __real_pthread_rwlock_wrlock (*pthread_rwlock_wrlock_ptr) -#define __real_pthread_rwlock_unlock (*pthread_rwlock_unlock_ptr) -#define __real_sleep (*sleep_ptr) - -// /** -// * @brief Retrieves the addresses of the symbols exposed by the -// * dynamic libraries that McMini redefines to override the -// * functions' original behavior -// * -// * McMini re-defines and exports symbols defined in other dynamic -// * libraries to change the behavior of a program which is dynamically -// * linked to the libraries containing those symbols. When McMini as a -// * dynamic libarary is pre-loaded into memory, a process making calls -// * to the dynamic library will result in McMini's symbols being -// * dynamically chosen instead. -// * -// * If the original behavior of the intercepted symbols is still -// * needed, they must be retrieved using dlsym(3) and other functions. -// * This function loads those symbols into the variables redefined -// * above. -// */ -void load_intercepted_symbol_addresses(); - -#endif diff --git a/docs/design/include/mcmini/coordinator/model_to_system_map.hpp b/docs/design/include/mcmini/coordinator/model_to_system_map.hpp deleted file mode 100644 index 230e6756..00000000 --- a/docs/design/include/mcmini/coordinator/model_to_system_map.hpp +++ /dev/null @@ -1,77 +0,0 @@ -#pragma once - -#include "mcmini/forwards.hpp" -#include "mcmini/misc/optional.hpp" -#include "mcmini/model/state.hpp" - -/** - * @brief A mapping between the remote addresses pointing to the C/C++ structs - * the objects in McMini's model are emulating. - * - * As McMini explores different paths of execution of the target program at - * runtime, it may discover new visible objects. However, visible objects are - * only a _representation in the McMini model_ of the actual underlying - * structs containing the information used to implement the primitive. The - * underlying process, however, refers to these objects as pointers to the - * multi-threaded primitives (e.g. a `pthread_mutex_t*` in - * `pthread_mutex_lock()`). McMini therefore needs to maintain a - * correspondence between these addresses and the identifiers in McMini's - * model used to represent those objects to the model checker. - */ -struct model_to_system_map final { - private: - coordinator &_coordinator; - - /* - * Prevent external construction (only the coordinator can construct - * instances of this class) - */ - model_to_system_map(coordinator &coordinator) : _coordinator(coordinator) {} - friend coordinator; - - public: - /* Prevent external construction */ - model_to_system_map() = delete; - - /** - * @brief Retrieve the remote address of the object with id `id`. - * - * TODO: See the TODOs below - */ - void *get_remote_process_handle_for_object(model::state::objid_t id) const; - - /** - * @brief Retrieve the object that corresponds to the given remote address, or - * the empty optional if no such address exists - */ - optional get_object_for_remote_process_handle( - void *) const; - - /** - * @brief Record the presence of a new visible object that is - * represented with the system id `system_handle`. - * - * @param remote_process_visible_object_handle the address containing - * the data for the new visible object across process handles of the - * - * TODO: Handles are assumed to remain valid _across process source - * invocations_. In the future we could support the ability to _remap_ - * process handles dynamically during each new re-execution scheduled by - * the coordinator to handle aliasing etc. - * - * TODO: The handle could be _any_ value that is used in the - * multi-threaded program. For now, we restrict it to addresses for the - * mutex case etc. - * - * TODO: This should probably have a `result` as a return type. If the - * handle is already mapped, how we should deal with this situation is a - * bit unclear. - */ - model::state::objid_t record_new_object_association( - void *remote_process_visible_object_handle, - std::unique_ptr initial_state); - - model::state::objid_t observe_remote_process_handle( - void *remote_process_visible_object_handle, - std::unique_ptr fallback_initial_state); -}; diff --git a/docs/design/include/mcmini/defines.hpp b/docs/design/include/mcmini/defines.hpp deleted file mode 100644 index 95b653eb..00000000 --- a/docs/design/include/mcmini/defines.hpp +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#define MCMINI_INLINE -#define MCMINI_LIBRARY_ENTRY_POINT -#define MCMINI_EXPORT __attribute__((visibility(default))) -#define MCMINI_PRIVATE __attribute__((visibility(hidden))) - -// TODO: Other defines here based on system types etc. -// TODO: Add "extern "C" where appropriate \ No newline at end of file diff --git a/docs/design/include/mcmini/detail/ddt.hpp b/docs/design/include/mcmini/detail/ddt.hpp deleted file mode 100644 index 151456ca..00000000 --- a/docs/design/include/mcmini/detail/ddt.hpp +++ /dev/null @@ -1,210 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "mcmini/misc/optional.hpp" - -template -struct double_dispatch_member_function_table; - -template -struct double_dispatch_member_function_table { - private: - using opaque_callback = ReturnType (InterfaceType::*)(InterfaceType*, - Args...); - using stored_callback = ReturnType (*)(InterfaceType*, InterfaceType*, - opaque_callback, Args...); - - std::unordered_map< - std::type_index, - std::unordered_map>> - _internal_table; - - // In the intermediate - // See "https://en.cppreference.com/w/cpp/language/reinterpret_cast" - // """ - // 10) A pointer to member function can be converted to pointer to a - // different member function of a different type. Conversion back to the - // original type yields the original value, otherwise the resulting - // pointer cannot be used safely. - // """ - // The reinterpret_cast<> here is used to restore the specific callback - // function specified at registration-time. Since function will only be - // invoked if the RTTI runtime type-check lookup in the `call`, the cast is - // safe. Furthermore, since the reinterpret_cast restores the original - // pointer-to-member function pointer, subsequently invoking the callback - // through `well_defined_handle` is defined. - template - static ReturnType casting_function(InterfaceType* t1, InterfaceType* t2, - opaque_callback callback, Args... args) { - auto well_defined_handle = - reinterpret_cast>(callback); - return (static_cast(t1)->*well_defined_handle)(static_cast(t2), - std::forward(args)...); - } - - template - static ReturnType casting_function_reverse(InterfaceType* t1, - InterfaceType* t2, - opaque_callback callback, - Args... args) { - auto well_defined_handle = - reinterpret_cast>(callback); - return (static_cast(t2)->*well_defined_handle)(static_cast(t1), - std::forward(args)...); - } - - public: - static_assert( - std::is_polymorphic::value, - "InterfaceType must be a polymorphic type for double dispatch table " - "to function properly. See the example and documentation for typeid() on " - "cppreference.com (https://en.cppreference.com/w/cpp/language/typeid) " - "for more details on why this is necessary"); - - template - using member_function_callback = ReturnType (T1::*)(T2*, Args...); - - template - void register_dd_entry(member_function_callback callback) { - static_assert(std::is_base_of::value, - "T1 must be a subclass of InterfaceType"); - static_assert(std::is_base_of::value, - "T2 must be a subclass of InterfaceType"); - // In the intermediate - // See "https://en.cppreference.com/w/cpp/language/reinterpret_cast" - // """ - // 10) A pointer to member function can be converted to pointer to a - // different member function of a different type. Conversion back to the - // original type yields the original value, otherwise the resulting pointer - // cannot be used safely. - // """ - // The reinterpret_cast<> here is used to store the variable-type callback - // _callback_ registered for the particular combination - auto unspecified_callback_handle = - reinterpret_cast(callback); - - // TODO: Check if the callback has been registered; if so, an error should - // be returned indicated that this is the case. - _internal_table[std::type_index(typeid(T1))][std::type_index(typeid(T2))] = - std::make_pair(casting_function, unspecified_callback_handle); - - _internal_table[std::type_index(typeid(T2))][std::type_index(typeid(T1))] = - std::make_pair(casting_function_reverse, - unspecified_callback_handle); - } - - optional call(InterfaceType* t1, InterfaceType* t2, - Args... args) { - auto t1_type = std::type_index(typeid(*t1)); - auto t2_type = std::type_index(typeid(*t2)); - if (_internal_table.count(t1_type) > 0) { - if (_internal_table[t1_type].count(t2_type) > 0) { - auto& pair = _internal_table[t1_type][t2_type]; - return optional( - pair.first(t1, t2, pair.second, std::forward(args)...)); - } - } - return optional(); - } -}; - -// TODO: Is there a way to avoid the duplication between these two -// specializations? The only difference is that of the `call` interface. -template -struct double_dispatch_member_function_table { - private: - using opaque_callback = void (InterfaceType::*)(InterfaceType*, Args...); - using stored_callback = void (*)(InterfaceType*, InterfaceType*, - opaque_callback, Args...); - - std::unordered_map< - std::type_index, - std::unordered_map>> - _internal_table; - - // In the intermediate - // See "https://en.cppreference.com/w/cpp/language/reinterpret_cast" - // """ - // 10) A pointer to member function can be converted to pointer to a - // different member function of a different type. Conversion back to the - // original type yields the original value, otherwise the resulting - // pointer cannot be used safely. - // """ - // The reinterpret_cast<> here is used to restore the specific callback - // function specified at registration-time. Since function will only be - // invoked if the RTTI runtime type-check lookup in the `call`, the cast is - // safe. Furthermore, since the reinterpret_cast restores the original - // pointer-to-member function pointer, subsequently invoking the callback - // through `well_defined_handle` is defined. - template - static void casting_function(InterfaceType* t1, InterfaceType* t2, - opaque_callback callback, Args... args) { - auto well_defined_handle = - reinterpret_cast>(callback); - (static_cast(t1)->*well_defined_handle)(static_cast(t2), - std::forward(args)...); - } - - template - static void casting_function_reverse(InterfaceType* t1, InterfaceType* t2, - opaque_callback callback, Args... args) { - auto well_defined_handle = - reinterpret_cast>(callback); - (static_cast(t2)->*well_defined_handle)(static_cast(t1), - std::forward(args)...); - } - - public: - static_assert( - std::is_polymorphic::value, - "InterfaceType must be a polymorphic type for double dispatch table " - "to function properly. See the example and documentation for typeid() on " - "cppreference.com (https://en.cppreference.com/w/cpp/language/typeid) " - "for more details on why this is necessary"); - - template - using member_function_callback = void (T1::*)(T2*, Args...); - - template - void register_dd_entry(member_function_callback callback) { - // In the intermediate - // See "https://en.cppreference.com/w/cpp/language/reinterpret_cast" - // """ - // 10) A pointer to member function can be converted to pointer to a - // different member function of a different type. Conversion back to the - // original type yields the original value, otherwise the resulting pointer - // cannot be used safely. - // """ - // The reinterpret_cast<> here is used to store the variable-type callback - // _callback_ registered for the particular combination - auto unspecified_callback_handle = - reinterpret_cast(callback); - - // TODO: Check if the callback has been registered; if so, an error should - // be returned indicated that this is the case. - _internal_table[std::type_index(typeid(T1))][std::type_index(typeid(T2))] = - std::make_pair(casting_function, unspecified_callback_handle); - - _internal_table[std::type_index(typeid(T2))][std::type_index(typeid(T1))] = - std::make_pair(casting_function_reverse, - unspecified_callback_handle); - } - - void call(InterfaceType* t1, InterfaceType* t2, Args... args) { - auto t1_type = std::type_index(typeid(*t1)); - auto t2_type = std::type_index(typeid(*t2)); - if (_internal_table.count(t1_type) > 0) { - if (_internal_table[t1_type].count(t2_type) > 0) { - auto& pair = _internal_table[t1_type][t2_type]; - pair.first(t1, t2, pair.second, std::forward(args)...); - } - } - } -}; \ No newline at end of file diff --git a/docs/design/include/mcmini/detail/volatile_mem_stream.hpp b/docs/design/include/mcmini/detail/volatile_mem_stream.hpp deleted file mode 100644 index 2e22d652..00000000 --- a/docs/design/include/mcmini/detail/volatile_mem_stream.hpp +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once - -#include - -#include "mcmini/real_world/shm.hpp" - -struct volatile_mem_stream : public std::streambuf { - private: - const real_world::shared_memory_region *read_write_region; - char *volatile_cache; - - public: - volatile_mem_stream(const real_world::shared_memory_region *read_write_region) - : read_write_region(read_write_region), - volatile_cache(new char[read_write_region->size()]) { - reset(); - } - - void reset() { - std::copy(read_write_region->byte_stream(), - read_write_region->byte_stream() + read_write_region->size(), - volatile_cache); - setg(volatile_cache, volatile_cache, - volatile_cache + read_write_region->size()); - setp(volatile_cache, volatile_cache + read_write_region->size()); - } - - ~volatile_mem_stream() { delete[] volatile_cache; } - - protected: - std::streamsize xsputn(const char *s, std::streamsize n) override { - std::ptrdiff_t n_bounded = n; // std::min(egptr() - gptr(), n); - std::copy(s, s + n_bounded, pptr()); - pbump(n_bounded); /* Move the write head */ - return n_bounded; - } - std::streamsize xsgetn(char *s, std::streamsize n) override { - std::ptrdiff_t n_bounded = n; // std::min(egptr() - gptr(), n); - std::copy(gptr(), gptr() + n_bounded, s); - gbump(n_bounded); /* Move the read head */ - return n_bounded; - } - int sync() override { - std::copy(volatile_cache, volatile_cache + read_write_region->size(), - read_write_region->byte_stream()); - return std::streambuf::sync(); - } -}; \ No newline at end of file diff --git a/docs/design/include/mcmini/mcmini.hpp b/docs/design/include/mcmini/mcmini.hpp deleted file mode 100644 index d30f861b..00000000 --- a/docs/design/include/mcmini/mcmini.hpp +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include "mcmini/forwards.hpp" -#include "mcmini/misc/asserts.hpp" -#include "mcmini/misc/optional.hpp" -#include "mcmini/model/transition.hpp" -#include "mcmini/model/visible_object.hpp" -#include "mcmini/model_checking/algorithm.hpp" -#include "mcmini/real_world/process.hpp" -#include "mcmini/real_world/process_source.hpp" diff --git a/docs/design/include/mcmini/misc/cli.hpp b/docs/design/include/mcmini/misc/cli.hpp deleted file mode 100644 index 9c64145c..00000000 --- a/docs/design/include/mcmini/misc/cli.hpp +++ /dev/null @@ -1,2122 +0,0 @@ -/* - -Copyright (c) 2014-2022 Jarryd Beck - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -*/ - -// vim: ts=2:sw=2:expandtab - -#ifndef CXXOPTS_HPP_INCLUDED -#define CXXOPTS_HPP_INCLUDED - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef CXXOPTS_NO_EXCEPTIONS -#include -#endif - -#if defined(__GNUC__) && !defined(__clang__) -#if (__GNUC__ * 10 + __GNUC_MINOR__) < 49 -#define CXXOPTS_NO_REGEX true -#endif -#endif -#if defined(_MSC_VER) && !defined(__clang__) -#define CXXOPTS_LINKONCE_CONST __declspec(selectany) extern -#define CXXOPTS_LINKONCE __declspec(selectany) extern -#else -#define CXXOPTS_LINKONCE_CONST -#define CXXOPTS_LINKONCE -#endif - -#ifndef CXXOPTS_NO_REGEX -#include -#endif // CXXOPTS_NO_REGEX - -// Nonstandard before C++17, which is coincidentally what we also need for -// -#ifdef __has_include -#if __has_include() -#include -#ifdef __cpp_lib_optional -#define CXXOPTS_HAS_OPTIONAL -#endif -#endif -#endif - -#define CXXOPTS_FALLTHROUGH -#ifdef __has_cpp_attribute -#if __has_cpp_attribute(fallthrough) -#undef CXXOPTS_FALLTHROUGH -#define CXXOPTS_FALLTHROUGH [[fallthrough]] -#endif -#endif - -#if __cplusplus >= 201603L -#define CXXOPTS_NODISCARD [[nodiscard]] -#else -#define CXXOPTS_NODISCARD -#endif - -#ifndef CXXOPTS_VECTOR_DELIMITER -#define CXXOPTS_VECTOR_DELIMITER ',' -#endif - -#define CXXOPTS__VERSION_MAJOR 3 -#define CXXOPTS__VERSION_MINOR 1 -#define CXXOPTS__VERSION_PATCH 1 - -#if (__GNUC__ < 10 || (__GNUC__ == 10 && __GNUC_MINOR__ < 1)) && __GNUC__ >= 6 -#define CXXOPTS_NULL_DEREF_IGNORE -#endif - -#if defined(__GNUC__) -#define DO_PRAGMA(x) _Pragma(#x) -#define CXXOPTS_DIAGNOSTIC_PUSH DO_PRAGMA(GCC diagnostic push) -#define CXXOPTS_DIAGNOSTIC_POP DO_PRAGMA(GCC diagnostic pop) -#define CXXOPTS_IGNORE_WARNING(x) DO_PRAGMA(GCC diagnostic ignored x) -#else -// define other compilers here if needed -#define CXXOPTS_DIAGNOSTIC_PUSH -#define CXXOPTS_DIAGNOSTIC_POP -#define CXXOPTS_IGNORE_WARNING(x) -#endif - -#ifdef CXXOPTS_NO_RTTI -#define CXXOPTS_RTTI_CAST static_cast -#else -#define CXXOPTS_RTTI_CAST dynamic_cast -#endif - -namespace cxxopts { -static constexpr struct { - uint8_t major, minor, patch; -} version = {CXXOPTS__VERSION_MAJOR, CXXOPTS__VERSION_MINOR, - CXXOPTS__VERSION_PATCH}; -} // namespace cxxopts - -// when we ask cxxopts to use Unicode, help strings are processed using ICU, -// which results in the correct lengths being computed for strings when they -// are formatted for the help output -// it is necessary to make sure that can be found by the -// compiler, and that icu-uc is linked in to the binary. - -#ifdef CXXOPTS_USE_UNICODE -#include - -namespace cxxopts { - -using String = icu::UnicodeString; - -inline String toLocalString(std::string s) { - return icu::UnicodeString::fromUTF8(std::move(s)); -} - -// GNU GCC with -Weffc++ will issue a warning regarding the upcoming class, we -// want to silence it: warning: base class 'class -// std::enable_shared_from_this' has accessible non-virtual -// destructor -CXXOPTS_DIAGNOSTIC_PUSH -CXXOPTS_IGNORE_WARNING("-Wnon-virtual-dtor") -// This will be ignored under other compilers like LLVM clang. -class UnicodeStringIterator { - public: - using iterator_category = std::forward_iterator_tag; - using value_type = int32_t; - using difference_type = std::ptrdiff_t; - using pointer = value_type*; - using reference = value_type&; - - UnicodeStringIterator(const icu::UnicodeString* string, int32_t pos) - : s(string), i(pos) {} - - value_type operator*() const { return s->char32At(i); } - - bool operator==(const UnicodeStringIterator& rhs) const { - return s == rhs.s && i == rhs.i; - } - - bool operator!=(const UnicodeStringIterator& rhs) const { - return !(*this == rhs); - } - - UnicodeStringIterator& operator++() { - ++i; - return *this; - } - - UnicodeStringIterator operator+(int32_t v) { - return UnicodeStringIterator(s, i + v); - } - - private: - const icu::UnicodeString* s; - int32_t i; -}; -CXXOPTS_DIAGNOSTIC_POP - -inline String& stringAppend(String& s, String a) { - return s.append(std::move(a)); -} - -inline String& stringAppend(String& s, std::size_t n, UChar32 c) { - for (std::size_t i = 0; i != n; ++i) { - s.append(c); - } - - return s; -} - -template -String& stringAppend(String& s, Iterator begin, Iterator end) { - while (begin != end) { - s.append(*begin); - ++begin; - } - - return s; -} - -inline size_t stringLength(const String& s) { - return static_cast(s.length()); -} - -inline std::string toUTF8String(const String& s) { - std::string result; - s.toUTF8String(result); - - return result; -} - -inline bool empty(const String& s) { return s.isEmpty(); } - -} // namespace cxxopts - -namespace std { - -inline cxxopts::UnicodeStringIterator begin(const icu::UnicodeString& s) { - return cxxopts::UnicodeStringIterator(&s, 0); -} - -inline cxxopts::UnicodeStringIterator end(const icu::UnicodeString& s) { - return cxxopts::UnicodeStringIterator(&s, s.length()); -} - -} // namespace std - -// ifdef CXXOPTS_USE_UNICODE -#else - -namespace cxxopts { - -using String = std::string; - -template -T toLocalString(T&& t) { - return std::forward(t); -} - -inline std::size_t stringLength(const String& s) { return s.length(); } - -inline String& stringAppend(String& s, const String& a) { return s.append(a); } - -inline String& stringAppend(String& s, std::size_t n, char c) { - return s.append(n, c); -} - -template -String& stringAppend(String& s, Iterator begin, Iterator end) { - return s.append(begin, end); -} - -template -std::string toUTF8String(T&& t) { - return std::forward(t); -} - -inline bool empty(const std::string& s) { return s.empty(); } - -} // namespace cxxopts - -// ifdef CXXOPTS_USE_UNICODE -#endif - -namespace cxxopts { - -namespace { -CXXOPTS_LINKONCE_CONST std::string LQUOTE("\'"); -CXXOPTS_LINKONCE_CONST std::string RQUOTE("\'"); -} // namespace - -// GNU GCC with -Weffc++ will issue a warning regarding the upcoming class, we -// want to silence it: warning: base class 'class -// std::enable_shared_from_this' has accessible non-virtual -// destructor This will be ignored under other compilers like LLVM clang. -CXXOPTS_DIAGNOSTIC_PUSH -CXXOPTS_IGNORE_WARNING("-Wnon-virtual-dtor") - -// some older versions of GCC warn under this warning -CXXOPTS_IGNORE_WARNING("-Weffc++") -class Value : public std::enable_shared_from_this { - public: - virtual ~Value() = default; - - virtual std::shared_ptr clone() const = 0; - - virtual void add(const std::string& text) const = 0; - - virtual void parse(const std::string& text) const = 0; - - virtual void parse() const = 0; - - virtual bool has_default() const = 0; - - virtual bool is_container() const = 0; - - virtual bool has_implicit() const = 0; - - virtual std::string get_default_value() const = 0; - - virtual std::string get_implicit_value() const = 0; - - virtual std::shared_ptr default_value(const std::string& value) = 0; - - virtual std::shared_ptr implicit_value(const std::string& value) = 0; - - virtual std::shared_ptr no_implicit_value() = 0; - - virtual bool is_boolean() const = 0; -}; - -CXXOPTS_DIAGNOSTIC_POP - -namespace exceptions { - -class exception : public std::exception { - public: - explicit exception(std::string message) : m_message(std::move(message)) {} - - CXXOPTS_NODISCARD - const char* what() const noexcept override { return m_message.c_str(); } - - private: - std::string m_message; -}; - -class specification : public exception { - public: - explicit specification(const std::string& message) : exception(message) {} -}; - -class parsing : public exception { - public: - explicit parsing(const std::string& message) : exception(message) {} -}; - -class option_already_exists : public specification { - public: - explicit option_already_exists(const std::string& option) - : specification("Option " + LQUOTE + option + RQUOTE + - " already exists") {} -}; - -class invalid_option_format : public specification { - public: - explicit invalid_option_format(const std::string& format) - : specification("Invalid option format " + LQUOTE + format + RQUOTE) {} -}; - -class invalid_option_syntax : public parsing { - public: - explicit invalid_option_syntax(const std::string& text) - : parsing("Argument " + LQUOTE + text + RQUOTE + - " starts with a - but has incorrect syntax") {} -}; - -class no_such_option : public parsing { - public: - explicit no_such_option(const std::string& option) - : parsing("Option " + LQUOTE + option + RQUOTE + " does not exist") {} -}; - -class missing_argument : public parsing { - public: - explicit missing_argument(const std::string& option) - : parsing("Option " + LQUOTE + option + RQUOTE + - " is missing an argument") {} -}; - -class option_requires_argument : public parsing { - public: - explicit option_requires_argument(const std::string& option) - : parsing("Option " + LQUOTE + option + RQUOTE + - " requires an argument") {} -}; - -class gratuitous_argument_for_option : public parsing { - public: - gratuitous_argument_for_option(const std::string& option, - const std::string& arg) - : parsing("Option " + LQUOTE + option + RQUOTE + - " does not take an argument, but argument " + LQUOTE + arg + - RQUOTE + " given") {} -}; - -class requested_option_not_present : public parsing { - public: - explicit requested_option_not_present(const std::string& option) - : parsing("Option " + LQUOTE + option + RQUOTE + " not present") {} -}; - -class option_has_no_value : public exception { - public: - explicit option_has_no_value(const std::string& option) - : exception(!option.empty() - ? ("Option " + LQUOTE + option + RQUOTE + " has no value") - : "Option has no value") {} -}; - -class incorrect_argument_type : public parsing { - public: - explicit incorrect_argument_type(const std::string& arg) - : parsing("Argument " + LQUOTE + arg + RQUOTE + " failed to parse") {} -}; - -} // namespace exceptions - -template -void throw_or_mimic(const std::string& text) { - static_assert(std::is_base_of::value, - "throw_or_mimic only works on std::exception and " - "deriving classes"); - -#ifndef CXXOPTS_NO_EXCEPTIONS - // If CXXOPTS_NO_EXCEPTIONS is not defined, just throw - throw T{text}; -#else - // Otherwise manually instantiate the exception, print what() to stderr, - // and exit - T exception{text}; - std::cerr << exception.what() << std::endl; - std::exit(EXIT_FAILURE); -#endif -} - -using OptionNames = std::vector; - -namespace values { - -namespace parser_tool { - -struct IntegerDesc { - std::string negative = ""; - std::string base = ""; - std::string value = ""; -}; -struct ArguDesc { - std::string arg_name = ""; - bool grouping = false; - bool set_value = false; - std::string value = ""; -}; - -#ifdef CXXOPTS_NO_REGEX -inline IntegerDesc SplitInteger(const std::string& text) { - if (text.empty()) { - throw_or_mimic(text); - } - IntegerDesc desc; - const char* pdata = text.c_str(); - if (*pdata == '-') { - pdata += 1; - desc.negative = "-"; - } - if (strncmp(pdata, "0x", 2) == 0) { - pdata += 2; - desc.base = "0x"; - } - if (*pdata != '\0') { - desc.value = std::string(pdata); - } else { - throw_or_mimic(text); - } - return desc; -} - -inline bool IsTrueText(const std::string& text) { - const char* pdata = text.c_str(); - if (*pdata == 't' || *pdata == 'T') { - pdata += 1; - if (strncmp(pdata, "rue\0", 4) == 0) { - return true; - } - } else if (strncmp(pdata, "1\0", 2) == 0) { - return true; - } - return false; -} - -inline bool IsFalseText(const std::string& text) { - const char* pdata = text.c_str(); - if (*pdata == 'f' || *pdata == 'F') { - pdata += 1; - if (strncmp(pdata, "alse\0", 5) == 0) { - return true; - } - } else if (strncmp(pdata, "0\0", 2) == 0) { - return true; - } - return false; -} - -inline OptionNames split_option_names(const std::string& text) { - OptionNames split_names; - - std::string::size_type token_start_pos = 0; - auto length = text.length(); - - if (length == 0) { - throw_or_mimic(text); - } - - while (token_start_pos < length) { - const auto& npos = std::string::npos; - auto next_non_space_pos = text.find_first_not_of(' ', token_start_pos); - if (next_non_space_pos == npos) { - throw_or_mimic(text); - } - token_start_pos = next_non_space_pos; - auto next_delimiter_pos = text.find(',', token_start_pos); - if (next_delimiter_pos == token_start_pos) { - throw_or_mimic(text); - } - if (next_delimiter_pos == npos) { - next_delimiter_pos = length; - } - auto token_length = next_delimiter_pos - token_start_pos; - // validate the token itself matches the regex /([:alnum:][-_[:alnum:]]*/ - { - const char* option_name_valid_chars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789" - "_-.?"; - - if (!std::isalnum(text[token_start_pos], std::locale::classic()) || - text.find_first_not_of(option_name_valid_chars, token_start_pos) < - next_delimiter_pos) { - throw_or_mimic(text); - } - } - split_names.emplace_back(text.substr(token_start_pos, token_length)); - token_start_pos = next_delimiter_pos + 1; - } - return split_names; -} - -inline ArguDesc ParseArgument(const char* arg, bool& matched) { - ArguDesc argu_desc; - const char* pdata = arg; - matched = false; - if (strncmp(pdata, "--", 2) == 0) { - pdata += 2; - if (isalnum(*pdata, std::locale::classic())) { - argu_desc.arg_name.push_back(*pdata); - pdata += 1; - while (isalnum(*pdata, std::locale::classic()) || *pdata == '-' || - *pdata == '_') { - argu_desc.arg_name.push_back(*pdata); - pdata += 1; - } - if (argu_desc.arg_name.length() > 1) { - if (*pdata == '=') { - argu_desc.set_value = true; - pdata += 1; - if (*pdata != '\0') { - argu_desc.value = std::string(pdata); - } - matched = true; - } else if (*pdata == '\0') { - matched = true; - } - } - } - } else if (strncmp(pdata, "-", 1) == 0) { - pdata += 1; - argu_desc.grouping = true; - while (isalnum(*pdata, std::locale::classic())) { - argu_desc.arg_name.push_back(*pdata); - pdata += 1; - } - matched = !argu_desc.arg_name.empty() && *pdata == '\0'; - } - return argu_desc; -} - -#else // CXXOPTS_NO_REGEX - -namespace { -CXXOPTS_LINKONCE -const char* const integer_pattern = "(-)?(0x)?([0-9a-zA-Z]+)|((0x)?0)"; -CXXOPTS_LINKONCE -const char* const truthy_pattern = "(t|T)(rue)?|1"; -CXXOPTS_LINKONCE -const char* const falsy_pattern = "(f|F)(alse)?|0"; -CXXOPTS_LINKONCE -const char* const option_pattern = - "--([[:alnum:]][-_[:alnum:]\\.]+)(=(.*))?|-([[:alnum:]].*)"; -CXXOPTS_LINKONCE -const char* const option_specifier_pattern = - "([[:alnum:]][-_[:alnum:]\\.]*)(,[ ]*[[:alnum:]][-_[:alnum:]]*)*"; -CXXOPTS_LINKONCE -const char* const option_specifier_separator_pattern = ", *"; - -} // namespace - -inline IntegerDesc SplitInteger(const std::string& text) { - static const std::basic_regex integer_matcher(integer_pattern); - - std::smatch match; - std::regex_match(text, match, integer_matcher); - - if (match.length() == 0) { - throw_or_mimic(text); - } - - IntegerDesc desc; - desc.negative = match[1]; - desc.base = match[2]; - desc.value = match[3]; - - if (match.length(4) > 0) { - desc.base = match[5]; - desc.value = "0"; - return desc; - } - - return desc; -} - -inline bool IsTrueText(const std::string& text) { - static const std::basic_regex truthy_matcher(truthy_pattern); - std::smatch result; - std::regex_match(text, result, truthy_matcher); - return !result.empty(); -} - -inline bool IsFalseText(const std::string& text) { - static const std::basic_regex falsy_matcher(falsy_pattern); - std::smatch result; - std::regex_match(text, result, falsy_matcher); - return !result.empty(); -} - -// Gets the option names specified via a single, comma-separated string, -// and returns the separate, space-discarded, non-empty names -// (without considering which or how many are single-character) -inline OptionNames split_option_names(const std::string& text) { - static const std::basic_regex option_specifier_matcher( - option_specifier_pattern); - if (!std::regex_match(text.c_str(), option_specifier_matcher)) { - throw_or_mimic(text); - } - - OptionNames split_names; - - static const std::basic_regex option_specifier_separator_matcher( - option_specifier_separator_pattern); - constexpr int use_non_matches{-1}; - auto token_iterator = std::sregex_token_iterator( - text.begin(), text.end(), option_specifier_separator_matcher, - use_non_matches); - std::copy(token_iterator, std::sregex_token_iterator(), - std::back_inserter(split_names)); - return split_names; -} - -inline ArguDesc ParseArgument(const char* arg, bool& matched) { - static const std::basic_regex option_matcher(option_pattern); - std::match_results result; - std::regex_match(arg, result, option_matcher); - matched = !result.empty(); - - ArguDesc argu_desc; - if (matched) { - argu_desc.arg_name = result[1].str(); - argu_desc.set_value = result[2].length() > 0; - argu_desc.value = result[3].str(); - if (result[4].length() > 0) { - argu_desc.grouping = true; - argu_desc.arg_name = result[4].str(); - } - } - - return argu_desc; -} - -#endif // CXXOPTS_NO_REGEX -#undef CXXOPTS_NO_REGEX -} // namespace parser_tool - -namespace detail { - -template -struct SignedCheck; - -template -struct SignedCheck { - template - void operator()(bool negative, U u, const std::string& text) { - if (negative) { - if (u > static_cast((std::numeric_limits::min)())) { - throw_or_mimic(text); - } - } else { - if (u > static_cast((std::numeric_limits::max)())) { - throw_or_mimic(text); - } - } - } -}; - -template -struct SignedCheck { - template - void operator()(bool, U, const std::string&) const {} -}; - -template -void check_signed_range(bool negative, U value, const std::string& text) { - SignedCheck::is_signed>()(negative, value, text); -} - -} // namespace detail - -template -void checked_negate(R& r, T&& t, const std::string&, std::true_type) { - // if we got to here, then `t` is a positive number that fits into - // `R`. So to avoid MSVC C4146, we first cast it to `R`. - // See https://github.com/jarro2783/cxxopts/issues/62 for more details. - r = static_cast(-static_cast(t - 1) - 1); -} - -template -void checked_negate(R&, T&&, const std::string& text, std::false_type) { - throw_or_mimic(text); -} - -template -void integer_parser(const std::string& text, T& value) { - parser_tool::IntegerDesc int_desc = parser_tool::SplitInteger(text); - - using US = typename std::make_unsigned::type; - constexpr bool is_signed = std::numeric_limits::is_signed; - - const bool negative = int_desc.negative.length() > 0; - const uint8_t base = int_desc.base.length() > 0 ? 16 : 10; - const std::string& value_match = int_desc.value; - - US result = 0; - - for (char ch : value_match) { - US digit = 0; - - if (ch >= '0' && ch <= '9') { - digit = static_cast(ch - '0'); - } else if (base == 16 && ch >= 'a' && ch <= 'f') { - digit = static_cast(ch - 'a' + 10); - } else if (base == 16 && ch >= 'A' && ch <= 'F') { - digit = static_cast(ch - 'A' + 10); - } else { - throw_or_mimic(text); - } - - const US next = static_cast(result * base + digit); - if (result > next) { - throw_or_mimic(text); - } - - result = next; - } - - detail::check_signed_range(negative, result, text); - - if (negative) { - checked_negate(value, result, text, - std::integral_constant()); - } else { - value = static_cast(result); - } -} - -template -void stringstream_parser(const std::string& text, T& value) { - std::stringstream in(text); - in >> value; - if (!in) { - throw_or_mimic(text); - } -} - -template ::value>::type* = nullptr> -void parse_value(const std::string& text, T& value) { - integer_parser(text, value); -} - -inline void parse_value(const std::string& text, bool& value) { - if (parser_tool::IsTrueText(text)) { - value = true; - return; - } - - if (parser_tool::IsFalseText(text)) { - value = false; - return; - } - - throw_or_mimic(text); -} - -inline void parse_value(const std::string& text, std::string& value) { - value = text; -} - -// The fallback parser. It uses the stringstream parser to parse all types -// that have not been overloaded explicitly. It has to be placed in the -// source code before all other more specialized templates. -template ::value>::type* = nullptr> -void parse_value(const std::string& text, T& value) { - stringstream_parser(text, value); -} - -template -void parse_value(const std::string& text, std::vector& value) { - if (text.empty()) { - T v; - parse_value(text, v); - value.emplace_back(std::move(v)); - return; - } - std::stringstream in(text); - std::string token; - while (!in.eof() && std::getline(in, token, CXXOPTS_VECTOR_DELIMITER)) { - T v; - parse_value(token, v); - value.emplace_back(std::move(v)); - } -} - -template -void add_value(const std::string& text, T& value) { - parse_value(text, value); -} - -template -void add_value(const std::string& text, std::vector& value) { - T v; - add_value(text, v); - value.emplace_back(std::move(v)); -} - -#ifdef CXXOPTS_HAS_OPTIONAL -template -void parse_value(const std::string& text, std::optional& value) { - T result; - parse_value(text, result); - value = std::move(result); -} -#endif - -inline void parse_value(const std::string& text, char& c) { - if (text.length() != 1) { - throw_or_mimic(text); - } - - c = text[0]; -} - -template -struct type_is_container { - static constexpr bool value = false; -}; - -template -struct type_is_container> { - static constexpr bool value = true; -}; - -template -class abstract_value : public Value { - using Self = abstract_value; - - public: - abstract_value() : m_result(std::make_shared()), m_store(m_result.get()) {} - - explicit abstract_value(T* t) : m_store(t) {} - - ~abstract_value() override = default; - - abstract_value& operator=(const abstract_value&) = default; - - abstract_value(const abstract_value& rhs) { - if (rhs.m_result) { - m_result = std::make_shared(); - m_store = m_result.get(); - } else { - m_store = rhs.m_store; - } - - m_default = rhs.m_default; - m_implicit = rhs.m_implicit; - m_default_value = rhs.m_default_value; - m_implicit_value = rhs.m_implicit_value; - } - - void add(const std::string& text) const override { - add_value(text, *m_store); - } - - void parse(const std::string& text) const override { - parse_value(text, *m_store); - } - - bool is_container() const override { return type_is_container::value; } - - void parse() const override { parse_value(m_default_value, *m_store); } - - bool has_default() const override { return m_default; } - - bool has_implicit() const override { return m_implicit; } - - std::shared_ptr default_value(const std::string& value) override { - m_default = true; - m_default_value = value; - return shared_from_this(); - } - - std::shared_ptr implicit_value(const std::string& value) override { - m_implicit = true; - m_implicit_value = value; - return shared_from_this(); - } - - std::shared_ptr no_implicit_value() override { - m_implicit = false; - return shared_from_this(); - } - - std::string get_default_value() const override { return m_default_value; } - - std::string get_implicit_value() const override { return m_implicit_value; } - - bool is_boolean() const override { return std::is_same::value; } - - const T& get() const { - if (m_store == nullptr) { - return *m_result; - } - return *m_store; - } - - protected: - std::shared_ptr m_result{}; - T* m_store{}; - - bool m_default = false; - bool m_implicit = false; - - std::string m_default_value{}; - std::string m_implicit_value{}; -}; - -template -class standard_value : public abstract_value { - public: - using abstract_value::abstract_value; - - CXXOPTS_NODISCARD - std::shared_ptr clone() const override { - return std::make_shared>(*this); - } -}; - -template <> -class standard_value : public abstract_value { - public: - ~standard_value() override = default; - - standard_value() { set_default_and_implicit(); } - - explicit standard_value(bool* b) : abstract_value(b) { - m_implicit = true; - m_implicit_value = "true"; - } - - std::shared_ptr clone() const override { - return std::make_shared>(*this); - } - - private: - void set_default_and_implicit() { - m_default = true; - m_default_value = "false"; - m_implicit = true; - m_implicit_value = "true"; - } -}; - -} // namespace values - -template -std::shared_ptr value() { - return std::make_shared>(); -} - -template -std::shared_ptr value(T& t) { - return std::make_shared>(&t); -} - -class OptionAdder; - -CXXOPTS_NODISCARD -inline const std::string& first_or_empty(const OptionNames& long_names) { - static const std::string empty{""}; - return long_names.empty() ? empty : long_names.front(); -} - -class OptionDetails { - public: - OptionDetails(std::string short_, OptionNames long_, String desc, - std::shared_ptr val) - : m_short(std::move(short_)), - m_long(std::move(long_)), - m_desc(std::move(desc)), - m_value(std::move(val)), - m_count(0) { - m_hash = std::hash{}(first_long_name() + m_short); - } - - OptionDetails(const OptionDetails& rhs) - : m_desc(rhs.m_desc), - m_value(rhs.m_value->clone()), - m_count(rhs.m_count) {} - - OptionDetails(OptionDetails&& rhs) = default; - - CXXOPTS_NODISCARD - const String& description() const { return m_desc; } - - CXXOPTS_NODISCARD - const Value& value() const { return *m_value; } - - CXXOPTS_NODISCARD - std::shared_ptr make_storage() const { return m_value->clone(); } - - CXXOPTS_NODISCARD - const std::string& short_name() const { return m_short; } - - CXXOPTS_NODISCARD - const std::string& first_long_name() const { return first_or_empty(m_long); } - - CXXOPTS_NODISCARD - const std::string& essential_name() const { - return m_long.empty() ? m_short : m_long.front(); - } - - CXXOPTS_NODISCARD - const OptionNames& long_names() const { return m_long; } - - std::size_t hash() const { return m_hash; } - - private: - std::string m_short{}; - OptionNames m_long{}; - String m_desc{}; - std::shared_ptr m_value{}; - int m_count; - - std::size_t m_hash{}; -}; - -struct HelpOptionDetails { - std::string s; - OptionNames l; - String desc; - bool has_default; - std::string default_value; - bool has_implicit; - std::string implicit_value; - std::string arg_help; - bool is_container; - bool is_boolean; -}; - -struct HelpGroupDetails { - std::string name{}; - std::string description{}; - std::vector options{}; -}; - -class OptionValue { - public: - void add(const std::shared_ptr& details, - const std::string& text) { - ensure_value(details); - ++m_count; - m_value->add(text); - m_long_names = &details->long_names(); - } - - void parse(const std::shared_ptr& details, - const std::string& text) { - ensure_value(details); - ++m_count; - m_value->parse(text); - m_long_names = &details->long_names(); - } - - void parse_default(const std::shared_ptr& details) { - ensure_value(details); - m_default = true; - m_long_names = &details->long_names(); - m_value->parse(); - } - - void parse_no_value(const std::shared_ptr& details) { - m_long_names = &details->long_names(); - } - -#if defined(CXXOPTS_NULL_DEREF_IGNORE) - CXXOPTS_DIAGNOSTIC_PUSH - CXXOPTS_IGNORE_WARNING("-Wnull-dereference") -#endif - - CXXOPTS_NODISCARD - std::size_t count() const noexcept { return m_count; } - -#if defined(CXXOPTS_NULL_DEREF_IGNORE) - CXXOPTS_DIAGNOSTIC_POP -#endif - - // TODO: maybe default options should count towards the number of arguments - CXXOPTS_NODISCARD - bool has_default() const noexcept { return m_default; } - - template - const T& as() const { - if (m_value == nullptr) { - throw_or_mimic( - m_long_names == nullptr ? "" : first_or_empty(*m_long_names)); - } - - return CXXOPTS_RTTI_CAST&>(*m_value).get(); - } - - private: - void ensure_value(const std::shared_ptr& details) { - if (m_value == nullptr) { - m_value = details->make_storage(); - } - } - - const OptionNames* m_long_names = nullptr; - // Holding this pointer is safe, since OptionValue's only exist in key-value - // pairs, where the key has the string we point to. - std::shared_ptr m_value{}; - std::size_t m_count = 0; - bool m_default = false; -}; - -class KeyValue { - public: - KeyValue(std::string key_, std::string value_) noexcept - : m_key(std::move(key_)), m_value(std::move(value_)) {} - - CXXOPTS_NODISCARD - const std::string& key() const { return m_key; } - - CXXOPTS_NODISCARD - const std::string& value() const { return m_value; } - - template - T as() const { - T result; - values::parse_value(m_value, result); - return result; - } - - private: - std::string m_key; - std::string m_value; -}; - -using ParsedHashMap = std::unordered_map; -using NameHashMap = std::unordered_map; - -class ParseResult { - public: - class Iterator { - public: - using iterator_category = std::forward_iterator_tag; - using value_type = KeyValue; - using difference_type = void; - using pointer = const KeyValue*; - using reference = const KeyValue&; - - Iterator() = default; - Iterator(const Iterator&) = default; - - // GCC complains about m_iter not being initialised in the member - // initializer list - CXXOPTS_DIAGNOSTIC_PUSH - CXXOPTS_IGNORE_WARNING("-Weffc++") - Iterator(const ParseResult* pr, bool end = false) : m_pr(pr) { - if (end) { - m_sequential = false; - m_iter = m_pr->m_defaults.end(); - } else { - m_sequential = true; - m_iter = m_pr->m_sequential.begin(); - - if (m_iter == m_pr->m_sequential.end()) { - m_sequential = false; - m_iter = m_pr->m_defaults.begin(); - } - } - } - CXXOPTS_DIAGNOSTIC_POP - - Iterator& operator++() { - ++m_iter; - if (m_sequential && m_iter == m_pr->m_sequential.end()) { - m_sequential = false; - m_iter = m_pr->m_defaults.begin(); - return *this; - } - return *this; - } - - Iterator operator++(int) { - Iterator retval = *this; - ++(*this); - return retval; - } - - bool operator==(const Iterator& other) const { - return (m_sequential == other.m_sequential) && (m_iter == other.m_iter); - } - - bool operator!=(const Iterator& other) const { return !(*this == other); } - - const KeyValue& operator*() { return *m_iter; } - - const KeyValue* operator->() { return m_iter.operator->(); } - - private: - const ParseResult* m_pr; - std::vector::const_iterator m_iter; - bool m_sequential = true; - }; - - ParseResult() = default; - ParseResult(const ParseResult&) = default; - - ParseResult(NameHashMap&& keys, ParsedHashMap&& values, - std::vector sequential, - std::vector default_opts, - std::vector&& unmatched_args) - : m_keys(std::move(keys)), - m_values(std::move(values)), - m_sequential(std::move(sequential)), - m_defaults(std::move(default_opts)), - m_unmatched(std::move(unmatched_args)) {} - - ParseResult& operator=(ParseResult&&) = default; - ParseResult& operator=(const ParseResult&) = default; - - Iterator begin() const { return Iterator(this); } - - Iterator end() const { return Iterator(this, true); } - - std::size_t count(const std::string& o) const { - auto iter = m_keys.find(o); - if (iter == m_keys.end()) { - return 0; - } - - auto viter = m_values.find(iter->second); - - if (viter == m_values.end()) { - return 0; - } - - return viter->second.count(); - } - - const OptionValue& operator[](const std::string& option) const { - auto iter = m_keys.find(option); - - if (iter == m_keys.end()) { - throw_or_mimic(option); - } - - auto viter = m_values.find(iter->second); - - if (viter == m_values.end()) { - throw_or_mimic(option); - } - - return viter->second; - } - - const std::vector& arguments() const { return m_sequential; } - - const std::vector& unmatched() const { return m_unmatched; } - - const std::vector& defaults() const { return m_defaults; } - - const std::string arguments_string() const { - std::string result; - for (const auto& kv : m_sequential) { - result += kv.key() + " = " + kv.value() + "\n"; - } - for (const auto& kv : m_defaults) { - result += kv.key() + " = " + kv.value() + " " + "(default)" + "\n"; - } - return result; - } - - private: - NameHashMap m_keys{}; - ParsedHashMap m_values{}; - std::vector m_sequential{}; - std::vector m_defaults{}; - std::vector m_unmatched{}; -}; - -struct Option { - Option(std::string opts, std::string desc, - std::shared_ptr value = ::cxxopts::value(), - std::string arg_help = "") - : opts_(std::move(opts)), - desc_(std::move(desc)), - value_(std::move(value)), - arg_help_(std::move(arg_help)) {} - - std::string opts_; - std::string desc_; - std::shared_ptr value_; - std::string arg_help_; -}; - -using OptionMap = - std::unordered_map>; -using PositionalList = std::vector; -using PositionalListIterator = PositionalList::const_iterator; - -class OptionParser { - public: - OptionParser(const OptionMap& options, const PositionalList& positional, - bool allow_unrecognised) - : m_options(options), - m_positional(positional), - m_allow_unrecognised(allow_unrecognised) {} - - ParseResult parse(int argc, const char* const* argv); - - bool consume_positional(const std::string& a, PositionalListIterator& next); - - void checked_parse_arg(int argc, const char* const* argv, int& current, - const std::shared_ptr& value, - const std::string& name); - - void add_to_option(const std::shared_ptr& value, - const std::string& arg); - - void parse_option(const std::shared_ptr& value, - const std::string& name, const std::string& arg = ""); - - void parse_default(const std::shared_ptr& details); - - void parse_no_value(const std::shared_ptr& details); - - private: - void finalise_aliases(); - - const OptionMap& m_options; - const PositionalList& m_positional; - - std::vector m_sequential{}; - std::vector m_defaults{}; - bool m_allow_unrecognised; - - ParsedHashMap m_parsed{}; - NameHashMap m_keys{}; -}; - -class Options { - public: - explicit Options(std::string program_name, std::string help_string = "") - : m_program(std::move(program_name)), - m_help_string(toLocalString(std::move(help_string))), - m_custom_help("[OPTION...]"), - m_positional_help("positional parameters"), - m_show_positional(false), - m_allow_unrecognised(false), - m_width(76), - m_tab_expansion(false), - m_options(std::make_shared()) {} - - Options& positional_help(std::string help_text) { - m_positional_help = std::move(help_text); - return *this; - } - - Options& custom_help(std::string help_text) { - m_custom_help = std::move(help_text); - return *this; - } - - Options& show_positional_help() { - m_show_positional = true; - return *this; - } - - Options& allow_unrecognised_options() { - m_allow_unrecognised = true; - return *this; - } - - Options& set_width(std::size_t width) { - m_width = width; - return *this; - } - - Options& set_tab_expansion(bool expansion = true) { - m_tab_expansion = expansion; - return *this; - } - - ParseResult parse(int argc, const char* const* argv); - - OptionAdder add_options(std::string group = ""); - - void add_options(const std::string& group, - std::initializer_list