From patchwork Thu Jun 16 23:32:48 2016 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Pedro Alves X-Patchwork-Id: 13144 Received: (qmail 115264 invoked by alias); 16 Jun 2016 23:33:06 -0000 Mailing-List: contact gdb-patches-help@sourceware.org; run by ezmlm Precedence: bulk List-Id: List-Unsubscribe: List-Subscribe: List-Archive: List-Post: List-Help: , Sender: gdb-patches-owner@sourceware.org Delivered-To: mailing list gdb-patches@sourceware.org Received: (qmail 115162 invoked by uid 89); 16 Jun 2016 23:33:05 -0000 Authentication-Results: sourceware.org; auth=none X-Virus-Found: No X-Spam-SWARE-Status: No, score=-3.3 required=5.0 tests=BAYES_00, RP_MATCHES_RCVD, SPF_HELO_PASS autolearn=ham version=3.3.2 spammy=gdb_stdlog, sk:fprintf, lwpid, STEP X-HELO: mx1.redhat.com Received: from mx1.redhat.com (HELO mx1.redhat.com) (209.132.183.28) by sourceware.org (qpsmtpd/0.93/v0.84-503-g423c35a) with (AES256-GCM-SHA384 encrypted) ESMTPS; Thu, 16 Jun 2016 23:32:54 +0000 Received: from int-mx11.intmail.prod.int.phx2.redhat.com (int-mx11.intmail.prod.int.phx2.redhat.com [10.5.11.24]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by mx1.redhat.com (Postfix) with ESMTPS id 7CC6BC04B305 for ; Thu, 16 Jun 2016 23:32:53 +0000 (UTC) Received: from cascais.lan (ovpn01.gateway.prod.ext.ams2.redhat.com [10.39.146.11]) by int-mx11.intmail.prod.int.phx2.redhat.com (8.14.4/8.14.4) with ESMTP id u5GNWmPZ025383 for ; Thu, 16 Jun 2016 19:32:52 -0400 From: Pedro Alves To: gdb-patches@sourceware.org Subject: [PATCH v4 3/3] Fix failure to detach if process exits while detaching on Linux Date: Fri, 17 Jun 2016 00:32:48 +0100 Message-Id: <1466119968-15171-4-git-send-email-palves@redhat.com> In-Reply-To: <1466119968-15171-1-git-send-email-palves@redhat.com> References: <1466119968-15171-1-git-send-email-palves@redhat.com> This commit fixes detaching on Linux when some thread exits the whole thread group (process) just while we're detaching. On Linux, a ptracer must detach from each LWP individually, with PTRACE_DETACH. Since PTRACE_DETACH sets the thread running free, if one of the already-detached threads causes the whole thread group to exit (e.g., simply calls exit), the kernel force-kills the other threads in the group, making them zombie, just as we're still detaching them. Since PTRACE_DETACH against a zombie thread fails with ESRCH, and gdb/gdbserver are not expecting this, the detach fails with an error like: "Can't detach process: No such process.". This patch detects this detach failure as normal, and instead of erroring out, reaps the now-dead thread. New test included, that exercises several different scenarios that cause GDB/GDBserver to error out when it should not. Tested on x86-64 GNU/Linux with {unix, native-gdbserver, native-extended-gdbserver} Note: without the previous fix, the "single-process + continue" variant of the new test would fail with: (gdb) PASS: gdb.threads/process-dies-while-detaching.exp: single-process: continue: watchpoint: switch to parent continue Continuing. Warning: Could not insert hardware watchpoint 3. Could not insert hardware breakpoints: You may have requested too many hardware breakpoints/watchpoints. Command aborted. (gdb) FAIL: gdb.threads/process-dies-while-detaching.exp: single-process: continue: watchpoint: continue gdb/gdbserver/ChangeLog: yyyy-mm-dd Pedro Alves Antoine Tremblay * linux-low.c: Change interface to take the target lwp_info pointer directly and return void. Handle detaching from a zombie thread. (linux_detach_lwp_callback): New function. (linux_detach): Detach from the leader thread after detaching from the clone threads. gdb/ChangeLog: yyyy-mm-dd Pedro Alves Antoine Tremblay * inf-ptrace.c (inf_ptrace_detach_success): New function, factored out from ... (inf_ptrace_detach): ... here. * inf-ptrace.h (inf_ptrace_detach_success): New declaration. * linux-nat.c (get_pending_status): Rename to ... (get_detach_signal): ... this, and return a host signal instead of filling in a wait status. (detach_one_lwp): New function, factored out from detach_callback and adjusted to handle detaching from a zombie thread. (detach_callback): Skip the leader thread. (linux_nat_detach): No longer defer to inf_ptrace_detach to detach the leader thread, nor build a signal string to pass down. Instead, use target_announce_detach, detach_one_lwp and inf_ptrace_detach_success. gdb/testsuite/ChangeLog: yyyy-mm-dd Pedro Alves Antoine Tremblay * gdb.threads/process-dies-while-detaching.c: New file. * gdb.threads/process-dies-while-detaching.exp: New file. --- gdb/gdbserver/linux-low.c | 116 ++++++-- gdb/inf-ptrace.c | 10 + gdb/inf-ptrace.h | 4 + gdb/linux-nat.c | 152 ++++++---- .../gdb.threads/process-dies-while-detaching.c | 116 ++++++++ .../gdb.threads/process-dies-while-detaching.exp | 327 +++++++++++++++++++++ 6 files changed, 655 insertions(+), 70 deletions(-) create mode 100644 gdb/testsuite/gdb.threads/process-dies-while-detaching.c create mode 100644 gdb/testsuite/gdb.threads/process-dies-while-detaching.exp diff --git a/gdb/gdbserver/linux-low.c b/gdb/gdbserver/linux-low.c index 81134b0..7ce37d9 100644 --- a/gdb/gdbserver/linux-low.c +++ b/gdb/gdbserver/linux-low.c @@ -266,6 +266,7 @@ static int kill_lwp (unsigned long lwpid, int signo); static void enqueue_pending_signal (struct lwp_info *lwp, int signal, siginfo_t *info); static void complete_ongoing_step_over (void); static int linux_low_ptrace_options (int attached); +static int check_ptrace_stopped_lwp_gone (struct lwp_info *lp); /* When the event-loop is doing a step-over, this points at the thread being stepped. */ @@ -1447,16 +1448,14 @@ get_detach_signal (struct thread_info *thread) } } -static int -linux_detach_one_lwp (struct inferior_list_entry *entry, void *args) +/* Detach from LWP. */ + +static void +linux_detach_one_lwp (struct lwp_info *lwp) { - struct thread_info *thread = (struct thread_info *) entry; - struct lwp_info *lwp = get_thread_lwp (thread); - int pid = * (int *) args; + struct thread_info *thread = get_lwp_thread (lwp); int sig; - - if (ptid_get_pid (entry->id) != pid) - return 0; + int lwpid; /* If there is a pending SIGSTOP, get rid of it. */ if (lwp->stop_expected) @@ -1469,22 +1468,94 @@ linux_detach_one_lwp (struct inferior_list_entry *entry, void *args) lwp->stop_expected = 0; } - /* Flush any pending changes to the process's registers. */ - regcache_invalidate_thread (thread); - /* Pass on any pending signal for this thread. */ sig = get_detach_signal (thread); - /* Finally, let it resume. */ - if (the_low_target.prepare_to_resume != NULL) - the_low_target.prepare_to_resume (lwp); - if (ptrace (PTRACE_DETACH, lwpid_of (thread), (PTRACE_TYPE_ARG3) 0, + /* Preparing to resume may try to write registers, and fail if the + lwp is zombie. If that happens, ignore the error. We'll handle + it below, when detach fails with ESRCH. */ + TRY + { + /* Flush any pending changes to the process's registers. */ + regcache_invalidate_thread (thread); + + /* Finally, let it resume. */ + if (the_low_target.prepare_to_resume != NULL) + the_low_target.prepare_to_resume (lwp); + } + CATCH (ex, RETURN_MASK_ERROR) + { + if (!check_ptrace_stopped_lwp_gone (lwp)) + throw_exception (ex); + } + END_CATCH + + lwpid = lwpid_of (thread); + if (ptrace (PTRACE_DETACH, lwpid, (PTRACE_TYPE_ARG3) 0, (PTRACE_TYPE_ARG4) (long) sig) < 0) - error (_("Can't detach %s: %s"), - target_pid_to_str (ptid_of (thread)), - strerror (errno)); + { + int save_errno = errno; + + /* We know the thread exists, so ESRCH must mean the lwp is + zombie. This can happen if one of the already-detached + threads exits the whole thread group. In that case we're + still attached, and must reap the lwp. */ + if (save_errno == ESRCH) + { + int ret, status; + + ret = my_waitpid (lwpid, &status, __WALL); + if (ret == -1) + { + warning (_("Couldn't reap LWP %d while detaching: %s"), + lwpid, strerror (errno)); + } + else if (!WIFEXITED (status) && !WIFSIGNALED (status)) + { + warning (_("Reaping LWP %d while detaching " + "returned unexpected status 0x%x"), + lwpid, status); + } + } + else + { + error (_("Can't detach %s: %s"), + target_pid_to_str (ptid_of (thread)), + strerror (save_errno)); + } + } + else if (debug_threads) + { + debug_printf ("PTRACE_DETACH (%s, %s, 0) (OK)\n", + target_pid_to_str (ptid_of (thread)), + strsignal (sig)); + } delete_lwp (lwp); +} + +/* Callback for find_inferior. Detaches from non-leader threads of a + given process. */ + +static int +linux_detach_lwp_callback (struct inferior_list_entry *entry, void *args) +{ + struct thread_info *thread = (struct thread_info *) entry; + struct lwp_info *lwp = get_thread_lwp (thread); + int pid = *(int *) args; + int lwpid = lwpid_of (thread); + + /* Skip other processes. */ + if (ptid_get_pid (entry->id) != pid) + return 0; + + /* We don't actually detach from the thread group leader just yet. + If the thread group exits, we must reap the zombie clone lwps + before we're able to reap the leader. */ + if (ptid_get_pid (entry->id) == lwpid) + return 0; + + linux_detach_one_lwp (lwp); return 0; } @@ -1492,6 +1563,7 @@ static int linux_detach (int pid) { struct process_info *process; + struct lwp_info *main_lwp; process = find_process_pid (pid); if (process == NULL) @@ -1515,7 +1587,13 @@ linux_detach (int pid) /* Stabilize threads (move out of jump pads). */ stabilize_threads (); - find_inferior (&all_threads, linux_detach_one_lwp, &pid); + /* Detach from the clone lwps first. If the thread group exits just + while we're detaching, we must reap the clone lwps before we're + able to reap the leader. */ + find_inferior (&all_threads, linux_detach_lwp_callback, &pid); + + main_lwp = find_lwp_pid (pid_to_ptid (pid)); + linux_detach_one_lwp (main_lwp); the_target->mourn (process); diff --git a/gdb/inf-ptrace.c b/gdb/inf-ptrace.c index dd11043..0896cff 100644 --- a/gdb/inf-ptrace.c +++ b/gdb/inf-ptrace.c @@ -257,6 +257,16 @@ inf_ptrace_detach (struct target_ops *ops, const char *args, int from_tty) error (_("This system does not support detaching from a process")); #endif + inf_ptrace_detach_success (ops); +} + +/* See inf-ptrace.h. */ + +void +inf_ptrace_detach_success (struct target_ops *ops) +{ + pid_t pid = ptid_get_pid (inferior_ptid); + inferior_ptid = null_ptid; detach_inferior (pid); diff --git a/gdb/inf-ptrace.h b/gdb/inf-ptrace.h index 0a26720..f1fc111 100644 --- a/gdb/inf-ptrace.h +++ b/gdb/inf-ptrace.h @@ -38,4 +38,8 @@ extern struct target_ops * extern pid_t get_ptrace_pid (ptid_t); + +/* Cleanup the inferior after a successful ptrace detach. */ +extern void inf_ptrace_detach_success (struct target_ops *ops); + #endif diff --git a/gdb/linux-nat.c b/gdb/linux-nat.c index e6d525f..7ab0eee 100644 --- a/gdb/linux-nat.c +++ b/gdb/linux-nat.c @@ -820,6 +820,7 @@ linux_nat_pass_signals (struct target_ops *self, static int stop_wait_callback (struct lwp_info *lp, void *data); static char *linux_child_pid_to_exec_file (struct target_ops *self, int pid); static int resume_stopped_resumed_lwps (struct lwp_info *lp, void *data); +static int check_ptrace_stopped_lwp_gone (struct lwp_info *lp); @@ -1295,9 +1296,13 @@ linux_nat_attach (struct target_ops *ops, const char *args, int from_tty) target_async (1); } -/* Get pending status of LP. */ +/* Get pending signal of THREAD as a host signal number, for detaching + purposes. This is the signal the thread last stopped for, which we + need to deliver to the thread when detaching, otherwise, it'd be + suppressed/lost. */ + static int -get_pending_status (struct lwp_info *lp, int *status) +get_detach_signal (struct lwp_info *lp) { enum gdb_signal signo = GDB_SIGNAL_0; @@ -1350,8 +1355,6 @@ get_pending_status (struct lwp_info *lp, int *status) } } - *status = 0; - if (signo == GDB_SIGNAL_0) { if (debug_linux_nat) @@ -1370,21 +1373,28 @@ get_pending_status (struct lwp_info *lp, int *status) } else { - *status = W_STOPCODE (gdb_signal_to_host (signo)); - if (debug_linux_nat) fprintf_unfiltered (gdb_stdlog, "GPT: lwp %s has pending signal %s\n", target_pid_to_str (lp->ptid), gdb_signal_to_string (signo)); + + return gdb_signal_to_host (signo); } return 0; } -static int -detach_callback (struct lwp_info *lp, void *data) +/* Detach from LP. If SIGNO_P is non-NULL, then it points to the + signal number that should be passed to the LWP when detaching. + Otherwise pass any pending signal the LWP may have, if any. */ + +static void +detach_one_lwp (struct lwp_info *lp, int *signo_p) { + int lwpid = ptid_get_lwp (lp->ptid); + int signo; + gdb_assert (lp->status == 0 || WIFSTOPPED (lp->status)); if (debug_linux_nat && lp->status) @@ -1400,36 +1410,83 @@ detach_callback (struct lwp_info *lp, void *data) "DC: Sending SIGCONT to %s\n", target_pid_to_str (lp->ptid)); - kill_lwp (ptid_get_lwp (lp->ptid), SIGCONT); + kill_lwp (lwpid, SIGCONT); lp->signalled = 0; } - /* We don't actually detach from the LWP that has an id equal to the - overall process id just yet. */ - if (ptid_get_lwp (lp->ptid) != ptid_get_pid (lp->ptid)) + if (signo_p == NULL) { - int status = 0; - /* Pass on any pending signal for this LWP. */ - get_pending_status (lp, &status); + signo = get_detach_signal (lp); + } + else + signo = *signo_p; + /* Preparing to resume may try to write registers, and fail if the + lwp is zombie. If that happens, ignore the error. We'll handle + it below, when detach fails with ESRCH. */ + TRY + { if (linux_nat_prepare_to_resume != NULL) linux_nat_prepare_to_resume (lp); - errno = 0; - if (ptrace (PTRACE_DETACH, ptid_get_lwp (lp->ptid), 0, - WSTOPSIG (status)) < 0) - error (_("Can't detach %s: %s"), target_pid_to_str (lp->ptid), - safe_strerror (errno)); + } + CATCH (ex, RETURN_MASK_ERROR) + { + if (!check_ptrace_stopped_lwp_gone (lp)) + throw_exception (ex); + } + END_CATCH - if (debug_linux_nat) - fprintf_unfiltered (gdb_stdlog, - "PTRACE_DETACH (%s, %s, 0) (OK)\n", - target_pid_to_str (lp->ptid), - strsignal (WSTOPSIG (status))); + if (ptrace (PTRACE_DETACH, lwpid, 0, signo) < 0) + { + int save_errno = errno; + + /* We know the thread exists, so ESRCH must mean the lwp is + zombie. This can happen if one of the already-detached + threads exits the whole thread group. In that case we're + still attached, and must reap the lwp. */ + if (save_errno == ESRCH) + { + int ret, status; - delete_lwp (lp->ptid); + ret = my_waitpid (lwpid, &status, __WALL); + if (ret == -1) + { + warning (_("Couldn't reap LWP %d while detaching: %s"), + lwpid, strerror (errno)); + } + else if (!WIFEXITED (status) && !WIFSIGNALED (status)) + { + warning (_("Reaping LWP %d while detaching " + "returned unexpected status 0x%x"), + lwpid, status); + } + } + else + { + error (_("Can't detach %s: %s"), target_pid_to_str (lp->ptid), + safe_strerror (save_errno)); + } + } + else if (debug_linux_nat) + { + fprintf_unfiltered (gdb_stdlog, + "PTRACE_DETACH (%s, %s, 0) (OK)\n", + target_pid_to_str (lp->ptid), + strsignal (signo)); } + delete_lwp (lp->ptid); +} + +static int +detach_callback (struct lwp_info *lp, void *data) +{ + /* We don't actually detach from the thread group leader just yet. + If the thread group exits, we must reap the zombie clone lwps + before we're able to reap the leader. */ + if (ptid_get_lwp (lp->ptid) != ptid_get_pid (lp->ptid)) + detach_one_lwp (lp, NULL); return 0; } @@ -1437,7 +1494,6 @@ static void linux_nat_detach (struct target_ops *ops, const char *args, int from_tty) { int pid; - int status; struct lwp_info *main_lwp; pid = ptid_get_pid (inferior_ptid); @@ -1459,29 +1515,6 @@ linux_nat_detach (struct target_ops *ops, const char *args, int from_tty) main_lwp = find_lwp_pid (pid_to_ptid (pid)); - /* Pass on any pending signal for the last LWP. */ - if ((args == NULL || *args == '\0') - && get_pending_status (main_lwp, &status) != -1 - && WIFSTOPPED (status)) - { - char *tem; - - /* Put the signal number in ARGS so that inf_ptrace_detach will - pass it along with PTRACE_DETACH. */ - tem = (char *) alloca (8); - xsnprintf (tem, 8, "%d", (int) WSTOPSIG (status)); - args = tem; - if (debug_linux_nat) - fprintf_unfiltered (gdb_stdlog, - "LND: Sending signal %s to %s\n", - args, - target_pid_to_str (main_lwp->ptid)); - } - - if (linux_nat_prepare_to_resume != NULL) - linux_nat_prepare_to_resume (main_lwp); - delete_lwp (main_lwp->ptid); - if (forks_exist_p ()) { /* Multi-fork case. The current inferior_ptid is being detached @@ -1491,7 +1524,24 @@ linux_nat_detach (struct target_ops *ops, const char *args, int from_tty) linux_fork_detach (args, from_tty); } else - linux_ops->to_detach (ops, args, from_tty); + { + int signo; + + target_announce_detach (from_tty); + + /* Pass on any pending signal for the last LWP, unless the user + requested detaching with a different signal (most likely 0, + meaning, discard the signal). */ + if (args != NULL) + signo = atoi (args); + else + signo = get_detach_signal (main_lwp); + + detach_one_lwp (main_lwp, &signo); + + inf_ptrace_detach_success (ops); + } + delete_lwp (main_lwp->ptid); } /* Resume execution of the inferior process. If STEP is nonzero, diff --git a/gdb/testsuite/gdb.threads/process-dies-while-detaching.c b/gdb/testsuite/gdb.threads/process-dies-while-detaching.c new file mode 100644 index 0000000..a28e804 --- /dev/null +++ b/gdb/testsuite/gdb.threads/process-dies-while-detaching.c @@ -0,0 +1,116 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2016 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 . */ + +#include +#include +#include +#include +#include +#include +#include + +/* This barrier ensures we only reach the initial breakpoint after all + threads have started. */ +pthread_barrier_t start_threads_barrier; + +/* Many threads in order to be fairly sure the process exits while GDB + is detaching from each thread in the process, on targets that need + to detach from each thread individually. */ +#define NTHREADS 256 + +/* GDB sets a watchpoint here. */ +int globalvar = 1; + +/* GDB reads this. */ +int mypid; + +/* Threads' entry point. */ + +void * +thread_function (void *arg) +{ + pthread_barrier_wait (&start_threads_barrier); + _exit (0); +} + +/* The fork child's entry point. */ + +void +child_function (void) +{ + pthread_t threads[NTHREADS]; + int i; + + pthread_barrier_init (&start_threads_barrier, NULL, NTHREADS + 1); + + for (i = 0; i < NTHREADS; i++) + pthread_create (&threads[i], NULL, thread_function, NULL); + pthread_barrier_wait (&start_threads_barrier); + + exit (0); +} + +/* This is defined by the .exp file if testing the multi-process + variant. */ +#ifdef MULTIPROCESS + +/* The fork parent's entry point. */ + +void +parent_function (pid_t child) +{ + int status, ret; + + alarm (300); + + ret = waitpid (child, &status, 0); + if (ret == -1) + exit (1); + else if (!WIFEXITED (status)) + exit (2); + else + { + printf ("exited, status=%d\n", WEXITSTATUS (status)); + exit (0); + } +} + +#endif + +int +main (void) +{ +#ifdef MULTIPROCESS + pid_t child; + + child = fork (); + if (child == -1) + return 1; +#endif + + mypid = getpid (); + +#ifdef MULTIPROCESS + if (child != 0) + parent_function (child); + else +#endif + child_function (); + + /* Not reached. */ + abort (); +} diff --git a/gdb/testsuite/gdb.threads/process-dies-while-detaching.exp b/gdb/testsuite/gdb.threads/process-dies-while-detaching.exp new file mode 100644 index 0000000..1ab3820 --- /dev/null +++ b/gdb/testsuite/gdb.threads/process-dies-while-detaching.exp @@ -0,0 +1,327 @@ +# Copyright 2016 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 . + +# This test spawns a few threads that immediately exit the whole +# process. On targets where the debugger needs to detach from each +# thread individually (such as on the Linux kernel), the debugger must +# handle the case of the process exiting while the detach is ongoing. +# +# Similarly, the process can also be killed from outside the debugger +# (e.g., with SIGKILL), _before_ the user requests a detach. The +# debugger must likewise detach gracefully. +# +# The testcase actually builds two variants of the test program: +# single-process, and multi-process. In the multi-process variant, +# the test program forks, and it's the fork child that spawns threads +# that exit just while the process is being detached from. The fork +# parent waits for its child to exit, so if GDB fails to detach from +# the child correctly, the parent hangs. Because continuing the +# parent can mask failure to detach from the child correctly (e.g., +# due to waitpid(-1,...) calls deep in the target layers managing to +# reap the child), we try immediately detaching from the parent too, +# and observing whether the parent exits via standard output. +# +# Normally, if testing with "target remote" against gdbserver, then +# after detaching from all attached processes, gdbserver exits. +# However, when gdbserver detaches from a process that is its own +# direct child, gdbserver does not exit immediately. Instead it +# "joins" (waits for) the child, only exiting when the child itself +# exits too. Thus, on Linux, if gdbserver fails to detach from the +# zombie child's threads correctly (or rather, reap them), it'll hang, +# because the leader thread will only return an exit status after all +# threads are reaped. We test that as well. + +standard_testfile + +# Test that GDBserver exits. + +proc test_server_exit {} { + global server_spawn_id + + set test "server exits" + gdb_expect { + -i $server_spawn_id + eof { + pass $test + wait -i $server_spawn_id + unset server_spawn_id + } + timeout { + fail "$test (timeout)" + } + } +} + +# If RESULT is not zero, make the caller return. + +proc return_if_fail { result } { + if {$result != 0} { + return -code return + } +} + +# Detach from a process, and ensure that it exits after detaching. +# This relies on inferior I/O. + +proc detach_and_expect_exit {test} { + global decimal + global gdb_spawn_id + global inferior_spawn_id + global gdb_prompt + + return_if_fail [gdb_test_multiple "detach" $test { + -re "Detaching from .*, process $decimal" { + } + }] + + set saw_prompt 0 + set saw_inf_exit 0 + while { !$saw_prompt && ! $saw_inf_exit } { + # We don't know what order the interesting things will arrive in. + # Using a pattern of the form 'x|y|z' instead of -re x ... -re y + # ... -re z ensures that expect always chooses the match that + # occurs leftmost in the input, and not the pattern appearing + # first in the script that occurs anywhere in the input, so that + # we don't skip anything. + return_if_fail [gdb_test_multiple "" $test { + -i "$inferior_spawn_id $gdb_spawn_id" + -re "(exited, status=0)|($gdb_prompt )" { + if {[info exists expect_out(1,string)]} { + verbose -log "saw inferior exit" + set saw_inf_exit 1 + } elseif {[info exists expect_out(2,string)]} { + verbose -log "saw prompt" + set saw_prompt 1 + } + array unset expect_out + } + }] + } + + pass $test +} + +# Run to _exit in the child. + +proc continue_to_exit_bp {} { + gdb_breakpoint "_exit" temporary + gdb_continue_to_breakpoint "_exit" ".*_exit.*" +} + +# If testing single-process, simply detach from the process. +# +# If testing multi-process, first detach from the child, then detach +# from the parent and confirm that the parent exits, thus unsuring +# we've detached from the child successfully, as the parent hangs in +# its waitpid call otherwise. +# +# If connected with "target remote", make sure gdbserver exits. +# +# CMD indicates what to do with the parent after detaching the child. +# Can be either "detach" to detach, or "continue", to continue to +# exit. If "continue", then CONTINUE_RE is the regexp to expect. +# Defaults to normal exit output. +# +proc do_detach {multi_process cmd {continue_re ""}} { + global decimal + global server_spawn_id + + if {$continue_re == ""} { + set continue_re "exited normally.*" + } + + set is_remote [expr {[target_info exists gdb_protocol] + && [target_info gdb_protocol] == "remote"}] + + if {$multi_process} { + gdb_test "detach" "Detaching from .*, process $decimal" \ + "detach child" + + gdb_test "inferior 1" "\[Switching to inferior $decimal\].*" \ + "switch to parent" + + if {$cmd == "detach"} { + # Make sure that detach works and that the parent process + # exits cleanly. + detach_and_expect_exit "detach parent" + } elseif {$cmd == "continue"} { + # Make sure that continuing works and that the parent process + # exits cleanly. + gdb_test "continue" $continue_re + } else { + perror "unhandled command: $mode: $cmd" + } + } else { + if $is_remote { + set extra "\r\nEnding remote debugging\." + } else { + set extra "" + } + if {$cmd == "detach"} { + gdb_test "detach" "Detaching from .*, process $decimal$extra" + } elseif {$cmd == "continue"} { + gdb_test "continue" $continue_re + } else { + perror "unhandled command: $mode: $cmd" + } + } + + # When connected in "target remote" mode, the server should exit + # when there are no processes left to debug. + if { $is_remote && [info exists server_spawn_id]} { + test_server_exit + } +} + +# Test detaching from a process that dies just while GDB is detaching. + +proc test_detach {multi_process cmd} { + with_test_prefix "detach" { + global binfile + + clean_restart ${binfile} + + if ![runto_main] { + fail "Can't run to main" + return -1 + } + + if {$multi_process} { + gdb_test_no_output "set detach-on-fork off" + gdb_test_no_output "set follow-fork-mode child" + } + + # Run to _exit in the child. + continue_to_exit_bp + + do_detach $multi_process $cmd + } +} + +# Same as test_detach, except set a watchpoint before detaching. + +proc test_detach_watch {multi_process cmd} { + with_test_prefix "watchpoint" { + global binfile decimal + + clean_restart ${binfile} + + if ![runto_main] { + fail "Can't run to main" + return -1 + } + + if {$multi_process} { + gdb_test_no_output "set detach-on-fork off" + gdb_test_no_output "set follow-fork-mode child" + + gdb_breakpoint "child_function" temporary + gdb_continue_to_breakpoint "child_function" ".*" + } + + # Set a watchpoint in the child. + gdb_test "watch globalvar" ".* watchpoint $decimal: globalvar" + + # Continue to the _exit breakpoint. This arms the watchpoint + # registers in all threads. Detaching will thus need to clear + # them out, and handle the case of the thread disappearing + # while doing that (on targets that need to detach from each + # thread individually). + continue_to_exit_bp + + do_detach $multi_process $cmd + } +} + +# Test detaching from a process that dies _before_ GDB starts +# detaching. + +proc test_detach_killed_outside {multi_process cmd} { + with_test_prefix "killed outside" { + global binfile + + clean_restart ${binfile} + + if ![runto_main] { + fail "Can't run to main" + return -1 + } + + gdb_test_no_output "set breakpoint always-inserted on" + + if {$multi_process} { + gdb_test_no_output "set detach-on-fork off" + gdb_test_no_output "set follow-fork-mode child" + } + + # Run to _exit in the child. + continue_to_exit_bp + + set childpid [get_integer_valueof "mypid" -1] + if { $childpid == -1 } { + untested "failed to extract child pid" + return -1 + } + + remote_exec target "kill -9 ${childpid}" + + # Give it some time to die. + sleep 2 + + if {$multi_process} { + set continue_re "exited with code 02.*" + } else { + set continue_re "terminated with signal SIGKILL.*" + } + do_detach $multi_process $cmd $continue_re + } +} + +# The test proper. MULTI_PROCESS is true if testing the multi-process +# variant. + +proc do_test {multi_process cmd} { + global testfile srcfile binfile + + if {$multi_process && $cmd == "detach" + && [target_info exists gdb,noinferiorio]} { + # This requires inferior I/O to tell whether both the parent + # and child exit successfully. + return + } + + set binfile [standard_output_file ${testfile}-$multi_process-$cmd] + set options {debug pthreads} + if {$multi_process} { + lappend options "additional_flags=-DMULTIPROCESS" + } + + if {[build_executable "failed to build" \ + $testfile-$multi_process-$cmd $srcfile $options] == -1} { + return -1 + } + + test_detach $multi_process $cmd + test_detach_watch $multi_process $cmd + test_detach_killed_outside $multi_process $cmd +} + +foreach multi_process {0 1} { + set mode [expr {$multi_process ? "single-process" : "multi-process"}] + foreach cmd {"detach" "continue"} { + with_test_prefix "$mode: $cmd" { + do_test $multi_process $cmd + } + } +}