src/bootstrap.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "DetourModKit/bootstrap.hpp" | ||
| 2 | #include "DetourModKit/diagnostics.hpp" | ||
| 3 | |||
| 4 | #include "DetourModKit/config.hpp" | ||
| 5 | #include "DetourModKit/hook_manager.hpp" | ||
| 6 | #include "DetourModKit/input.hpp" | ||
| 7 | #include "DetourModKit/logger.hpp" | ||
| 8 | #include "DetourModKit/async_logger.hpp" | ||
| 9 | |||
| 10 | #include <DetourModKit.hpp> | ||
| 11 | |||
| 12 | #include <windows.h> | ||
| 13 | |||
| 14 | #include <atomic> | ||
| 15 | #include <cwchar> | ||
| 16 | #include <exception> | ||
| 17 | |||
| 18 | namespace DetourModKit::Bootstrap | ||
| 19 | { | ||
| 20 | namespace | ||
| 21 | { | ||
| 22 | HANDLE s_shutdown_event = nullptr; | ||
| 23 | HANDLE s_worker_thread = nullptr; | ||
| 24 | HANDLE s_instance_mutex = nullptr; | ||
| 25 | HMODULE s_module = nullptr; | ||
| 26 | std::atomic<bool> s_detach_called{false}; | ||
| 27 | |||
| 28 | std::function<bool()> s_init_fn; | ||
| 29 | std::function<void()> s_shutdown_fn; | ||
| 30 | |||
| 31 | 9 | bool is_target_process(std::string_view expected) noexcept | |
| 32 | { | ||
| 33 |
2/2✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 8 times.
|
9 | if (expected.empty()) |
| 34 | { | ||
| 35 | 1 | return true; | |
| 36 | } | ||
| 37 | |||
| 38 | // Resolve the running executable path as wide rather than via GetModuleFileNameA: the ANSI form maps the | ||
| 39 | // path through the active code page, so a game whose EXE basename contains non-ASCII characters would be | ||
| 40 | // mangled and could miss (or false-match) the gate. The rest of bootstrap already works in wide for the | ||
| 41 | // instance-mutex name, so the wide module path keeps the process gate consistent with the kit's UTF-8 | ||
| 42 | // stance. | ||
| 43 | 8 | wchar_t exe_path[MAX_PATH]{}; | |
| 44 | 8 | const DWORD len = GetModuleFileNameW(nullptr, exe_path, MAX_PATH); | |
| 45 |
2/4✓ Branch 6 → 7 taken 8 times.
✗ Branch 6 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 8 times.
|
8 | if (len == 0 || len >= MAX_PATH) |
| 46 | { | ||
| 47 | ✗ | return false; | |
| 48 | } | ||
| 49 | |||
| 50 | 8 | const wchar_t *exe_name = std::wcsrchr(exe_path, L'\\'); | |
| 51 |
1/2✓ Branch 10 → 11 taken 8 times.
✗ Branch 10 → 12 not taken.
|
8 | exe_name = exe_name ? exe_name + 1 : exe_path; |
| 52 | |||
| 53 | // Widen the caller-supplied UTF-8 name into a bounded stack buffer for a wide case-insensitive compare. | ||
| 54 | // This helper is noexcept and runs on the bootstrap path, so it avoids a throwing allocation; a name that | ||
| 55 | // cannot fit a module file name cannot match the running executable. MultiByteToWideChar with a fixed | ||
| 56 | // destination and MB_ERR_INVALID_CHARS fails closed (returns 0) on overflow or malformed UTF-8 rather than | ||
| 57 | // truncating into a false match. | ||
| 58 |
1/2✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 8 times.
|
8 | if (expected.size() >= MAX_PATH) |
| 59 | { | ||
| 60 | ✗ | return false; | |
| 61 | } | ||
| 62 | wchar_t expected_buf[MAX_PATH]; | ||
| 63 | 8 | const int wide_len = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, expected.data(), | |
| 64 | 8 | static_cast<int>(expected.size()), expected_buf, MAX_PATH - 1); | |
| 65 |
1/2✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 8 times.
|
8 | if (wide_len <= 0) |
| 66 | { | ||
| 67 | ✗ | return false; | |
| 68 | } | ||
| 69 | 8 | expected_buf[wide_len] = L'\0'; | |
| 70 | 8 | return _wcsicmp(exe_name, expected_buf) == 0; | |
| 71 | } | ||
| 72 | |||
| 73 | 7 | bool acquire_instance_mutex(std::string_view prefix) noexcept | |
| 74 | { | ||
| 75 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 7 times.
|
7 | if (prefix.empty()) |
| 76 | { | ||
| 77 | ✗ | return true; | |
| 78 | } | ||
| 79 | |||
| 80 | // Build the name in a std::wstring rather than formatting into a fixed wchar_t buffer: the prefix is | ||
| 81 | // caller-supplied, so a bounded formatter would have to truncate or fail, and an unbounded one (wsprintfW) | ||
| 82 | // could overflow. CreateMutexW rejects an over-long name on its own, which the null-handle check below | ||
| 83 | // already handles. The allocation is wrapped because this helper is noexcept and runs on the bootstrap | ||
| 84 | // path; an out-of-memory failure fails closed (no single-instance guard) rather than terminating the | ||
| 85 | // process. | ||
| 86 | 7 | std::wstring mutex_name; | |
| 87 | try | ||
| 88 | { | ||
| 89 |
1/2✓ Branch 7 → 8 taken 7 times.
✗ Branch 7 → 32 not taken.
|
7 | mutex_name.reserve(prefix.size() + 10); |
| 90 |
2/2✓ Branch 12 → 10 taken 337 times.
✓ Branch 12 → 13 taken 7 times.
|
344 | for (char c : prefix) |
| 91 | { | ||
| 92 |
1/2✓ Branch 10 → 11 taken 337 times.
✗ Branch 10 → 32 not taken.
|
337 | mutex_name.push_back(static_cast<wchar_t>(static_cast<unsigned char>(c))); |
| 93 | } | ||
| 94 |
3/6✓ Branch 13 → 14 taken 7 times.
✗ Branch 13 → 31 not taken.
✓ Branch 14 → 15 taken 7 times.
✗ Branch 14 → 31 not taken.
✓ Branch 15 → 16 taken 7 times.
✗ Branch 15 → 29 not taken.
|
7 | mutex_name += std::to_wstring(GetCurrentProcessId()); |
| 95 | } | ||
| 96 | ✗ | catch (...) | |
| 97 | { | ||
| 98 | ✗ | return false; | |
| 99 | ✗ | } | |
| 100 | |||
| 101 | 7 | HANDLE mutex_handle = CreateMutexW(nullptr, FALSE, mutex_name.c_str()); | |
| 102 |
1/2✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 7 times.
|
7 | if (!mutex_handle) |
| 103 | { | ||
| 104 | ✗ | return false; | |
| 105 | } | ||
| 106 |
2/2✓ Branch 22 → 23 taken 3 times.
✓ Branch 22 → 25 taken 4 times.
|
7 | if (GetLastError() == ERROR_ALREADY_EXISTS) |
| 107 | { | ||
| 108 | 3 | CloseHandle(mutex_handle); | |
| 109 | 3 | return false; | |
| 110 | } | ||
| 111 | |||
| 112 | 4 | s_instance_mutex = mutex_handle; | |
| 113 | 4 | return true; | |
| 114 | 7 | } | |
| 115 | |||
| 116 | 4 | DWORD WINAPI lifecycle_thread(LPVOID) noexcept | |
| 117 | { | ||
| 118 | 4 | Logger &logger = Logger::get_instance(); | |
| 119 | |||
| 120 | 4 | bool init_ok = false; | |
| 121 |
1/2✓ Branch 4 → 5 taken 4 times.
✗ Branch 4 → 7 not taken.
|
4 | if (s_init_fn) |
| 122 | { | ||
| 123 | try | ||
| 124 | { | ||
| 125 |
2/2✓ Branch 5 → 6 taken 3 times.
✓ Branch 5 → 19 taken 1 time.
|
4 | init_ok = s_init_fn(); |
| 126 | } | ||
| 127 |
1/2✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 24 not taken.
|
1 | catch (const std::exception &e) |
| 128 | { | ||
| 129 | 1 | logger.error("Bootstrap: init_fn threw: {}", e.what()); | |
| 130 | 1 | } | |
| 131 | ✗ | catch (...) | |
| 132 | { | ||
| 133 | ✗ | logger.error("Bootstrap: init_fn threw unknown exception."); | |
| 134 | ✗ | } | |
| 135 | } | ||
| 136 | else | ||
| 137 | { | ||
| 138 | ✗ | init_ok = true; | |
| 139 | } | ||
| 140 | |||
| 141 |
2/2✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 11 taken 3 times.
|
4 | if (!init_ok) |
| 142 | { | ||
| 143 | 1 | logger.error("Bootstrap: init_fn returned failure; worker idling until detach."); | |
| 144 | } | ||
| 145 | |||
| 146 |
1/2✓ Branch 11 → 12 taken 4 times.
✗ Branch 11 → 13 not taken.
|
4 | if (s_shutdown_event) |
| 147 | { | ||
| 148 | 4 | WaitForSingleObject(s_shutdown_event, INFINITE); | |
| 149 | } | ||
| 150 | |||
| 151 |
1/2✓ Branch 14 → 15 taken 4 times.
✗ Branch 14 → 16 not taken.
|
4 | if (s_shutdown_fn) |
| 152 | { | ||
| 153 | try | ||
| 154 | { | ||
| 155 |
2/2✓ Branch 15 → 16 taken 3 times.
✓ Branch 15 → 27 taken 1 time.
|
4 | s_shutdown_fn(); |
| 156 | } | ||
| 157 |
1/2✓ Branch 27 → 28 taken 1 time.
✗ Branch 27 → 32 not taken.
|
1 | catch (const std::exception &e) |
| 158 | { | ||
| 159 | 1 | logger.error("Bootstrap: shutdown_fn threw: {}", e.what()); | |
| 160 | 1 | } | |
| 161 | ✗ | catch (...) | |
| 162 | { | ||
| 163 | ✗ | logger.error("Bootstrap: shutdown_fn threw unknown exception."); | |
| 164 | ✗ | } | |
| 165 | } | ||
| 166 | |||
| 167 | 4 | DMK_Shutdown(); | |
| 168 | 4 | return 0; | |
| 169 | } | ||
| 170 | |||
| 171 | 4 | void release_instance_mutex() noexcept | |
| 172 | { | ||
| 173 |
1/2✓ Branch 2 → 3 taken 4 times.
✗ Branch 2 → 5 not taken.
|
4 | if (s_instance_mutex) |
| 174 | { | ||
| 175 | 4 | CloseHandle(s_instance_mutex); | |
| 176 | 4 | s_instance_mutex = nullptr; | |
| 177 | } | ||
| 178 | 4 | } | |
| 179 | |||
| 180 | // Shared teardown path for early-attach failures. Ensures singletons initialized by Logger::configure / user | ||
| 181 | // init_fn are torn down before on_dll_attach returns FALSE, so a subsequent DLL load starts clean. | ||
| 182 | ✗ | void unwind_early_attach_failure() noexcept | |
| 183 | { | ||
| 184 | ✗ | if (s_shutdown_event) | |
| 185 | { | ||
| 186 | ✗ | CloseHandle(s_shutdown_event); | |
| 187 | ✗ | s_shutdown_event = nullptr; | |
| 188 | } | ||
| 189 | ✗ | release_instance_mutex(); | |
| 190 | ✗ | s_init_fn = nullptr; | |
| 191 | ✗ | s_shutdown_fn = nullptr; | |
| 192 | ✗ | s_module = nullptr; | |
| 193 | try | ||
| 194 | { | ||
| 195 | ✗ | DMK_Shutdown(); | |
| 196 | } | ||
| 197 | catch (...) | ||
| 198 | { | ||
| 199 | } | ||
| 200 | ✗ | } | |
| 201 | |||
| 202 | // The throwing core of on_dll_attach, separated so the public entry point can be noexcept. on_dll_attach is | ||
| 203 | // called from DllMain under the Windows loader lock, where an escaping exception is undefined behavior. Any | ||
| 204 | // throwing step here -- most concretely Logger::configure, which builds std::string / std::wstring for the log | ||
| 205 | // prefix and path and can raise std::bad_alloc -- propagates to the noexcept wrapper, which fails closed. | ||
| 206 | 10 | [[nodiscard]] bool32_t attach_core(module_handle_t hMod, const ModInfo &info, std::function<bool()> init_fn, | |
| 207 | std::function<void()> shutdown_fn) | ||
| 208 | { | ||
| 209 |
3/4✓ Branch 2 → 3 taken 9 times.
✓ Branch 2 → 4 taken 1 time.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 9 times.
|
10 | if (s_shutdown_event || s_worker_thread) |
| 210 | { | ||
| 211 | 1 | return FALSE; | |
| 212 | } | ||
| 213 | |||
| 214 | 9 | s_module = hMod; | |
| 215 |
2/2✓ Branch 5 → 6 taken 8 times.
✓ Branch 5 → 7 taken 1 time.
|
9 | if (hMod) |
| 216 | { | ||
| 217 | 8 | DisableThreadLibraryCalls(hMod); | |
| 218 | } | ||
| 219 | |||
| 220 |
2/2✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 7 times.
|
9 | if (!is_target_process(info.game_process_name)) |
| 221 | { | ||
| 222 | 2 | s_module = nullptr; | |
| 223 | 2 | return FALSE; | |
| 224 | } | ||
| 225 | |||
| 226 |
2/2✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 13 taken 4 times.
|
7 | if (!acquire_instance_mutex(info.instance_mutex_prefix)) |
| 227 | { | ||
| 228 | 3 | s_module = nullptr; | |
| 229 | 3 | return FALSE; | |
| 230 | } | ||
| 231 | |||
| 232 |
1/2✓ Branch 14 → 15 taken 4 times.
✗ Branch 14 → 34 not taken.
|
4 | Logger::configure(info.prefix, info.log_file); |
| 233 | 4 | Logger::get_instance().enable_async_mode(info.async_cfg); | |
| 234 | |||
| 235 | 4 | s_init_fn = std::move(init_fn); | |
| 236 | 4 | s_shutdown_fn = std::move(shutdown_fn); | |
| 237 | |||
| 238 | 4 | s_shutdown_event = CreateEventW(nullptr, TRUE, FALSE, nullptr); | |
| 239 |
1/2✗ Branch 24 → 25 not taken.
✓ Branch 24 → 27 taken 4 times.
|
4 | if (!s_shutdown_event) |
| 240 | { | ||
| 241 | ✗ | unwind_early_attach_failure(); | |
| 242 | ✗ | return FALSE; | |
| 243 | } | ||
| 244 | |||
| 245 | 4 | s_worker_thread = CreateThread(nullptr, 0, lifecycle_thread, nullptr, 0, nullptr); | |
| 246 |
1/2✗ Branch 28 → 29 not taken.
✓ Branch 28 → 31 taken 4 times.
|
4 | if (!s_worker_thread) |
| 247 | { | ||
| 248 | ✗ | unwind_early_attach_failure(); | |
| 249 | ✗ | return FALSE; | |
| 250 | } | ||
| 251 | |||
| 252 | // Re-arm the detach gate now that a fresh attach has fully succeeded, so the matching on_dll_detach runs | ||
| 253 | // its teardown instead of no-opping. Without this reset s_detach_called stays true after the first detach | ||
| 254 | // and a second attach/detach cycle would leak the worker thread, shutdown event, and instance mutex (the | ||
| 255 | // header contract promises a subsequent attach starts from a clean slate). Reset only on the success path: | ||
| 256 | // early failures above never set the gate, so they must not clear it either. Release pairs with the acquire | ||
| 257 | // in on_dll_detach's compare_exchange. Both run serialized under the loader lock. | ||
| 258 | 4 | s_detach_called.store(false, std::memory_order_release); | |
| 259 | |||
| 260 | 4 | return TRUE; | |
| 261 | } | ||
| 262 | } // anonymous namespace | ||
| 263 | |||
| 264 | 10 | [[nodiscard]] bool32_t on_dll_attach(module_handle_t hMod, const ModInfo &info, std::function<bool()> init_fn, | |
| 265 | std::function<void()> shutdown_fn) noexcept | ||
| 266 | { | ||
| 267 | // Fail closed on any exception so nothing unwinds across the loader lock (see attach_core). On a throw the | ||
| 268 | // partially-built attach state is rolled back through the same path the explicit early-failure returns use. | ||
| 269 | try | ||
| 270 | { | ||
| 271 |
1/2✓ Branch 8 → 9 taken 10 times.
✗ Branch 8 → 14 not taken.
|
20 | return attach_core(hMod, info, std::move(init_fn), std::move(shutdown_fn)); |
| 272 | } | ||
| 273 | ✗ | catch (...) | |
| 274 | { | ||
| 275 | ✗ | unwind_early_attach_failure(); | |
| 276 | ✗ | return FALSE; | |
| 277 | ✗ | } | |
| 278 | } | ||
| 279 | |||
| 280 | 8 | void request_shutdown() noexcept | |
| 281 | { | ||
| 282 |
2/2✓ Branch 2 → 3 taken 6 times.
✓ Branch 2 → 4 taken 2 times.
|
8 | if (s_shutdown_event) |
| 283 | { | ||
| 284 | 6 | SetEvent(s_shutdown_event); | |
| 285 | } | ||
| 286 | 8 | } | |
| 287 | |||
| 288 | 4 | void on_dll_detach(bool32_t is_process_exit) noexcept | |
| 289 | { | ||
| 290 | 4 | bool expected = false; | |
| 291 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 4 times.
|
4 | if (!s_detach_called.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) |
| 292 | { | ||
| 293 | ✗ | return; | |
| 294 | } | ||
| 295 | |||
| 296 | // On process exit Windows has already terminated every other thread in the process before calling DllMain with | ||
| 297 | // DLL_PROCESS_DETACH. Waiting on s_worker_thread would block forever because the worker was abruptly killed | ||
| 298 | // mid-WaitForSingleObject. Skip the wait and run the explicit shutdown path directly. | ||
| 299 |
1/2✗ Branch 5 → 6 not taken.
✓ Branch 5 → 10 taken 4 times.
|
4 | if (is_process_exit) |
| 300 | { | ||
| 301 | ✗ | if (s_shutdown_fn) | |
| 302 | { | ||
| 303 | try | ||
| 304 | { | ||
| 305 | ✗ | s_shutdown_fn(); | |
| 306 | } | ||
| 307 | ✗ | catch (const std::exception &e) | |
| 308 | { | ||
| 309 | try | ||
| 310 | { | ||
| 311 | ✗ | Logger::get_instance().error("Bootstrap: shutdown_fn threw: {}", e.what()); | |
| 312 | } | ||
| 313 | ✗ | catch (...) | |
| 314 | { | ||
| 315 | ✗ | } | |
| 316 | ✗ | } | |
| 317 | ✗ | catch (...) | |
| 318 | { | ||
| 319 | try | ||
| 320 | { | ||
| 321 | ✗ | Logger::get_instance().error("Bootstrap: shutdown_fn threw unknown exception."); | |
| 322 | } | ||
| 323 | ✗ | catch (...) | |
| 324 | { | ||
| 325 | ✗ | } | |
| 326 | ✗ | } | |
| 327 | } | ||
| 328 | ✗ | DMK_Shutdown(); | |
| 329 | } | ||
| 330 |
1/2✓ Branch 10 → 11 taken 4 times.
✗ Branch 10 → 13 not taken.
|
4 | else if (s_shutdown_event) |
| 331 | { | ||
| 332 | // Dynamic unload under loader lock. Signal the worker but do NOT wait here: blocking under loader lock | ||
| 333 | // deadlocks any peer DllMain that touches Win32 APIs. The contract (see bootstrap.hpp) is that callers who | ||
| 334 | // need a clean unload call request_shutdown() before FreeLibrary and give the worker time to drain. | ||
| 335 | 4 | SetEvent(s_shutdown_event); | |
| 336 | 4 | DetourModKit::Diagnostics::record_intentional_leak(DetourModKit::Diagnostics::LeakSubsystem::Bootstrap); | |
| 337 | } | ||
| 338 | |||
| 339 |
1/2✓ Branch 13 → 14 taken 4 times.
✗ Branch 13 → 16 not taken.
|
4 | if (s_shutdown_event) |
| 340 | { | ||
| 341 | 4 | CloseHandle(s_shutdown_event); | |
| 342 | 4 | s_shutdown_event = nullptr; | |
| 343 | } | ||
| 344 | |||
| 345 |
1/2✓ Branch 16 → 17 taken 4 times.
✗ Branch 16 → 19 not taken.
|
4 | if (s_worker_thread) |
| 346 | { | ||
| 347 | 4 | CloseHandle(s_worker_thread); | |
| 348 | 4 | s_worker_thread = nullptr; | |
| 349 | } | ||
| 350 | |||
| 351 | 4 | release_instance_mutex(); | |
| 352 | 4 | s_module = nullptr; | |
| 353 | } | ||
| 354 | |||
| 355 | 6 | module_handle_t module_handle() noexcept | |
| 356 | { | ||
| 357 | 6 | return s_module; | |
| 358 | } | ||
| 359 | |||
| 360 | 9 | void on_logic_dll_unload(std::span<const std::string_view> hook_names, | |
| 361 | std::span<const std::string_view> binding_names) noexcept | ||
| 362 | { | ||
| 363 | 9 | Logger &logger = Logger::get_instance(); | |
| 364 | 9 | size_t hooks_removed = 0; | |
| 365 | 9 | size_t bindings_removed = 0; | |
| 366 | |||
| 367 |
2/2✓ Branch 26 → 5 taken 8 times.
✓ Branch 26 → 27 taken 9 times.
|
26 | for (const auto name : hook_names) |
| 368 | { | ||
| 369 | try | ||
| 370 | { | ||
| 371 |
2/4✓ Branch 7 → 8 taken 8 times.
✗ Branch 7 → 49 not taken.
✓ Branch 8 → 9 taken 8 times.
✗ Branch 8 → 49 not taken.
|
8 | auto result = HookManager::get_instance().remove_hook(name); |
| 372 |
2/2✓ Branch 10 → 11 taken 5 times.
✓ Branch 10 → 12 taken 3 times.
|
8 | if (result) |
| 373 | { | ||
| 374 | 5 | ++hooks_removed; | |
| 375 | } | ||
| 376 | else | ||
| 377 | { | ||
| 378 |
1/2✓ Branch 14 → 15 taken 3 times.
✗ Branch 14 → 47 not taken.
|
3 | logger.debug("Bootstrap: on_logic_dll_unload: hook '{}' not removed ({}).", name, |
| 379 | 6 | Hook::error_to_string(result.error())); | |
| 380 | } | ||
| 381 | } | ||
| 382 | ✗ | catch (const std::exception &e) | |
| 383 | { | ||
| 384 | ✗ | logger.error("Bootstrap: on_logic_dll_unload caught exception removing hook '{}': {}", name, e.what()); | |
| 385 | ✗ | } | |
| 386 | ✗ | catch (...) | |
| 387 | { | ||
| 388 | ✗ | logger.error("Bootstrap: on_logic_dll_unload caught unknown exception removing hook '{}'.", name); | |
| 389 | ✗ | } | |
| 390 | } | ||
| 391 | |||
| 392 |
2/2✓ Branch 42 → 29 taken 9 times.
✓ Branch 42 → 43 taken 9 times.
|
27 | for (const auto name : binding_names) |
| 393 | { | ||
| 394 | try | ||
| 395 | { | ||
| 396 | // Pass invoke_callbacks=false because this helper is documented as safe from DllMain detach paths. User | ||
| 397 | // on_state_change(false) | ||
| 398 | // callbacks for held bindings live in the unloading Logic DLL; | ||
| 399 | // running them under loader lock is the deadlock-or-crash vector that the leak-on-purpose discipline | ||
| 400 | // was set up to forbid. | ||
| 401 | 9 | bindings_removed += InputManager::get_instance().remove_binding_by_name(name, false); | |
| 402 | } | ||
| 403 | catch (const std::exception &e) | ||
| 404 | { | ||
| 405 | logger.error("Bootstrap: on_logic_dll_unload caught exception removing binding '{}': {}", name, | ||
| 406 | e.what()); | ||
| 407 | } | ||
| 408 | catch (...) | ||
| 409 | { | ||
| 410 | logger.error("Bootstrap: on_logic_dll_unload caught unknown exception removing binding '{}'.", name); | ||
| 411 | } | ||
| 412 | } | ||
| 413 | |||
| 414 | 9 | logger.info("Bootstrap: on_logic_dll_unload drained {} hook(s) and {} binding(s).", hooks_removed, | |
| 415 | bindings_removed); | ||
| 416 | |||
| 417 | // Wipe the Config registry last because the prior hook and binding teardown may invoke a registered setter one | ||
| 418 | // final time (a setter that observes a binding-driven flag, for instance). Clearing first would orphan that | ||
| 419 | // final-fire path mid-call. The registered std::function setters' call operators, vtables, and destructors live | ||
| 420 | // in the Logic DLL's | ||
| 421 | // .text segment; once the loader unmaps that segment, every surviving entry becomes a use-after-unload hazard. | ||
| 422 | // The next attach's replace_or_append destroys the stale slot before installing the fresh one, which would | ||
| 423 | // invoke the old setter's destructor against freed pages. | ||
| 424 | // | ||
| 425 | // Stop the auto-reload watcher before wiping the registry: its worker can fire reload() (running the registered | ||
| 426 | // setters) at any time, and both the user on_reload callback and those setters live in the unloading Logic DLL. | ||
| 427 | // A survivor would call into unmapped pages after the DLL goes away, and would make the next attach's | ||
| 428 | // enable_auto_reload() report AlreadyRunning instead of starting fresh. disable_auto_reload() is noexcept and a | ||
| 429 | // no-op when no watcher is running. | ||
| 430 | 9 | Config::disable_auto_reload(); | |
| 431 | 9 | Config::clear_registered_items(); | |
| 432 | 9 | } | |
| 433 | |||
| 434 | 10 | void on_logic_dll_unload_all() noexcept | |
| 435 | { | ||
| 436 | 10 | Logger &logger = Logger::get_instance(); | |
| 437 | |||
| 438 | // Hooks first so the original prologue bytes are restored before the binding teardown can disturb any callback | ||
| 439 | // that still trampolines through SafetyHook. remove_all_hooks() resets m_shutdown_called at the end, leaving | ||
| 440 | // HookManager re-usable for the next attach. | ||
| 441 | try | ||
| 442 | { | ||
| 443 |
1/2✓ Branch 3 → 4 taken 10 times.
✗ Branch 3 → 12 not taken.
|
10 | HookManager::get_instance().remove_all_hooks(); |
| 444 | } | ||
| 445 | ✗ | catch (const std::exception &e) | |
| 446 | { | ||
| 447 | try | ||
| 448 | { | ||
| 449 | ✗ | logger.error("Bootstrap: on_logic_dll_unload_all caught exception in remove_all_hooks: {}", e.what()); | |
| 450 | } | ||
| 451 | ✗ | catch (...) | |
| 452 | { | ||
| 453 | ✗ | } | |
| 454 | ✗ | } | |
| 455 | ✗ | catch (...) | |
| 456 | { | ||
| 457 | try | ||
| 458 | { | ||
| 459 | ✗ | logger.error("Bootstrap: on_logic_dll_unload_all caught unknown exception in remove_all_hooks."); | |
| 460 | } | ||
| 461 | ✗ | catch (...) | |
| 462 | { | ||
| 463 | ✗ | } | |
| 464 | ✗ | } | |
| 465 | |||
| 466 | // clear_bindings() leaves the poll thread running and ready to accept fresh bindings, matching the "tear down | ||
| 467 | // per-Logic-DLL state but keep the manager re-usable" contract that the named-list overload honours. Pass | ||
| 468 | // invoke_callbacks=false because this helper is documented as safe from DllMain detach paths: user release | ||
| 469 | // callbacks live in the unloading Logic DLL and must not be invoked under loader lock. | ||
| 470 | try | ||
| 471 | { | ||
| 472 | 10 | InputManager::get_instance().clear_bindings(false); | |
| 473 | } | ||
| 474 | catch (const std::exception &e) | ||
| 475 | { | ||
| 476 | try | ||
| 477 | { | ||
| 478 | logger.error("Bootstrap: on_logic_dll_unload_all caught exception in clear_bindings: {}", e.what()); | ||
| 479 | } | ||
| 480 | catch (...) | ||
| 481 | { | ||
| 482 | } | ||
| 483 | } | ||
| 484 | catch (...) | ||
| 485 | { | ||
| 486 | try | ||
| 487 | { | ||
| 488 | logger.error("Bootstrap: on_logic_dll_unload_all caught unknown exception in clear_bindings."); | ||
| 489 | } | ||
| 490 | catch (...) | ||
| 491 | { | ||
| 492 | } | ||
| 493 | } | ||
| 494 | |||
| 495 | try | ||
| 496 | { | ||
| 497 |
1/2✓ Branch 7 → 8 taken 10 times.
✗ Branch 7 → 31 not taken.
|
10 | logger.info("Bootstrap: on_logic_dll_unload_all drained all hooks and bindings."); |
| 498 | } | ||
| 499 | ✗ | catch (...) | |
| 500 | { | ||
| 501 | ✗ | } | |
| 502 | |||
| 503 | // Wipe the Config registry last for the same reason as the named-list overload: the prior remove_all_hooks / | ||
| 504 | // clear_bindings calls may fire a registered setter one final time, and clearing first would orphan that path. | ||
| 505 | // The registered std::function setters' call operators, vtables, | ||
| 506 | // and destructors live in the unloading Logic DLL's .text; | ||
| 507 | // every surviving entry becomes a use-after-unload hazard the moment the loader reclaims those pages. | ||
| 508 | // | ||
| 509 | // Stop the auto-reload watcher before wiping the registry: its worker can fire reload() (running the registered | ||
| 510 | // setters) at any time, and both the user on_reload callback and those setters live in the unloading Logic DLL. | ||
| 511 | // A survivor would call into unmapped pages after the DLL goes away, and would make the next attach's | ||
| 512 | // enable_auto_reload() report AlreadyRunning instead of starting fresh. disable_auto_reload() is noexcept and a | ||
| 513 | // no-op when no watcher is running. | ||
| 514 | 10 | Config::disable_auto_reload(); | |
| 515 | 10 | Config::clear_registered_items(); | |
| 516 | 10 | } | |
| 517 | } // namespace DetourModKit::Bootstrap | ||
| 518 |