Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Doc/c-api/typeobj.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions Lib/test/test_capi/test_mem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix a crash in debug builds (or with :envvar:`PYTHONMALLOC=debug <PYTHONMALLOC>`)
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.
41 changes: 41 additions & 0 deletions Modules/_testcapi/heaptype.c
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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},
};

Expand Down
35 changes: 34 additions & 1 deletion Objects/typeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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);
Expand Down
Loading