Skip to content

Commit 8dfa840

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

File tree

13 files changed

+378
-69
lines changed

13 files changed

+378
-69
lines changed

Doc/library/faulthandler.rst

+39-1
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

+9
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

+1
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

+3
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

+40
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

+1-1
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)
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

+57-3
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)