WIP: TSAN checkpoint restart#9
Conversation
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>
📝 WalkthroughWalkthroughAdds 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. ChangesTSan Port Implementation
Estimated code review effort: 4 (Complex) | ~75 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
cfba39d to
6c4aaa8
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
src/lib/tsan_support.c (1)
5-28: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winValidate
signorange before shifting.
sigblk >> (signo - 1)(Line 27) is undefined behavior ifsigno <= 0orsigno > 64(shift by negative or by ≥ width ofunsigned 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 winArena isn't guaranteed to be 16-byte aligned.
mc_ts_allocroundsnto 16-byte boundaries (Line 14) implying a malloc-like alignment guarantee, butmc_ts_arenais a plainchar[]with no explicit alignment. The C standard only guarantees 1-byte alignment forchararrays; 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
📒 Files selected for processing (27)
CMakeLists.txtdocs/superpowers/plans/2026-07-04-tsan-port-phase1-r1-implementation.mddocs/superpowers/plans/2026-07-04-tsan-port-phase2-r2-r3-r4-implementation.mddocs/superpowers/plans/2026-07-05-tsan-port-phase3-r5-implementation.mddocs/superpowers/specs/2026-07-04-tsan-port-phase1-r1-design.mddocs/superpowers/specs/2026-07-04-tsan-port-phase2-r2-r3-r4-design.mddocs/superpowers/specs/2026-07-05-tsan-port-phase3-r5-design.mdinclude/dmtcp.hinclude/mcmini/mem.hinclude/mcmini/model/pending_transitions.hppinclude/mcmini/model/state.hppinclude/mcmini/model_checking/algorithms/classic_dpor/stack_item.hppinclude/mcmini/spy/checkpointing/record.hinclude/mcmini/spy/checkpointing/tsan_support.hinclude/mcmini/spy/intercept/interception.hinclude/mcmini/spy/intercept/wrappers.hsrc/common/mem.csrc/common/multithreaded_fork.csrc/lib/dmtcp-callback.csrc/lib/interception.csrc/lib/record.csrc/lib/tsan_support.csrc/lib/wrappers.ctest/tsan_support/test_fastpath_fork_clone_fiber.ctest/tsan_support/test_pthread_exit_forwarding.ctest/tsan_support/test_thread_blocks_signal.ctest/tsan_support/test_tid_from_descriptor_offset.c
| 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); | ||
| } |
There was a problem hiding this comment.
🎯 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()}")
PYRepository: 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())
PYRepository: 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.
| 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(); | ||
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
🗄️ 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.
| 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.
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>
…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>
There was a problem hiding this comment.
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
📒 Files selected for processing (5)
PLAN.txtdocs/superpowers/plans/2026-07-05-tsan-port-phase4-hardening-implementation.mddocs/superpowers/specs/2026-07-05-tsan-port-phase4-hardening-design.mdsrc/lib/dmtcp-callback.ctest/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
| 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); | ||
| } |
There was a problem hiding this comment.
🩺 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()}")
PYRepository: 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.
| 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.
| 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. | ||
| ``` |
There was a problem hiding this comment.
📐 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
| 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 | ||
| ``` |
There was a problem hiding this comment.
🩺 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.
| 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.
| #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> |
There was a problem hiding this comment.
🩺 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.cRepository: mcminickpt/deep-debug
Length of output: 1695
🏁 Script executed:
#!/bin/bash
sed -n '110,210p' test/tsan_support/test_repeated_fork_stress.cRepository: 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.
| 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); | ||
| } |
There was a problem hiding this comment.
🩺 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.cRepository: 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 20Repository: 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.
| 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().
| 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); |
There was a problem hiding this comment.
🩺 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.cRepository: 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
| 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); |
There was a problem hiding this comment.
🩺 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.cRepository: 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.cRepository: 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.
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>
There was a problem hiding this comment.
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 liftSkip unreadable thread status entries
src/lib/dmtcp-callback.c:469-473—thread_blocks_signal()returns0on/proc/self/task/<tid>/statusopen/parse failure, so a transient/exited thread is treated as countable here and can overinflatethread_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
📒 Files selected for processing (3)
include/mcmini/spy/intercept/interception.hsrc/lib/dmtcp-callback.csrc/lib/interception.c
| // 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); |
There was a problem hiding this comment.
🩺 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.
| // 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.
Still working on it with Claude, but mostly ready to start being reviewed. This includes the commits of PR #8.
Summary by CodeRabbit