Skip to content

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

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

Joined threads now terminate, not park forever (and other TSAN-friendly fixes)#12
gc00 wants to merge 5 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.


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 per-thread join/exit synchronization to ensure threads terminate cleanly in all cases.
    • Thread joins now wait until the target thread has fully exited before returning results.
    • Improved termination reporting and robustness when backtracking encounters abnormal termination scenarios.

gc00 added 2 commits July 26, 2026 11:40
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.
@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

📝 Walkthrough

Walkthrough

Adds per-thread exit permission semaphores, makes child-thread termination wait for permission, and changes target-branch joins to perform real pthread joins. Abnormal termination reporting and DPOR backtracking now handle replay edge cases.

Changes

Thread exit and join synchronization

Layer / File(s) Summary
Register per-thread exit permissions
src/lib/wrappers.c
pthread_map_t stores and initializes an exit semaphore, with lookup support for registered threads.
Gate termination and perform real joins
src/lib/wrappers.c
mc_exit_thread_in_child() waits for exit permission, while mc_pthread_join() grants permission and calls the underlying pthread join.

Abnormal termination handling

Layer / File(s) Summary
Handle missing culprit transitions
src/mcmini/mcmini.cpp
Termination reporting distinguishes between culprits with pending transitions and culprits that are no longer pending.
Report replay termination errors
src/mcmini/model_checking/algorithms/classic_dpor.cpp
DPOR backtracking catches replay termination errors, invokes the abnormal-termination callback when available, and exits verification.

Estimated code review effort: 3 (Moderate) | ~20 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 exit permission
  mc_exit_thread_in_child->>exit_permission_sem: wait for permission
  mc_exit_thread_in_child-->>libpthread_pthread_join: thread can terminate
  mc_pthread_join->>libpthread_pthread_join: perform real join
Loading

Suggested reviewers: maxwellpirtle

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main change: joined threads now terminate instead of waiting indefinitely, with a brief note about additional fixes.
✨ 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
Comment on lines +366 to +368
sem_t *exit_permission = find_exit_permission_sem(pthread_self());
assert(exit_permission != NULL);
libpthread_sem_wait(exit_permission);

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
ast-grep outline src/lib/wrappers.c --items all
rg -n -C4 '\blibpthread_sem_wait\s*\(|\bsem_wait\s*\(' src include

Repository: mcminickpt/deep-debug

Length of output: 19086


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '350,375p' src/lib/wrappers.c
sed -n '170,195p' src/lib/sem-wrappers.c
sed -n '282,303p' src/lib/interception.c
sed -n '45,60p' include/mcmini/spy/checkpointing/record.h
sgrep --version 2>/dev/null || true

Repository: mcminickpt/deep-debug

Length of output: 3290


Retry interrupted exit-permission semaphore waits.

libpthread_sem_wait() can return -1/EINTR before acquiring join permission; here that makes the exiting child skip the assert() and continue past the guarded termination boundary. Let libpthread_sem_wait()/mc_sem_wait() retry EINTR internally, or wrap this site with the existing retry helper before proceeding after 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/lib/wrappers.c` around lines 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.

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.

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 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 and others added 2 commits July 27, 2026 01:25
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.
@gc00
gc00 force-pushed the mcmini-thread-exit-fix branch from 2b0d27d to d222a73 Compare July 27, 2026 05:26
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