Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 8dfa840

Browse filesBrowse files
gh-127604: Add C stack dumps to faulthandler (#128159)
1 parent ea8ec95 commit 8dfa840
Copy full SHA for 8dfa840

File tree

Expand file treeCollapse file tree

13 files changed

+378
-69
lines changed
Filter options
Expand file treeCollapse file tree

13 files changed

+378
-69
lines changed

‎Doc/library/faulthandler.rst

Copy file name to clipboardExpand all lines: Doc/library/faulthandler.rst
+39-1Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,41 @@ Dumping the traceback
6666
Added support for passing file descriptor to this function.
6767

6868

69+
Dumping the C stack
70+
-------------------
71+
72+
.. versionadded:: next
73+
74+
.. function:: dump_c_stack(file=sys.stderr)
75+
76+
Dump the C stack trace of the current thread into *file*.
77+
78+
If the Python build does not support it or the operating system
79+
does not provide a stack trace, then this prints an error in place
80+
of a dumped C stack.
81+
82+
.. _c-stack-compatibility:
83+
84+
C Stack Compatibility
85+
*********************
86+
87+
If the system does not support the C-level :manpage:`backtrace(3)`,
88+
:manpage:`backtrace_symbols(3)`, or :manpage:`dladdr(3)`, then C stack dumps
89+
will not work. An error will be printed instead of the stack.
90+
91+
Additionally, some compilers do not support :term:`CPython's <CPython>`
92+
implementation of C stack dumps. As a result, a different error may be printed
93+
instead of the stack, even if the the operating system supports dumping stacks.
94+
95+
.. note::
96+
97+
Dumping C stacks can be arbitrarily slow, depending on the DWARF level
98+
of the binaries in the call stack.
99+
69100
Fault handler state
70101
-------------------
71102

72-
.. function:: enable(file=sys.stderr, all_threads=True)
103+
.. function:: enable(file=sys.stderr, all_threads=True, c_stack=True)
73104

74105
Enable the fault handler: install handlers for the :const:`~signal.SIGSEGV`,
75106
:const:`~signal.SIGFPE`, :const:`~signal.SIGABRT`, :const:`~signal.SIGBUS`
@@ -81,6 +112,10 @@ Fault handler state
81112
The *file* must be kept open until the fault handler is disabled: see
82113
:ref:`issue with file descriptors <faulthandler-fd>`.
83114

115+
If *c_stack* is ``True``, then the C stack trace is printed after the Python
116+
traceback, unless the system does not support it. See :func:`dump_c_stack` for
117+
more information on compatibility.
118+
84119
.. versionchanged:: 3.5
85120
Added support for passing file descriptor to this function.
86121

@@ -95,6 +130,9 @@ Fault handler state
95130
Only the current thread is dumped if the :term:`GIL` is disabled to
96131
prevent the risk of data races.
97132

133+
.. versionchanged:: next
134+
The dump now displays the C stack trace if *c_stack* is true.
135+
98136
.. function:: disable()
99137

100138
Disable the fault handler: uninstall the signal handlers installed by

‎Doc/whatsnew/3.14.rst

Copy file name to clipboardExpand all lines: Doc/whatsnew/3.14.rst
+9Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,15 @@ errno
699699
(Contributed by James Roy in :gh:`126585`.)
700700

701701

702+
faulthandler
703+
------------
704+
705+
* Add support for printing the C stack trace on systems that
706+
:ref:`support it <c-stack-compatibility>` via :func:`faulthandler.dump_c_stack`
707+
or via the *c_stack* argument in :func:`faulthandler.enable`.
708+
(Contributed by Peter Bierma in :gh:`127604`.)
709+
710+
702711
fnmatch
703712
-------
704713

‎Include/internal/pycore_faulthandler.h

Copy file name to clipboardExpand all lines: Include/internal/pycore_faulthandler.h
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ struct _faulthandler_runtime_state {
5656
#ifdef MS_WINDOWS
5757
void *exc_handler;
5858
#endif
59+
int c_stack;
5960
} fatal_error;
6061

6162
struct {

‎Include/internal/pycore_traceback.h

Copy file name to clipboardExpand all lines: Include/internal/pycore_traceback.h
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ extern int _PyTraceBack_Print(
9999
extern int _Py_WriteIndentedMargin(int, const char*, PyObject *);
100100
extern int _Py_WriteIndent(int, PyObject *);
101101

102+
// Export for the faulthandler module
103+
PyAPI_FUNC(void) _Py_DumpStack(int fd);
104+
102105
#ifdef __cplusplus
103106
}
104107
#endif

‎Lib/test/test_faulthandler.py

Copy file name to clipboardExpand all lines: Lib/test/test_faulthandler.py
+40Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ def temporary_filename():
5555
finally:
5656
os_helper.unlink(filename)
5757

58+
59+
ADDRESS_EXPR = "0x[0-9a-f]+"
60+
C_STACK_REGEX = [
61+
r"Current thread's C stack trace \(most recent call first\):",
62+
fr'( Binary file ".+"(, at .*(\+|-){ADDRESS_EXPR})? \[{ADDRESS_EXPR}\])|(<.+>)'
63+
]
64+
5865
class FaultHandlerTests(unittest.TestCase):
5966

6067
def get_output(self, code, filename=None, fd=None):
@@ -103,6 +110,7 @@ def check_error(self, code, lineno, fatal_error, *,
103110
fd=None, know_current_thread=True,
104111
py_fatal_error=False,
105112
garbage_collecting=False,
113+
c_stack=True,
106114
function='<module>'):
107115
"""
108116
Check that the fault handler for fatal errors is enabled and check the
@@ -134,6 +142,8 @@ def check_error(self, code, lineno, fatal_error, *,
134142
if garbage_collecting and not all_threads_disabled:
135143
regex.append(' Garbage-collecting')
136144
regex.append(fr' File "<string>", line {lineno} in {function}')
145+
if c_stack:
146+
regex.extend(C_STACK_REGEX)
137147
regex = '\n'.join(regex)
138148

139149
if other_regex:
@@ -950,5 +960,35 @@ def run(self):
950960
_, exitcode = self.get_output(code)
951961
self.assertEqual(exitcode, 0)
952962

963+
def check_c_stack(self, output):
964+
starting_line = output.pop(0)
965+
self.assertRegex(starting_line, C_STACK_REGEX[0])
966+
self.assertGreater(len(output), 0)
967+
968+
for line in output:
969+
with self.subTest(line=line):
970+
if line != '': # Ignore trailing or leading newlines
971+
self.assertRegex(line, C_STACK_REGEX[1])
972+
973+
974+
def test_dump_c_stack(self):
975+
code = dedent("""
976+
import faulthandler
977+
faulthandler.dump_c_stack()
978+
""")
979+
output, exitcode = self.get_output(code)
980+
self.assertEqual(exitcode, 0)
981+
self.check_c_stack(output)
982+
983+
984+
def test_dump_c_stack_file(self):
985+
import tempfile
986+
987+
with tempfile.TemporaryFile("w+") as tmp:
988+
faulthandler.dump_c_stack(file=tmp)
989+
tmp.flush() # Just in case
990+
tmp.seek(0)
991+
self.check_c_stack(tmp.read().split("\n"))
992+
953993
if __name__ == "__main__":
954994
unittest.main()

‎Lib/test/test_inspect/test_inspect.py

Copy file name to clipboardExpand all lines: Lib/test/test_inspect/test_inspect.py
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5760,7 +5760,7 @@ def test_errno_module_has_signatures(self):
57605760

57615761
def test_faulthandler_module_has_signatures(self):
57625762
import faulthandler
5763-
unsupported_signature = {'dump_traceback', 'dump_traceback_later', 'enable'}
5763+
unsupported_signature = {'dump_traceback', 'dump_traceback_later', 'enable', 'dump_c_stack'}
57645764
unsupported_signature |= {name for name in ['register']
57655765
if hasattr(faulthandler, name)}
57665766
self._test_module_has_signatures(faulthandler, unsupported_signature=unsupported_signature)
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add support for printing the C stack trace on systems that support it via
2+
:func:`faulthandler.dump_c_stack` or via the *c_stack* argument in
3+
:func:`faulthandler.enable`.

‎Modules/faulthandler.c

Copy file name to clipboardExpand all lines: Modules/faulthandler.c
+57-3Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
#include "pycore_sysmodule.h" // _PySys_GetRequiredAttr()
1010
#include "pycore_time.h" // _PyTime_FromSecondsObject()
1111
#include "pycore_traceback.h" // _Py_DumpTracebackThreads
12-
1312
#ifdef HAVE_UNISTD_H
1413
# include <unistd.h> // _exit()
1514
#endif
15+
1616
#include <signal.h> // sigaction()
1717
#include <stdlib.h> // abort()
1818
#if defined(HAVE_PTHREAD_SIGMASK) && !defined(HAVE_BROKEN_PTHREAD_SIGMASK) && defined(HAVE_PTHREAD_H)
@@ -210,6 +210,25 @@ faulthandler_dump_traceback(int fd, int all_threads,
210210
reentrant = 0;
211211
}
212212

213+
static void
214+
faulthandler_dump_c_stack(int fd)
215+
{
216+
static volatile int reentrant = 0;
217+
218+
if (reentrant) {
219+
return;
220+
}
221+
222+
reentrant = 1;
223+
224+
if (fatal_error.c_stack) {
225+
PUTS(fd, "\n");
226+
_Py_DumpStack(fd);
227+
}
228+
229+
reentrant = 0;
230+
}
231+
213232
static PyObject*
214233
faulthandler_dump_traceback_py(PyObject *self,
215234
PyObject *args, PyObject *kwargs)
@@ -260,6 +279,33 @@ faulthandler_dump_traceback_py(PyObject *self,
260279
Py_RETURN_NONE;
261280
}
262281

282+
static PyObject *
283+
faulthandler_dump_c_stack_py(PyObject *self,
284+
PyObject *args, PyObject *kwargs)
285+
{
286+
static char *kwlist[] = {"file", NULL};
287+
PyObject *file = NULL;
288+
289+
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
290+
"|O:dump_c_stack", kwlist,
291+
&file)) {
292+
return NULL;
293+
}
294+
295+
int fd = faulthandler_get_fileno(&file);
296+
if (fd < 0) {
297+
return NULL;
298+
}
299+
300+
_Py_DumpStack(fd);
301+
302+
if (PyErr_CheckSignals()) {
303+
return NULL;
304+
}
305+
306+
Py_RETURN_NONE;
307+
}
308+
263309
static void
264310
faulthandler_disable_fatal_handler(fault_handler_t *handler)
265311
{
@@ -350,6 +396,7 @@ faulthandler_fatal_error(int signum)
350396

351397
faulthandler_dump_traceback(fd, deduce_all_threads(),
352398
fatal_error.interp);
399+
faulthandler_dump_c_stack(fd);
353400

354401
_Py_DumpExtensionModules(fd, fatal_error.interp);
355402

@@ -425,6 +472,7 @@ faulthandler_exc_handler(struct _EXCEPTION_POINTERS *exc_info)
425472

426473
faulthandler_dump_traceback(fd, deduce_all_threads(),
427474
fatal_error.interp);
475+
faulthandler_dump_c_stack(fd);
428476

429477
/* call the next exception handler */
430478
return EXCEPTION_CONTINUE_SEARCH;
@@ -519,14 +567,15 @@ faulthandler_enable(void)
519567
static PyObject*
520568
faulthandler_py_enable(PyObject *self, PyObject *args, PyObject *kwargs)
521569
{
522-
static char *kwlist[] = {"file", "all_threads", NULL};
570+
static char *kwlist[] = {"file", "all_threads", "c_stack", NULL};
523571
PyObject *file = NULL;
524572
int all_threads = 1;
525573
int fd;
574+
int c_stack = 1;
526575
PyThreadState *tstate;
527576

528577
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
529-
"|Op:enable", kwlist, &file, &all_threads))
578+
"|Opp:enable", kwlist, &file, &all_threads, &c_stack))
530579
return NULL;
531580

532581
fd = faulthandler_get_fileno(&file);
@@ -543,6 +592,7 @@ faulthandler_py_enable(PyObject *self, PyObject *args, PyObject *kwargs)
543592
fatal_error.fd = fd;
544593
fatal_error.all_threads = all_threads;
545594
fatal_error.interp = PyThreadState_GetInterpreter(tstate);
595+
fatal_error.c_stack = c_stack;
546596

547597
if (faulthandler_enable() < 0) {
548598
return NULL;
@@ -1238,6 +1288,10 @@ static PyMethodDef module_methods[] = {
12381288
PyDoc_STR("dump_traceback($module, /, file=sys.stderr, all_threads=True)\n--\n\n"
12391289
"Dump the traceback of the current thread, or of all threads "
12401290
"if all_threads is True, into file.")},
1291+
{"dump_c_stack",
1292+
_PyCFunction_CAST(faulthandler_dump_c_stack_py), METH_VARARGS|METH_KEYWORDS,
1293+
PyDoc_STR("dump_c_stack($module, /, file=sys.stderr)\n--\n\n"
1294+
"Dump the C stack of the current thread.")},
12411295
{"dump_traceback_later",
12421296
_PyCFunction_CAST(faulthandler_dump_traceback_later), METH_VARARGS|METH_KEYWORDS,
12431297
PyDoc_STR("dump_traceback_later($module, /, timeout, repeat=False, file=sys.stderr, exit=False)\n--\n\n"

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.