diff options
author | Pedro Alves <palves@redhat.com> | 2016-07-01 11:16:33 +0100 |
---|---|---|
committer | Pedro Alves <palves@redhat.com> | 2016-07-01 11:27:06 +0100 |
commit | ced2dffbf17bc661e959da1e39411d706ade9f77 (patch) | |
tree | 093cb8ad73368e53d7b30fec408b7ba1bb53a729 /gdb/testsuite/gdb.threads | |
parent | 630008884535a5b26828325e48e729034c110536 (diff) | |
download | gdb-ced2dffbf17bc661e959da1e39411d706ade9f77.zip gdb-ced2dffbf17bc661e959da1e39411d706ade9f77.tar.gz gdb-ced2dffbf17bc661e959da1e39411d706ade9f77.tar.bz2 |
Fix failure to detach if process exits while detaching on Linux
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:
2016-07-01 Pedro Alves <palves@redhat.com>
Antoine Tremblay <antoine.tremblay@ericsson.com>
* 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:
2016-07-01 Pedro Alves <palves@redhat.com>
Antoine Tremblay <antoine.tremblay@ericsson.com>
* 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:
2016-07-01 Pedro Alves <palves@redhat.com>
Antoine Tremblay <antoine.tremblay@ericsson.com>
* gdb.threads/process-dies-while-detaching.c: New file.
* gdb.threads/process-dies-while-detaching.exp: New file.
Diffstat (limited to 'gdb/testsuite/gdb.threads')
-rw-r--r-- | gdb/testsuite/gdb.threads/process-dies-while-detaching.c | 116 | ||||
-rw-r--r-- | gdb/testsuite/gdb.threads/process-dies-while-detaching.exp | 327 |
2 files changed, 443 insertions, 0 deletions
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 <http://www.gnu.org/licenses/>. */ + +#include <pthread.h> +#include <unistd.h> +#include <stdlib.h> +#include <stdio.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <assert.h> + +/* 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..8dc1aec --- /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 <http://www.gnu.org/licenses/>. + +# 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 ensuring +# 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: $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: $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 + } + } +} |