gh-127604: Add C stack dumps to faulthandler (#128159)

This commit is contained in:
Peter Bierma 2025-04-21 15:48:02 -04:00 committed by GitHub
parent ea8ec95cfa
commit 8dfa840773
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 378 additions and 69 deletions

View file

@ -66,10 +66,41 @@ Dumping the traceback
Added support for passing file descriptor to this function.
Dumping the C stack
-------------------
.. versionadded:: next
.. function:: dump_c_stack(file=sys.stderr)
Dump the C stack trace of the current thread into *file*.
If the Python build does not support it or the operating system
does not provide a stack trace, then this prints an error in place
of a dumped C stack.
.. _c-stack-compatibility:
C Stack Compatibility
*********************
If the system does not support the C-level :manpage:`backtrace(3)`,
:manpage:`backtrace_symbols(3)`, or :manpage:`dladdr(3)`, then C stack dumps
will not work. An error will be printed instead of the stack.
Additionally, some compilers do not support :term:`CPython's <CPython>`
implementation of C stack dumps. As a result, a different error may be printed
instead of the stack, even if the the operating system supports dumping stacks.
.. note::
Dumping C stacks can be arbitrarily slow, depending on the DWARF level
of the binaries in the call stack.
Fault handler state
-------------------
.. function:: enable(file=sys.stderr, all_threads=True)
.. function:: enable(file=sys.stderr, all_threads=True, c_stack=True)
Enable the fault handler: install handlers for the :const:`~signal.SIGSEGV`,
:const:`~signal.SIGFPE`, :const:`~signal.SIGABRT`, :const:`~signal.SIGBUS`
@ -81,6 +112,10 @@ Fault handler state
The *file* must be kept open until the fault handler is disabled: see
:ref:`issue with file descriptors <faulthandler-fd>`.
If *c_stack* is ``True``, then the C stack trace is printed after the Python
traceback, unless the system does not support it. See :func:`dump_c_stack` for
more information on compatibility.
.. versionchanged:: 3.5
Added support for passing file descriptor to this function.
@ -95,6 +130,9 @@ Fault handler state
Only the current thread is dumped if the :term:`GIL` is disabled to
prevent the risk of data races.
.. versionchanged:: next
The dump now displays the C stack trace if *c_stack* is true.
.. function:: disable()
Disable the fault handler: uninstall the signal handlers installed by

View file

@ -699,6 +699,15 @@ errno
(Contributed by James Roy in :gh:`126585`.)
faulthandler
------------
* Add support for printing the C stack trace on systems that
:ref:`support it <c-stack-compatibility>` via :func:`faulthandler.dump_c_stack`
or via the *c_stack* argument in :func:`faulthandler.enable`.
(Contributed by Peter Bierma in :gh:`127604`.)
fnmatch
-------

View file

@ -56,6 +56,7 @@ struct _faulthandler_runtime_state {
#ifdef MS_WINDOWS
void *exc_handler;
#endif
int c_stack;
} fatal_error;
struct {

View file

@ -99,6 +99,9 @@ extern int _PyTraceBack_Print(
extern int _Py_WriteIndentedMargin(int, const char*, PyObject *);
extern int _Py_WriteIndent(int, PyObject *);
// Export for the faulthandler module
PyAPI_FUNC(void) _Py_DumpStack(int fd);
#ifdef __cplusplus
}
#endif

View file

@ -55,6 +55,13 @@ def temporary_filename():
finally:
os_helper.unlink(filename)
ADDRESS_EXPR = "0x[0-9a-f]+"
C_STACK_REGEX = [
r"Current thread's C stack trace \(most recent call first\):",
fr'( Binary file ".+"(, at .*(\+|-){ADDRESS_EXPR})? \[{ADDRESS_EXPR}\])|(<.+>)'
]
class FaultHandlerTests(unittest.TestCase):
def get_output(self, code, filename=None, fd=None):
@ -103,6 +110,7 @@ class FaultHandlerTests(unittest.TestCase):
fd=None, know_current_thread=True,
py_fatal_error=False,
garbage_collecting=False,
c_stack=True,
function='<module>'):
"""
Check that the fault handler for fatal errors is enabled and check the
@ -134,6 +142,8 @@ class FaultHandlerTests(unittest.TestCase):
if garbage_collecting and not all_threads_disabled:
regex.append(' Garbage-collecting')
regex.append(fr' File "<string>", line {lineno} in {function}')
if c_stack:
regex.extend(C_STACK_REGEX)
regex = '\n'.join(regex)
if other_regex:
@ -950,5 +960,35 @@ class FaultHandlerTests(unittest.TestCase):
_, exitcode = self.get_output(code)
self.assertEqual(exitcode, 0)
def check_c_stack(self, output):
starting_line = output.pop(0)
self.assertRegex(starting_line, C_STACK_REGEX[0])
self.assertGreater(len(output), 0)
for line in output:
with self.subTest(line=line):
if line != '': # Ignore trailing or leading newlines
self.assertRegex(line, C_STACK_REGEX[1])
def test_dump_c_stack(self):
code = dedent("""
import faulthandler
faulthandler.dump_c_stack()
""")
output, exitcode = self.get_output(code)
self.assertEqual(exitcode, 0)
self.check_c_stack(output)
def test_dump_c_stack_file(self):
import tempfile
with tempfile.TemporaryFile("w+") as tmp:
faulthandler.dump_c_stack(file=tmp)
tmp.flush() # Just in case
tmp.seek(0)
self.check_c_stack(tmp.read().split("\n"))
if __name__ == "__main__":
unittest.main()

View file

@ -5760,7 +5760,7 @@ class TestSignatureDefinitions(unittest.TestCase):
def test_faulthandler_module_has_signatures(self):
import faulthandler
unsupported_signature = {'dump_traceback', 'dump_traceback_later', 'enable'}
unsupported_signature = {'dump_traceback', 'dump_traceback_later', 'enable', 'dump_c_stack'}
unsupported_signature |= {name for name in ['register']
if hasattr(faulthandler, name)}
self._test_module_has_signatures(faulthandler, unsupported_signature=unsupported_signature)

View file

@ -0,0 +1,3 @@
Add support for printing the C stack trace on systems that support it via
:func:`faulthandler.dump_c_stack` or via the *c_stack* argument in
:func:`faulthandler.enable`.

View file

@ -9,10 +9,10 @@
#include "pycore_sysmodule.h" // _PySys_GetRequiredAttr()
#include "pycore_time.h" // _PyTime_FromSecondsObject()
#include "pycore_traceback.h" // _Py_DumpTracebackThreads
#ifdef HAVE_UNISTD_H
# include <unistd.h> // _exit()
#endif
#include <signal.h> // sigaction()
#include <stdlib.h> // abort()
#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,
reentrant = 0;
}
static void
faulthandler_dump_c_stack(int fd)
{
static volatile int reentrant = 0;
if (reentrant) {
return;
}
reentrant = 1;
if (fatal_error.c_stack) {
PUTS(fd, "\n");
_Py_DumpStack(fd);
}
reentrant = 0;
}
static PyObject*
faulthandler_dump_traceback_py(PyObject *self,
PyObject *args, PyObject *kwargs)
@ -260,6 +279,33 @@ faulthandler_dump_traceback_py(PyObject *self,
Py_RETURN_NONE;
}
static PyObject *
faulthandler_dump_c_stack_py(PyObject *self,
PyObject *args, PyObject *kwargs)
{
static char *kwlist[] = {"file", NULL};
PyObject *file = NULL;
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
"|O:dump_c_stack", kwlist,
&file)) {
return NULL;
}
int fd = faulthandler_get_fileno(&file);
if (fd < 0) {
return NULL;
}
_Py_DumpStack(fd);
if (PyErr_CheckSignals()) {
return NULL;
}
Py_RETURN_NONE;
}
static void
faulthandler_disable_fatal_handler(fault_handler_t *handler)
{
@ -350,6 +396,7 @@ faulthandler_fatal_error(int signum)
faulthandler_dump_traceback(fd, deduce_all_threads(),
fatal_error.interp);
faulthandler_dump_c_stack(fd);
_Py_DumpExtensionModules(fd, fatal_error.interp);
@ -425,6 +472,7 @@ faulthandler_exc_handler(struct _EXCEPTION_POINTERS *exc_info)
faulthandler_dump_traceback(fd, deduce_all_threads(),
fatal_error.interp);
faulthandler_dump_c_stack(fd);
/* call the next exception handler */
return EXCEPTION_CONTINUE_SEARCH;
@ -519,14 +567,15 @@ faulthandler_enable(void)
static PyObject*
faulthandler_py_enable(PyObject *self, PyObject *args, PyObject *kwargs)
{
static char *kwlist[] = {"file", "all_threads", NULL};
static char *kwlist[] = {"file", "all_threads", "c_stack", NULL};
PyObject *file = NULL;
int all_threads = 1;
int fd;
int c_stack = 1;
PyThreadState *tstate;
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
"|Op:enable", kwlist, &file, &all_threads))
"|Opp:enable", kwlist, &file, &all_threads, &c_stack))
return NULL;
fd = faulthandler_get_fileno(&file);
@ -543,6 +592,7 @@ faulthandler_py_enable(PyObject *self, PyObject *args, PyObject *kwargs)
fatal_error.fd = fd;
fatal_error.all_threads = all_threads;
fatal_error.interp = PyThreadState_GetInterpreter(tstate);
fatal_error.c_stack = c_stack;
if (faulthandler_enable() < 0) {
return NULL;
@ -1238,6 +1288,10 @@ static PyMethodDef module_methods[] = {
PyDoc_STR("dump_traceback($module, /, file=sys.stderr, all_threads=True)\n--\n\n"
"Dump the traceback of the current thread, or of all threads "
"if all_threads is True, into file.")},
{"dump_c_stack",
_PyCFunction_CAST(faulthandler_dump_c_stack_py), METH_VARARGS|METH_KEYWORDS,
PyDoc_STR("dump_c_stack($module, /, file=sys.stderr)\n--\n\n"
"Dump the C stack of the current thread.")},
{"dump_traceback_later",
_PyCFunction_CAST(faulthandler_dump_traceback_later), METH_VARARGS|METH_KEYWORDS,
PyDoc_STR("dump_traceback_later($module, /, timeout, repeat=False, file=sys.stderr, exit=False)\n--\n\n"

View file

@ -18,7 +18,25 @@
#ifdef HAVE_UNISTD_H
# include <unistd.h> // lseek()
#endif
#if defined(HAVE_EXECINFO_H) && defined(HAVE_DLFCN_H) && defined(HAVE_LINK_H)
# include <execinfo.h> // backtrace(), backtrace_symbols()
# include <dlfcn.h> // dladdr1()
# include <link.h> // struct DL_info
# if defined(HAVE_BACKTRACE) && defined(HAVE_BACKTRACE_SYMBOLS) && defined(HAVE_DLADDR1)
# define CAN_C_BACKTRACE
# endif
#endif
#if defined(__STDC_NO_VLA__) && (__STDC_NO_VLA__ == 1)
/* Use alloca() for VLAs. */
# define VLA(type, name, size) type *name = alloca(size)
#elif !defined(__STDC_NO_VLA__) || (__STDC_NO_VLA__ == 0)
/* Use actual C VLAs.*/
# define VLA(type, name, size) type name[size]
#elif defined(CAN_C_BACKTRACE)
/* VLAs are not possible. Disable C stack trace functions. */
# undef CAN_C_BACKTRACE
#endif
#define OFF(x) offsetof(PyTracebackObject, x)
#define PUTS(fd, str) (void)_Py_write_noraise(fd, str, strlen(str))
@ -1166,3 +1184,93 @@ _Py_DumpTracebackThreads(int fd, PyInterpreterState *interp,
return NULL;
}
#ifdef CAN_C_BACKTRACE
/* Based on glibc's implementation of backtrace_symbols(), but only uses stack memory. */
void
_Py_backtrace_symbols_fd(int fd, void *const *array, Py_ssize_t size)
{
VLA(Dl_info, info, size);
VLA(int, status, size);
/* Fill in the information we can get from dladdr() */
for (Py_ssize_t i = 0; i < size; ++i) {
struct link_map *map;
status[i] = dladdr1(array[i], &info[i], (void **)&map, RTLD_DL_LINKMAP);
if (status[i] != 0
&& info[i].dli_fname != NULL
&& info[i].dli_fname[0] != '\0') {
/* The load bias is more useful to the user than the load
address. The use of these addresses is to calculate an
address in the ELF file, so its prelinked bias is not
something we want to subtract out */
info[i].dli_fbase = (void *) map->l_addr;
}
}
for (Py_ssize_t i = 0; i < size; ++i) {
if (status[i] == 0
|| info[i].dli_fname == NULL
|| info[i].dli_fname[0] == '\0'
) {
dprintf(fd, " Binary file '<unknown>' [%p]\n", array[i]);
continue;
}
if (info[i].dli_sname == NULL) {
/* We found no symbol name to use, so describe it as
relative to the file. */
info[i].dli_saddr = info[i].dli_fbase;
}
if (info[i].dli_sname == NULL
&& info[i].dli_saddr == 0) {
dprintf(fd, " Binary file \"%s\" [%p]\n",
info[i].dli_fname,
array[i]);
}
else {
char sign;
ptrdiff_t offset;
if (array[i] >= (void *) info[i].dli_saddr) {
sign = '+';
offset = array[i] - info[i].dli_saddr;
}
else {
sign = '-';
offset = info[i].dli_saddr - array[i];
}
const char *symbol_name = info[i].dli_sname != NULL ? info[i].dli_sname : "";
dprintf(fd, " Binary file \"%s\", at %s%c%#tx [%p]\n",
info[i].dli_fname,
symbol_name,
sign, offset, array[i]);
}
}
}
void
_Py_DumpStack(int fd)
{
#define BACKTRACE_SIZE 32
PUTS(fd, "Current thread's C stack trace (most recent call first):\n");
VLA(void *, callstack, BACKTRACE_SIZE);
int frames = backtrace(callstack, BACKTRACE_SIZE);
if (frames == 0) {
// Some systems won't return anything for the stack trace
PUTS(fd, " <system returned no stack trace>\n");
return;
}
_Py_backtrace_symbols_fd(fd, callstack, frames);
if (frames == BACKTRACE_SIZE) {
PUTS(fd, " <truncated rest of calls>\n");
}
#undef BACKTRACE_SIZE
}
#else
void
_Py_DumpStack(int fd)
{
PUTS(fd, "Current thread's C stack trace (most recent call first):\n");
PUTS(fd, " <cannot get C stack on this system>\n");
}
#endif

View file

@ -166,6 +166,7 @@ Python/sysmodule.c - _preinit_xoptions -
# thread-safety
# XXX need race protection?
Modules/faulthandler.c faulthandler_dump_traceback reentrant -
Modules/faulthandler.c faulthandler_dump_c_stack reentrant -
Python/pylifecycle.c _Py_FatalErrorFormat reentrant -
Python/pylifecycle.c fatal_error reentrant -

Can't render this file because it has a wrong number of fields in line 4.

161
configure generated vendored
View file

@ -2313,6 +2313,70 @@ fi
} # ac_fn_c_try_run
# ac_fn_c_check_func LINENO FUNC VAR
# ----------------------------------
# Tests whether FUNC exists, setting the cache variable VAR accordingly
ac_fn_c_check_func ()
{
as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $2" >&5
printf %s "checking for $2... " >&6; }
if eval test \${$3+y}
then :
printf %s "(cached) " >&6
else case e in #(
e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext
/* end confdefs.h. */
/* Define $2 to an innocuous variant, in case <limits.h> declares $2.
For example, HP-UX 11i <limits.h> declares gettimeofday. */
#define $2 innocuous_$2
/* System header to define __stub macros and hopefully few prototypes,
which can conflict with char $2 (void); below. */
#include <limits.h>
#undef $2
/* Override any GCC internal prototype to avoid an error.
Use char because int might match the return type of a GCC
builtin and then its argument prototype would still apply. */
#ifdef __cplusplus
extern "C"
#endif
char $2 (void);
/* The GNU C library defines this for functions which it implements
to always fail with ENOSYS. Some functions are actually named
something starting with __ and the normal name is an alias. */
#if defined __stub_$2 || defined __stub___$2
choke me
#endif
int
main (void)
{
return $2 ();
;
return 0;
}
_ACEOF
if ac_fn_c_try_link "$LINENO"
then :
eval "$3=yes"
else case e in #(
e) eval "$3=no" ;;
esac
fi
rm -f core conftest.err conftest.$ac_objext conftest.beam \
conftest$ac_exeext conftest.$ac_ext ;;
esac
fi
eval ac_res=\$$3
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5
printf "%s\n" "$ac_res" >&6; }
eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno
} # ac_fn_c_check_func
# ac_fn_c_check_type LINENO TYPE VAR INCLUDES
# -------------------------------------------
# Tests whether TYPE exists after having included INCLUDES, setting cache
@ -2567,70 +2631,6 @@ rm -f conftest.val
} # ac_fn_c_compute_int
# ac_fn_c_check_func LINENO FUNC VAR
# ----------------------------------
# Tests whether FUNC exists, setting the cache variable VAR accordingly
ac_fn_c_check_func ()
{
as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $2" >&5
printf %s "checking for $2... " >&6; }
if eval test \${$3+y}
then :
printf %s "(cached) " >&6
else case e in #(
e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext
/* end confdefs.h. */
/* Define $2 to an innocuous variant, in case <limits.h> declares $2.
For example, HP-UX 11i <limits.h> declares gettimeofday. */
#define $2 innocuous_$2
/* System header to define __stub macros and hopefully few prototypes,
which can conflict with char $2 (void); below. */
#include <limits.h>
#undef $2
/* Override any GCC internal prototype to avoid an error.
Use char because int might match the return type of a GCC
builtin and then its argument prototype would still apply. */
#ifdef __cplusplus
extern "C"
#endif
char $2 (void);
/* The GNU C library defines this for functions which it implements
to always fail with ENOSYS. Some functions are actually named
something starting with __ and the normal name is an alias. */
#if defined __stub_$2 || defined __stub___$2
choke me
#endif
int
main (void)
{
return $2 ();
;
return 0;
}
_ACEOF
if ac_fn_c_try_link "$LINENO"
then :
eval "$3=yes"
else case e in #(
e) eval "$3=no" ;;
esac
fi
rm -f core conftest.err conftest.$ac_objext conftest.beam \
conftest$ac_exeext conftest.$ac_ext ;;
esac
fi
eval ac_res=\$$3
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5
printf "%s\n" "$ac_res" >&6; }
eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno
} # ac_fn_c_check_func
# ac_fn_check_decl LINENO SYMBOL VAR INCLUDES EXTRA-OPTIONS FLAG-VAR
# ------------------------------------------------------------------
# Tests whether SYMBOL is declared in INCLUDES, setting cache variable VAR
@ -11877,6 +11877,39 @@ fi
fi
# for faulthandler
for ac_header in execinfo.h link.h dlfcn.h
do :
as_ac_Header=`printf "%s\n" "ac_cv_header_$ac_header" | sed "$as_sed_sh"`
ac_fn_c_check_header_compile "$LINENO" "$ac_header" "$as_ac_Header" "$ac_includes_default"
if eval test \"x\$"$as_ac_Header"\" = x"yes"
then :
cat >>confdefs.h <<_ACEOF
#define `printf "%s\n" "HAVE_$ac_header" | sed "$as_sed_cpp"` 1
_ACEOF
ac_fn_c_check_func "$LINENO" "backtrace" "ac_cv_func_backtrace"
if test "x$ac_cv_func_backtrace" = xyes
then :
printf "%s\n" "#define HAVE_BACKTRACE 1" >>confdefs.h
fi
ac_fn_c_check_func "$LINENO" "backtrace_symbols" "ac_cv_func_backtrace_symbols"
if test "x$ac_cv_func_backtrace_symbols" = xyes
then :
printf "%s\n" "#define HAVE_BACKTRACE_SYMBOLS 1" >>confdefs.h
fi
ac_fn_c_check_func "$LINENO" "dladdr1" "ac_cv_func_dladdr1"
if test "x$ac_cv_func_dladdr1" = xyes
then :
printf "%s\n" "#define HAVE_DLADDR1 1" >>confdefs.h
fi
fi
done
# bluetooth/bluetooth.h has been known to not compile with -std=c99.
# http://permalink.gmane.org/gmane.linux.bluez.kernel/22294
SAVE_CFLAGS=$CFLAGS

View file

@ -2985,6 +2985,10 @@ AC_CHECK_HEADERS([ \
AC_HEADER_DIRENT
AC_HEADER_MAJOR
# for faulthandler
AC_CHECK_HEADERS([execinfo.h link.h dlfcn.h],
[AC_CHECK_FUNCS(backtrace backtrace_symbols dladdr1)])
# bluetooth/bluetooth.h has been known to not compile with -std=c99.
# http://permalink.gmane.org/gmane.linux.bluez.kernel/22294
SAVE_CFLAGS=$CFLAGS

View file

@ -89,6 +89,12 @@
/* Define to 1 if you have the 'atanh' function. */
#undef HAVE_ATANH
/* Define to 1 if you have the 'backtrace' function. */
#undef HAVE_BACKTRACE
/* Define to 1 if you have the 'backtrace_symbols' function. */
#undef HAVE_BACKTRACE_SYMBOLS
/* Define if you have the 'bind' function. */
#undef HAVE_BIND
@ -289,6 +295,9 @@
/* Define to 1 if you have the 'dladdr' function. */
#undef HAVE_DLADDR
/* Define to 1 if you have the 'dladdr1' function. */
#undef HAVE_DLADDR1
/* Define to 1 if you have the <dlfcn.h> header file. */
#undef HAVE_DLFCN_H
@ -334,6 +343,9 @@
/* Define if you have the 'eventfd' function. */
#undef HAVE_EVENTFD
/* Define to 1 if you have the <execinfo.h> header file. */
#undef HAVE_EXECINFO_H
/* Define to 1 if you have the 'execv' function. */
#undef HAVE_EXECV
@ -705,6 +717,9 @@
/* Define to 1 if you have the 'linkat' function. */
#undef HAVE_LINKAT
/* Define to 1 if you have the <link.h> header file. */
#undef HAVE_LINK_H
/* Define to 1 if you have the <linux/auxvec.h> header file. */
#undef HAVE_LINUX_AUXVEC_H