Skip to content

DeepDebug: McMini, DMTCP and TSAN - #11

Open
gc00 wants to merge 43 commits into
mcminickpt:mainfrom
gc00:mcmini-dmtcp-tsan
Open

DeepDebug: McMini, DMTCP and TSAN#11
gc00 wants to merge 43 commits into
mcminickpt:mainfrom
gc00:mcmini-dmtcp-tsan

Conversation

@gc00

@gc00 gc00 commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

DMTCP now supports TSAN. Working on porting this support to McMini/DeepDebug.
Part of the issue is that McMini, TSAN and DMTCP all have wrapper functions, and they can interfere with each other if we are not careful.

This includes PR #7, PR #12, etc. Then this commit. PR #12 is the first 6 commits of this PR.

Summary by CodeRabbit

  • New Features

    • Added optional ThreadSanitizer-instrumented library builds.
    • Added ThreadSanitizer-compatible threading, synchronization, checkpoint/restart, and fork handling.
    • Added producer-consumer, exit, parking, stress, and condition-variable examples, including safe variants.
    • Improved mutex ownership tracking and nonzero-exit diagnostics.
  • Bug Fixes

    • Fixed crashes, hangs, deadlocks, and synchronization inconsistencies during ThreadSanitizer and checkpoint/restart workflows.
    • Improved process-exit and thread-termination handling.
  • Documentation

    • Added troubleshooting and verification notes covering ThreadSanitizer, checkpoint/restart, synchronization, logging, and thread exits.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This change adds optional TSan instrumentation, TSan-aware thread and fork handling, futex mailbox synchronization, checkpoint model-state preservation, process-status handling, example targets, tests, and diagnostic documentation.

Changes

TSan and checkpoint/restart integration

Layer / File(s) Summary
Contracts and model state
include/dmtcp.h, include/mcmini/model/..., include/mcmini/spy/..., include/mcmini/mem.h
Public checkpoint, TSan, thread-wrapper, mutex, condition-variable, and external-thread interfaces are updated.
Runtime TSan support and interception
CMakeLists.txt, src/common/mem.c, src/lib/interception.c, src/lib/dmtcp-callback.c, src/lib/wrappers.c, src/lib/tsan_support.c, src/lib/log.c
TSan detection, fiber switching, raw clone and allocation paths, pthread forwarding, fork recovery, logging synchronization, and thread lifecycle handling are added.
Synchronization and model transitions
src/common/runner_mailbox.c, src/lib/sem-wrappers.c, src/lib/wrappers.c, include/mcmini/model/transitions/..., src/mcmini/model/transitions/...
Mailbox child signaling uses raw futex operations. Condition-variable and mutex transitions preserve policy, location, status, and ownership state.
Process and error handling
src/mcmini/mcmini.cpp, src/mcmini/model_checking/..., src/mcmini/real_world/..., src/common/multithreaded_fork.c
Process status handling, SIGCHLD filtering, termination callbacks, PID conversion, and post-fork logging reset are updated.
Examples, tests, and diagnostics
src/examples/*, test/tsan_support/*, doc/*
Producer-consumer, exit, parking, stress, and condition-variable examples and TSan targets are added. Diagnostic documents describe TSan and checkpoint/restart failures and fixes.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • mcminickpt/deep-debug#12: Modifies shared join, exit, interception, DMTCP callback, and model-checking error paths.
  • mcminickpt/deep-debug#13: Overlaps on futex semaphores, mutex and condition-variable state, DMTCP integration, and TSan support.

Suggested labels: bug

Suggested reviewers: maxwellpirtle

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: integrating McMini, DMTCP, and TSan support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gc00 gc00 added the enhancement New feature or request label Jul 25, 2026
@gc00
gc00 force-pushed the mcmini-dmtcp-tsan branch 6 times, most recently from 9f80a5d to a917800 Compare July 28, 2026 21:53
gc00 and others added 4 commits July 30, 2026 16:24
mc_pthread_join's RECORD loop called pthread_timedjoin_np directly,
which resolves to libtsan's interceptor under a TSAN target. Its
ConsumeThreadUserId trips a thread-registry CHECK
(sanitizer_thread_registry.cpp:348) and aborts.

Add a libpthread_timedjoin_np handle (dlsym'd from libpthread, like
the mutex/cond/sem wrappers) that bypasses libtsan, and call it from
mc_pthread_join's RECORD loop instead of the raw symbol.

This completes end-to-end TSAN-target checkpointing under deep-debug
(mcmini record mode), alongside 5be8500 (DMTCP plugin API v3->v4) and
4bf2720 (TSan-safe RECORD prologue).

Verified: `mcmini -i 3 ~/dmtcp.git/test/tsan_target` runs with no
SEGV/ThreadSanitizer errors, producing a valid checkpoint matching
the no-mcmini baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TSAN's pthread_join() interceptor delegates to a genuine OS-level
join and blocks via the kernel until the target thread actually
dies -- it does not rely on its own creation-time bookkeeping. But
mc_pthread_join()'s TARGET_BRANCH case only simulated success at the
model level, while the joined thread was kept parked in
thread_block_indefinitely() forever, so a real join on it (e.g. from
TSan) could never complete.

Give each thread its own exit_permission_sem (alongside its existing
pthread_map entry). A finishing thread waits on it before returning;
mc_pthread_join() posts it and performs a real libpthread_pthread_join()
before returning, and as a bonus, pthread_join returns a return value.
classic_dpor::verify_using()'s forward-exploration path catches
real_world::process::termination_error and reports it via the
abnormal_termination callback, letting the run end cleanly. The
backtrack-replay path (coordinator::return_to_depth(), which replays
prior transitions against a freshly restarted process) had no such
handling, so the same exception there escaped all the way to the
top-level catch-all instead.

Wrap return_to_depth() the same way. found_abnormal_termination()
also needed a null check: return_to_depth()'s target thread may have
no pending transition in the model's current view (unlike the forward
path, where the culprit is always the runner DPOR just selected as
enabled). The report then falls back to a plain "no longer pending"
line instead of dereferencing a null transition.
@gc00
gc00 force-pushed the mcmini-dmtcp-tsan branch from d990af5 to 603a44d Compare July 30, 2026 20:27
@gc00
gc00 force-pushed the mcmini-dmtcp-tsan branch 2 times, most recently from 3310863 to 6513b37 Compare July 31, 2026 06:00
@gc00
gc00 marked this pull request as ready for review July 31, 2026 06:07
Copilot AI review requested due to automatic review settings July 31, 2026 06:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR extends DeepDebug/McMini’s DMTCP integration to work with ThreadSanitizer (TSAN), addressing multiple interposition/order-of-initialization conflicts between McMini wrappers, TSAN interceptors, and DMTCP restart semantics.

Changes:

  • Hardened restart/branch execution paths to correctly handle thread/process termination, SIGCHLD attribution, and exit/join semantics under TSAN + DMTCP.
  • Added TSAN-specific support in libmcmini (fiber switching, annotations, TSAN-safe allocation/initialization, and target-side --wrap shims for interceptors that bypass interposition).
  • Added new TSAN-focused examples, tests, and extensive documentation of discovered failure modes and fixes.

Reviewed changes

Copilot reviewed 56 out of 56 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
test/tsan_support/test_thread_blocks_signal.c Standalone test for /proc-based signal-block detection helper.
src/mcmini/real_world/local_linux_process.cpp Improves SIGCHLD handling and distinguishes unrelated descendant deaths.
src/mcmini/real_world/dmtcp_process_source.cpp Removes DMTCP coordinator cleanup logic from destructor.
src/mcmini/model/transitions/mutex.cpp Preserves mutex location/id when creating model objects.
src/mcmini/model/transitions/condition_variables.cpp Simplifies condvar init and relies on constructor defaults for policy.
src/mcmini/model_checking/algorithms/classic_dpor.cpp Reports termination/nonzero exits during backtrack replay via callbacks.
src/mcmini/mcmini.cpp Restores mutex owner from checkpoints; adds nonzero-exit reporting callback.
src/lib/wrappers.c Major TSAN/DMTCP compatibility work: join/exit semantics, TSAN annotations, init ordering, CV behavior, etc.
src/lib/tsan_support.c Adds TSAN-safe helpers for thread classification and real TID detection.
src/lib/sem-wrappers.c Renames CHECKPOINT_THREAD handling to EXTERNAL_THREAD forwarding.
src/lib/record.c Adds TSAN-safe record entry allocation and TSAN-internal-thread gating in mode detection.
src/lib/pthread_join_wrap.c Target-side --wrap=pthread_join shim to bypass TSAN join hangs.
src/lib/pthread_cond_wait_wrap.c Target-side --wrap=pthread_cond_wait shim because TSAN bypasses interposition.
src/lib/pthread_cond_signal_wrap.c Target-side --wrap=pthread_cond_signal shim because TSAN bypasses interposition.
src/lib/main.c Adds early runtime warning for TSAN targets missing required --wrap join shim.
src/lib/log.c Makes logging thread-safe (tz init, lock) and adds post-_Fork reset hook; TSAN annotations.
src/lib/interception.c Adds TSAN-aware init fast path and new interposed functions (pthread_exit, timedjoin, clone, libc_start_main).
src/lib/dmtcp-callback.c TSAN fiber + fork syscalls hooks; safer clone usage; improved thread counting barrier; one-shot checkpoint thread behavior.
src/examples/producer-consumer-safe.c Race-free variant of producer/consumer example for TSAN signal-to-noise.
src/examples/producer-consumer-park.c Test target with alive-but-parked threads at process exit.
src/examples/producer-consumer-exit.c Test target explicitly exercising pthread_exit() paths.
src/examples/exit-stress.c Stress test for pthread_exit behavior at higher thread counts.
src/examples/exit-stress-noop.c Reliable reproducer for checkpoint-thread/TSAN background-thread init race.
src/examples/cv-producer-consumer.c Condition-variable based example target for restart/TSAN scenarios.
src/examples/cv-producer-consumer-safe.c Race-free condvar example variant for cleaner TSAN runs.
src/examples/CMakeLists.txt Adds many TSAN-instrumented example targets with required --wrap link flags and post-build copies.
src/common/runner_mailbox.c Replaces child-side glibc sem_t with raw futex-based counting semaphore to avoid desync.
src/common/multithreaded_fork.c Updates DMTCP API calls and resets log mutex post-_Fork in shared-library path.
src/common/mem.c Implements TSAN-safe bump allocator (mc_ts_alloc) for pre-TSAN-registration allocations.
include/mcmini/spy/intercept/wrappers.h Exposes new helpers: internal-thread creation flag, recreated-thread query, deferred join API, mc_pthread_exit.
include/mcmini/spy/intercept/interception.h Declares tsan_or_real_pthread_create, timedjoin handle, pthread_exit forwarding, and raw libc_clone.
include/mcmini/spy/checkpointing/tsan_support.h Declares TSAN support helpers used by mode detection and restart logic.
include/mcmini/spy/checkpointing/record.h Renames CHECKPOINT_THREAD to EXTERNAL_THREAD; exposes checkpoint-thread window helpers and TSAN-safe record allocation.
include/mcmini/spy/checkpointing/objects.h Extends mutex state to include owner for correct post-checkpoint reconstruction.
include/mcmini/real_world/process/dmtcp_process_source.hpp Updates includes and removes coordinator member / destructor.
include/mcmini/real_world/mailbox/runner_mailbox.h Changes child-side mailbox semaphore from sem_t to raw futex word.
include/mcmini/model/transitions/process/exit.hpp Marks exiting thread as exited to avoid DPOR re-selecting exit transitions.
include/mcmini/model/transitions/mutex/mutex_unlock.hpp Aligns mutex unlock transition with new mutex constructor signature.
include/mcmini/model/transitions/mutex/mutex_init.hpp Preserves mutex location when initializing model state.
include/mcmini/model/transitions/condition_variables/condition_variables_wait.hpp Avoids in-place policy mutation; clones policy for diff_state correctness.
include/mcmini/model/transitions/condition_variables/condition_variables_signal.hpp Avoids in-place policy mutation; preserves mutex association and policy.
include/mcmini/model/transitions/condition_variables/condition_variable_enqueue_thread.hpp Avoids in-place policy mutation; preserves mutex location; carries cloned policy forward.
include/mcmini/model/transitions/condition_variables/condition_variable_brdcast.hpp Avoids in-place policy mutation; uses cloned policy and preserves it in new CV object.
include/mcmini/model/objects/mutex.hpp Consolidates mutex constructors; enforces passing mutex location and tracks owner.
include/mcmini/model/objects/condition_variables.hpp Refactors constructors/policy handling to support policy cloning and state reconstruction.
include/mcmini/mem.h Declares TSAN-safe allocator API.
include/mcmini/lib/log.h Declares post-_Fork log mutex reset helper.
include/dmtcp.h Updates DMTCP plugin API version and adds/renames several API definitions and helpers.
doc/tsan-mutex-annotation-false-positive.txt Documents mutex annotation false positives and the annotation-based fix.
doc/pthread-exit-abort-and-fiber-crash.txt Documents pthread_exit failures and the fiber + raw-exit workaround.
doc/log-mutex-fork-desync.txt Documents log mutex deadlock across _Fork and the reset fix.
doc/glibc-sem-desync.txt Documents glibc semaphore desync and rationale for raw futex usage.
doc/glibc-cond-var-desync.txt Documents condvar desync risk and removal of real condvar ops post-restart.
doc/cond-wait-tsan-interceptor-bypass.txt Documents TSAN bypass of pthread_cond_wait and the need for target-side wraps.
doc/classic-mode-thread-registration-segv.txt Documents classic-mode TSAN thread registration crash and RTLD_NEXT create fix.
CMakeLists.txt Adds option to build libmcmini with TSAN and includes new TSAN support source.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/lib/interception.c
Comment on lines +64 to +68
if (libmcmini_init_done) {
return;
}
pthread_once(&libmcini_init, &mc_load_intercepted_pthread_functions);
libmcmini_init_done = true;
Comment thread include/dmtcp.h
char padding[1792];
} DmtcpCkptHeader;

static_assert(sizeof(DmtcpCkptHeader) == 4096, "DmtcpCkptHeader must be 4096 bytes");
Comment on lines +35 to 39
ConditionVariablePolicy* policy;

public:
condition_variable() = default;
~condition_variable() = default;
@gc00
gc00 force-pushed the mcmini-dmtcp-tsan branch from 6513b37 to f15119f Compare July 31, 2026 14:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 20

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (11)
test/tsan_support/test_thread_blocks_signal.c-49-51 (1)

49-51: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a deterministically invalid task ID.

Task ID 999999 can exist. If this process receives that ID, the test fails intermittently. Use -1; thread_blocks_signal() maps it to /proc/self/task/0/status, which cannot exist.

Proposed fix
-  assert(thread_blocks_signal(999999 /* bogus tid, should not exist */, SIGUSR1) == 0);
+  assert(thread_blocks_signal(-1, SIGUSR1) == 0);
🤖 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 `@test/tsan_support/test_thread_blocks_signal.c` around lines 49 - 51, Replace
the hard-coded bogus task ID in the thread_blocks_signal() invalid-ID assertion
with -1, preserving the expected return value of 0 and ensuring the test always
targets a nonexistent task status path.
src/examples/exit-stress.c-27-29 (1)

27-29: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the pre-exit checkpoint window signal-resilient.

POSIX sleep() returns the unslept seconds when interrupted by a signal, and src/examples/producer-consumer-park.c documents DMTCP pre-checkpoint signals returning early from blocking syscalls. If this sleep() wakes early, workers increment counter and exit before the checkpoint is taken, so the restart may no longer exercise the recreated-thread pthread_exit() path under this scenario.

Use a monotonic deadline loop such as the existing busy wait for the worker delay.

🤖 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 `@src/examples/exit-stress.c` around lines 27 - 29, Replace the single
sleep(10) delay in the exit-stress scenario with a monotonic deadline loop,
following the existing worker-delay busy-wait pattern, so pre-checkpoint signals
cannot shorten the window before workers exit. Preserve the ten-second delay and
ensure the loop continues until the deadline is reached.
doc/glibc-sem-desync.txt-88-93 (1)

88-93: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the condition-variable status.

These lines state that no condition-variable fix exists. doc/glibc-cond-var-desync.txt documents the condition-variable fix in this PR. Replace this stale status with a reference to that document and list only remaining limitations.

Proposed documentation update
-Condition variables (pthread_cond_wait/pthread_cond_signal) have the same
-class of vulnerability via glibc's G1/G2 waiter-group bookkeeping, for the
-same reason (checkpoint/restart + externally-managed reinitialization). An
-analogous fix has not yet been applied there.
+Condition variables have the same class of vulnerability via glibc's G1/G2
+waiter-group bookkeeping. The post-restart signal, broadcast, init, and
+destroy paths are handled in doc/glibc-cond-var-desync.txt.
🤖 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/glibc-sem-desync.txt` around lines 88 - 93, Update the condition-variable
section in glibc-sem-desync.txt to remove the stale “Not yet fixed” claim,
reference glibc-cond-var-desync.txt as documenting the applied fix, and retain
only the limitations that still remain.
src/examples/producer-consumer-park.c-54-54 (1)

54-54: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Retry only EINTR from sem_wait().

The loop retries every nonzero return value, so permanent sem_wait() failures such as EINVAL spin indefinitely and hide semaphore setup failures. Retry only when sem_wait() returns -1 with errno == EINTR, then abort on other errors.

Proposed fix
+#include <errno.h>
+
-static void sem_wait_retry(sem_t *s) { while (sem_wait(s) != 0) /* EINTR */; }
+static void sem_wait_retry(sem_t *s) {
+    while (sem_wait(s) == -1) {
+        if (errno != EINTR) {
+            perror("sem_wait");
+            abort();
+        }
+    }
+}
🤖 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 `@src/examples/producer-consumer-park.c` at line 54, Update sem_wait_retry so
it loops only when sem_wait returns -1 with errno equal to EINTR; for any other
failure, abort immediately and preserve the semaphore error visibility instead
of retrying indefinitely.
src/lib/wrappers.c-232-241 (1)

232-241: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Reset owner on a successful re-initialization.

Line 233 sets .owner = RID_INVALID only when the record is created. For an already-known mutex, line 241 sets status = UNLOCKED and leaves owner at its previous value. A mutex that was locked before pthread_mutex_init() therefore keeps a stale owner while the status says unlocked. condition_variable_enqueue_thread::modify() checks mutex->is_locked_by(executor), so the model can read an inconsistent pair.

🐛 Proposed fix
         mutex_record->vo.mut_state.status = UNLOCKED;
+        mutex_record->vo.mut_state.owner = RID_INVALID;
         libpthread_mutex_unlock(&rec_list_lock);
🤖 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 `@src/lib/wrappers.c` around lines 232 - 241, In the successful initialization
branch after libpthread_mutex_init returns zero, update the existing
mutex_record state so owner is reset to RID_INVALID alongside status = UNLOCKED.
Apply this to both newly created and already-known mutex records, using
mutex_record->vo.mut_state.
include/mcmini/spy/checkpointing/record.h-198-202 (1)

198-202: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align stale TSAN design-document references

The code references TSAN-McMini-DMTCP.txt and TSAN-pthread-join.md, but those exact files are not in doc/. Update these references to the actual added doc files, or add the referenced documents with stable names.

🤖 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 `@include/mcmini/spy/checkpointing/record.h` around lines 198 - 202, Update the
stale documentation references associated with add_rec_entry_record_mode_ts and
the related TSAN pthread-join references to point to the actual documents
present in doc/, or add the missing documents using stable names; ensure no
references remain to nonexistent TSAN-McMini-DMTCP.txt or TSAN-pthread-join.md
files.
src/common/multithreaded_fork.c-181-183 (1)

181-183: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Use virtual TIDs consistently before sending SIG_MULTITHREADED_FORK.

multithreaded_fork() passes the virtualized /proc/self/task entries directly to tgkill(), but DMTCP comments say tgkill() needs the same virtual identifier. Since ctl is already virtualized in both callers, make the direct Sys_gettid() value virtual too before the tids[i] != mytid comparison.

🤖 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 `@src/common/multithreaded_fork.c` around lines 181 - 183, Virtualize the ctid
value obtained from Sys_gettid() using dmtcp_pid_real_to_virtual() before the
tids[i] != mytid comparison in the multithreaded_fork function, ensuring that
the real thread ID from Sys_gettid() is converted to a virtual TID to match the
virtual identifiers already present in the tids array from /proc/self/task
before the comparison is performed.
src/lib/main.c-30-39 (1)

30-39: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Avoid probing __wrap_pthread_join from libmcmini.so.

producer-consumer-tsan and the other TSan link lines compile src/lib/pthread_join_wrap.c with -Wl,--wrap=pthread_join but do not use -rdynamic or -Wl,--export-dynamic. The shared library’s weak undefined reference can therefore stay NULL even though the executable has the wrapper, and the constructor prints the warning incorrectly. Do not rely on this dynamic lookup unless the TSan targets export symbols, or switch to an explicit registration call from the wrapper.

🤖 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 `@src/lib/main.c` around lines 30 - 39, Remove the __wrap_pthread_join probe
and warning from warn_if_tsan_target_missing_wrap, since libmcmini.so cannot
reliably observe the executable’s linker wrapper. Use an explicit registration
call from pthread_join_wrap.c instead, or otherwise ensure TSan targets export
the wrapper before retaining the lookup.
include/mcmini/model/objects/mutex.hpp-24-33 (1)

24-33: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Requiring loc is correct, but the defaulted default constructor still bypasses it.

The single constructor now forces every caller to supply the real address. Line 21 still declares mutex() = default;, which leaves location and owner indeterminate. That reintroduces exactly the failure the comment describes, because get_location() then returns garbage.

Add default member initializers for location and owner, or delete the default constructor if no caller needs it.

🛡️ Proposed fix
   state current_state = state::uninitialized;
-  pthread_mutex_t* location;
-  runner_id_t owner;
+  pthread_mutex_t* location = nullptr;
+  runner_id_t owner = RID_INVALID;
🤖 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 `@include/mcmini/model/objects/mutex.hpp` around lines 24 - 33, Fix the default
construction path in the mutex class by removing mutex() = default if it is
unused, or initialize location and owner to safe values through default member
initializers. Ensure every mutex instance has a valid location state and
RID_INVALID ownership before get_location(), mutex_lock, or mutex_unlock can use
it, while preserving the explicit constructor’s behavior.
src/lib/interception.c-392-400 (1)

392-400: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle a failed __libc_start_main resolution.

dlsym(RTLD_NEXT, "__libc_start_main") can return NULL. The code then calls a NULL pointer, and every target process crashes before main() with no diagnostic. Report the failure explicitly.

🛡️ Proposed fix
   libc_start_main_fn real_start_main =
       (libc_start_main_fn)dlsym(RTLD_NEXT, "__libc_start_main");
+  if (real_start_main == NULL) {
+    fprintf(stderr, "mcmini: dlsym(RTLD_NEXT, \"__libc_start_main\") failed: %s\n",
+            dlerror());
+    fflush(stderr);
+    _exit(1);
+  }
   return real_start_main(wrapped_main, argc, argv, init, fini, rtld_fini,
                          stack_end);

Note: use _exit(2) here, not libc_exit(), because this path runs before initialization completes.

🤖 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 `@src/lib/interception.c` around lines 392 - 400, Update __libc_start_main to
validate the result of dlsym before invoking real_start_main; when resolution
fails, report the failure explicitly and terminate with _exit(2), avoiding
libc_exit because initialization is incomplete. Preserve the existing delegation
to wrapped_main when the symbol resolves successfully.
src/lib/interception.c-125-125 (1)

125-125: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Check clone_ptr before calling libc_clone().

dlsym(libc_handle, "__clone") can return NULL, and libc_clone() later invokes (*clone_ptr). Add a NULL check around this resolve with a diagnostic, then libc_abort() to avoid crashing during restart.

🤖 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 `@src/lib/interception.c` at line 125, Validate the result assigned to
clone_ptr immediately after resolving "__clone" with dlsym in libc_clone(); if
it is NULL, emit a diagnostic and call libc_abort() before any indirect
invocation. Preserve the existing libc_clone restart flow when clone_ptr
resolves successfully.
🧹 Nitpick comments (12)
src/examples/CMakeLists.txt (1)

24-168: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated TSan-target boilerplate into one CMake function.

The eight TSan target blocks (producer-consumer-tsan, producer-consumer-safe-tsan, producer-consumer-exit-tsan, producer-consumer-park-tsan, exit-stress-tsan, exit-stress-noop-tsan, cv-producer-consumer-tsan, cv-producer-consumer-safe-tsan) repeat the same five steps: add_executable, target_compile_options(... -fsanitize=thread), target_link_options(... -fsanitize=thread [+ --wrap flags]), target_link_libraries(... -pthread libmcmini), and the add_custom_command(POST_BUILD copy ...). Each new TSan example requires copying all five steps and keeping them in sync by hand.

Extract a function that takes the target name, source list, and wrap symbols, and generates all five steps once.

♻️ Proposed refactor
function(add_tsan_example name)
  cmake_parse_arguments(ARG "" "" "SOURCES;WRAP" ${ARGN})
  add_executable(${name} ${ARG_SOURCES})
  target_compile_options(${name} PUBLIC -fsanitize=thread)

  set(wrap_flags "")
  foreach(sym ${ARG_WRAP})
    list(APPEND wrap_flags -Wl,--wrap=${sym})
  endforeach()
  target_link_options(${name} PUBLIC -fsanitize=thread ${wrap_flags})
  target_link_libraries(${name} PUBLIC -pthread libmcmini)

  add_custom_command(TARGET ${name} POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy
            $<TARGET_FILE:${name}>
            ${CMAKE_BINARY_DIR}/${name})
endfunction()

add_tsan_example(producer-consumer-tsan
  SOURCES producer-consumer.c ../lib/pthread_join_wrap.c
  WRAP pthread_join)

add_tsan_example(cv-producer-consumer-tsan
  SOURCES cv-producer-consumer.c ../lib/pthread_join_wrap.c
          ../lib/pthread_cond_wait_wrap.c ../lib/pthread_cond_signal_wrap.c
  WRAP pthread_join pthread_cond_wait pthread_cond_signal)

add_tsan_example(producer-consumer-park-tsan
  SOURCES producer-consumer-park.c)
# ... remaining targets follow the same pattern
🤖 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 `@src/examples/CMakeLists.txt` around lines 24 - 168, Extract the repeated
setup from the eight TSan targets into a shared CMake function, such as
add_tsan_example, accepting the target name, source files, and pthread wrap
symbols. Have it generate the executable, ThreadSanitizer compile/link options,
wrap flags, libmcmini linkage, and POST_BUILD copy command; then replace each
producer-consumer, exit-stress, and cv-producer-consumer block with function
calls while preserving each target’s existing sources and wrap symbols.
src/lib/tsan_support.c (2)

30-31: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Two small cleanups in thread_blocks_signal.

  1. Line 31 discards signo with (void)signo, but line 89 uses signo. Remove the stale cast.
  2. The read loop stops on any negative return, including EINTR. A partial mask read then yields a wrong answer instead of a retry. Retry on EINTR.
♻️ Proposed cleanup
 int thread_blocks_signal(pid_t tid, int signo) {
-  (void)signo;
   char path[64];
@@
   while (total < (ssize_t)sizeof(buf) - 1 &&
-         (n = syscall(SYS_read, fd, buf + total, sizeof(buf) - 1 - total)) > 0) {
-    total += n;
+         ((n = syscall(SYS_read, fd, buf + total, sizeof(buf) - 1 - total)) > 0 ||
+          (n < 0 && errno == EINTR))) {
+    if (n > 0) total += n;
   }

Also applies to: 56-59

🤖 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 `@src/lib/tsan_support.c` around lines 30 - 31, Update thread_blocks_signal by
removing the stale (void)signo cast, since signo is used later, and adjust the
signal-mask read loop to retry when the read operation returns EINTR while
preserving the existing handling for other negative returns and partial reads.

92-105: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Replace the readlink/atoi path with a direct gettid syscall.

mc_real_tid() reads /proc/thread-self, then parses the result with atoi. atoi performs no error detection and has undefined behavior on out-of-range values. syscall(SYS_gettid) returns the same kernel thread ID with one raw syscall, no string parsing, and no procfs dependency. It is also interceptor-free, which is the property this file needs. src/lib/wrappers.c line 832 already uses syscall(SYS_gettid) for the same purpose.

♻️ Proposed simplification
 pid_t mc_real_tid(void) {
-  char linkbuf[64];
-  ssize_t n = syscall(SYS_readlink, "/proc/thread-self", linkbuf,
-                      sizeof(linkbuf) - 1);
-  if (n < 0) {
-    return -1;
-  }
-  linkbuf[n] = '\0';
-  const char *task = strstr(linkbuf, "/task/");
-  if (task == NULL) {
-    return -1;
-  }
-  return (pid_t)atoi(task + strlen("/task/"));
+  return (pid_t)syscall(SYS_gettid);
 }
🤖 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 `@src/lib/tsan_support.c` around lines 92 - 105, Replace the procfs readlink
and atoi logic in mc_real_tid() with a direct syscall(SYS_gettid) return,
matching the existing usage in wrappers.c and preserving the function’s pid_t
return type.

Source: Linters/SAST tools

src/common/mem.c (1)

16-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Report the requested size, and stop the offset from wrapping past the arena.

Two small robustness points on the exhaustion path:

  1. mc_ts_off keeps increasing on every failed call. With repeated large requests the counter can wrap and let a later call pass the bounds check and return an in-range but already-used pointer. The current path calls exit_group, so this is not reachable today. It becomes reachable if the failure policy ever changes.
  2. The message does not report the requested size or the arena size, which makes a real exhaustion hard to diagnose.

Consider a compare-exchange loop that leaves mc_ts_off unchanged on failure.

🤖 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 `@src/common/mem.c` around lines 16 - 23, The exhaustion handling in
mc_ts_alloc must avoid advancing mc_ts_off when a request exceeds the arena and
include both the requested size and MC_TS_ARENA_SIZE in the raw-syscall
diagnostic. Update the offset reservation logic to use a compare-exchange loop
that commits the new offset only when it fits, leaving mc_ts_off unchanged on
failure while preserving the interceptor-free exit path.
include/dmtcp.h (1)

506-523: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guard DMTCP API names from macro capture.

dmtcp_tsan_background_thread_virtual_tid() and dmtcp_skip_post_restart_checkpoint_loop() expand to guarded calls, but any definition site that includes include/dmtcp.h still has those names replaced. Add #undef before defining them, or use non-macro names for macros so the definitions keep the exported function names intact.

🤖 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 `@include/dmtcp.h` around lines 506 - 523, Prevent the fallback macros for
dmtcp_tsan_background_thread_virtual_tid and
dmtcp_skip_post_restart_checkpoint_loop from rewriting the corresponding
exported function declarations or definitions. Undefine each function-name macro
before its declaration/definition, or rename the macro wrappers while preserving
the public API names and guarded-call behavior.
include/mcmini/spy/checkpointing/tsan_support.h (1)

3-8: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the #include directives above the extern "C" block.

The header opens extern "C" at line 4 and then includes <stdbool.h> and <sys/types.h> at lines 7-8. Any declaration those headers pull in then gets C language linkage. System headers usually guard themselves, but the portable convention is to include first and then open the linkage block.

♻️ Proposed reordering
 `#pragma` once
 
+#include <stdbool.h>
+#include <sys/types.h>
+
 `#ifdef` __cplusplus
 extern "C" {
 `#endif`
-
-#include <stdbool.h>
-#include <sys/types.h>
🤖 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 `@include/mcmini/spy/checkpointing/tsan_support.h` around lines 3 - 8, Move the
<stdbool.h> and <sys/types.h> include directives before the __cplusplus extern
"C" block in the checkpointing header, leaving only this header’s declarations
inside the C-linkage wrapper.
src/lib/dmtcp-callback.c (2)

226-229: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Check the libc_clone() return value.

If libc_clone() fails, the thread is never recreated. The template thread then waits on dmtcp_restart_sem for a thread that does not exist and the barrier at lines 421-423 blocks forever. Fail loudly instead.

♻️ Proposed error check
-    libc_clone(child_setcontext_fast,
-               stack,
-               clone_flags,
-               (void *)&threadInfos[i], ptid, (void *)threadInfos[i].fs, ctid);
+    int clone_rc = libc_clone(child_setcontext_fast,
+                              stack,
+                              clone_flags,
+                              (void *)&threadInfos[i], ptid,
+                              (void *)threadInfos[i].fs, ctid);
+    if (clone_rc == -1) {
+      // A missing thread makes the restart barrier block forever.
+      libc_abort();
+    }
🤖 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 `@src/lib/dmtcp-callback.c` around lines 226 - 229, Check the return value of
libc_clone() in the thread recreation path and fail loudly when it indicates
failure, rather than continuing to wait on dmtcp_restart_sem. Preserve the
existing successful clone flow and ensure the failure path prevents the barrier
from blocking indefinitely.

272-274: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Guard __tsan_create_fiber as well as __tsan_switch_to_fiber.

Both sites test only __tsan_switch_to_fiber and then call the weak __tsan_create_fiber unconditionally. If only one symbol resolves, the call dereferences a null function pointer. Test both symbols, and also reject a null fiber.

🛡️ Proposed guard (apply at both sites)
-    if (__tsan_switch_to_fiber != NULL) {
-      __tsan_switch_to_fiber(__tsan_create_fiber(0), 0);
-    }
+    if (__tsan_switch_to_fiber != NULL && __tsan_create_fiber != NULL) {
+      void *fiber = __tsan_create_fiber(0);
+      if (fiber != NULL) __tsan_switch_to_fiber(fiber, 0);
+    }

Also applies to: 329-331

🤖 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 `@src/lib/dmtcp-callback.c` around lines 272 - 274, Update both TSan
fiber-switch call sites in the callback logic to require __tsan_switch_to_fiber
and __tsan_create_fiber to be non-null before invoking either; store the result
of __tsan_create_fiber(0), verify the fiber is non-null, and only then call
__tsan_switch_to_fiber.
src/mcmini/mcmini.cpp (1)

182-221: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared trace-reporting body.

found_nonzero_exit_code duplicates found_abnormal_termination from the std::stringstream onward. Only the first std::cerr line and the error type differ. Extract a helper that takes the coordinator, the stats, and the culprit runner_id_t, then call it from both callbacks.

♻️ Sketch of the shared helper
static void report_trace_and_pending(const coordinator& c, const stats& stats,
                                     runner_id_t culprit) {
  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* culprit_transition =
      program_model.get_pending_transition_for(culprit);
  if (culprit_transition != nullptr) {
    ss << "thread " << culprit_transition->get_executor() << ": "
       << culprit_transition->to_string() << "\n";
  } else {
    ss << "thread " << culprit << ": (no longer pending)\n";
  }
  ss << "\nNEXT THREAD OPERATIONS\n";
  for (const auto& tpair : program_model.get_pending_transitions()) {
    if (culprit_transition != nullptr &&
        tpair.first == culprit_transition->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();
}
🤖 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 `@src/mcmini/mcmini.cpp` around lines 182 - 221, Extract the duplicated trace
and pending-transition reporting logic from found_nonzero_exit_code and
found_abnormal_termination into a shared helper accepting const coordinator&,
const stats&, and runner_id_t culprit. Replace both callbacks’ duplicated bodies
with calls to that helper, while preserving their distinct std::cerr error
messages and passing nzec.culprit or the corresponding abnormal-termination
culprit.
include/mcmini/model/objects/condition_variables.hpp (1)

57-73: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Combine the two passes over thread_states.

The first loop registers prewaiting and waiting threads. The second loop collects signaled threads. One pass can do both.

♻️ Proposed refactor
-    // 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<runner_id_t> signaled_threads;
+    // 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_SIGNALED) {
+      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);
+      } else if (thread_with_state.second == CV_SIGNALED) {
         signaled_threads.push_back(thread_with_state.first);
       }
     }
🤖 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 `@include/mcmini/model/objects/condition_variables.hpp` around lines 57 - 73,
Combine the two iterations over thread_states in the policy initialization
logic: keep registering CV_PREWAITING and CV_WAITING entries through
add_waiter_with_state, while collecting CV_SIGNALED thread IDs for the existing
add_to_wake_groups call after the single pass.
src/lib/interception.c (1)

29-29: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the ignored __noreturn__ attribute on the pointer object.

GCC and Clang ignore __attribute__((__noreturn__)) on an object declaration. The attribute belongs to the type, and typeof(&pthread_exit) already carries glibc's noreturn attribute. Clang also reports a parse error on this line.

♻️ Proposed change
-__attribute__((__noreturn__)) typeof(&pthread_exit) libpthread_pthread_exit_ptr;
+typeof(&pthread_exit) libpthread_pthread_exit_ptr;
🤖 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 `@src/lib/interception.c` at line 29, Update the libpthread_pthread_exit_ptr
declaration to remove the object-level __attribute__((__noreturn__)) annotation,
retaining typeof(&pthread_exit) as the pointer type.

Source: Linters/SAST tools

src/mcmini/real_world/local_linux_process.cpp (1)

141-154: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

State the _exit(0) case in the code-0 message.

A target that calls _exit(0) directly bypasses the exit interposition, as noted in src/lib/interception.c. That target reaches this branch and reports a McMini protocol violation, which misleads the user. Add the alternative cause to the message.

♻️ Proposed change
       throw process::execution_error(
           "Runner " + std::to_string(id) +
           "'s process exited normally (code 0) while a transition was "
-          "still pending on it, bypassing the model-driven exit protocol.");
+          "still pending on it, bypassing the model-driven exit protocol. "
+          "This happens if the target calls _exit(2) directly, which McMini "
+          "cannot intercept, or if McMini's own exit protocol was violated.");
🤖 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 `@src/mcmini/real_world/local_linux_process.cpp` around lines 141 - 154, Update
the code-0 execution_error message in the SIGCHLD handling branch to explicitly
include direct target calls to _exit(0) as an alternative cause, alongside
bypassing the model-driven exit protocol. Keep the existing distinction from
nonzero target exits and preserve the current error behavior.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@doc/cond-wait-tsan-interceptor-bypass.txt`:
- Around line 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.

In `@include/mcmini/model/objects/condition_variables.hpp`:
- Around line 27-56: Update the condition_variable data members to provide safe
default member initializers, at minimum setting policy to nullptr and hadwaiters
to false; also initialize running_thread to RID_INVALID and associated_mutex to
nullptr to match the default-constructor contract. Preserve the parameterized
constructor’s policy allocation behavior for non-null policy arguments.
- Around line 38-56: The policy member variable is currently a raw owning
pointer that causes memory leaks (allocated in the parameterized constructor but
never freed) and unintended aliasing via the defaulted copy constructor. Replace
the raw pointer policy member with std::unique_ptr<ConditionVariablePolicy> to
enable automatic cleanup. Remove the defaulted copy constructor and implement an
explicit copy constructor that deep-copies the policy by creating a new
ConditionVariablePolicy instance for the copied object. Update the parameterized
constructor to initialize the unique_ptr correctly, allocating a fresh policy
when p is nullptr or wrapping the provided policy pointer.

In
`@include/mcmini/model/transitions/condition_variables/condition_variable_enqueue_thread.hpp`:
- Around line 54-55: Condition-variable transitions leak the default policy by
replacing it after construction. In
include/mcmini/model/transitions/condition_variables/condition_variable_enqueue_thread.hpp
lines 54-55, condition_variable_brdcast.hpp lines 52-78, and
condition_variables_wait.hpp lines 59-60, pass new_policy as the constructor’s
sixth argument and remove set_policy. In condition_variables_signal.hpp lines
92-98, pass cv->get_mutex() and new_policy to the constructor, then remove both
set_policy and set_associated_mutex.
- Around line 44-52: Update the enqueue path around
ConditionVariablePolicy::add_waiter_with_state to detect and remove or replace
any existing executor waiter before adding its CV_WAITING entry, especially when
the current state is CV_PREWAITING. Ensure the queue contains at most one entry
for executor so new_waiting_count and cv_waiting_count remain accurate.

In
`@include/mcmini/model/transitions/condition_variables/condition_variables_signal.hpp`:
- Around line 100-101: Update the signal lost-wakeup handling to call
check_for_lost_wakeup(true, prev_waiting_count) on new_cv, which owns
new_policy, instead of the temporary mutable_cv. Remove the throwaway
condition_variable construction so no empty policy is allocated or leaked, while
preserving the existing state and waiting-count updates.
- Around line 92-98: Update the condition_variable constructor call to pass the
associated mutex and policy directly as constructor parameters instead of using
nullptr and then calling set_associated_mutex() and set_policy() separately.
Replace the nullptr argument with cv->get_mutex() and add new_policy as the
appropriate constructor parameter, then remove the subsequent set_policy() and
set_associated_mutex() calls to eliminate the extra steps and prevent leaking
the policy allocated by the constructor.

In `@src/common/mem.c`:
- Around line 9-24: Declare the static arena used by mc_ts_alloc with an
explicit 16-byte alignment, preserving its existing size and allocation logic.
Update the mc_ts_arena declaration so every returned address remains suitably
aligned for the objects allocated by this function.

In `@src/lib/dmtcp-callback.c`:
- Around line 411-416: Protect the traversal of head_record_mode in the
thread-counting logic with rec_list_lock. Update the surrounding function so the
lock is acquired before iterating through rec_list entries and released after
the count is complete, while preserving the existing THREAD and ALIVE filtering
and subsequent barrier behavior.
- Around line 195-204: The mc_pthread_is_recreated_thread function reads
threadInfos entries without acquire semantics, creating a race where it observes
maxThreadIdx incremented before the entry is fully written. Add an atomic
published field (or convert origTid to atomic) in struct threadinfo, set it last
with memory_order_release in saveThreadStateBeforeFork after all fields are
populated, and check it here with memory_order_acquire instead of testing
origTid != 0 to establish the release/acquire pairing between
thread_handle_after_dmtcp_restart's writer and this reader.

In `@src/lib/interception.c`:
- Around line 15-21: Update libmcmini_init_done and its accesses in
libmcmini_init to use atomic acquire/release builtins: publish the flag with
release ordering only after mc_load_intercepted_pthread_functions() completes,
and read it with acquire ordering on the pthread-free fast path. Preserve the
existing plain-memory path without introducing pthread_once or TSAN-intercepted
operations.

In `@src/lib/pthread_cond_signal_wrap.c`:
- Around line 1-19: Add the matching pthread_cond_broadcast wrapper alongside
__wrap_pthread_cond_signal, forwarding to mc_pthread_cond_broadcast; compile it
into the target and add -Wl,--wrap=pthread_cond_broadcast to the TSan target
link options. Update doc/cond-wait-tsan-interceptor-bypass.txt to state that
TSan bypasses interposition for broadcast as well as signal.

In `@src/lib/tsan_support.c`:
- Around line 48-66: Replace the procfs open/read logic in
mc_is_current_thread_tsan_internal() with syscall(SYS_rt_sigprocmask, SIG_BLOCK,
NULL, &set, sizeof(set)) for current-thread checks, while retaining the procfs
path only for explicit arbitrary-tid queries. In src/lib/record.c lines 186-208,
remove the tsan_internal_check_applies mode gate and invoke
mc_is_current_thread_tsan_internal() for every mode, as required by the record.h
contract.
- Line 48: Add a feature-test macro enabling POSIX.1-2008 or GNU extensions at
the very top of the tsan_support.c translation unit, before every include, so
<fcntl.h> exposes AT_FDCWD for the syscall(SYS_openat, ...) call. Preserve the
existing openat behavior.

In `@src/lib/wrappers.c`:
- Around line 787-807: Update the call site around mark_ckpt_window_candidate to
pass the thread’s DMTCP virtual thread ID rather than the real kernel ID
returned by syscall(SYS_gettid), using mcmini_virtual_pid() for the translation.
Preserve resolve_ckpt_window_candidate_if_pending() and its virtual-ID
comparison unchanged.
- Around line 796-803: Update the TSan background-thread lookup in
get_current_mode to use a bounded retry/timeout and an explicit failure path
when dmtcp_tsan_background_thread_virtual_tid remains unavailable, rather than
spinning indefinitely. Distinguish the guard-macro result of 0 from valid
virtual thread IDs, report that the symbol is absent, and do not call
record_checkpoint_thread in that case. Preserve checkpoint-thread recording only
when a valid published ID differs from ckpt_window_candidate_virtual_tid.
- Around line 86-97: The insert_pthread_map function calls get_current_mode()
while holding pthread_map_lock, which blocks other threads trying to access the
map during the unbounded spin inside get_current_mode(). Move the
get_current_mode() call before libpthread_mutex_lock is acquired, store the
result in a local variable, and then use that variable to set
n->registered_post_restart after creating the node. This ensures the lock
protects only the actual map manipulation, not the mode resolution.
- Around line 1447-1454: Guard the find_thread_record_mode result in the
condition-variable initialization path before accessing
thrd_record->vo.thrd_state.id, using a safe fallback identity or a specific
abort when no record exists. Apply the same protection in
mc_pthread_cond_broadcast and preserve the existing behavior when a thread
record is present.
- Around line 458-474: Update the exit-permission wait in the thread termination
path to retry libpthread_sem_wait(exit_permission) when it returns -1 with errno
== EINTR, and only continue after the semaphore wait succeeds. Apply the same
EINTR retry behavior to the really_exited semaphore wait in
mc_pthread_join_impl, preserving existing handling for successful waits and
other errors.

In `@src/mcmini/real_world/local_linux_process.cpp`:
- Around line 76-78: In src/mcmini/real_world/local_linux_process.cpp lines
76-78, update the destructor’s waitpid drain to reap only child PIDs spawned by
this handle, or use the handle’s process group, instead of waitpid(-1, ...). In
lines 130-134, update the execute_runner waitpid error handling so errno ==
ECHILD is reported as the child status already being consumed, separately from
genuine waitpid failures.

---

Minor comments:
In `@doc/glibc-sem-desync.txt`:
- Around line 88-93: Update the condition-variable section in
glibc-sem-desync.txt to remove the stale “Not yet fixed” claim, reference
glibc-cond-var-desync.txt as documenting the applied fix, and retain only the
limitations that still remain.

In `@include/mcmini/model/objects/mutex.hpp`:
- Around line 24-33: Fix the default construction path in the mutex class by
removing mutex() = default if it is unused, or initialize location and owner to
safe values through default member initializers. Ensure every mutex instance has
a valid location state and RID_INVALID ownership before get_location(),
mutex_lock, or mutex_unlock can use it, while preserving the explicit
constructor’s behavior.

In `@include/mcmini/spy/checkpointing/record.h`:
- Around line 198-202: Update the stale documentation references associated with
add_rec_entry_record_mode_ts and the related TSAN pthread-join references to
point to the actual documents present in doc/, or add the missing documents
using stable names; ensure no references remain to nonexistent
TSAN-McMini-DMTCP.txt or TSAN-pthread-join.md files.

In `@src/common/multithreaded_fork.c`:
- Around line 181-183: Virtualize the ctid value obtained from Sys_gettid()
using dmtcp_pid_real_to_virtual() before the tids[i] != mytid comparison in the
multithreaded_fork function, ensuring that the real thread ID from Sys_gettid()
is converted to a virtual TID to match the virtual identifiers already present
in the tids array from /proc/self/task before the comparison is performed.

In `@src/examples/exit-stress.c`:
- Around line 27-29: Replace the single sleep(10) delay in the exit-stress
scenario with a monotonic deadline loop, following the existing worker-delay
busy-wait pattern, so pre-checkpoint signals cannot shorten the window before
workers exit. Preserve the ten-second delay and ensure the loop continues until
the deadline is reached.

In `@src/examples/producer-consumer-park.c`:
- Line 54: Update sem_wait_retry so it loops only when sem_wait returns -1 with
errno equal to EINTR; for any other failure, abort immediately and preserve the
semaphore error visibility instead of retrying indefinitely.

In `@src/lib/interception.c`:
- Around line 392-400: Update __libc_start_main to validate the result of dlsym
before invoking real_start_main; when resolution fails, report the failure
explicitly and terminate with _exit(2), avoiding libc_exit because
initialization is incomplete. Preserve the existing delegation to wrapped_main
when the symbol resolves successfully.
- Line 125: Validate the result assigned to clone_ptr immediately after
resolving "__clone" with dlsym in libc_clone(); if it is NULL, emit a diagnostic
and call libc_abort() before any indirect invocation. Preserve the existing
libc_clone restart flow when clone_ptr resolves successfully.

In `@src/lib/main.c`:
- Around line 30-39: Remove the __wrap_pthread_join probe and warning from
warn_if_tsan_target_missing_wrap, since libmcmini.so cannot reliably observe the
executable’s linker wrapper. Use an explicit registration call from
pthread_join_wrap.c instead, or otherwise ensure TSan targets export the wrapper
before retaining the lookup.

In `@src/lib/wrappers.c`:
- Around line 232-241: In the successful initialization branch after
libpthread_mutex_init returns zero, update the existing mutex_record state so
owner is reset to RID_INVALID alongside status = UNLOCKED. Apply this to both
newly created and already-known mutex records, using mutex_record->vo.mut_state.

In `@test/tsan_support/test_thread_blocks_signal.c`:
- Around line 49-51: Replace the hard-coded bogus task ID in the
thread_blocks_signal() invalid-ID assertion with -1, preserving the expected
return value of 0 and ensuring the test always targets a nonexistent task status
path.

---

Nitpick comments:
In `@include/dmtcp.h`:
- Around line 506-523: Prevent the fallback macros for
dmtcp_tsan_background_thread_virtual_tid and
dmtcp_skip_post_restart_checkpoint_loop from rewriting the corresponding
exported function declarations or definitions. Undefine each function-name macro
before its declaration/definition, or rename the macro wrappers while preserving
the public API names and guarded-call behavior.

In `@include/mcmini/model/objects/condition_variables.hpp`:
- Around line 57-73: Combine the two iterations over thread_states in the policy
initialization logic: keep registering CV_PREWAITING and CV_WAITING entries
through add_waiter_with_state, while collecting CV_SIGNALED thread IDs for the
existing add_to_wake_groups call after the single pass.

In `@include/mcmini/spy/checkpointing/tsan_support.h`:
- Around line 3-8: Move the <stdbool.h> and <sys/types.h> include directives
before the __cplusplus extern "C" block in the checkpointing header, leaving
only this header’s declarations inside the C-linkage wrapper.

In `@src/common/mem.c`:
- Around line 16-23: The exhaustion handling in mc_ts_alloc must avoid advancing
mc_ts_off when a request exceeds the arena and include both the requested size
and MC_TS_ARENA_SIZE in the raw-syscall diagnostic. Update the offset
reservation logic to use a compare-exchange loop that commits the new offset
only when it fits, leaving mc_ts_off unchanged on failure while preserving the
interceptor-free exit path.

In `@src/examples/CMakeLists.txt`:
- Around line 24-168: Extract the repeated setup from the eight TSan targets
into a shared CMake function, such as add_tsan_example, accepting the target
name, source files, and pthread wrap symbols. Have it generate the executable,
ThreadSanitizer compile/link options, wrap flags, libmcmini linkage, and
POST_BUILD copy command; then replace each producer-consumer, exit-stress, and
cv-producer-consumer block with function calls while preserving each target’s
existing sources and wrap symbols.

In `@src/lib/dmtcp-callback.c`:
- Around line 226-229: Check the return value of libc_clone() in the thread
recreation path and fail loudly when it indicates failure, rather than
continuing to wait on dmtcp_restart_sem. Preserve the existing successful clone
flow and ensure the failure path prevents the barrier from blocking
indefinitely.
- Around line 272-274: Update both TSan fiber-switch call sites in the callback
logic to require __tsan_switch_to_fiber and __tsan_create_fiber to be non-null
before invoking either; store the result of __tsan_create_fiber(0), verify the
fiber is non-null, and only then call __tsan_switch_to_fiber.

In `@src/lib/interception.c`:
- Line 29: Update the libpthread_pthread_exit_ptr declaration to remove the
object-level __attribute__((__noreturn__)) annotation, retaining
typeof(&pthread_exit) as the pointer type.

In `@src/lib/tsan_support.c`:
- Around line 30-31: Update thread_blocks_signal by removing the stale
(void)signo cast, since signo is used later, and adjust the signal-mask read
loop to retry when the read operation returns EINTR while preserving the
existing handling for other negative returns and partial reads.
- Around line 92-105: Replace the procfs readlink and atoi logic in
mc_real_tid() with a direct syscall(SYS_gettid) return, matching the existing
usage in wrappers.c and preserving the function’s pid_t return type.

In `@src/mcmini/mcmini.cpp`:
- Around line 182-221: Extract the duplicated trace and pending-transition
reporting logic from found_nonzero_exit_code and found_abnormal_termination into
a shared helper accepting const coordinator&, const stats&, and runner_id_t
culprit. Replace both callbacks’ duplicated bodies with calls to that helper,
while preserving their distinct std::cerr error messages and passing
nzec.culprit or the corresponding abnormal-termination culprit.

In `@src/mcmini/real_world/local_linux_process.cpp`:
- Around line 141-154: Update the code-0 execution_error message in the SIGCHLD
handling branch to explicitly include direct target calls to _exit(0) as an
alternative cause, alongside bypassing the model-driven exit protocol. Keep the
existing distinction from nonzero target exits and preserve the current error
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7c606018-fc24-4e4f-b520-057e65ebf39f

📥 Commits

Reviewing files that changed from the base of the PR and between 7a2d504 and f15119f.

📒 Files selected for processing (56)
  • CMakeLists.txt
  • doc/classic-mode-thread-registration-segv.txt
  • doc/cond-wait-tsan-interceptor-bypass.txt
  • doc/glibc-cond-var-desync.txt
  • doc/glibc-sem-desync.txt
  • doc/log-mutex-fork-desync.txt
  • doc/pthread-exit-abort-and-fiber-crash.txt
  • doc/tsan-mutex-annotation-false-positive.txt
  • include/dmtcp.h
  • include/mcmini/lib/log.h
  • include/mcmini/mem.h
  • include/mcmini/model/objects/condition_variables.hpp
  • include/mcmini/model/objects/mutex.hpp
  • include/mcmini/model/transitions/condition_variables/condition_variable_brdcast.hpp
  • include/mcmini/model/transitions/condition_variables/condition_variable_enqueue_thread.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/mutex/mutex_init.hpp
  • include/mcmini/model/transitions/mutex/mutex_unlock.hpp
  • include/mcmini/model/transitions/process/exit.hpp
  • include/mcmini/real_world/mailbox/runner_mailbox.h
  • include/mcmini/real_world/process/dmtcp_process_source.hpp
  • include/mcmini/spy/checkpointing/objects.h
  • include/mcmini/spy/checkpointing/record.h
  • include/mcmini/spy/checkpointing/tsan_support.h
  • include/mcmini/spy/intercept/interception.h
  • include/mcmini/spy/intercept/wrappers.h
  • src/common/mem.c
  • src/common/multithreaded_fork.c
  • src/common/runner_mailbox.c
  • src/examples/CMakeLists.txt
  • src/examples/cv-producer-consumer-safe.c
  • src/examples/cv-producer-consumer.c
  • src/examples/exit-stress-noop.c
  • src/examples/exit-stress.c
  • src/examples/producer-consumer-exit.c
  • src/examples/producer-consumer-park.c
  • src/examples/producer-consumer-safe.c
  • src/lib/dmtcp-callback.c
  • src/lib/interception.c
  • src/lib/log.c
  • src/lib/main.c
  • src/lib/pthread_cond_signal_wrap.c
  • src/lib/pthread_cond_wait_wrap.c
  • src/lib/pthread_join_wrap.c
  • src/lib/record.c
  • src/lib/sem-wrappers.c
  • src/lib/tsan_support.c
  • src/lib/wrappers.c
  • src/mcmini/mcmini.cpp
  • src/mcmini/model/transitions/condition_variables.cpp
  • src/mcmini/model/transitions/mutex.cpp
  • src/mcmini/model_checking/algorithms/classic_dpor.cpp
  • src/mcmini/real_world/dmtcp_process_source.cpp
  • src/mcmini/real_world/local_linux_process.cpp
  • test/tsan_support/test_thread_blocks_signal.c
💤 Files with no reviewable changes (1)
  • src/mcmini/real_world/dmtcp_process_source.cpp

Comment on lines +59 to +72
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.

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.

Comment on lines +27 to +56
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();
ConditionVariablePolicy* policy;

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<std::pair<runner_id_t, condition_variable_status>>& 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<runner_id_t> 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);
}
}
// The only non-copy constructor: `tid`/`mutex` get sentinel defaults
// (RID_INVALID/nullptr) rather than being left uninitialized (there was
// previously no default member initializer for either, unlike `waiting_count`)
// -- e.g. mutex(state, location) needed the identical fix for `location`
// after commit 9bd9ecf. `p` (policy) defaults to nullptr meaning "create a
// fresh one"; every transition that advances an *existing* condition
// variable's state must still pass its own (possibly cloned) policy
// explicitly here or via set_policy() afterward -- see that setter's
// comment for why an omitted policy silently discards in-progress waiter
// tracking rather than just being harmlessly empty.
condition_variable(state s, runner_id_t tid = RID_INVALID,
pthread_mutex_t* mutex = nullptr, int count = 0,
const std::vector<std::pair<runner_id_t, condition_variable_status>>& thread_states = {},
ConditionVariablePolicy* p = nullptr)
: current_state(s), running_thread(tid), associated_mutex(mutex), waiting_count(count),
policy(p ? p : new ConditionVariableArbitraryPolicy()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Give policy and hadwaiters default member initializers.

The policy field lost its default initializer, but line 38 still declares a public defaulted default constructor. condition_variable() therefore leaves policy indeterminate. Every accessor that dereferences it (has_waiters(), get_policy(), waiter_can_exit(), remove_waiter()) then reads an invalid pointer. hadwaiters, running_thread, and associated_mutex have the same gap.

Set policy = nullptr at minimum, so the failure is a null dereference and not an arbitrary one.

🛡️ Proposed fix
   state current_state = state::cv_uninitialized;
-  bool hadwaiters;
+  bool hadwaiters = false;
   mutable unsigned int numRemainingSpuriousWakeups = 0;
-  runner_id_t running_thread;
-  pthread_mutex_t* associated_mutex;
+  runner_id_t running_thread = RID_INVALID;
+  pthread_mutex_t* associated_mutex = nullptr;
   int waiting_count = 0;
   int prev_waiting_count = 0;
   int lost_wakeups = 0;
-  ConditionVariablePolicy* policy;
+  ConditionVariablePolicy* policy = nullptr;

Run the following script to check whether the default constructor is reachable:

#!/bin/bash
# Find default-constructed condition_variable objects.
rg -nP --type=cpp --type=cc -C3 '\bcondition_variable\s+\w+\s*(;|\{\s*\})' -g '!**/build/**' || true
ast-grep run --pattern 'new condition_variable()' --lang cpp .
ast-grep outline include/mcmini/model/objects/condition_variables.hpp --items all
🤖 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 `@include/mcmini/model/objects/condition_variables.hpp` around lines 27 - 56,
Update the condition_variable data members to provide safe default member
initializers, at minimum setting policy to nullptr and hadwaiters to false; also
initialize running_thread to RID_INVALID and associated_mutex to nullptr to
match the default-constructor contract. Preserve the parameterized constructor’s
policy allocation behavior for non-null policy arguments.

Source: Linters/SAST tools

Comment on lines 38 to +56
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<std::pair<runner_id_t, condition_variable_status>>& 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<runner_id_t> 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);
}
}
// The only non-copy constructor: `tid`/`mutex` get sentinel defaults
// (RID_INVALID/nullptr) rather than being left uninitialized (there was
// previously no default member initializer for either, unlike `waiting_count`)
// -- e.g. mutex(state, location) needed the identical fix for `location`
// after commit 9bd9ecf. `p` (policy) defaults to nullptr meaning "create a
// fresh one"; every transition that advances an *existing* condition
// variable's state must still pass its own (possibly cloned) policy
// explicitly here or via set_policy() afterward -- see that setter's
// comment for why an omitted policy silently discards in-progress waiter
// tracking rather than just being harmlessly empty.
condition_variable(state s, runner_id_t tid = RID_INVALID,
pthread_mutex_t* mutex = nullptr, int count = 0,
const std::vector<std::pair<runner_id_t, condition_variable_status>>& thread_states = {},
ConditionVariablePolicy* p = nullptr)
: current_state(s), running_thread(tid), associated_mutex(mutex), waiting_count(count),
policy(p ? p : new ConditionVariableArbitraryPolicy()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

policy is an owning raw pointer with a defaulted copy constructor and destructor.

The parameterized constructor allocates a policy with new. The defaulted destructor never frees it, so every constructed condition_variable leaks one policy. The defaulted copy constructor copies the pointer, so clone() returns an object that shares one policy with the source object.

This shared policy defeats the cloning added in the transitions. condition_variable_enqueue_thread::modify() clones the policy so that a mutation cannot escape a throwaway diff_state, but a state-level clone() still aliases the policy of the cloned state. A later wake_thread() or add_to_wake_groups() through either object mutates the state seen by both.

Hold the policy in a std::unique_ptr<ConditionVariablePolicy> and deep-copy it in the copy constructor.

🤖 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 `@include/mcmini/model/objects/condition_variables.hpp` around lines 38 - 56,
The policy member variable is currently a raw owning pointer that causes memory
leaks (allocated in the parameterized constructor but never freed) and
unintended aliasing via the defaulted copy constructor. Replace the raw pointer
policy member with std::unique_ptr<ConditionVariablePolicy> to enable automatic
cleanup. Remove the defaulted copy constructor and implement an explicit copy
constructor that deep-copies the policy by creating a new
ConditionVariablePolicy instance for the copied object. Update the parameterized
constructor to initialize the unique_ptr correctly, allocating a fresh policy
when p is nullptr or wrapping the provided policy pointer.

Comment on lines +44 to +52
ConditionVariablePolicy* new_policy = cv->get_policy()->clone();
condition_variable_status current_state = new_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);
new_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));
new_policy->add_waiter_with_state(executor, CV_WAITING);
const int new_waiting_count = new_policy->return_wait_queue().size();

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate the policy class and inspect its waiter-queue mutators.
fd -i 'condition_variable*polic*' -e hpp -e cpp -e h
rg -nP -C10 '\b(add_waiter_with_state|update_thread_cv_state)\s*\(' -g '*.hpp' -g '*.cpp' -g '*.h'

Repository: mcminickpt/deep-debug

Length of output: 159


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repository root:"
pwd
echo "Tracked files matching condition_variables/policy names:"
git ls-files | rg -n 'condition_variables|condition_variable|polic' | head -200 || true

echo
echo "Search for file/comment context:"
git ls-files | rg 'condition_variable_enqueue_thread\.hpp$|condition_variable.*policy|condition_variable.*policy.*hpp|condition_variable.*policy.*hpp' || true

Repository: mcminickpt/deep-debug

Length of output: 1459


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "enqueue_thread.hpp:"
cat -n include/mcmini/model/transitions/condition_variables/condition_variable_enqueue_thread.hpp | sed -n '1,120p'

echo
echo "Policy headers:"
for f in include/mcmini/misc/cond/cond_var_default_policy.hpp include/mcmini/misc/Cond_var_arbitrary_policy.hpp include/mcmini/misc/cond/cond_var_policy.hpp include/mcmini/misc/cond/cond_var_single_grp_policy.hpp; do
  if [ -f "$f" ]; then
    echo "--- $f"
    cat -n "$f" | sed -n '1,240p'
  fi
done

echo
echo "Policy source mutators:"
for f in src/mcmini/model/cond_var_default_policy.cpp src/mcmini/model/cond_var_arbitrary_policy.cpp src/mcmini/model/cond_var_single_grp_policy.cpp include/mcmini/model/objects/condition_variables.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_brdcast.hpp; do
  if [ -f "$f" ]; then
    echo "--- $f"
    rg -n -C 8 '\b(add_waiter_with_state|update_thread_cv_state|get_thread_cv_state|return_wait_queue|waiter|Wait|waiting|CV_)\b' "$f"
  fi
done

echo
echo "Search all tracked files:"
rg -n -C 8 '\b(add_waiter_with_state|update_thread_cv_state|get_thread_cv_state|return_wait_queue)\b' -g '*.hpp' -g '*.cpp' -g '*.h' . || true

Repository: mcminickpt/deep-debug

Length of output: 15836


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "All match output for relevant symbols:"
rg -n -C 12 '\b(add_waiter_with_state|add_waiter\(|update_thread_cv_state|get_thread_cv_state|return_wait_queue|invalid_thread_addition|invalid_thread_wakeup_exception)\b' -g '*.hpp' -g '*.cpp' . || true

echo
echo "Full single group policy source:"
if [ -f src/mcmini/model/cond_var_single_grp_policy.cpp ]; then
  cat -n src/mcmini/model/cond_var_single_grp_policy.cpp | sed -n '1,200p'
fi

echo
echo "Full arbitrary policy source:"
if [ -f src/mcmini/model/cond_var_arbitrary_policy.cpp ]; then
  cat -n src/mcmini/model/cond_var_arbitrary_policy.cpp | sed -n '1,220p'
fi

Repository: mcminickpt/deep-debug

Length of output: 49833


Check and clear the waiter queue before calling add_waiter_with_state.

add_waiter_with_state calls add_waiter, and the single-group policy implementation pushes tid into wait_queue unconditionally:

void ConditionVariableSingleGroupPolicy::add_waiter(runner_id_t tid) {
  this->wait_queue.push_back(tid);
}

If CV_PREWAITING already has an entry for executor, the subsequent CV_WAITING update leaves that entry and appends a duplicate, inflating new_waiting_count and corrupting cv_waiting_count. Handle the pre-existing waiter in this enqueue path, or make add_waiter_with_state reject/replace existing waiters if that is the intended invariant.

🤖 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
`@include/mcmini/model/transitions/condition_variables/condition_variable_enqueue_thread.hpp`
around lines 44 - 52, Update the enqueue path around
ConditionVariablePolicy::add_waiter_with_state to detect and remove or replace
any existing executor waiter before adding its CV_WAITING entry, especially when
the current state is CV_PREWAITING. Ensure the queue contains at most one entry
for executor so new_waiting_count and cv_waiting_count remain accurate.

Comment thread src/lib/wrappers.c
Comment on lines +458 to +474

// Wait for whichever thread eventually calls pthread_join() on this one
// (see mc_pthread_join()'s TARGET_BRANCH*/DMTCP_RESTART_INTO_BRANCH-ish
// cases) to grant permission before this thread is allowed to really
// terminate. This keeps this thread's pthread_t/tid valid for exactly as
// long as a real, unjoined POSIX thread's would be -- no longer --
// rather than parking it forever regardless of whether anyone ever
// joins it.
sem_t *exit_permission = find_exit_permission_sem(pthread_self());
assert(exit_permission != NULL);
libpthread_sem_wait(exit_permission);

// Signal genuine completion, in case the joiner can't use a real join
// (see mc_pthread_join()'s TARGET_BRANCH_AFTER_RESTART case).
sem_t *really_exited = find_really_exited_sem(pthread_self());
assert(really_exited != NULL);
libpthread_sem_post(really_exited);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Retry the exit-permission wait on EINTR.

libpthread_sem_wait(exit_permission) resolves to glibc's sem_wait, which returns -1 with errno == EINTR when a signal handler runs. This code ignores the return value. DMTCP and this library both deliver real-time signals to target threads (SIG_MULTITHREADED_FORK, see src/lib/tsan_support.c line 15), so an interrupted wait is reachable.

On EINTR this thread proceeds to post really_exited_sem and then terminates for real, before any joiner granted permission. The joiner in mc_pthread_join_impl's TARGET_BRANCH case (line 1126) then performs a real join on a thread that already exited outside the model's control, and the model's view of the thread's lifetime no longer matches reality.

🔒️ Proposed fix
   sem_t *exit_permission = find_exit_permission_sem(pthread_self());
   assert(exit_permission != NULL);
-  libpthread_sem_wait(exit_permission);
+  while (libpthread_sem_wait(exit_permission) != 0) {
+    assert(errno == EINTR);
+  }

Apply the same retry to the really_exited_sem wait in mc_pthread_join_impl (line 1163).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Wait for whichever thread eventually calls pthread_join() on this one
// (see mc_pthread_join()'s TARGET_BRANCH*/DMTCP_RESTART_INTO_BRANCH-ish
// cases) to grant permission before this thread is allowed to really
// terminate. This keeps this thread's pthread_t/tid valid for exactly as
// long as a real, unjoined POSIX thread's would be -- no longer --
// rather than parking it forever regardless of whether anyone ever
// joins it.
sem_t *exit_permission = find_exit_permission_sem(pthread_self());
assert(exit_permission != NULL);
libpthread_sem_wait(exit_permission);
// Signal genuine completion, in case the joiner can't use a real join
// (see mc_pthread_join()'s TARGET_BRANCH_AFTER_RESTART case).
sem_t *really_exited = find_really_exited_sem(pthread_self());
assert(really_exited != NULL);
libpthread_sem_post(really_exited);
// Wait for whichever thread eventually calls pthread_join() on this one
// (see mc_pthread_join()'s TARGET_BRANCH*/DMTCP_RESTART_INTO_BRANCH-ish
// cases) to grant permission before this thread is allowed to really
// terminate. This keeps this thread's pthread_t/tid valid for exactly as
// long as a real, unjoined POSIX thread's would be -- no longer --
// rather than parking it forever regardless of whether anyone ever
// joins it.
sem_t *exit_permission = find_exit_permission_sem(pthread_self());
assert(exit_permission != NULL);
while (libpthread_sem_wait(exit_permission) != 0) {
assert(errno == EINTR);
}
// Signal genuine completion, in case the joiner can't use a real join
// (see mc_pthread_join()'s TARGET_BRANCH_AFTER_RESTART case).
sem_t *really_exited = find_really_exited_sem(pthread_self());
assert(really_exited != NULL);
libpthread_sem_post(really_exited);
🤖 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 `@src/lib/wrappers.c` around lines 458 - 474, Update the exit-permission wait
in the thread termination path to retry libpthread_sem_wait(exit_permission)
when it returns -1 with errno == EINTR, and only continue after the semaphore
wait succeeds. Apply the same EINTR retry behavior to the really_exited
semaphore wait in mc_pthread_join_impl, preserving existing handling for
successful waits and other errors.

Comment thread src/lib/wrappers.c
Comment thread src/lib/wrappers.c
Comment on lines +796 to +803
int tsan_bg_virtual_tid = dmtcp_tsan_background_thread_virtual_tid();
while (tsan_bg_virtual_tid == -1) {
sched_yield();
tsan_bg_virtual_tid = dmtcp_tsan_background_thread_virtual_tid();
}
if (tsan_bg_virtual_tid != ckpt_window_candidate_virtual_tid) {
record_checkpoint_thread();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound the spin, and handle the "symbol absent" case explicitly.

Two problems in this loop:

  1. The loop has no bound and no timeout. If DMTCP never publishes a value other than -1, the calling thread spins forever inside get_current_mode(), which every wrapper calls. Add a bounded retry with an explicit failure path.
  2. When the DMTCP build does not export dmtcp_tsan_background_thread_virtual_tid, the guard macro in include/dmtcp.h line 511 yields 0. 0 is never a valid thread ID, so the comparison always takes the record_checkpoint_thread() branch. For a TSan-instrumented target on such a DMTCP build, TSan's background thread is then recorded as the checkpoint thread with no warning. Detect the absent symbol and report it instead of silently misclassifying.
🤖 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 `@src/lib/wrappers.c` around lines 796 - 803, Update the TSan background-thread
lookup in get_current_mode to use a bounded retry/timeout and an explicit
failure path when dmtcp_tsan_background_thread_virtual_tid remains unavailable,
rather than spinning indefinitely. Distinguish the guard-macro result of 0 from
valid virtual thread IDs, report that the symbol is absent, and do not call
record_checkpoint_thread in that case. Preserve checkpoint-thread recording only
when a valid published ID differs from ckpt_window_candidate_virtual_tid.

Comment thread src/lib/wrappers.c
Comment on lines +1447 to +1454
pthread_t this_thread = pthread_self();
rec_list *thrd_record = find_thread_record_mode(this_thread);
visible_object vo = {
.type = CONDITION_VARIABLE, .location = cond, .cond_state = { .status = CV_INITIALIZED,
.interacting_thread = thrd_record->vo.thrd_state.id,
.associated_mutex = NULL, .count = 0, .waiting_threads = create_thread_queue() }
};
cond_record = add_rec_entry_record_mode(&vo);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard thrd_record before you dereference it.

find_thread_record_mode(this_thread) returns NULL when the calling thread has no record-mode entry. Line 1451 dereferences the result without a check. mc_pthread_cond_wait's own RECORD case has the same shape at line 1276-1278, so a record normally exists for a model-checked thread. This new path exists precisely because the call can arrive on a thread whose bookkeeping did not follow the normal order — for example a signaling thread that reaches this wrapper before its own thread record is inserted.

Use a safe fallback identity when the record is absent, or abort with a specific message instead of dereferencing NULL.

The same code appears in mc_pthread_cond_broadcast at lines 1556-1563.

🐛 Proposed fix
         pthread_t this_thread = pthread_self();
         rec_list *thrd_record = find_thread_record_mode(this_thread);
+        runner_id_t signaler =
+            thrd_record != NULL ? thrd_record->vo.thrd_state.id : RID_INVALID;
         visible_object vo = {
           .type = CONDITION_VARIABLE, .location = cond, .cond_state = { .status = CV_INITIALIZED,
-          .interacting_thread = thrd_record->vo.thrd_state.id,
+          .interacting_thread = signaler,
           .associated_mutex = NULL, .count = 0, .waiting_threads = create_thread_queue() }
         };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pthread_t this_thread = pthread_self();
rec_list *thrd_record = find_thread_record_mode(this_thread);
visible_object vo = {
.type = CONDITION_VARIABLE, .location = cond, .cond_state = { .status = CV_INITIALIZED,
.interacting_thread = thrd_record->vo.thrd_state.id,
.associated_mutex = NULL, .count = 0, .waiting_threads = create_thread_queue() }
};
cond_record = add_rec_entry_record_mode(&vo);
pthread_t this_thread = pthread_self();
rec_list *thrd_record = find_thread_record_mode(this_thread);
runner_id_t signaler =
thrd_record != NULL ? thrd_record->vo.thrd_state.id : RID_INVALID;
visible_object vo = {
.type = CONDITION_VARIABLE, .location = cond, .cond_state = { .status = CV_INITIALIZED,
.interacting_thread = signaler,
.associated_mutex = NULL, .count = 0, .waiting_threads = create_thread_queue() }
};
cond_record = add_rec_entry_record_mode(&vo);
🤖 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 `@src/lib/wrappers.c` around lines 1447 - 1454, Guard the
find_thread_record_mode result in the condition-variable initialization path
before accessing thrd_record->vo.thrd_state.id, using a safe fallback identity
or a specific abort when no record exists. Apply the same protection in
mc_pthread_cond_broadcast and preserve the existing behavior when a thread
record is present.

Comment on lines +76 to +78
while (waitpid(-1, &status, WNOHANG) > 0) {
signal_tracker::instance().try_consume_signal(SIGCHLD);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Wait-status ownership is process-wide, not per handle. The destructor reaps every reapable zombie in the McMini process and the execute_runner loop then cannot find its own child's status. One handle can therefore consume a status that another handle still needs.

  • src/mcmini/real_world/local_linux_process.cpp#L76-L78: limit the drain to pids this handle spawned, or reap by process group, instead of waitpid(-1, ...).
  • src/mcmini/real_world/local_linux_process.cpp#L130-L134: handle errno == ECHILD as "status already consumed" and report it separately from a real waitpid failure.
📍 Affects 1 file
  • src/mcmini/real_world/local_linux_process.cpp#L76-L78 (this comment)
  • src/mcmini/real_world/local_linux_process.cpp#L130-L134
🤖 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 `@src/mcmini/real_world/local_linux_process.cpp` around lines 76 - 78, In
src/mcmini/real_world/local_linux_process.cpp lines 76-78, update the
destructor’s waitpid drain to reap only child PIDs spawned by this handle, or
use the handle’s process group, instead of waitpid(-1, ...). In lines 130-134,
update the execute_runner waitpid error handling so errno == ECHILD is reported
as the child status already being consumed, separately from genuine waitpid
failures.

@gc00
gc00 force-pushed the mcmini-dmtcp-tsan branch 3 times, most recently from 7d6a1af to 6c4245f Compare July 31, 2026 18:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with 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.

Inline comments:
In `@src/lib/interception.c`:
- Around line 282-285: Update the pthread-exit interception flow around
libpthread_pthread_exit and mc_pthread_exit to add a DMTCP pthread_exit
forwarding pointer alongside the existing libdmtcp_pthread_create/join pointers.
Initialize and select the DMTCP wrapper when DMTCP is enabled, while retaining
the native libpthread pointer otherwise, so pre-checkpoint exits are forwarded
through DMTCP before restart handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1fae7b25-7b8c-4bdf-90c3-59a364da8675

📥 Commits

Reviewing files that changed from the base of the PR and between 7d6a1af and 6c4245f.

📒 Files selected for processing (20)
  • doc/cond-wait-tsan-interceptor-bypass.txt
  • doc/log-mutex-fork-desync.txt
  • doc/pthread-exit-abort-and-fiber-crash.txt
  • doc/tsan-mutex-annotation-false-positive.txt
  • include/mcmini/lib/log.h
  • include/mcmini/spy/intercept/interception.h
  • include/mcmini/spy/intercept/wrappers.h
  • src/common/multithreaded_fork.c
  • src/examples/CMakeLists.txt
  • src/examples/cv-producer-consumer-safe.c
  • src/examples/exit-stress-noop.c
  • src/examples/exit-stress.c
  • src/examples/producer-consumer-exit.c
  • src/examples/producer-consumer-safe.c
  • src/lib/dmtcp-callback.c
  • src/lib/interception.c
  • src/lib/log.c
  • src/lib/pthread_cond_signal_wrap.c
  • src/lib/pthread_cond_wait_wrap.c
  • src/lib/wrappers.c
🚧 Files skipped from review as they are similar to previous changes (14)
  • src/lib/pthread_cond_signal_wrap.c
  • include/mcmini/lib/log.h
  • src/lib/log.c
  • doc/pthread-exit-abort-and-fiber-crash.txt
  • doc/cond-wait-tsan-interceptor-bypass.txt
  • src/examples/exit-stress-noop.c
  • include/mcmini/spy/intercept/wrappers.h
  • include/mcmini/spy/intercept/interception.h
  • src/examples/exit-stress.c
  • src/lib/pthread_cond_wait_wrap.c
  • src/common/multithreaded_fork.c
  • src/examples/CMakeLists.txt
  • src/lib/dmtcp-callback.c
  • src/lib/wrappers.c

Comment thread src/lib/interception.c
Comment on lines +282 to +285
MCMINI_NO_RETURN void libpthread_pthread_exit(void *retval) {
libmcmini_init();
(*libpthread_pthread_exit_ptr)(retval);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Trace all local pthread-exit dispatch paths and DMTCP forwarding symbols.
rg -n -C 6 '\b(mc_pthread_exit|pthread_exit|libpthread_pthread_exit|libdmtcp_pthread_exit)\b' src include
rg -n -C 4 'libdmtcp_pthread_(create|join|exit)|dmtcp.*pthread_exit' src include

Repository: mcminickpt/deep-debug

Length of output: 34051


🌐 Web query:

DMTCP pthread_exit DMTCP callback dlsym pthread_exit dmtcp_pthread_exit

💡 Result:

DMTCP (Distributed MultiThreaded Checkpointing) utilizes symbol interposition to intercept various system and library calls, including POSIX threads functions like pthread_exit [1][2]. When DMTCP intercepts these calls, it often needs to resolve the original, underlying system function to maintain correct application state [3][4]. Technical details regarding the interaction between DMTCP, pthread_exit, and dlsym: 1. Symbol Interposition and Recursion: DMTCP interposes functions such as pthread_create and pthread_exit by implementing wrappers that record state or perform necessary checkpointing operations [1][2]. A common challenge is that these wrappers may need to call the "real" underlying system function [5][4]. Using standard dlsym(RTLD_NEXT,...) within these wrappers can lead to infinite recursion or deadlocks if the dlsym call itself triggers code paths that require the same locks or state management as the DMTCP wrapper [3][5][6]. 2. Custom dlsym Implementation: To mitigate issues like infinite recursion—especially in modern GLIBC versions where libdl/dlsym may call malloc or other intercepted functions—DMTCP includes its own internal implementation of dlsym [3]. This custom dlsym is used to resolve real symbols without triggering the overhead or side effects of the system's dynamic linker-based dlsym in sensitive contexts [3]. 3. Deadlock Risks: Historically, DMTCP has encountered deadlocks during checkpointing events (e.g., DMTCP_EVENT_THREADS_SUSPEND) when a thread is interrupted while inside a dlsym call [6]. If the DMTCP checkpoint thread then calls dlsym (or a function that calls dlsym, such as a pthread wrapper) and attempts to acquire a mutex already held by the suspended thread, a deadlock occurs [6]. Developers have addressed such issues by employing internal wrappers and locking mechanisms (e.g., WRAPPER_EXECUTION_LOCK) to ensure that interposition calls are atomic and safe during checkpoint/restart operations [6]. 4. Practical Usage: While dmtcp_pthread_exit is not typically a public API, it represents the internal handling logic within the DMTCP interception layer to manage thread termination cleanly during migration or checkpointing [1][2]. If you are encountering issues related to these symbols, it is often due to the order of library preloading (LD_PRELOAD) or linker-related symbol resolution conflicts [5][7]. Ensuring that DMTCP's wrappers are correctly positioned in the library load order is essential for stability [5].

Citations:


🌐 Web query:

dmTCP code pthread_exit dlsym RTLD_NEXT DMTCP wrapper pthread_exit

💡 Result:

DMTCP (Distributed MultiThreaded Checkpointing) uses library interposition to intercept system and library calls, including POSIX thread functions like pthread_exit [1][2]. The mechanism relies on dlsym(RTLD_NEXT, ...) to locate and call the "real" underlying implementation of a function after the wrapper has performed its necessary checkpointing or virtualization tasks [3][4][5]. When DMTCP wraps a function such as pthread_exit, it typically follows this pattern: 1. Interposition: The wrapper function in the DMTCP library intercepts the call, as it is loaded into the application's address space (often via LD_PRELOAD) [1][6]. 2. Resolution: The wrapper uses dlsym(RTLD_NEXT, "pthread_exit") to retrieve the address of the actual glibc implementation of pthread_exit [4][7][8]. 3. Execution: The wrapper executes its logic (e.g., updating thread state for checkpointing purposes) and then calls the resolved real function [1][9]. Technical challenges associated with this approach include: - Infinite Recursion: Using dlsym within a wrapper can trigger recursion if dlsym itself triggers a call to a wrapped function (like malloc or a thread-related function) [1][9]. To mitigate this, DMTCP often uses its own internal, low-level implementations of dlsym or safeguards like WRAPPER_EXECUTION_LOCK to prevent deadlocks and recursion during sensitive operations [1][9]. - Symbol Resolution: Ensuring RTLD_NEXT correctly finds the next symbol requires careful management of link order and library dependencies [7][8]. If the application or other libraries interfere with the search order, dlsym may fail or return an incorrect symbol [7]. - Architecture and Environment: DMTCP manages differences in dlsym behavior across architectures and glibc versions, sometimes using environment variables (such as DMTCP_DLSYM_OFFSET) or specific build configurations to ensure robust symbol lookup [10][1]. In summary, the use of dlsym(RTLD_NEXT, ...) in DMTCP wrappers is a standard technique for transparently interposing library calls, with specific internal modifications implemented to avoid the common pitfalls of deadlock and infinite recursion that occur when wrapping core system functionality [4][1][9].

Citations:


🌐 Web query:

DMTCP source pthread_exit dlsym pthread_exit libdmtcp_pthread_exit

💡 Result:

DMTCP (Distributed MultiThreaded Checkpointing) utilizes symbol interposition to intercept various system and library calls, including thread-related functions like pthread_exit [1][2]. This interposition allows DMTCP to manage checkpointing and thread state synchronization [1][2]. Regarding the specific symbols mentioned: 1. pthread_exit: DMTCP provides wrappers for pthread functions (often found in files like wrappers.cpp or similar components within the source tree) to ensure that thread lifecycle events, such as thread creation and exit, are correctly tracked for consistent checkpointing [3][4][2]. 2. dlsym: DMTCP implements its own internal dlsym mechanism to avoid infinite recursion and deadlocks [1][5]. In many GLIBC versions, calling standard libdl/dlsym functions can trigger further libc/malloc calls, which may conflict with DMTCP's internal state or initialization sequence [1][5][3]. Consequently, DMTCP frequently uses its own dlsym wrapper—often bypassing standard libdl or using RTLD_NEXT to reach the "real" underlying system function—to maintain control without triggering circular dependencies [6][7][5]. 3. libdmtcp_pthread_exit: This notation generally refers to the internal symbol or the wrapped implementation of pthread_exit within the DMTCP library (libdmtcp.so) [8][4]. When an application is run under DMTCP, the linker/loader redirects calls to the original pthread_exit to the DMTCP-provided wrapper, which performs the necessary bookkeeping before potentially invoking the original system pthread_exit [6][7]. The architecture relies on these wrappers to handle the complexities of multi-threaded application state, specifically ensuring that threads are suspended or in a known state during the checkpointing process [5][4]. The use of dlsym within this context is a critical technical pattern in DMTCP to ensure that the interception layer remains robust even when applications or other libraries (like malloc or libdl) attempt to perform dynamic symbol resolution [1][5][3].

Citations:


🌐 Web query:

github dmtcp dmtcp pthread_exit RTLD_NEXT "pthread_exit"

💡 Result:

The combination of DMTCP (Distributed MultiThreaded Checkpointing), pthread_exit, and RTLD_NEXT is typically associated with the implementation of function wrappers for checkpoint/restart (C/R) functionality [1][2]. DMTCP and similar interception-based tools often use dlsym(RTLD_NEXT, "pthread_exit") to locate the original, unmodified implementation of pthread_exit within the system's libc or libpthread [3][1][2]. This allows the tool to intercept calls made by the application, perform necessary C/R housekeeping (such as updating metadata or managing state), and then invoke the original function to complete the operation [4][2]. Common technical challenges associated with this pattern include: 1. Symbol Resolution and Infinite Loops: When a library is preloaded (using LD_PRELOAD) or linked in a way that it is searched first, using dlsym(RTLD_NEXT, ...) correctly finds the "next" occurrence of the symbol [2][5]. If the lookup is misconfigured or if multiple wrappers conflict, it can lead to symbol resolution errors or infinite recursion if the wrapper accidentally calls itself [2]. 2. The noreturn Attribute: pthread_exit is frequently declared with the attribute((noreturn)) in system headers [4]. This can cause compiler optimizations that make it difficult to reliably wrap or override the function [4]. Developers often use macros to redefine pthread_exit to a wrapper function that lacks the noreturn attribute, ensuring that control can return to the shim after the wrapper code executes [6][4]. 3. Dependency on Unwinding/libgcc_s.so.1: pthread_exit often requires stack unwinding support (for C++ exception cleanup or thread cancellation handlers) [7][8]. Historically, this was managed by libpthread, but in modern glibc (since version 2.34), libpthread has been merged into libc [9][10]. If a wrapper or environment fails to correctly handle the dynamic loading of dependencies like libgcc_s.so.1, applications may crash or exit with errors stating that the library must be installed for pthread_exit to work [7][8][10]. 4. Deadlocks during Checkpointing: If dlsym() is called during a checkpoint event (when threads are suspended), it may trigger a deadlock if the dynamic linker itself requires a mutex that is already held by the suspended process [11]. This is why sophisticated wrappers often require specific synchronization or locking mechanisms (such as WRAPPER_EXECUTION_LOCK) to prevent re-entrancy during critical state-capture operations [11]. For issues involving DMTCP specifically, if you are experiencing failures, ensure that your environment correctly handles symbol resolution and that your wrappers are compiled to accommodate modern glibc symbol structures [2][12]. If you encounter segfaults, checking for -Wl,--no-as-needed during the link phase can sometimes resolve issues where the linker fails to correctly map the symbols at runtime [5].

Citations:


🌐 Web query:

DMTCP code dmtcp-pthread.cpp pthread_exit dlsym RTLD_NEXT

💡 Result:

DMTCP (Distributed MultiThreaded Checkpointing) uses function interposition—typically via LD_PRELOAD—to intercept libc and pthreads library calls [1][2][3]. Within its source code, such as files like dmtcp-pthread.cpp, DMTCP utilizes dlsym(RTLD_NEXT,...) to locate and invoke the "real" underlying libc/pthreads functions [4][5]. Key aspects of this mechanism include: 1. Interposition and dlsym(RTLD_NEXT): When DMTCP intercepts a function like pthread_exit, it must eventually call the original implementation provided by the system's glibc to maintain correctness [2]. Using dlsym(RTLD_NEXT, "pthread_exit") allows DMTCP to bypass its own wrapper and retrieve the address of the next symbol in the search order, which is the actual glibc function [6][5][7]. 2. Handling Recursion and Deadlocks: A significant challenge with this approach is avoiding infinite recursion and deadlocks. If a wrapper calls dlsym, and dlsym itself triggers a call to a function that DMTCP also wraps (such as malloc or a mutex lock), it can lead to deadlocks or infinite loops [1][8]. To mitigate this, DMTCP has implemented several strategies: - Implementing custom dlsym logic to reduce reliance on the system's libdl, which may call libc functions that DMTCP hooks [1]. - Using internal locks (like WRAPPER_EXECUTION_LOCK) to prevent re-entrant calls or deadlocks when the checkpointing thread and user threads compete for internal resources during event hooks [8]. 3. Architectural Variations: The implementation details, such as the exact dlsym offset, can vary between architectures (e.g., 32-bit vs. 64-bit), and DMTCP handles these differences by calculating offsets during launch/restart and storing them in shared data structures [9]. By wrapping these functions, DMTCP creates a "shadow" environment that allows it to track process state, manage thread lifecycle events, and coordinate checkpoints without modifying the target application's source code [1][2][3].

Citations:


🌐 Web query:

github dmtcp pthread_exit wrapper dlsym RTLD_NEXT

💡 Result:

In DMTCP (Distributed MultiThreaded Checkpointing), interposition of library functions like pthread_exit is a core mechanism used to track state and manage checkpoint/restart operations [1][2][3]. To safely intercept these functions, DMTCP typically uses a wrapper pattern [4][5][2]. When a wrapper function intercepts a call (e.g., pthread_exit), it often needs to invoke the original libc implementation to ensure correct behavior [4][6][7]. Using dlsym(RTLD_NEXT, "function_name") is the standard mechanism to find the address of the next symbol in the search order—specifically, the original version provided by the C library (libc/libpthread) rather than the wrapper itself [4][6][8][9]. Key technical aspects include: 1. Preventing Infinite Recursion: If a wrapper function calls the function it is wrapping (e.g., a wrapper for pthread_exit calls pthread_exit), it creates an infinite loop. Using RTLD_NEXT with dlsym allows the wrapper to bypass itself and call the next available implementation in the symbol search scope [4][9]. 2. Robustness and Cleanup: DMTCP has evolved its approach to handle symbol resolution more robustly, including replacing manual dlsym calls with internal abstractions like dmtcp_dlsym to better manage platform-specific nuances, versioning, and potential errors during symbol lookup [5][10][7]. 3. Configuration: DMTCP provides configuration options (such as --disable-dlsym-wrapper) to allow users to handle special environments where standard symbol interposition might behave unexpectedly or conflict with other libraries [11][10]. 4. Complexities: Overriding functions declared with attribute((noreturn))—like pthread_exit—can be particularly challenging, sometimes requiring macro-based overrides or specific shims to prevent the compiler from optimizing away the wrapper or misinterpreting the control flow [12][13]. In summary, the use of dlsym with RTLD_NEXT in DMTCP wrappers is essential for transparently interposing on standard library functions without causing infinite recursion or breaking the original functionality of the application being checkpointed [4][9][2].

Citations:


🌐 Web query:

DMTCP pthread_create pthread_join pthread_exit wrapper dmtcp-pthread.cpp

💡 Result:

DMTCP (Distributed MultiThreaded CheckPointing) uses interposition via wrappers to intercept and manage POSIX thread (pthread) operations transparently [1][2]. While the specific file name in the DMTCP source code may vary by version or refactoring, thread management logic is typically implemented in files such as threadwrappers.cpp or similar components that handle POSIX thread library calls [3][1]. DMTCP wraps critical functions like pthread_create, pthread_join, and pthread_exit to maintain consistency during checkpointing and restart [1][4]: 1. pthread_create: The wrapper intercepts the creation of new threads. This allows DMTCP to track active threads, manage their state, and ensure that if a checkpoint occurs, the process can correctly recreate the threading environment upon restart [4]. 2. pthread_join: By wrapping this function, DMTCP can monitor thread synchronization and dependencies. It ensures that the checkpointing process accounts for threads waiting for others to finish, preventing inconsistent states [5][6]. 3. pthread_exit: The wrapper for pthread_exit allows DMTCP to clean up internal tracking data structures when a thread terminates, ensuring that the process's thread metadata remains accurate during its lifecycle [3][4]. DMTCP operates at the user level, using these wrappers to "spy" on and record the necessary thread state without requiring kernel modifications [7][1][8]. By interposing on libpthread.so, DMTCP can quiesce threads during checkpointing by forcing them to block in a signal handler and then resume them once the checkpoint image is captured [4]. Plugin architectures in DMTCP also allow for additional hooks and user-defined wrappers around these library calls to support virtualization of resources [9].

Citations:


🌐 Web query:

dmTCP dmtcp-pthread.cpp pthread_exit dmtcp library dlsym RTLD_NEXT

💡 Result:

DMTCP (Distributed MultiThreaded Checkpointing) utilizes symbol interposition to intercept various libc and libpthread functions, including pthread_exit, to maintain process state for checkpointing and restoration [1][2]. The file dmtcp-pthread.cpp (or similar files within the DMTCP source tree) implements these wrappers [1][2]. To intercept functions like pthread_exit while still being able to call the original (libc) implementation, DMTCP commonly uses dlsym with RTLD_NEXT [3][4]. This technique allows the DMTCP library to obtain the address of the "next" occurrence of a symbol in the dynamic library search order, effectively bypassing the DMTCP wrapper itself to reach the underlying system implementation [3][4]. Key technical considerations regarding this mechanism include: 1. Infinite Recursion: Using dlsym(RTLD_NEXT, ...) inside a wrapper function is a standard way to call the real function. However, if the implementation of dlsym or other internal DMTCP mechanisms (like libdl calls) inadvertently trigger another intercepted function, it can lead to infinite recursion or deadlocks [5][6]. DMTCP has historically implemented its own internal dlsym or specific guards to mitigate these recursion issues, especially when modern glibc implementations of dlsym internally call malloc or other functions that DMTCP also wraps [5][6]. 2. Thread Safety and Deadlocks: Intercepting pthread functions is complex because these functions are often used by the checkpointing mechanism itself (e.g., when DMTCP needs to suspend or manage threads) [6][7]. If dlsym is called while the process is in a state where internal mutexes are held (or if the suspension logic interacts poorly with the libdl lock), deadlocks can occur [6]. DMTCP employs synchronization primitives, such as WRAPPER_EXECUTION_LOCK, within its wrappers to prevent these race conditions during checkpointing events [6]. 3. Symbol Visibility and Linking: For symbol interposition to function correctly, the DMTCP library must be loaded before the application (typically via LD_PRELOAD) [1]. If the application or its dependencies are linked in a way that ignores these preloaded symbols (e.g., using --as-needed or static linking), interposition may fail [4]. DMTCP's architecture for wrapping is designed to be transparent to the user, acting as a shadow database for process internals while forwarding the ultimate execution request to the glibc/kernel [8]. Over time, DMTCP has refined these wrappers to be more robust, moving away from simple dlsym usage toward custom implementations to ensure reliability across different glibc versions and architectures [9][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant interfaces, dispatch paths, and DMTCP loading/use without executing repo code.
sed -n '1,170p' src/lib/interception.c
printf '\n--- wrappers dispatch ---\n'
sed -n '580,635p' src/lib/wrappers.c
sed -n '836,945p' src/lib/wrappers.c
printf '\n--- creation wrapper declaration ---\n'
sed -n '82,115p' include/mcmini/spy/intercept/interception.h

Repository: mcminickpt/deep-debug

Length of output: 15434


🌐 Web query:

DMTCP dmtcp-pthread.cpp symbol "pthread_exit" "dlsym" "pthread_create"

💡 Result:

DMTCP (Distributed MultiThreaded Checkpointing) uses function interposition (via LD_PRELOAD) to intercept POSIX thread (pthread) calls, enabling transparent checkpointing and restoration of multi-threaded applications [1][2][3]. Regarding the symbols and functions mentioned: 1. dlsym and symbol resolution: To avoid infinite recursion caused by the fact that many glibc functions (including libdl functions) internally call malloc or other intercepted functions, DMTCP implements its own version of dlsym [4]. This custom dlsym allows DMTCP to retrieve the addresses of original libc functions without triggering the wrappers it has injected [4]. 2. pthread_create and pthread_exit: DMTCP wraps these functions to maintain awareness of the application's thread state, which is critical for consistent checkpointing [2]. - By intercepting pthread_create, DMTCP can track thread lifecycles, assign internal thread IDs, and ensure that all threads are in a quiescent or consistent state when a checkpoint is triggered [2]. - pthread_exit is wrapped to ensure that when a thread terminates, DMTCP updates its internal thread management structures (e.g., thread lists) correctly [5]. This is necessary to maintain an accurate representation of the process state for restoration [2][5]. While dmtcp-pthread.cpp (or similar source files in the DMTCP repository) contains these wrapper implementations, the mechanism relies on obtaining the true address of the underlying libc/pthread functions (often stored in internal function pointers) and then executing them after DMTCP performs its own necessary tracking or state-saving operations [6][7]. If you are investigating specific behavior related to these functions, note that DMTCP frequently updates its pthread handling to accommodate changes in glibc versions, such as those related to thread stack allocation, signal handling (e.g., SIGCANCEL), and internal thread list management [5][8][9]. Recent versions have focused on robustness improvements for these wrappers [4][8].

Citations:


🌐 Web query:

DMTCP 3.0 dmtcp-pthread.cpp pthread_exit dlsym pthread_exit wrapper

💡 Result:

DMTCP (Distributed MultiThreaded CheckPointing) uses a wrapping mechanism for POSIX thread and dynamic linking functions to intercept calls for checkpointing and state management [1][2]. In DMTCP, the dlsym wrapper is critical for maintaining consistency in dynamically linked applications. Because DMTCP intercepts library calls (like pthread functions) using LD_PRELOAD, it must ensure that subsequent calls to dlsym (e.g., inside an application) correctly resolve to either the real system library or, when necessary, the DMTCP-interposed version to avoid infinite recursion or deadlocks [3][4]. The pthread_exit wrapper in dmtcp-pthread.cpp (or related thread wrapper files) is responsible for handling thread termination within the context of DMTCP's thread management. When a thread calls pthread_exit, the wrapper allows DMTCP to clean up internal thread-local state, update connection lists, or manage thread synchronization events (such as quiescence for checkpointing) before allowing the thread to exit via the real pthread_exit provided by glibc [2][4]. Key technical aspects include: 1. Wrapping Mechanism: DMTCP often uses dlsym(RTLD_NEXT,...) within its wrappers to locate the original libc function once it has performed its own internal monitoring or state update [5][3]. 2. Deadlock Prevention: As noted in past issues, calls to dlsym inside wrappers can lead to deadlocks if they occur during events like thread suspension or checkpointing. Consequently, DMTCP implementations often employ internal locking (e.g., WRAPPER_EXECUTION_LOCK) to ensure that the thread executing the dlsym or wrapper code is not suspended or interrupted in a way that causes recursion or blocking [4]. 3. DMTCP 3.0: This version, released in 2023, continues to maintain these core architectural principles, though it introduced stricter C++14 requirements and specific bug fixes for platform-specific pthread behaviors [6][7][8]. Users encountering issues with these wrappers—such as hanging or unexpected exits—are generally advised to verify their environment's LD_PRELOAD order or, in specific cases, use configuration flags like --disable-dlsym-wrapper if the application's linking behavior conflicts with the standard DMTCP interception logic [6][3].

Citations:


🌐 Web query:

github dmtcp dmtcp-pthread.cpp pthread_exit pthread_create dlsym

💡 Result:

In DMTCP, the interaction between dmtcp-pthread.cpp, pthread_create, pthread_exit, and dlsym is central to its ability to transparently checkpoint and restart multi-threaded applications [1]. DMTCP uses symbol interposition (wrappers) to intercept calls to POSIX thread functions like pthread_create and pthread_exit [1]. These wrappers allow DMTCP to manage thread lifecycle, handle synchronization during checkpointing, and ensure proper state restoration upon restart [2][3]. Key implementation details include: 1. Custom dlsym Implementation: DMTCP implements its own version of dlsym to avoid infinite recursion issues that occur in modern GLIBC versions where libdl may call malloc or other intercepted functions [4][5]. By using this internal dlsym, DMTCP can reliably find the address of the "real" underlying libc functions without triggering intercepted wrappers recursively [6][7]. 2. Handling Interception: When a wrapper function (e.g., pthread_create) is called, it typically uses the NEXT_FUNC macro (or equivalent mechanism) to forward the call to the actual libc implementation [6]. To perform this lookup, DMTCP uses its internal dlsym [4][5]. 3. Multithreading & Deadlocks: Because dlsym may use internal locks, using it during checkpoint events can lead to deadlocks if the main thread is suspended while holding a lock that the checkpoint thread needs [7]. To mitigate this, DMTCP often employs specific locking strategies (e.g., WRAPPER_EXECUTION_LOCK) within its wrappers to safely manage concurrent calls to intercepted functions [7]. 4. Multi-architecture Support: DMTCP calculates dlsym offsets for both 32-bit and 64-bit architectures, storing these as environment variables (e.g., DMTCP_DLSYM_OFFSET) or in a shared data area to ensure that the correct symbol resolution occurs regardless of the process architecture [8]. In essence, DMTCP wraps thread functions to gain control over execution, uses a custom dlsym to bypass GLIBC's own redirection mechanisms and avoid recursion, and carefully manages locking to prevent deadlocks during the quiescence phase of checkpointing [4][7][3].

Citations:


🌐 Web query:

github dmtcp dmtcp-pthread.cpp dmtcp_pthread_exit dmtcp_pthread_create dlsym RTLD_NEXT

💡 Result:

In the DMTCP (Distributed MultiThreaded CheckPointing) project, the use of dlsym with RTLD_NEXT is a standard technique for intercepting and wrapping system calls like pthread_create and pthread_exit [1][2][3]. DMTCP functions as a transparent checkpointing tool by injecting a library (typically via LD_PRELOAD) that interposes on key libc and pthread symbols [4][5]. By defining its own versions of functions like pthread_create, DMTCP can track thread creation, manage thread-local storage, and ensure correct state restoration during checkpoint and restart operations [4][5]. Technical details regarding this implementation include: 1. Interception and Forwarding: When an application calls a wrapped function like pthread_create, the DMTCP implementation intercepts the call [1][5]. To invoke the actual system library implementation of the function (the "real" version), DMTCP uses dlsym(RTLD_NEXT, "function_name") [1][6][2]. The RTLD_NEXT handle is critical as it instructs the dynamic linker to find the next occurrence of the symbol in the search order—specifically, the version in the actual system libraries (e.g., libpthread) rather than the wrapper itself, thus avoiding infinite recursion [1][2][3]. 2. Handling Recursion and Deadlocks: Because dlsym itself can trigger library calls that might lead to further interceptions (and potential infinite recursion or deadlocks), DMTCP has historically implemented robust wrappers [4]. As noted in project issues, improper use of dlsym can lead to deadlocks if it triggers a call to a function that is already being intercepted or if it competes for internal locks within the dynamic linker [1][7]. To mitigate this, DMTCP often uses its own internal dlsym implementation or employs locking mechanisms (such as WRAPPER_EXECUTION_LOCK) to ensure that the process of resolving the real symbol is thread-safe and does not interfere with the checkpointing events [7][4]. 3. Code Context: The pattern is visible in various source files within the DMTCP repository, such as those related to thread wrappers (e.g., threadwrappers.cpp) and plugin-based system call interception [7][8][9]. The use of dlsym(RTLD_NEXT,...) is the standard pattern for creating these trampolines to the underlying system calls [3][10]. In summary, DMTCP relies on the dlsym(RTLD_NEXT,...) mechanism to safely interpose on pthread and system functions to maintain its checkpointing state while transparently passing execution to the underlying system implementation [1][7][4].

Citations:


🌐 Web query:

github dmtcp dmtcp-pthread.cpp "pthread_exit" "dlsym"

💡 Result:

In the DMTCP (Distributed MultiThreaded Checkpointing) project, the use of dmtcp-pthread.cpp, pthread_exit, and dlsym relates to the project's strategy for interposing POSIX thread functions to support transparent checkpointing [1][2]. DMTCP wraps pthread functions to intercept application behavior, allowing it to capture and restore the state of multi-threaded applications [1]. A key challenge in this interposition is avoiding infinite recursion and deadlock, particularly because DMTCP itself may need to call underlying system functions (like those in libpthread or libdl) that it is also interposing [3][4]. Key aspects regarding these components in DMTCP include: 1. Interposition and dlsym: DMTCP often uses dlsym(RTLD_NEXT, ...) to locate and call the "real" underlying libc or pthread functions [5][6]. However, direct use of dlsym can be problematic in recent GLIBC versions because libdl functions may call malloc or other functions that DMTCP also interposes, creating infinite recursion or circular dependencies [4][7]. 2. Custom dlsym Implementation: To address these issues, DMTCP implemented its own internal dlsym wrapper [4][7]. This custom implementation is designed to be more robust, avoiding reliance on standard libdl functions where possible to prevent deadlocks during critical DMTCP events (e.g., when threads are being suspended for a checkpoint) [3][4]. 3. pthread_exit handling: Like other pthread functions, pthread_exit is intercepted to ensure that when a thread exits, DMTCP can properly update its internal thread accounting and state management before the thread actually terminates [2]. If an application uses pthread_exit, the DMTCP wrapper captures the call, performs necessary bookkeeping (such as cleaning up thread-specific state), and then invokes the original pthread_exit (often retrieved via the internal dlsym mechanism) to allow the thread to exit normally [2]. If you are investigating specific behavior in dmtcp-pthread.cpp (or related files like pthread_override.c), the logic typically follows the pattern of identifying the current thread, recording the state if needed, and safely calling the real library function through DMTCP's internal symbol resolution mechanism [2][3]. If you encounter hangs or crashes, it is often due to the wrapper attempting to perform an operation (like dlsym or a mutex lock) that is not re-entrant or is deadlocking due to the thread suspension state [3].

Citations:


🌐 Web query:

github dmtcp dmtcp-pthread.cpp "dmtcp_intercept_next_func" "pthread_exit"

💡 Result:

In the DMTCP (Distributed MultiThreaded CheckPointing) source code, dmtcp_intercept_next_func is a utility function used within the library's interception mechanism to call the original, underlying system or library function that DMTCP has wrapped [1]. DMTCP operates by transparently intercepting various system calls and library functions (such as POSIX thread functions) to manage checkpointing and restoration state [2][1]. Because DMTCP overrides these standard library symbols (e.g., using LD_PRELOAD), it requires a way to invoke the original implementation (e.g., the actual pthread_exit or other pthread functions) to ensure the application's intended behavior is maintained alongside DMTCP's management tasks [1]. While specific implementation details can change across versions, files like dmtcp-pthread.cpp typically house the wrappers for POSIX thread functions [3]. The dmtcp_intercept_next_func utility is essential in these wrappers to: 1. Allow the application to execute the original logic provided by glibc or other libraries. 2. Maintain the transparency of the checkpointing process by ensuring that intercepting a call does not break the expected behavior of the thread or process. If you are encountering issues related to pthread_exit or other thread-related functions in DMTCP, it is often due to complex interactions with specific glibc versions or thread cleanup handling [3][4][5]. Recent development in the DMTCP GitHub repository has focused on addressing compatibility issues with newer glibc versions, including bug fixes for pthread cancellation and thread management during restart [3][4][6].

Citations:


🌐 Web query:

"dmtcp-pthread.cpp" "pthread_exit" "pthread_create"

💡 Result:

DMTCP (Distributed MultiThreaded Checkpointing) uses wrappers for pthread functions, including pthread_create and pthread_exit, to enable transparent checkpointing of multi-threaded applications [1][2]. These wrappers allow DMTCP to intercept thread creation and termination to track the state of threads across checkpoints and restarts [3][4][5]. Key technical details regarding these functions in the context of DMTCP include: Wrappers and Interception: DMTCP intercepts POSIX thread library calls (like pthread_create) via library interposition, often by linking a shared library (dmtcp.so) that overrides the system implementation [6][1]. This allows DMTCP to maintain its own metadata about application threads [4]. Implementation in source code: Within the DMTCP source, thread-related functionality is often handled in files like threadwrappers.cpp [4][5]. For example, when a thread is created, DMTCP typically uses a wrapper that initializes tracking structures before invoking the real system pthread_create (often accessed via dlsym(RTLD_NEXT,...)) [6]. pthread_exit handling: Wrappers for pthread_exit are used to ensure that when a thread terminates, DMTCP correctly updates its internal process/thread state, allowing it to accurately resume the application during a restart [3][4]. Common Challenges: Infinite Recursion: Because DMTCP wraps pthread functions, issues can arise if an overridden function inadvertently calls itself or another wrapped function without correctly bypassing the wrapper (e.g., via dlsym(RTLD_NEXT)) [6]. Thread State Tracking: Bugs have occasionally appeared related to how DMTCP tracks thread state, particularly with detached threads or specific glibc versions, where the internal metadata (like struct pthread) needs careful synchronization with the operating system's thread management [4]. System Integration: DMTCP is designed to work in user space without kernel modifications [1][2]. Consequently, its thread wrappers must rely on standard glibc interfaces (like NPTL) [4][7], which can lead to complexities when the underlying glibc implementation changes or behaves unexpectedly [4]. In summary, dmtcp-pthread.cpp (or corresponding thread wrapper files) acts as a compatibility layer that intercepts standard pthread calls to manage the lifecycle and state of threads for checkpoint/restart purposes [3][4][5].

Citations:


Forward DMTCP thread exits through the DMTCP wrapper.

DMTCp interposes on pthread_exit, but mc_pthread_exit() still calls libpthread_pthread_exit(retval) before DMTCP restart handling for pre-checkpoint paths. Add a DMTCP pthread_exit forwarding pointer like libdmtcp_pthread_{create,join} and select it when DMTCP is 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 `@src/lib/interception.c` around lines 282 - 285, Update the pthread-exit
interception flow around libpthread_pthread_exit and mc_pthread_exit to add a
DMTCP pthread_exit forwarding pointer alongside the existing
libdmtcp_pthread_create/join pointers. Initialize and select the DMTCP wrapper
when DMTCP is enabled, while retaining the native libpthread pointer otherwise,
so pre-checkpoint exits are forwarded through DMTCP before restart handling.

@gc00
gc00 force-pushed the mcmini-dmtcp-tsan branch from 6c4245f to 9500922 Compare July 31, 2026 22:12
The TSAN-supporting DMTCP branch (tsan-phased-init) bumped the
plugin API from v3 to v4, an ABI change (DmtcpPluginDescriptor_t /
DmtcpUniqueProcessId, new DmtcpCkptHeader etc.). DMTCP refused to
load libmcmini.so:

  ASSERT pluginmanager.cpp:228: incompatible DMTCP plugin API version:
  plugin_api=3 expected=4

Sync the vendored include/dmtcp.h to DMTCP's v4 header (correct
version string and descriptor ABI), and carry forward the only
McMini-specific additions -- the mcmini_virtual_pid / mcmini_real_pid
macros -- updated to the v4 function names
(dmtcp_{real_to_virtual,virtual_to_real}_pid became
dmtcp_pid_{real_to_virtual,virtual_to_real}). Also update the two
direct callers in multithreaded_fork.c. The unused
dmtcp_restore_buf_* decls are dropped (not referenced by libmcmini,
and gone from v4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
gc00 and others added 19 commits July 31, 2026 18:15
mc_pthread_cond_wait() never calls the real libpthread_cond_wait()/
libpthread_cond_timedwait() in any post-restart mode (DMTCP_RESTART_
INTO_BRANCH/TEMPLATE, TARGET_BRANCH, TARGET_BRANCH_AFTER_RESTART): the
wait is entirely simulated via the mailbox handshake, true even in
classic (non-DMTCP) mode. But mc_pthread_cond_signal()/_broadcast()/
_init()/_destroy() still called the real libpthread_cond_signal()/
broadcast()/init()/destroy() in those same modes -- an asymmetry.

That asymmetry is dangerous specifically under DMTCP restart: a
clone()-recreated thread can still be genuinely, kernel-level blocked
inside a pre-restart real pthread_cond_timedwait() call (from RECORD
mode, if the checkpoint landed mid-call). A real signal/broadcast
reaching that thread wakes it for real, letting it resume running
application code without ever going through the model checker's own
scheduling -- breaking DPOR's single-stepping invariant. Unlike the
child_side_sem fix, this isn't a lost-wakeup story: it's a real,
uncontrolled wakeup escaping the model checker entirely, which is
arguably worse than a hang.

Fix: remove the real libpthread_cond_signal()/broadcast()/init()/
destroy() calls from all four post-restart cases, mirroring what
mc_pthread_cond_wait() already did -- once wait never consults the
real object's state, touching it from signal/broadcast/init/destroy
serves no purpose, only risk. RECORD/PRE_CHECKPOINT mode is untouched:
those real calls remain necessary and safe there, since it's one
continuous execution before any checkpoint exists.

Verified classic-mode cv-test produces byte-for-byte-equivalent output
(modulo debug-log pid/interleaving) before and after, confirming no
behavior change for the already-real-call-free wait path this mirrors.

See doc/glibc-cond-var-desync.txt for the full analysis, including why
a normally single-process (pshared=0) condition variable is affected by
the same class of bug as the pshared=1 child_side_sem mailbox semaphore
despite the different pshared requirement.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
producer-consumer has no condition variables at all, so it can't
exercise the "CV desync fix: stop touching cond_t after restart" fix.
Add a structurally identical producer-consumer variant that uses a
mutex + condition variable (count-based bounded buffer, single shared
cond, classic while-loop predicate wait) instead of the two
semaphores.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
mc_pthread_mutex_lock(), mc_pthread_join_impl(), and
mc_pthread_cond_wait() each hoist a struct timespec {.tv_sec = 2,
...} once, outside their RECORD-mode retry loop, and pass it to
libpthread_mutex_timedlock()/libpthread_timedjoin_np()/
libpthread_cond_timedwait() as an absolute deadline every iteration.
Since it's never computed from the current time, it means "2 seconds
past the epoch" -- decades in the past -- so every call returns
ETIMEDOUT immediately instead of ever genuinely blocking.
mc_sem_wait() (sem-wrappers.c) already does this correctly, calling
clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec++; fresh on each
iteration.

Found while trying to live-test the pthread_cond_t desync fix
(f54bb73) under a real DMTCP+TSan checkpoint/restart cycle: doing so
requires a thread to be genuinely, kernel-level blocked inside a
real pthread_cond_wait() at checkpoint time, which this bug made
impossible -- the RECORD-mode wait was actually a tight busy-poll,
never blocking long enough for anything to observe.

Fix: recompute the deadline via clock_gettime() on every iteration
in all three loops, matching mc_sem_wait()'s existing pattern.

Verified: cv-test, deadly-embrace, and classic-mode
producer-consumer give identical results; 20 consecutive
fresh-checkpoint --multithreaded-fork cycles against
producer-consumer-tsan still complete cleanly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
condition_variable_enqueue_thread::modify() requires the mutex to be
locked_by(executor) before the "enter wait" transition can be enabled,
but the recorded mutex_state carried only LOCKED/UNLOCKED, no owner --
translate_recorded_object_to_model() built the restored mutex with the
2-arg constructor, leaving owner default-constructed/unset. Every
restart with a thread mid-cond_wait therefore deadlocked immediately:
the mutex looked locked by no one that matched, so neither the
producer's lock nor the consumer's own wait-entry could ever become
enabled.

Added an owner field to mutex_state (objects.h), set it to tid_self
on every successful RECORD-mode mutex_lock and clear it on unlock, and
pass it through in mcmini.cpp's mutex reconstruction. Verified: the
same restart-from-checkpoint scenario that previously deadlocked
instantly now correctly treats the consumer's cond_wait as enabled at
the initial state. producer-consumer-tsan (mutex-only, no CVs)
regression-checked clean, 9 traces, no behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
transition::is_enabled_in() (transition.hpp) runs modify() against a
throwaway diff_state purely to check the returned status, discarding
the diff afterward. But condition_variable_enqueue_thread/_wait/
_signal/_brdcast's modify() all called mutating policy methods
(add_waiter_with_state, wake_thread, add_to_wake_groups,
receive_broadcast_message) directly on cv->get_policy() -- a pointer
into the *previous*, already-committed state's object, not anything
scoped to the throwaway diff. Every such "is this enabled" check,
even ones never actually applied, permanently corrupted the committed
policy. Confirmed live: a waiter that should appear once in a CV's
wait queue was appearing 4 times.

Fixed by cloning the policy (ConditionVariablePolicy::clone(), already
implemented but unused by these four call sites) and performing every
mutation on the clone, attaching it to the replacement object via the
existing set_policy() setter instead.

condition_variable_signal's replacement object additionally used a
constructor that never sets associated_mutex at all, leaving it
uninitialized -- condition_variable_wait::modify()'s mutex-location
check then never matches again for the rest of the run. Preserve it
via the existing set_associated_mutex() setter, same call site as the
set_policy() fix above.

Reduced the duplicate-waiter count in the live repro from 4 to 2 (real
progress, not yet a full fix) and didn't affect a second, still-open
bug in the same repro: a mutex object's location field reads back
corrupted partway through the same run, which looks like a separate,
deeper issue in state_sequence's replay/backtracking bookkeeping
rather than anything CV-specific -- not yet root-caused.

Regression-checked clean against all 6 existing TSan targets.
Off by default. Experiment to see whether instrumenting
libmcmini.so itself (as opposed to only the target) changes
TSan-registration-order crashes; empirically it did not move the
crash site, so the normal path stays uninstrumented per PLAN.txt's
suppressions-file design.
When a -fsanitize=thread target runs under DMTCP, dmtcp_launch
prepends libtsan ahead of libmcmini, so a new thread's entry order is

  dmtcp thread_start -> mc_thread_routine_wrapper -> libtsan
  trampoline -> user routine.

libmcmini's wrapper thus runs BEFORE libtsan has registered the
thread. Its prologue then called libc functions that libtsan
intercepts (malloc, and the raw pthread_rwlock_* in
insert_pthread_map), and those interceptors dereference the
unregistered thread's null ThreadState -> SEGV / TSan
"sanitizer_thread_registry.cpp:348" CHECK. (Full analysis in
TSAN-McMini-DMTCP.txt.)

Make the prologue free of TSan-intercepted libc calls:
  - Add mc_ts_alloc (mem.c/mem.h): a bump allocator over a static BSS
    arena. No libc call and no syscall on the fast path (only an
    atomic bump), so it never enters a TSan interceptor. Fail path
    uses raw syscalls only. Never frees (the pthread_map / rec_list
    nodes it backs are never freed).
  - insert_pthread_map / search_pthread_map: use libmcmini's
    libpthread_* handle wrappers (which bypass libtsan) instead of
    the raw pthread_rwlock_* symbols, and mc_ts_alloc instead of
    malloc. Also fixes a pre-existing missing-unlock bug in
    search_pthread_map's found path.
  - add_rec_entry_record_mode_ts (record.c): mc_ts_alloc-backed
    variant of add_rec_entry_record_mode; the prologue's THREAD-record
    insert uses it.

Verified: mcmini -i 3 on ~/dmtcp.git/test/tsan_target (plugin API v4)
no longer crashes in the thread-creation prologue; both worker
threads now run under RECORD mode.

Scope: this fixes the prologue only. mc_pthread_join still calls
pthread_timedjoin_np directly (no bypass handle), tripping libtsan's
ConsumeThreadUserId CHECK -- addressed next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
libmcmini's template thread was created via libdmtcp_pthread_create,
bypassing libtsan's pthread_create interceptor. When the target is
built with -fsanitize=thread, that left the template thread with no
libtsan ThreadState. On DMTCP restart, libtsan's setjmp/longjmp
restore (reached from threadlist.cpp:stopthisthread) dereferences
the thread's absent ThreadState and crashes with SIGSEGV during
thread restore.

Create the template thread through the public pthread_create
instead, so libtsan's interceptor registers it and wraps its start
routine. A new thread-local flag mc_creating_internal_thread tells
mc_pthread_create to skip the user-thread/model-checking machinery
and still route the actual creation through DMTCP
(libdmtcp_pthread_create), keeping the thread DMTCP-known. When the
target is not instrumented, the public pthread_create resolves
straight to mc_pthread_create and behavior is unchanged.

With this, record -> checkpoint -> restart of a TSAN target survives
thread restore (verified: raw dmtcp_restart restores all threads
with no SIGSEGV, where it previously crashed).

mc_pthread_create()'s other branch, for ordinary application threads
in classic (non-DMTCP) mode, had the same underlying disease: its
TARGET_BRANCH/TARGET_BRANCH_AFTER_RESTART case created every new
thread via libpthread_pthread_create(), resolved via
dlopen("libpthread")+dlsym() -- always real, raw glibc, bypassing
any interceptor unconditionally regardless of load order. TSan's own
pthread_create interceptor never got a chance to run for these
threads either. The very next libpthread_*-wrapped call from such a
thread (mc_register_this_thread() -> libpthread_mutex_lock() ->
libmcmini_init()'s lazy pthread_once()) hits TSan's own pthread_once
interceptor, which dereferences this thread's (nonexistent)
ThreadState and crashes: 100% reproducible SEGV in classic mode
against any TSan target (see
doc/classic-mode-thread-registration-segv.txt for the full backtrace
and analysis).

Fixed the same way in spirit: added tsan_or_real_pthread_create(),
resolved via dlsym(RTLD_NEXT, "pthread_create") instead of the
libpthread-handle dlsym. RTLD_NEXT finds whatever comes after
libmcmini.so in this process's actual load order: TSan's own
interceptor in classic mode (libmcmini loads ahead of libtsan
there), or real glibc under DMTCP (libtsan loads ahead of libmcmini
there, identical to today's behavior -- no regression risk for the
DMTCP+TSan restart flow, which handles its own recreated threads'
TSan registration via an unrelated mechanism, R3/R4's __clone +
fresh-fiber approach).

Verified: producer-consumer-tsan and cv-producer-consumer-tsan,
classic mode, zero crashes across multiple runs (previously 100%
crash). Classic-mode regression check (cv-test, deadly-embrace,
producer-consumer, cv-producer-consumer) unchanged. 15 consecutive
fresh-checkpoint --multithreaded-fork cycles against
producer-consumer-tsan under DMTCP still complete cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
restart_child_threads_fast() recreates each pre-checkpoint thread via
a raw clone() call, and fast_multithreaded_fork() forks the branch
process via _Fork() -- both bypass every interceptor libtsan relies
on to track threads, corrupting its runtime in three distinct ways:

- R2: _Fork() skips libtsan's fork() interceptor entirely, so its
  BeforeFork/AfterFork pipeline never runs -- the child inherits
  TSan's internal locks still held and a ThreadRegistry full of
  now-dead parent threads. Bracket _Fork() with
  __sanitizer_syscall_pre/post_impl_fork (weak, no-op for non-TSan
  targets) to re-run that pipeline.

- R3: the public clone() symbol hits libtsan's own interceptor, which
  treats every call as a fork and corrupts its thread-slot state for
  the CLONE_THREAD clone used here. Route through libc_clone(), a
  forwarding function dlsym'd from libc once at startup (added to
  interception.c's existing libc_abort()/libc_fork() table), rather
  than calling the raw __clone symbol directly -- DMTCP's own
  libdmtcp.so *also* strongly exports __clone and aborts unless the
  caller is mid-DMTCP's-own pthread_create, so bypassing libtsan's
  interceptor alone isn't enough; libc_clone() resolves the real,
  uninterposed primitive around both.

- R4: a clone()-recreated thread never went through libtsan's
  pthread_create interceptor, so it has no valid TSan ThreadState;
  the forking thread's own inherited (fork-copied) state also has a
  shadow call stack that can overflow as it keeps running. Give both
  a fresh TSan fiber (weak __tsan_create_fiber/__tsan_switch_to_fiber,
  no-op for non-TSan targets) before either does any TSAN-intercepted
  work.

Verified end-to-end against a real DMTCP checkpoint/restart of
producer-consumer (record via `mcmini -i 1`, restart via `mcmini
--from-checkpoint ... --multithreaded-fork`): the full pipeline
completes cleanly, correct DEADLOCK detection, exit 0, "Deep
debugging completed!", zero ASSERT/CHECK-failed/segfault occurrences.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implements a helper function to detect if a thread has a signal
blocked via its /proc/self/task/<tid>/status SigBlk mask. This will
be consumed by Task 2 to identify and skip ThreadSanitizer's
internal background thread during DMTCP checkpoint restart.

- Add include/mcmini/spy/checkpointing/tsan_support.h with the
  function declaration
- Add src/lib/tsan_support.c with implementation
- Add test/tsan_support/test_thread_blocks_signal.c standalone unit
  test
- Wire new source file into CMakeLists.txt LIBMCMINI_C_SRC list

Test verified: compiles with -Wall -Werror, test passes (exit code
0).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Under a TSan-instrumented target, TSan's own background thread is
spawned as a nested pthread_create() during the same window in which
DMTCP creates its checkpoint thread. Both requests reach libmcmini's
dmtcp_create_checkpoint_thread_wrapper(), which previously had no way
to tell them apart at creation time. The real checkpoint thread could
end up misclassified as an ordinary user thread and get recorded into
the target's rec_list, corrupting the recording and causing "Expected
a callback for 0" on restart.

dmtcp_is_tsan_background_thread() resolves this, but classification
requires DMTCP's endCkptThreadCreationWindow(), which can't run until
the new thread has been registered with TSan -- which only happens
once dmtcp_create_checkpoint_thread_wrapper() calls its real routine.
Blocking on classification before that call therefore deadlocks.

Fix: mark the thread as an unresolved candidate and run its real
routine immediately, then resolve classification lazily on this same
thread's first subsequent wrapped call (get_current_mode()), by which
point TSan registration is guaranteed to have already happened.
Any thread whose libmcmini prologue runs before TSAN's own
pthread_create registration trampoline has no TSAN ThreadState yet.
This hit two cases: TSAN's own lazily-spawned background thread
(nested inside TSAN's pthread_create interceptor while it's still
handling DMTCP's checkpoint-thread creation request) and ordinary
DMTCP-RECORD-mode worker threads (mc_thread_routine_wrapper's
prologue runs before TSAN's trampoline). Both crashed the first time
they made any wrapped call, since libmcmini_init()'s pthread_once()
reaches TSAN's own interceptor, which dereferences that nonexistent
ThreadState.

libmcmini_init() now checks a plain, non-intercepted flag first and
returns immediately once genuinely initialized, so pthread_once()/
TSAN's interceptor is only ever reached once, from a normal thread.
mc_pthread_create() now also eagerly forces libmcmini_init() to
complete on the creating thread (already TSAN-registered) before
spawning, in both the PRE_CHECKPOINT_THREAD and
RECORD/PRE_CHECKPOINT/DMTCP_RESTART_INTO_* cases -- pthread_create()
is itself a synchronization point, so this guarantees the fast path
applies on the new thread regardless of which of the two cases above
it turns out to be.

Confirmed live via gdb: both TSAN's background thread and DMTCP's
real checkpoint thread now start and run normally. A dedicated
reproducer follows in a later commit, once the surrounding TSan
example infrastructure exists.

Also documents (FIXME, no functional change) a separate,
pre-existing, accepted trade-off: mc_pthread_join's RECORD-mode loop
bypasses TSAN's join interceptor to avoid a harder abort, which makes
TSAN report a (nonfatal by label, but exitcode-affecting) thread-leak
warning instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The mode previously meant only "the DMTCP checkpoint thread is
calling in". ThreadSanitizer's own internal background thread calls
into libmcmini.so's overridden functions too, on a thread that is
likewise not part of the target program, so get_current_mode() now
reports the same value for it. Renamed to reflect the broader meaning.

Detection reuses thread_blocks_signal() (already used by the R1
restart-barrier fix) via two new tsan_support.c helpers:
mc_real_tid(), which reads /proc/thread-self since DMTCP virtualizes
gettid() (including via the raw syscall path); and
mc_is_current_thread_tsan_internal(), a per-thread-cached wrapper
around it.

Isolating which of thread_blocks_signal()'s calls were themselves
TSan-safe required bisecting its RECORD-mode-original fopen/fgets/
sscanf/fclose implementation one call at a time. Findings: raw
syscalls for openat/read/readlink are safe from a thread TSan has
not yet registered, but even a raw syscall(SYS_close, fd) is not, so
the one fd this code opens is deliberately never closed (bounded to
one leak per thread, since the result is cached for that thread's
lifetime).
mc_is_current_thread_tsan_internal() is only needed to distinguish
TSan's internal thread in TARGET_BRANCH(_AFTER_RESTART) and
DMTCP_RESTART_INTO_BRANCH/TEMPLATE. Its first call per thread leaks
an fd (see tsan_support.c) pointing at that thread's own
/proc/<pid>/task/<tid>/status, still open at checkpoint time, so
DMTCP must restore a path whose pid/tid cannot exist after restart.

Skip the check via a positive inclusion list of the four modes where
it actually applies (matching every wrapper's own case-label
grouping), rather than an incomplete negative exclusion list that
missed PRE_DMTCP_INIT and PRE_CHECKPOINT_THREAD. Confirmed via
/proc/<pid>/fd census: zero leaked fds for the whole RECORD phase,
and template_thread() now wakes up after restart instead of hanging
there.

This alone left a separate potential hang open: restart from a
checkpoint recorded this way still hung, later, in
template_thread()'s dmtcp_restart_sem count loop. Resolved by an
earlier commit, "Fix restart-barrier race in template_thread()",
which switched that count from a live /proc/self/task scan to
head_record_mode's THREAD entries -- a list TSan's own background
thread never reaches, since it is spawned via the same
PRE_CHECKPOINT_THREAD path already excluded from that list for the
checkpoint thread itself. Reverified live: DMTCP+TSan record/restart
of producer-consumer-park-tsan now completes cleanly, with no hang
in the consistent-state barrier.
Under DMTCP, libtsan.so resolves ahead of libmcmini.so, so a
target's own pthread_join() calls reach TSan's interceptor first.
For a thread DMTCP resurrected via clone() (bypassing TSan's
pthread_create() interceptor), TSan can never resolve its Tid and
hangs forever -- see TSAN-pthread-join.md.

Fix: link the target with -Wl,--wrap=pthread_join plus the new
pthread_join_wrap.c, rewriting the target's own pthread_join() calls
at link time, so they reach mc_pthread_join_maybe_defer() before the
dynamic linker (and TSan) ever sees them. A DMTCP-resurrected
thread's join is handled entirely via a really_exited_sem handshake;
any thread with a valid TSan Tid (classic mode, or created via
pthread_create() after restart) is still really joined via
__real_pthread_join(), so TSan keeps seeing and tracking that join
normally.
Without --wrap (see src/lib/pthread_join_wrap.c), a DMTCP-resurrected
thread's pthread_join() can hang forever under TSan. Detect both
conditions via weak symbols (same idiom as the other TSan/DMTCP
optional-symbol checks in this codebase): __tsan_acquire is non-NULL
only when libtsan.so is loaded (i.e. a TSan target), and
__wrap_pthread_join is non-NULL only if the target itself was linked
with --wrap. Warn as early as possible (a constructor) when the
former is true and the latter isn't.

Verified via `mcmini`: a TSan target built without --wrap prints the
warning, producer-consumer-tsan (built with --wrap) and a plain
non-TSan target both stay silent.
Intercepts pthread_exit(), routing it through the existing
THREAD_EXIT_TYPE model-checking machinery instead of falling
through to the real libpthread pthread_exit, which crashes on a
__clone()-recreated thread (not registered with libtsan's real
interceptor).

No example ever called pthread_exit() explicitly, so this path
was never exercised. Added producer-consumer-exit(-tsan) to test
it, which uncovered two bugs:

1. The TARGET_BRANCH case never actually terminated the thread,
   falling through to libc_abort() instead. Fixed by calling the
   real libpthread_pthread_exit() there.

2. That alone crashes DMTCP-restart of a pre-checkpoint thread
   (SIGSEGV): such a thread runs on a TSan fiber, not a real TSan
   thread, and glibc's pthread_exit() cleanup-unwind crashes
   against that fiber's state. Fixed by detecting recreated
   threads (mc_pthread_is_recreated_thread()) and terminating
   them via a fiber switch + raw exit syscall instead.

mc_pthread_is_recreated_thread(), added for bug 2 above, also
fixed a third, unrelated bug: found while stress-testing
--multithreaded-fork with a target whose worker threads finish
before main ever joins them (i.e. they exit during RECORD mode,
well before any checkpoint). mc_pthread_join_impl's
TARGET_BRANCH_AFTER_RESTART case treated every pre-restart thread
as one DMTCP resurrected via clone(), waiting on that thread's
really_exited_sem -- but really_exited_sem is only ever posted by
mc_exit_thread_in_child(), which only runs for a thread actually
executing post-restart. A thread that already, genuinely exited
before the checkpoint never runs that code again, so the wait
blocks forever.

Fixed using the same mc_pthread_is_recreated_thread() check to
distinguish the two cases: if a pre-restart thread was never
actually recreated by restart_child_threads_fast(), it's already
exited for good, and the model already knows this (see
translate_recorded_runner_to_model()) -- the join is trivially
satisfied, so return immediately instead of waiting.

Regression-checked clean against all 6 existing TSan targets.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds exit-stress.c / exit-stress-noop.c and their -tsan CMake
targets: stress tests for pthread_exit() robustness at higher
thread counts, and the reproducer used to find and confirm the
TSAN ThreadState fix from "Fix TSAN ThreadState crashes before
registration".

Confirmed live via gdb: both TSAN's background thread and DMTCP's
real checkpoint thread now start and run normally;
exit-stress-noop-tsan completes cleanly across repeated runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
mcmini_log() calls localtime() from any thread, unsynchronized.
glibc's tzset_internal() (invoked on every localtime_r() call, not
just the first) is not safe to run concurrently even when TZ never
changes, so under TSan this showed up as a data race and, at least
once, a subsequent SEGV.

A real pthread mutex around the whole function isn't enough by
itself: it's taken via libpthread_mutex_lock(), which resolves
straight to libpthread's own symbol (see
mc_load_intercepted_pthread_functions() in interception.c),
bypassing TSan's interceptor entirely. So, the lock genuinely
serializes the calls, but TSan never sees it and keeps reporting
a race. Fix that with TSan's own public annotation API
(__tsan_acquire/__tsan_release, declared weak, so this is a no-op
without a TSan runtime in the process) around the same lock.

Also switch localtime() to localtime_r() with a stack-local struct
tm, and add tzset() as a constructor, so the timezone database is
loaded once, single-threaded, before any thread can race on it.

Verified against a freshly-recorded DMTCP checkpoint (rebuilding
libmcmini.so does not affect restarts of an already-recorded
checkpoint, which pins whatever plugin build was loaded at record
time): three consecutive restarts now show zero tzset_internal
races, versus 3/3 racing beforehand.
@gc00
gc00 force-pushed the mcmini-dmtcp-tsan branch from 9500922 to c7c17c6 Compare July 31, 2026 22:21
gc00 and others added 6 commits August 2, 2026 17:44
mc_pthread_mutex_lock()/unlock() and mc_pthread_cond_wait()'s real mutex
release/reacquire all go through libpthread_mutex_lock/unlock/timedlock,
resolved via a raw dlopen("libpthread.so")+dlsym() that always bypasses
TSan's interceptor. TSan never sees these events. So it can't establish
a happens-before edge between critical sections in different threads --
reporting a false race on every variable the mutex protects (e.g.
cv-producer-consumer's count/buffer), even though mcmini's own scheduler
correctly serializes access. Same class of bug already fixed once for
mcmini_log()'s own mutex (log.c); this applies the same __tsan_acquire/
__tsan_release annotation fix to the application's mutex.

Dedicated -safe test targets exercising this fix follow in a later
commit, once the surrounding TSan example infrastructure exists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A CMake-managed, TSan-instrumented build of producer-consumer, wired
with -Wl,--wrap=pthread_join and src/lib/pthread_join_wrap.c, so it
exercises the DMTCP-restart + TSan pthread_join fix without a manual
ad hoc compile step.

do_recording() (src/mcmini/mcmini.cpp) resolves the DMTCP plugin path
as getcwd()/libmcmini.so, so mcmini must be invoked from the directory
holding libmcmini.so -- CMAKE_BINARY_DIR, not this target's own output
directory (build/src/examples/). A manually-placed copy in
CMAKE_BINARY_DIR from days ago silently went stale relative to
rebuilds of libmcmini.so and this target itself, costing significant
debugging time chasing what looked like a live TSan/DMTCP hang but was
actually just a pre-fix binary missing -Wl,--wrap=pthread_join's
effect. Add a POST_BUILD copy, so this can't happen again.

Also adds cv-producer-consumer-tsan: same build recipe, for testing
McMini's DMTCP-restart + TSan integration against condition variables
specifically (see doc/glibc-cond-var-desync.txt).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TSan's pthread_cond_wait interceptor implements the whole wait itself
and never delegates to libmcmini, unlike mutex_lock/sem_wait/cond_signal
(confirmed via GOT inspection: all resolve to libtsan, but only
cond_wait never forwards). Under DMTCP a checkpoint/restart-resurrected
thread genuinely blocked inside TSan's own real, untimed wait, with
nothing left to wake it after the earlier CV desync fix. --wrap it at
the target's link step, same approach as pthread_join_wrap.c.

Verified live: restart-from-checkpoint of a thread mid-cond_wait, which
previously hung forever, now completes promptly with the wait visible
as a real modeled transition. See
doc/cond-wait-tsan-interceptor-bypass.txt, which also records a
second, separate CV-state-reconstruction bug this verification
uncovered (tracked separately, not fixed here).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
multithreaded_fork()'s process-duplication step is _Fork() (glibc's
internal, minimal fork primitive), which deliberately skips
pthread_atfork() handlers -- the mechanism a real fork() would use to
let a lock protecting shared state reset itself safely in the child.
mcmini_log()'s own serialization mutex (log_mut) can be left frozen,
permanently locked if _Fork() snapshots memory for a new branch at the
exact instant some other thread (most plausibly the template thread,
which logs constantly) holds it -- every thread in that branch later
deadlocks calling mcmini_log(). Reproduced live via gdb + a raw memory
read: log_mut's lock word was 2 ("locked, with waiter"), a genuinely
correct glibc state nobody alive in the process could ever release.
Full analysis in doc/log-mutex-fork-desync.txt.

Fixed with mcmini_log_reset_after_fork(), which force-reinitializes
log_mut via the libpthread bypass handle (libpthread_mutex_init(), not
the plain pthread_mutex_init() -- that symbol is libmcmini's own
interposed mc_pthread_mutex_init(), which would recursively re-enter
its own restart-mode dispatch and assert on a tid_self that isn't
valid yet this early after _Fork()). Called from
fast_multithreaded_fork() (dmtcp-callback.c)'s childpid==0 branch,
right after the existing TSan fiber switch and before
restart_child_threads_fast() -- while still the only OS thread alive
in the child, so the reset can never race a concurrent lock/unlock
attempt.

(An earlier version of this fix wired the reset into
src/common/multithreaded_fork.c's multithreaded_fork() instead --
confirmed dead code, superseded by fast_multithreaded_fork() per
commit 74679cf, so that version of the fix was never actually active.
This is the corrected version, verified against the real
--multithreaded-fork path.)

Verified: 40 consecutive fresh-checkpoint producer-consumer-tsan
--multithreaded-fork cycles, zero hangs, zero crashes. The originally
observed hang rate was roughly 1-in-13 to 1-in-25.
Adds producer-consumer-safe.c / cv-producer-consumer-safe.c and
their -tsan CMake targets to formally exercise the
__tsan_acquire/__tsan_release annotation fix from "Fix false TSan
races on mutex-protected globals".

Verified: producer-consumer-safe-tsan went from 79 false-positive race
reports across classic-mode's explored interleavings to zero;
cv-producer-consumer-safe-tsan from 208 (all attributable to the
unrelated a[] race below) to zero. DMTCP+TSan record/restart cycles for
both stayed clean.

While chasing this, a live gdb backtrace caught TSan mid-report for a
genuine, separate, pre-existing race in the original examples' main()
(both pthread_create loops reuse one shared, undersized
thread-argument array, letting main() overwrite a producer's slot
while it's still being read). That race's report-generation happening
to overlap a DMTCP checkpoint signal is what caused a real deadlock
against TSan's own internal locking -- a narrow DMTCP/TSan interaction,
not a mcmini bug, and left alone in the original files. The new -safe
variants fix just that race, so other tests can isolate future bugs
from it. Full analysis in doc/tsan-mutex-annotation-false-positive.txt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
pthread_cond_signal (and broadcast) never reach
mc_pthread_cond_signal() under DMTCP, contrary to
doc/cond-wait-tsan-interceptor-bypass.txt's assumption -- confirmed
live via a call-count diagnostic. Needs the same -Wl,--wrap treatment
already applied to pthread_cond_wait; added pthread_cond_signal_wrap.c
and wired it into both cv-producer-consumer TSan targets.

Wrapping cond_signal newly exposed a second, pre-existing bug: RECORD
mode's mc_pthread_cond_signal()/_broadcast() abort if the condition
variable's record doesn't exist yet, but pthread_cond_init() has the
same interceptor-bypass problem, so a producer signaling before any
consumer has ever waited legitimately hits this. Mirrors
mc_pthread_cond_wait()'s own lazy-init instead of aborting.

Regression-checked 5 consecutive record+restart cycles each on
cv-producer-consumer(-safe)-tsan plus the existing producer-consumer
family, all clean.
@gc00
gc00 force-pushed the mcmini-dmtcp-tsan branch from c7c17c6 to 9f9182f Compare August 2, 2026 21:52

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
src/examples/producer-consumer-park.c (1)

12-23: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Replace the busy wait with an absolute monotonic sleep.

This loop consumes a CPU core for the full ten-second test window. Retry clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, ...) on EINTR so DMTCP signals do not shorten the delay.

Proposed change
+#include <errno.h>
+
-static void busy_wait_seconds(int seconds) {
-    struct timespec start, now;
-    clock_gettime(CLOCK_MONOTONIC, &start);
-    do {
-        clock_gettime(CLOCK_MONOTONIC, &now);
-    } while (now.tv_sec - start.tv_sec < seconds);
+static void wait_seconds(int seconds) {
+    struct timespec deadline;
+    int rc;
+
+    clock_gettime(CLOCK_MONOTONIC, &deadline);
+    deadline.tv_sec += seconds;
+    do {
+        rc = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &deadline, NULL);
+    } while (rc == EINTR);
 }
...
-    busy_wait_seconds(10);
+    wait_seconds(10);
🤖 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 `@src/examples/producer-consumer-park.c` around lines 12 - 23, Replace the
CPU-consuming loop in busy_wait_seconds with an absolute CLOCK_MONOTONIC
clock_nanosleep targeting the start time plus the requested seconds. Retry the
sleep when it returns EINTR so interruptions do not shorten the full delay,
while preserving the function’s existing duration behavior.
src/lib/tsan_support.c (1)

31-31: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the stale (void)signo;.

signo is used at line 89. The cast-to-void marker states the opposite and misleads a reader into thinking the parameter is ignored.

♻️ Proposed change
 int thread_blocks_signal(pid_t tid, int signo) {
-  (void)signo;
   char path[64];
🤖 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 `@src/lib/tsan_support.c` at line 31, Remove the stale (void)signo; statement
from the signal-handling function, leaving the existing signo usage unchanged.
src/examples/cv-producer-consumer.c (1)

73-82: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document that the argument-array race is intentional.

Line 80 overwrites a[i] while the producer thread can still read the same slot through pno at line 31. This is the deliberate TSan-detectable race that cv-producer-consumer-safe.c contrasts against, and that file carries an explaining comment at its lines 73-76. This file carries none. A future reader can "repair" the race and silently remove the test case.

♻️ Proposed comment
+    // INTENTIONAL DATA RACE (the -safe variant of this file removes it):
+    // one shared argument array means the second loop below overwrites a
+    // producer's slot while that producer thread may still be reading it
+    // via pno. Do not "fix" this: it is the TSan-detectable race this
+    // example exists to produce.
     int a[NUM_PRODUCERS > NUM_CONSUMERS ? NUM_PRODUCERS : NUM_CONSUMERS];
🤖 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 `@src/examples/cv-producer-consumer.c` around lines 73 - 82, Add a concise
explanatory comment adjacent to the producer/consumer thread setup, especially
the consumer-loop assignment to a[i], documenting that overwriting the argument
array while producers may still read pno is intentional and preserves this
TSan-detectable race. Match the explanatory intent of
cv-producer-consumer-safe.c without changing the implementation.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@doc/glibc-sem-desync.txt`:
- Around line 88-93: Update the condition-variable status documentation: in
doc/glibc-sem-desync.txt lines 88-93, replace the “Not yet fixed” text with a
reference to doc/glibc-cond-var-desync.txt; in doc/glibc-cond-var-desync.txt
lines 123-127, make verification reference cv-producer-consumer-safe-tsan and
its DMTCP+TSan coverage; and in lines 129-140, remove or revise the obsolete
timed-wait warning to reflect that mc_pthread_cond_wait() recomputes an absolute
deadline on each retry.

In `@include/mcmini/model/transitions/process/exit.hpp`:
- Line 25: Update the exit transition around add_state_for_runner to pass a
const model::runner_state* rather than new thread(thread::exited). Create the
appropriate runner_state object for the exited state, or call the existing state
mutator overload that accepts a thread object, while preserving the executor’s
exited state.

In `@src/lib/interception.c`:
- Line 125: Check the dlsym result for clone_ptr in src/lib/interception.c lines
125-125 immediately after resolving __clone; on NULL, print dlerror() and call
libc_abort(), matching nearby dlopen failure handling. Also check
real_start_main in src/lib/interception.c lines 392-400 before invoking it; on
NULL, print dlerror() and call _exit(1), not libc_abort(), because
initialization has not yet run.

In `@src/lib/log.c`:
- Around line 88-93: Synchronize the global_log_level access in mcmini_log by
making it atomic or by guarding the read with level_mut and applying matching
TSan annotations to the read and the write in mcmini_log_set_level. Preserve the
existing log-level filtering behavior while ensuring concurrent level updates do
not race.

---

Nitpick comments:
In `@src/examples/cv-producer-consumer.c`:
- Around line 73-82: Add a concise explanatory comment adjacent to the
producer/consumer thread setup, especially the consumer-loop assignment to a[i],
documenting that overwriting the argument array while producers may still read
pno is intentional and preserves this TSan-detectable race. Match the
explanatory intent of cv-producer-consumer-safe.c without changing the
implementation.

In `@src/examples/producer-consumer-park.c`:
- Around line 12-23: Replace the CPU-consuming loop in busy_wait_seconds with an
absolute CLOCK_MONOTONIC clock_nanosleep targeting the start time plus the
requested seconds. Retry the sleep when it returns EINTR so interruptions do not
shorten the full delay, while preserving the function’s existing duration
behavior.

In `@src/lib/tsan_support.c`:
- Line 31: Remove the stale (void)signo; statement from the signal-handling
function, leaving the existing signo usage unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 256ac28e-4bfc-445b-9bd8-c7edf8826ffe

📥 Commits

Reviewing files that changed from the base of the PR and between 9500922 and 9f9182f.

📒 Files selected for processing (56)
  • CMakeLists.txt
  • doc/classic-mode-thread-registration-segv.txt
  • doc/cond-wait-tsan-interceptor-bypass.txt
  • doc/glibc-cond-var-desync.txt
  • doc/glibc-sem-desync.txt
  • doc/log-mutex-fork-desync.txt
  • doc/pthread-exit-abort-and-fiber-crash.txt
  • doc/tsan-mutex-annotation-false-positive.txt
  • include/dmtcp.h
  • include/mcmini/lib/log.h
  • include/mcmini/mem.h
  • include/mcmini/model/objects/condition_variables.hpp
  • include/mcmini/model/objects/mutex.hpp
  • include/mcmini/model/transitions/condition_variables/condition_variable_brdcast.hpp
  • include/mcmini/model/transitions/condition_variables/condition_variable_enqueue_thread.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/mutex/mutex_init.hpp
  • include/mcmini/model/transitions/mutex/mutex_unlock.hpp
  • include/mcmini/model/transitions/process/exit.hpp
  • include/mcmini/real_world/mailbox/runner_mailbox.h
  • include/mcmini/real_world/process/dmtcp_process_source.hpp
  • include/mcmini/spy/checkpointing/objects.h
  • include/mcmini/spy/checkpointing/record.h
  • include/mcmini/spy/checkpointing/tsan_support.h
  • include/mcmini/spy/intercept/interception.h
  • include/mcmini/spy/intercept/wrappers.h
  • src/common/mem.c
  • src/common/multithreaded_fork.c
  • src/common/runner_mailbox.c
  • src/examples/CMakeLists.txt
  • src/examples/cv-producer-consumer-safe.c
  • src/examples/cv-producer-consumer.c
  • src/examples/exit-stress-noop.c
  • src/examples/exit-stress.c
  • src/examples/producer-consumer-exit.c
  • src/examples/producer-consumer-park.c
  • src/examples/producer-consumer-safe.c
  • src/lib/dmtcp-callback.c
  • src/lib/interception.c
  • src/lib/log.c
  • src/lib/main.c
  • src/lib/pthread_cond_signal_wrap.c
  • src/lib/pthread_cond_wait_wrap.c
  • src/lib/pthread_join_wrap.c
  • src/lib/record.c
  • src/lib/sem-wrappers.c
  • src/lib/tsan_support.c
  • src/lib/wrappers.c
  • src/mcmini/mcmini.cpp
  • src/mcmini/model/transitions/condition_variables.cpp
  • src/mcmini/model/transitions/mutex.cpp
  • src/mcmini/model_checking/algorithms/classic_dpor.cpp
  • src/mcmini/real_world/dmtcp_process_source.cpp
  • src/mcmini/real_world/local_linux_process.cpp
  • test/tsan_support/test_thread_blocks_signal.c
💤 Files with no reviewable changes (1)
  • src/mcmini/real_world/dmtcp_process_source.cpp
🚧 Files skipped from review as they are similar to previous changes (41)
  • include/mcmini/lib/log.h
  • test/tsan_support/test_thread_blocks_signal.c
  • src/lib/sem-wrappers.c
  • include/mcmini/mem.h
  • include/mcmini/model/transitions/condition_variables/condition_variables_wait.hpp
  • src/examples/exit-stress-noop.c
  • src/mcmini/model/transitions/mutex.cpp
  • include/mcmini/spy/checkpointing/objects.h
  • src/mcmini/model/transitions/condition_variables.cpp
  • src/mcmini/model_checking/algorithms/classic_dpor.cpp
  • src/lib/record.c
  • src/lib/pthread_cond_wait_wrap.c
  • CMakeLists.txt
  • include/mcmini/real_world/process/dmtcp_process_source.hpp
  • include/mcmini/model/transitions/condition_variables/condition_variables_signal.hpp
  • src/lib/pthread_cond_signal_wrap.c
  • include/mcmini/real_world/mailbox/runner_mailbox.h
  • doc/pthread-exit-abort-and-fiber-crash.txt
  • doc/classic-mode-thread-registration-segv.txt
  • src/lib/main.c
  • include/mcmini/model/objects/mutex.hpp
  • include/mcmini/model/transitions/condition_variables/condition_variable_brdcast.hpp
  • include/mcmini/model/transitions/condition_variables/condition_variable_enqueue_thread.hpp
  • src/common/multithreaded_fork.c
  • doc/cond-wait-tsan-interceptor-bypass.txt
  • src/mcmini/mcmini.cpp
  • src/mcmini/real_world/local_linux_process.cpp
  • include/mcmini/spy/checkpointing/tsan_support.h
  • src/common/runner_mailbox.c
  • include/mcmini/model/transitions/mutex/mutex_unlock.hpp
  • include/mcmini/spy/intercept/wrappers.h
  • src/common/mem.c
  • include/mcmini/spy/checkpointing/record.h
  • src/lib/pthread_join_wrap.c
  • include/mcmini/spy/intercept/interception.h
  • src/examples/CMakeLists.txt
  • include/dmtcp.h
  • include/mcmini/model/transitions/mutex/mutex_init.hpp
  • src/examples/exit-stress.c
  • include/mcmini/model/objects/condition_variables.hpp
  • src/lib/wrappers.c

Comment thread doc/glibc-sem-desync.txt
Comment on lines +88 to +93
Not yet fixed
---------------
Condition variables (pthread_cond_wait/pthread_cond_signal) have the same
class of vulnerability via glibc's G1/G2 waiter-group bookkeeping, for the
same reason (checkpoint/restart + externally-managed reinitialization). An
analogous fix has not yet been applied there.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the stale condition-variable status statements.

The diagnostic documents describe condition-variable work as incomplete even though this PR documents and implements the fix, the TSan-safe target, and the absolute-deadline correction.

  • doc/glibc-sem-desync.txt#L88-L93: replace the “Not yet fixed” statement with a reference to doc/glibc-cond-var-desync.txt.
  • doc/glibc-cond-var-desync.txt#L123-L127: update the verification section to reference cv-producer-consumer-safe-tsan and its DMTCP+TSan coverage.
  • doc/glibc-cond-var-desync.txt#L129-L140: remove or revise the obsolete timed-wait warning because the supplied mc_pthread_cond_wait() implementation now computes a fresh absolute deadline each retry.
📍 Affects 2 files
  • doc/glibc-sem-desync.txt#L88-L93 (this comment)
  • doc/glibc-cond-var-desync.txt#L123-L127
  • doc/glibc-cond-var-desync.txt#L129-L140
🤖 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/glibc-sem-desync.txt` around lines 88 - 93, Update the condition-variable
status documentation: in doc/glibc-sem-desync.txt lines 88-93, replace the “Not
yet fixed” text with a reference to doc/glibc-cond-var-desync.txt; in
doc/glibc-cond-var-desync.txt lines 123-127, make verification reference
cv-producer-consumer-safe-tsan and its DMTCP+TSan coverage; and in lines
129-140, remove or revise the obsolete timed-wait warning to reflect that
mc_pthread_cond_wait() recomputes an absolute deadline on each retry.

// exit code its program_exit_code() > 0 check doesn't already catch
// (i.e. exit code 0).
using namespace model::objects;
s.add_state_for_runner(executor, new thread(thread::exited));

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 | 🔴 Critical | ⚡ Quick win

Pass a model::runner_state to add_state_for_runner.

Line 25 passes model::objects::thread*, but add_state_for_runner requires const model::runner_state*. This prevents the project from compiling. Create the required runner-state object, or use the state mutator that accepts a thread object.

🧰 Tools
🪛 Clang (14.0.6)

[error] 25-25: cannot initialize a parameter of type 'const model::runner_state *' with an rvalue of type 'model::objects::thread *'

(clang-diagnostic-error)

🤖 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 `@include/mcmini/model/transitions/process/exit.hpp` at line 25, Update the
exit transition around add_state_for_runner to pass a const model::runner_state*
rather than new thread(thread::exited). Create the appropriate runner_state
object for the exited state, or call the existing state mutator overload that
accepts a thread object, while preserving the executor’s exited state.

Source: Linters/SAST tools

Comment thread src/lib/interception.c
exit_ptr = dlsym(libc_handle, "exit");
abort_ptr = dlsym(libc_handle, "abort");
fork_ptr = dlsym(libc_handle, "fork");
clone_ptr = dlsym(libc_handle, "__clone");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unchecked dlsym results in src/lib/interception.c turn a symbol-resolution failure into a NULL call. Both new lookups store the returned pointer without testing it. The failure is then deferred to the call site, where it appears as a NULL function-pointer call with no diagnostic, in contexts that are hard to debug: a freshly forked restart child, and pre-main() startup. The surrounding code already aborts loudly on dlopen failure; apply the same policy to these dlsym results.

  • src/lib/interception.c#L125-L125: test clone_ptr immediately after the dlsym(libc_handle, "__clone") call. If it is NULL, print dlerror() and call libc_abort(), matching the dlopen failure blocks above.
  • src/lib/interception.c#L392-L400: test real_start_main before calling it. If it is NULL, print dlerror() and call _exit(1). Do not use libc_abort() here, because libmcmini_init() has deliberately not run yet and abort_ptr is still NULL.
📍 Affects 1 file
  • src/lib/interception.c#L125-L125 (this comment)
  • src/lib/interception.c#L392-L400
🤖 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 `@src/lib/interception.c` at line 125, Check the dlsym result for clone_ptr in
src/lib/interception.c lines 125-125 immediately after resolving __clone; on
NULL, print dlerror() and call libc_abort(), matching nearby dlopen failure
handling. Also check real_start_main in src/lib/interception.c lines 392-400
before invoking it; on NULL, print dlerror() and call _exit(1), not
libc_abort(), because initialization has not yet run.

Comment thread src/lib/log.c
Comment on lines 88 to +93
void mcmini_log(int level, const char *file, int line, const char *fmt, ...) {
if (level < global_log_level) {
return;
}
libpthread_mutex_lock(&log_mut);
if (__tsan_acquire) __tsan_acquire(&log_mut);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Synchronize the global_log_level read.

mcmini_log_set_level() writes global_log_level under level_mut, but Line 89 reads it without that mutex or an atomic operation. A concurrent level update races with logging. Make global_log_level atomic, or protect and TSan-annotate both accesses with level_mut.

🤖 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 `@src/lib/log.c` around lines 88 - 93, Synchronize the global_log_level access
in mcmini_log by making it atomic or by guarding the read with level_mut and
applying matching TSan annotations to the read and the write in
mcmini_log_set_level. Preserve the existing log-level filtering behavior while
ensuring concurrent level updates do not race.

cv-producer-consumer-multi-tsan: 5 producers + 5 consumers on a
shared mutex+condvar, exercising 3+ same-role threads no existing
-safe target here does (they're all 1+1) -- this is what exposed
the mc_pthread_cond_wait() RECORD-mode annotation gap fixed in
"Fix false TSan races on mutex-protected globals".

script/check-tsan.sh runs it through mcmini -i and fails on any
TSan summary beyond the accepted thread leak. Wired into a new
'make check-tsan' target (top-level CMakeLists.txt), which also
picks up a local dmtcp.git/bin build for PATH/LD_LIBRARY_PATH when
present.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants