Skip to content

Commit

Permalink
bpo-42923: Dump extension modules on fatal error (GH-24207)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
vstinner committed Jan 18, 2021
1 parent f7b5bac commit 250035d
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Include/internal/pycore_pyerrors.h
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions Include/moduleobject.h
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions Lib/test/test_capi.py
Expand Up @@ -556,6 +556,16 @@ def test_fatal_error(self):
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):

Expand Down
19 changes: 19 additions & 0 deletions Lib/test/test_faulthandler.py
Expand Up @@ -2,6 +2,7 @@
import datetime
import faulthandler
import os
import re
import signal
import subprocess
import sys
Expand Down Expand Up @@ -329,6 +330,24 @@ def test_disable(self):
"%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:
Expand Down
@@ -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.
3 changes: 3 additions & 0 deletions 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 <signal.h>
#include <object.h>
Expand Down Expand Up @@ -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) {
Expand Down
13 changes: 13 additions & 0 deletions Objects/moduleobject.c
Expand Up @@ -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)
{
Expand Down
41 changes: 41 additions & 0 deletions Python/pylifecycle.c
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 250035d

Please sign in to comment.