[binutils-gdb] Don't pretend infcalls don't set the inferior running (PR gdb/34082)

Pedro Alves via Gdb-cvs <[email protected]>
Newsgroups gmane.comp.gdb.cvs
Message-ID <[email protected]>
https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;h=519774805a14850f13064cc6fcc4179c5b700631

commit 519774805a14850f13064cc6fcc4179c5b700631
Author: Pedro Alves <[email protected]>
Date:   Tue Apr 21 21:08:11 2026 +0100

    Don't pretend infcalls don't set the inferior running (PR gdb/34082)
    
    Commit 2954dd2b73 ("thread_info::executing+resumed ->
    thread_info::internal_state"), caused a regression in
    gdb.threads/hand-call-new-thread.exp:
    
     ...
     (gdb) PASS: gdb.threads/hand-call-new-thread.exp: iter 1: no thread marked running
     p new_thread ()
     .../src/gdb/infrun.c:3742: internal-error: proceed: Assertion `!thread_is_in_step_over_chain (&tp)' failed.
     A problem internal to GDB has been detected,
     further debugging may prove unreliable.
     ----- Backtrace -----
     FAIL: gdb.threads/hand-call-new-thread.exp: iter 2: gdb-command<p new_thread ()> (GDB internal error)
     ...
    
    This commit fixes it.
    
    Let's say we have three threads, 1, 2, and 3.  User does:
    
     (gdb) continue
    
    This makes GDB switch all three threads to THREAD_RUNNING.
    
    If some of those threads, now running, spawns a new thread, that
    thread is also set to state THREAD_RUNNING.  We end up with four
    threads marked THREAD_RUNNING.
    
    If e.g., threads 2 and 3 both hit a breakpoint that needs to be
    stepped over (e.g., condition evals false), and there is only one
    displaced-stepping slot, then one thread starts a displaced stepping
    sequence, while the other is put in the step-over queue, waiting for
    its turn.
    
    Now, if meanwhile thread 1 hits a user-visible stop, GDB stops all
    threads, and transitions all their states to THREAD_STOPPED.  Any
    thread that was still waiting for its turn in the step-over queue is
    removed from the queue.  That happens in the THREAD_RUNNING =>
    THREAD_STOPPED transition, here:
    
      thread_state
      thread_info::set_state (thread_state state, bool suppress_notification)
      {
      ...
        switch (m_state)
          {
          case THREAD_STOPPED:
            if (thread_is_in_step_over_chain (this))
              global_thread_step_over_chain_remove (this);
    
    The next time the user continues execution, if the breakpoint is still
    inserted, proceed() sets them stepping the breakpoint again.  And
    again, if there is more than one thread that needs to step-over, and
    there aren't enough slots, some threads may end up in the step-over
    queue.  Rinse, repeat.
    
    All this works well with normal resumption commands, like continue,
    step, next, etc.
    
    The problem exposed by gdb.threads/hand-call-new-thread.exp is if you
    resume execution with an infcall instead of a normal execution
    command.  In that case, proceed() skips transitioning (pre-existing)
    threads to THREAD_RUNNING, here:
    
      proceed (CORE_ADDR addr, enum gdb_signal siggnal)
      {
       ...
          /* Even if RESUME_PTID is a wildcard, and we end up resuming fewer
             threads in RESUME_PTID are now running.  Unless we're calling an
             inferior function, as in that case we pretend the inferior
             doesn't run at all.  */
          if (!cur_thr->control.in_infcall)
            set_state (resume_target, resume_ptid, THREAD_RUNNING);
    
    So later, when the call finishes for any reason (normal call finish,
    or some other user-visible stop happens), and GDB transitions all
    threads to THREAD_STOPPED, we hit the early return in
    thread_info::set_state:
    
      thread_state
      thread_info::set_state (thread_state state, bool suppress_notification)
      {
        thread_state prev_state = m_state;
        if (prev_state == state)
          return prev_state;          // <== EARLY RETURN
    
        m_state = state;
        switch (m_state)
          {
          case THREAD_STOPPED:
            if (thread_is_in_step_over_chain (this))
              global_thread_step_over_chain_remove (this);  // NOT REACHED
            break;
          ...
          }
      }
    
    ... and so if any thread had been put in the step-over queue since the
    last proceed(), it will incorrectly be left still in the step-over
    queue, with THREAD_STOPPED state.
    
    If/when the user re-resumes the program again, we trip the assertion
    in proceed:
    
      (gdb) p new_thread ()
      ../../src/gdb/infrun.c:3742: internal-error: proceed: Assertion `!thread_is_in_step_over_chain (&tp)' failed.
      A problem internal to GDB has been detected,
      further debugging may prove unreliable.
    
    Before commit 2954dd2b73 ("thread_info::executing+resumed ->
    thread_info::internal_state"), this didn't happen because
    set_running_thread(..., running=false) would remove threads from the
    step-over queue unconditionally, even if they were already marked
    stopped.
    
    I think the right fix is to stop pretending that infcalls don't set
    the target running.  I can't think of a reason we do that.  It really
    does run.  Some thoughts:
    
    - I added the code to skip set_running (nowadays 'set_state(...,
      THREAD_RUNNING))' for infcalls back in commit 4d9d9d0423 over 10
      years ago, but I honestly don't recall why.  My guess is that it
      must have been to keep backwards compatibility with something, and
      the code has probably changed sufficiently since then making it no
      longer necessary.
    
    - infcalls are always synchronous, so the intermediate running state
      can't be observed with commands.
    
    - MI still suppresses *running
    
      The running state could potentially be observed on frontends, with
      e.g., MI's *running => *stopped transitions, but if that is a
      problem, I think it should be handled by the interpreter layer not
      emiting the notifications, instead of hacking the threads's core
      state.  I.e., make it a presentation detail.
    
      I don't think it would be a problem for frontends to see *running
      during infcalls, but in any case, MI is already suppressing such
      notifications when they are caused by an infcall.  See
      mi_interp::on_target_resumed.
    
      So e.g., if we let the threads transition to THREAD_RUNNING, with
      MI, we still see no *running/*stopped:
    
        (gdb)
        p malloc(0)
        &"p malloc(0)\n"
        ~"$2 = (void *) 0x555555560320\n"
        ^done
        (gdb)
    
      I don't know if it'd be a problem for DAP, but I assume not too.
    
      Note how the code in proceed that skips setting threads to
      THREAD_RUNNING only applies to already-known threads.  Any new
      thread that appears while the infcall is ongoing will end up marked
      THREAD_RUNNING, causing frontend notifications.  This shows how
      hiding the running state doesn't really hide it completely.
    
    So this is what this commit does.  It lets threads transition to
    THREAD_RUNNING even during infcalls, fixing the problem described
    above, as now there will be proper THREAD_RUNNING => THREAD_STOPPED
    transitions when the infcall finishes.
    
    It also tweaks the testcase to spawn 10 threads per infcall instead of
    one.  This makes it much more likely to reproduce the problem on my
    machine.  Without it, the test still passes for me, which is why I
    didn't see the problem before merging 2954dd2b73.
    
    Tested on x86-64 Linux, native and gdbserver.
    
    Approved-By: Andrew Burgess <[email protected]>
    Change-Id: I1bdc733ac865102d3f7bf0a4f7f56e6f7d75d457
    commit-id:d2abd130

Diff:
---
 gdb/infrun.c                                     |  9 +++------
 gdb/testsuite/gdb.threads/hand-call-new-thread.c | 16 +++++++++++-----
 2 files changed, 14 insertions(+), 11 deletions(-)

diff --git a/gdb/infrun.c b/gdb/infrun.c
index 6401df78e0a..0e359f0ed74 100644
--- a/gdb/infrun.c
+++ b/gdb/infrun.c
@@ -3688,11 +3688,8 @@ proceed (CORE_ADDR addr, enum gdb_signal siggnal)
   /* Even if RESUME_PTID is a wildcard, and we end up resuming fewer
      threads (e.g., we might need to set threads stepping over
      breakpoints first), from the user/frontend's point of view, all
-     threads in RESUME_PTID are now running.  Unless we're calling an
-     inferior function, as in that case we pretend the inferior
-     doesn't run at all.  */
-  if (!cur_thr->control.in_infcall)
-    set_state (resume_target, resume_ptid, THREAD_RUNNING);
+     threads in RESUME_PTID are now running.  */
+  set_state (resume_target, resume_ptid, THREAD_RUNNING);
 
   infrun_debug_printf ("addr=%s, signal=%s, resume_ptid=%s",
 		       paddress (gdbarch, addr),
@@ -6636,7 +6633,7 @@ restart_threads (struct thread_info *event_thread, inferior *inf)
 	  continue;
 	}
 
-      if (!(tp.state () == THREAD_RUNNING || tp.control.in_infcall))
+      if (tp.state () != THREAD_RUNNING)
 	{
 	  infrun_debug_printf ("restart threads: [%s] not meant to be running",
 			       tp.ptid.to_string ().c_str ());
diff --git a/gdb/testsuite/gdb.threads/hand-call-new-thread.c b/gdb/testsuite/gdb.threads/hand-call-new-thread.c
index 620322fff10..04154576351 100644
--- a/gdb/testsuite/gdb.threads/hand-call-new-thread.c
+++ b/gdb/testsuite/gdb.threads/hand-call-new-thread.c
@@ -34,14 +34,20 @@ thread_function (void *arg)
     foo ();
 }
 
+#define NTHREADS 10
+
 void
 new_thread (void)
 {
-  pthread_t thread;
-  int res;
-
-  res = pthread_create (&thread, NULL, thread_function, NULL);
-  assert (res == 0);
+  pthread_t thread[NTHREADS];
+  int i;
+
+  for (i = 0; i < NTHREADS; i++)
+    {
+      int res;
+      res = pthread_create (&thread[i], NULL, thread_function, NULL);
+      assert (res == 0);
+    }
 }
 
 int
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.