Skip to content

[v636][metacling] Move wrapper caches from local statics to file scope - #23873

Merged
guitargeek merged 3 commits into
root-project:v6-36-00-patchesfrom
guitargeek:v6-36-fix-23728
Oct 31, 2026
Merged

guitargeek merged 3 commits into
root-project:v6-36-00-patchesfrom
guitargeek:v6-36-fix-23728

Conversation

@guitargeek

Copy link
Copy Markdown
Contributor

TClingCallFunc caches the JIT-compiled call, constructor and destructor wrappers in std::maps that were function-local statics. Such a static registers its destructor on first use, and for the destructor-wrapper cache the first use can be in the middle of the user's program, e.g. when PyROOT destroys an object of a class template instantiation without compiled dictionary. That is after ROOT registered its atexit handlers, so at exit the cache was destroyed before TROOT::EndOfProcessCleanups() deleted the files and trees still alive, and the destructor lookup for a branch object without compiled dictionary (e.g. std::vector<std::vector>) ran on a dead map: segfault or "corrupted double-linked list" at exit, depending on the heap state.

Making the maps globals of libCling ties their lifetime to the library, like the other interpreter globals. They are then constructed when libCling is loaded by TROOT::InitInterpreter(), i.e. before the atexit handlers that run the end-of-process cleanups are registered, and outlive them.

This is the 6.36 version of the fix for #23728. In 6.40 and later the caches live in CppInterOp, which fixed the same issue by moving them to per-interpreter storage (CppInterOp commit b8ae9f666c).

Assisted-by: claude-code:claude-fable-5-1

TClingCallFunc caches the JIT-compiled call, constructor and destructor
wrappers in std::maps that were function-local statics. Such a static
registers its destructor on first use, and for the destructor-wrapper cache
the first use can be in the middle of the user's program, e.g. when PyROOT
destroys an object of a class template instantiation without compiled
dictionary. That is after ROOT registered its atexit handlers, so at exit the
cache was destroyed before TROOT::EndOfProcessCleanups() deleted the files
and trees still alive, and the destructor lookup for a branch object without
compiled dictionary (e.g. std::vector<std::vector<int>>) ran on a dead map:
segfault or "corrupted double-linked list" at exit, depending on the heap
state.

Making the maps globals of libCling ties their lifetime to the library, like
the other interpreter globals. They are then constructed when libCling is
loaded by TROOT::InitInterpreter(), i.e. before the atexit handlers that run
the end-of-process cleanups are registered, and outlive them.

This is the 6.36 version of the fix for root-project#23728. In 6.40 and later the caches
live in CppInterOp, which fixed the same issue by moving them to
per-interpreter storage (CppInterOp commit b8ae9f666c).

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 guitargeek self-assigned this Oct 28, 2026
@guitargeek guitargeek changed the title [v636][Python] Add regression test for the exit-time crash of issue #23728 [v636][metacling] Move wrapper caches from local statics to file scope Oct 28, 2026
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Test Results

    15 files      15 suites   2d 6h 3m 41s ⏱️
 2 747 tests  2 747 ✅ 0 💤 0 ❌
39 859 runs  39 859 ✅ 0 💤 0 ❌

Results for commit 9d2c54a.

♻️ This comment has been updated with latest results.

@pcanal

pcanal commented Oct 28, 2026

Copy link
Copy Markdown
Member

The errors (that happens to be in the Windows) build seems completely related to this change and are likely indicative of additional order destruction issues. (For example, a 'con' of this change is now the map might still contain stuff and try to delete them after the interpreter is torn down).

@guitargeek

Copy link
Copy Markdown
Contributor Author

Yes I'm on it. I'm still missing some fix, but is must be possible to get the test also to work on Windows, because I have merged the exact same test also in master where it passes fine on Windows

…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
(cherry picked from commit 98526e5)
@guitargeek

Copy link
Copy Markdown
Contributor Author

Windows failures fixed by 98526e5, backported to this feature branch.

@guitargeek
guitargeek merged commit 3d1701d into root-project:v6-36-00-patches Oct 31, 2026
19 checks passed
@guitargeek
guitargeek deleted the v6-36-fix-23728 branch October 31, 2026 12:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants