]> granicus.if.org Git - python/commitdiff
bpo-32374: m_traverse may be called with m_state=NULL (GH-5140)
authorMarcel Plch <gmarcel.plch@gmail.com>
Sat, 17 Mar 2018 05:41:20 +0000 (06:41 +0100)
committerNick Coghlan <ncoghlan@gmail.com>
Sat, 17 Mar 2018 05:41:20 +0000 (15:41 +1000)
Multi-phase initialized modules allow m_traverse to be called while the
module is still being initialized, so module authors may need to account
for that.

Doc/c-api/module.rst
Lib/test/test_importlib/extension/test_loader.py
Misc/NEWS.d/next/C API/2018-01-09-17-03-54.bpo-32374.SwwLoz.rst [new file with mode: 0644]
Modules/_testmultiphase.c
Objects/moduleobject.c

index 7efab28af724aa4754ccc63319679eed7c3c3afb..017b656854a8cd1e5231d507597ef3edfddeefbe 100644 (file)
@@ -196,17 +196,23 @@ or request "multi-phase initialization" by returning the definition struct itsel
    .. c:member:: traverseproc m_traverse
 
       A traversal function to call during GC traversal of the module object, or
-      *NULL* if not needed.
+      *NULL* if not needed. This function may be called before module state
+      is allocated (:c:func:`PyModule_GetState()` may return `NULL`),
+      and before the :c:member:`Py_mod_exec` function is executed.
 
    .. c:member:: inquiry m_clear
 
       A clear function to call during GC clearing of the module object, or
-      *NULL* if not needed.
+      *NULL* if not needed. This function may be called before module state
+      is allocated (:c:func:`PyModule_GetState()` may return `NULL`),
+      and before the :c:member:`Py_mod_exec` function is executed.
 
    .. c:member:: freefunc m_free
 
       A function to call during deallocation of the module object, or *NULL* if
-      not needed.
+      not needed. This function may be called before module state
+      is allocated (:c:func:`PyModule_GetState()` may return `NULL`),
+      and before the :c:member:`Py_mod_exec` function is executed.
 
 Single-phase initialization
 ...........................
index 8d20040aab8d9a2e1555bb23a1d77a48d955ac79..53ac3c71d4b500f19bdb844b2d5b8680ac2e3707 100644 (file)
@@ -9,7 +9,7 @@ import types
 import unittest
 import importlib.util
 import importlib
-
+from test.support.script_helper import assert_python_failure
 
 class LoaderTests(abc.LoaderTests):
 
@@ -267,6 +267,20 @@ class MultiPhaseExtensionModuleTests(abc.LoaderTests):
                 self.assertEqual(module.__name__, name)
                 self.assertEqual(module.__doc__, "Module named in %s" % lang)
 
+    @unittest.skipIf(not hasattr(sys, 'gettotalrefcount'),
+            '--with-pydebug has to be enabled for this test')
+    def test_bad_traverse(self):
+        ''' Issue #32374: Test that traverse fails when accessing per-module
+            state before Py_mod_exec was executed.
+            (Multiphase initialization modules only)
+        '''
+        script = """if True:
+                import importlib.util as util
+                spec = util.find_spec('_testmultiphase')
+                spec.name = '_testmultiphase_with_bad_traverse'
+                m = spec.loader.create_module(spec)"""
+        assert_python_failure("-c", script)
+
 
 (Frozen_MultiPhaseExtensionModuleTests,
  Source_MultiPhaseExtensionModuleTests
diff --git a/Misc/NEWS.d/next/C API/2018-01-09-17-03-54.bpo-32374.SwwLoz.rst b/Misc/NEWS.d/next/C API/2018-01-09-17-03-54.bpo-32374.SwwLoz.rst
new file mode 100644 (file)
index 0000000..f9cf6d6
--- /dev/null
@@ -0,0 +1,2 @@
+Document that m_traverse for multi-phase initialized modules can be called
+with m_state=NULL, and add a sanity check
index 9b04ebf15586c1e991a73deec540698957697149..8090a485f4a9f18bfc0333cff3f1158db4d690e6 100644 (file)
@@ -10,6 +10,10 @@ typedef struct {
     PyObject            *x_attr;        /* Attributes dictionary */
 } ExampleObject;
 
+typedef struct {
+    PyObject *integer;
+} testmultiphase_state;
+
 /* Example methods */
 
 static int
@@ -218,18 +222,21 @@ static int execfunc(PyObject *m)
 }
 
 /* Helper for module definitions; there'll be a lot of them */
-#define TEST_MODULE_DEF(name, slots, methods) { \
+
+#define TEST_MODULE_DEF_EX(name, slots, methods, statesize, traversefunc) { \
     PyModuleDef_HEAD_INIT,                      /* m_base */ \
     name,                                       /* m_name */ \
     PyDoc_STR("Test module " name),             /* m_doc */ \
-    0,                                          /* m_size */ \
+    statesize,                                  /* m_size */ \
     methods,                                    /* m_methods */ \
     slots,                                      /* m_slots */ \
-    NULL,                                       /* m_traverse */ \
+    traversefunc,                               /* m_traverse */ \
     NULL,                                       /* m_clear */ \
     NULL,                                       /* m_free */ \
 }
 
+#define TEST_MODULE_DEF(name, slots, methods) TEST_MODULE_DEF_EX(name, slots, methods, 0, NULL)
+
 PyModuleDef_Slot main_slots[] = {
     {Py_mod_exec, execfunc},
     {0, NULL},
@@ -613,6 +620,44 @@ PyInit__testmultiphase_exec_unreported_exception(PyObject *spec)
     return PyModuleDef_Init(&def_exec_unreported_exception);
 }
 
+static int
+bad_traverse(PyObject *self, visitproc visit, void *arg) {
+    testmultiphase_state *m_state;
+
+    m_state = PyModule_GetState(self);
+    Py_VISIT(m_state->integer);
+    return 0;
+}
+
+static int
+execfunc_with_bad_traverse(PyObject *mod) {
+    testmultiphase_state *m_state;
+
+    m_state = PyModule_GetState(mod);
+    if (m_state == NULL) {
+        return -1;
+    }
+
+    m_state->integer = PyLong_FromLong(0x7fffffff);
+    Py_INCREF(m_state->integer);
+
+    return 0;
+}
+
+static PyModuleDef_Slot slots_with_bad_traverse[] = {
+    {Py_mod_exec, execfunc_with_bad_traverse},
+    {0, NULL}
+};
+
+static PyModuleDef def_with_bad_traverse = TEST_MODULE_DEF_EX(
+       "_testmultiphase_with_bad_traverse", slots_with_bad_traverse, NULL,
+       sizeof(testmultiphase_state), bad_traverse);
+
+PyMODINIT_FUNC
+PyInit__testmultiphase_with_bad_traverse(PyObject *spec) {
+    return PyModuleDef_Init(&def_with_bad_traverse);
+}
+
 /*** Helper for imp test ***/
 
 static PyModuleDef imp_dummy_def = TEST_MODULE_DEF("imp_dummy", main_slots, testexport_methods);
@@ -622,3 +667,4 @@ PyInit_imp_dummy(PyObject *spec)
 {
     return PyModuleDef_Init(&imp_dummy_def);
 }
+
index d6cde4024336c51dc5e8e4d52f2983c2279215fe..8fb368e41474a8445552345ca45aaa1cc25588be 100644 (file)
@@ -21,6 +21,17 @@ static PyMemberDef module_members[] = {
     {0}
 };
 
+
+/* Helper for sanity check for traverse not handling m_state == NULL
+ * Issue #32374 */
+#ifdef Py_DEBUG
+static int
+bad_traverse_test(PyObject *self, void *arg) {
+    assert(self != NULL);
+    return 0;
+}
+#endif
+
 PyTypeObject PyModuleDef_Type = {
     PyVarObject_HEAD_INIT(&PyType_Type, 0)
     "moduledef",                                /* tp_name */
@@ -345,6 +356,16 @@ PyModule_FromDefAndSpec2(struct PyModuleDef* def, PyObject *spec, int module_api
         }
     }
 
+    /* Sanity check for traverse not handling m_state == NULL
+     * This doesn't catch all possible cases, but in many cases it should
+     * make many cases of invalid code crash or raise Valgrind issues
+     * sooner than they would otherwise.
+     * Issue #32374 */
+#ifdef Py_DEBUG
+    if (def->m_traverse != NULL) {
+        def->m_traverse(m, bad_traverse_test, NULL);
+    }
+#endif
     Py_DECREF(nameobj);
     return m;