mirror of
https://github.com/pybind/pybind11.git
synced 2026-05-12 01:10:34 +00:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user