-
Notifications
You must be signed in to change notification settings - Fork 3
DeepDebug: McMini, DMTCP and TSAN #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
gc00
wants to merge
43
commits into
mcminickpt:main
Choose a base branch
from
gc00:mcmini-dmtcp-tsan
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
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 2df3f55
Joined threads now terminate, not park forever
gc00 0bd036b
Fix pthread_map_lock leak in search_pthread_map()
gc00 b53a77b
DPOR backtrack replay: Report abnormal termination
gc00 39d9823
Port libmcmini's DMTCP plugin to API v4, from v3
gc00 9bd964d
Fix restart-barrier race in template_thread()
gc00 9d56beb
Don't resume checkpoint loop after branch restart
gc00 fbc3cde
Remove dead coordinator-shutdown cleanup code
gc00 0613e44
Stop leaking stale SIGCHLD into next branch
gc00 0808df6
Fix exit() hang and false-deadlock report
gc00 aa3b2b0
Wire up nonzero_exit_code callback
gc00 0a7a5f9
Sem desync fix: switch to a plain futex word
gc00 a74fb0b
Fix restart-quiescence bypass on plain return
gc00 0440193
Fix CV restart deadlock: mutex drops location
gc00 fc8f262
Fix mc_pthread_cond_wait's restart double-call abort
gc00 9d64042
Harden mutex constructor against dropped location
gc00 6be89b0
Harden condition_variable constructor like mutex's
gc00 92a8316
CV desync fix: stop touching cond_t after restart
gc00 f7c6a27
Add cv-producer-consumer to test CV desync fix
gc00 a4ca154
Fix RECORD-mode retry loops' stale deadline bug
gc00 017f929
Track mutex owner for cond_wait restart rebuild
gc00 b9cc8e8
Clone CV policy before mutating it
gc00 867df32
Add MCMINI_LIBMCMINI_TSAN build option
gc00 0ad1e77
Make RECORD-mode thread creation TSan-safe
gc00 31bc62a
Register libmcmini's template thread with TSan
gc00 3e99161
Make multithreaded-fork's thread recreation TSan-safe
gc00 3718dfb
Add thread_blocks_signal() TSan probe
gc00 b4e8428
Classify checkpoint vs TSan-helper thread lazily
gc00 530a782
Fix TSAN ThreadState crashes before registration
gc00 053a077
Rename CHECKPOINT_THREAD to EXTERNAL_THREAD
gc00 51750f9
Skip TSan-internal check when unneeded
gc00 eda4b4b
Win the race against TSan: --wrap=pthread_join
gc00 69400ab
Warn when a TSan target lacks --wrap=pthread_join
gc00 e1e01a7
Add mc_pthread_exit() interceptor
gc00 0d9906d
Add exit-stress(-noop)-tsan targets
gc00 94c5af6
Serialize mcmini_log() and make TSan see the lock
gc00 2e9df55
Fix false TSan races on mutex-protected globals
gc00 0f1e323
Add producer-consumer-tsan CMake target
gc00 b6f03bb
Use --wrap=pthread_cond_wait for DMTCP restart
gc00 2de0367
MULTITHR-FORK: Reset log mutex after _Fork()
gc00 b4ca5a7
Add -safe targets to test the TSan mutex-race fix
gc00 9f9182f
Wrap pthread_cond_signal for DMTCP restart
gc00 5b6fac7
Add check-tsan target and multi-producer/consumer CV stress test
gc00 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 inpthread_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