[DynInst_API:] [dyninst/dyninst] dde8a8: proccontrol: don't cache raw int_thread* in events


Date: Tue, 07 Jul 2026 04:34:37 -0700
From: bbiiggppiigg <noreply@xxxxxxxxxx>
Subject: [DynInst_API:] [dyninst/dyninst] dde8a8: proccontrol: don't cache raw int_thread* in events
  Branch: refs/heads/bbiiggppiigg/fix-test-thread-1
  Home:   https://github.com/dyninst/dyninst
  Commit: dde8a8b668bd60ff5d43d697aa0f7190c6e24f96
      https://github.com/dyninst/dyninst/commit/dde8a8b668bd60ff5d43d697aa0f7190c6e24f96
  Author: Hsuan-Heng Wu <bbiiggppiigg@xxxxxxxxx>
  Date:   2026-07-07 (Tue, 07 Jul 2026)

  Changed paths:
    M proccontrol/src/event.C
    M proccontrol/src/handler.C
    M proccontrol/src/int_event.h
    M proccontrol/src/int_thread_db.C
    M proccontrol/src/process.C

  Log Message:
  -----------
  proccontrol: don't cache raw int_thread* in events

int_eventBreakpoint::thrd and int_eventNewUserThread::thr held raw
int_thread* pointers.  A breakpoint or thread-create event can outlive
the thread that triggered it: the thread exits while the event is still
queued (or sitting in the callback/muxer layer), the int_thread is
deleted, and the cached pointer dangles -- observed as sporadic SIGSEGVs
in lookupInstalledBreakpoint() and EventNewUserThread::getNewThread()
when running the testsuite's test_thread_1.

Store the reference-counted Thread wrapper instead: it survives thread
destruction and its accessors are exit-safe (llthrd() returns NULL,
getLWP() falls back to the exit state).  Guard the dereference sites
that can now legitimately see an exited thread: lookupInstalledBreakpoint,
EventBreakpoint::getBreakpoints, the read-only breakpoint/single-step
handlers, ThreadDBCreateHandler, and int_process::getBreakpoint (whose
mem is NULL once the process is cleaned up).

Co-Authored-By: Claude Fable 5 <noreply@xxxxxxxxxxxxx>


  Commit: f91a973f6c974274672e9bfcb387a148f983fa7a
      https://github.com/dyninst/dyninst/commit/f91a973f6c974274672e9bfcb387a148f983fa7a
  Author: Hsuan-Heng Wu <bbiiggppiigg@xxxxxxxxx>
  Date:   2026-07-07 (Tue, 07 Jul 2026)

  Changed paths:
    M proccontrol/src/process.C

  Log Message:
  -----------
  proccontrol: fix debug prints on exited threads

The pthrd_printf calls in ProcStopEventManager::prepEvent/checkEvents
dereference ev->getThread()->llthrd()->getLWP() unconditionally.  A
held proc-stopper event's thread can exit before the event is released,
making llthrd() NULL and crashing the process -- but only when
DYNINST_DEBUG_PROCCONTROL is enabled, which made the underlying races
in this area impossible to trace.

Co-Authored-By: Claude Fable 5 <noreply@xxxxxxxxxxxxx>


  Commit: f123fd96e4bc790db5cd51be6857165949a21815
      https://github.com/dyninst/dyninst/commit/f123fd96e4bc790db5cd51be6857165949a21815
  Author: Hsuan-Heng Wu <bbiiggppiigg@xxxxxxxxx>
  Date:   2026-07-07 (Tue, 07 Jul 2026)

  Changed paths:
    M proccontrol/src/handler.C

  Log Message:
  -----------
  proccontrol: restore state desyncs on thread death

The breakpoint machinery brackets proc-wide stops in desyncStateProc /
restoreStateProc pairs on the BreakpointHold, Breakpoint and
BreakpointResume state layers.  When the thread that owns the event
dies mid-sequence, the restoring half was skipped, leaving every
surviving thread force-stopped forever (new threads inherit the desync
too), which hangs the process.

Two shapes of the leak:

- HandleBreakpointContinue/Clear/Restore bail out when the event's
  thread is gone, skipping their paired restore.  The restore is a
  process-wide walk that only needs a live thread as its handle, so
  route it through any thread still in the pool (the pool only holds
  live threads; initialThread() can be stale during teardown).

- A thread that dies while single-stepping over a cleared (suspended)
  breakpoint never produces the single-step trap, so the decoder never
  generates the BreakpointRestore event at all: the breakpoint stays
  out of memory and the proc-wide BreakpointResume desyncs leak.
  Perform that recovery in HandleThreadCleanup while the dying thread
  is still in the pool, so one restore walk exactly balances the one
  desync each thread received.

Co-Authored-By: Claude Fable 5 <noreply@xxxxxxxxxxxxx>


  Commit: fdfcf0db24c1bac6703e80bac9f9bf8f7bde1577
      https://github.com/dyninst/dyninst/commit/fdfcf0db24c1bac6703e80bac9f9bf8f7bde1577
  Author: Hsuan-Heng Wu <bbiiggppiigg@xxxxxxxxx>
  Date:   2026-07-07 (Tue, 07 Jul 2026)

  Changed paths:
    M proccontrol/src/int_thread_db.C
    M proccontrol/src/process.C

  Log Message:
  -----------
  proccontrol: fix thread_db stopped-thread asserts

thread_db_process::getEventForThread() asserts all threads are
handler-stopped before reading thread_db state out of the mutatee.
Two races broke the assertion when mutatee threads are created and
destroyed rapidly:

- Exiting/exited threads can have transiently stale handler states (the
  exit event is decoded but not yet handled).  Such threads cannot
  execute user code and so cannot race the memory reads; make
  allHandlerStopped() skip them, mirroring the exiting-thread handling
  in syncRunState().

- A thread created after the proc-stopping thread_db breakpoint event
  was released, but before the dispatch handler ran, is still running.
  Its ThreadCreate event is pending in the mailbox and will stop it
  (HandleThreadCreate initializes new threads to the process's desync'd
  states), so defer the dispatch with ret_again until that has
  happened.

Notes for reviewers: allHandlerStopped() has one other caller, in the
FreeBSD startup path; skipping exiting threads is believed equally
correct there (a thread that cannot run cannot violate quiescence) but
was not tested on that platform.  Handler::ret_again existed but had no
in-tree users before this change; this is its first use.

Co-Authored-By: Claude Fable 5 <noreply@xxxxxxxxxxxxx>


  Commit: 05ff5ec924393859613eea122c171998ba3306c8
      https://github.com/dyninst/dyninst/commit/05ff5ec924393859613eea122c171998ba3306c8
  Author: Hsuan-Heng Wu <bbiiggppiigg@xxxxxxxxx>
  Date:   2026-07-07 (Tue, 07 Jul 2026)

  Changed paths:
    M proccontrol/src/response.C
    M proccontrol/src/response.h

  Log Message:
  -----------
  proccontrol: never destroy the response id locks

The response.C id_lock mutexes were plain statics.  Static destructor
order across translation units is unspecified, so they could be
destroyed before int_cleanup (generator.C) stops the handler thread;
the handler thread then locked a destroyed mutex while draining
teardown events (glibc marks a destroyed mutex __kind = -1 and
pthread_mutex_lock returns EINVAL, which boost turns into a lock_error
exception and std::terminate).

Heap-allocate and intentionally leak both locks, mirroring the existing
fix and comment for Counter::locks in process.C.

Co-Authored-By: Claude Fable 5 <noreply@xxxxxxxxxxxxx>


  Commit: df25fea79798330175696b0c0d608ba0ba514664
      https://github.com/dyninst/dyninst/commit/df25fea79798330175696b0c0d608ba0ba514664
  Author: Hsuan-Heng Wu <bbiiggppiigg@xxxxxxxxx>
  Date:   2026-07-07 (Tue, 07 Jul 2026)

  Changed paths:
    M proccontrol/src/linux.C

  Log Message:
  -----------
  proccontrol: bound generator eviction spin at exit

GeneratorLinux::evictFromWaitpid() sends SIGUSR2 to the generator
thread and spins until the signal handler sets a flag.  The generator
can exit on its own initiative (no live processes left) concurrently
with the eviction; the signal is then queued to a dying thread and
discarded, the flag never fires, and the destructor spins forever at
100% CPU inside exit().

Bound the wait to a couple of seconds: the eviction is best-effort --
we are already exiting and exit_group() will reap the generator thread
regardless.

Co-Authored-By: Claude Fable 5 <noreply@xxxxxxxxxxxxx>


  Commit: 35371fb77990ff7461fa32e21f2f6a1751b7eb36
      https://github.com/dyninst/dyninst/commit/35371fb77990ff7461fa32e21f2f6a1751b7eb36
  Author: Hsuan-Heng Wu <bbiiggppiigg@xxxxxxxxx>
  Date:   2026-07-07 (Tue, 07 Jul 2026)

  Changed paths:
    M proccontrol/src/linux.C
    M proccontrol/src/process.C

  Log Message:
  -----------
  proccontrol: don't stop threads that are exiting

If intStop() sends SIGSTOP to a thread whose exit event is already in
flight (PTRACE_EVENT_EXIT observed), the stop can never be delivered:
the thread just dies.  The PendingStop state layer then stays desync'd
to running forever, which (a) blocks isStopped() for every higher
layer, so held proc-stopper breakpoint events are never released, and
(b) forces a run target for a thread that can only die.  The process
deadlocks with the mutatee ptrace-stopped, the generator idle, and an
unreaped zombie thread -- the dominant sporadic hang when running the
testsuite's test_thread_1.

LinuxHandleLWPDestroy was written for exactly this case (clear pending
stops on pre-LWPDestroy; its comment describes the bug) but was never
registered with the handler pool.  Register it, guard it against
already-destroyed threads, and additionally skip intStop() for threads
already observed exiting.  The two halves are intentionally redundant:
the intStop() guard covers stops requested after the exit was observed,
the handler covers stops requested before it (SIGSTOP landing on a
thread already inside the exit syscall).

Co-Authored-By: Claude Fable 5 <noreply@xxxxxxxxxxxxx>


  Commit: 36f4fd0e98e4d73596be07469b97a90db001d134
      https://github.com/dyninst/dyninst/commit/36f4fd0e98e4d73596be07469b97a90db001d134
  Author: Hsuan-Heng Wu <bbiiggppiigg@xxxxxxxxx>
  Date:   2026-07-07 (Tue, 07 Jul 2026)

  Changed paths:
    M proccontrol/src/process.C

  Log Message:
  -----------
  proccontrol: re-sync run state before blocking

The event loop can park with an actionable run-state divergence left
unserviced: a thread sits handler-stopped but targeted running -- e.g.
the PendingStop state wants it continued so an in-flight SIGSTOP can be
delivered -- yet no event is in flight to drive another syncRunState
pass.  waitAndHandleEvents then blocks on an empty mailbox (its own
should_block check even counts the pending stop as a reason to block),
the generator sees nothing runnable and idles, and the process
deadlocks: mutatee ptrace-stopped, SIGSTOP pinned undelivered in
SigPnd, proc-stopper events held forever.

Captured live twice with different micro-interleavings; in both cases
injecting a single nop event -- whose only effect is one more
syncRunState pass -- fully unwound the deadlock.  Make the loop do that
for itself: before blocking, sweep for processes with a divergent
thread and re-run syncRunState on them.  The pass is idempotent
(threads already in their target state or with a stop in flight are
skipped), any action it takes produces the event that wakes the
dequeue, and the sweep runs at most once per park, so an unresolvable
divergence blocks exactly as before.

Co-Authored-By: Claude Fable 5 <noreply@xxxxxxxxxxxxx>


Compare: https://github.com/dyninst/dyninst/compare/e9df01dae0a1...36f4fd0e98e4

To unsubscribe from these emails, change your notification settings at https://github.com/dyninst/dyninst/settings/notifications
[← Prev in Thread] Current Thread [Next in Thread→]
  • [DynInst_API:] [dyninst/dyninst] dde8a8: proccontrol: don't cache raw int_thread* in events, bbiiggppiigg <=