Skip to content

Commit 5772823

Browse files
committed
fix(core): add Runtime destruction callback to clean G2P Manager before plugin DLL unload
Root cause: G2P Manager is a static singleton whose destructor runs AFTER the host's Runtime (local variable). When the Manager destructor calls removeAllObjects, the released Task/SessionFactory shared_ptrs invoke virtual destructors whose vtables reside in already-unloaded plugin DLLs (srt-driver-onnx.dll), causing use-after-free crash in shared_ptr::_Decref. Previous weak_ptr fix in G2pOnnxSessionFactory only addressed the factory-to-driver edge; Task-to-Session-to-Driver references remained. Fix: (1) Added Runtime::addDestructionCallback API (srt-core) with LIFO callback list executed in ~Runtime::Impl BEFORE PluginFactory unloads DLLs. (2) Added PackageManager::shutdown() (srt-g2p) which releases all ObjectPool references and closes loaded packages (idempotent, also called from destructor). (3) setupG2pOnnxDriver registers a callback that calls Manager::shutdown() during Runtime destruction, ensuring Task/SessionFactory references are released while plugin DLLs are still loaded. No changes required on the lite side.
1 parent 3d1d66e commit 5772823

7 files changed

Lines changed: 108 additions & 0 deletions

File tree

‎include/synthrt/Core/Core/Runtime.h‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22

33
#include <filesystem>
4+
#include <functional>
45
#include <memory>
56
#include <string_view>
67
#include <vector>
@@ -125,6 +126,24 @@ namespace srt::core {
125126
/// specs, (3) call \c ModelRegistry::unbindPackage(pkgId).
126127
Expected<void> unloadPackage(const std::filesystem::path &path);
127128

129+
// --- Destruction callbacks (DLL unload ordering) ---
130+
// Register a callback to be invoked in ~Runtime(), BEFORE PluginFactory
131+
// unloads plugin DLLs. This allows subsystems that hold shared_ptrs to
132+
// plugin-DLL-resident objects (e.g. srt::g2p::Manager holding Task/
133+
// SessionFactory instances whose vtables live in srt-driver-onnx.dll)
134+
// to release those references while the plugin DLLs are still loaded.
135+
//
136+
// The callback executes during Runtime destruction, which happens
137+
// BEFORE static singleton destructors run. Without this hook, a static
138+
// singleton (e.g. srt::g2p::Manager) would release its shared_ptrs in
139+
// its own destructor (after Runtime destruction), accessing freed
140+
// plugin DLL memory through dangling vtable pointers.
141+
//
142+
// Callbacks run in reverse order of registration (LIFO). The callback
143+
// MUST NOT capture a shared_ptr to the Runtime itself (would create a
144+
// cycle); capture raw pointers or weak_ptr instead.
145+
void addDestructionCallback(std::function<void()> callback);
146+
128147
protected:
129148
static void registerModuleCategoryFactory(ModuleCategory *(*fac)(Runtime *));
130149

‎include/synthrt/G2P/Core/PackageManager.h‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,17 @@ namespace srt::g2p {
119119
std::vector<srt::dependency::ModuleMetadata> getModuleMetadatas(
120120
const srt::core::ContextKey &ctxKey);
121121

122+
/// Release all object references held by category ObjectPools and
123+
/// clear loaded packages. Called automatically by setupG2pOnnxDriver's
124+
/// Runtime destruction callback, so manual invocation is typically
125+
/// unnecessary.
126+
///
127+
/// This is the counterpart to the constructor: it performs the same
128+
/// cleanup as the destructor but can be called early (before plugin
129+
/// DLLs are unloaded) to prevent use-after-free when static singleton
130+
/// destructors run after Runtime destruction.
131+
void shutdown();
132+
122133
protected:
123134
class Impl;
124135
std::unique_ptr<Impl> _impl;

‎lib/Core/Core/Runtime.cpp‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,19 @@ namespace srt::core {
3939
}
4040

4141
Runtime::Impl::~Impl() {
42+
// Run destruction callbacks in LIFO order BEFORE deleting module
43+
// categories or unloading plugin DLLs (the PluginFactory is owned by
44+
// m_services, which is destroyed after this destructor body runs as
45+
// part of member destruction). This lets subsystems release shared_ptrs
46+
// to plugin-DLL-resident objects while the DLLs are still loaded.
47+
for (auto it = m_destructionCallbacks.rbegin();
48+
it != m_destructionCallbacks.rend(); ++it) {
49+
if (*it) {
50+
(*it)();
51+
}
52+
}
53+
m_destructionCallbacks.clear();
54+
4255
for (const auto &[name, cate] : m_moduleCategories) {
4356
(void) name;
4457
delete cate;
@@ -60,6 +73,10 @@ namespace srt::core {
6073

6174
Runtime::~Runtime() = default;
6275

76+
void Runtime::addDestructionCallback(std::function<void()> callback) {
77+
_impl->m_destructionCallbacks.push_back(std::move(callback));
78+
}
79+
6380
// --- Service registry (ARCH-03) ---
6481

6582
ServiceRegistry &Runtime::services() {

‎lib/Core/Core/Runtime_p.h‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include <atomic>
44
#include <filesystem>
5+
#include <functional>
56
#include <map>
67
#include <memory>
78
#include <shared_mutex>
@@ -75,6 +76,24 @@ namespace srt::core {
7576
// Shared mutex protecting category/contribute operations across packages.
7677
mutable std::shared_mutex m_su_mtx;
7778

79+
// --- Destruction callbacks (DLL unload ordering) ---
80+
// Runtime is typically a local variable in the host (e.g. lite's
81+
// SynthrtEngine::m_runtime), while some objects that reference Runtime
82+
// resources (e.g. srt::g2p::Manager singleton holding Task/SessionFactory
83+
// shared_ptrs that reference plugin DLL code) are static singletons
84+
// whose destructors run AFTER the Runtime. When the Runtime destructs,
85+
// PluginFactory unloads plugin DLLs (e.g. srt-driver-onnx.dll); the
86+
// static singleton's later destructor would then access freed memory
87+
// through dangling vtable pointers.
88+
//
89+
// Destruction callbacks run in ~Runtime::Impl (BEFORE PluginFactory
90+
// unloads DLLs) so subsystems can release object references while the
91+
// plugin DLLs are still loaded. Callbacks execute in reverse order of
92+
// registration (LIFO), and are cleared after execution to break
93+
// reference cycles (captured shared_ptrs would otherwise keep the
94+
// callback itself alive).
95+
std::vector<std::function<void()>> m_destructionCallbacks;
96+
7897
public:
7998
static llvm::SmallVector<ModuleCategory *(*)(Runtime *)> moduleCategoryFactories;
8099
};

‎lib/G2P/Core/PackageManager.cpp‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,16 @@ namespace srt::g2p {
246246
}
247247

248248
PackageManager::Impl::~Impl() {
249+
shutdown();
250+
}
251+
252+
void PackageManager::Impl::shutdown() {
253+
// Idempotent: safe to call from both shutdown() and the destructor.
254+
// Also safe to call when m_categories is already empty (no-op).
255+
if (m_categories.empty() && m_tasks.empty()) {
256+
return;
257+
}
258+
249259
// Destruction order (matters!):
250260
//
251261
// 1. m_tasks.clear() — release the NO<Task> references held by the
@@ -614,6 +624,11 @@ namespace srt::g2p {
614624

615625
PackageManager::~PackageManager() = default;
616626

627+
void PackageManager::shutdown() {
628+
auto &impl = *static_cast<Impl *>(_impl.get());
629+
impl.shutdown();
630+
}
631+
617632
srt::core::ModuleCategory *PackageManager::category(const std::string_view &name) const {
618633
auto &impl = *static_cast<Impl *>(_impl.get());
619634
const auto it = impl.m_categories.find(name);

‎lib/G2P/Core/PackageManager_p.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ namespace srt::g2p {
5252
void closeAllLoadedPackages();
5353
void refreshPackageIndexes(const srt::core::ContextKey &ctxKey);
5454

55+
/// Release all object references held by category ObjectPools, close
56+
/// loaded packages, and delete categories. Idempotent (safe to call
57+
/// again from the destructor). Called by PackageManager::shutdown()
58+
/// which is invoked from the Runtime destruction callback registered
59+
/// in setupG2pOnnxDriver.
60+
void shutdown();
61+
5562
/// Erase all per-context state for \p ctxKey (TD-02). Centralizes the
5663
/// 7-map erase previously duplicated in removeContextsByPrefix
5764
/// overloads. New per-context state should be added here so retire

‎lib/G2P/G2pOnnxSetup.cpp‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,26 @@ namespace srt::g2p {
223223
driverCat->removeObjects(srt::g2p::kG2pOnnxDriverName);
224224
driverCat->addObject(srt::g2p::kG2pOnnxDriverName, factory);
225225

226+
// 5. Register a Runtime destruction callback to call
227+
// Manager::shutdown() BEFORE the Runtime (and its
228+
// PluginFactory) destructs. This releases Task/SessionFactory
229+
// shared_ptrs held by the Manager's category ObjectPools while
230+
// plugin DLLs (e.g. srt-driver-onnx.dll) are still loaded.
231+
//
232+
// Without this, the Manager (a static singleton) would destruct
233+
// AFTER the Runtime (a local variable in the host), releasing
234+
// those shared_ptrs and invoking virtual destructors whose
235+
// vtables reside in already-unloaded plugin DLLs — a
236+
// use-after-free that crashes in shared_ptr::_Decref.
237+
//
238+
// The callback captures `mgr` as a raw pointer (the Manager is
239+
// a static singleton whose address is stable throughout the
240+
// process lifetime, so raw-pointer capture is safe). shutdown()
241+
// is idempotent, so a later destructor call is a no-op.
242+
runtime.addDestructionCallback([mgr]() {
243+
mgr->shutdown();
244+
});
245+
226246
return srt::core::Expected<void>{};
227247
} catch (const std::exception &e) {
228248
// CODING-02 / ROBUST-02: do not let exceptions cross the public

0 commit comments

Comments
 (0)