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