| import os | |
| import re | |
| import shlex | |
| import shutil | |
| import subprocess | |
| import sys | |
| import sysconfig | |
| import unittest | |
| from test import support | |
| GDB_PROGRAM = shutil.which('gdb') or 'gdb' | |
| # Location of custom hooks file in a repository checkout. | |
| CHECKOUT_HOOK_PATH = os.path.join(os.path.dirname(sys.executable), | |
| 'python-gdb.py') | |
| SAMPLE_SCRIPT = os.path.join(os.path.dirname(__file__), 'gdb_sample.py') | |
| BREAKPOINT_FN = 'builtin_id' | |
| PYTHONHASHSEED = '123' | |
| # gh-91960, bpo-40019: gdb reports these when the optimizer has dropped | |
| # python-frame debug info; the test can't read what's not there. | |
| _OPTIMIZED_OUT_PATTERNS = ( | |
| '(frame information optimized out)', | |
| 'Unable to read information on python frame', | |
| '(unable to read python frame information)', | |
| ) | |
| # gdb prints this when the unwinder genuinely failed to walk a frame — | |
| # i.e. the CFI (ours or a library's) is wrong. Treat as a hard failure, | |
| # not a skip, so regressions in our own unwind info don't hide. | |
| _UNWIND_FAILURE_PATTERNS = ( | |
| 'Backtrace stopped: frame did not save the PC', | |
| ) | |
| # gh-104736: " ?? ()" in the bt usually means the unwinder bailed early, | |
| # but can also be unrelated frames without debug info (e.g. libc). Tests | |
| # that validate the JIT-relevant part of the backtrace themselves can | |
| # opt out via skip_on_truncation=False. | |
| _TRUNCATED_BACKTRACE_PATTERNS = ( | |
| ' ?? ()', | |
| ) | |
| def clean_environment(): | |
| # Remove PYTHON* environment variables such as PYTHONHOME | |
| return {name: value for name, value in os.environ.items() | |
| if not name.startswith('PYTHON')} | |
| # Temporary value until it's initialized by get_gdb_version() below | |
| GDB_VERSION = (0, 0) | |
| def run_gdb(*args, exitcode=0, check=True, **env_vars): | |
| """Runs gdb in --batch mode with the additional arguments given by *args. | |
| Returns its (stdout, stderr) decoded from utf-8 using the replace handler. | |
| """ | |
| env = clean_environment() | |
| if env_vars: | |
| env.update(env_vars) | |
| cmd = [GDB_PROGRAM, | |
| # Batch mode: Exit after processing all the command files | |
| # specified with -x/--command | |
| '--batch', | |
| # -nx: Do not execute commands from any .gdbinit initialization | |
| # files (gh-66384) | |
| '-nx'] | |
| if GDB_VERSION >= (7, 4): | |
| cmd.extend(('--init-eval-command', | |
| f'add-auto-load-safe-path {CHECKOUT_HOOK_PATH}')) | |
| cmd.extend(args) | |
| proc = subprocess.run( | |
| cmd, | |
| # Redirect stdin to prevent gdb from messing with the terminal settings | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE, | |
| encoding="ascii", errors="surrogateescape", | |
| env=env) | |
| stdout = proc.stdout | |
| stderr = proc.stderr | |
| if check and proc.returncode != exitcode: | |
| cmd_text = shlex.join(cmd) | |
| raise Exception(f"{cmd_text} failed with exit code {proc.returncode}, " | |
| f"expected exit code {exitcode}:\n" | |
| f"stdout={stdout!r}\n" | |
| f"stderr={stderr!r}") | |
| return (stdout, stderr) | |
| def get_gdb_version(): | |
| try: | |
| stdout, stderr = run_gdb('--version') | |
| except OSError as exc: | |
| # This is what "no gdb" looks like. There may, however, be other | |
| # errors that manifest this way too. | |
| raise unittest.SkipTest(f"Couldn't find gdb program on the path: {exc}") | |
| # Regex to parse: | |
| # 'GNU gdb (GDB; SUSE Linux Enterprise 12) 7.7\n' -> 7.7 | |
| # 'GNU gdb (GDB) Fedora 7.9.1-17.fc22\n' -> 7.9 | |
| # 'GNU gdb 6.1.1 [FreeBSD]\n' -> 6.1 | |
| # 'GNU gdb (GDB) Fedora (7.5.1-37.fc18)\n' -> 7.5 | |
| # 'HP gdb 6.7 for HP Itanium (32 or 64 bit) and target HP-UX 11iv2 and 11iv3.\n' -> 6.7 | |
| match = re.search(r"^(?:GNU|HP) gdb.*?\b(\d+)\.(\d+)", stdout) | |
| if match is None: | |
| raise Exception("unable to parse gdb version: %r" % stdout) | |
| version_text = stdout | |
| major = int(match.group(1)) | |
| minor = int(match.group(2)) | |
| version = (major, minor) | |
| return (version_text, version) | |
| GDB_VERSION_TEXT, GDB_VERSION = get_gdb_version() | |
| if GDB_VERSION < (7, 0): | |
| raise unittest.SkipTest( | |
| f"gdb versions before 7.0 didn't support python embedding. " | |
| f"Saw gdb version {GDB_VERSION[0]}.{GDB_VERSION[1]}:\n" | |
| f"{GDB_VERSION_TEXT}") | |
| def check_usable_gdb(): | |
| # Verify that "gdb" was built with the embedded Python support enabled and | |
| # verify that "gdb" can load our custom hooks, as OS security settings may | |
| # disallow this without a customized .gdbinit. | |
| stdout, stderr = run_gdb( | |
| '--eval-command=python import sys; print(sys.version_info)', | |
| '--args', sys.executable, | |
| check=False) | |
| if "auto-loading has been declined" in stderr: | |
| raise unittest.SkipTest( | |
| f"gdb security settings prevent use of custom hooks; " | |
| f"stderr: {stderr!r}") | |
| if not stdout: | |
| raise unittest.SkipTest( | |
| f"gdb not built with embedded python support; " | |
| f"stderr: {stderr!r}") | |
| if "major=2" in stdout: | |
| raise unittest.SkipTest("gdb built with Python 2") | |
| check_usable_gdb() | |
| # Control-flow enforcement technology | |
| def cet_protection(): | |
| cflags = sysconfig.get_config_var('CFLAGS') | |
| if not cflags: | |
| return False | |
| flags = cflags.split() | |
| # True if "-mcet -fcf-protection" options are found, but false | |
| # if "-fcf-protection=none" or "-fcf-protection=return" is found. | |
| return (('-mcet' in flags) | |
| and any((flag.startswith('-fcf-protection') | |
| and not flag.endswith(("=none", "=return"))) | |
| for flag in flags)) | |
| CET_PROTECTION = cet_protection() | |
| def setup_module(): | |
| if support.verbose: | |
| print(f"gdb version {GDB_VERSION[0]}.{GDB_VERSION[1]}:") | |
| for line in GDB_VERSION_TEXT.splitlines(): | |
| print(" " * 4 + line) | |
| print(f" path: {GDB_PROGRAM}") | |
| print() | |
| class DebuggerTests(unittest.TestCase): | |
| """Test that the debugger can debug Python.""" | |
| def get_stack_trace(self, source=None, script=None, | |
| breakpoint=BREAKPOINT_FN, | |
| cmds_after_breakpoint=None, | |
| import_site=False, | |
| ignore_stderr=False, | |
| skip_on_truncation=True, | |
| **env_vars): | |
| ''' | |
| Run 'python -c SOURCE' under gdb with a breakpoint. | |
| Support injecting commands after the breakpoint is reached | |
| Returns the stdout from gdb | |
| cmds_after_breakpoint: if provided, a list of strings: gdb commands | |
| ''' | |
| # We use "set breakpoint pending yes" to avoid blocking with a: | |
| # Function "foo" not defined. | |
| # Make breakpoint pending on future shared library load? (y or [n]) | |
| # error, which typically happens python is dynamically linked (the | |
| # breakpoints of interest are to be found in the shared library) | |
| # When this happens, we still get: | |
| # Function "textiowrapper_write" not defined. | |
| # emitted to stderr each time, alas. | |
| # Initially I had "--eval-command=continue" here, but removed it to | |
| # avoid repeated print breakpoints when traversing hierarchical data | |
| # structures | |
| # Generate a list of commands in gdb's language: | |
| commands = [ | |
| 'set breakpoint pending yes', | |
| 'break %s' % breakpoint, | |
| # The tests assume that the first frame of printed | |
| # backtrace will not contain program counter, | |
| # that is however not guaranteed by gdb | |
| # therefore we need to use 'set print address off' to | |
| # make sure the counter is not there. For example: | |
| # #0 in PyObject_Print ... | |
| # is assumed, but sometimes this can be e.g. | |
| # #0 0x00003fffb7dd1798 in PyObject_Print ... | |
| 'set print address off', | |
| 'run', | |
| ] | |
| # GDB as of 7.4 onwards can distinguish between the | |
| # value of a variable at entry vs current value: | |
| # http://sourceware.org/gdb/onlinedocs/gdb/Variables.html | |
| # which leads to the selftests failing with errors like this: | |
| # AssertionError: 'v@entry=()' != '()' | |
| # Disable this: | |
| if GDB_VERSION >= (7, 4): | |
| commands += ['set print entry-values no'] | |
| if cmds_after_breakpoint: | |
| if CET_PROTECTION: | |
| # bpo-32962: When Python is compiled with -mcet | |
| # -fcf-protection, function arguments are unusable before | |
| # running the first instruction of the function entry point. | |
| # The 'next' command makes the required first step. | |
| commands += ['next'] | |
| commands += cmds_after_breakpoint | |
| else: | |
| commands += ['backtrace'] | |
| # print commands | |
| # Use "commands" to generate the arguments with which to invoke "gdb": | |
| args = ['--eval-command=%s' % cmd for cmd in commands] | |
| args += ["--args", | |
| sys.executable] | |
| args.extend(subprocess._args_from_interpreter_flags()) | |
| if not import_site: | |
| # -S suppresses the default 'import site' | |
| args += ["-S"] | |
| if source: | |
| args += ["-c", source] | |
| elif script: | |
| args += [script] | |
| # Use "args" to invoke gdb, capturing stdout, stderr: | |
| out, err = run_gdb(*args, PYTHONHASHSEED=PYTHONHASHSEED, **env_vars) | |
| if not ignore_stderr: | |
| for line in err.splitlines(): | |
| print(line, file=sys.stderr) | |
| # bpo-34007: Sometimes some versions of the shared libraries that | |
| # are part of the traceback are compiled in optimised mode and the | |
| # Program Counter (PC) is not present, not allowing gdb to walk the | |
| # frames back. When this happens, the Python bindings of gdb raise | |
| # an exception, making the test impossible to succeed. | |
| if "PC not saved" in err: | |
| raise unittest.SkipTest("gdb cannot walk the frame object" | |
| " because the Program Counter is" | |
| " not present") | |
| for pattern in _UNWIND_FAILURE_PATTERNS: | |
| if pattern in out: | |
| raise AssertionError( | |
| f"gdb unwinder failed ({pattern!r}) — CFI bug in our " | |
| f"generated code or in a linked library.\n" | |
| f"Full gdb output:\n{out}" | |
| ) | |
| # bpo-40019: Skip the test if gdb failed to read debug information | |
| # because the Python binary is optimized. | |
| patterns = _OPTIMIZED_OUT_PATTERNS | |
| if skip_on_truncation: | |
| patterns = patterns + _TRUNCATED_BACKTRACE_PATTERNS | |
| for pattern in patterns: | |
| if pattern in out: | |
| raise unittest.SkipTest(f"{pattern!r} found in gdb output") | |
| return out | |
| def assertMultilineMatches(self, actual, pattern): | |
| m = re.match(pattern, actual, re.DOTALL) | |
| if not m: | |
| self.fail(msg='%r did not match %r' % (actual, pattern)) |