Repository navigation
PyType_GetModuleByDef isn't safe to call in tp_dealloc #160840
Description
Crash report
// example.c
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stddef.h>
static PyModuleDef examplemodule = {
PyModuleDef_HEAD_INIT,
"example",
"Example module with a heap type.",
-1,
NULL,
NULL,
NULL,
NULL,
NULL
};
typedef struct {
PyObject_HEAD
PyObject *first;
PyObject *second;
} HeapPairObject;
static void
HeapPair_dealloc(HeapPairObject *self)
{
PyTypeObject *tp = Py_TYPE(self);
PyType_GetModuleByDef(tp, &examplemodule);
PyObject_GC_UnTrack(self);
Py_XDECREF(self->first);
Py_XDECREF(self->second);
tp->tp_free((PyObject *)self);
Py_CLEAR(tp);
}
static void
HeapPair_clear(HeapPairObject *self) {
Py_CLEAR(self->first);
Py_CLEAR(self->second);
}
static int
HeapPair_traverse(HeapPairObject *self, visitproc visit, void *arg) {
Py_VISIT(self->first);
Py_VISIT(self->second);
Py_VISIT(Py_TYPE(self));
return 0;
}
static PyObject *
HeapPair_new(PyTypeObject *subtype, PyObject *args, PyObject *kwds) {
HeapPairObject *out = (HeapPairObject*)subtype->tp_alloc(subtype, 0);
if (out) {
// Create some exciting circular references
out->first = Py_NewRef(subtype);
out->second = Py_NewRef(out);
}
return (PyObject*)out;
}
static PyMemberDef HeapPair_members[] = {
{"first", Py_T_OBJECT_EX, offsetof(HeapPairObject, first), 0, "first attribute"},
{"second", Py_T_OBJECT_EX, offsetof(HeapPairObject, second), 0, "second attribute"},
{NULL, 0, 0, 0, NULL}
};
static PyType_Slot HeapPair_slots[] = {
{Py_tp_dealloc, HeapPair_dealloc},
{Py_tp_members, HeapPair_members},
{Py_tp_new, HeapPair_new},
{Py_tp_clear, HeapPair_clear},
{Py_tp_traverse, HeapPair_traverse},
{0, NULL},
};
static PyType_Spec HeapPair_spec = {
.name = "example.HeapPair",
.basicsize = sizeof(HeapPairObject),
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HEAPTYPE | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
.slots = HeapPair_slots,
};
PyMODINIT_FUNC
PyInit_example(void)
{
PyObject *m;
PyObject *type;
m = PyModule_Create(&examplemodule);
if (m == NULL) {
return NULL;
}
type = PyType_FromSpec(&HeapPair_spec);
if (type == NULL) {
Py_DECREF(m);
return NULL;
}
if (PyModule_AddObject(m, "HeapPair", type) < 0) {
Py_DECREF(type);
Py_DECREF(m);
return NULL;
}
return m;
}# setup.py - For convenience - you can build it how you like
from setuptools import Extension, setup
setup(
name="example",
version="0.1.0",
ext_modules=[
Extension(
"example",
["example.c"],
)
],
)# test_example.py
import example
def testfun():
class C(example.HeapPair):
pass
C.inst = C()
for n in range(100):
testfun()I build the module and then run python3 test_example.py. It crashes with:
python3: Objects/typeobject.c:5537: PyObject *PyType_GetModuleByDef(PyTypeObject *, PyModuleDef *): Assertion `mro != ((void*)0)' failed.
Aborted (core dumped) python3 test_example.py
What I believe is happening:
I've created a bunch of circular references so my Python type C (which inherits from my C type) and the instance of C will both need the GC to free them. I think it's pretty arbitrary which one gets cleared first. If C gets cleared first then tp_mro is cleared:
Line 7175 in 19270d3
| Py_CLEAR(type->tp_mro); |
However the mro is needed to find the module:
Lines 5919 to 5921 in 19270d3
| PyObject *mro = lookup_tp_mro(type); | |
| // The type must be ready | |
| assert(mro != NULL); |
I don't believe using the _DuringGC variant fixes the problem. I've tested on 3.14, but the relevant bits of the current main CPython source looks the same. I think the same argument also applies to tp_clear too.
Note that one good reason to get the module is if I need the module state for PyObject_GetTypeData (i.e. I need to clear up an opaque type in abi3t).
Obviously there's a bit of a general nightmare here that once the GC is involved then you could also be clearing up the module state too so it's very hard to get a good order of destruction that suits everyone (and I can't see a great solution for that). But obviously this is an assert that I'm hitting so I can't even attempt some best effort error handling.
@encukou - tagging you because I think this is something you might be interested in.
CPython versions tested on:
3.14
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
Python 3.14.6 (main, Jun 23 2026, 15:17:22) [Clang 22.1.3 ]
Activity
[-]`PyType_GetModuleByDef` isn't save to call in `tp_dealloc`[/-][+]`PyType_GetModuleByDef` isn't safe to call in `tp_dealloc`[/+]da-woods commentedon Oct 30, 2026
I think for a lot of proposed uses,
PyType_GetBaseByTokenwould work since it doesn't seem to have the same restriction about MRO currently being non-NULL.