Skip to content

Route pthread_create via RTLD_NEXT so TSan targets don't crash#5

Open
gc00 wants to merge 3 commits into
mainfrom
tsan-target-support
Open

Route pthread_create via RTLD_NEXT so TSan targets don't crash#5
gc00 wants to merge 3 commits into
mainfrom
tsan-target-support

Conversation

@gc00

@gc00 gc00 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

[ This PR is stacked on top of PR #3 so that it can handle PTHREAD_MUTEX_INITIALIZER in the target. ]

Model checking a -fsanitize=thread target crashed: worker threads spawned
by libmcmini were created through libc's raw pthread_create, bypassing
libtsan's interceptor, so ThreadSanitizer never set up their ThreadState
and the first instrumented access hit a null deref (SEGV inside libtsan).

Fix: in classic mode, resolve pthread_create via dlsym(RTLD_NEXT). Since
libmcmini.so is LD_PRELOAD'd ahead of the target's libraries, this routes
through the sanitizer's interceptor when present and falls back to libc
otherwise. Gated on !dmtcp_is_enabled(), leaving the DMTCP path unchanged.

Verified: mcmini now model checks tsan_target with no SEGV.

Caveats: crash fix only (mutex/cond/sem still bypass libtsan, so their ops
are untracked but no longer crash); classic mode only, DMTCP+TSan is
future work. Stacks on pthread-mutex-initializer-support.

gc00 and others added 3 commits June 30, 2026 23:44
A mutex declared with PTHREAD_MUTEX_INITIALIZER never flows through the
pthread_mutex_init wrapper, so the model never observes it being
initialized. Previously mutex_lock_callback threw "uninitialized mutex"
in that case (the `// TODO: add code from Gene's PR here` placeholder).

Port the original McMini behavior: on a lock of a mutex the model has
not seen, read the target's memory via process_vm_readv and compare
against PTHREAD_MUTEX_INITIALIZER. On a match, lazily register the mutex
as initialized+unlocked; otherwise report undefined behavior. Unlock is
left reporting undefined behavior for an unknown mutex (faithful to the
original, which does not lazily register on unlock).

- model_to_system_map: expose get_target_pid() so callbacks can read the
  live target's memory.
- coordinator: implement get_target_pid() from the current process handle.
- mutex.cpp: add the process_vm_readv validation helper and port the lock
  callback; fix the mis-copied unlock message.

A FIXME documents that, as in the original McMini, this only detects an
uninitialized mutex for stack/garbage mutexes; a never-initialized
data/heap mutex is zero-filled (== PTHREAD_MUTEX_INITIALIZER on glibc)
and still slips through.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Exercises the reject path of the PTHREAD_MUTEX_INITIALIZER support: a
stack-allocated pthread_mutex_t filled with non-zero garbage (so it cannot
be mistaken for the all-zero PTHREAD_MUTEX_INITIALIZER) is locked without
initialization. McMini reports:

  UNDEFINED BEHAVIOR:
  Attempting to lock an uninitialized mutex

Note the data/heap case is intentionally not detected (zero-filled ==
PTHREAD_MUTEX_INITIALIZER); see the FIXME in
src/mcmini/model/transitions/mutex.cpp.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When model checking a target compiled with -fsanitize=thread, the worker
threads that libmcmini spawns in TARGET_BRANCH mode segfaulted on their
first instrumented memory access: they were created through libc's raw
pthread_create (resolved from an explicit dlopen("libpthread.so")
handle), which bypasses libtsan's pthread_create interceptor, so
ThreadSanitizer never initialized the per-thread ThreadState. The first
__tsan_* call then dereferenced a null ThreadState (SEGV on a small fixed
offset such as 0x828, inside libtsan).

In classic model checking, resolve pthread_create via dlsym(RTLD_NEXT)
instead. Because libmcmini.so is LD_PRELOAD'd ahead of the target's
DT_NEEDED libraries, RTLD_NEXT lands on the sanitizer's interceptor when
the target is instrumented (which sets up the thread's state), and falls
through to libc/libpthread otherwise.

The change is gated on !dmtcp_is_enabled(): under DMTCP, libdmtcp is also
in the link map and RTLD_NEXT could resolve to its interceptor, and the
DMTCP thread-creation paths are handled separately via
libdmtcp_pthread_create. The explicit-handle behavior is preserved there
byte-for-byte, so deep debugging is unaffected. (Only pthread_create is
routed this way; the model-checking join path never calls the real
pthread_join. Sanitizer support under deep debugging is future work.)

Verified: mcmini now model checks ~/dmtcp.git/test/tsan_target (a
-fsanitize=thread program) without the SEGV; spawned worker threads run
their instrumented lock/increment/unlock loops under scheduler control.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gc00 gc00 requested review from aayushi363 and maxwellpirtle July 1, 2026 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant