Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
ac1f54b
Bypass libtsan in mc_pthread_join's timed join
gc00 Jul 1, 2026
2df3f55
Joined threads now terminate, not park forever
gc00 Jul 26, 2026
0bd036b
Fix pthread_map_lock leak in search_pthread_map()
gc00 Jul 26, 2026
b53a77b
DPOR backtrack replay: Report abnormal termination
gc00 Jul 27, 2026
39d9823
Port libmcmini's DMTCP plugin to API v4, from v3
gc00 Jul 1, 2026
9bd964d
Fix restart-barrier race in template_thread()
gc00 Jul 27, 2026
9d56beb
Don't resume checkpoint loop after branch restart
gc00 Jul 26, 2026
fbc3cde
Remove dead coordinator-shutdown cleanup code
gc00 Jul 27, 2026
0613e44
Stop leaking stale SIGCHLD into next branch
gc00 Jul 27, 2026
0808df6
Fix exit() hang and false-deadlock report
gc00 Jul 28, 2026
aa3b2b0
Wire up nonzero_exit_code callback
gc00 Jul 28, 2026
0a7a5f9
Sem desync fix: switch to a plain futex word
gc00 Jul 31, 2026
a74fb0b
Fix restart-quiescence bypass on plain return
gc00 Jul 28, 2026
0440193
Fix CV restart deadlock: mutex drops location
gc00 Jul 28, 2026
fc8f262
Fix mc_pthread_cond_wait's restart double-call abort
gc00 Jul 31, 2026
9d64042
Harden mutex constructor against dropped location
gc00 Jul 28, 2026
6be89b0
Harden condition_variable constructor like mutex's
gc00 Jul 28, 2026
92a8316
CV desync fix: stop touching cond_t after restart
gc00 Jul 31, 2026
f7c6a27
Add cv-producer-consumer to test CV desync fix
gc00 Jul 31, 2026
a4ca154
Fix RECORD-mode retry loops' stale deadline bug
gc00 Jul 27, 2026
017f929
Track mutex owner for cond_wait restart rebuild
gc00 Jul 27, 2026
b9cc8e8
Clone CV policy before mutating it
gc00 Jul 28, 2026
867df32
Add MCMINI_LIBMCMINI_TSAN build option
gc00 Jul 24, 2026
0ad1e77
Make RECORD-mode thread creation TSan-safe
gc00 Jul 1, 2026
31bc62a
Register libmcmini's template thread with TSan
gc00 Jul 2, 2026
3e99161
Make multithreaded-fork's thread recreation TSan-safe
gc00 Jul 5, 2026
3718dfb
Add thread_blocks_signal() TSan probe
gc00 Jul 5, 2026
b4e8428
Classify checkpoint vs TSan-helper thread lazily
gc00 Jul 25, 2026
530a782
Fix TSAN ThreadState crashes before registration
gc00 Jul 30, 2026
053a077
Rename CHECKPOINT_THREAD to EXTERNAL_THREAD
gc00 Jul 24, 2026
51750f9
Skip TSan-internal check when unneeded
gc00 Jul 24, 2026
eda4b4b
Win the race against TSan: --wrap=pthread_join
gc00 Jul 31, 2026
69400ab
Warn when a TSan target lacks --wrap=pthread_join
gc00 Jul 27, 2026
e1e01a7
Add mc_pthread_exit() interceptor
gc00 Jul 28, 2026
0d9906d
Add exit-stress(-noop)-tsan targets
gc00 Jul 30, 2026
94c5af6
Serialize mcmini_log() and make TSan see the lock
gc00 Jul 27, 2026
2e9df55
Fix false TSan races on mutex-protected globals
gc00 Jul 28, 2026
0f1e323
Add producer-consumer-tsan CMake target
gc00 Jul 27, 2026
b6f03bb
Use --wrap=pthread_cond_wait for DMTCP restart
gc00 Jul 27, 2026
2de0367
MULTITHR-FORK: Reset log mutex after _Fork()
gc00 Jul 27, 2026
b4ca5a7
Add -safe targets to test the TSan mutex-race fix
gc00 Jul 28, 2026
9f9182f
Wrap pthread_cond_signal for DMTCP restart
gc00 Jul 31, 2026
5b6fac7
Add check-tsan target and multi-producer/consumer CV stress test
gc00 Aug 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED TRUE)
# Project configuration
option(BUILD_TESTS OFF)
option(VERBOSE_TESTING OFF)
option(MCMINI_LIBMCMINI_TSAN "Build libmcmini.so itself with -fsanitize=thread" OFF)
set(MCMINI_DIR "${CMAKE_SOURCE_DIR}")
set(MCMINI_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/include")
set(MCMINI_CMAKE_MODULE_DIR "${CMAKE_SOURCE_DIR}/cmake")
Expand Down Expand Up @@ -74,6 +75,7 @@ set(LIBMCMINI_C_SRC
src/lib/sem-wrappers.c
src/lib/template/loop.c
src/lib/template/sig.c
src/lib/tsan_support.c
src/lib/wrappers.c
src/mcmini/Thread_queue.c
)
Expand All @@ -91,6 +93,13 @@ set(LIBMCMINI_EXTRA_COMPILER_DEFINITIONS MC_SHARED_LIBRARY=1 DMTCP=1 LOGGING_ROO
# -ldl -> dlsym etc.
set(LIBMCMINI_EXTRA_LINK_FLAGS -lrt -pthread -lm -ldl)

# libmcmini.so is normally uninstrumented even when the target links libtsan
# (see PLAN.txt); MCMINI_LIBMCMINI_TSAN is an experiment to instrument it too.
if(MCMINI_LIBMCMINI_TSAN)
list(APPEND LIBMCMINI_EXTRA_COMPILER_FLAGS -fsanitize=thread)
list(APPEND LIBMCMINI_EXTRA_LINK_FLAGS -fsanitize=thread)
endif()

# libmcmini.so -> the dylib which is loaded
add_library(libmcmini SHARED "${LIBMCMINI_C_SRC}")
set_target_properties(libmcmini PROPERTIES OUTPUT_NAME "mcmini")
Expand Down Expand Up @@ -120,3 +129,19 @@ target_link_libraries(mcmini

add_subdirectory(src/examples)
add_subdirectory(test)

# `make check-tsan` builds and runs the TSan example targets that exercise
# McMini's DMTCP+TSan integration through a checkpoint/restart cycle (see
# script/check-tsan.sh). Needs dmtcp_launch/dmtcp_restart on PATH, or a local
# DMTCP build's bin/ dir passed as DMTCP_BIN_DIR (defaults to dmtcp.git/bin
# alongside this CMakeLists.txt, if present).
if(NOT DEFINED DMTCP_BIN_DIR AND EXISTS "${CMAKE_SOURCE_DIR}/dmtcp.git/bin")
set(DMTCP_BIN_DIR "${CMAKE_SOURCE_DIR}/dmtcp.git/bin")
endif()

add_custom_target(check-tsan
COMMAND ${CMAKE_SOURCE_DIR}/script/check-tsan.sh
${CMAKE_BINARY_DIR} ${DMTCP_BIN_DIR}
DEPENDS mcmini libmcmini cv-producer-consumer-multi-tsan
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
USES_TERMINAL)
108 changes: 108 additions & 0 deletions doc/classic-mode-thread-registration-segv.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
Classic-mode SEGV: new application threads never registered with TSan
=============================================================================

Symptom
--------
Running any TSan-instrumented target (producer-consumer-tsan,
cv-producer-consumer-tsan) under classic (non-DMTCP) model checking --
`mcmini ./target`, no `-i`/`--from-checkpoint` involved at all -- crashed
100% of the time, deterministically, the moment the target's first
non-main thread reached its first `libpthread_*`-wrapped call:

ThreadSanitizer:DEADLYSIGNAL
==PID==ERROR: ThreadSanitizer: SEGV on unknown address 0x000000000000
==PID==The signal is caused by a READ memory access.
==PID==Hint: address points to the zero page.
ThreadSanitizer: nested bug in the same thread, aborting.

TSan's own crash handler could not even produce a symbolicated backtrace
(a second fault while handling the first -- itself a symptom of the same
root cause: the crashing thread has no valid per-thread TSan state, and
neither does TSan's own signal-handling code when it tries to use that
same state to report the crash).

Root cause
-----------
Caught live via `gdb --args ./mcmini ./producer-consumer-tsan` with
`set follow-fork-mode child`, `set detach-on-fork off`, and
`set schedule-multiple on` (needed, so the parent `mcmini` process keeps
running -- classic mode requires it to actively coordinate with the
branch process, so freezing it while continuing only the child deadlocks
instead of crashing). `handle SIGSEGV stop nopass` lets gdb catch the
fault before TSan's own handler consumes it. Full backtrace of the
crashing thread:

#0 __tsan::SlotLock (thr=0x...) at tsan_rtl.cpp:366
#1 __tsan::SlotLocker::SlotLocker (...) at tsan_rtl.h:641
#2 __tsan::Acquire (thr=..., pc=..., addr=...) at tsan_rtl_mutex.cpp:448
#3 guard_acquire (thr=..., g=<libmcini_init>, ...) at tsan_interceptors_posix.cpp:891
#4 ___interceptor_pthread_once (o=<libmcini_init>, f=mc_load_intercepted_pthread_functions)
#5 libmcmini_init () from ./libmcmini.so
#6 libpthread_mutex_lock () from ./libmcmini.so
#7 mc_register_this_thread () from ./libmcmini.so
#8 mc_thread_routine_wrapper () from ./libmcmini.so
#9 start_thread
#10 clone3

The chain: `mc_thread_routine_wrapper()` (the new thread's actual OS-level
entry point) calls `mc_register_this_thread()` as its first statement,
which calls `libpthread_mutex_lock()`, which calls `libmcmini_init()`,
which lazily resolves all the `libpthread_*` bypass handles exactly once
via `pthread_once()`. `pthread_once` is a public, dynamically-resolved
symbol that TSan intercepts -- and TSan's interceptor needs this thread's
own `ThreadState` to do its own bookkeeping. But this thread was never
registered with TSan in the first place: `mc_pthread_create()`'s
TARGET_BRANCH/TARGET_BRANCH_AFTER_RESTART case (src/lib/wrappers.c)
created it via `libpthread_pthread_create()` -- a handle resolved via
`dlopen("libpthread")` + `dlsym()`, which always finds real, raw glibc,
bypassing *any* interceptor (TSan's or libmcmini's own) unconditionally,
regardless of load order. TSan's own `pthread_create` interceptor -- the
thing that would normally set up this thread's `ThreadState` -- never ran
for this thread at all.

The comment above the original call ("Calling libpthread_pthread_create
simplifies all this") suggests this was chosen for convenience, not as a
deliberate TSan-avoidance measure -- unlike the `__clone`-based recreation
work for DMTCP-restarted threads (doc files elsewhere in this directory),
which deliberately bypasses TSan's interceptor for well-documented,
load-order-specific reasons. This one had no such reason and was simply a
bug: classic mode has no DMTCP/checkpoint-restart concerns at all, so
there was never a reason to avoid TSan's own registration for these
threads.

The fix
--------
Added `tsan_or_real_pthread_create()` (src/lib/interception.c), resolved
via `dlsym(RTLD_NEXT, "pthread_create")` instead of the libpthread-handle
dlsym used by `libpthread_pthread_create_ptr`. `RTLD_NEXT` finds whatever
comes *after* libmcmini.so in this process's actual, specific load order:

- Classic mode: libmcmini loads ahead of libtsan (see
TSAN-McMini-DMTCP.txt's own explanation of this load order), so
"next after libmcmini" correctly lands on TSan's own pthread_create
interceptor -- the fix.
- DMTCP mode: libtsan loads ahead of libmcmini, so "next after
libmcmini" still resolves past both and lands on real glibc, exactly
like libpthread_pthread_create's behavior today -- no change, no
regression risk for the already-extensively-verified DMTCP+TSan
restart flow (which handles TSan registration for its own
clone()-recreated threads via an entirely separate mechanism: R3/R4's
__clone + fresh-fiber approach, not pthread_create at all).

`mc_pthread_create()`'s TARGET_BRANCH/TARGET_BRANCH_AFTER_RESTART case
(src/lib/wrappers.c) now calls `tsan_or_real_pthread_create()` instead of
`libpthread_pthread_create()`.

Verification
-------------
- producer-consumer-tsan and cv-producer-consumer-tsan, classic mode: 100%
reproducible SEGV before the fix, zero crashes across multiple runs
after (cv-producer-consumer-tsan takes longer to fully explore -- more
interleavings from the mutex+cond-based synchronization -- but completes
cleanly given enough time).
- Classic-mode regression check: cv-test, deadly-embrace,
producer-consumer, and cv-producer-consumer (non-TSan) all give
unchanged results.
- DMTCP+TSan --multithreaded-fork restart flow (producer-consumer-tsan):
batch-verified unaffected, consistent with the RTLD_NEXT resolution
falling through to real glibc in that load order exactly as before.
72 changes: 72 additions & 0 deletions doc/cond-wait-tsan-interceptor-bypass.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
pthread_cond_wait bypasses libmcmini under DMTCP+TSan
=======================================================

Symptom
-------
A DMTCP-restart-from-checkpoint of a TSan-instrumented target hangs
forever as soon as any thread is genuinely blocked in pthread_cond_wait()
at checkpoint time. mcmini's own trace output never even reaches
"INITIAL STATE" for that thread; it just waits on a mailbox message that
never arrives.

Root cause
----------
Under DMTCP, libtsan.so loads ahead of libmcmini.so (the reverse of
classic mode), so the target's own calls to the public pthread_cond_wait()
symbol resolve to TSan's interceptor first, via the PLT/GOT -- confirmed
empirically by reading the resolved GOT entry out of /proc/pid/mem.

This is the same load-order problem pthread_join already had (see
pthread_join_wrap.c), but pthread_cond_wait can't be fixed the same way
sem_wait/pthread_mutex_lock/pthread_cond_signal/broadcast are handled:
those TSan interceptors do their own bookkeeping and then delegate the
actual operation to whatever's next in the interposition chain (landing
on libmcmini's mc_* wrapper, since it's next after libtsan under DMTCP).
pthread_cond_wait must atomically release the mutex and suspend the
thread, then atomically reacquire the mutex on wakeup; TSan implements
that whole sequence itself, inside its own runtime, rather than risk a
race window from splitting the atomicity across an interposed call. So
the target's pthread_cond_wait() calls never reach mc_pthread_cond_wait()
at all -- the thread genuinely, permanently blocks inside TSan's own real,
untimed wait, with nothing left to wake it (see glibc-cond-var-desync.txt
for why the earlier CV desync fix removed the only thing that could have).

Fix
---
-Wl,--wrap=pthread_cond_wait, applied when linking the TARGET (not
libmcmini.so), rewrites the target's own pthread_cond_wait() calls to
__wrap_pthread_cond_wait() at link time -- before the dynamic linker (and
hence TSan's interceptor) is ever involved. See pthread_cond_wait_wrap.c.
Unlike pthread_join, there's no "sometimes a real wait is still needed"
case: mc_pthread_cond_wait() is already fully self-contained in every
libmcmini_mode, so the wrapper forwards unconditionally with no
__real_pthread_cond_wait() fallback.

Verification
------------
Confirmed via GOT inspection that pthread_mutex_lock, pthread_mutex_unlock,
sem_post, sem_wait, and pthread_cond_wait all resolve to libtsan.so
directly with no exception -- the asymmetry is in TSan's internal
delegation behavior per function, not in symbol resolution.

Confirmed live: before the fix, restarting cv-producer-consumer-tsan from
a checkpoint taken mid-consumer-cond_wait hung indefinitely (trace log
truncated right after "waiting for them to get into a consistent state").
After the fix, the same restart completes promptly, and the consumer's
cond_wait shows up as a genuine, modeled NEXT THREAD OPERATIONS entry
instead of an invisible black-box call.

Follow-on issue (separate from this fix)
-----------------------------------------
That same verification run surfaced a second, previously-unreachable bug:
once the consumer's cond_wait is actually visible to the model, the
restart reports an immediate DEADLOCK. The reconstructed state shows the
mutex still "locked" by the consumer and the condition_variable still at
cv_initialized (never advanced to cv_waiting), so neither the producer's
pthread_mutex_lock nor the consumer's own resume-from-wait transition
(condition_variables_wait.hpp's modify()) can ever become enabled. This
looks like a bug in how the checkpoint/restart path reconstructs CV
waiter-queue state (the CV_PREWAITING/CV_WAITING bookkeeping described in
Cond_Var_Readme.md) rather than anything to do with the TSan-bypass fix
above, since this code path was never reachable before this fix landed.
Tracked as a separate, follow-on investigation.
Comment on lines +59 to +72

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Resolve the restored condition-variable state before merge.

Line 61 reports an immediate deadlock after the bypass succeeds. The restored mutex remains owned, and the condition variable never enters cv_waiting. A DMTCP+TSan target checkpointed in pthread_cond_wait() cannot restart successfully.

Restore the waiter queue and mutex ownership so the producer lock and consumer resume transitions become enabled.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@doc/cond-wait-tsan-interceptor-bypass.txt` around lines 59 - 72, Fix the
checkpoint/restart reconstruction path so condition-variable waiters are
restored with the correct CV_PREWAITING/CV_WAITING state and waiter queue, while
preserving the consumer’s mutex ownership until its wait transition resumes.
Ensure the reconstructed state enables both the producer’s pthread_mutex_lock
and condition_variables_wait.hpp modify() consumer-resume transition,
eliminating the immediate DEADLOCK after restart.

Loading