Skip to content

Joined threads now terminate, not park forever (and other TSAN-friendly fixes; 5 commits) - #12

Open
gc00 wants to merge 6 commits into
mcminickpt:mainfrom
gc00:mcmini-thread-exit-fix
Open

Joined threads now terminate, not park forever (and other TSAN-friendly fixes; 5 commits)#12
gc00 wants to merge 6 commits into
mcminickpt:mainfrom
gc00:mcmini-thread-exit-fix

Conversation

@gc00

@gc00 gc00 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

@maxwellpirtle, Please review this very small PR. The first commit is for what we originally talked about when you decided to let a joined thread park forever instead of exiting. The second commit is trivial. The next three are fixing original bugs in McMini (or updating to DMTCP's newer v4 plugin API). Thanks for reviewing this. It simplifies my work, since I always have to rebase on this PR before creating new commits.

And now there are 2 more McMini bugs that I'll be adding here (currently in the mcmini-dmtcp-tsan branch). And Claude seems to have even discovered a known bug/misfeature of semaphores and condition variables in the GLIBC implementation. So, there will be additional commits to fix the GLIBC bugs! Weird.


Before pushing in these commits, we should push in PR #3, PR #4, PR #5, PR #7. And this PR #12 (which already includes PR #6). After that, we can push in the HUGE PR #11 (which will contain this PR #12 as the first few commits).
For PR #3 and PR #4, @maxwellpirtle is still updating those PR's with a better commit.


6 commits in total:

Zeroth commit: Bypass libtsan in mc_pthread_join's timed join

First commit: 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.

Second commit: Also fix a pthread_map_lock leak in search_pthread_map() found along
the way: on a match, the loop returned directly, skipping the unlock
below it, so a later insert_pthread_map() write-lock would block
forever. Break out of the loop and unlock on every path instead.

Third commit: DPOR backtrack replay: Report abnormal termination
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.

Fourth commit: Port libmcmini DMTCP plugin from plugin API v3 to v4
We will target DMTCP-5.0 (soon to be released). From now on, we use the
new plugin API.

Fifth commit: Fix TOCTOU race in template_thread()'s restart barrier
Fixes a bug in McMini, concerning a thread moving past a barrier (see commit msg)

Summary by CodeRabbit

  • Bug Fixes

    • Improved thread joining and exit handling to prevent indefinite waits.
    • Fixed abnormal-termination reporting when thread state is no longer available.
    • Improved recovery when replay or backtracking encounters a terminated process.
    • Made checkpoint restart thread tracking more reliable and resistant to race conditions.
  • Compatibility

    • Updated the checkpointing interface and plugin compatibility version.
    • Expanded checkpoint and process metadata support.
    • Updated process and thread identifier handling for more consistent behavior.

@gc00
gc00 requested review from Copilot and maxwellpirtle July 26, 2026 15:51
@gc00 gc00 added the enhancement New feature or request label Jul 26, 2026
@coderabbitai

coderabbitai Bot commented Jul 26, 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

The changes update DMTCP interfaces, PID translation, checkpoint thread accounting, pthread exit coordination, timed joins, and replay termination handling. They also handle missing model transitions during abnormal termination reporting.

Changes

Runtime synchronization and replay handling

Layer / File(s) Summary
DMTCP interfaces and PID translation
include/dmtcp.h, src/common/multithreaded_fork.c
DMTCP adds checkpoint and thread metadata types, updates the plugin ABI and PID APIs, and adds TSAN background-thread support. Thread discovery uses the renamed PID translation function.
Checkpoint thread accounting and child setup
src/lib/dmtcp-callback.c
Checkpoint waits count recorded alive threads. Child-context setup relies on ucontext_t for signal-mask restoration, and the inactive clone path is removed.
Per-thread exit permissions
src/lib/wrappers.c
The pthread registry stores zero-initialized exit-permission semaphores and exposes semaphore lookup.
Exit and join coordination
include/mcmini/spy/intercept/interception.h, src/lib/interception.c, src/lib/wrappers.c
Child-thread exit waits for permission. Target-branch joins post permission and call the real pthread join. Recording-mode joins call the resolved libpthread timed-join implementation.
Replay state and termination handling
src/mcmini/mcmini.cpp, src/mcmini/model_checking/algorithms/classic_dpor.cpp
Abnormal termination reporting handles missing pending transitions. DPOR backtracking reports process termination through the configured callback.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant mc_pthread_join
  participant exit_permission_sem
  participant mc_exit_thread_in_child
  participant libpthread_pthread_join
  mc_pthread_join->>exit_permission_sem: post target exit permission
  mc_exit_thread_in_child->>exit_permission_sem: wait for exit permission
  mc_pthread_join->>libpthread_pthread_join: perform real join
Loading

Possibly related PRs

  • mcminickpt/deep-debug#13: Shares changes across the DMTCP interface, pthread wrappers, timed joins, checkpoint callbacks, and replay handling.

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 6.25% 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 primary change: joined threads now terminate instead of remaining parked indefinitely.
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.

@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/wrappers.c`:
- Around line 366-368: Update the exit-permission wait in the code surrounding
find_exit_permission_sem so an EINTR result from libpthread_sem_wait cannot
allow execution to continue without acquiring the semaphore. Prefer making
libpthread_sem_wait or mc_sem_wait retry EINTR internally; otherwise use the
existing retry helper at this call site before proceeding past the guarded
termination boundary.
🪄 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: 90adac79-b815-4ec7-90ea-c3742a2876a8

📥 Commits

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

📒 Files selected for processing (1)
  • src/lib/wrappers.c

Comment thread src/lib/wrappers.c

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 updates the thread-join/exit interaction in the McMini pthread wrappers so that a thread that is joined in the TARGET_BRANCH mode can actually terminate at the OS level (allowing real pthread_join()—including TSAN’s interceptor—to complete), instead of remaining parked indefinitely.

Changes:

  • Add a per-thread semaphore (exit_permission_sem) in the pthread→runner-id map to coordinate when an exiting thread is allowed to truly terminate.
  • Update mc_exit_thread_in_child() to wait on that semaphore (instead of parking forever), and update mc_pthread_join() in TARGET_BRANCH to post the semaphore and then perform a real pthread_join() (populating *rv).
  • Fix a pthread_map_lock read-lock leak in search_pthread_map() by ensuring the unlock occurs on all paths.

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

@gc00 gc00 changed the title Joined threads now terminate, not park forever Joined threads now terminate, not park forever (and other TSAN-friendly fixes) Jul 27, 2026
@gc00
gc00 force-pushed the mcmini-thread-exit-fix branch from 2b0d27d to d222a73 Compare July 27, 2026 05:26

@maxwellpirtle maxwellpirtle left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this should be OK. We should just check eventually against cases where the pthread_t is reused by libpthread in Phase I mode, although we might be storing based on the unique thread ID but we'd have to double check

Comment thread src/lib/dmtcp-callback.c
// pthread_create(), so neither is ever recorded here), and -- since a
// DMTCP checkpoint is a full memory snapshot -- this list is preserved
// exactly as it was at record time across every restart, with no
// dependence on restart-time scheduling.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good catch

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-thread-exit-fix branch from d222a73 to e98dd6c Compare July 30, 2026 22:15
@gc00
gc00 force-pushed the mcmini-thread-exit-fix branch from e98dd6c to d222a73 Compare July 31, 2026 04:29
@gc00 gc00 changed the title Joined threads now terminate, not park forever (and other TSAN-friendly fixes) Joined threads now terminate, not park forever (and other TSAN-friendly fixes; 5 commits) Jul 31, 2026
@gc00
gc00 force-pushed the mcmini-thread-exit-fix branch from d222a73 to 0e0e810 Compare July 31, 2026 05:58
Comment thread src/lib/wrappers.c
// genuinely populated -- before we return, instead of only
// simulating success at the model level.
sem_t *exit_permission = find_exit_permission_sem(t);
assert(exit_permission != NULL);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think the semaphores are correct but unnecessary. Why do we block the threads until we need to join on them?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@maxwellpirtle , I can add a more informative comment if you like. Tell me what you'd like to emphasize. As for the reason that it's necessary, Claude says:

  • The semaphores aren't just bookkeeping — they're what prevents pthread_t reuse from aliasing two different threads in pthread_map. mc_exit_thread_in_child() reports the model-visible EXIT transition before waiting on exit_permission_sem; what the wait actually delays is the real OS exit, until the corresponding join happens. glibc can recycle a joinable thread's pthread_t/TCB the moment it's reaped, and our lookups (find_pthread_map_entry et al.) identify threads only by pthread_equal() on that value — so if we let the OS thread really die before the model processes its join, a brand-new thread could get assigned the same pthread_t and get misidentified as the "exited" one. Blocking until join keeps the model's "not yet joined" belief and the OS's "not yet reaped" reality in sync, same as it stays in sync for any real, un-joined joinable thread. This is the same aliasing risk you flagged in your other comment about pthread_t reuse in Phase I mode.

gc00 and others added 2 commits July 31, 2026 18:14
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>
template_thread() computed the number of threads to wait for by
scanning /proc/self/task live, at restart time, then subtracting a
blanket 2 (for itself and the checkpoint thread). DMTCP recreates
checkpointed threads asynchronously via clone(), so a scan that runs
before it has finished recreating all of them undercounts -- this
barrier then declares "consistent state" and lets the template
thread proceed before every thread has actually restarted. Confirmed
via added diagnostic logging: one thread's own restart-completion
signal could arrive after the barrier already released.

Fix: count ALIVE THREAD entries in head_record_mode instead. That
list only ever gets entries for genuine target threads (the template
thread and checkpoint thread never go through libmcmini's wrapped
pthread_create(), so neither is ever recorded there), and since a
DMTCP checkpoint is a full memory snapshot, it's preserved exactly
as-is across every restart -- immune to any restart-time scheduling
race.

Also remove fast_multithreaded_fork()'s #if 1/#else wrapper: the
#else side held an early, never-compiled clone()-based fork
prototype that predates this file's current _Fork()-based approach
and was always dead code.

Also remove the needless signal-mask check in threaded fork:
getcontext()/setcontext() already restore a thread's blocked-signal
set via uc_sigmask, even across the raw clone() used to recreate a
checkpointed thread, so the thread_sigmask field and abort were
unnecessary. That check ran on every restarted thread regardless of
mode, so it also blocked plain (non-multithreaded-fork) restart of
any target with a blocked-signal thread.
@gc00
gc00 force-pushed the mcmini-thread-exit-fix branch from 0e0e810 to 9bd964d Compare July 31, 2026 22:17
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.

3 participants