aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gdb/ChangeLog6
-rw-r--r--gdb/linux-nat.c14
-rw-r--r--gdb/testsuite/ChangeLog7
-rw-r--r--gdb/testsuite/gdb.threads/attach-slow-waitpid.c77
-rw-r--r--gdb/testsuite/gdb.threads/attach-slow-waitpid.exp100
-rw-r--r--gdb/testsuite/gdb.threads/slow-waitpid.c342
6 files changed, 542 insertions, 4 deletions
diff --git a/gdb/ChangeLog b/gdb/ChangeLog
index 44d213e..4886f1c 100644
--- a/gdb/ChangeLog
+++ b/gdb/ChangeLog
@@ -1,3 +1,9 @@
+2018-06-16 Andrew Burgess <andrew.burgess@embecosm.com>
+ Richard Bunt <Richard.Bunt@arm.com>
+
+ * linux-nat.c (stop_wait_callback): Don't discard SIGSTOP if it
+ was requested by GDB.
+
2018-06-15 Tom de Vries <tdevries@suse.de>
* MAINTAINERS (Write After Approval): Add Tom de Vries.
diff --git a/gdb/linux-nat.c b/gdb/linux-nat.c
index 445b59f..a07f41c 100644
--- a/gdb/linux-nat.c
+++ b/gdb/linux-nat.c
@@ -2527,17 +2527,23 @@ stop_wait_callback (struct lwp_info *lp, void *data)
}
else
{
- /* We caught the SIGSTOP that we intended to catch, so
- there's no SIGSTOP pending. */
+ /* We caught the SIGSTOP that we intended to catch. */
if (debug_linux_nat)
fprintf_unfiltered (gdb_stdlog,
"SWC: Expected SIGSTOP caught for %s.\n",
target_pid_to_str (lp->ptid));
- /* Reset SIGNALLED only after the stop_wait_callback call
- above as it does gdb_assert on SIGNALLED. */
lp->signalled = 0;
+
+ /* If we are waiting for this stop so we can report the thread
+ stopped then we need to record this status. Otherwise, we can
+ now discard this stop event. */
+ if (lp->last_resume_kind == resume_stop)
+ {
+ lp->status = status;
+ save_stop_reason (lp);
+ }
}
}
diff --git a/gdb/testsuite/ChangeLog b/gdb/testsuite/ChangeLog
index 6d1f5f7..c7fee80 100644
--- a/gdb/testsuite/ChangeLog
+++ b/gdb/testsuite/ChangeLog
@@ -1,3 +1,10 @@
+2018-06-16 Andrew Burgess <andrew.burgess@embecosm.com>
+ Richard Bunt <Richard.Bunt@arm.com>
+
+ * gdb.threads/attach-slow-waitpid.c: New file.
+ * gdb.threads/attach-slow-waitpid.exp: New file.
+ * gdb.threads/slow-waitpid.c: New file.
+
2018-06-14 Pedro Alves <palves@redhat.com>
* gdb.base/fork-running-state.c: Include <errno.h>.
diff --git a/gdb/testsuite/gdb.threads/attach-slow-waitpid.c b/gdb/testsuite/gdb.threads/attach-slow-waitpid.c
new file mode 100644
index 0000000..06e99ab
--- /dev/null
+++ b/gdb/testsuite/gdb.threads/attach-slow-waitpid.c
@@ -0,0 +1,77 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2018 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 <stdio.h>
+#include <stdlib.h>
+#include <assert.h>
+#define NUM_THREADS 4
+
+/* Crude spin lock. Threads all spin until this is set to 0. */
+int go = 1;
+
+/* Thread function, just spin until GO is set to 0. */
+void *
+perform_work (void *argument)
+{
+ /* Cast to volatile to ensure that ARGUMENT is loaded each time around
+ the loop. */
+ while (*((volatile int*) argument))
+ {
+ /* Nothing. */
+ }
+ return NULL;
+}
+
+/* The spin loop for the main thread. */
+void
+function (void)
+{
+ (void) perform_work (&go);
+ printf ("Finished from function\n");
+}
+
+/* Main program, create some threads which all spin waiting for GO to be
+ set to 0. */
+int
+main (void)
+{
+ pthread_t threads[NUM_THREADS];
+ int result_code;
+ unsigned index;
+
+ /* Create some threads. */
+ for (index = 0; index < NUM_THREADS; ++index)
+ {
+ printf ("In main: creating thread %d\n", index);
+ result_code = pthread_create (&threads[index], NULL, perform_work, &go);
+ assert (!result_code);
+ }
+
+ function ();
+
+ /* Wait for each thread to complete. */
+ for (index = 0; index < NUM_THREADS; ++index)
+ {
+ /* Block until thread INDEX completes. */
+ result_code = pthread_join (threads[index], NULL);
+ assert (!result_code);
+ printf ("In main: thread %d has completed\n", index);
+ }
+ printf ("In main: All threads completed successfully\n");
+ return 0;
+}
diff --git a/gdb/testsuite/gdb.threads/attach-slow-waitpid.exp b/gdb/testsuite/gdb.threads/attach-slow-waitpid.exp
new file mode 100644
index 0000000..095c193
--- /dev/null
+++ b/gdb/testsuite/gdb.threads/attach-slow-waitpid.exp
@@ -0,0 +1,100 @@
+# Copyright 2018 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 script tries to expose a bug in some of the uses of
+# waitpid in the Linux native support within GDB. The problem was
+# spotted on systems which were heavily loaded when attaching to
+# threaded test programs. What happened was that during the initial
+# attach, the loop of waitpid calls that normally received the stop
+# events from each of the threads in the inferior was not receiving a
+# stop event for some threads (the kernel just hadn't sent the stop
+# event yet).
+#
+# GDB would then trigger a call to stop_all_threads which would
+# continue to wait for all of the outstanding threads to stop, when
+# the outstanding stop events finally arrived GDB would then
+# (incorrectly) discard the stop event, resume the thread, and
+# continue to wait for the thread to stop.... which it now never
+# would.
+#
+# In order to try and expose this issue reliably, this test preloads a
+# library that intercepts waitpid calls. All waitpid calls targeting
+# pid -1 with the WNOHANG flag are rate limited so that only 1 per
+# second can complete. Additional calls are forced to return 0
+# indicating no event waiting. This is enough to trigger the bug
+# during the attach phase.
+
+# This test only works on Linux
+if { ![isnative] || [is_remote host] || [use_gdb_stub]
+ || ![istarget *-linux*] } {
+ continue
+}
+
+standard_testfile
+
+set libfile slow-waitpid
+set libsrc "${srcdir}/${subdir}/${libfile}.c"
+set libobj [standard_output_file ${libfile}.so]
+
+with_test_prefix "compile preload library" {
+ # Compile the preload library. We only get away with this as we
+ # limit this test to running when ISNATIVE is true.
+ if { [gdb_compile_shlib_pthreads \
+ $libsrc $libobj {debug}] != "" } then {
+ return -1
+ }
+}
+
+with_test_prefix "compile test executable" {
+ # Compile the test program
+ if { [gdb_compile_pthreads \
+ "${srcdir}/${subdir}/${srcfile}" "${binfile}" \
+ executable {debug}] != "" } {
+ return -1
+ }
+}
+
+# Spawn GDB with LIB preloaded with LD_PRELOAD.
+
+proc gdb_spawn_with_ld_preload {lib} {
+ global env
+
+ save_vars { env(LD_PRELOAD) } {
+ if { ![info exists env(LD_PRELOAD) ]
+ || $env(LD_PRELOAD) == "" } {
+ set env(LD_PRELOAD) "$lib"
+ } else {
+ append env(LD_PRELOAD) ":$lib"
+ }
+
+ gdb_start
+ }
+}
+
+# Run test program in the background.
+set test_spawn_id [spawn_wait_for_attach $binfile]
+set testpid [spawn_id_get_pid $test_spawn_id]
+
+# Start GDB with preload library in place.
+gdb_spawn_with_ld_preload $libobj
+
+# Load binary, and attach to running program.
+gdb_load ${binfile}
+gdb_test "attach $testpid" "Attaching to program.*" "attach to target"
+
+gdb_exit
+
+# Kill of test program.
+kill_wait_spawned_process $test_spawn_id
diff --git a/gdb/testsuite/gdb.threads/slow-waitpid.c b/gdb/testsuite/gdb.threads/slow-waitpid.c
new file mode 100644
index 0000000..93304ef
--- /dev/null
+++ b/gdb/testsuite/gdb.threads/slow-waitpid.c
@@ -0,0 +1,342 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2018 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 file contains a library that can be preloaded into GDB on Linux
+ using the LD_PRELOAD technique.
+
+ The library intercepts calls to WAITPID and SIGSUSPEND in order to
+ simulate the behaviour of a heavily loaded kernel.
+
+ When GDB wants to stop all threads in an inferior each thread is sent a
+ SIGSTOP, GDB will then wait for the signal to be received by the thread
+ with a waitpid call.
+
+ If the kernel is slow in either delivering the signal, or making the
+ result available to the waitpid call then GDB will enter a sigsuspend
+ call in order to wait for the inferior threads to change state, this is
+ signalled to GDB with a SIGCHLD.
+
+ A bug in GDB meant that in some cases we would deadlock during this
+ process. This was rarely seen as the kernel is usually quick at
+ delivering signals and making the results available to waitpid, so quick
+ that GDB would gather the statuses from all inferior threads in the
+ original pass.
+
+ The idea in this library is to rate limit calls to waitpid (where pid is
+ -1 and the WNOHANG option is set) so that only 1 per second can return
+ an answer. Any additional calls will report that no threads are
+ currently ready. This should match the behaviour we see on a slow
+ kernel.
+
+ However, given that usually when using this library, the kernel does
+ have the waitpid result ready this means that the kernel will never send
+ GDB a SIGCHLD. This means that when GDB enters sigsuspend it will block
+ forever. Alternatively, if GDB enters its polling loop the lack of
+ SIGCHLD means that we will never see an event on the child threads. To
+ resolve these problems the library intercepts calls to sigsuspend and
+ forces the call to exit if there is a pending waitpid result. Also,
+ when we know that there's a waitpid result that we've ignored, we create
+ a new thread which, after a short delay, will send GDB a SIGCHLD. */
+
+#define _GNU_SOURCE
+
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <sys/time.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <dlfcn.h>
+#include <string.h>
+#include <stdarg.h>
+#include <signal.h>
+#include <errno.h>
+#include <pthread.h>
+#include <unistd.h>
+
+/* Logging. */
+
+static void
+log_msg (const char *fmt, ...)
+{
+#ifdef LOGGING
+ va_list ap;
+
+ va_start (ap, fmt);
+ vfprintf (stderr, fmt, ap);
+ va_end (ap);
+#endif /* LOGGING */
+}
+
+/* Error handling, message and exit. */
+
+static void
+error (const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start (ap, fmt);
+ vfprintf (stderr, fmt, ap);
+ va_end (ap);
+
+ exit (EXIT_FAILURE);
+}
+
+/* Cache the result of a waitpid call that has not been reported back to
+ GDB yet. We only ever cache a single result. Once we have a result
+ cached then later calls to waitpid with the WNOHANG option will return a
+ result of 0. */
+
+static struct
+{
+ /* Flag to indicate when we have a result cached. */
+ int cached_p;
+
+ /* The cached result fields from a waitpid call. */
+ pid_t pid;
+ int wstatus;
+} cached_wait_status;
+
+/* Lock to hold when modifying SIGNAL_THREAD_ACTIVE_P. */
+
+static pthread_mutex_t thread_creation_lock_obj = PTHREAD_MUTEX_INITIALIZER;
+#define thread_creation_lock (&thread_creation_lock_obj)
+
+/* This flag is only modified while holding the THREAD_CREATION_LOCK mutex.
+ When this flag is true then there is a signal thread alive that will be
+ sending a SIGCHLD at some point in the future. */
+
+static int signal_thread_active_p;
+
+/* When we last allowed a waitpid to complete. */
+
+static struct timeval last_waitpid_time = { 0, 0 };
+
+/* The number of seconds that must elapse between calls to waitpid where
+ the pid is -1 and the WNOHANG option is set. If calls occur faster than
+ this then we force a result of 0 to be returned from waitpid. */
+
+#define WAITPID_MIN_TIME (1)
+
+/* Return true (non-zero) if we should skip this call to waitpid, or false
+ (zero) if this waitpid call should be handled with a call to the "real"
+ waitpid function. Allows 1 waitpid call per second. */
+
+static int
+should_skip_waitpid (void)
+{
+ struct timeval *tv = &last_waitpid_time;
+ if (tv->tv_sec == 0)
+ {
+ if (gettimeofday (tv, NULL) < 0)
+ error ("error: gettimeofday failed\n");
+ return 0; /* Don't skip. */
+ }
+ else
+ {
+ struct timeval new_tv;
+
+ if (gettimeofday (&new_tv, NULL) < 0)
+ error ("error: gettimeofday failed\n");
+
+ if ((new_tv.tv_sec - tv->tv_sec) < WAITPID_MIN_TIME)
+ return 1; /* Skip. */
+
+ *tv = new_tv;
+ }
+
+ /* Don't skip. */
+ return 0;
+}
+
+/* Perform a real waitpid call. */
+
+static pid_t
+real_waitpid (pid_t pid, int *wstatus, int options)
+{
+ typedef pid_t (*fptr_t) (pid_t, int *, int);
+ static fptr_t real_func = NULL;
+
+ if (real_func == NULL)
+ {
+ real_func = dlsym (RTLD_NEXT, "waitpid");
+ if (real_func == NULL)
+ error ("error: failed to find real waitpid\n");
+ }
+
+ return (*real_func) (pid, wstatus, options);
+}
+
+/* Thread worker created when we cache a waitpid result. Delays for a
+ short period of time and then sends SIGCHLD to the GDB process. This
+ should trigger GDB to call waitpid again, at which point we will make
+ the cached waitpid result available. */
+
+static void*
+send_sigchld_thread (void *arg)
+{
+ /* Delay one second longer than WAITPID_MIN_TIME so that there can be no
+ chance that a call to SHOULD_SKIP_WAITPID will return true once the
+ SIGCHLD is delivered and handled. */
+ sleep (WAITPID_MIN_TIME + 1);
+
+ pthread_mutex_lock (thread_creation_lock);
+ signal_thread_active_p = 0;
+
+ if (cached_wait_status.cached_p)
+ {
+ log_msg ("signal-thread: sending SIGCHLD\n");
+ kill (getpid (), SIGCHLD);
+ }
+
+ pthread_mutex_unlock (thread_creation_lock);
+ return NULL;
+}
+
+/* The waitpid entry point function. */
+
+pid_t
+waitpid (pid_t pid, int *wstatus, int options)
+{
+ log_msg ("waitpid: waitpid (%d, %p, 0x%x)\n", pid, wstatus, options);
+
+ if ((options & WNOHANG) != 0
+ && pid == -1
+ && should_skip_waitpid ())
+ {
+ if (!cached_wait_status.cached_p)
+ {
+ /* Do the waitpid call, but hold the result back. */
+ pid_t tmp_pid;
+ int tmp_wstatus;
+
+ tmp_pid = real_waitpid (-1, &tmp_wstatus, options);
+ if (tmp_pid > 0)
+ {
+ log_msg ("waitpid: delaying waitpid result (pid = %d)\n",
+ tmp_pid);
+
+ /* Cache the result. */
+ cached_wait_status.pid = tmp_pid;
+ cached_wait_status.wstatus = tmp_wstatus;
+ cached_wait_status.cached_p = 1;
+
+ /* Is there a thread around that will be sending a signal in
+ the near future? The prevents us from creating one
+ thread per call to waitpid when the calls occur in a
+ sequence. */
+ pthread_mutex_lock (thread_creation_lock);
+ if (!signal_thread_active_p)
+ {
+ sigset_t old_ss, new_ss;
+ pthread_t thread_id;
+ pthread_attr_t attr;
+
+ /* Create the new signal sending thread in detached
+ state. This means that the thread doesn't need to be
+ pthread_join'ed. Which is fine as there's no result
+ we care about. */
+ pthread_attr_init (&attr);
+ pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED);
+
+ /* Ensure the signal sending thread has all signals
+ blocked. We don't want any signals to GDB to be
+ handled in that thread. */
+ sigfillset (&new_ss);
+ sigprocmask (SIG_BLOCK, &new_ss, &old_ss);
+
+ log_msg ("waitpid: spawn thread to signal us\n");
+ if (pthread_create (&thread_id, &attr,
+ send_sigchld_thread, NULL) != 0)
+ error ("error: pthread_create failed\n");
+
+ signal_thread_active_p = 1;
+ sigprocmask (SIG_SETMASK, &old_ss, NULL);
+ pthread_attr_destroy (&attr);
+ }
+
+ pthread_mutex_unlock (thread_creation_lock);
+ }
+ }
+
+ log_msg ("waitpid: skipping\n");
+ return 0;
+ }
+
+ /* If we have a cached result that is a suitable reply for this call to
+ waitpid then send that cached result back now. */
+ if (cached_wait_status.cached_p
+ && (pid == -1 || pid == cached_wait_status.pid))
+ {
+ pid_t pid;
+
+ pid = cached_wait_status.pid;
+ log_msg ("waitpid: return cached result (%d)\n", pid);
+ *wstatus = cached_wait_status.wstatus;
+ cached_wait_status.cached_p = 0;
+ return pid;
+ }
+
+ log_msg ("waitpid: real waitpid call\n");
+ return real_waitpid (pid, wstatus, options);
+}
+
+/* Perform a real sigsuspend call. */
+
+static int
+real_sigsuspend (const sigset_t *mask)
+{
+ typedef int (*fptr_t) (const sigset_t *);
+ static fptr_t real_func = NULL;
+
+ if (real_func == NULL)
+ {
+ real_func = dlsym (RTLD_NEXT, "sigsuspend");
+ if (real_func == NULL)
+ error ("error: failed to find real sigsuspend\n");
+ }
+
+ return (*real_func) (mask);
+}
+
+/* The sigsuspend entry point function. */
+
+int
+sigsuspend (const sigset_t *mask)
+{
+ log_msg ("sigsuspend: sigsuspend (0x%p)\n", ((void *) mask));
+
+ /* If SIGCHLD is _not_ in MASK, and is therefore deliverable, then if we
+ have a pending wait status pretend that a signal arrived. We will
+ have a thread alive that is going to deliver a signal but doing this
+ will boost the speed as we don't have to wait for a signal. If the
+ signal ends up being delivered then it should be harmless, we'll just
+ perform an additional waitpid call. */
+ if (!sigismember (mask, SIGCHLD))
+ {
+ if (cached_wait_status.cached_p)
+ {
+ log_msg ("sigsuspend: interrupt for cached waitstatus\n");
+ last_waitpid_time.tv_sec = 0;
+ last_waitpid_time.tv_usec = 0;
+ errno = EINTR;
+ return -1;
+ }
+ }
+
+ log_msg ("sigsuspend: real sigsuspend call\n");
+ return real_sigsuspend (mask);
+}