diff --git a/Doc/library/faulthandler.rst b/Doc/library/faulthandler.rst index 3a5badd0ffa..3c49649c74b 100644 --- a/Doc/library/faulthandler.rst +++ b/Doc/library/faulthandler.rst @@ -68,6 +68,9 @@ Fault handler state .. versionchanged:: 3.5 Added support for passing file descriptor to this function. + .. versionchanged:: 3.6 + On Windows, a handler for Windows exception is also installed. + .. function:: disable() Disable the fault handler: uninstall the signal handlers installed by diff --git a/Doc/whatsnew/3.6.rst b/Doc/whatsnew/3.6.rst index 986c145dcc8..928d7486ad6 100644 --- a/Doc/whatsnew/3.6.rst +++ b/Doc/whatsnew/3.6.rst @@ -199,6 +199,14 @@ directives ``%G``, ``%u`` and ``%V``. (Contributed by Ashley Anderson in :issue:`12006`.) +faulthandler +------------ + +On Windows, the :mod:`faulthandler` module now installs an handler for Windows +exceptions: see :func:`faulthandler.enable`. (Contributed by Victor Stinner in +:issue:`23848`.) + + os -- diff --git a/Lib/test/test_faulthandler.py b/Lib/test/test_faulthandler.py index c3cd657e52e..fbd535b9f93 100644 --- a/Lib/test/test_faulthandler.py +++ b/Lib/test/test_faulthandler.py @@ -23,6 +23,7 @@ except ImportError: _testcapi = None TIMEOUT = 0.5 +MS_WINDOWS = (os.name == 'nt') def expected_traceback(lineno1, lineno2, header, min_count=1): regex = header @@ -76,9 +77,9 @@ class FaultHandlerTests(unittest.TestCase): output = output.decode('ascii', 'backslashreplace') return output.splitlines(), exitcode - def check_fatal_error(self, code, line_number, name_regex, - filename=None, all_threads=True, other_regex=None, - fd=None, know_current_thread=True): + def check_error(self, code, line_number, fatal_error, *, + filename=None, all_threads=True, other_regex=None, + fd=None, know_current_thread=True): """ Check that the fault handler for fatal errors is enabled and check the traceback from the child process output. @@ -93,14 +94,14 @@ class FaultHandlerTests(unittest.TestCase): else: header = 'Stack' regex = """ - ^Fatal Python error: {name} + ^{fatal_error} {header} \(most recent call first\): File "", line {lineno} in """ regex = dedent(regex.format( lineno=line_number, - name=name_regex, + fatal_error=fatal_error, header=header)).strip() if other_regex: regex += '|' + other_regex @@ -109,17 +110,36 @@ class FaultHandlerTests(unittest.TestCase): self.assertRegex(output, regex) self.assertNotEqual(exitcode, 0) + def check_fatal_error(self, code, line_number, name_regex, **kw): + fatal_error = 'Fatal Python error: %s' % name_regex + self.check_error(code, line_number, fatal_error, **kw) + + def check_windows_exception(self, code, line_number, name_regex, **kw): + fatal_error = 'Windows exception: %s' % name_regex + self.check_error(code, line_number, fatal_error, **kw) + @unittest.skipIf(sys.platform.startswith('aix'), "the first page of memory is a mapped read-only on AIX") def test_read_null(self): - self.check_fatal_error(""" - import faulthandler - faulthandler.enable() - faulthandler._read_null() - """, - 3, - # Issue #12700: Read NULL raises SIGILL on Mac OS X Lion - '(?:Segmentation fault|Bus error|Illegal instruction)') + if not MS_WINDOWS: + self.check_fatal_error(""" + import faulthandler + faulthandler.enable() + faulthandler._read_null() + """, + 3, + # Issue #12700: Read NULL raises SIGILL on Mac OS X Lion + '(?:Segmentation fault' + '|Bus error' + '|Illegal instruction)') + else: + self.check_windows_exception(""" + import faulthandler + faulthandler.enable() + faulthandler._read_null() + """, + 3, + 'access violation') def test_sigsegv(self): self.check_fatal_error(""" @@ -708,6 +728,22 @@ class FaultHandlerTests(unittest.TestCase): with self.check_stderr_none(): faulthandler.register(signal.SIGUSR1) + @unittest.skipUnless(MS_WINDOWS, 'specific to Windows') + def test_raise_exception(self): + for exc, name in ( + ('EXCEPTION_ACCESS_VIOLATION', 'access violation'), + ('EXCEPTION_INT_DIVIDE_BY_ZERO', 'int divide by zero'), + ('EXCEPTION_STACK_OVERFLOW', 'stack overflow'), + ): + self.check_windows_exception(f""" + import faulthandler + faulthandler.enable() + faulthandler._raise_exception(faulthandler._{exc}) + """, + 3, + name) + + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS b/Misc/NEWS index 29fc65d8396..70ff3de8eb7 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -232,6 +232,10 @@ Core and Builtins Library ------- +- Issue #23848: On Windows, faulthandler.enable() now also installs an + exception handler to dump the traceback of all Python threads on any Windows + exception, not only on UNIX signals (SIGSEGV, SIGFPE, SIGABRT). + - Issue #26530: Add C functions :c:func:`_PyTraceMalloc_Track` and :c:func:`_PyTraceMalloc_Untrack` to track memory blocks using the :mod:`tracemalloc` module. Add :c:func:`_PyTraceMalloc_GetTraceback` to get diff --git a/Modules/faulthandler.c b/Modules/faulthandler.c index 1c247b72a80..22144661bc7 100644 --- a/Modules/faulthandler.c +++ b/Modules/faulthandler.c @@ -119,7 +119,7 @@ static fault_handler_t faulthandler_handlers[] = { handler fails in faulthandler_fatal_error() */ {SIGSEGV, 0, "Segmentation fault", } }; -static const unsigned char faulthandler_nsignals = \ +static const size_t faulthandler_nsignals = \ Py_ARRAY_LENGTH(faulthandler_handlers); #ifdef HAVE_SIGALTSTACK @@ -290,6 +290,19 @@ faulthandler_dump_traceback_py(PyObject *self, Py_RETURN_NONE; } +static void +faulthandler_disable_fatal_handler(fault_handler_t *handler) +{ + if (!handler->enabled) + return; + handler->enabled = 0; +#ifdef HAVE_SIGACTION + (void)sigaction(handler->signum, &handler->previous, NULL); +#else + (void)signal(handler->signum, handler->previous); +#endif +} + /* Handler for SIGSEGV, SIGFPE, SIGABRT, SIGBUS and SIGILL signals. @@ -308,7 +321,7 @@ static void faulthandler_fatal_error(int signum) { const int fd = fatal_error.fd; - unsigned int i; + size_t i; fault_handler_t *handler = NULL; int save_errno = errno; @@ -326,12 +339,7 @@ faulthandler_fatal_error(int signum) } /* restore the previous handler */ -#ifdef HAVE_SIGACTION - (void)sigaction(signum, &handler->previous, NULL); -#else - (void)signal(signum, handler->previous); -#endif - handler->enabled = 0; + faulthandler_disable_fatal_handler(handler); PUTS(fd, "Fatal Python error: "); PUTS(fd, handler->name); @@ -353,20 +361,110 @@ faulthandler_fatal_error(int signum) raise(signum); } +#ifdef MS_WINDOWS +static LONG WINAPI +faulthandler_exc_handler(struct _EXCEPTION_POINTERS *exc_info) +{ + const int fd = fatal_error.fd; + DWORD code = exc_info->ExceptionRecord->ExceptionCode; + + PUTS(fd, "Windows exception: "); + switch (code) + { + /* only format most common errors */ + case EXCEPTION_ACCESS_VIOLATION: PUTS(fd, "access violation"); break; + case EXCEPTION_FLT_DIVIDE_BY_ZERO: PUTS(fd, "float divide by zero"); break; + case EXCEPTION_FLT_OVERFLOW: PUTS(fd, "float overflow"); break; + case EXCEPTION_INT_DIVIDE_BY_ZERO: PUTS(fd, "int divide by zero"); break; + case EXCEPTION_INT_OVERFLOW: PUTS(fd, "integer overflow"); break; + case EXCEPTION_IN_PAGE_ERROR: PUTS(fd, "page error"); break; + case EXCEPTION_STACK_OVERFLOW: PUTS(fd, "stack overflow"); break; + default: + PUTS(fd, "code 0x"); + _Py_DumpHexadecimal(fd, code, sizeof(DWORD)); + } + PUTS(fd, "\n\n"); + + if (code == EXCEPTION_ACCESS_VIOLATION) { + /* disable signal handler for SIGSEGV */ + size_t i; + for (i=0; i < faulthandler_nsignals; i++) { + fault_handler_t *handler = &faulthandler_handlers[i]; + if (handler->signum == SIGSEGV) { + faulthandler_disable_fatal_handler(handler); + break; + } + } + } + + faulthandler_dump_traceback(fd, fatal_error.all_threads, + fatal_error.interp); + + /* call the next exception handler */ + return EXCEPTION_CONTINUE_SEARCH; +} +#endif + /* Install the handler for fatal signals, faulthandler_fatal_error(). */ -static PyObject* -faulthandler_enable(PyObject *self, PyObject *args, PyObject *kwargs) +int +faulthandler_enable(void) { - static char *kwlist[] = {"file", "all_threads", NULL}; - PyObject *file = NULL; - int all_threads = 1; - unsigned int i; + size_t i; fault_handler_t *handler; #ifdef HAVE_SIGACTION struct sigaction action; #endif int err; + + if (fatal_error.enabled) { + return 0; + } + + fatal_error.enabled = 1; + + for (i=0; i < faulthandler_nsignals; i++) { + handler = &faulthandler_handlers[i]; + +#ifdef HAVE_SIGACTION + action.sa_handler = faulthandler_fatal_error; + sigemptyset(&action.sa_mask); + /* Do not prevent the signal from being received from within + its own signal handler */ + action.sa_flags = SA_NODEFER; +#ifdef HAVE_SIGALTSTACK + if (stack.ss_sp != NULL) { + /* Call the signal handler on an alternate signal stack + provided by sigaltstack() */ + action.sa_flags |= SA_ONSTACK; + } +#endif + err = sigaction(handler->signum, &action, &handler->previous); +#else + handler->previous = signal(handler->signum, + faulthandler_fatal_error); + err = (handler->previous == SIG_ERR); +#endif + if (err) { + PyErr_SetFromErrno(PyExc_RuntimeError); + return -1; + } + + handler->enabled = 1; + } + +#ifdef MS_WINDOWS + AddVectoredExceptionHandler(1, faulthandler_exc_handler); +#endif + return 0; +} + +static PyObject* +faulthandler_py_enable(PyObject *self, PyObject *args, PyObject *kwargs) +{ + static char *kwlist[] = {"file", "all_threads", NULL}; + PyObject *file = NULL; + int all_threads = 1; int fd; PyThreadState *tstate; @@ -388,37 +486,10 @@ faulthandler_enable(PyObject *self, PyObject *args, PyObject *kwargs) fatal_error.all_threads = all_threads; fatal_error.interp = tstate->interp; - if (!fatal_error.enabled) { - fatal_error.enabled = 1; - - for (i=0; i < faulthandler_nsignals; i++) { - handler = &faulthandler_handlers[i]; -#ifdef HAVE_SIGACTION - action.sa_handler = faulthandler_fatal_error; - sigemptyset(&action.sa_mask); - /* Do not prevent the signal from being received from within - its own signal handler */ - action.sa_flags = SA_NODEFER; -#ifdef HAVE_SIGALTSTACK - if (stack.ss_sp != NULL) { - /* Call the signal handler on an alternate signal stack - provided by sigaltstack() */ - action.sa_flags |= SA_ONSTACK; - } -#endif - err = sigaction(handler->signum, &action, &handler->previous); -#else - handler->previous = signal(handler->signum, - faulthandler_fatal_error); - err = (handler->previous == SIG_ERR); -#endif - if (err) { - PyErr_SetFromErrno(PyExc_RuntimeError); - return NULL; - } - handler->enabled = 1; - } + if (faulthandler_enable() < 0) { + return NULL; } + Py_RETURN_NONE; } @@ -432,14 +503,7 @@ faulthandler_disable(void) fatal_error.enabled = 0; for (i=0; i < faulthandler_nsignals; i++) { handler = &faulthandler_handlers[i]; - if (!handler->enabled) - continue; -#ifdef HAVE_SIGACTION - (void)sigaction(handler->signum, &handler->previous, NULL); -#else - (void)signal(handler->signum, handler->previous); -#endif - handler->enabled = 0; + faulthandler_disable_fatal_handler(handler); } } @@ -991,7 +1055,10 @@ faulthandler_fatal_error_py(PyObject *self, PyObject *args) Py_RETURN_NONE; } + #if defined(HAVE_SIGALTSTACK) && defined(HAVE_SIGACTION) +#define FAULTHANDLER_STACK_OVERFLOW + #ifdef __INTEL_COMPILER /* Issue #23654: Turn off ICC's tail call optimization for the * stack_overflow generator. ICC turns the recursive tail call into @@ -1005,12 +1072,21 @@ stack_overflow(Py_uintptr_t min_sp, Py_uintptr_t max_sp, size_t *depth) /* allocate 4096 bytes on the stack at each call */ unsigned char buffer[4096]; Py_uintptr_t sp = (Py_uintptr_t)&buffer; + Py_uintptr_t stop; + *depth += 1; - if (sp < min_sp || max_sp < sp) + if (sp < min_sp || max_sp < sp) { + printf("call #%lu\n", (unsigned long)*depth); return sp; - buffer[0] = 1; - buffer[4095] = 0; - return stack_overflow(min_sp, max_sp, depth); + } + + memset(buffer, (unsigned char)*depth, sizeof(buffer)); + stop = stack_overflow(min_sp, max_sp, depth) + buffer[0]; + + memset(buffer, (unsigned char)stop, sizeof(buffer)); + stop = stack_overflow(min_sp, max_sp, depth) + buffer[0]; + + return stop; } static PyObject * @@ -1018,13 +1094,19 @@ faulthandler_stack_overflow(PyObject *self) { size_t depth, size; Py_uintptr_t sp = (Py_uintptr_t)&depth; - Py_uintptr_t stop; + Py_uintptr_t min_sp, max_sp, stop; faulthandler_suppress_crash_report(); + depth = 0; - stop = stack_overflow(sp - STACK_OVERFLOW_MAX_SIZE, - sp + STACK_OVERFLOW_MAX_SIZE, - &depth); + if (sp > STACK_OVERFLOW_MAX_SIZE) + min_sp = sp - STACK_OVERFLOW_MAX_SIZE; + else + min_sp = 0; + max_sp = sp + STACK_OVERFLOW_MAX_SIZE; + + stop = stack_overflow(min_sp, max_sp, &depth); + if (sp < stop) size = stop - sp; else @@ -1035,7 +1117,7 @@ faulthandler_stack_overflow(PyObject *self) size, depth); return NULL; } -#endif +#endif /* (defined(HAVE_SIGALTSTACK) && defined(HAVE_SIGACTION)) ... */ static int @@ -1058,12 +1140,25 @@ faulthandler_traverse(PyObject *module, visitproc visit, void *arg) return 0; } +#ifdef MS_WINDOWS +static PyObject * +faulthandler_raise_exception(PyObject *self, PyObject *args) +{ + unsigned int code, flags = 0; + if (!PyArg_ParseTuple(args, "I|I:_raise_exception", &code, &flags)) + return NULL; + faulthandler_suppress_crash_report(); + RaiseException(code, flags, 0, NULL); + Py_RETURN_NONE; +} +#endif + PyDoc_STRVAR(module_doc, "faulthandler module."); static PyMethodDef module_methods[] = { {"enable", - (PyCFunction)faulthandler_enable, METH_VARARGS|METH_KEYWORDS, + (PyCFunction)faulthandler_py_enable, METH_VARARGS|METH_KEYWORDS, PyDoc_STR("enable(file=sys.stderr, all_threads=True): " "enable the fault handler")}, {"disable", (PyCFunction)faulthandler_disable_py, METH_NOARGS, @@ -1117,9 +1212,13 @@ static PyMethodDef module_methods[] = { PyDoc_STR("_sigfpe(): raise a SIGFPE signal")}, {"_fatal_error", faulthandler_fatal_error_py, METH_VARARGS, PyDoc_STR("_fatal_error(message): call Py_FatalError(message)")}, -#if defined(HAVE_SIGALTSTACK) && defined(HAVE_SIGACTION) +#ifdef FAULTHANDLER_STACK_OVERFLOW {"_stack_overflow", (PyCFunction)faulthandler_stack_overflow, METH_NOARGS, PyDoc_STR("_stack_overflow(): recursive call to raise a stack overflow")}, +#endif +#ifdef MS_WINDOWS + {"_raise_exception", faulthandler_raise_exception, METH_VARARGS, + PyDoc_STR("raise_exception(code, flags=0): Call RaiseException(code, flags).")}, #endif {NULL, NULL} /* sentinel */ }; @@ -1139,7 +1238,33 @@ static struct PyModuleDef module_def = { PyMODINIT_FUNC PyInit_faulthandler(void) { - return PyModule_Create(&module_def); + PyObject *m = PyModule_Create(&module_def); + if (m == NULL) + return NULL; + + /* Add constants for unit tests */ +#ifdef MS_WINDOWS + /* RaiseException() codes (prefixed by an underscore) */ + if (PyModule_AddIntConstant(m, "_EXCEPTION_ACCESS_VIOLATION", + EXCEPTION_ACCESS_VIOLATION)) + return NULL; + if (PyModule_AddIntConstant(m, "_EXCEPTION_INT_DIVIDE_BY_ZERO", + EXCEPTION_INT_DIVIDE_BY_ZERO)) + return NULL; + if (PyModule_AddIntConstant(m, "_EXCEPTION_STACK_OVERFLOW", + EXCEPTION_STACK_OVERFLOW)) + return NULL; + + /* RaiseException() flags (prefixed by an underscore) */ + if (PyModule_AddIntConstant(m, "_EXCEPTION_NONCONTINUABLE", + EXCEPTION_NONCONTINUABLE)) + return NULL; + if (PyModule_AddIntConstant(m, "_EXCEPTION_NONCONTINUABLE_EXCEPTION", + EXCEPTION_NONCONTINUABLE_EXCEPTION)) + return NULL; +#endif + + return m; } /* Call faulthandler.enable() if the PYTHONFAULTHANDLER environment variable