• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import os
2import re
3import shlex
4import shutil
5import subprocess
6import sys
7import sysconfig
8import unittest
9from test import support
10
11
12GDB_PROGRAM = shutil.which('gdb') or 'gdb'
13
14# Location of custom hooks file in a repository checkout.
15CHECKOUT_HOOK_PATH = os.path.join(os.path.dirname(sys.executable),
16                                  'python-gdb.py')
17
18SAMPLE_SCRIPT = os.path.join(os.path.dirname(__file__), 'gdb_sample.py')
19BREAKPOINT_FN = 'builtin_id'
20
21PYTHONHASHSEED = '123'
22
23
24def clean_environment():
25    # Remove PYTHON* environment variables such as PYTHONHOME
26    return {name: value for name, value in os.environ.items()
27            if not name.startswith('PYTHON')}
28
29
30# Temporary value until it's initialized by get_gdb_version() below
31GDB_VERSION = (0, 0)
32
33def run_gdb(*args, exitcode=0, check=True, **env_vars):
34    """Runs gdb in --batch mode with the additional arguments given by *args.
35
36    Returns its (stdout, stderr) decoded from utf-8 using the replace handler.
37    """
38    env = clean_environment()
39    if env_vars:
40        env.update(env_vars)
41
42    cmd = [GDB_PROGRAM,
43           # Batch mode: Exit after processing all the command files
44           # specified with -x/--command
45           '--batch',
46            # -nx: Do not execute commands from any .gdbinit initialization
47            # files (gh-66384)
48           '-nx']
49    if GDB_VERSION >= (7, 4):
50        cmd.extend(('--init-eval-command',
51                    f'add-auto-load-safe-path {CHECKOUT_HOOK_PATH}'))
52    cmd.extend(args)
53
54    proc = subprocess.run(
55        cmd,
56        # Redirect stdin to prevent gdb from messing with the terminal settings
57        stdin=subprocess.PIPE,
58        stdout=subprocess.PIPE,
59        stderr=subprocess.PIPE,
60        encoding="utf8", errors="backslashreplace",
61        env=env)
62
63    stdout = proc.stdout
64    stderr = proc.stderr
65    if check and proc.returncode != exitcode:
66        cmd_text = shlex.join(cmd)
67        raise Exception(f"{cmd_text} failed with exit code {proc.returncode}, "
68                        f"expected exit code {exitcode}:\n"
69                        f"stdout={stdout!r}\n"
70                        f"stderr={stderr!r}")
71
72    return (stdout, stderr)
73
74
75def get_gdb_version():
76    try:
77        stdout, stderr = run_gdb('--version')
78    except OSError as exc:
79        # This is what "no gdb" looks like.  There may, however, be other
80        # errors that manifest this way too.
81        raise unittest.SkipTest(f"Couldn't find gdb program on the path: {exc}")
82
83    # Regex to parse:
84    # 'GNU gdb (GDB; SUSE Linux Enterprise 12) 7.7\n' -> 7.7
85    # 'GNU gdb (GDB) Fedora 7.9.1-17.fc22\n' -> 7.9
86    # 'GNU gdb 6.1.1 [FreeBSD]\n' -> 6.1
87    # 'GNU gdb (GDB) Fedora (7.5.1-37.fc18)\n' -> 7.5
88    # 'HP gdb 6.7 for HP Itanium (32 or 64 bit) and target HP-UX 11iv2 and 11iv3.\n' -> 6.7
89    match = re.search(r"^(?:GNU|HP) gdb.*?\b(\d+)\.(\d+)", stdout)
90    if match is None:
91        raise Exception("unable to parse gdb version: %r" % stdout)
92    version_text = stdout
93    major = int(match.group(1))
94    minor = int(match.group(2))
95    version = (major, minor)
96    return (version_text, version)
97
98GDB_VERSION_TEXT, GDB_VERSION = get_gdb_version()
99if GDB_VERSION < (7, 0):
100    raise unittest.SkipTest(
101        f"gdb versions before 7.0 didn't support python embedding. "
102        f"Saw gdb version {GDB_VERSION[0]}.{GDB_VERSION[1]}:\n"
103        f"{GDB_VERSION_TEXT}")
104
105
106def check_usable_gdb():
107    # Verify that "gdb" was built with the embedded Python support enabled and
108    # verify that "gdb" can load our custom hooks, as OS security settings may
109    # disallow this without a customized .gdbinit.
110    stdout, stderr = run_gdb(
111        '--eval-command=python import sys; print(sys.version_info)',
112        '--args', sys.executable,
113        check=False)
114
115    if "auto-loading has been declined" in stderr:
116        raise unittest.SkipTest(
117            f"gdb security settings prevent use of custom hooks; "
118            f"stderr: {stderr!r}")
119
120    if not stdout:
121        raise unittest.SkipTest(
122            f"gdb not built with embedded python support; "
123            f"stderr: {stderr!r}")
124
125    if "major=2" in stdout:
126        raise unittest.SkipTest("gdb built with Python 2")
127
128check_usable_gdb()
129
130
131# Control-flow enforcement technology
132def cet_protection():
133    cflags = sysconfig.get_config_var('CFLAGS')
134    if not cflags:
135        return False
136    flags = cflags.split()
137    # True if "-mcet -fcf-protection" options are found, but false
138    # if "-fcf-protection=none" or "-fcf-protection=return" is found.
139    return (('-mcet' in flags)
140            and any((flag.startswith('-fcf-protection')
141                     and not flag.endswith(("=none", "=return")))
142                    for flag in flags))
143CET_PROTECTION = cet_protection()
144
145
146def setup_module():
147    if support.verbose:
148        print(f"gdb version {GDB_VERSION[0]}.{GDB_VERSION[1]}:")
149        for line in GDB_VERSION_TEXT.splitlines():
150            print(" " * 4 + line)
151        print(f"    path: {GDB_PROGRAM}")
152        print()
153
154
155class DebuggerTests(unittest.TestCase):
156
157    """Test that the debugger can debug Python."""
158
159    def get_stack_trace(self, source=None, script=None,
160                        breakpoint=BREAKPOINT_FN,
161                        cmds_after_breakpoint=None,
162                        import_site=False,
163                        ignore_stderr=False):
164        '''
165        Run 'python -c SOURCE' under gdb with a breakpoint.
166
167        Support injecting commands after the breakpoint is reached
168
169        Returns the stdout from gdb
170
171        cmds_after_breakpoint: if provided, a list of strings: gdb commands
172        '''
173        # We use "set breakpoint pending yes" to avoid blocking with a:
174        #   Function "foo" not defined.
175        #   Make breakpoint pending on future shared library load? (y or [n])
176        # error, which typically happens python is dynamically linked (the
177        # breakpoints of interest are to be found in the shared library)
178        # When this happens, we still get:
179        #   Function "textiowrapper_write" not defined.
180        # emitted to stderr each time, alas.
181
182        # Initially I had "--eval-command=continue" here, but removed it to
183        # avoid repeated print breakpoints when traversing hierarchical data
184        # structures
185
186        # Generate a list of commands in gdb's language:
187        commands = [
188            'set breakpoint pending yes',
189            'break %s' % breakpoint,
190
191            # The tests assume that the first frame of printed
192            #  backtrace will not contain program counter,
193            #  that is however not guaranteed by gdb
194            #  therefore we need to use 'set print address off' to
195            #  make sure the counter is not there. For example:
196            # #0 in PyObject_Print ...
197            #  is assumed, but sometimes this can be e.g.
198            # #0 0x00003fffb7dd1798 in PyObject_Print ...
199            'set print address off',
200
201            'run',
202        ]
203
204        # GDB as of 7.4 onwards can distinguish between the
205        # value of a variable at entry vs current value:
206        #   http://sourceware.org/gdb/onlinedocs/gdb/Variables.html
207        # which leads to the selftests failing with errors like this:
208        #   AssertionError: 'v@entry=()' != '()'
209        # Disable this:
210        if GDB_VERSION >= (7, 4):
211            commands += ['set print entry-values no']
212
213        if cmds_after_breakpoint:
214            if CET_PROTECTION:
215                # bpo-32962: When Python is compiled with -mcet
216                # -fcf-protection, function arguments are unusable before
217                # running the first instruction of the function entry point.
218                # The 'next' command makes the required first step.
219                commands += ['next']
220            commands += cmds_after_breakpoint
221        else:
222            commands += ['backtrace']
223
224        # print commands
225
226        # Use "commands" to generate the arguments with which to invoke "gdb":
227        args = ['--eval-command=%s' % cmd for cmd in commands]
228        args += ["--args",
229                 sys.executable]
230        args.extend(subprocess._args_from_interpreter_flags())
231
232        if not import_site:
233            # -S suppresses the default 'import site'
234            args += ["-S"]
235
236        if source:
237            args += ["-c", source]
238        elif script:
239            args += [script]
240
241        # Use "args" to invoke gdb, capturing stdout, stderr:
242        out, err = run_gdb(*args, PYTHONHASHSEED=PYTHONHASHSEED)
243
244        if not ignore_stderr:
245            for line in err.splitlines():
246                print(line, file=sys.stderr)
247
248        # bpo-34007: Sometimes some versions of the shared libraries that
249        # are part of the traceback are compiled in optimised mode and the
250        # Program Counter (PC) is not present, not allowing gdb to walk the
251        # frames back. When this happens, the Python bindings of gdb raise
252        # an exception, making the test impossible to succeed.
253        if "PC not saved" in err:
254            raise unittest.SkipTest("gdb cannot walk the frame object"
255                                    " because the Program Counter is"
256                                    " not present")
257
258        # bpo-40019: Skip the test if gdb failed to read debug information
259        # because the Python binary is optimized.
260        for pattern in (
261            '(frame information optimized out)',
262            'Unable to read information on python frame',
263
264            # gh-91960: On Python built with "clang -Og", gdb gets
265            # "frame=<optimized out>" for _PyEval_EvalFrameDefault() parameter
266            '(unable to read python frame information)',
267
268            # gh-104736: On Python built with "clang -Og" on ppc64le,
269            # "py-bt" displays a truncated or not traceback, but "where"
270            # logs this error message:
271            'Backtrace stopped: frame did not save the PC',
272
273            # gh-104736: When "bt" command displays something like:
274            # "#1  0x0000000000000000 in ?? ()", the traceback is likely
275            # truncated or wrong.
276            ' ?? ()',
277        ):
278            if pattern in out:
279                raise unittest.SkipTest(f"{pattern!r} found in gdb output")
280
281        return out
282
283    def assertEndsWith(self, actual, exp_end):
284        '''Ensure that the given "actual" string ends with "exp_end"'''
285        self.assertTrue(actual.endswith(exp_end),
286                        msg='%r did not end with %r' % (actual, exp_end))
287
288    def assertMultilineMatches(self, actual, pattern):
289        m = re.match(pattern, actual, re.DOTALL)
290        if not m:
291            self.fail(msg='%r did not match %r' % (actual, pattern))
292