Skip to content

PyType_GetModuleByDef isn't safe to call in tp_dealloc #160840

Open

Description

@da-woods

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:

Py_CLEAR(type->tp_mro);

However the mro is needed to find the module:

cpython/Objects/typeobject.c

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

  1. added
    type-crashA hard crash of the interpreter, possibly with a core dump
    on Oct 28, 2026
  2. changed the title [-]`PyType_GetModuleByDef` isn't save to call in `tp_dealloc`[/-] [+]`PyType_GetModuleByDef` isn't safe to call in `tp_dealloc`[/+] on Oct 28, 2026
  3. da-woods commented on Oct 30, 2026

    @da-woods
    ContributorAuthor

    I think for a lot of proposed uses, PyType_GetBaseByToken would work since it doesn't seem to have the same restriction about MRO currently being non-NULL.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)topic-C-APItype-crashA hard crash of the interpreter, possibly with a core dump

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions