| 1 | /* Smoke testing GDB process attach with thread-local variable access. |
| 2 | Copyright (C) 2021-2024 Free Software Foundation, Inc. |
| 3 | This file is part of the GNU C Library. |
| 4 | |
| 5 | The GNU C Library is free software; you can redistribute it and/or |
| 6 | modify it under the terms of the GNU Lesser General Public |
| 7 | License as published by the Free Software Foundation; either |
| 8 | version 2.1 of the License, or (at your option) any later version. |
| 9 | |
| 10 | The GNU C Library is distributed in the hope that it will be useful, |
| 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 13 | Lesser General Public License for more details. |
| 14 | |
| 15 | You should have received a copy of the GNU Lesser General Public |
| 16 | License along with the GNU C Library; if not, see |
| 17 | <https://www.gnu.org/licenses/>. */ |
| 18 | |
| 19 | /* This test runs GDB against a forked copy of itself, to check |
| 20 | whether libthread_db can be loaded, and that access to thread-local |
| 21 | variables works. */ |
| 22 | |
| 23 | #include <elf.h> |
| 24 | #include <errno.h> |
| 25 | #include <fcntl.h> |
| 26 | #include <signal.h> |
| 27 | #include <stdbool.h> |
| 28 | #include <stdlib.h> |
| 29 | #include <string.h> |
| 30 | #include <support/capture_subprocess.h> |
| 31 | #include <support/check.h> |
| 32 | #include <support/xptrace.h> |
| 33 | #include <support/subprocess.h> |
| 34 | #include <support/support.h> |
| 35 | #include <support/temp_file.h> |
| 36 | #include <support/test-driver.h> |
| 37 | #include <support/xstdio.h> |
| 38 | #include <support/xthread.h> |
| 39 | #include <support/xunistd.h> |
| 40 | #include <unistd.h> |
| 41 | |
| 42 | /* Starts out as zero, changed to 1 or 2 by the debugger, depending on |
| 43 | the thread. */ |
| 44 | __thread volatile int altered_by_debugger; |
| 45 | |
| 46 | /* Common prefix between 32-bit and 64-bit ELF. */ |
| 47 | struct elf_prefix |
| 48 | { |
| 49 | unsigned char e_ident[EI_NIDENT]; |
| 50 | uint16_t e_type; |
| 51 | uint16_t e_machine; |
| 52 | uint32_t e_version; |
| 53 | }; |
| 54 | _Static_assert (sizeof (struct elf_prefix) == EI_NIDENT + 8, |
| 55 | "padding in struct elf_prefix" ); |
| 56 | |
| 57 | /* Reads the ELF header from PATH. Returns true if the header can be |
| 58 | read, false if the file is too short. */ |
| 59 | static bool |
| 60 | (const char *path, struct elf_prefix *elf) |
| 61 | { |
| 62 | int fd = xopen (path, O_RDONLY, 0); |
| 63 | bool result = read (fd, elf, sizeof (*elf)) == sizeof (*elf); |
| 64 | xclose (fd); |
| 65 | return result; |
| 66 | } |
| 67 | |
| 68 | /* Searches for "gdb" alongside the path variable. See execvpe. */ |
| 69 | static char * |
| 70 | find_gdb (void) |
| 71 | { |
| 72 | const char *path = getenv ("PATH" ); |
| 73 | if (path == NULL) |
| 74 | return NULL; |
| 75 | while (true) |
| 76 | { |
| 77 | const char *colon = strchrnul (s: path, c: ':'); |
| 78 | char *candidate = xasprintf (format: "%.*s/gdb" , (int) (colon - path), path); |
| 79 | if (access (name: candidate, X_OK) == 0) |
| 80 | return candidate; |
| 81 | free (ptr: candidate); |
| 82 | if (*colon == '\0') |
| 83 | break; |
| 84 | path = colon + 1; |
| 85 | } |
| 86 | return NULL; |
| 87 | } |
| 88 | |
| 89 | /* Writes the GDB script to run the test to PATH. */ |
| 90 | static void |
| 91 | write_gdbscript (const char *path, int tested_pid) |
| 92 | { |
| 93 | FILE *fp = xfopen (path, mode: "w" ); |
| 94 | fprintf (fp, |
| 95 | "set trace-commands on\n" |
| 96 | "set debug libthread-db 1\n" |
| 97 | #if DO_ADD_SYMBOL_FILE |
| 98 | /* Do not do this unconditionally to work around a GDB |
| 99 | assertion failure: ../../gdb/symtab.c:6404: |
| 100 | internal-error: CORE_ADDR get_msymbol_address(objfile*, |
| 101 | const minimal_symbol*): Assertion `(objf->flags & |
| 102 | OBJF_MAINLINE) == 0' failed. */ |
| 103 | "add-symbol-file %1$s/nptl/tst-pthread-gdb-attach\n" |
| 104 | #endif |
| 105 | "set auto-load safe-path %1$s/nptl_db\n" |
| 106 | "set libthread-db-search-path %1$s/nptl_db\n" |
| 107 | "attach %2$d\n" , |
| 108 | support_objdir_root, tested_pid); |
| 109 | fputs ("break debugger_inspection_point\n" |
| 110 | "continue\n" |
| 111 | "thread 1\n" |
| 112 | "print altered_by_debugger\n" |
| 113 | "print altered_by_debugger = 1\n" |
| 114 | "thread 2\n" |
| 115 | "print altered_by_debugger\n" |
| 116 | "print altered_by_debugger = 2\n" |
| 117 | "continue\n" , |
| 118 | fp); |
| 119 | xfclose (fp); |
| 120 | } |
| 121 | |
| 122 | /* The test sets a breakpoint on this function and alters the |
| 123 | altered_by_debugger thread-local variable. */ |
| 124 | void __attribute__ ((weak)) |
| 125 | debugger_inspection_point (void) |
| 126 | { |
| 127 | } |
| 128 | |
| 129 | /* Thread function for the test thread in the subprocess. */ |
| 130 | static void * |
| 131 | subprocess_thread (void *closure) |
| 132 | { |
| 133 | /* Wait until altered_by_debugger changes the value away from 0. */ |
| 134 | while (altered_by_debugger == 0) |
| 135 | { |
| 136 | usleep (useconds: 100 * 1000); |
| 137 | debugger_inspection_point (); |
| 138 | } |
| 139 | |
| 140 | TEST_COMPARE (altered_by_debugger, 2); |
| 141 | return NULL; |
| 142 | } |
| 143 | |
| 144 | /* This function implements the subprocess under test. It creates a |
| 145 | second thread, waiting for its value to change to 2, and checks |
| 146 | that the main thread also changed its value to 1. */ |
| 147 | static void |
| 148 | in_subprocess (void *arg) |
| 149 | { |
| 150 | pthread_t thr = xpthread_create (NULL, thread_func: subprocess_thread, NULL); |
| 151 | TEST_VERIFY (xpthread_join (thr) == NULL); |
| 152 | TEST_COMPARE (altered_by_debugger, 1); |
| 153 | _exit (0); |
| 154 | } |
| 155 | |
| 156 | static void |
| 157 | gdb_process (const char *gdb_path, const char *gdbscript, pid_t *tested_pid) |
| 158 | { |
| 159 | /* Create a copy of current test to check with gdb. As the |
| 160 | target_process is a child of this gdb_process, gdb is also able |
| 161 | to attach to target_process if YAMA is configured to 1 = |
| 162 | "restricted ptrace". */ |
| 163 | struct support_subprocess target = support_subprocess (callback: in_subprocess, NULL); |
| 164 | |
| 165 | write_gdbscript (path: gdbscript, tested_pid: target.pid); |
| 166 | *tested_pid = target.pid; |
| 167 | |
| 168 | xdup2 (STDOUT_FILENO, STDERR_FILENO); |
| 169 | execl (gdb_path, "gdb" , "-nx" , "-batch" , "-x" , gdbscript, NULL); |
| 170 | if (errno == ENOENT) |
| 171 | _exit (EXIT_UNSUPPORTED); |
| 172 | else |
| 173 | _exit (1); |
| 174 | } |
| 175 | |
| 176 | static int |
| 177 | do_test (void) |
| 178 | { |
| 179 | char *gdb_path = find_gdb (); |
| 180 | if (gdb_path == NULL) |
| 181 | FAIL_UNSUPPORTED ("gdb command not found in PATH: %s" , getenv ("PATH" )); |
| 182 | |
| 183 | /* Check that libthread_db is compatible with the gdb architecture |
| 184 | because gdb loads it via dlopen. */ |
| 185 | { |
| 186 | char *threaddb_path = xasprintf (format: "%s/nptl_db/libthread_db.so" , |
| 187 | support_objdir_root); |
| 188 | struct elf_prefix elf_threaddb; |
| 189 | TEST_VERIFY_EXIT (read_elf_header (threaddb_path, &elf_threaddb)); |
| 190 | struct elf_prefix elf_gdb; |
| 191 | /* If the ELF header cannot be read or "gdb" is not an ELF file, |
| 192 | assume this is a wrapper script that can run. */ |
| 193 | if (read_elf_header (path: gdb_path, elf: &elf_gdb) |
| 194 | && memcmp (&elf_gdb, ELFMAG, SELFMAG) == 0) |
| 195 | { |
| 196 | if (elf_gdb.e_ident[EI_CLASS] != elf_threaddb.e_ident[EI_CLASS]) |
| 197 | FAIL_UNSUPPORTED ("GDB at %s has wrong class" , gdb_path); |
| 198 | if (elf_gdb.e_ident[EI_DATA] != elf_threaddb.e_ident[EI_DATA]) |
| 199 | FAIL_UNSUPPORTED ("GDB at %s has wrong data" , gdb_path); |
| 200 | if (elf_gdb.e_machine != elf_threaddb.e_machine) |
| 201 | FAIL_UNSUPPORTED ("GDB at %s has wrong machine" , gdb_path); |
| 202 | } |
| 203 | free (ptr: threaddb_path); |
| 204 | } |
| 205 | |
| 206 | /* Check if our subprocess can be debugged with ptrace. */ |
| 207 | { |
| 208 | int ptrace_scope = support_ptrace_scope (); |
| 209 | if (ptrace_scope >= 2) |
| 210 | FAIL_UNSUPPORTED ("/proc/sys/kernel/yama/ptrace_scope >= 2" ); |
| 211 | } |
| 212 | |
| 213 | char *gdbscript; |
| 214 | xclose (create_temp_file (base: "tst-pthread-gdb-attach-" , filename: &gdbscript)); |
| 215 | |
| 216 | /* Run 'gdb' on test subprocess which will be created in gdb_process. |
| 217 | The pid of the subprocess will be written to 'tested_pid'. */ |
| 218 | pid_t *tested_pid = support_shared_allocate (size: sizeof (pid_t)); |
| 219 | |
| 220 | pid_t gdb_pid = xfork (); |
| 221 | if (gdb_pid == 0) |
| 222 | gdb_process (gdb_path, gdbscript, tested_pid); |
| 223 | |
| 224 | int status; |
| 225 | TEST_COMPARE (xwaitpid (gdb_pid, &status, 0), gdb_pid); |
| 226 | if (WIFEXITED (status) && WEXITSTATUS (status) == EXIT_UNSUPPORTED) |
| 227 | /* gdb is not installed. */ |
| 228 | return EXIT_UNSUPPORTED; |
| 229 | TEST_COMPARE (status, 0); |
| 230 | |
| 231 | kill (pid: *tested_pid, SIGKILL); |
| 232 | |
| 233 | support_shared_free (tested_pid); |
| 234 | free (ptr: gdbscript); |
| 235 | free (ptr: gdb_path); |
| 236 | return 0; |
| 237 | } |
| 238 | |
| 239 | #include <support/test-driver.c> |
| 240 | |