Fix memory leak: clear managed dict in pybind11_object_dealloc on Python 3.13+ (#5999)

* fix: clear managed dict in pybind11_object_dealloc on Python 3.13+

On Python 3.14, PyObject_GC_Del (tp_free) no longer implicitly clears
the managed dict of objects with Py_TPFLAGS_MANAGED_DICT. Without an
explicit PyObject_ClearManagedDict() call before tp_free(), objects
stored in the __dict__ of py::dynamic_attr() instances have their
refcounts permanently abandoned, causing memory leaks — capsule
destructors for numpy arrays (and other objects) never run.

Adds a regression test: stores a py::capsule in the __dict__ of a
DynamicClass instance and asserts the capsule destructor is called
when the instance is deleted.

* [tests]: mark test_dynamic_attr_dealloc_frees_dict_contents to be strict=False xfail on PYPY

* [docs]: clarify Python version comments in pybind11_object_dealloc

Distinguish between when the API is available (3.13+, where
PyObject_ClearManagedDict was introduced) and when the leak actually
manifests (3.14+, where tp_free stopped implicitly clearing the
managed dict).

---------

Co-authored-by: Yury Matveev <yury.matveev@desy.de>
This commit is contained in:
Yury Matveev
2026-03-24 05:14:00 +01:00
committed by GitHub
parent 0a45af2531
commit dd95d53f0a
3 changed files with 52 additions and 0 deletions

View File

@@ -11,6 +11,12 @@
#include "constructor_stats.h"
#include "pybind11_tests.h"
#if !defined(PYPY_VERSION)
// Flag set by the capsule destructor in test_dynamic_attr_dealloc_frees_dict_contents.
// File scope so the captureless capsule destructor (void(*)(void*)) can access it.
static bool s_dynamic_attr_capsule_freed = false;
#endif
#if !defined(PYBIND11_OVERLOAD_CAST)
template <typename... Args>
using overload_cast_ = pybind11::detail::overload_cast_impl<Args...>;
@@ -388,6 +394,24 @@ TEST_SUBMODULE(methods_and_attributes, m) {
class CppDerivedDynamicClass : public DynamicClass {};
py::class_<CppDerivedDynamicClass, DynamicClass>(m, "CppDerivedDynamicClass").def(py::init());
// test_dynamic_attr_dealloc_frees_dict_contents
// Regression test: pybind11_object_dealloc() must call PyObject_ClearManagedDict()
// before tp_free() so that objects stored in a py::dynamic_attr() instance __dict__
// have their refcounts decremented when the pybind11 object is freed. On Python 3.14+
// tp_free no longer implicitly clears the managed dict, causing permanent leaks.
m.def("make_dynamic_attr_with_capsule", []() -> py::object {
s_dynamic_attr_capsule_freed = false;
auto *dummy = new int(0);
py::capsule cap(dummy, [](void *ptr) {
delete static_cast<int *>(ptr);
s_dynamic_attr_capsule_freed = true;
});
py::object obj = py::cast(new DynamicClass(), py::return_value_policy::take_ownership);
obj.attr("data") = cap;
return obj;
});
m.def("is_dynamic_attr_capsule_freed", []() { return s_dynamic_attr_capsule_freed; });
#endif
// test_bad_arg_default