Skip to content

WIP: TSAN checkpoint restart#9

Open
gc00 wants to merge 29 commits into
mainfrom
tsan-checkpoint-restart
Open

WIP: TSAN checkpoint restart#9
gc00 wants to merge 29 commits into
mainfrom
tsan-checkpoint-restart

Conversation

@gc00

@gc00 gc00 commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Still working on it with Claude, but mostly ready to start being reviewed. This includes the commits of PR #8.

Summary by CodeRabbit

  • New Features
    • Enhanced ThreadSanitizer multithreaded fork/restart support with sanitizer-aware thread handling and safer pthread exit/join behavior for recreated threads.
  • Bug Fixes
    • Reduced ThreadSanitizer-enabled hangs and crashes by improving fork barrier behavior and thread counting.
  • Documentation
    • Added phased TSAN port design/implementation and hardening plans for the multithreaded fork workflow.
  • Tests
    • Added standalone coverage for signal-block detection, thread-id extraction, fork fast-path behavior, pthread exit forwarding, and repeated-fork stress.
  • Compatibility / API Updates
    • Updated DMTCP plugin API version and checkpoint image header format to v4.0.

gc00 and others added 23 commits June 30, 2026 11:56
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).

Verified: libmcmini.so builds against v4 and DMTCP now loads it without the
version assert (mcmini record mode reaches RECORD-phase execution under
dmtcp_launch).

Known follow-up (out of scope here): checkpointing a -fsanitize=thread
target under deep-debug still crashes in RECORD mode with the TSan null-
ThreadState SEGV, because libmcmini creates threads via
libdmtcp_pthread_create (dlopen'd directly), bypassing libtsan's
pthread_create interceptor -- the same class of issue the classic-mode
RTLD_NEXT fix addresses, but on the DMTCP record path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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 -> libmcmini's 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 (DMTCP tsan-phased-init,
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. A separate TSan-interception site remains
downstream -- mc_pthread_join calls pthread_timedjoin_np directly (no bypass
handle), tripping libtsan's ConsumeThreadUserId CHECK -- to be addressed next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mc_pthread_join's RECORD loop called pthread_timedjoin_np directly. Under a
TSAN target, that symbol resolves to libtsan's interceptor, whose
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.

With this, the three-fix stack achieves END-TO-END checkpointing of a
-fsanitize=thread target under deep-debug (mcmini record mode):
  1. 5be8500  DMTCP plugin API v3 -> v4      (DMTCP loads libmcmini.so)
  2. 4bf2720  TSan-safe RECORD prologue      (worker creation doesn't crash
             before libtsan registers the thread)
  3. this     timed-join via bypass handle   (join loop doesn't trip the
             registry CHECK)

Verified: `mcmini -i 3 ~/dmtcp.git/test/tsan_target` (DMTCP tsan-phased-init)
now runs with no SEGV / no ThreadSanitizer errors and DMTCP produces a valid
checkpoint (ckpt_tsan_target_*.dmtcp, ~5 MB, matching the no-mcmini baseline).

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).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the throwaway SIGSEGV-backtrace handler and its install call
sites added during TSan-restart investigation, and restore the
DMTCP_EVENT_RESTART SIG_DFL reset for SIGSEGV. Keep the R2 (TSan fork
syscall hooks around _Fork()) and R4 (fresh TSan fiber for recreated
threads) prototypes as the starting point for the multithreaded_fork
TSan port described in PLAN.txt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…eads from restart barrier

Documents the per-tid classification design for template_thread()'s
thread-count barrier: skip self, the checkpoint thread (via a new
read-only tid lookup), and TSan-internal threads (via a SigBlk probe
in a new src/lib/tsan_support.c). Ready for the writing-plans step.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bite-sized task plan for docs/superpowers/specs/2026-07-04-tsan-port-phase1-r1-design.md:
Task 1 adds thread_blocks_signal() in a new src/lib/tsan_support.c;
Task 2 adds a read-only get_tid_from_pthread_descriptor() and rewires
template_thread()'s restart barrier to per-tid classification; Task 3
is the environment-gated end-to-end DMTCP+TSan verification. All code
snippets were test-compiled in a scratch copy before committing.

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 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>
template_thread() previously counted expected restart-barrier posts by
taking /proc/self/task's thread count and subtracting a blanket 2 (for
itself and the checkpoint thread). Under TSan, libtsan spawns its own
background thread that blocks all signals at creation and never calls
into libmcmini's wrappers, so it never posts to dmtcp_restart_sem,
causing template_thread() to hang waiting for a post that never comes.

Replace the blanket subtraction with per-tid classification: skip the
template thread's own tid and the checkpoint thread's tid (read via
the new read-only get_tid_from_pthread_descriptor() sibling of
patchThreadDescriptor()), and skip any tid that blocks
SIG_MULTITHREADED_FORK (thread_blocks_signal(), from Task 1) as a
TSan-internal thread. A self-check re-verifies the pthread_t->tid
offset against pthread_self() before trusting it for the checkpoint
thread's descriptor, aborting via libc_abort() on mismatch.

Adds a standalone host-side test
(test/tsan_support/test_tid_from_descriptor_offset.c) proving the
read-only offset technique works for reading another thread's
descriptor, not just pthread_self().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…pt tid

The per-tid /proc/self/task walk skips self_tid (always reliable, from
SYS_gettid) and ckpt_tid (read from ckpt_pthread_descriptor via a raw
offset read, which could be stale or point at a since-changed offset).
If ckpt_tid doesn't match any live tid, the checkpoint thread's real
entry silently falls into the countable branch, corrupting
thread_count and hanging the subsequent barrier loop forever with no
diagnostic. Track how many times each tid was actually seen and abort
with a clear message if either isn't exactly 1, turning a silent hang
back into the exact stabilization bug this code was written to fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Documents that R2 and half of R4 are already committed from Phase 0's
keeper prototypes, scoping this phase down to R3 (__clone) and R4's
remaining half (forking-thread fiber switch). Also documents that
ThreadSanitizer itself works in this sandbox under `setarch -R`,
enabling a standalone test harness that mirrors dmtcp-callback.c's
actual getcontext-direct-call mechanism (not the vendor package's
signal-based one) without needing DMTCP.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bite-sized plan for docs/superpowers/specs/2026-07-04-tsan-port-phase2-r2-r3-r4-design.md:
Task 1 writes a standalone harness mirroring dmtcp-callback.c's actual
getcontext-direct-call mechanism and captures real RED (TSan CHECK
failed in ForkChildAfter) and GREEN evidence via a compile-time
MTF_BUGGY toggle; Task 2 applies the identical fix (__clone extern +
swap, forking-thread fiber switch) to production; Task 3 is the
environment-gated end-to-end DMTCP+TSan verification. Both the harness
and the production diff were test-compiled/run in a scratch copy
before committing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ber) under TSan

Adds test/tsan_support/test_fastpath_fork_clone_fiber.c, a self-contained
host-side test (compiled/run directly with gcc, not via CMake) that mirrors
dmtcp-callback.c's fast-path fork+clone+fiber resumption mechanism to prove
under real ThreadSanitizer that:

- R3 (using libc's __clone instead of the public, libtsan-intercepted
  clone()) and R4's forking-thread fiber switch are both required: with
  -DMTF_BUGGY (public clone(), no forker fiber switch) the recreated
  threads crash with "ThreadSanitizer: CHECK failed: tsan_rtl.cpp:253
  ((!thr->slot)) != (0)" inside ForkChildAfter, and the child process exits
  with code 66.
- The default (fixed) configuration passes cleanly: both parent and child
  report shared_counter=300000 (expected 300000), the child exits 0, and no
  CHECK failures occur across repeated runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ded_fork

restart_child_threads_fast() now calls libc's raw __clone() instead of the
public clone(), since libtsan's clone() interceptor treats every call as a
fork and corrupts its thread-slot state for the CLONE_THREAD clone used to
recreate pre-checkpoint threads. Also switch the forking thread itself onto
a fresh TSan fiber in fast_multithreaded_fork()'s child branch, since it
otherwise keeps its inherited (fork-copied) ThreadState whose shadow call
stack can overflow as the thread keeps running. Both fixes were already
proven against a standalone TSan repro harness in a prior commit; this
applies them to the production DMTCP restart path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rness comment

Final review found the harness's forking thread (main()) never does enough
post-fork instrumented work to trigger the shadow-call-stack overflow that
the forking-thread fiber switch (R4 remainder) guards against, so the
RED/GREEN evidence only actually falsifies/confirms R3 (__clone), not R4.
Rewrite the header comment to state this accurately; no code logic changes.
…witch

The fiber switch is guarded by #ifndef MTF_BUGGY, so it does not execute
in the buggy build -- only the default (fixed) one. Doesn't change any
of the comment's substantive conclusions (still not independently
validated by this harness), per final re-review.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Static code-path analysis of mc_pthread_join resolves PLAN.txt's own
open question (Q1): the join CHECK-fail cannot reproduce here, since
no code path reachable by a recreated thread ever calls a real
libc/libpthread join (TARGET_BRANCH* is a pure mailbox handshake with
the mcmini scheduler). No join-side change needed.

pthread_exit is a real gap (not intercepted at all today) with a
second, previously-undocumented consequence found during this
investigation: McMini's existing THREAD_EXIT_TYPE machinery is only
triggered by a thread routine returning normally, never by an explicit
pthread_exit() call, for any thread. Designs a single mc_pthread_exit()
interceptor (mirroring mc_transparent_exit/abort's mode-dispatch) that
fixes both the TSan-safety gap and the model-checking gap together,
without needing the standalone package's retval-stash/fiber-switch
machinery -- McMini's own thread-exit never terminates the OS thread
at the kernel level, so that machinery would have nothing to protect.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bite-sized plan for docs/superpowers/specs/2026-07-05-tsan-port-phase3-r5-design.md:
Task 1 proves the dlopen+dlsym forwarding technique standalone; Task 2
adds mc_pthread_exit() and wires it into interception.c/wrappers.c;
Task 3 is the environment-gated end-to-end DMTCP+TSan verification
with an explicit-pthread_exit() target. The full production diff was
test-applied and built clean in a scratch copy, and the standalone
forwarding test was run (PASS), before committing this plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…technique

This test demonstrates that pthread_exit can be safely resolved via dlopen+dlsym
and called from a worker thread, with the return value correctly delivered to
pthread_join(). This technique is used in the next task for production code.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Intercepts pthread_exit() and routes it through the existing
THREAD_EXIT_TYPE model-checking machinery (mc_exit_thread_in_child() /
mc_exit_main_thread_in_child()) uniformly across all restart/branch
modes, instead of falling through to the real libpthread pthread_exit
and, on a __clone()-recreated thread, tripping libtsan's real
interceptor (which is not registered for that "fiber" thread and can
crash). Pre-restart modes forward to the real pthread_exit via a new
libtsan-bypassing libpthread_pthread_exit() handle, resolved with the
same dlopen+dlsym technique already used for pthread_timedjoin_np and
validated standalone in the prior commit's harness.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ion.h

Matches the header's existing convention (libc_exit/libc_abort use the
macro two lines below); interception.c's own pointer declarations
already use the raw attribute for a different, pre-existing reason
(exit_ptr/abort_ptr), so that file is untouched. Per Phase 3 Task 2
review's one Minor finding.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…as live

DMTCP_RESTART_INTO_BRANCH/TEMPLATE were grouped with TARGET_BRANCH* in a
single case, skipping the mandatory thread_handle_after_dmtcp_restart()
call that every other wrapper performs for those two restart modes.
Split them into their own case that reports THREAD_EXIT_TYPE, calls
thread_handle_after_dmtcp_restart(), then falls through into the
unchanged TARGET_BRANCH*/TARGET_BRANCH_AFTER_RESTART body, matching the
mc_transparent_exit/mc_transparent_abort fallthrough precedent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@gc00 gc00 requested a review from aayushi363 July 5, 2026 05:31
@gc00 gc00 added the enhancement New feature or request label Jul 5, 2026
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds TSan-safe support for multithreaded fork/restart, including a TSAN-safe allocator, signal-based thread classification, fork/restart fiber handling, pthread_exit interception, DMTCP header and pid API updates, and matching plans, specs, and tests.

Changes

TSan Port Implementation

Layer / File(s) Summary
Allocator and signal classifier
src/common/mem.c, include/mcmini/mem.h, include/mcmini/spy/checkpointing/record.h, src/lib/record.c, include/mcmini/spy/checkpointing/tsan_support.h, src/lib/tsan_support.c, test/tsan_support/test_thread_blocks_signal.c, CMakeLists.txt
Adds mc_ts_alloc, a TSAN-safe record-list append helper, and thread_blocks_signal() with standalone coverage.
Restart barrier classification
include/dmtcp.h, src/common/multithreaded_fork.c, src/lib/dmtcp-callback.c, include/mcmini/spy/intercept/wrappers.h, test/tsan_support/test_tid_from_descriptor_offset.c
Updates checkpoint/pid declarations, adds the read-only pthread-descriptor TID helper, classifies restart-barrier threads per TID, and switches internal template-thread creation to public pthread_create.
Fork and fiber restart path
src/lib/dmtcp-callback.c, test/tsan_support/test_fastpath_fork_clone_fiber.c, test/tsan_support/test_repeated_fork_stress.c
Adds TSAN fork/fiber hooks, uses __clone in restart thread recreation, and switches forked/resurrected threads onto fresh TSAN fibers, with standalone harnesses for the fast path and repeated-fork stress.
pthread_exit interception
src/lib/interception.c, src/lib/wrappers.c, include/mcmini/spy/intercept/interception.h, include/mcmini/spy/intercept/wrappers.h, test/tsan_support/test_pthread_exit_forwarding.c
Adds mc_pthread_exit, the real pthread_exit forwarding path, TSan-safe timed-join bypassing, and the pthread-map synchronization changes.
Plans and specs
PLAN.txt, docs/superpowers/plans/*, docs/superpowers/specs/*
Adds Phase 1–4 design and implementation documents for the TSAN multithreaded-fork port.
Model/header cleanups
include/mcmini/model/pending_transitions.hpp, include/mcmini/model/state.hpp, include/mcmini/model_checking/algorithms/classic_dpor/stack_item.hpp
Adds pending_transitions::empty(), un-hides base overloads in mutable_state, and reorders an include list.

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

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is related to the TSAN checkpoint/restart work, but it is too vague to convey the main change. Use a more specific title such as "Add TSAN-safe checkpoint/restart support" or mention the key fix, e.g. "TSAN checkpoint restart: add libc_clone and pthread_exit handling".
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tsan-checkpoint-restart

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

@gc00 gc00 force-pushed the tsan-checkpoint-restart branch from cfba39d to 6c4aaa8 Compare July 5, 2026 05:34

@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: 2

🧹 Nitpick comments (2)
src/lib/tsan_support.c (1)

5-28: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Validate signo range before shifting.

sigblk >> (signo - 1) (Line 27) is undefined behavior if signo <= 0 or signo > 64 (shift by negative or by ≥ width of unsigned long long). The header doesn't document a valid range, and this is a general-purpose public API, so a future caller passing an out-of-range value would hit UB. Current call site passes a fixed macro, so this isn't triggered today, but a guard is cheap insurance.

🔧 Proposed fix
 int thread_blocks_signal(pid_t tid, int signo) {
+  if (signo < 1 || signo > 64) {
+    return 0;
+  }
   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` around lines 5 - 28, Validate the signo argument in
thread_blocks_signal before using it in the bit shift; the current return
expression can invoke undefined behavior for signo values outside 1..64. Add a
range check near the start of thread_blocks_signal, and return 0 for invalid
inputs before reading or shifting sigblk so the helper remains safe for all
callers.
src/common/mem.c (1)

9-11: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Arena isn't guaranteed to be 16-byte aligned.

mc_ts_alloc rounds n to 16-byte boundaries (Line 14) implying a malloc-like alignment guarantee, but mc_ts_arena is a plain char[] with no explicit alignment. The C standard only guarantees 1-byte alignment for char arrays; nothing forces the base address onto a 16-byte boundary, so offsets computed relative to it aren't guaranteed absolute-aligned. This could produce misaligned pointers for stricter-alignment consumers on some platforms/compilers.

🔧 Proposed fix
-static char mc_ts_arena[MC_TS_ARENA_SIZE];
+static _Alignas(16) char mc_ts_arena[MC_TS_ARENA_SIZE];
🤖 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 9 - 11, The arena backing `mc_ts_alloc` is not
guaranteed to start on a 16-byte boundary, so the alignment promise made by
rounding allocations up in `mc_ts_alloc` is incomplete. Update the `mc_ts_arena`
storage in `mem.c` so the arena itself has explicit 16-byte alignment, and keep
the existing size/offset logic in `mc_ts_alloc` consistent with that guarantee.
Reference the `MC_TS_ARENA_SIZE`, `mc_ts_arena`, and `mc_ts_alloc` symbols when
making the change.
🤖 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
`@docs/superpowers/plans/2026-07-04-tsan-port-phase2-r2-r3-r4-implementation.md`:
- Around line 104-107: The harness snippet in setTLSPointer is calling
arch_prctl with the wrong syscall shape and an inverted success check, so update
it to use the direct ARCH_SET_FS and ARCH_SET_GS usage on ti->fs and ti->gs and
make the assertions validate success with == 0. Keep the fix localized to
setTLSPointer so the sample reaches the TSan path as intended.

In `@src/lib/wrappers.c`:
- Around line 468-496: The RECORD/PRE_CHECKPOINT path in mc_pthread_exit
currently forwards directly to libpthread_pthread_exit without updating the
thread’s rec_list status, leaving an exited thread marked ALIVE. Update
mc_pthread_exit so that, before calling libpthread_pthread_exit in the RECORD
and PRE_CHECKPOINT cases, it follows the same rec_list_lock / thread-record
lookup pattern used by mc_thread_routine_wrapper and sets thrd_state.status to
EXITED for the current thread. Keep the restart and TARGET_BRANCH paths
unchanged.

---

Nitpick comments:
In `@src/common/mem.c`:
- Around line 9-11: The arena backing `mc_ts_alloc` is not guaranteed to start
on a 16-byte boundary, so the alignment promise made by rounding allocations up
in `mc_ts_alloc` is incomplete. Update the `mc_ts_arena` storage in `mem.c` so
the arena itself has explicit 16-byte alignment, and keep the existing
size/offset logic in `mc_ts_alloc` consistent with that guarantee. Reference the
`MC_TS_ARENA_SIZE`, `mc_ts_arena`, and `mc_ts_alloc` symbols when making the
change.

In `@src/lib/tsan_support.c`:
- Around line 5-28: Validate the signo argument in thread_blocks_signal before
using it in the bit shift; the current return expression can invoke undefined
behavior for signo values outside 1..64. Add a range check near the start of
thread_blocks_signal, and return 0 for invalid inputs before reading or shifting
sigblk so the helper remains safe for all callers.
🪄 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: b898a1c1-f28e-4c7e-9165-d3136b2322cd

📥 Commits

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

📒 Files selected for processing (27)
  • CMakeLists.txt
  • docs/superpowers/plans/2026-07-04-tsan-port-phase1-r1-implementation.md
  • docs/superpowers/plans/2026-07-04-tsan-port-phase2-r2-r3-r4-implementation.md
  • docs/superpowers/plans/2026-07-05-tsan-port-phase3-r5-implementation.md
  • docs/superpowers/specs/2026-07-04-tsan-port-phase1-r1-design.md
  • docs/superpowers/specs/2026-07-04-tsan-port-phase2-r2-r3-r4-design.md
  • docs/superpowers/specs/2026-07-05-tsan-port-phase3-r5-design.md
  • include/dmtcp.h
  • include/mcmini/mem.h
  • include/mcmini/model/pending_transitions.hpp
  • include/mcmini/model/state.hpp
  • include/mcmini/model_checking/algorithms/classic_dpor/stack_item.hpp
  • 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/lib/dmtcp-callback.c
  • src/lib/interception.c
  • src/lib/record.c
  • src/lib/tsan_support.c
  • src/lib/wrappers.c
  • test/tsan_support/test_fastpath_fork_clone_fiber.c
  • test/tsan_support/test_pthread_exit_forwarding.c
  • test/tsan_support/test_thread_blocks_signal.c
  • test/tsan_support/test_tid_from_descriptor_offset.c

Comment on lines +104 to +107
static void setTLSPointer(struct threadinfo *ti) {
assert(syscall(SYS_arch_prctl, 2, ARCH_SET_FS, ti->fs) != 0);
assert(syscall(SYS_arch_prctl, 2, ARCH_SET_GS, ti->gs) != 0);
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the reviewed file around the referenced lines.
file='docs/superpowers/plans/2026-07-04-tsan-port-phase2-r2-r3-r4-implementation.md'
wc -l "$file"
sed -n '90,120p' "$file"

# Find any arch_prctl usage in the repo for comparison.
rg -n "arch_prctl|ARCH_SET_FS|ARCH_SET_GS|SYS_arch_prctl" -S .

Repository: mcminickpt/deep-debug

Length of output: 268


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Determine syscall convention from system headers/manpage if available in sandbox.
python3 - <<'PY'
import os, subprocess, textwrap, sys
cands = [
    "/usr/include/x86_64-linux-gnu/sys/syscall.h",
    "/usr/include/sys/syscall.h",
    "/usr/include/x86_64-linux-gnu/asm/prctl.h",
    "/usr/include/asm/prctl.h",
]
for p in cands:
    if os.path.exists(p):
        print(f"--- {p} ---")
        with open(p, 'r', encoding='utf-8', errors='ignore') as f:
            for i, line in enumerate(f, 1):
                if "ARCH_SET_FS" in line or "ARCH_SET_GS" in line or "SYS_arch_prctl" in line or "arch_prctl" in line:
                    print(f"{i}:{line.rstrip()}")
PY

Repository: mcminickpt/deep-debug

Length of output: 323


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Programmatically check the expected syscall arity/signature from headers/examples if present.
python3 - <<'PY'
import os, re

paths = [
    "/usr/include/x86_64-linux-gnu/asm/prctl.h",
    "/usr/include/asm/prctl.h",
    "/usr/include/x86_64-linux-gnu/sys/syscall.h",
    "/usr/include/sys/syscall.h",
]
for path in paths:
    if not os.path.exists(path):
        continue
    print(f"\n== {path} ==")
    with open(path, 'r', encoding='utf-8', errors='ignore') as f:
        for line in f:
            if any(tok in line for tok in ("ARCH_SET_FS", "ARCH_SET_GS", "SYS_arch_prctl", "arch_prctl")):
                print(line.rstrip())
PY

Repository: mcminickpt/deep-debug

Length of output: 317


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the document and inspect nearby lines in the actual file.
git ls-files 'docs/**' | rg 'tsan-port-phase2|implementation\.md|r2-r3-r4'

Repository: mcminickpt/deep-debug

Length of output: 159


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the document by name or unique path fragments.
find docs -type f | rg 'tsan|phase2|r2|r3|r4|implementation\.md'

Repository: mcminickpt/deep-debug

Length of output: 199


Fix the arch_prctl calls in the harness snippet.
syscall(SYS_arch_prctl, 2, ARCH_SET_FS, ti->fs) passes the wrong argument shape and assert(... != 0) inverts success, so this sample fails before it reaches the TSan path. Use the direct ARCH_SET_FS / ARCH_SET_GS calls with == 0 on success.

🤖 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
`@docs/superpowers/plans/2026-07-04-tsan-port-phase2-r2-r3-r4-implementation.md`
around lines 104 - 107, The harness snippet in setTLSPointer is calling
arch_prctl with the wrong syscall shape and an inverted success check, so update
it to use the direct ARCH_SET_FS and ARCH_SET_GS usage on ti->fs and ti->gs and
make the assertions validate success with == 0. Keep the fix localized to
setTLSPointer so the sample reaches the TSan path as intended.

Comment thread src/lib/wrappers.c
Comment on lines +468 to +496
MCMINI_NO_RETURN void mc_pthread_exit(void *retval) {
switch (get_current_mode()) {
case PRE_DMTCP_INIT:
case PRE_CHECKPOINT_THREAD:
case CHECKPOINT_THREAD:
case RECORD:
case PRE_CHECKPOINT: {
libpthread_pthread_exit(retval);
}
case DMTCP_RESTART_INTO_BRANCH:
case DMTCP_RESTART_INTO_TEMPLATE: {
thread_get_mailbox()->type = THREAD_EXIT_TYPE;
thread_handle_after_dmtcp_restart();
// Fallthrough
}
case TARGET_BRANCH:
case TARGET_BRANCH_AFTER_RESTART: {
if (tid_self == RID_MAIN_THREAD) {
mc_exit_main_thread_in_child();
} else {
mc_exit_thread_in_child();
}
}
default: {
libc_abort();
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

mc_pthread_exit's RECORD-mode path doesn't mark the thread's rec_list entry EXITED.

Compare with mc_thread_routine_wrapper's normal-return exit path (RECORD/PRE_CHECKPOINT case), which explicitly locks rec_list_lock, finds the thread's record, and sets thrd_state.status = EXITED before returning. Here, when a thread calls pthread_exit() explicitly in RECORD/PRE_CHECKPOINT mode, control forwards straight to libpthread_pthread_exit(retval) and never updates the record. The status only becomes correct later if/when mc_pthread_join succeeds on that thread — until then, any code inspecting rec_list sees a stale ALIVE status for an already-exited thread.

🐛 Proposed fix: mark EXITED before forwarding in RECORD/PRE_CHECKPOINT
 MCMINI_NO_RETURN void mc_pthread_exit(void *retval) {
   switch (get_current_mode()) {
-    case PRE_DMTCP_INIT:
-    case PRE_CHECKPOINT_THREAD:
-    case CHECKPOINT_THREAD:
-    case RECORD:
-    case PRE_CHECKPOINT: {
+    case PRE_DMTCP_INIT:
+    case PRE_CHECKPOINT_THREAD:
+    case CHECKPOINT_THREAD: {
+      libpthread_pthread_exit(retval);
+    }
+    case RECORD:
+    case PRE_CHECKPOINT: {
+      libpthread_mutex_lock(&rec_list_lock);
+      rec_list *thread_record = find_thread_record_mode(pthread_self());
+      assert(thread_record != NULL);
+      thread_record->vo.thrd_state.status = EXITED;
+      libpthread_mutex_unlock(&rec_list_lock);
       libpthread_pthread_exit(retval);
     }
📝 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
MCMINI_NO_RETURN void mc_pthread_exit(void *retval) {
switch (get_current_mode()) {
case PRE_DMTCP_INIT:
case PRE_CHECKPOINT_THREAD:
case CHECKPOINT_THREAD:
case RECORD:
case PRE_CHECKPOINT: {
libpthread_pthread_exit(retval);
}
case DMTCP_RESTART_INTO_BRANCH:
case DMTCP_RESTART_INTO_TEMPLATE: {
thread_get_mailbox()->type = THREAD_EXIT_TYPE;
thread_handle_after_dmtcp_restart();
// Fallthrough
}
case TARGET_BRANCH:
case TARGET_BRANCH_AFTER_RESTART: {
if (tid_self == RID_MAIN_THREAD) {
mc_exit_main_thread_in_child();
} else {
mc_exit_thread_in_child();
}
}
default: {
libc_abort();
}
}
}
MCMINI_NO_RETURN void mc_pthread_exit(void *retval) {
switch (get_current_mode()) {
case PRE_DMTCP_INIT:
case PRE_CHECKPOINT_THREAD:
case CHECKPOINT_THREAD: {
libpthread_pthread_exit(retval);
}
case RECORD:
case PRE_CHECKPOINT: {
libpthread_mutex_lock(&rec_list_lock);
rec_list *thread_record = find_thread_record_mode(pthread_self());
assert(thread_record != NULL);
thread_record->vo.thrd_state.status = EXITED;
libpthread_mutex_unlock(&rec_list_lock);
libpthread_pthread_exit(retval);
}
case DMTCP_RESTART_INTO_BRANCH:
case DMTCP_RESTART_INTO_TEMPLATE: {
thread_get_mailbox()->type = THREAD_EXIT_TYPE;
thread_handle_after_dmtcp_restart();
// Fallthrough
}
case TARGET_BRANCH:
case TARGET_BRANCH_AFTER_RESTART: {
if (tid_self == RID_MAIN_THREAD) {
mc_exit_main_thread_in_child();
} else {
mc_exit_thread_in_child();
}
}
default: {
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/wrappers.c` around lines 468 - 496, The RECORD/PRE_CHECKPOINT path in
mc_pthread_exit currently forwards directly to libpthread_pthread_exit without
updating the thread’s rec_list status, leaving an exited thread marked ALIVE.
Update mc_pthread_exit so that, before calling libpthread_pthread_exit in the
RECORD and PRE_CHECKPOINT cases, it follows the same rec_list_lock /
thread-record lookup pattern used by mc_thread_routine_wrapper and sets
thrd_state.status to EXITED for the current thread. Keep the restart and
TARGET_BRANCH paths unchanged.

gc00 and others added 2 commits July 5, 2026 01:44
Documents two investigations, both already empirically verified:
Dimension A (repeated fast_multithreaded_fork calls from the same
long-lived template process, matching mc_template_thread_loop_forever's
real while(1) structure) ran 10/10 clean under real TSan, confirming R2's
fork-hook bracketing doesn't degrade across repetition. Dimension B
(explicit pthread_exit at 8 threads, PLAN.txt's D7 residual) reproduced
the vendor package's documented ~1/30 shadow-stack-overflow crash, but
confirms it's specific to vendor-only join/exit-shim code McMini's
production mc_pthread_exit/mc_pthread_join never exercises (per Phase 3's
R5 finding) -- closing D7 without a code change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bite-sized plan for docs/superpowers/specs/2026-07-05-tsan-port-phase4-hardening-design.md:
Task 1 commits the repeated-fork stress harness (Dimension A, already
verified 10/10 passing); Task 2 closes PLAN.txt's own D7/Q4 open
question with the Dimension B finding (vendor-only residual, doesn't
apply to McMini); Task 3 is the environment-gated end-to-end DMTCP+TSan
verification with real example targets. The harness code and PLAN.txt
edits were both re-verified in a scratch copy before committing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
gc00 and others added 3 commits July 5, 2026 01:55
…al branches

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
… apply to McMini

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
… too

Discovered via a real DMTCP restart of a non-TSan target: syscall(SYS_gettid)
returns a DMTCP-virtualized tid under restart (confirmed: raw gettid()=40004,
but the real, /proc/self/task-visible tid for the same thread was 280256).
template_thread()'s barrier walk compares self_tid/ckpt_tid against
/proc/self/task, which is always a real (unvirtualized) kernel view -- so
without translation, neither ever matched, and the sanity check added in
Phase 1's final review (commit cfba39d in the deep-debug.git history) aborted
unconditionally on every real restart, a case none of the standalone
harnesses could exercise since none of them link real libdmtcp.so.

Fix: wrap self_tid and ckpt_tid in mcmini_real_pid(), the same
dmtcp_pid_virtual_to_real() translation already used for getpid()/getppid()
elsewhere in this file. Also: compare the offset self-check raw-to-raw
(both sides share the same virtualized source, so translating either first
is redundant and, per code review, could in principle mask a real offset
bug through mcmini_real_pid()'s non-injective fallback), and realify getpid()
in both diagnostic messages for consistency with the now-realified values
they're printed alongside.

Verified against a real DMTCP restart (~/dmtcp-tsan.git toolchain,
producer-consumer target, --multithreaded-fork): the barrier now correctly
stabilizes and the template process successfully forks a branch child.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@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: 7

🤖 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
`@docs/superpowers/plans/2026-07-05-tsan-port-phase4-hardening-implementation.md`:
- Around line 271-309: The replacement fenced snippets in the plan update are
missing explicit language tags, which will keep markdownlint MD040 failing.
Update the affected fenced blocks in the document to use the appropriate labels
from the surrounding content, such as c for code snippets, bash for shell
commands, and text for prose-only replacements. Use the existing snippet
locations around the D7 and Q4 replacements to apply the fence labels
consistently.
- Around line 358-365: The DMTCP verification command in the TSAN hardening plan
is missing the required ASLR suppression, so update the launch sequence to run
the existing dmtcp_launch invocation through setarch -R. Keep the current
TSAN_OPTIONS and plugin arguments intact, and adjust the documented command so
the producer-consumer-tsan run is executed from the build directory with ASLR
disabled as required by the plan.
- Around line 103-106: The TLS restore logic in setTLSPointer is calling
syscall(SYS_arch_prctl, ...) with an extra argument and the assertion is
inverted, so the arch_prctl setup will fail. Update the setTLSPointer helper to
use the correct syscall signature for ARCH_SET_FS and ARCH_SET_GS with just the
code and pointer argument, and assert for a zero return value to confirm
success. Keep the fix localized to setTLSPointer so TLS restoration works
correctly.

In `@test/tsan_support/test_repeated_fork_stress.c`:
- Around line 106-111: The child setup in the repeated fork stress test does not
guard against allocation or clone failure, which can leave the worker path
broken. In the setup around malloc, __clone, and child_setcontext_fast, add
explicit checks for a null malloc result and a failed __clone() return, then
abort or exit the test immediately on either failure so the completion path is
never skipped.
- Around line 23-30: In test_repeated_fork_stress.c, the fork/wait handling can
fall through on _Fork() or waitpid() failure and then decode an uninitialized
status value. Update the control flow around _Fork(), waitpid(), and the
WIFEXITED/WEXITSTATUS checks so the parent only inspects status after a
successful waitpid and bails out or retries cleanly when _Fork() returns -1 or
waitpid() is interrupted/fails.
- Around line 173-178: The worker launch and synchronization in the repeated
fork stress test need error handling and interrupt-safe waits. Update the thread
startup loop around pthread_create in the worker setup to detect creation
failures and fail fast instead of continuing into sem_wait deadlock, and replace
the raw sem_wait calls in this function (including the check-in and done gate
waits) with the existing retry wrapper used elsewhere so EINTR does not let the
fork/done phase advance prematurely.
- Around line 71-74: The TLS setup in setTLSPointer is calling
syscall(SYS_arch_prctl, ...) with an extra bogus argument and the assertion is
inverted, so the arch_prctl operation is never applied correctly. Update the
setTLSPointer helper to use the real arch_prctl calling convention with the
proper ARCH_SET_FS/ARCH_SET_GS arguments and make the assertions check for
success, ensuring TLS is restored before setcontext().
🪄 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: ada950e5-44ca-460f-9514-02e9e65b1a89

📥 Commits

Reviewing files that changed from the base of the PR and between cfba39d and faf71a1.

📒 Files selected for processing (5)
  • PLAN.txt
  • docs/superpowers/plans/2026-07-05-tsan-port-phase4-hardening-implementation.md
  • docs/superpowers/specs/2026-07-05-tsan-port-phase4-hardening-design.md
  • src/lib/dmtcp-callback.c
  • test/tsan_support/test_repeated_fork_stress.c
✅ Files skipped from review due to trivial changes (2)
  • docs/superpowers/specs/2026-07-05-tsan-port-phase4-hardening-design.md
  • PLAN.txt
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/dmtcp-callback.c

Comment on lines +103 to +106
static void setTLSPointer(struct threadinfo *ti) {
assert(syscall(SYS_arch_prctl, 2, ARCH_SET_FS, ti->fs) != 0);
assert(syscall(SYS_arch_prctl, 2, ARCH_SET_GS, ti->gs) != 0);
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant plan section with line numbers
sed -n '90,115p' docs/superpowers/plans/2026-07-05-tsan-port-phase4-hardening-implementation.md | cat -n

# Find other arch_prctl usages in the repo for comparison
rg -n "arch_prctl|ARCH_SET_FS|ARCH_SET_GS|SYS_arch_prctl" .

# If available, show the syscall prototype from local headers
python3 - <<'PY'
import os, glob
paths = [
    "/usr/include/x86_64-linux-gnu/sys/syscall.h",
    "/usr/include/sys/syscall.h",
    "/usr/include/asm/prctl.h",
    "/usr/include/x86_64-linux-gnu/asm/prctl.h",
]
for p in paths:
    if os.path.exists(p):
        print(f"\n== {p} ==")
        with open(p, "r", errors="ignore") as f:
            for i, line in enumerate(f, 1):
                if "ARCH_SET_FS" in line or "ARCH_SET_GS" in line or "SYS_arch_prctl" in line:
                    print(f"{i}:{line.rstrip()}")
PY

Repository: mcminickpt/deep-debug

Length of output: 4807


Fix the arch_prctl restore call.

syscall(SYS_arch_prctl, ...) takes (code, addr) and returns 0 on success. The current call shifts the arguments and asserts on failure, so TLS restoration will break.

Suggested fix
 static void setTLSPointer(struct threadinfo *ti) {
-  assert(syscall(SYS_arch_prctl, 2, ARCH_SET_FS, ti->fs) != 0);
-  assert(syscall(SYS_arch_prctl, 2, ARCH_SET_GS, ti->gs) != 0);
+  assert(syscall(SYS_arch_prctl, ARCH_SET_FS, ti->fs) == 0);
+  assert(syscall(SYS_arch_prctl, ARCH_SET_GS, ti->gs) == 0);
 }
📝 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
static void setTLSPointer(struct threadinfo *ti) {
assert(syscall(SYS_arch_prctl, 2, ARCH_SET_FS, ti->fs) != 0);
assert(syscall(SYS_arch_prctl, 2, ARCH_SET_GS, ti->gs) != 0);
}
static void setTLSPointer(struct threadinfo *ti) {
assert(syscall(SYS_arch_prctl, ARCH_SET_FS, ti->fs) == 0);
assert(syscall(SYS_arch_prctl, ARCH_SET_GS, ti->gs) == 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
`@docs/superpowers/plans/2026-07-05-tsan-port-phase4-hardening-implementation.md`
around lines 103 - 106, The TLS restore logic in setTLSPointer is calling
syscall(SYS_arch_prctl, ...) with an extra argument and the assertion is
inverted, so the arch_prctl setup will fail. Update the setTLSPointer helper to
use the correct syscall signature for ARCH_SET_FS and ARCH_SET_GS with just the
code and pointer argument, and assert for a zero return value to confirm
success. Keep the fix localized to setTLSPointer so TLS restoration works
correctly.

Comment on lines +271 to +309
In `PLAN.txt`, current lines 163-165:

```
D7. The residual explicit-pthread_exit()-at-scale issue (standalone) may or may not
matter here, since branch threads are model-checker-scheduled (rarely many
simultaneous real pthread_exit calls). Decide whether to require robustness.
```

Replace with:

```
D7. RESOLVED (Phase 4): the residual explicit-pthread_exit()-at-scale issue does
NOT matter here. Reproduced it in the standalone package directly (built
test_exit.c with NUM_WORKERS=8, ran 30x under real TSan: 1/30 runs crashed
with "stack smashing detected", matching the vendor's own ~1/30 rate) --
confirming the residual is real, but it is specific to the vendor's own
join/exit shim (tsan_join_shim.c), which performs a REAL pthread_exit() +
REAL pthread_join() on a recreated thread. McMini's production
mc_pthread_exit()/mc_pthread_join() never do this: TARGET_BRANCH* mode is
always a shared-memory mailbox handshake with the mcmini scheduler (see
Phase 3's R5 investigation). No McMini robustness work needed.
```

- [ ] **Step 2: Update the Q4 entry**

In `PLAN.txt`, current lines 252-253:

```
Q4. Do we require explicit-pthread_exit()-at-scale robustness (D7), or document
it as a limitation as the standalone does?
```

Replace with:

```
Q4. RESOLVED (Phase 4): no robustness work required -- see D7. Documented as a
vendor-package-only limitation that McMini's production code structurally
cannot hit, matching the standalone's own documented-not-fixed approach.
```

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

Add language tags to the replacement fences.

The four snippets in this block are missing fenced-code languages, so markdownlint MD040 will keep failing here. Please mark them explicitly (c, bash, or text as appropriate).

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 273-273: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 281-281: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 298-298: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 305-305: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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
`@docs/superpowers/plans/2026-07-05-tsan-port-phase4-hardening-implementation.md`
around lines 271 - 309, The replacement fenced snippets in the plan update are
missing explicit language tags, which will keep markdownlint MD040 failing.
Update the affected fenced blocks in the document to use the appropriate labels
from the surrounding content, such as c for code snippets, bash for shell
commands, and text for prose-only replacements. Use the existing snippet
locations around the D7 and Q4 replacements to apply the fence labels
consistently.

Source: Linters/SAST tools

Comment on lines +358 to +365
From the directory containing `libmcmini.so`:

```bash
cd build
TSAN_OPTIONS="handle_segv=0 die_after_fork=0" \
dmtcp_launch --disable-alloc-plugin -i 5 --with-plugin "$PWD/libmcmini.so" \
/tmp/producer-consumer-tsan
```

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

Wrap the DMTCP launch with setarch -R too.

This step launches a TSan-instrumented target without the ASLR suppression required by the plan's own global constraint, so the documented verification run can diverge or fail here.

Suggested fix
-TSAN_OPTIONS="handle_segv=0 die_after_fork=0" \
-  dmtcp_launch --disable-alloc-plugin -i 5 --with-plugin "$PWD/libmcmini.so" \
-  /tmp/producer-consumer-tsan
+setarch -R env TSAN_OPTIONS="handle_segv=0 die_after_fork=0" \
+  dmtcp_launch --disable-alloc-plugin -i 5 --with-plugin "$PWD/libmcmini.so" \
+  /tmp/producer-consumer-tsan
📝 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
From the directory containing `libmcmini.so`:
```bash
cd build
TSAN_OPTIONS="handle_segv=0 die_after_fork=0" \
dmtcp_launch --disable-alloc-plugin -i 5 --with-plugin "$PWD/libmcmini.so" \
/tmp/producer-consumer-tsan
```
cd build
setarch -R env TSAN_OPTIONS="handle_segv=0 die_after_fork=0" \
dmtcp_launch --disable-alloc-plugin -i 5 --with-plugin "$PWD/libmcmini.so" \
/tmp/producer-consumer-tsan
🤖 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
`@docs/superpowers/plans/2026-07-05-tsan-port-phase4-hardening-implementation.md`
around lines 358 - 365, The DMTCP verification command in the TSAN hardening
plan is missing the required ASLR suppression, so update the launch sequence to
run the existing dmtcp_launch invocation through setarch -R. Keep the current
TSAN_OPTIONS and plugin arguments intact, and adjust the documented command so
the producer-consumer-tsan run is executed from the build directory with ASLR
disabled as required by the plan.

Comment on lines +23 to +30
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <ucontext.h>
#include <unistd.h>
#include <sys/wait.h>

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:

#!/bin/bash
rg -n -C4 '\b_Fork\(|\bwaitpid\(|WEXITSTATUS|WIFEXITED' test/tsan_support/test_repeated_fork_stress.c

Repository: mcminickpt/deep-debug

Length of output: 1695


🏁 Script executed:

#!/bin/bash
sed -n '110,210p' test/tsan_support/test_repeated_fork_stress.c

Repository: mcminickpt/deep-debug

Length of output: 3209


🏁 Script executed:

#!/bin/bash
nl -ba test/tsan_support/test_repeated_fork_stress.c | sed -n '110,210p'

Repository: mcminickpt/deep-debug

Length of output: 195


Guard _Fork()/waitpid() before decoding status. If _Fork() returns -1, the parent path falls through to waitpid(-1, ...); if waitpid() fails or is interrupted, status is still read with WIFEXITED/WEXITSTATUS, which makes the log use uninitialized data.

🤖 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_repeated_fork_stress.c` around lines 23 - 30, In
test_repeated_fork_stress.c, the fork/wait handling can fall through on _Fork()
or waitpid() failure and then decode an uninitialized status value. Update the
control flow around _Fork(), waitpid(), and the WIFEXITED/WEXITSTATUS checks so
the parent only inspects status after a successful waitpid and bails out or
retries cleanly when _Fork() returns -1 or waitpid() is interrupted/fails.

Comment on lines +71 to +74
static void setTLSPointer(struct threadinfo *ti) {
assert(syscall(SYS_arch_prctl, 2, ARCH_SET_FS, ti->fs) != 0);
assert(syscall(SYS_arch_prctl, 2, ARCH_SET_GS, ti->gs) != 0);
}

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:

#!/bin/bash
rg -n -C4 '\b(setTLSPointer|getTLSPointer|SYS_arch_prctl|ARCH_SET_[FG]S)\b' \
  src/lib/dmtcp-callback.c test/tsan_support/test_repeated_fork_stress.c

Repository: mcminickpt/deep-debug

Length of output: 6670


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- test/tsan_support/test_repeated_fork_stress.c ---'
sed -n '67,74p' test/tsan_support/test_repeated_fork_stress.c

printf '\n%s\n' '--- src/lib/dmtcp-callback.c ---'
sed -n '94,101p' src/lib/dmtcp-callback.c

printf '\n%s\n' '--- arch_prctl constants from local headers ---'
rg -n 'ARCH_(SET|GET)_[FG]S' /usr/include -g 'prctl.h' -g 'arch*.h' | head -n 20

Repository: mcminickpt/deep-debug

Length of output: 1367


Restore TLS using the real arch_prctl arguments. SYS_arch_prctl takes (code, addr), so the extra 2 makes this call use the wrong operation, and != 0 treats the failure path as success. That leaves TLS unset before setcontext().

Suggested fix
 static void setTLSPointer(struct threadinfo *ti) {
-  assert(syscall(SYS_arch_prctl, 2, ARCH_SET_FS, ti->fs) != 0);
-  assert(syscall(SYS_arch_prctl, 2, ARCH_SET_GS, ti->gs) != 0);
+  assert(syscall(SYS_arch_prctl, ARCH_SET_FS, ti->fs) == 0);
+  assert(syscall(SYS_arch_prctl, ARCH_SET_GS, ti->gs) == 0);
 }
📝 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
static void setTLSPointer(struct threadinfo *ti) {
assert(syscall(SYS_arch_prctl, 2, ARCH_SET_FS, ti->fs) != 0);
assert(syscall(SYS_arch_prctl, 2, ARCH_SET_GS, ti->gs) != 0);
}
static void setTLSPointer(struct threadinfo *ti) {
assert(syscall(SYS_arch_prctl, ARCH_SET_FS, ti->fs) == 0);
assert(syscall(SYS_arch_prctl, ARCH_SET_GS, ti->gs) == 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_repeated_fork_stress.c` around lines 71 - 74, The TLS
setup in setTLSPointer is calling syscall(SYS_arch_prctl, ...) with an extra
bogus argument and the assertion is inverted, so the arch_prctl operation is
never applied correctly. Update the setTLSPointer helper to use the real
arch_prctl calling convention with the proper ARCH_SET_FS/ARCH_SET_GS arguments
and make the assertions check for success, ensuring TLS is restored before
setcontext().

Comment on lines +106 to +111
void *stack = malloc(0x10000) + 0x10000 - 128; // 64 KB, intentionally leaked
int offset = pthreadDescriptorTidOffset();
pid_t *ctid = (pid_t *)((char *)threadInfos[i].pthread_descriptor + offset);
pid_t *ptid = ctid;
__clone(child_setcontext_fast, stack, clone_flags,
(void *)&threadInfos[i], ptid, threadInfos[i].fs, ctid);

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:

#!/bin/bash
rg -n -C4 'malloc\(0x10000\)|__clone\(' \
  test/tsan_support/test_repeated_fork_stress.c src/lib/dmtcp-callback.c

Repository: mcminickpt/deep-debug

Length of output: 5229


Handle malloc()/__clone() failures in the child path test/tsan_support/test_repeated_fork_stress.c:106-111
malloc(0x10000) is used before any null check, and a failed __clone() can leave the test without a spawned worker to signal its completion path. Exit explicitly on either failure.

🧰 Tools
🪛 Cppcheck (2.21.0)

[error] 106-106: If memory allocation fails

(nullPointerArithmeticOutOfMemory)

🤖 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_repeated_fork_stress.c` around lines 106 - 111, The
child setup in the repeated fork stress test does not guard against allocation
or clone failure, which can leave the worker path broken. In the setup around
malloc, __clone, and child_setcontext_fast, add explicit checks for a null
malloc result and a failed __clone() return, then abort or exit the test
immediately on either failure so the completion path is never skipped.

Source: Linters/SAST tools

Comment on lines +173 to +178
pthread_t th[NUM_WORKERS];
for (int i = 0; i < NUM_WORKERS; i++) {
pthread_create(&th[i], NULL, worker, NULL);
}
for (int i = 0; i < NUM_WORKERS; i++) {
sem_wait(&sem_checkin);

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:

#!/bin/bash
rg -n -C3 '\bpthread_create\(|\bsem_wait\(' test/tsan_support/test_repeated_fork_stress.c

Repository: mcminickpt/deep-debug

Length of output: 944


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file structure and inspect the relevant ranges.
wc -l test/tsan_support/test_repeated_fork_stress.c
ast-grep outline test/tsan_support/test_repeated_fork_stress.c --view expanded

echo
sed -n '1,240p' test/tsan_support/test_repeated_fork_stress.c

Repository: mcminickpt/deep-debug

Length of output: 7618


Check thread creation errors and retry semaphore waits
pthread_create() failures leave the main thread waiting forever for check-ins, and the raw sem_wait() calls here and at 185-186 should use the existing retry wrapper so an interrupt doesn’t advance the fork or done gate early.

🤖 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_repeated_fork_stress.c` around lines 173 - 178, The
worker launch and synchronization in the repeated fork stress test need error
handling and interrupt-safe waits. Update the thread startup loop around
pthread_create in the worker setup to detect creation failures and fail fast
instead of continuing into sem_wait deadlock, and replace the raw sem_wait calls
in this function (including the check-in and done gate waits) with the existing
retry wrapper used elsewhere so EINTR does not let the fork/done phase advance
prematurely.

@xuyao0127 xuyao0127 requested a review from Copilot July 5, 2026 13:01

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Real DMTCP restart testing (dmtcp-tsan.git) surfaced a second
LD_PRELOAD interceptor on the raw __clone symbol used to recreate
threads after checkpoint restart: DMTCP's own libdmtcp.so strongly
exports __clone (threadwrappers.cpp) and unconditionally aborts
unless the caller is mid-DMTCP's-own pthread_create --

  ASSERT at threadwrappers.cpp:177: false: Thread creation with
  clone syscall is not supported: flags=4001536

matching restart_child_threads_fast()'s exact clone_flags. Calling
the bare `__clone` symbol hits this interceptor instead of libc's
real implementation, exactly like the public clone() hits libtsan's
(the reason this code already avoided clone() in favor of __clone).

Resolve the real, uninterposed __clone the same way this codebase
already resolves other real libc primitives around interceptors:
add a libc_clone() forwarding function to interception.c's existing
mc_load_intercepted_pthread_functions() table, dlsym'd from libc
once at process startup, well before any fork or checkpoint restart
ever happens. An initial version of this fix instead did a fresh
dlopen/dlsym lazily inside restart_child_threads_fast() itself
(post-_Fork(), in the child); code review flagged that as a
duplicate resolution mechanism that also risked deadlocking on a
dynamic-linker lock left held by a thread that no longer exists in
that child, so it was folded into the existing, already-proven-safe
libc_* table instead.

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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/dmtcp-callback.c (1)

469-473: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Skip unreadable thread status entries src/lib/dmtcp-callback.c:469-473thread_blocks_signal() returns 0 on /proc/self/task/<tid>/status open/parse failure, so a transient/exited thread is treated as countable here and can overinflate thread_count, leaving the restart barrier stuck. Use a tri-state helper or explicitly skip/retry unreadable entries.

🤖 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 469 - 473, The restart barrier
counting in the thread scan treats unreadable /proc thread status entries as
countable because thread_blocks_signal() only returns a boolean-like 0 on
open/parse failure. Update the logic around thread_blocks_signal() and the
thread_count increment so transient or exited threads are explicitly skipped or
retried instead of being counted, and ensure the helper can distinguish “cannot
read status” from “thread does not block the signal.”
🤖 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/dmtcp-callback.c`:
- Around line 224-230: The raw clone call in the thread recreation loop is not
checked, so a failed libc_clone() can leave thread state incomplete and break
later restart/barrier logic. Update the code around libc_clone() in the thread
restore path to capture its return value, detect failure, and stop or report the
error instead of silently continuing. Use the existing child_setcontext_fast and
threadInfos[i] context to keep the failure handling localized where the thread
is recreated.

---

Outside diff comments:
In `@src/lib/dmtcp-callback.c`:
- Around line 469-473: The restart barrier counting in the thread scan treats
unreadable /proc thread status entries as countable because
thread_blocks_signal() only returns a boolean-like 0 on open/parse failure.
Update the logic around thread_blocks_signal() and the thread_count increment so
transient or exited threads are explicitly skipped or retried instead of being
counted, and ensure the helper can distinguish “cannot read status” from “thread
does not block the signal.”
🪄 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: 556e9e2e-0975-44d8-8cbb-e75ba8bb0b2f

📥 Commits

Reviewing files that changed from the base of the PR and between faf71a1 and 7773e63.

📒 Files selected for processing (3)
  • include/mcmini/spy/intercept/interception.h
  • src/lib/dmtcp-callback.c
  • src/lib/interception.c

Comment thread src/lib/dmtcp-callback.c
Comment on lines +224 to +230
// R3: use libc's raw __clone (via libc_clone(), see above), bypassing
// both libtsan's and DMTCP's own interception of the public clone()/
// __clone symbols.
libc_clone(child_setcontext_fast,
stack,
clone_flags,
(void *)&threadInfos[i], ptid, (void *)threadInfos[i].fs, ctid);

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

Check libc_clone() failure before continuing.

If raw clone fails, the loop silently skips recreating that thread and later restart/barrier logic can hang or resume with missing state.

Proposed fix
-    libc_clone(child_setcontext_fast,
-               stack,
-               clone_flags,
-               (void *)&threadInfos[i], ptid, (void *)threadInfos[i].fs, ctid);
+    int rc = libc_clone(child_setcontext_fast,
+                        stack,
+                        clone_flags,
+                        (void *)&threadInfos[i], ptid,
+                        (void *)threadInfos[i].fs, ctid);
+    if (rc < 0) {
+      perror("libc_clone");
+      libc_abort();
+    }
📝 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
// R3: use libc's raw __clone (via libc_clone(), see above), bypassing
// both libtsan's and DMTCP's own interception of the public clone()/
// __clone symbols.
libc_clone(child_setcontext_fast,
stack,
clone_flags,
(void *)&threadInfos[i], ptid, (void *)threadInfos[i].fs, ctid);
int rc = libc_clone(child_setcontext_fast,
stack,
clone_flags,
(void *)&threadInfos[i], ptid,
(void *)threadInfos[i].fs, ctid);
if (rc < 0) {
perror("libc_clone");
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 224 - 230, The raw clone call in the
thread recreation loop is not checked, so a failed libc_clone() can leave thread
state incomplete and break later restart/barrier logic. Update the code around
libc_clone() in the thread restore path to capture its return value, detect
failure, and stop or report the error instead of silently continuing. Use the
existing child_setcontext_fast and threadInfos[i] context to keep the failure
handling localized where the thread is recreated.

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