From fea40f2ed1d2dbc3d19c49ff0ba4f3a66c4ab3e3 Mon Sep 17 00:00:00 2001 From: Sami Agourram Date: Wed, 28 Oct 2026 08:10:49 +0100 Subject: [PATCH] Fix use-after-free of pyState at process exit on Linux At exit, glibc runs the C++ static destructors (__run_exit_handlers) before the libraries' destructor functions (_dl_fini). The namespace-scope shared_ptr pyState was therefore already destroyed when onLibraryUnload -> finalizePythonInterpreter assigned it nullptr, and that assignment decremented a reference count in freed memory, corrupting the heap (glibc could then abort in a later free). Keep the state and its mutex in objects that are allocated once and never destroyed, reached through function-local static pointers, so finalizePythonInterpreter always resets a live shared_ptr. It also takes the mutex, as instantiation does. --- .../src/pythonfmu/PySlaveInstance.cpp | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp b/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp index 59508be..f4d71ad 100644 --- a/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp +++ b/pythonfmu3/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp @@ -850,8 +850,22 @@ PySlaveInstance::~PySlaveInstance() } // namespace pythonfmu namespace { - std::mutex pyStateMutex{}; - std::shared_ptr pyState{}; + // Never destroyed (allocated once, no static destructor). On Linux the library's + // destructor function (onLibraryUnload, run by _dl_fini) resets the state after the C++ + // static destructors have run at exit: a namespace-scope shared_ptr was destroyed by then, + // and resetting it again decremented a reference count in freed memory, corrupting the + // heap (glibc then aborted in a later free, e.g. libgfortran's close_units). + std::mutex& pyStateMutex() + { + static auto* const mutex = new std::mutex(); + return *mutex; + } + + std::shared_ptr& pyState() + { + static auto* const state = new std::shared_ptr(); + return *state; + } } std::unique_ptr CppfmuInstantiateSlave( @@ -879,13 +893,13 @@ std::unique_ptr CppfmuInstantiateSlave( { auto const ensurePyStateAlive = [&]() { - auto const lock = std::lock_guard{pyStateMutex}; - if (nullptr == pyState) pyState = std::make_shared(); + auto const lock = std::lock_guard{pyStateMutex()}; + if (nullptr == pyState()) pyState() = std::make_shared(); + return pyState(); }; - ensurePyStateAlive(); return std::make_unique( - instanceName, resources, logger, visible, pyState); + instanceName, resources, logger, visible, ensurePyStateAlive()); } } @@ -897,7 +911,8 @@ extern "C" { // Thus, use DllMain on Windows and __attribute__((destructor)) on Linux for signaling to the PyState about de-initialization. void finalizePythonInterpreter() { - pyState = nullptr; + auto const lock = std::lock_guard{pyStateMutex()}; + pyState().reset(); } }