Skip to content

Commit 1da0479

Browse files
miss-islingtonMarcel Plch
andauthored
bpo-32374: m_traverse may be called with m_state=NULL (GH-5140)
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. (cherry picked from commit c2b0b12) Co-authored-by: Marcel Plch <[email protected]>
1 parent a954919 commit 1da0479

File tree

5 files changed

+96
-7
lines changed

5 files changed

+96
-7
lines changed

Doc/c-api/module.rst

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,17 +196,23 @@ or request "multi-phase initialization" by returning the definition struct itsel
196196
.. c:member:: traverseproc m_traverse
197197
198198
A traversal function to call during GC traversal of the module object, or
199-
*NULL* if not needed.
199+
*NULL* if not needed. This function may be called before module state
200+
is allocated (:c:func:`PyModule_GetState()` may return `NULL`),
201+
and before the :c:member:`Py_mod_exec` function is executed.
200202
201203
.. c:member:: inquiry m_clear
202204
203205
A clear function to call during GC clearing of the module object, or
204-
*NULL* if not needed.
206+
*NULL* if not needed. This function may be called before module state
207+
is allocated (:c:func:`PyModule_GetState()` may return `NULL`),
208+
and before the :c:member:`Py_mod_exec` function is executed.
205209
206210
.. c:member:: freefunc m_free
207211
208212
A function to call during deallocation of the module object, or *NULL* if
209-
not needed.
213+
not needed. This function may be called before module state
214+
is allocated (:c:func:`PyModule_GetState()` may return `NULL`),
215+
and before the :c:member:`Py_mod_exec` function is executed.
210216
211217
Single-phase initialization
212218
...........................

Lib/test/test_importlib/extension/test_loader.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import unittest
1010
import importlib.util
1111
import importlib
12-
12+
from test.support.script_helper import assert_python_failure
1313

1414
class LoaderTests(abc.LoaderTests):
1515

@@ -267,6 +267,20 @@ def test_nonascii(self):
267267
self.assertEqual(module.__name__, name)
268268
self.assertEqual(module.__doc__, "Module named in %s" % lang)
269269

270+
@unittest.skipIf(not hasattr(sys, 'gettotalrefcount'),
271+
'--with-pydebug has to be enabled for this test')
272+
def test_bad_traverse(self):
273+
''' Issue #32374: Test that traverse fails when accessing per-module
274+
state before Py_mod_exec was executed.
275+
(Multiphase initialization modules only)
276+
'''
277+
script = """if True:
278+
import importlib.util as util
279+
spec = util.find_spec('_testmultiphase')
280+
spec.name = '_testmultiphase_with_bad_traverse'
281+
m = spec.loader.create_module(spec)"""
282+
assert_python_failure("-c", script)
283+
270284

271285
(Frozen_MultiPhaseExtensionModuleTests,
272286
Source_MultiPhaseExtensionModuleTests
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Document that m_traverse for multi-phase initialized modules can be called
2+
with m_state=NULL, and add a sanity check

Modules/_testmultiphase.c

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ typedef struct {
1010
PyObject *x_attr; /* Attributes dictionary */
1111
} ExampleObject;
1212

13+
typedef struct {
14+
PyObject *integer;
15+
} testmultiphase_state;
16+
1317
/* Example methods */
1418

1519
static int
@@ -219,18 +223,21 @@ static int execfunc(PyObject *m)
219223
}
220224

221225
/* Helper for module definitions; there'll be a lot of them */
222-
#define TEST_MODULE_DEF(name, slots, methods) { \
226+
227+
#define TEST_MODULE_DEF_EX(name, slots, methods, statesize, traversefunc) { \
223228
PyModuleDef_HEAD_INIT, /* m_base */ \
224229
name, /* m_name */ \
225230
PyDoc_STR("Test module " name), /* m_doc */ \
226-
0, /* m_size */ \
231+
statesize, /* m_size */ \
227232
methods, /* m_methods */ \
228233
slots, /* m_slots */ \
229-
NULL, /* m_traverse */ \
234+
traversefunc, /* m_traverse */ \
230235
NULL, /* m_clear */ \
231236
NULL, /* m_free */ \
232237
}
233238

239+
#define TEST_MODULE_DEF(name, slots, methods) TEST_MODULE_DEF_EX(name, slots, methods, 0, NULL)
240+
234241
PyModuleDef_Slot main_slots[] = {
235242
{Py_mod_exec, execfunc},
236243
{0, NULL},
@@ -614,6 +621,44 @@ PyInit__testmultiphase_exec_unreported_exception(PyObject *spec)
614621
return PyModuleDef_Init(&def_exec_unreported_exception);
615622
}
616623

624+
static int
625+
bad_traverse(PyObject *self, visitproc visit, void *arg) {
626+
testmultiphase_state *m_state;
627+
628+
m_state = PyModule_GetState(self);
629+
Py_VISIT(m_state->integer);
630+
return 0;
631+
}
632+
633+
static int
634+
execfunc_with_bad_traverse(PyObject *mod) {
635+
testmultiphase_state *m_state;
636+
637+
m_state = PyModule_GetState(mod);
638+
if (m_state == NULL) {
639+
return -1;
640+
}
641+
642+
m_state->integer = PyLong_FromLong(0x7fffffff);
643+
Py_INCREF(m_state->integer);
644+
645+
return 0;
646+
}
647+
648+
static PyModuleDef_Slot slots_with_bad_traverse[] = {
649+
{Py_mod_exec, execfunc_with_bad_traverse},
650+
{0, NULL}
651+
};
652+
653+
static PyModuleDef def_with_bad_traverse = TEST_MODULE_DEF_EX(
654+
"_testmultiphase_with_bad_traverse", slots_with_bad_traverse, NULL,
655+
sizeof(testmultiphase_state), bad_traverse);
656+
657+
PyMODINIT_FUNC
658+
PyInit__testmultiphase_with_bad_traverse(PyObject *spec) {
659+
return PyModuleDef_Init(&def_with_bad_traverse);
660+
}
661+
617662
/*** Helper for imp test ***/
618663

619664
static PyModuleDef imp_dummy_def = TEST_MODULE_DEF("imp_dummy", main_slots, testexport_methods);
@@ -623,3 +668,4 @@ PyInit_imp_dummy(PyObject *spec)
623668
{
624669
return PyModuleDef_Init(&imp_dummy_def);
625670
}
671+

Objects/moduleobject.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@ static PyMemberDef module_members[] = {
2020
{0}
2121
};
2222

23+
24+
/* Helper for sanity check for traverse not handling m_state == NULL
25+
* Issue #32374 */
26+
#ifdef Py_DEBUG
27+
static int
28+
bad_traverse_test(PyObject *self, void *arg) {
29+
assert(self != NULL);
30+
return 0;
31+
}
32+
#endif
33+
2334
PyTypeObject PyModuleDef_Type = {
2435
PyVarObject_HEAD_INIT(&PyType_Type, 0)
2536
"moduledef", /* tp_name */
@@ -338,6 +349,16 @@ PyModule_FromDefAndSpec2(struct PyModuleDef* def, PyObject *spec, int module_api
338349
}
339350
}
340351

352+
/* Sanity check for traverse not handling m_state == NULL
353+
* This doesn't catch all possible cases, but in many cases it should
354+
* make many cases of invalid code crash or raise Valgrind issues
355+
* sooner than they would otherwise.
356+
* Issue #32374 */
357+
#ifdef Py_DEBUG
358+
if (def->m_traverse != NULL) {
359+
def->m_traverse(m, bad_traverse_test, NULL);
360+
}
361+
#endif
341362
Py_DECREF(nameobj);
342363
return m;
343364

0 commit comments

Comments
 (0)