-
-
Notifications
You must be signed in to change notification settings - Fork 34.5k
gh-127604: Add C stack dumps to faulthandler
#128159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 33 commits
0ebdc69
0ccf9fb
52945e5
9bdd802
66f2641
0591013
64707a2
8a5b784
4f09358
e1aa619
6b515d3
c69ee9d
f08b1dd
dbb6d25
faf1a3e
ec832aa
524f167
dd08bcb
bda3dcd
a3564a5
f3fcea1
db97dd2
c17457f
d5f7d4b
e79e661
0c84f8a
8198997
0f670f0
079f186
892a085
896abd1
cab079e
1784071
3304e2a
aa97f24
50b4964
533b1db
58b3580
f62dac8
4feaf09
c95369f
95833e7
3e2701d
56127fa
b70bd43
7a070ab
e9c3d7c
195a539
9dd6c3b
ce9c39f
c344ad7
e899792
b136f71
2c381b9
52c0748
bd47026
07a20d0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ | |
| #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) | ||
|
|
@@ -218,6 +219,25 @@ faulthandler_dump_traceback(int fd, int all_threads, | |
| reentrant = 0; | ||
| } | ||
|
|
||
| static void | ||
| faulthandler_dump_c_stack(int fd) | ||
| { | ||
| static volatile int reentrant = 0; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe it is better to use atomics here? AFAIU volatile have different behavior for msvc for different platforms.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could be, but regardless, that should be done in a different PR. |
||
|
|
||
| if (reentrant) { | ||
| return; | ||
| } | ||
|
|
||
| reentrant = 1; | ||
|
ZeroIntensity marked this conversation as resolved.
|
||
|
|
||
| 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) | ||
|
|
@@ -264,6 +284,32 @@ faulthandler_dump_traceback_py(PyObject *self, | |
| Py_RETURN_NONE; | ||
| } | ||
|
|
||
| static PyObject * | ||
| faulthandler_dump_c_stack_py(PyObject *self, | ||
|
picnixz marked this conversation as resolved.
|
||
| PyObject *args, PyObject *kwargs) | ||
| { | ||
| static char *kwlist[] = {"file", NULL}; | ||
| PyObject *file = NULL; | ||
|
ZeroIntensity marked this conversation as resolved.
|
||
|
|
||
| if (!PyArg_ParseTupleAndKeywords(args, kwargs, | ||
| "|p:dump_c_stack", kwlist, | ||
| &file)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PEP 7: add braces
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this got brought up before. The rest of the file avoids the braces, it's probably better to just stay consistent.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I respectfully disagree, we are adding new functions so it's better to go closer to the standard that deviate more from it
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As the author of the original code, I would prefer that new code respects PEP 7 (add braces) :-)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will do. cc @picnixz if you want to bicker about styling (I'm pretty sure you were the one that originally brought this up). |
||
| return NULL; | ||
|
ZeroIntensity marked this conversation as resolved.
|
||
|
|
||
| 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) | ||
| { | ||
|
|
@@ -354,6 +400,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); | ||
|
|
||
|
|
@@ -430,6 +477,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; | ||
|
|
@@ -524,14 +572,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; | ||
|
ZeroIntensity marked this conversation as resolved.
|
||
| 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); | ||
|
|
@@ -547,6 +596,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; | ||
|
|
@@ -1237,6 +1287,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" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,9 @@ | |
| #ifdef HAVE_UNISTD_H | ||
| # include <unistd.h> // lseek() | ||
| #endif | ||
| #ifdef HAVE_EXECINFO_H | ||
| # include <execinfo.h> // backtrace(), backtrace_symbols() | ||
| #endif | ||
|
|
||
|
|
||
| #define OFF(x) offsetof(PyTracebackObject, x) | ||
|
|
@@ -1101,3 +1104,68 @@ _Py_DumpTracebackThreads(int fd, PyInterpreterState *interp, | |
| return NULL; | ||
| } | ||
|
|
||
| #define TRACEBACK_ENTRY_MAX_SIZE 256 | ||
|
|
||
| static void | ||
| format_entry(char *entry_str, const char *the_entry, Py_ssize_t *length_ptr) | ||
| { | ||
| int length = PyOS_snprintf(entry_str, TRACEBACK_ENTRY_MAX_SIZE, " %s\n", the_entry); | ||
| if (length == TRACEBACK_ENTRY_MAX_SIZE) { | ||
| /* We exceeded the size, make it look prettier */ | ||
| // Add ellipsis to last 3 characters | ||
| entry_str[TRACEBACK_ENTRY_MAX_SIZE - 5] = '.'; | ||
| entry_str[TRACEBACK_ENTRY_MAX_SIZE - 4] = '.'; | ||
| entry_str[TRACEBACK_ENTRY_MAX_SIZE - 3] = '.'; | ||
| // Ensure trailing newline | ||
| entry_str[TRACEBACK_ENTRY_MAX_SIZE - 2] = '\n'; | ||
| // Ensure that it's null-terminated | ||
| entry_str[TRACEBACK_ENTRY_MAX_SIZE - 1] = '\0'; | ||
| } | ||
|
|
||
| *length_ptr = (Py_ssize_t)length; | ||
|
ZeroIntensity marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| /* This is for faulthandler. | ||
| * Apparently, backtrace() doesn't play well across DLL boundaries on macOS */ | ||
| #if defined(HAVE_EXECINFO_H) && defined(HAVE_BACKTRACE) && defined(HAVE_BACKTRACE_SYMBOLS) | ||
| void | ||
| _Py_DumpStack(int fd) | ||
| { | ||
| #define BACKTRACE_SIZE 32 | ||
| PUTS(fd, "Current thread's C stack trace (most recent call first):\n"); | ||
| 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; | ||
| } | ||
|
|
||
| char **strings = backtrace_symbols(callstack, BACKTRACE_SIZE); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One note (not a blocker) is that this function can be arbitrarily slow depending on the DWARF level of the binary. I don't think is a huge problem but maybe is worth documenting
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How slow are we talking? Like, a few extra milliseconds, or several seconds to minutes?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both are possible depending on how monstrous the extension modules and other shared objects that are loaded look like. Minutes will be very rare but in heavy monolithic enterprise applications is likely possible.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LOL yeah the https://github.com/abseil/abseil-cpp/tree/master/absl/debugging stack trace dumper could also take a L o n g t i m e on huge C++ binaries. |
||
| if (strings == NULL) { | ||
| PUTS(fd, " <not enough memory to get stack trace>\n"); | ||
| return; | ||
| } | ||
| for (int i = 0; i < frames; ++i) { | ||
| char entry_str[TRACEBACK_ENTRY_MAX_SIZE]; | ||
| Py_ssize_t length; | ||
| format_entry(entry_str, strings[i], &length); | ||
| _Py_write_noraise(fd, entry_str, length); | ||
| } | ||
|
|
||
| if (frames == BACKTRACE_SIZE) { | ||
| PUTS(fd, " <truncated rest of calls>\n"); | ||
| } | ||
|
|
||
| free(strings); | ||
|
ZeroIntensity marked this conversation as resolved.
Outdated
|
||
| #undef BACKTRACE_SIZE | ||
| #undef TRACEBACK_ENTRY_MAX_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 | ||
Uh oh!
There was an error while loading. Please reload this page.