Mirror of the gdb-patches mailing list
 help / color / mirror / Atom feed
* [PATCH] gdb: fix non-stop attach leaving non-main threads stopped forever
@ 2026-10-09  7:28 LiRvs-Miria
  0 siblings, 0 replies; only message in thread
From: LiRvs-Miria @ 2026-10-09  7:28 UTC (permalink / raw)
  To: gdb-patches; +Cc: Tom de Vries, Pedro Alves, Tankut Baris Aktemur

When attaching natively to a multi-threaded process in non-stop mode,
linux-nat collects the leader's stop synchronously but leaves every
other LWP's attach SIGSTOP pending ("the next time we wait for this LWP
we'll see a SIGSTOP"), while the worker threads are reported as running.

A synchronous -target-attach reports ^done as soon as attach_command
returns -- before those pending attach stops are consumed.  An MI
consumer that then sends "-exec-continue --all" (as DAP adapters and
session services do) hits that window: proceed_one_thread skips every
thread whose state is not THREAD_STOPPED, so only the leader gets
resumed.  The workers' attach stops are consumed later, but nothing
ever resumes those threads: they remain stranded in ptrace-stop
forever, with no error reported.  A synchronous CLI attach is not
affected, because a foreground attach removes stdin from the event
loop until the attach continuation has really finished.

Fix by draining the pending stops with stop_all_threads in the
non-stop path of attach_post_wait (ATTACH_POST_WAIT_STOP), scoped to
the inferior being attached, mirroring what the target-always-non-stop
path right below already does, and then syncing the user-visible
thread states of that inferior with finish_thread_state, so the attach
command only completes when the whole process is really stopped, with
consistent thread states.  This also stops the pending attach stops
from leaking later to the UI as bogus
"*stopped,reason="signal-received",signal-name="0"" events.

If the target does not actually run in non-stop mode (e.g. "maint set
target-async off"), keep the previous plain target_stop request, as
stop_all_threads asserts on exists_non_stop_target.

The async "attach&" path (ATTACH_POST_WAIT_RESUME) is unaffected; it
already behaves correctly.

Testing: the previous revision of this fix was validated on gdb 18.1
(x86_64-linux): the reproducer went from 4/10 to 0/10 failures, and a
regression subset (gdb.base/attach.exp, gdb.mi/mi-nonstop.exp,
gdb.threads/non-stop-fair-events.exp, gdb.threads/attach-into-signal.exp)
showed no differences (534 passes, 2 unsupported both before and after).
The testsuite changes in this revision are syntax-checked and dry-run
only; full testsuite runs against master will follow (my main build
machine is temporarily down with a hardware issue).

Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=28270
---
 gdb/infcmd.c                                  |  40 +++-
 .../gdb.mi/mi-nonstop-attach-continue.exp     | 172 ++++++++++++++++++
 gdb/testsuite/gdb.threads/attach-non-stop.exp |  33 ++--
 3 files changed, 226 insertions(+), 19 deletions(-)
 create mode 100644 gdb/testsuite/gdb.mi/mi-nonstop-attach-continue.exp

diff --git a/gdb/infcmd.c b/gdb/infcmd.c
index d754416..ce33eb1 100644
--- a/gdb/infcmd.c
+++ b/gdb/infcmd.c
@@ -2818,11 +2818,43 @@ attach_post_wait (int from_tty, enum attach_post_wait_mode mode)
 
       /* In all-stop, by definition, all threads have to be already
 	 stopped at this point.  In non-stop, however, although the
-	 selected thread is stopped, others may still be executing.
-	 Be sure to explicitly stop all threads of the process.  This
-	 should have no effect on already stopped threads.  */
+	 selected thread is stopped, other threads may still be
+	 executing -- or, right after a native attach, may be waiting
+	 for their attach SIGSTOP to be collected while being reported
+	 as running.  Stop all threads of this inferior, and wait until
+	 every stop has been collected, so that this command only
+	 completes when the whole process is really stopped.  Otherwise,
+	 an MI "-exec-continue --all" issued right after
+	 -target-attach's ^done would only resume the thread(s) that
+	 reported a stop so far, silently leaving the remaining threads
+	 stopped forever (the attach stops are consumed later, but
+	 nothing resumes those threads in non-stop mode).  This should
+	 have no effect on already stopped threads.  */
       if (non_stop)
-	target_stop (ptid_t (inferior->pid));
+	{
+	  if (target_is_non_stop_p ())
+	    {
+	      stop_all_threads ("attaching", inferior);
+
+	      /* Sync the user-visible thread states of this inferior:
+		 the attach stops consumed above flipped the internal
+		 state of threads that had been reported as running
+		 during the attach, and nothing else will publish those
+		 states.  Without this, the user-visible states of those
+		 threads stay stale (running), even though the threads
+		 are stopped, which e.g. makes a subsequent
+		 "-exec-continue --all" skip them.  Syncing is silent:
+		 no thread becomes running here.  */
+	      finish_thread_state (inferior->process_target (),
+				    ptid_t (inferior->pid));
+	    }
+	  else
+	    /* The target is not actually operating in non-stop mode
+	       (e.g. "maint set target-async off"); just request a stop
+	       like before -- stop_all_threads asserts on
+	       exists_non_stop_target.  */
+	    target_stop (ptid_t (inferior->pid));
+	}
       else if (target_is_non_stop_p ())
 	{
 	  struct thread_info *lowest = inferior_thread ();
diff --git a/gdb/testsuite/gdb.mi/mi-nonstop-attach-continue.exp b/gdb/testsuite/gdb.mi/mi-nonstop-attach-continue.exp
new file mode 100644
index 0000000..3e7827d
--- /dev/null
+++ b/gdb/testsuite/gdb.mi/mi-nonstop-attach-continue.exp
@@ -0,0 +1,172 @@
+# Copyright 2026 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Test attaching to a multi-threaded process in non-stop mode via MI,
+# and then resuming all threads with "-exec-continue --all".
+#
+# This is a regression test for a bug where a synchronous native
+# attach in non-stop mode returned before all the pending attach stops
+# had been processed: the stops of the non-main threads dribbled in
+# after the prompt as bogus '*stopped,reason="signal-received",
+# signal-name="0"' events, and a following "-exec-continue --all" could
+# leave those threads stranded.
+#
+# The attach logic now drains all the pending attach stops with
+# stop_all_threads before the attach command completes, so when
+# "-target-attach" reports "^done", all threads of the attached process
+# are already stopped, and no stop notification leaks after the prompt.
+# That makes the checks below deterministic.  (On builds without the
+# fix, the same checks are racy.)
+
+load_lib mi-support.exp
+set MIFLAGS "-i=mi"
+
+require can_spawn_for_attach
+
+if {![istarget "*-*-linux*"]} {
+    unsupported "native non-stop attach test"
+    return
+}
+
+standard_testfile
+
+# Number of worker threads started by the program.
+set n_workers 6
+
+# Number of threads GDB should see: the main thread plus the workers.
+set n_threads [expr {$n_workers + 1}]
+
+# The debuggee: the main thread creates the workers and then blocks in
+# pthread_join, while the workers loop in nanosleep.  The process is
+# thus quiescent (no signals, no thread exits) when we attach to it.
+set src {
+#include <pthread.h>
+#include <unistd.h>
+#include <time.h>
+
+#define N_WORKERS 6
+
+static void *
+worker_function (void *arg)
+{
+  struct timespec ts;
+
+  for (;;)
+    {
+      ts.tv_sec = 0;
+      ts.tv_nsec = 100000000;
+
+      nanosleep (&ts, NULL);
+    }
+
+  return NULL; /* Not reached.  */
+}
+
+int
+main (void)
+{
+  pthread_t workers[N_WORKERS];
+  int i;
+
+  alarm (300);
+
+  for (i = 0; i < N_WORKERS; i++)
+    pthread_create (&workers[i], NULL, worker_function, NULL);
+
+  for (i = 0; i < N_WORKERS; i++)
+    pthread_join (workers[i], NULL);
+
+  return 0;
+}
+}
+
+set srcfile [standard_output_file ${testfile}.c]
+set binfile [standard_output_file $testfile]
+
+set fd [open $srcfile w]
+puts $fd $src
+close $fd
+
+set options [list debug]
+if {[gdb_compile_pthreads $srcfile $binfile executable $options] != ""} {
+    return
+}
+
+set spawn_id [spawn_wait_for_attach $::binfile]
+set prog_pid [spawn_id_get_pid $spawn_id]
+
+# Give slow machines some head room.
+with_timeout_factor 4 {
+
+    # Start GDB with non-stop mode on.
+    save_vars { GDBFLAGS } {
+	append GDBFLAGS " -ex \"set non-stop on\""
+	mi_clean_restart $::testfile
+    }
+
+    # Attach to the spawned process, synchronously.  All the pending
+    # attach stops are drained before the attach command completes, so
+    # any "*stopped" notifications appear before "^done"; consume them
+    # here.
+    mi_gdb_test "-target-attach $prog_pid" ".*\\^done.*" \
+	"attach to process $prog_pid"
+
+    # No stop notification should leak out after the attach command
+    # completed.  Builds without the fix dribble in bogus
+    # '*stopped,reason="signal-received",signal-name="0"'
+    # notifications here, after the prompt.
+    set test "no stops after attach prompt"
+    gdb_expect 2 {
+	-re "\\*stopped" {
+	    fail $test
+	}
+	timeout {
+	    pass $test
+	}
+    }
+
+    # When the attach command completed, all threads of the attached
+    # process were already stopped.
+    set states {}
+    for {set i 0} {$i < $n_threads} {incr i} {
+	lappend states "stopped"
+    }
+    mi_check_thread_states $states "all threads stopped after attach"
+
+    # Resume all threads, and expect one "*running" notification per
+    # thread.  Don't require a specific thread-id ordering in the
+    # notifications; the all-running state check below covers that.
+    set running_re ""
+    for {set i 1} {$i <= $n_threads} {incr i} {
+	append running_re "\\*running,thread-id=\"\[0-9\]+\"\r\n"
+    }
+    set running_re [string range $running_re 0 end-2]
+
+    mi_gdb_test "-exec-continue --all" ".*$running_re.*" \
+	"resume all threads"
+
+    # Every thread should now be reported as running.
+    set states {}
+    for {set i 0} {$i < $n_threads} {incr i} {
+	lappend states "running"
+    }
+    mi_check_thread_states $states "all threads running after continue"
+
+    mi_gdb_test "-target-detach" "\\^done" "detach from process $prog_pid"
+}
+
+kill_wait_spawned_process $spawn_id
+
+mi_gdb_exit
diff --git a/gdb/testsuite/gdb.threads/attach-non-stop.exp b/gdb/testsuite/gdb.threads/attach-non-stop.exp
index 3d36339..131b458 100644
--- a/gdb/testsuite/gdb.threads/attach-non-stop.exp
+++ b/gdb/testsuite/gdb.threads/attach-non-stop.exp
@@ -48,7 +48,12 @@ proc test {target_non_stop non_stop cmd} {
     set any "\[^\r\n\]*"
 
     if {$cmd == "attach"} {
+	set stops 0
 	gdb_test_multiple "attach $testpid" $test {
+	    -re "Thread $::decimal ${any} stopped" {
+		incr stops
+		exp_continue
+	    }
 	    -re "Attaching to program:${any}process $testpid\r\n.*$gdb_prompt " {
 		pass $test
 		set attached 1
@@ -61,22 +66,20 @@ proc test {target_non_stop non_stop cmd} {
 	}
 
 	if {$non_stop} {
-	    # In non-stop, we will see one stop per thread after
-	    # the prompt.
-	    set stops 0
-	    set test "seen all stops"
-	    for {set thread 1} { $thread <= $n_threads } { incr thread } {
-		gdb_test_multiple "" $test {
-		    -re "Thread $::decimal ${any} stopped" {
-			incr stops
-		    }
+	    # In non-stop, all the pending attach stops are drained
+	    # before the attach command completes, so we should have
+	    # seen one stop per thread before the prompt.
+	    gdb_assert {$stops == $n_threads} "seen all stops"
+
+	    # No stop should dribble in after the prompt, either.
+	    set test "no stops after the prompt"
+	    gdb_expect 2 {
+		-re "Thread $::decimal ${any} stopped" {
+		    fail $test
+		}
+		timeout {
+		    pass $test
 		}
-	    }
-
-	    # If we haven't seen all stops, then the gdb_test_multiple
-	    # in the loop above will have already issued a FAIL.
-	    if {$stops == $n_threads} {
-		pass $test
 	    }
 	}
 
-- 
2.34.1


^ permalink raw reply	[flat|nested] only message in thread

only message in thread, other threads:[~2026-10-09  7:29 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-09  7:28 [PATCH] gdb: fix non-stop attach leaving non-main threads stopped forever LiRvs-Miria

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox