| /* This testcase is part of GDB, the GNU debugger. |
| |
| Copyright 2018-2021 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); |
| } |