Repository navigation
[v640] Fixes and unit test for Python exit-time crash issue #23728 - #23851
Open
guitargeek wants to merge 4 commits into
Open
guitargeek wants to merge 4 commits into
guitargeek wants to merge 4 commits into
Conversation
guitargeek
requested review from
dpiparo,
pcanal and
vepadulano
as code owners
October 27, 2026 06:46
guitargeek
force-pushed
the
v6-40-fix-23728
branch
from
October 27, 2026 06:47
aa143af to
6693438
Compare
Test Results 23 files 23 suites 3d 12h 7m 10s ⏱️ Results for commit 07f41e6. ♻️ This comment has been updated with latest results. |
The JIT-compiled call and destructor wrappers were cached in function-local
static maps in make_wrapper() and make_dtor_wrapper(). The destructor of such
a static is registered with the runtime at its first use, and C++ runs these
destructors in reverse order of registration. When the first use happened
after ROOT registered its atexit handlers, the cache was already destroyed by
the time TROOT::EndOfProcessCleanups() ran, and deleting an object of a class
without compiled dictionary from there (e.g. the object held by a TBranchElement
of a dictionary-less collection) called find()/insert() on a dead std::map:
std::map<...>::insert (this=<make_dtor_wrapper::gDtorWrapperStore>)
CppImpl::(anonymous namespace)::make_dtor_wrapper
CppImpl::Destruct
TClingCallFunc::ExecDestructor
TClingClassInfo::Delete
TBranchElement::ReleaseObject
...
TROOT::EndOfProcessCleanups
In PyROOT this is what happens as soon as the script constructs and drops
any object of a class without compiled dictionary (for example a new template
instantiation such as std::vector<TNamed>): its destructor is the first use
of the cache, well after `import ROOT`.
Store the caches in InterpreterInfo instead. The interpreter registry is an
llvm::ManagedStatic, so the caches now live as long as the interpreter they
belong to, and are also freed together with it.
This is a backport of upstream CppInterOp commit b8ae9f666c ("Move wrapper
caches from function-local statics to per-interpreter storage"), which ROOT
master received with the CppInterOp upgrade in c8626da.
Fixes root-project#23728
Assisted-by: claude-code:claude-fable-5-1
Cpp::Destruct takes (object, scope, withFree, count), but TClingCallFunc::ExecDestructor passed (nary, withFree). Through the implicit conversions, TClingClassInfo::Delete (nary=0, withFree=true) ran the destructor but never freed the memory, so every object of a class without compiled dictionary deleted through TClass::Destructor leaked, and TClingClassInfo::DeleteArray (nary=1, withFree=true) used `delete` instead of `delete[]`. Measured with 180 interpreted objects of 1 MiB created with TClass::New and deleted with TClass::Destructor: RSS grew by 179 MiB before, 0 MiB after. Master has the right order since 17c57aa. Assisted-by: claude-code:claude-fable-5-1
…oject#23728 Reads a tree with a branch of a dictionary-less collection in a fresh Python process, lets TROOT::EndOfProcessCleanups delete it at exit, and constructs another dictionary-less object before exiting. This crashed in the JIT-compiled destructor lookup before the CppInterOp upgrade. The child process runs with MALLOC_PERTURB_ set, because a use of destroyed objects only crashes reliably when freed memory is poisoned, and with ROOT_OBJECT_AUTO_REGISTRATION=1 plus explicit checks that the file is registered with gROOT and owns the tree, so the test keeps exercising the scenario when object auto-registration is disabled. Assisted-by: claude-code:claude-fable-5-1 (cherry picked from commit a41307c)
guitargeek
force-pushed
the
v6-40-fix-23728
branch
from
October 28, 2026 06:42
6693438 to
ef5c2b9
Compare
…vely The test left the destructor of the branch class uncompiled until exit, so that cling had to JIT-compile it from TROOT::EndOfProcessCleanups(). On Windows, that cleanup runs from libCore's DLL_PROCESS_DETACH, after libCling's statics have already been torn down, so compiling anything there crashes with or without the fix (the no-fix run in root-project#23850 fails identically). Pre-compile the exact destructor instead: the exit-time call is then a pure cache lookup, which is the regular exit path on Windows. The bug was in that lookup on the destroyed function-local static, so the test still fails without the fix. Write the branch through the overload taking the class name and the address. The templated Branch() that the pythonization on master calls refuses collections without a compiled CollectionProxy, which silently left the tree without the branch and the test without its purpose. Also give the child process a timeout: without the fix, ROOT's crash handler can hang instead of terminating the process. Assisted-by: claude-code:claude-fable-5-1
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is backporting targeted changes from master and adding a regression test.
A test run without the fixes is launched in #23850.