diff --git a/Doc/c-api/typeobj.rst b/Doc/c-api/typeobj.rst index 4000f8aa1d4c0c7..097596db6840aa0 100644 --- a/Doc/c-api/typeobj.rst +++ b/Doc/c-api/typeobj.rst @@ -1565,6 +1565,15 @@ and :c:data:`PyType_Type` effectively act as defaults.) type object. This is exposed as the :attr:`~type.__doc__` attribute on the type and instances of the type. + For a heap type (:c:macro:`Py_TPFLAGS_HEAPTYPE`), if this field is set + directly rather than through the :c:data:`Py_tp_doc` slot, the string + must be allocated with :c:func:`PyMem_Malloc`, since CPython frees it + with :c:func:`PyMem_Free` when the type is deallocated. (As a + backwards-compatibility fallback, a string allocated with + :c:func:`PyObject_Malloc` is also detected and freed correctly, but + this fallback is not guaranteed and may be removed in a future + version -- see :gh:`118909`.) + **Inheritance:** This field is *not* inherited by subtypes. diff --git a/Lib/test/test_capi/test_mem.py b/Lib/test/test_capi/test_mem.py index e6389b4e0c1e924..e21043c2df3eb5a 100644 --- a/Lib/test/test_capi/test_mem.py +++ b/Lib/test/test_capi/test_mem.py @@ -92,6 +92,17 @@ def test_pyobject_malloc_without_gil(self): code = 'import _testcapi; _testcapi.pyobject_malloc_without_gil()' self.check_malloc_without_gil(code) + def test_pyobject_malloc_tp_doc(self): + # gh-118909: tp_doc allocated with PyObject_Malloc() (as some + # extensions do) must still be freeable when the type is + # deallocated, even with debug allocator hooks enabled. + assert_python_ok( + '-c', 'import _testcapi; _testcapi.test_pyobject_malloc_tp_doc()', + PYTHONMALLOC=self.PYTHONMALLOC, + MALLOC_CONF="junk:false", + MALLOC_OPTIONS="j", + ) + def check_pyobject_is_freed(self, func_name): code = textwrap.dedent(f''' import gc, os, sys, _testinternalcapi diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-05-15-13-15.gh-issue-118909.p3Zk8q.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-05-15-13-15.gh-issue-118909.p3Zk8q.rst new file mode 100644 index 000000000000000..e8affbe6b46fdee --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-05-15-13-15.gh-issue-118909.p3Zk8q.rst @@ -0,0 +1,4 @@ +Fix a crash in debug builds (or with :envvar:`PYTHONMALLOC=debug `) +when deallocating a heap type whose :c:member:`~PyTypeObject.tp_doc` was +allocated with :c:func:`PyObject_Malloc` instead of :c:func:`PyMem_Malloc`, +as some C extensions do. diff --git a/Modules/_testcapi/heaptype.c b/Modules/_testcapi/heaptype.c index 963f464c47b9029..50c79cee8c2bc20 100644 --- a/Modules/_testcapi/heaptype.c +++ b/Modules/_testcapi/heaptype.c @@ -578,6 +578,46 @@ create_heapctype_with_none_bases_slot(PyObject *self, PyObject *Py_UNUSED(ignore } +static PyType_Slot NoDocSlots_slots[] = { + {0, 0}, +}; + +static PyType_Spec NoDocSlots_spec = { + .name = "_testcapi.PyObjectMallocDocType", + .basicsize = sizeof(PyObject), + .flags = Py_TPFLAGS_DEFAULT, + .slots = NoDocSlots_slots, +}; + +static PyObject * +test_pyobject_malloc_tp_doc(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + /* Regression test for gh-118909: some C extensions (e.g. older + * pybind11/nanobind versions) allocate tp_doc with PyObject_Malloc() + * directly instead of going through PyType_FromSpec()'s Py_tp_doc + * slot, relying on CPython to free it when the type is deallocated. + * Make sure that still works when debug allocator hooks are + * enabled (PYTHONMALLOC=debug). */ + PyObject *type = PyType_FromSpec(&NoDocSlots_spec); + if (type == NULL) { + return NULL; + } + assert(((PyTypeObject *)type)->tp_doc == NULL); + + static const char doc[] = "some docstring"; + char *tp_doc = PyObject_Malloc(sizeof(doc)); + if (tp_doc == NULL) { + Py_DECREF(type); + return PyErr_NoMemory(); + } + memcpy(tp_doc, doc, sizeof(doc)); + ((PyTypeObject *)type)->tp_doc = tp_doc; + + Py_DECREF(type); // triggers type_dealloc(), which frees tp_doc + Py_RETURN_NONE; +} + + static PyMethodDef TestMethods[] = { {"pytype_fromspec_meta", pytype_fromspec_meta, METH_O}, {"test_type_from_ephemeral_spec", test_type_from_ephemeral_spec, METH_NOARGS}, @@ -598,6 +638,7 @@ static PyMethodDef TestMethods[] = { {"pytype_getmodulebytoken", pytype_getmodulebytoken, METH_VARARGS}, {"create_heapctype_with_none_bases_slot", create_heapctype_with_none_bases_slot, METH_NOARGS}, + {"test_pyobject_malloc_tp_doc", test_pyobject_malloc_tp_doc, METH_NOARGS}, {NULL}, }; diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 572e302df8d80d5..7acb20b1c02cbf8 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -16,6 +16,7 @@ #include "pycore_object_alloc.h" // _PyObject_MallocWithType() #include "pycore_pyatomic_ft_wrappers.h" #include "pycore_pyerrors.h" // _PyErr_Occurred() +#include "pycore_pymem.h" // _PyMem_DebugEnabled() #include "pycore_pystate.h" // _PyThreadState_GET() #include "pycore_slots.h" // _PySlotIterator_Init #include "pycore_symtable.h" // _Py_Mangle() @@ -6857,6 +6858,38 @@ _PyTypes_FiniCachedDescriptors(PyInterpreterState *interp) } +/* Free a heap type's tp_doc. + * + * tp_doc is documented as being allocated by the type's creator, and + * historically some C extensions (e.g. older versions of pybind11 and + * nanobind) set it directly with PyObject_Malloc(), relying on CPython + * to free it here. gh-114574 switched CPython's own allocation of + * tp_doc from PyObject_Malloc() to PyMem_Malloc(). The two domains + * share the same underlying allocator in a release build, so this + * didn't matter in practice, but a build with the debug allocator + * hooks enabled (Py_DEBUG, or PYTHONMALLOC=debug) tags each domain's + * blocks and aborts if a block is freed with the wrong one. + * + * Detect which allocator was actually used, from the tag debug builds + * write just before the returned pointer, and free with the matching + * function so extensions written against the old contract keep + * working. This is a backwards-compatibility fallback, not a stable + * API: extensions should allocate tp_doc with PyMem_Malloc() (see + * gh-118909). + */ +static void +type_free_tp_doc(char *tp_doc) +{ + if (tp_doc != NULL && _PyMem_DebugEnabled()) { + char api_id = ((char *)tp_doc)[-(Py_ssize_t)SIZEOF_SIZE_T]; + if (api_id == 'o') { + PyObject_Free(tp_doc); + return; + } + } + PyMem_Free(tp_doc); +} + static void type_dealloc(PyObject *self) { @@ -6910,7 +6943,7 @@ type_dealloc(PyObject *self) /* A type's tp_doc is heap allocated, unlike the tp_doc slots * of most other objects. It's okay to cast it to char *. */ - PyMem_Free((char *)type->tp_doc); + type_free_tp_doc((char *)type->tp_doc); PyHeapTypeObject *et = (PyHeapTypeObject *)type; Py_XDECREF(et->ht_name);