mirror of
https://github.com/pybind/pybind11.git
synced 2026-05-11 17:00: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:
@@ -503,6 +503,17 @@ extern "C" inline void pybind11_object_dealloc(PyObject *self) {
|
||||
PyObject_GC_UnTrack(self);
|
||||
}
|
||||
|
||||
#if PY_VERSION_HEX >= 0x030D0000
|
||||
// PyObject_ClearManagedDict() is available from Python 3.13+. It must be
|
||||
// called before tp_free() because on Python 3.14+ tp_free no longer
|
||||
// implicitly clears the managed dict, which would abandon the refcounts of
|
||||
// objects stored in __dict__ of py::dynamic_attr() types, causing permanent
|
||||
// memory leaks.
|
||||
if (PyType_HasFeature(type, Py_TPFLAGS_MANAGED_DICT)) {
|
||||
PyObject_ClearManagedDict(self);
|
||||
}
|
||||
#endif
|
||||
|
||||
clear_instance(self);
|
||||
|
||||
type->tp_free(self);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -383,6 +383,23 @@ def test_cyclic_gc():
|
||||
assert cstats.alive() == 0
|
||||
|
||||
|
||||
@pytest.mark.xfail("env.PYPY", strict=False)
|
||||
@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC")
|
||||
def test_dynamic_attr_dealloc_frees_dict_contents():
|
||||
"""Regression: py::dynamic_attr() objects must free __dict__ contents on dealloc.
|
||||
|
||||
pybind11_object_dealloc() did not call PyObject_ClearManagedDict() before tp_free(),
|
||||
causing objects stored in __dict__ to have their refcounts permanently abandoned on
|
||||
Python 3.14+ (where tp_free no longer implicitly clears the managed dict).
|
||||
This caused capsule destructors to never run, leaking the underlying C++ data.
|
||||
"""
|
||||
instance = m.make_dynamic_attr_with_capsule()
|
||||
assert not m.is_dynamic_attr_capsule_freed()
|
||||
del instance
|
||||
pytest.gc_collect()
|
||||
assert m.is_dynamic_attr_capsule_freed()
|
||||
|
||||
|
||||
def test_bad_arg_default(msg):
|
||||
from pybind11_tests import detailed_error_messages_enabled
|
||||
|
||||
|
||||
Reference in New Issue
Block a user