From 6c74d678b260188290a77e247e4cbcdee0186158 Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Fri, 9 Oct 2026 16:05:50 +0000 Subject: [PATCH] [cppjit] Fix leaked container reference in vector_iter [upstream] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vector_iter() took two references to the container, one when storing it and one after deciding whether the container is a temporary, while the iterator's dealloc releases only one. Every iteration over a std::vector proxy therefore leaked a reference to it: the proxy, and the C++ object if Python owns it, never got freed. ROOT's own cppyy copy got this right in root-project/root#19017 (commit 700fee6e7b6c), which moved the temporary check before the increment so that PyUnstable_Object_IsUniqueReferencedTemporary() sees the untouched refcount on Python 3.14, and dropped the original increment. The port of that change to the compiler-research fork, compiler-research/CPyCppyy#151 (commit 8eba11b58c, reapplied as 7e086216a3), added the new increment after the check but kept the original one. cppjit inherited both in its initial import (compiler-research/cppjit@38b22b99be), and ROOT with the vendoring in 552d291c328. Upstream cppjit still has both. Beyond the memory, the leak made pyroot-pyz-ttree-branch flaky: the proxies leaked by the earlier read test stayed registered in the memory regulator under addresses the closed file had freed, so when a later `new std::vector` landed on one of them, reading the pointer data member returned the stale proxy instead of a reference proxy, and TTree::Branch() got bound to the proxy's own pointer slot. 🤖 Done with the help of AI --- bindings/pyroot/cppjit/src/cpyrt/Pythonize.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/pyroot/cppjit/src/cpyrt/Pythonize.cxx b/bindings/pyroot/cppjit/src/cpyrt/Pythonize.cxx index 5781345db3fdd..02ad193ae39a8 100644 --- a/bindings/pyroot/cppjit/src/cpyrt/Pythonize.cxx +++ b/bindings/pyroot/cppjit/src/cpyrt/Pythonize.cxx @@ -686,10 +686,10 @@ static PyObject* vector_iter(PyObject* v) { if (!vi) return nullptr; - Py_INCREF(v); vi->ii_container = v; // tell the iterator code to set a life line if this container is a temporary + // (checked before taking the iterator's own reference) vi->vi_flags = vectoriterobject::kDefault; #if PY_VERSION_HEX >= 0x030e0000 if (PyUnstable_Object_IsUniqueReferencedTemporary(v) ||