From 250035d134ad482e724f73ce654682254b513ee0 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 18 Jan 2021 20:47:13 +0100 Subject: [PATCH] bpo-42923: Dump extension modules on fatal error (GH-24207) The Py_FatalError() function and the faulthandler module now dump the list of extension modules on a fatal error. Add _Py_DumpExtensionModules() and _PyModule_IsExtension() internal functions. --- Include/internal/pycore_pyerrors.h | 2 + Include/moduleobject.h | 6 +++ Lib/test/test_capi.py | 10 +++++ Lib/test/test_faulthandler.py | 19 +++++++++ .../2021-01-13-12-15-13.bpo-42923.zBiNls.rst | 2 + Modules/faulthandler.c | 3 ++ Objects/moduleobject.c | 13 ++++++ Python/pylifecycle.c | 41 +++++++++++++++++++ 8 files changed, 96 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2021-01-13-12-15-13.bpo-42923.zBiNls.rst diff --git a/Include/internal/pycore_pyerrors.h b/Include/internal/pycore_pyerrors.h index 2cf1160afc0..9dd66aec9c3 100644 --- a/Include/internal/pycore_pyerrors.h +++ b/Include/internal/pycore_pyerrors.h @@ -84,6 +84,8 @@ PyAPI_FUNC(PyObject *) _PyErr_FormatFromCauseTstate( PyAPI_FUNC(int) _PyErr_CheckSignalsTstate(PyThreadState *tstate); +PyAPI_FUNC(void) _Py_DumpExtensionModules(int fd, PyInterpreterState *interp); + #ifdef __cplusplus } #endif diff --git a/Include/moduleobject.h b/Include/moduleobject.h index cf9ad40c0a1..49b116ca1c3 100644 --- a/Include/moduleobject.h +++ b/Include/moduleobject.h @@ -84,6 +84,12 @@ typedef struct PyModuleDef{ freefunc m_free; } PyModuleDef; + +// Internal C API +#ifdef Py_BUILD_CORE +extern int _PyModule_IsExtension(PyObject *obj); +#endif + #ifdef __cplusplus } #endif diff --git a/Lib/test/test_capi.py b/Lib/test/test_capi.py index 0d5c97dcc2a..5e72ba9eb04 100644 --- a/Lib/test/test_capi.py +++ b/Lib/test/test_capi.py @@ -556,6 +556,16 @@ class CAPITest(unittest.TestCase): self.assertIn('Fatal Python error: test_fatal_error: MESSAGE\n', err) + match = re.search('^Extension modules:(.*)$', err, re.MULTILINE) + if not match: + self.fail(f"Cannot find 'Extension modules:' in {err!r}") + modules = set(match.group(1).strip().split(', ')) + # Test _PyModule_IsExtension(): the list doesn't have to + # be exhaustive. + for name in ('sys', 'builtins', '_imp', '_thread', '_weakref', + '_io', 'marshal', '_signal', '_abc', '_testcapi'): + self.assertIn(name, modules) + class TestPendingCalls(unittest.TestCase): diff --git a/Lib/test/test_faulthandler.py b/Lib/test/test_faulthandler.py index bc61aab9c0f..c6b763a9555 100644 --- a/Lib/test/test_faulthandler.py +++ b/Lib/test/test_faulthandler.py @@ -2,6 +2,7 @@ from contextlib import contextmanager import datetime import faulthandler import os +import re import signal import subprocess import sys @@ -329,6 +330,24 @@ class FaultHandlerTests(unittest.TestCase): "%r is present in %r" % (not_expected, stderr)) self.assertNotEqual(exitcode, 0) + @skip_segfault_on_android + def test_dump_ext_modules(self): + code = """ + import faulthandler + faulthandler.enable() + faulthandler._sigsegv() + """ + stderr, exitcode = self.get_output(code) + stderr = '\n'.join(stderr) + match = re.search('^Extension modules:(.*)$', stderr, re.MULTILINE) + if not match: + self.fail(f"Cannot find 'Extension modules:' in {stderr!r}") + modules = set(match.group(1).strip().split(', ')) + # Only check for a few extensions, the list doesn't have to be + # exhaustive. + for ext in ('sys', 'builtins', '_io', 'faulthandler'): + self.assertIn(ext, modules) + def test_is_enabled(self): orig_stderr = sys.stderr try: diff --git a/Misc/NEWS.d/next/Library/2021-01-13-12-15-13.bpo-42923.zBiNls.rst b/Misc/NEWS.d/next/Library/2021-01-13-12-15-13.bpo-42923.zBiNls.rst new file mode 100644 index 00000000000..bb566f982b5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-01-13-12-15-13.bpo-42923.zBiNls.rst @@ -0,0 +1,2 @@ +The :c:func:`Py_FatalError` function and the :mod:`faulthandler` module now +dump the list of extension modules on a fatal error. diff --git a/Modules/faulthandler.c b/Modules/faulthandler.c index fe5dbc1cc0f..da8b7741345 100644 --- a/Modules/faulthandler.c +++ b/Modules/faulthandler.c @@ -1,5 +1,6 @@ #include "Python.h" #include "pycore_initconfig.h" // _PyStatus_ERR +#include "pycore_pyerrors.h" // _Py_DumpExtensionModules #include "pycore_traceback.h" // _Py_DumpTracebackThreads #include #include @@ -349,6 +350,8 @@ faulthandler_fatal_error(int signum) faulthandler_dump_traceback(fd, fatal_error.all_threads, fatal_error.interp); + _Py_DumpExtensionModules(fd, fatal_error.interp); + errno = save_errno; #ifdef MS_WINDOWS if (signum == SIGSEGV) { diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index 6590387dac5..e57ea86e769 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -35,6 +35,19 @@ PyTypeObject PyModuleDef_Type = { }; +int +_PyModule_IsExtension(PyObject *obj) +{ + if (!PyModule_Check(obj)) { + return 0; + } + PyModuleObject *module = (PyModuleObject*)obj; + + struct PyModuleDef *def = module->md_def; + return (def != NULL && def->m_methods != NULL); +} + + PyObject* PyModuleDef_Init(struct PyModuleDef* def) { diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index c02071780b8..ee64b0f125b 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -2496,6 +2496,45 @@ fatal_error_exit(int status) } +// Dump the list of extension modules of sys.modules into fd file descriptor. +// This function is called by a signal handler in faulthandler: avoid memory +// allocations and keep the implementation simple. For example, the list +// is not sorted on purpose. +void +_Py_DumpExtensionModules(int fd, PyInterpreterState *interp) +{ + if (interp == NULL) { + return; + } + PyObject *modules = interp->modules; + if (!PyDict_Check(modules)) { + return; + } + + PUTS(fd, "\nExtension modules: "); + + Py_ssize_t pos = 0; + PyObject *key, *value; + int comma = 0; + while (PyDict_Next(modules, &pos, &key, &value)) { + if (!PyUnicode_Check(key)) { + continue; + } + if (!_PyModule_IsExtension(value)) { + continue; + } + + if (comma) { + PUTS(fd, ", "); + } + comma = 1; + + _Py_DumpASCII(fd, key); + } + PUTS(fd, "\n"); +} + + static void _Py_NO_RETURN fatal_error(int fd, int header, const char *prefix, const char *msg, int status) @@ -2557,6 +2596,8 @@ fatal_error(int fd, int header, const char *prefix, const char *msg, _Py_FatalError_DumpTracebacks(fd, interp, tss_tstate); } + _Py_DumpExtensionModules(fd, interp); + /* The main purpose of faulthandler is to display the traceback. This function already did its best to display a traceback. Disable faulthandler to prevent writing a second traceback