diff options
-rw-r--r-- | gdb/linux-nat.c | 24 | ||||
-rw-r--r-- | gdb/remote.c | 27 | ||||
-rw-r--r-- | gdb/testsuite/gdb.threads/main-thread-exit-during-detach.c | 61 | ||||
-rw-r--r-- | gdb/testsuite/gdb.threads/main-thread-exit-during-detach.exp | 140 | ||||
-rw-r--r-- | gdbserver/mem-break.cc | 11 |
5 files changed, 255 insertions, 8 deletions
diff --git a/gdb/linux-nat.c b/gdb/linux-nat.c index 9148dda..1c9756c 100644 --- a/gdb/linux-nat.c +++ b/gdb/linux-nat.c @@ -1420,10 +1420,12 @@ linux_nat_target::detach (inferior *inf, int from_tty) iterate_over_lwps (ptid_t (pid), detach_callback); - /* Only the initial process should be left right now. */ - gdb_assert (num_lwps (pid) == 1); - - main_lwp = find_lwp_pid (ptid_t (pid)); + /* We have detached from everything except the main thread now, so + should only have one thread left. However, in non-stop mode the + main thread might have exited, in which case we'll have no threads + left. */ + gdb_assert (num_lwps (pid) == 1 + || (target_is_non_stop_p () && num_lwps (pid) == 0)); if (forks_exist_p ()) { @@ -1437,10 +1439,18 @@ linux_nat_target::detach (inferior *inf, int from_tty) { target_announce_detach (from_tty); - /* Pass on any pending signal for the last LWP. */ - int signo = get_detach_signal (main_lwp); + /* In non-stop mode it is possible that the main thread has exited, + in which case we don't try to detach. */ + main_lwp = find_lwp_pid (ptid_t (pid)); + if (main_lwp != nullptr) + { + /* Pass on any pending signal for the last LWP. */ + int signo = get_detach_signal (main_lwp); - detach_one_lwp (main_lwp, &signo); + detach_one_lwp (main_lwp, &signo); + } + else + gdb_assert (target_is_non_stop_p ()); detach_success (inf); } diff --git a/gdb/remote.c b/gdb/remote.c index b58dbd4..6927104 100644 --- a/gdb/remote.c +++ b/gdb/remote.c @@ -6165,7 +6165,32 @@ remote_target::remote_detach_pid (int pid) else if (rs->buf[0] == '\0') error (_("Remote doesn't know how to detach")); else - error (_("Can't detach process.")); + { + /* It is possible that we have an unprocessed exit event for this + pid. If this is the case then we can ignore the failure to detach + and just pretend that the detach worked, as far as the user is + concerned, the process exited immediately after the detach. */ + bool process_has_already_exited = false; + remote_notif_get_pending_events (¬if_client_stop); + for (stop_reply_up &reply : rs->stop_reply_queue) + { + if (reply->ptid.pid () != pid) + continue; + + enum target_waitkind kind = reply->ws.kind (); + if (kind == TARGET_WAITKIND_EXITED + || kind == TARGET_WAITKIND_SIGNALLED) + { + process_has_already_exited = true; + remote_debug_printf + ("detach failed, but process already exited"); + break; + } + } + + if (!process_has_already_exited) + error (_("can't detach process: %s"), (char *) rs->buf.data ()); + } } /* This detaches a program to which we previously attached, using diff --git a/gdb/testsuite/gdb.threads/main-thread-exit-during-detach.c b/gdb/testsuite/gdb.threads/main-thread-exit-during-detach.c new file mode 100644 index 0000000..5335842 --- /dev/null +++ b/gdb/testsuite/gdb.threads/main-thread-exit-during-detach.c @@ -0,0 +1,61 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2023 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/>. */ + +#include <pthread.h> +#include <unistd.h> + +/* This is set from GDB to allow the main thread to exit. */ + +volatile int dont_exit_just_yet = 1; + +/* Somewhere to place a breakpoint. */ + +void +breakpt () +{ + /* Just spin. When the test is started under GDB we never enter the spin + loop, but when we attach, the worker thread will be spinning here. */ + while (dont_exit_just_yet) + sleep (1); +} + +/* Thread function, doesn't do anything but hit a breakpoint. */ + +void * +thread_worker (void *arg) +{ + breakpt (); + return NULL; +} + +int +main () +{ + pthread_t thr; + + alarm (300); + + /* Create a thread. */ + pthread_create (&thr, NULL, thread_worker, NULL); + pthread_detach (thr); + + /* Spin until GDB releases us. */ + while (dont_exit_just_yet) + sleep (1); + + _exit (0); +} diff --git a/gdb/testsuite/gdb.threads/main-thread-exit-during-detach.exp b/gdb/testsuite/gdb.threads/main-thread-exit-during-detach.exp new file mode 100644 index 0000000..83f5e85 --- /dev/null +++ b/gdb/testsuite/gdb.threads/main-thread-exit-during-detach.exp @@ -0,0 +1,140 @@ +# Copyright 2023 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/>. + +# Check for a race condition where in non-stop mode, the user might +# have a thread other than the main (original) thread selected and use +# the 'detach' command. +# +# As GDB tries to detach it is possible that the main thread might +# exit, the main thread is still running due to non-stop mode. +# +# GDB used to assume that the main thread would always exist when +# processing the detach, clearly this isn't the case, and this +# assumption would lead to assertion failures and segfaults. +# +# Triggering the precise timing is pretty hard, we need the main +# thread to exit after the user has entered the 'detach' command, but +# before GDB enters the detach implementation and stops all threads, +# the window of opportunity for this bug is actually tiny. +# +# However, we can trigger this bug 100% from Python, as GDB's +# event-loop only kicks in once we return from a Python function. +# Thus, if we have a single Python function that causes the main +# thread to exit, and then calls detach GDB will not have a chance to +# handle the main thread exiting before entering the detach code. + +standard_testfile + +require allow_python_tests + +if {[build_executable "failed to prepare" $testfile $srcfile \ + {debug pthreads}] == -1} { + return -1 +} + +# Run the test. When SPAWN_INFERIOR is true the inferior is started +# as a separate process which GDB then attaches too. When +# SPAWN_INFERIOR is false the inferior is started directly within GDB. + +proc run_test { spawn_inferior } { + save_vars { ::GDBFLAGS } { + append ::GDBFLAGS " -ex \"set non-stop on\"" + clean_restart $::binfile + } + + # Setup the inferior. When complete the main thread (#1) will + # still be running (due to non-stop mode), while the worker thread + # (#2) will be stopped. + # + # There are two setup modes, when SPAWN_INFERIOR is true we span a + # separate process and attach to it, after the attach both threads + # are stopped, so it is necessary to resume thread #1. + # + # When SPAWN_INFERIOR is false we just start the inferior within + # GDB, in this case we place a breakpoint that will be hit by + # thread #2. When the breakpoint is hit thread #1 will remain + # running. + if {$spawn_inferior} { + set test_spawn_id [spawn_wait_for_attach $::binfile] + set testpid [spawn_id_get_pid $test_spawn_id] + + set escapedbinfile [string_to_regexp $::binfile] + gdb_test -no-prompt-anchor "attach $testpid" \ + "Attaching to program.*`?$escapedbinfile'?, process $testpid.*" \ + "attach to the inferior" + + # Attaching to a multi-threaded application in non-stop mode + # can result in thread stops being reported after the prompt + # is displayed. + # + # Send a simple command now just to resync the command prompt. + gdb_test "p 1 + 2" " = 3" + + # Set thread 1 (the current thread) running again. + gdb_test "continue&" + } else { + if {![runto_main]} { + return -1 + } + + gdb_breakpoint "breakpt" + gdb_continue_to_breakpoint "run to breakpoint" + } + + # Switch to thread 2. + gdb_test "thread 2" \ + [multi_line \ + "Switching to thread 2\[^\r\n\]*" \ + "#0\\s+.*"] + + # Create a Python function that sets a variable in the inferior and + # then detaches. Setting the variable in the inferior will allow the + # main thread to exit, we even sleep for a short while in order to + # give the inferior a chance to exit. + # + # However, we don't want GDB to notice the exit before we call detach, + # which is why we perform both these actions from a Python function. + gdb_test_multiline "Create worker function" \ + "python" "" \ + "import time" "" \ + "def set_and_detach():" "" \ + " gdb.execute(\"set variable dont_exit_just_yet=0\")" "" \ + " time.sleep(1)" "" \ + " gdb.execute(\"detach\")" "" \ + "end" "" + + # The Python function performs two actions, the first causes the + # main thread to exit, while the second detaches from the inferior. + # + # In both cases the stop arrives while GDB is processing the + # detach, however, for remote targets GDB doesn't report the stop, + # while for local targets GDB does report the stop. + if {![gdb_is_target_remote]} { + set stop_re "\\\[Thread.*exited\\\]\r\n" + } else { + set stop_re "" + } + gdb_test "python set_and_detach()" \ + "${stop_re}\\\[Inferior.*detached\\\]" +} + +foreach_with_prefix spawn_inferior { true false } { + if {$spawn_inferior && ![can_spawn_for_attach]} { + # If spawning (and attaching too) a separate inferior is not + # supported for the current board, then skip this test. + continue + } + + run_test $spawn_inferior +} diff --git a/gdbserver/mem-break.cc b/gdbserver/mem-break.cc index 897b9a2..3bee8bc8 100644 --- a/gdbserver/mem-break.cc +++ b/gdbserver/mem-break.cc @@ -976,6 +976,17 @@ static struct gdb_breakpoint * find_gdb_breakpoint (char z_type, CORE_ADDR addr, int kind) { struct process_info *proc = current_process (); + + /* In some situations the current process exits, we inform GDB, but + before GDB can acknowledge that the process has exited GDB tries to + detach from the inferior. As part of the detach process GDB will + remove all breakpoints, which means we can end up here when the + current process has already exited and so PROC is nullptr. In this + case just claim we can't find (and so delete) the breakpoint, GDB + will ignore this error during detach. */ + if (proc == nullptr) + return nullptr; + struct breakpoint *bp; enum bkpt_type type = Z_packet_to_bkpt_type (z_type); |