GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 76.0% 414 / 0 / 545
Functions: 93.3% 28 / 0 / 30
Branches: 36.2% 390 / 0 / 1077

src/hook_manager.cpp
Line Branch Exec Source
1 #include "DetourModKit/hook_manager.hpp"
2 #include "DetourModKit/format.hpp"
3 #include "DetourModKit/memory.hpp"
4 #include "platform.hpp"
5 #include "x86_decode.hpp"
6
7 #include <windows.h>
8
9 #include <algorithm>
10 #include <cstddef>
11 #include <cstdint>
12 #include <format>
13 #include <memory>
14 #include <new>
15 #include <optional>
16 #include <vector>
17
18 using namespace DetourModKit;
19 using namespace DetourModKit::Scanner;
20
21 namespace
22 {
23 enum class PrehookState
24 {
25 NotHooked,
26 HookedBySameModule,
27 HookedByOtherModule
28 };
29
30 struct PrehookDetection
31 {
32 PrehookState state{PrehookState::NotHooked};
33 std::uintptr_t jmp_destination{0};
34 };
35
36 75 std::optional<std::uintptr_t> decode_prehook_destination(std::uintptr_t target_address) noexcept
37 {
38
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 75 times.
75 if (!Memory::is_readable(reinterpret_cast<const void *>(target_address), 2))
39 {
40 return std::nullopt;
41 }
42 75 const auto *bytes = reinterpret_cast<const std::uint8_t *>(target_address);
43
44
2/2
✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 7 taken 73 times.
75 if (bytes[0] == 0xE9)
45 {
46 2 return detail::decode_e9_rel32(target_address);
47 }
48
49
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 73 times.
73 if (bytes[0] == 0xEB)
50 {
51 return detail::decode_eb_rel8(target_address);
52 }
53
54
1/4
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 12 taken 73 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
73 if (bytes[0] == 0xFF && bytes[1] == 0x25)
55 {
56 return detail::decode_ff25_indirect(target_address);
57 }
58
59 73 return std::nullopt;
60 }
61
62 75 PrehookDetection detect_existing_inline_hook(std::uintptr_t target_address) noexcept
63 {
64 75 PrehookDetection result;
65
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 75 times.
75 if (target_address == 0)
66 {
67 return result;
68 }
69
70 75 const auto destination_opt = decode_prehook_destination(target_address);
71
2/2
✓ Branch 6 → 7 taken 73 times.
✓ Branch 6 → 8 taken 2 times.
75 if (!destination_opt)
72 {
73 73 return result;
74 }
75 2 const auto destination = *destination_opt;
76 2 result.jmp_destination = destination;
77
78 2 HMODULE target_module = nullptr;
79 2 HMODULE dest_module = nullptr;
80
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 2 times.
2 if (!GetModuleHandleExW(
81 GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
82 reinterpret_cast<LPCWSTR>(target_address), &target_module))
83 {
84 target_module = nullptr;
85 }
86
1/2
✓ Branch 13 → 14 taken 2 times.
✗ Branch 13 → 15 not taken.
2 if (!GetModuleHandleExW(
87 GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
88 reinterpret_cast<LPCWSTR>(destination), &dest_module))
89 {
90 2 dest_module = nullptr;
91 }
92
93
1/4
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 18 taken 2 times.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
2 if (dest_module != nullptr && target_module == dest_module)
94 {
95 result.state = PrehookState::HookedBySameModule;
96 }
97 else
98 {
99 2 result.state = PrehookState::HookedByOtherModule;
100 }
101 2 return result;
102 }
103 } // anonymous namespace
104
105 240 HookManager &HookManager::get_instance()
106 {
107
5/10
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 9 taken 239 times.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 9 not taken.
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 11 not taken.
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
✗ Branch 11 → 12 not taken.
✗ Branch 11 → 13 not taken.
240 static HookManager instance;
108 240 return instance;
109 }
110
111 1 HookManager::HookManager(Logger &logger)
112 1 : m_logger(logger)
113 {
114
1/2
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 19 not taken.
1 m_allocator = safetyhook::Allocator::global();
115
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 15 taken 1 time.
1 if (!m_allocator)
116 {
117 m_logger.error("HookManager: Failed to get SafetyHook global allocator! Hook creation will fail.");
118 }
119 else
120 {
121
1/2
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 21 not taken.
1 m_logger.debug("HookManager: SafetyHook global allocator obtained.");
122 }
123
1/2
✓ Branch 17 → 18 taken 1 time.
✗ Branch 17 → 22 not taken.
1 m_logger.debug("HookManager: Initialized.");
124 1 }
125
126 2 HookManager::~HookManager() noexcept
127 {
128
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1 time.
1 if (m_shutdown_called.load(std::memory_order_acquire))
129 {
130 return;
131 }
132
133 // Fallback teardown path. Reached only when the process did not call
134 // DMK_Shutdown() / HookManager::shutdown() before static destruction
135 // (abnormal exit, FreeLibrary race, host crash).
136 //
137 // Ordering (matches the shutdown() contract so readers see one story):
138 // 1. Flip m_shutdown_called under m_mutator_gate (exclusive) to
139 // block new mutators and serialize with a late shutdown() call.
140 // 2. Disable all hooks under shared m_hooks_mutex so that in-flight
141 // trampoline callers can drain from SafetyHook::disable().
142 // 3. Acquire exclusive m_hooks_mutex to wait out any shared_lock
143 // holder still inside a with_* callback. Only then clear the
144 // maps -- destroying the Hook objects would UAF a live reader.
145 //
146 // Loader-lock fallback: if the destructor is fired with the OS loader
147 // lock held (e.g. during abnormal DLL unload), acquiring m_mutator_gate
148 // or blocking readers can deadlock against another thread waiting on a
149 // loader callback. Leak the maps in that case; the pinned module keeps
150 // the hooks' code pages live so SafetyHook trampolines do not dangle.
151 // Mirrors the pattern used in Logger::shutdown_internal().
152
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 30 taken 1 time.
1 if (detail::is_loader_lock_held())
153 {
154 detail::pin_current_module();
155 // Intentional leak under loader lock: draining readers or destroying
156 // Hook / VmtHookEntry values here can deadlock against another thread
157 // waiting on a loader callback. The pinned module keeps the SafetyHook
158 // trampoline pages live for the remainder of the process, so the leaked
159 // maps and their contents stay valid storage even though no one will
160 // ever observe them again. HookManager is a singleton so this branch
161 // runs at most once per process.
162 //
163 // Heap-allocate each map directly rather than nesting them inside an
164 // outer container. A container of containers would force the standard
165 // library to instantiate a copy-construction fallback for the element
166 // type whenever the element's move constructor is not unconditionally
167 // noexcept, and that copy path would try to copy a move-only member
168 // (VmtHookEntry owns a safetyhook::VmtHook).
169 //
170 // Default-construct each map on the heap, then swap content in.
171 // std::unordered_map's move constructor is not guaranteed noexcept on
172 // every standard library implementation (some mark it noexcept(false)
173 // so allocator-propagation fallbacks can allocate); invoking it from a
174 // noexcept destructor would risk std::terminate on an unexpected
175 // allocator fallback. With a default std::allocator (stateless,
176 // is_always_equal) and noexcept hasher / comparator, member swap() is
177 // specified noexcept and performs an O(1) pointer swap that allocates
178 // nothing, so it is safe to call from a noexcept context. On
179 // allocation failure the new (std::nothrow) expression returns
180 // nullptr, the source maps keep their contents, and control falls
181 // through to the normal ~HookManager member destruction epilogue.
182 // That epilogue is best-effort under loader lock, but the pinned
183 // module still keeps trampoline code pages live so straggler
184 // trampoline calls land on valid memory.
185 auto *leaked_hooks = new (std::nothrow) detail::HookMap();
186 auto *leaked_vmt_hooks = new (std::nothrow) detail::VmtHookMap();
187 if (leaked_hooks != nullptr)
188 {
189 leaked_hooks->swap(m_hooks);
190 }
191 if (leaked_vmt_hooks != nullptr)
192 {
193 leaked_vmt_hooks->swap(m_vmt_hooks);
194 }
195 m_shutdown_called.store(true, std::memory_order_release);
196 return;
197 }
198
199 1 std::unique_lock<std::shared_mutex> mutator_gate(m_mutator_gate);
200 1 m_shutdown_called.store(true, std::memory_order_release);
201
202 {
203 1 std::shared_lock<std::shared_mutex> shared(m_hooks_mutex);
204
1/2
✗ Branch 42 → 35 not taken.
✓ Branch 42 → 43 taken 1 time.
1 for (auto &[name, hook] : m_hooks)
205 {
206 (void)hook->disable();
207 }
208 1 }
209
210 1 std::unique_lock<std::shared_mutex> lock(m_hooks_mutex);
211 1 m_vmt_hooks.clear();
212 1 m_hooks.clear();
213
3/6
✓ Branch 52 → 53 taken 1 time.
✗ Branch 52 → 54 not taken.
✓ Branch 56 → 57 taken 1 time.
✗ Branch 56 → 58 not taken.
✓ Branch 60 → 61 taken 1 time.
✗ Branch 60 → 62 not taken.
1 }
214
215 44 void HookManager::shutdown()
216 {
217 // Serialize with remove_all_hooks() via compare_exchange_strong.
218 // Only one teardown owner proceeds.
219 44 bool expected = false;
220
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 44 times.
44 if (!m_shutdown_called.compare_exchange_strong(expected, true, std::memory_order_acq_rel))
221 return;
222
223 // Block all mutators (create_*_hook, enable, disable, remove) before
224 // entering phase 1. They hold shared on m_mutator_gate, so acquiring
225 // exclusive here waits for active mutators and blocks new ones.
226
1/2
✓ Branch 5 → 6 taken 44 times.
✗ Branch 5 → 34 not taken.
44 std::unique_lock<std::shared_mutex> mutator_gate(m_mutator_gate);
227
228 // Two-phase shutdown: disable hooks under a shared lock first so that
229 // hooked threads blocked on m_hooks_mutex can drain from SafetyHook's
230 // disable() without deadlock, then clear the maps under exclusive lock.
231 {
232
1/2
✓ Branch 6 → 7 taken 44 times.
✗ Branch 6 → 30 not taken.
44 std::shared_lock<std::shared_mutex> shared(m_hooks_mutex);
233
2/2
✓ Branch 16 → 9 taken 10 times.
✓ Branch 16 → 17 taken 44 times.
54 for (auto &[name, hook] : m_hooks)
234 {
235
1/2
✓ Branch 13 → 14 taken 10 times.
✗ Branch 13 → 26 not taken.
10 (void)hook->disable();
236 }
237 44 }
238 {
239
1/2
✓ Branch 18 → 19 taken 44 times.
✗ Branch 18 → 31 not taken.
44 std::unique_lock<std::shared_mutex> lock(m_hooks_mutex);
240 44 m_vmt_hooks.clear();
241 44 m_hooks.clear();
242
243 // Reset under the lock so concurrent create_*_hook calls cannot
244 // observe the flag as true (rejected) and then immediately see it
245 // as false (accepted) before the map is fully cleared.
246 //
247 // This intentionally allows reuse after shutdown (hot-reload).
248 // The exclusive lock on m_mutator_gate serializes the entire
249 // clear-and-reset sequence, so there is no window where a
250 // concurrent create_*_hook can slip through against a half-cleared
251 // map. The mutator_gate exclusive lock is released here, allowing
252 // new mutators to proceed with a fresh m_shutdown_called=false.
253 44 m_shutdown_called.store(false, std::memory_order_release);
254 44 }
255 44 }
256
257 std::string HookManager::error_to_string(const safetyhook::InlineHook::Error &err) const
258 {
259 const int type_int = static_cast<int>(err.type);
260 const auto ip_str = DetourModKit::Format::format_address(reinterpret_cast<uintptr_t>(err.ip));
261
262 switch (err.type)
263 {
264 case safetyhook::InlineHook::Error::BAD_ALLOCATION:
265 return std::format("SafetyHook InlineHook Error (Type: {}): Bad allocation (Allocator error: {})",
266 type_int, static_cast<int>(err.allocator_error));
267 case safetyhook::InlineHook::Error::FAILED_TO_DECODE_INSTRUCTION:
268 return std::format("SafetyHook InlineHook Error (Type: {}): Failed to decode instruction at address {}",
269 type_int, ip_str);
270 case safetyhook::InlineHook::Error::SHORT_JUMP_IN_TRAMPOLINE:
271 return std::format("SafetyHook InlineHook Error (Type: {}): Short jump found in trampoline at address {}",
272 type_int, ip_str);
273 case safetyhook::InlineHook::Error::IP_RELATIVE_INSTRUCTION_OUT_OF_RANGE:
274 return std::format("SafetyHook InlineHook Error (Type: {}): IP-relative instruction out of range at address {}",
275 type_int, ip_str);
276 case safetyhook::InlineHook::Error::UNSUPPORTED_INSTRUCTION_IN_TRAMPOLINE:
277 return std::format("SafetyHook InlineHook Error (Type: {}): Unsupported instruction in trampoline at address {}",
278 type_int, ip_str);
279 case safetyhook::InlineHook::Error::FAILED_TO_UNPROTECT:
280 return std::format("SafetyHook InlineHook Error (Type: {}): Failed to unprotect memory at address {}",
281 type_int, ip_str);
282 case safetyhook::InlineHook::Error::NOT_ENOUGH_SPACE:
283 return std::format("SafetyHook InlineHook Error (Type: {}): Not enough space for the hook (prologue too short) at address {}",
284 type_int, ip_str);
285 default:
286 return std::format("SafetyHook InlineHook Error (Type: {}): Unknown error type", type_int);
287 }
288 }
289
290 std::string HookManager::error_to_string(const safetyhook::MidHook::Error &err) const
291 {
292 const int type_int = static_cast<int>(err.type);
293
294 switch (err.type)
295 {
296 case safetyhook::MidHook::Error::BAD_ALLOCATION:
297 return std::format("SafetyHook MidHook Error (Type: {}): Bad allocation (Allocator error: {})",
298 type_int, static_cast<int>(err.allocator_error));
299 case safetyhook::MidHook::Error::BAD_INLINE_HOOK:
300 return std::format("SafetyHook MidHook Error (Type: {}): Bad underlying inline hook. Details: {}",
301 type_int, error_to_string(err.inline_hook_error));
302 default:
303 return std::format("SafetyHook MidHook Error (Type: {}): Unknown error type", type_int);
304 }
305 }
306
307 83 std::expected<std::string, HookError> HookManager::create_inline_hook(
308 std::string_view name,
309 uintptr_t target_address,
310 void *detour_function,
311 void **original_trampoline,
312 const HookConfig &config)
313 {
314 // Non-locking fast-fail: avoid acquiring mutator_gate when shutdown
315 // is already in progress.
316
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 10 taken 83 times.
83 if (m_shutdown_called.load(std::memory_order_acquire))
317 {
318 if (original_trampoline)
319 *original_trampoline = nullptr;
320 m_logger.error("HookManager: Shutdown in progress. Cannot create inline hook '{}'.", name);
321 return std::unexpected(HookError::ShutdownInProgress);
322 }
323
324 83 auto [result, deferred_logs] = [&]() -> std::pair<std::expected<std::string, HookError>, std::vector<DeferredLogEntry>>
325 {
326
1/2
✓ Branch 2 → 3 taken 83 times.
✗ Branch 2 → 515 not taken.
83 std::shared_lock<std::shared_mutex> mutator_gate(m_mutator_gate);
327
1/2
✓ Branch 3 → 4 taken 83 times.
✗ Branch 3 → 513 not taken.
83 std::unique_lock<std::shared_mutex> lock(m_hooks_mutex);
328
329 // Re-check after acquiring locks (double-checked pattern).
330
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 20 taken 83 times.
83 if (m_shutdown_called.load(std::memory_order_acquire))
331 {
332 return {std::unexpected(HookError::ShutdownInProgress),
333 {{std::format("HookManager: Shutdown in progress. Cannot create inline hook '{}'.", name), LogLevel::Error}}};
334 }
335
1/2
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 36 taken 83 times.
83 if (!m_allocator)
336 {
337 return {std::unexpected(HookError::AllocatorNotAvailable),
338 {{std::format("HookManager: Allocator not available. Cannot create inline hook '{}'.", name), LogLevel::Error}}};
339 }
340
2/2
✓ Branch 36 → 37 taken 4 times.
✓ Branch 36 → 51 taken 79 times.
83 if (target_address == 0)
341 {
342 4 return {std::unexpected(HookError::InvalidTargetAddress),
343
3/6
✓ Branch 41 → 42 taken 4 times.
✗ Branch 41 → 252 not taken.
✓ Branch 46 → 47 taken 4 times.
✓ Branch 46 → 48 taken 4 times.
✗ Branch 256 → 257 not taken.
✗ Branch 256 → 258 not taken.
12 {{std::format("HookManager: Target address is NULL for inline hook '{}'.", name), LogLevel::Error}}};
344 }
345
2/2
✓ Branch 51 → 52 taken 2 times.
✓ Branch 51 → 66 taken 77 times.
79 if (detour_function == nullptr)
346 {
347 2 return {std::unexpected(HookError::InvalidDetourFunction),
348
3/6
✓ Branch 56 → 57 taken 2 times.
✗ Branch 56 → 272 not taken.
✓ Branch 61 → 62 taken 2 times.
✓ Branch 61 → 63 taken 2 times.
✗ Branch 276 → 277 not taken.
✗ Branch 276 → 278 not taken.
6 {{std::format("HookManager: Detour function is NULL for inline hook '{}'.", name), LogLevel::Error}}};
349 }
350
2/2
✓ Branch 66 → 67 taken 1 time.
✓ Branch 66 → 81 taken 76 times.
77 if (original_trampoline == nullptr)
351 {
352 1 return {std::unexpected(HookError::InvalidTrampolinePointer),
353
3/6
✓ Branch 71 → 72 taken 1 time.
✗ Branch 71 → 292 not taken.
✓ Branch 76 → 77 taken 1 time.
✓ Branch 76 → 78 taken 1 time.
✗ Branch 296 → 297 not taken.
✗ Branch 296 → 298 not taken.
3 {{std::format("HookManager: Original trampoline pointer (output) is NULL for inline hook '{}'.", name), LogLevel::Error}}};
354 }
355 76 *original_trampoline = nullptr;
356
357
3/4
✓ Branch 81 → 82 taken 76 times.
✗ Branch 81 → 511 not taken.
✓ Branch 82 → 83 taken 1 time.
✓ Branch 82 → 97 taken 75 times.
76 if (hook_id_exists_locked(name))
358 {
359 1 return {std::unexpected(HookError::HookAlreadyExists),
360
3/6
✓ Branch 87 → 88 taken 1 time.
✗ Branch 87 → 312 not taken.
✓ Branch 92 → 93 taken 1 time.
✓ Branch 92 → 94 taken 1 time.
✗ Branch 316 → 317 not taken.
✗ Branch 316 → 318 not taken.
3 {{std::format("HookManager: A hook with the name '{}' already exists.", name), LogLevel::Error}}};
361 }
362
363 75 const auto prehook = detect_existing_inline_hook(target_address);
364
2/2
✓ Branch 98 → 99 taken 2 times.
✓ Branch 98 → 118 taken 73 times.
75 if (prehook.state == PrehookState::HookedByOtherModule &&
365
2/2
✓ Branch 99 → 100 taken 1 time.
✓ Branch 99 → 118 taken 1 time.
2 config.fail_if_already_hooked)
366 {
367 1 return {std::unexpected(HookError::TargetAlreadyHookedInProcess),
368 {{std::format("HookManager: Target {} for inline hook '{}' is already inline-hooked by another module (JMP -> {}). Aborting under strict mode.",
369 DetourModKit::Format::format_address(target_address), name,
370 1 DetourModKit::Format::format_address(prehook.jmp_destination)),
371
3/6
✓ Branch 106 → 107 taken 1 time.
✗ Branch 106 → 332 not taken.
✓ Branch 111 → 112 taken 1 time.
✓ Branch 111 → 113 taken 1 time.
✗ Branch 336 → 337 not taken.
✗ Branch 336 → 338 not taken.
4 LogLevel::Error}}};
372 }
373
374 try
375 {
376 74 auto sh_flags = config.auto_enable
377
2/2
✓ Branch 118 → 119 taken 72 times.
✓ Branch 118 → 120 taken 2 times.
74 ? safetyhook::InlineHook::Default
378 : safetyhook::InlineHook::StartDisabled;
379
380 auto hook_creation_result = safetyhook::InlineHook::create(
381 74 m_allocator,
382 74 reinterpret_cast<void *>(target_address),
383 detour_function,
384
1/2
✓ Branch 121 → 122 taken 74 times.
✗ Branch 121 → 433 not taken.
74 sh_flags);
385
386
1/2
✗ Branch 123 → 124 not taken.
✓ Branch 123 → 143 taken 74 times.
74 if (!hook_creation_result)
387 {
388 return {std::unexpected(HookError::SafetyHookError),
389 {{std::format("HookManager: Failed to create SafetyHook::InlineHook for '{}' at {}. Error: {}",
390 name, DetourModKit::Format::format_address(target_address), error_to_string(hook_creation_result.error())),
391 LogLevel::Error}}};
392 }
393
394
1/2
✓ Branch 143 → 144 taken 74 times.
✗ Branch 143 → 431 not taken.
148 auto sh_inline_hook = std::move(hook_creation_result.value());
395 74 void *trampoline = sh_inline_hook.original<void *>();
396
397
2/2
✓ Branch 149 → 150 taken 72 times.
✓ Branch 149 → 151 taken 2 times.
74 HookStatus initial_status = sh_inline_hook.enabled() ? HookStatus::Active : HookStatus::Disabled;
398
399 // Pre-build log entries before committing to m_hooks so that
400 // allocation failures in std::format cannot leave a ghost hook.
401
3/4
✓ Branch 154 → 155 taken 72 times.
✓ Branch 154 → 156 taken 2 times.
✓ Branch 157 → 158 taken 74 times.
✗ Branch 157 → 384 not taken.
74 std::string status_message = (initial_status == HookStatus::Active) ? "and enabled" : "(disabled)";
402 74 std::vector<DeferredLogEntry> logs;
403
1/2
✓ Branch 159 → 160 taken 74 times.
✗ Branch 159 → 425 not taken.
74 logs.reserve(3);
404 74 logs.push_back({std::format("HookManager: Successfully created {} inline hook '{}' targeting {}.",
405 status_message, name, DetourModKit::Format::format_address(target_address)),
406 LogLevel::Info});
407
408
2/2
✓ Branch 167 → 168 taken 1 time.
✓ Branch 167 → 178 taken 73 times.
74 if (prehook.state == PrehookState::HookedByOtherModule)
409 {
410 1 logs.push_back({std::format("HookManager: Target {} for inline hook '{}' was already inline-hooked by another module (JMP -> {}); SafetyHook layered on top.",
411 DetourModKit::Format::format_address(target_address), name,
412 1 DetourModKit::Format::format_address(prehook.jmp_destination)),
413 LogLevel::Warning});
414 }
415
416
3/4
✓ Branch 178 → 179 taken 2 times.
✓ Branch 178 → 186 taken 72 times.
✗ Branch 179 → 180 not taken.
✓ Branch 179 → 186 taken 2 times.
74 if (initial_status == HookStatus::Disabled && config.auto_enable)
417 {
418 logs.push_back({std::format("HookManager: Inline hook '{}' was configured for auto-enable but is currently disabled post-creation.", name),
419 LogLevel::Warning});
420 }
421
422
1/2
✓ Branch 188 → 189 taken 74 times.
✗ Branch 188 → 417 not taken.
222 std::string name_str{name};
423
1/2
✓ Branch 192 → 193 taken 74 times.
✗ Branch 192 → 423 not taken.
74 auto managed_hook = std::make_unique<InlineHook>(name_str, target_address, std::move(sh_inline_hook), initial_status);
424
1/2
✓ Branch 195 → 196 taken 74 times.
✗ Branch 195 → 420 not taken.
148 m_hooks.emplace(name_str, std::move(managed_hook));
425 74 *original_trampoline = trampoline;
426
427 148 return {std::move(name_str), std::move(logs)};
428 74 }
429 catch (const std::exception &e)
430 {
431 return {std::unexpected(HookError::UnknownError),
432 {{std::format("HookManager: An std::exception occurred during inline hook creation for '{}': {}", name, e.what()),
433 LogLevel::Error}}};
434 }
435 catch (...)
436 {
437 return {std::unexpected(HookError::UnknownError),
438 {{std::format("HookManager: An unknown exception occurred during inline hook creation for '{}'.", name),
439 LogLevel::Error}}};
440 }
441
22/138
✗ Branch 7 → 8 not taken.
✗ Branch 7 → 222 not taken.
✓ Branch 10 → 11 taken 83 times.
✗ Branch 10 → 39 not taken.
✗ Branch 17 → 18 not taken.
✗ Branch 17 → 19 not taken.
✗ Branch 23 → 24 not taken.
✗ Branch 23 → 242 not taken.
✗ Branch 33 → 34 not taken.
✗ Branch 33 → 35 not taken.
✓ Branch 38 → 39 taken 4 times.
✗ Branch 38 → 262 not taken.
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 50 taken 4 times.
✓ Branch 53 → 54 taken 2 times.
✗ Branch 53 → 282 not taken.
✗ Branch 63 → 64 not taken.
✓ Branch 63 → 65 taken 2 times.
✓ Branch 68 → 69 taken 1 time.
✗ Branch 68 → 302 not taken.
✗ Branch 78 → 79 not taken.
✓ Branch 78 → 80 taken 1 time.
✓ Branch 84 → 85 taken 1 time.
✗ Branch 84 → 322 not taken.
✗ Branch 94 → 95 not taken.
✓ Branch 94 → 96 taken 1 time.
✓ Branch 101 → 102 taken 1 time.
✗ Branch 101 → 348 not taken.
✓ Branch 102 → 103 taken 1 time.
✗ Branch 102 → 345 not taken.
✓ Branch 103 → 104 taken 1 time.
✗ Branch 103 → 342 not taken.
✗ Branch 113 → 114 not taken.
✓ Branch 113 → 115 taken 1 time.
✗ Branch 128 → 129 not taken.
✗ Branch 128 → 368 not taken.
✗ Branch 138 → 139 not taken.
✗ Branch 138 → 140 not taken.
✓ Branch 160 → 161 taken 74 times.
✗ Branch 160 → 395 not taken.
✓ Branch 161 → 162 taken 74 times.
✗ Branch 161 → 392 not taken.
✓ Branch 162 → 163 taken 74 times.
✗ Branch 162 → 387 not taken.
✗ Branch 164 → 165 not taken.
✓ Branch 164 → 166 taken 74 times.
✓ Branch 168 → 169 taken 1 time.
✗ Branch 168 → 408 not taken.
✓ Branch 169 → 170 taken 1 time.
✗ Branch 169 → 405 not taken.
✓ Branch 170 → 171 taken 1 time.
✗ Branch 170 → 402 not taken.
✓ Branch 171 → 172 taken 1 time.
✗ Branch 171 → 397 not taken.
✗ Branch 173 → 174 not taken.
✓ Branch 173 → 175 taken 1 time.
✗ Branch 180 → 181 not taken.
✗ Branch 180 → 415 not taken.
✗ Branch 181 → 182 not taken.
✗ Branch 181 → 410 not taken.
✗ Branch 183 → 184 not taken.
✗ Branch 183 → 185 not taken.
✗ Branch 219 → 220 not taken.
✗ Branch 219 → 221 not taken.
✗ Branch 223 → 224 not taken.
✗ Branch 223 → 227 not taken.
✗ Branch 225 → 226 not taken.
✗ Branch 225 → 227 not taken.
✗ Branch 239 → 240 not taken.
✗ Branch 239 → 241 not taken.
✗ Branch 243 → 244 not taken.
✗ Branch 243 → 247 not taken.
✗ Branch 245 → 246 not taken.
✗ Branch 245 → 247 not taken.
✗ Branch 259 → 260 not taken.
✗ Branch 259 → 261 not taken.
✗ Branch 263 → 264 not taken.
✗ Branch 263 → 267 not taken.
✗ Branch 265 → 266 not taken.
✗ Branch 265 → 267 not taken.
✗ Branch 279 → 280 not taken.
✗ Branch 279 → 281 not taken.
✗ Branch 283 → 284 not taken.
✗ Branch 283 → 287 not taken.
✗ Branch 285 → 286 not taken.
✗ Branch 285 → 287 not taken.
✗ Branch 299 → 300 not taken.
✗ Branch 299 → 301 not taken.
✗ Branch 303 → 304 not taken.
✗ Branch 303 → 307 not taken.
✗ Branch 305 → 306 not taken.
✗ Branch 305 → 307 not taken.
✗ Branch 319 → 320 not taken.
✗ Branch 319 → 321 not taken.
✗ Branch 323 → 324 not taken.
✗ Branch 323 → 327 not taken.
✗ Branch 325 → 326 not taken.
✗ Branch 325 → 327 not taken.
✗ Branch 339 → 340 not taken.
✗ Branch 339 → 341 not taken.
✗ Branch 349 → 350 not taken.
✗ Branch 349 → 353 not taken.
✗ Branch 351 → 352 not taken.
✗ Branch 351 → 353 not taken.
✗ Branch 365 → 366 not taken.
✗ Branch 365 → 367 not taken.
✗ Branch 375 → 376 not taken.
✗ Branch 375 → 379 not taken.
✗ Branch 377 → 378 not taken.
✗ Branch 377 → 379 not taken.
✗ Branch 389 → 390 not taken.
✗ Branch 389 → 391 not taken.
✗ Branch 399 → 400 not taken.
✗ Branch 399 → 401 not taken.
✗ Branch 412 → 413 not taken.
✗ Branch 412 → 414 not taken.
✗ Branch 438 → 439 not taken.
✗ Branch 438 → 476 not taken.
✗ Branch 448 → 449 not taken.
✗ Branch 448 → 450 not taken.
✗ Branch 453 → 454 not taken.
✗ Branch 453 → 499 not taken.
✗ Branch 463 → 464 not taken.
✗ Branch 463 → 465 not taken.
✗ Branch 465 → 208 not taken.
✗ Branch 465 → 511 not taken.
✗ Branch 473 → 474 not taken.
✗ Branch 473 → 475 not taken.
✗ Branch 478 → 479 not taken.
✗ Branch 478 → 482 not taken.
✗ Branch 480 → 481 not taken.
✗ Branch 480 → 482 not taken.
✗ Branch 496 → 497 not taken.
✗ Branch 496 → 498 not taken.
✗ Branch 500 → 501 not taken.
✗ Branch 500 → 504 not taken.
✗ Branch 502 → 503 not taken.
✗ Branch 502 → 504 not taken.
326 }();
442
443
2/2
✓ Branch 28 → 15 taken 84 times.
✓ Branch 28 → 29 taken 83 times.
250 for (const auto &entry : deferred_logs)
444 {
445
1/2
✓ Branch 18 → 19 taken 84 times.
✗ Branch 18 → 40 not taken.
84 m_logger.log(entry.level, entry.msg);
446 }
447 83 return result;
448
1/4
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 83 times.
✗ Branch 41 → 42 not taken.
✗ Branch 41 → 43 not taken.
166 }
449
450 8 std::expected<std::string, HookError> HookManager::create_inline_hook_aob(
451 std::string_view name,
452 uintptr_t module_base,
453 size_t module_size,
454 std::string_view aob_pattern_str,
455 ptrdiff_t aob_offset,
456 void *detour_function,
457 void **original_trampoline,
458 const HookConfig &config)
459 {
460
1/2
✓ Branch 3 → 4 taken 8 times.
✗ Branch 3 → 35 not taken.
8 m_logger.debug("HookManager: Attempting AOB scan for inline hook '{}' with pattern: \"{}\", offset: {}.",
461
1/2
✓ Branch 2 → 3 taken 8 times.
✗ Branch 2 → 38 not taken.
16 name, aob_pattern_str, DetourModKit::Format::format_hex(aob_offset));
462
463
1/2
✓ Branch 5 → 6 taken 8 times.
✗ Branch 5 → 53 not taken.
8 auto pattern = parse_aob(aob_pattern_str);
464
2/2
✓ Branch 7 → 8 taken 3 times.
✓ Branch 7 → 14 taken 5 times.
8 if (!pattern.has_value())
465 {
466
1/2
✓ Branch 8 → 9 taken 3 times.
✗ Branch 8 → 39 not taken.
3 m_logger.error("HookManager: AOB pattern parsing failed for inline hook '{}'. Pattern: \"{}\".", name, aob_pattern_str);
467
1/2
✓ Branch 9 → 10 taken 3 times.
✗ Branch 9 → 11 not taken.
3 if (original_trampoline)
468 3 *original_trampoline = nullptr;
469 3 return std::unexpected(HookError::InvalidTargetAddress);
470 }
471
472
2/4
✓ Branch 14 → 15 taken 5 times.
✗ Branch 14 → 51 not taken.
✓ Branch 15 → 16 taken 5 times.
✗ Branch 15 → 51 not taken.
5 const std::byte *found_address_start = find_pattern(reinterpret_cast<const std::byte *>(module_base), module_size, pattern.value());
473
2/2
✓ Branch 16 → 17 taken 3 times.
✓ Branch 16 → 23 taken 2 times.
5 if (!found_address_start)
474 {
475
1/2
✓ Branch 17 → 18 taken 3 times.
✗ Branch 17 → 40 not taken.
3 m_logger.error("HookManager: AOB pattern not found for inline hook '{}'. Pattern: \"{}\".", name, aob_pattern_str);
476
1/2
✓ Branch 18 → 19 taken 3 times.
✗ Branch 18 → 20 not taken.
3 if (original_trampoline)
477 3 *original_trampoline = nullptr;
478 3 return std::unexpected(HookError::InvalidTargetAddress);
479 }
480
481 2 uintptr_t target_address = reinterpret_cast<uintptr_t>(found_address_start) + aob_offset;
482
1/2
✓ Branch 26 → 27 taken 2 times.
✗ Branch 26 → 41 not taken.
2 m_logger.debug("HookManager: AOB pattern for inline hook '{}' found at {}. Applying offset {}. Final target hook address: {}.",
483
1/2
✓ Branch 25 → 26 taken 2 times.
✗ Branch 25 → 44 not taken.
4 name, DetourModKit::Format::format_address(reinterpret_cast<uintptr_t>(found_address_start)),
484
2/4
✓ Branch 23 → 24 taken 2 times.
✗ Branch 23 → 50 not taken.
✓ Branch 24 → 25 taken 2 times.
✗ Branch 24 → 47 not taken.
4 DetourModKit::Format::format_hex(aob_offset), DetourModKit::Format::format_address(target_address));
485
486
1/2
✓ Branch 30 → 31 taken 2 times.
✗ Branch 30 → 51 not taken.
2 return create_inline_hook(name, target_address, detour_function, original_trampoline, config);
487 8 }
488
489 27 std::expected<std::string, HookError> HookManager::create_mid_hook(
490 std::string_view name,
491 uintptr_t target_address,
492 safetyhook::MidHookFn detour_function,
493 const HookConfig &config)
494 {
495
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 27 times.
27 if (m_shutdown_called.load(std::memory_order_acquire))
496 {
497 m_logger.error("HookManager: Shutdown in progress. Cannot create mid hook '{}'.", name);
498 return std::unexpected(HookError::ShutdownInProgress);
499 }
500
501 27 auto [result, deferred_logs] = [&]() -> std::pair<std::expected<std::string, HookError>, std::vector<DeferredLogEntry>>
502 {
503
1/2
✓ Branch 2 → 3 taken 27 times.
✗ Branch 2 → 408 not taken.
27 std::shared_lock<std::shared_mutex> mutator_gate(m_mutator_gate);
504
1/2
✓ Branch 3 → 4 taken 27 times.
✗ Branch 3 → 406 not taken.
27 std::unique_lock<std::shared_mutex> lock(m_hooks_mutex);
505
506
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 20 taken 27 times.
27 if (m_shutdown_called.load(std::memory_order_acquire))
507 {
508 return {std::unexpected(HookError::ShutdownInProgress),
509 {{std::format("HookManager: Shutdown in progress. Cannot create mid hook '{}'.", name), LogLevel::Error}}};
510 }
511
1/2
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 36 taken 27 times.
27 if (!m_allocator)
512 {
513 return {std::unexpected(HookError::AllocatorNotAvailable),
514 {{std::format("HookManager: Allocator not available. Cannot create mid hook '{}'.", name), LogLevel::Error}}};
515 }
516
2/2
✓ Branch 36 → 37 taken 3 times.
✓ Branch 36 → 51 taken 24 times.
27 if (target_address == 0)
517 {
518 3 return {std::unexpected(HookError::InvalidTargetAddress),
519
3/6
✓ Branch 41 → 42 taken 3 times.
✗ Branch 41 → 204 not taken.
✓ Branch 46 → 47 taken 3 times.
✓ Branch 46 → 48 taken 3 times.
✗ Branch 208 → 209 not taken.
✗ Branch 208 → 210 not taken.
9 {{std::format("HookManager: Target address is NULL for mid hook '{}'.", name), LogLevel::Error}}};
520 }
521
2/2
✓ Branch 51 → 52 taken 1 time.
✓ Branch 51 → 66 taken 23 times.
24 if (detour_function == nullptr)
522 {
523 1 return {std::unexpected(HookError::InvalidDetourFunction),
524
3/6
✓ Branch 56 → 57 taken 1 time.
✗ Branch 56 → 224 not taken.
✓ Branch 61 → 62 taken 1 time.
✓ Branch 61 → 63 taken 1 time.
✗ Branch 228 → 229 not taken.
✗ Branch 228 → 230 not taken.
3 {{std::format("HookManager: Detour function is NULL for mid hook '{}'.", name), LogLevel::Error}}};
525 }
526
527
3/4
✓ Branch 66 → 67 taken 23 times.
✗ Branch 66 → 404 not taken.
✓ Branch 67 → 68 taken 1 time.
✓ Branch 67 → 82 taken 22 times.
23 if (hook_id_exists_locked(name))
528 {
529 1 return {std::unexpected(HookError::HookAlreadyExists),
530
3/6
✓ Branch 72 → 73 taken 1 time.
✗ Branch 72 → 244 not taken.
✓ Branch 77 → 78 taken 1 time.
✓ Branch 77 → 79 taken 1 time.
✗ Branch 248 → 249 not taken.
✗ Branch 248 → 250 not taken.
3 {{std::format("HookManager: A hook with the name '{}' already exists.", name), LogLevel::Error}}};
531 }
532
533 try
534 {
535 22 auto sh_flags = config.auto_enable
536
2/2
✓ Branch 82 → 83 taken 20 times.
✓ Branch 82 → 84 taken 2 times.
22 ? safetyhook::MidHook::Default
537 : safetyhook::MidHook::StartDisabled;
538
539 auto hook_creation_result = safetyhook::MidHook::create(
540 22 m_allocator,
541 22 reinterpret_cast<void *>(target_address),
542 detour_function,
543
1/2
✓ Branch 85 → 86 taken 22 times.
✗ Branch 85 → 326 not taken.
22 sh_flags);
544
545
1/2
✗ Branch 87 → 88 not taken.
✓ Branch 87 → 107 taken 22 times.
22 if (!hook_creation_result)
546 {
547 return {std::unexpected(HookError::SafetyHookError),
548 {{std::format("HookManager: Failed to create SafetyHook::MidHook for '{}' at {}. Error: {}",
549 name, DetourModKit::Format::format_address(target_address), error_to_string(hook_creation_result.error())),
550 LogLevel::Error}}};
551 }
552
553
1/2
✓ Branch 107 → 108 taken 22 times.
✗ Branch 107 → 324 not taken.
44 auto sh_mid_hook = std::move(hook_creation_result.value());
554
555
2/2
✓ Branch 112 → 113 taken 20 times.
✓ Branch 112 → 114 taken 2 times.
22 HookStatus initial_status = sh_mid_hook.enabled() ? HookStatus::Active : HookStatus::Disabled;
556
557 // Pre-build log entries before committing to m_hooks so that
558 // allocation failures in std::format cannot leave a ghost hook.
559
3/4
✓ Branch 117 → 118 taken 20 times.
✓ Branch 117 → 119 taken 2 times.
✓ Branch 120 → 121 taken 22 times.
✗ Branch 120 → 290 not taken.
22 std::string status_message = (initial_status == HookStatus::Active) ? "and enabled" : "(disabled)";
560 22 std::vector<DeferredLogEntry> logs;
561
1/2
✓ Branch 122 → 123 taken 22 times.
✗ Branch 122 → 318 not taken.
22 logs.reserve(2);
562 22 logs.push_back({std::format("HookManager: Successfully created {} mid hook '{}' targeting {}.",
563 status_message, name, DetourModKit::Format::format_address(target_address)),
564 LogLevel::Info});
565
566
3/4
✓ Branch 130 → 131 taken 2 times.
✓ Branch 130 → 138 taken 20 times.
✗ Branch 131 → 132 not taken.
✓ Branch 131 → 138 taken 2 times.
22 if (initial_status == HookStatus::Disabled && config.auto_enable)
567 {
568 logs.push_back({std::format("HookManager: Mid hook '{}' was configured for auto-enable but is currently disabled post-creation.", name),
569 LogLevel::Warning});
570 }
571
572
1/2
✓ Branch 140 → 141 taken 22 times.
✗ Branch 140 → 310 not taken.
66 std::string name_str{name};
573
1/2
✓ Branch 144 → 145 taken 22 times.
✗ Branch 144 → 316 not taken.
22 auto managed_hook = std::make_unique<MidHook>(name_str, target_address, std::move(sh_mid_hook), initial_status);
574
1/2
✓ Branch 147 → 148 taken 22 times.
✗ Branch 147 → 313 not taken.
44 m_hooks.emplace(name_str, std::move(managed_hook));
575
576 44 return {std::move(name_str), std::move(logs)};
577 22 }
578 catch (const std::exception &e)
579 {
580 return {std::unexpected(HookError::UnknownError),
581 {{std::format("HookManager: An std::exception occurred during mid hook creation for '{}': {}", name, e.what()),
582 LogLevel::Error}}};
583 }
584 catch (...)
585 {
586 return {std::unexpected(HookError::UnknownError),
587 {{std::format("HookManager: An unknown exception occurred during mid hook creation for '{}'.", name),
588 LogLevel::Error}}};
589 }
590
11/102
✗ Branch 7 → 8 not taken.
✗ Branch 7 → 174 not taken.
✓ Branch 8 → 9 taken 27 times.
✗ Branch 8 → 37 not taken.
✗ Branch 17 → 18 not taken.
✗ Branch 17 → 19 not taken.
✗ Branch 23 → 24 not taken.
✗ Branch 23 → 194 not taken.
✗ Branch 33 → 34 not taken.
✗ Branch 33 → 35 not taken.
✓ Branch 38 → 39 taken 3 times.
✗ Branch 38 → 214 not taken.
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 50 taken 3 times.
✓ Branch 53 → 54 taken 1 time.
✗ Branch 53 → 234 not taken.
✗ Branch 63 → 64 not taken.
✓ Branch 63 → 65 taken 1 time.
✓ Branch 69 → 70 taken 1 time.
✗ Branch 69 → 254 not taken.
✗ Branch 79 → 80 not taken.
✓ Branch 79 → 81 taken 1 time.
✗ Branch 92 → 93 not taken.
✗ Branch 92 → 274 not taken.
✗ Branch 102 → 103 not taken.
✗ Branch 102 → 104 not taken.
✓ Branch 123 → 124 taken 22 times.
✗ Branch 123 → 301 not taken.
✓ Branch 124 → 125 taken 22 times.
✗ Branch 124 → 298 not taken.
✓ Branch 125 → 126 taken 22 times.
✗ Branch 125 → 293 not taken.
✗ Branch 127 → 128 not taken.
✓ Branch 127 → 129 taken 22 times.
✗ Branch 132 → 133 not taken.
✗ Branch 132 → 308 not taken.
✗ Branch 133 → 134 not taken.
✗ Branch 133 → 303 not taken.
✗ Branch 135 → 136 not taken.
✗ Branch 135 → 137 not taken.
✗ Branch 171 → 172 not taken.
✗ Branch 171 → 173 not taken.
✗ Branch 175 → 176 not taken.
✗ Branch 175 → 179 not taken.
✗ Branch 177 → 178 not taken.
✗ Branch 177 → 179 not taken.
✗ Branch 191 → 192 not taken.
✗ Branch 191 → 193 not taken.
✗ Branch 195 → 196 not taken.
✗ Branch 195 → 199 not taken.
✗ Branch 197 → 198 not taken.
✗ Branch 197 → 199 not taken.
✗ Branch 211 → 212 not taken.
✗ Branch 211 → 213 not taken.
✗ Branch 215 → 216 not taken.
✗ Branch 215 → 219 not taken.
✗ Branch 217 → 218 not taken.
✗ Branch 217 → 219 not taken.
✗ Branch 231 → 232 not taken.
✗ Branch 231 → 233 not taken.
✗ Branch 235 → 236 not taken.
✗ Branch 235 → 239 not taken.
✗ Branch 237 → 238 not taken.
✗ Branch 237 → 239 not taken.
✗ Branch 251 → 252 not taken.
✗ Branch 251 → 253 not taken.
✗ Branch 255 → 256 not taken.
✗ Branch 255 → 259 not taken.
✗ Branch 257 → 258 not taken.
✗ Branch 257 → 259 not taken.
✗ Branch 271 → 272 not taken.
✗ Branch 271 → 273 not taken.
✗ Branch 281 → 282 not taken.
✗ Branch 281 → 285 not taken.
✗ Branch 283 → 284 not taken.
✗ Branch 283 → 285 not taken.
✗ Branch 295 → 296 not taken.
✗ Branch 295 → 297 not taken.
✗ Branch 305 → 306 not taken.
✗ Branch 305 → 307 not taken.
✗ Branch 331 → 332 not taken.
✗ Branch 331 → 369 not taken.
✗ Branch 341 → 342 not taken.
✗ Branch 341 → 343 not taken.
✗ Branch 346 → 347 not taken.
✗ Branch 346 → 392 not taken.
✗ Branch 356 → 357 not taken.
✗ Branch 356 → 358 not taken.
✗ Branch 358 → 160 not taken.
✗ Branch 358 → 404 not taken.
✗ Branch 366 → 367 not taken.
✗ Branch 366 → 368 not taken.
✗ Branch 371 → 372 not taken.
✗ Branch 371 → 375 not taken.
✗ Branch 373 → 374 not taken.
✗ Branch 373 → 375 not taken.
✗ Branch 389 → 390 not taken.
✗ Branch 389 → 391 not taken.
✗ Branch 393 → 394 not taken.
✗ Branch 393 → 397 not taken.
✗ Branch 395 → 396 not taken.
✗ Branch 395 → 397 not taken.
103 }();
591
592
2/2
✓ Branch 26 → 13 taken 27 times.
✓ Branch 26 → 27 taken 27 times.
81 for (const auto &entry : deferred_logs)
593 {
594
1/2
✓ Branch 16 → 17 taken 27 times.
✗ Branch 16 → 38 not taken.
27 m_logger.log(entry.level, entry.msg);
595 }
596 27 return result;
597
1/4
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 27 times.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
54 }
598
599 6 std::expected<std::string, HookError> HookManager::create_mid_hook_aob(
600 std::string_view name,
601 uintptr_t module_base,
602 size_t module_size,
603 std::string_view aob_pattern_str,
604 ptrdiff_t aob_offset,
605 safetyhook::MidHookFn detour_function,
606 const HookConfig &config)
607 {
608
1/2
✓ Branch 3 → 4 taken 6 times.
✗ Branch 3 → 31 not taken.
6 m_logger.debug("HookManager: Attempting AOB scan for mid hook '{}' with pattern: \"{}\", offset: {}.",
609
1/2
✓ Branch 2 → 3 taken 6 times.
✗ Branch 2 → 34 not taken.
12 name, aob_pattern_str, DetourModKit::Format::format_hex(aob_offset));
610
611
1/2
✓ Branch 5 → 6 taken 6 times.
✗ Branch 5 → 49 not taken.
6 auto pattern = parse_aob(aob_pattern_str);
612
2/2
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 12 taken 4 times.
6 if (!pattern.has_value())
613 {
614
1/2
✓ Branch 8 → 9 taken 2 times.
✗ Branch 8 → 35 not taken.
2 m_logger.error("HookManager: AOB pattern parsing failed for mid hook '{}'. Pattern: \"{}\".", name, aob_pattern_str);
615 2 return std::unexpected(HookError::InvalidTargetAddress);
616 }
617
618
2/4
✓ Branch 12 → 13 taken 4 times.
✗ Branch 12 → 47 not taken.
✓ Branch 13 → 14 taken 4 times.
✗ Branch 13 → 47 not taken.
4 const std::byte *found_address_start = find_pattern(reinterpret_cast<const std::byte *>(module_base), module_size, pattern.value());
619
2/2
✓ Branch 14 → 15 taken 3 times.
✓ Branch 14 → 19 taken 1 time.
4 if (!found_address_start)
620 {
621
1/2
✓ Branch 15 → 16 taken 3 times.
✗ Branch 15 → 36 not taken.
3 m_logger.error("HookManager: AOB pattern not found for mid hook '{}'. Pattern: \"{}\".", name, aob_pattern_str);
622 3 return std::unexpected(HookError::InvalidTargetAddress);
623 }
624
625 1 uintptr_t target_address = reinterpret_cast<uintptr_t>(found_address_start) + aob_offset;
626
1/2
✓ Branch 22 → 23 taken 1 time.
✗ Branch 22 → 37 not taken.
1 m_logger.debug("HookManager: AOB pattern for mid hook '{}' found at {}. Applying offset {}. Final target hook address: {}.",
627
1/2
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 40 not taken.
2 name, DetourModKit::Format::format_address(reinterpret_cast<uintptr_t>(found_address_start)),
628
2/4
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 46 not taken.
✓ Branch 20 → 21 taken 1 time.
✗ Branch 20 → 43 not taken.
2 DetourModKit::Format::format_hex(aob_offset), DetourModKit::Format::format_address(target_address));
629
630
1/2
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 47 not taken.
1 return create_mid_hook(name, target_address, detour_function, config);
631 6 }
632
633 20 bool HookManager::is_target_already_hooked(uintptr_t target_address) const noexcept
634 {
635
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 19 times.
20 if (target_address == 0)
636 {
637 1 return false;
638 }
639 19 std::shared_lock<std::shared_mutex> lock(m_hooks_mutex);
640
2/2
✓ Branch 22 → 7 taken 11 times.
✓ Branch 22 → 23 taken 12 times.
23 for (const auto &[name, hook_ptr] : m_hooks)
641 {
642
5/6
✓ Branch 12 → 13 taken 11 times.
✗ Branch 12 → 17 not taken.
✓ Branch 15 → 16 taken 7 times.
✓ Branch 15 → 17 taken 4 times.
✓ Branch 18 → 19 taken 7 times.
✓ Branch 18 → 20 taken 4 times.
22 if (hook_ptr->get_type() == HookType::Inline &&
643 11 hook_ptr->get_target_address() == target_address)
644 {
645 7 return true;
646 }
647 }
648 12 return false;
649 19 }
650
651 29 std::expected<void, HookError> HookManager::remove_hook(std::string_view hook_id)
652 {
653
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 9 taken 29 times.
29 if (m_shutdown_called.load(std::memory_order_acquire))
654 {
655 m_logger.warning("HookManager: Shutdown in progress. Cannot remove hook '{}'.", hook_id);
656 return std::unexpected(HookError::ShutdownInProgress);
657 }
658
659
1/2
✓ Branch 9 → 10 taken 29 times.
✗ Branch 9 → 68 not taken.
29 std::shared_lock<std::shared_mutex> mutator_gate(m_mutator_gate);
660
661 // Two-phase removal: disable under shared lock first so that in-flight
662 // trampoline callers (which may acquire shared_lock via with_inline_hook)
663 // can drain before we take the exclusive lock to erase. Without this,
664 // SafetyHook's destructor waiting for trampoline threads while holding
665 // the exclusive lock would deadlock against those threads.
666 {
667
1/2
✓ Branch 10 → 11 taken 29 times.
✗ Branch 10 → 58 not taken.
29 std::shared_lock<std::shared_mutex> shared(m_hooks_mutex);
668
1/2
✓ Branch 11 → 12 taken 29 times.
✗ Branch 11 → 56 not taken.
29 auto it = m_hooks.find(hook_id);
669
2/2
✓ Branch 14 → 15 taken 6 times.
✓ Branch 14 → 20 taken 23 times.
29 if (it == m_hooks.end())
670 {
671
1/2
✓ Branch 15 → 16 taken 6 times.
✗ Branch 15 → 54 not taken.
6 m_logger.warning("HookManager: Attempted to remove hook with ID '{}', but it was not found.", hook_id);
672 6 return std::unexpected(HookError::HookNotFound);
673 }
674
1/2
✓ Branch 22 → 23 taken 23 times.
✗ Branch 22 → 55 not taken.
23 (void)it->second->disable();
675
2/2
✓ Branch 25 → 26 taken 23 times.
✓ Branch 25 → 28 taken 6 times.
29 }
676
677
1/2
✓ Branch 27 → 29 taken 23 times.
✗ Branch 27 → 66 not taken.
23 std::unique_lock<std::shared_mutex> lock(m_hooks_mutex);
678
1/2
✓ Branch 29 → 30 taken 23 times.
✗ Branch 29 → 64 not taken.
23 auto it = m_hooks.find(hook_id);
679
1/2
✓ Branch 32 → 33 taken 23 times.
✗ Branch 32 → 47 not taken.
23 if (it != m_hooks.end())
680 {
681
1/2
✓ Branch 36 → 37 taken 23 times.
✗ Branch 36 → 63 not taken.
23 std::string name_of_removed_hook = it->second->get_name();
682 23 HookType type_of_removed_hook = it->second->get_type();
683
1/2
✓ Branch 40 → 41 taken 23 times.
✗ Branch 40 → 61 not taken.
23 m_hooks.erase(it);
684 m_logger.debug("HookManager: Hook '{}' of type '{}' has been removed and unhooked.",
685
3/4
✓ Branch 41 → 42 taken 17 times.
✓ Branch 41 → 43 taken 6 times.
✓ Branch 44 → 45 taken 23 times.
✗ Branch 44 → 59 not taken.
23 name_of_removed_hook, (type_of_removed_hook == HookType::Inline ? "Inline" : "Mid"));
686 23 }
687 23 return {};
688 29 }
689
690 334 void HookManager::remove_all_hooks()
691 {
692 // Serialize with shutdown() via compare_exchange_strong.
693 // Only one teardown owner proceeds.
694 334 bool expected = false;
695
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 334 times.
334 if (!m_shutdown_called.compare_exchange_strong(expected, true, std::memory_order_acq_rel))
696 return;
697
698 // Block all mutators (create_*_hook, enable, disable, remove) before
699 // entering phase 1. They hold shared on m_mutator_gate, so acquiring
700 // exclusive here waits for active mutators and blocks new ones.
701
1/2
✓ Branch 5 → 6 taken 334 times.
✗ Branch 5 → 52 not taken.
334 std::unique_lock<std::shared_mutex> mutator_gate(m_mutator_gate);
702
703 // Two-phase removal: disable hooks under shared lock first so that
704 // in-flight trampoline callers (which may hold shared_lock via
705 // with_inline_hook) can drain before we take the exclusive lock
706 // to erase. Without this, SafetyHook's destructor waiting for
707 // trampoline threads while holding the exclusive lock would
708 // deadlock against those threads.
709 {
710
1/2
✓ Branch 6 → 7 taken 334 times.
✗ Branch 6 → 42 not taken.
334 std::shared_lock<std::shared_mutex> shared(m_hooks_mutex);
711
2/2
✓ Branch 16 → 9 taken 63 times.
✓ Branch 16 → 17 taken 334 times.
397 for (auto &[name, hook] : m_hooks)
712 {
713
1/2
✓ Branch 13 → 14 taken 63 times.
✗ Branch 13 → 38 not taken.
63 (void)hook->disable();
714 }
715 334 }
716
717
1/2
✓ Branch 18 → 19 taken 334 times.
✗ Branch 18 → 50 not taken.
334 std::unique_lock<std::shared_mutex> lock(m_hooks_mutex);
718
719 334 size_t num_vmt = m_vmt_hooks.size();
720
2/2
✓ Branch 20 → 21 taken 2 times.
✓ Branch 20 → 23 taken 332 times.
334 if (num_vmt > 0)
721 {
722
1/2
✓ Branch 21 → 22 taken 2 times.
✗ Branch 21 → 43 not taken.
2 m_logger.debug("HookManager: Removing all {} VMT hooks...", num_vmt);
723 2 m_vmt_hooks.clear();
724 }
725
726
2/2
✓ Branch 24 → 25 taken 55 times.
✓ Branch 24 → 30 taken 279 times.
334 if (!m_hooks.empty())
727 {
728 55 size_t num_hooks = m_hooks.size();
729
1/2
✓ Branch 26 → 27 taken 55 times.
✗ Branch 26 → 44 not taken.
55 m_logger.debug("HookManager: Removing all {} managed hooks...", num_hooks);
730 55 m_hooks.clear();
731
1/2
✓ Branch 28 → 29 taken 55 times.
✗ Branch 28 → 45 not taken.
55 m_logger.debug("HookManager: All {} managed hooks have been removed and unhooked.", num_hooks);
732 }
733
2/2
✓ Branch 30 → 31 taken 277 times.
✓ Branch 30 → 33 taken 2 times.
279 else if (num_vmt == 0)
734 {
735
1/2
✓ Branch 31 → 32 taken 277 times.
✗ Branch 31 → 47 not taken.
277 m_logger.debug("HookManager: remove_all_hooks called, but no hooks were active to remove.");
736 }
737
738 // Reset under the lock so concurrent create_*_hook calls cannot
739 // observe the flag as true (rejected) and then immediately see it
740 // as false (accepted) before the map is fully cleared.
741 // The mutator_gate exclusive lock is released here, allowing new
742 // mutators to proceed with a fresh m_shutdown_called=false.
743 334 m_shutdown_called.store(false, std::memory_order_release);
744 334 }
745
746 450 std::expected<void, HookError> HookManager::enable_hook(std::string_view hook_id)
747 {
748
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 9 taken 450 times.
450 if (m_shutdown_called.load(std::memory_order_acquire))
749 {
750 m_logger.warning("HookManager: Shutdown in progress. Cannot enable hook '{}'.", hook_id);
751 return std::unexpected(HookError::ShutdownInProgress);
752 }
753
754
1/2
✓ Branch 9 → 10 taken 450 times.
✗ Branch 9 → 65 not taken.
450 std::shared_lock<std::shared_mutex> mutator_gate(m_mutator_gate);
755
1/2
✓ Branch 10 → 11 taken 450 times.
✗ Branch 10 → 63 not taken.
450 std::shared_lock<std::shared_mutex> lock(m_hooks_mutex);
756
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 18 taken 450 times.
450 if (m_shutdown_called.load(std::memory_order_acquire))
757 {
758 m_logger.warning("HookManager: Shutdown in progress. Cannot enable hook '{}'.", hook_id);
759 return std::unexpected(HookError::ShutdownInProgress);
760 }
761
1/2
✓ Branch 18 → 19 taken 450 times.
✗ Branch 18 → 61 not taken.
450 auto it = m_hooks.find(hook_id);
762
2/2
✓ Branch 21 → 22 taken 43 times.
✓ Branch 21 → 27 taken 407 times.
450 if (it == m_hooks.end())
763 {
764
1/2
✓ Branch 22 → 23 taken 43 times.
✗ Branch 22 → 55 not taken.
43 m_logger.warning("HookManager: Hook ID '{}' not found for enable operation.", hook_id);
765 43 return std::unexpected(HookError::HookNotFound);
766 }
767
768 407 Hook *hook = it->second.get();
769
1/2
✓ Branch 29 → 30 taken 407 times.
✗ Branch 29 → 61 not taken.
407 auto result = hook->enable();
770
2/2
✓ Branch 31 → 32 taken 114 times.
✓ Branch 31 → 36 taken 293 times.
407 if (result)
771 {
772
1/2
✓ Branch 32 → 33 taken 114 times.
✗ Branch 32 → 56 not taken.
114 m_logger.debug("HookManager: Hook '{}' successfully enabled.", hook_id);
773 114 return {};
774 }
775
776 293 const auto error = result.error();
777
1/2
✓ Branch 37 → 38 taken 293 times.
✗ Branch 37 → 42 not taken.
293 if (error == HookError::InvalidHookState)
778 {
779
1/2
✓ Branch 40 → 41 taken 293 times.
✗ Branch 40 → 57 not taken.
293 m_logger.warning("HookManager: Hook '{}' cannot be enabled. Current status: {}", hook_id, Hook::status_to_string(hook->get_status()));
780 }
781 else
782 {
783 m_logger.error("HookManager: Failed to enable hook '{}': {}", hook_id, Hook::error_to_string(error));
784 }
785 293 return std::unexpected(error);
786 450 }
787
788 452 std::expected<void, HookError> HookManager::disable_hook(std::string_view hook_id)
789 {
790
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 9 taken 452 times.
452 if (m_shutdown_called.load(std::memory_order_acquire))
791 {
792 m_logger.warning("HookManager: Shutdown in progress. Cannot disable hook '{}'.", hook_id);
793 return std::unexpected(HookError::ShutdownInProgress);
794 }
795
796
1/2
✓ Branch 9 → 10 taken 452 times.
✗ Branch 9 → 65 not taken.
452 std::shared_lock<std::shared_mutex> mutator_gate(m_mutator_gate);
797
1/2
✓ Branch 10 → 11 taken 452 times.
✗ Branch 10 → 63 not taken.
452 std::shared_lock<std::shared_mutex> lock(m_hooks_mutex);
798
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 18 taken 452 times.
452 if (m_shutdown_called.load(std::memory_order_acquire))
799 {
800 m_logger.warning("HookManager: Shutdown in progress. Cannot disable hook '{}'.", hook_id);
801 return std::unexpected(HookError::ShutdownInProgress);
802 }
803
1/2
✓ Branch 18 → 19 taken 452 times.
✗ Branch 18 → 61 not taken.
452 auto it = m_hooks.find(hook_id);
804
2/2
✓ Branch 21 → 22 taken 43 times.
✓ Branch 21 → 27 taken 409 times.
452 if (it == m_hooks.end())
805 {
806
1/2
✓ Branch 22 → 23 taken 43 times.
✗ Branch 22 → 55 not taken.
43 m_logger.warning("HookManager: Hook ID '{}' not found for disable operation.", hook_id);
807 43 return std::unexpected(HookError::HookNotFound);
808 }
809
810 409 Hook *hook = it->second.get();
811
1/2
✓ Branch 29 → 30 taken 409 times.
✗ Branch 29 → 61 not taken.
409 auto result = hook->disable();
812
2/2
✓ Branch 31 → 32 taken 134 times.
✓ Branch 31 → 36 taken 274 times.
409 if (result)
813 {
814
1/2
✓ Branch 32 → 33 taken 135 times.
✗ Branch 32 → 56 not taken.
134 m_logger.debug("HookManager: Hook '{}' successfully disabled.", hook_id);
815 135 return {};
816 }
817
818 274 const auto error = result.error();
819
1/2
✓ Branch 37 → 38 taken 274 times.
✗ Branch 37 → 42 not taken.
274 if (error == HookError::InvalidHookState)
820 {
821
1/2
✓ Branch 40 → 41 taken 274 times.
✗ Branch 40 → 57 not taken.
274 m_logger.warning("HookManager: Hook '{}' cannot be disabled. Current status: {}", hook_id, Hook::status_to_string(hook->get_status()));
822 }
823 else
824 {
825 m_logger.error("HookManager: Failed to disable hook '{}': {}", hook_id, Hook::error_to_string(error));
826 }
827 274 return std::unexpected(error);
828 452 }
829
830 41720 std::optional<HookStatus> HookManager::get_hook_status(std::string_view hook_id) const
831 {
832
1/2
✓ Branch 2 → 3 taken 41753 times.
✗ Branch 2 → 21 not taken.
41720 std::shared_lock<std::shared_mutex> lock(m_hooks_mutex);
833
1/2
✓ Branch 3 → 4 taken 41696 times.
✗ Branch 3 → 19 not taken.
41753 auto it = m_hooks.find(hook_id);
834
2/2
✓ Branch 6 → 7 taken 41637 times.
✓ Branch 6 → 13 taken 45 times.
41696 if (it != m_hooks.end())
835 {
836 41637 return it->second->get_status();
837 }
838 45 return std::nullopt;
839 41692 }
840
841 45 std::unordered_map<HookStatus, size_t> HookManager::get_hook_counts() const
842 {
843
1/2
✓ Branch 2 → 3 taken 45 times.
✗ Branch 2 → 28 not taken.
45 std::shared_lock<std::shared_mutex> lock(m_hooks_mutex);
844 45 std::unordered_map<HookStatus, size_t> counts;
845
1/2
✓ Branch 4 → 5 taken 45 times.
✗ Branch 4 → 20 not taken.
45 counts[HookStatus::Active] = 0;
846
1/2
✓ Branch 5 → 6 taken 45 times.
✗ Branch 5 → 21 not taken.
45 counts[HookStatus::Disabled] = 0;
847
2/2
✓ Branch 16 → 8 taken 4 times.
✓ Branch 16 → 17 taken 45 times.
49 for (const auto &[name, hook_ptr] : m_hooks)
848 {
849
1/2
✓ Branch 13 → 14 taken 4 times.
✗ Branch 13 → 22 not taken.
4 counts[hook_ptr->get_status()]++;
850 }
851 45 return counts;
852 45 }
853
854 58 std::vector<std::string> HookManager::get_hook_ids(std::optional<HookStatus> status_filter) const
855 {
856
1/2
✓ Branch 2 → 3 taken 58 times.
✗ Branch 2 → 31 not taken.
58 std::shared_lock<std::shared_mutex> lock(m_hooks_mutex);
857 58 std::vector<std::string> ids;
858
1/2
✓ Branch 4 → 5 taken 58 times.
✗ Branch 4 → 27 not taken.
58 ids.reserve(m_hooks.size());
859
2/2
✓ Branch 22 → 7 taken 9 times.
✓ Branch 22 → 23 taken 58 times.
67 for (const auto &[name, hook_ptr] : m_hooks)
860 {
861
5/8
✓ Branch 11 → 12 taken 6 times.
✓ Branch 11 → 16 taken 3 times.
✓ Branch 14 → 15 taken 6 times.
✗ Branch 14 → 26 not taken.
✓ Branch 15 → 16 taken 6 times.
✗ Branch 15 → 17 not taken.
✓ Branch 18 → 19 taken 9 times.
✗ Branch 18 → 20 not taken.
9 if (!status_filter.has_value() || hook_ptr->get_status() == status_filter.value())
862 {
863
1/2
✓ Branch 19 → 20 taken 9 times.
✗ Branch 19 → 26 not taken.
9 ids.push_back(name);
864 }
865 }
866 58 return ids;
867 58 }
868
869 30 std::expected<std::string, HookError> HookManager::create_vmt_hook(
870 std::string_view name, void *object)
871 {
872
2/2
✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 8 taken 24 times.
30 if (m_shutdown_called.load(std::memory_order_acquire))
873 {
874
1/2
✓ Branch 4 → 5 taken 6 times.
✗ Branch 4 → 36 not taken.
5 m_logger.error("HookManager: Shutdown in progress. Cannot create VMT hook '{}'.", name);
875 6 return std::unexpected(HookError::ShutdownInProgress);
876 }
877
878 24 auto [result, deferred_logs] = [&]() -> std::pair<std::expected<std::string, HookError>, std::vector<DeferredLogEntry>>
879 {
880
1/2
✓ Branch 2 → 3 taken 24 times.
✗ Branch 2 → 287 not taken.
24 std::shared_lock<std::shared_mutex> mutator_gate(m_mutator_gate);
881
1/2
✓ Branch 3 → 4 taken 24 times.
✗ Branch 3 → 285 not taken.
24 std::unique_lock<std::shared_mutex> lock(m_hooks_mutex);
882
883
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 20 taken 23 times.
24 if (m_shutdown_called.load(std::memory_order_acquire))
884 {
885 1 return {std::unexpected(HookError::ShutdownInProgress),
886
3/6
✓ Branch 10 → 11 taken 1 time.
✗ Branch 10 → 100 not taken.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 17 taken 1 time.
✗ Branch 104 → 105 not taken.
✗ Branch 104 → 106 not taken.
3 {{std::format("HookManager: Shutdown in progress. Cannot create VMT hook '{}'.", name), LogLevel::Error}}};
887 }
888
2/2
✓ Branch 20 → 21 taken 1 time.
✓ Branch 20 → 35 taken 22 times.
23 if (object == nullptr)
889 {
890 1 return {std::unexpected(HookError::InvalidObject),
891
3/6
✓ Branch 25 → 26 taken 1 time.
✗ Branch 25 → 120 not taken.
✓ Branch 30 → 31 taken 1 time.
✓ Branch 30 → 32 taken 1 time.
✗ Branch 124 → 125 not taken.
✗ Branch 124 → 126 not taken.
3 {{std::format("HookManager: Object pointer is NULL for VMT hook '{}'.", name), LogLevel::Error}}};
892 }
893
3/4
✓ Branch 35 → 36 taken 22 times.
✗ Branch 35 → 283 not taken.
✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 51 taken 21 times.
22 if (vmt_hook_exists_locked(name))
894 {
895 1 return {std::unexpected(HookError::HookAlreadyExists),
896
3/6
✓ Branch 41 → 42 taken 1 time.
✗ Branch 41 → 140 not taken.
✓ Branch 46 → 47 taken 1 time.
✓ Branch 46 → 48 taken 1 time.
✗ Branch 144 → 145 not taken.
✗ Branch 144 → 146 not taken.
3 {{std::format("HookManager: A VMT hook with the name '{}' already exists.", name), LogLevel::Error}}};
897 }
898
899 try
900 {
901
1/2
✓ Branch 51 → 52 taken 21 times.
✗ Branch 51 → 205 not taken.
21 auto vmt_result = safetyhook::VmtHook::create(object);
902
903
1/2
✗ Branch 53 → 54 not taken.
✓ Branch 53 → 70 taken 21 times.
21 if (!vmt_result)
904 {
905 return {std::unexpected(HookError::SafetyHookError),
906 {{std::format("HookManager: Failed to create SafetyHook::VmtHook for '{}' on object {}.",
907 name, DetourModKit::Format::format_address(reinterpret_cast<uintptr_t>(object))),
908 LogLevel::Error}}};
909 }
910
911
1/2
✓ Branch 72 → 73 taken 21 times.
✗ Branch 72 → 183 not taken.
42 std::string name_str{name};
912
913 21 std::vector<DeferredLogEntry> logs;
914 21 logs.push_back({std::format("HookManager: Successfully created VMT hook '{}' on object {}.",
915 name, DetourModKit::Format::format_address(reinterpret_cast<uintptr_t>(object))),
916 LogLevel::Info});
917
918
1/2
✓ Branch 86 → 87 taken 21 times.
✗ Branch 86 → 196 not taken.
21 m_vmt_hooks.emplace(
919 std::piecewise_construct,
920 21 std::forward_as_tuple(name_str),
921
1/2
✓ Branch 81 → 82 taken 21 times.
✗ Branch 81 → 197 not taken.
63 std::forward_as_tuple(name_str, std::move(vmt_result.value())));
922
923 42 return {std::move(name_str), std::move(logs)};
924 21 }
925 catch (const std::exception &e)
926 {
927 return {std::unexpected(HookError::UnknownError),
928 {{std::format("HookManager: Exception during VMT hook creation for '{}': {}", name, e.what()),
929 LogLevel::Error}}};
930 }
931 catch (...)
932 {
933 return {std::unexpected(HookError::UnknownError),
934 {{std::format("HookManager: Unknown exception during VMT hook creation for '{}'.", name),
935 LogLevel::Error}}};
936 }
937
11/76
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 110 not taken.
✓ Branch 8 → 9 taken 24 times.
✗ Branch 8 → 37 not taken.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
✗ Branch 22 → 130 not taken.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 1 time.
✓ Branch 38 → 39 taken 1 time.
✗ Branch 38 → 150 not taken.
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 50 taken 1 time.
✗ Branch 55 → 56 not taken.
✗ Branch 55 → 173 not taken.
✗ Branch 56 → 57 not taken.
✗ Branch 56 → 170 not taken.
✗ Branch 66 → 67 not taken.
✗ Branch 66 → 68 not taken.
✓ Branch 74 → 75 taken 21 times.
✗ Branch 74 → 194 not taken.
✓ Branch 75 → 76 taken 21 times.
✗ Branch 75 → 191 not taken.
✓ Branch 76 → 77 taken 21 times.
✗ Branch 76 → 186 not taken.
✗ Branch 78 → 79 not taken.
✓ Branch 78 → 80 taken 21 times.
✗ Branch 107 → 108 not taken.
✗ Branch 107 → 109 not taken.
✗ Branch 111 → 112 not taken.
✗ Branch 111 → 115 not taken.
✗ Branch 113 → 114 not taken.
✗ Branch 113 → 115 not taken.
✗ Branch 127 → 128 not taken.
✗ Branch 127 → 129 not taken.
✗ Branch 131 → 132 not taken.
✗ Branch 131 → 135 not taken.
✗ Branch 133 → 134 not taken.
✗ Branch 133 → 135 not taken.
✗ Branch 147 → 148 not taken.
✗ Branch 147 → 149 not taken.
✗ Branch 151 → 152 not taken.
✗ Branch 151 → 155 not taken.
✗ Branch 153 → 154 not taken.
✗ Branch 153 → 155 not taken.
✗ Branch 167 → 168 not taken.
✗ Branch 167 → 169 not taken.
✗ Branch 174 → 175 not taken.
✗ Branch 174 → 178 not taken.
✗ Branch 176 → 177 not taken.
✗ Branch 176 → 178 not taken.
✗ Branch 188 → 189 not taken.
✗ Branch 188 → 190 not taken.
✗ Branch 210 → 211 not taken.
✗ Branch 210 → 248 not taken.
✗ Branch 220 → 221 not taken.
✗ Branch 220 → 222 not taken.
✗ Branch 225 → 226 not taken.
✗ Branch 225 → 271 not taken.
✗ Branch 235 → 236 not taken.
✗ Branch 235 → 237 not taken.
✗ Branch 237 → 96 not taken.
✗ Branch 237 → 283 not taken.
✗ Branch 245 → 246 not taken.
✗ Branch 245 → 247 not taken.
✗ Branch 250 → 251 not taken.
✗ Branch 250 → 254 not taken.
✗ Branch 252 → 253 not taken.
✗ Branch 252 → 254 not taken.
✗ Branch 268 → 269 not taken.
✗ Branch 268 → 270 not taken.
✗ Branch 272 → 273 not taken.
✗ Branch 272 → 276 not taken.
✗ Branch 274 → 275 not taken.
✗ Branch 274 → 276 not taken.
93 }();
938
939
2/2
✓ Branch 26 → 13 taken 24 times.
✓ Branch 26 → 27 taken 24 times.
72 for (const auto &entry : deferred_logs)
940 {
941
1/2
✓ Branch 16 → 17 taken 24 times.
✗ Branch 16 → 38 not taken.
24 m_logger.log(entry.level, entry.msg);
942 }
943 24 return result;
944
1/4
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 24 times.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
48 }
945
946 2 std::expected<void, HookError> HookManager::remove_vmt_hook(std::string_view vmt_name)
947 {
948
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 9 taken 2 times.
2 if (m_shutdown_called.load(std::memory_order_acquire))
949 {
950 m_logger.warning("HookManager: Shutdown in progress. Cannot remove VMT hook '{}'.", vmt_name);
951 return std::unexpected(HookError::ShutdownInProgress);
952 }
953
954
1/2
✓ Branch 9 → 10 taken 2 times.
✗ Branch 9 → 43 not taken.
2 std::shared_lock<std::shared_mutex> mutator_gate(m_mutator_gate);
955
1/2
✓ Branch 10 → 11 taken 2 times.
✗ Branch 10 → 41 not taken.
2 std::unique_lock<std::shared_mutex> lock(m_hooks_mutex);
956
1/2
✓ Branch 11 → 12 taken 2 times.
✗ Branch 11 → 39 not taken.
2 auto it = m_vmt_hooks.find(vmt_name);
957
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 24 taken 1 time.
2 if (it != m_vmt_hooks.end())
958 {
959
1/2
✓ Branch 17 → 18 taken 1 time.
✗ Branch 17 → 37 not taken.
1 std::string removed_name = it->second.get_name();
960
1/2
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 35 not taken.
1 m_vmt_hooks.erase(it);
961
1/2
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 34 not taken.
1 m_logger.debug("HookManager: VMT hook '{}' has been removed.", removed_name);
962 1 return {};
963 1 }
964
1/2
✓ Branch 24 → 25 taken 1 time.
✗ Branch 24 → 38 not taken.
1 m_logger.warning("HookManager: Attempted to remove VMT hook '{}', but it was not found.", vmt_name);
965 1 return std::unexpected(HookError::VmtHookNotFound);
966 2 }
967
968 2 std::expected<void, HookError> HookManager::remove_vmt_method(std::string_view vmt_name, size_t method_index)
969 {
970
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 9 taken 2 times.
2 if (m_shutdown_called.load(std::memory_order_acquire))
971 {
972 m_logger.warning("HookManager: Shutdown in progress. Cannot remove VMT method on '{}'.", vmt_name);
973 return std::unexpected(HookError::ShutdownInProgress);
974 }
975
976
1/2
✓ Branch 9 → 10 taken 2 times.
✗ Branch 9 → 44 not taken.
2 std::shared_lock<std::shared_mutex> mutator_gate(m_mutator_gate);
977
1/2
✓ Branch 10 → 11 taken 2 times.
✗ Branch 10 → 42 not taken.
2 std::unique_lock<std::shared_mutex> lock(m_hooks_mutex);
978
1/2
✓ Branch 11 → 12 taken 2 times.
✗ Branch 11 → 40 not taken.
2 auto it = m_vmt_hooks.find(vmt_name);
979
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 20 taken 2 times.
2 if (it == m_vmt_hooks.end())
980 {
981 m_logger.warning("HookManager: VMT hook '{}' not found for method removal.", vmt_name);
982 return std::unexpected(HookError::VmtHookNotFound);
983 }
984
985
3/4
✓ Branch 21 → 22 taken 2 times.
✗ Branch 21 → 40 not taken.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 27 taken 1 time.
2 if (it->second.remove_method_hook(method_index))
986 {
987
1/2
✓ Branch 23 → 24 taken 1 time.
✗ Branch 23 → 38 not taken.
1 m_logger.debug("HookManager: VMT '{}' method index {} has been unhooked.", vmt_name, method_index);
988 1 return {};
989 }
990
991
1/2
✓ Branch 27 → 28 taken 1 time.
✗ Branch 27 → 39 not taken.
1 m_logger.warning("HookManager: VMT '{}' has no hooked method at index {}.", vmt_name, method_index);
992 1 return std::unexpected(HookError::MethodNotFound);
993 2 }
994
995 3 bool HookManager::apply_vmt_hook(std::string_view vmt_name, void *object)
996 {
997
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 3 times.
3 if (m_shutdown_called.load(std::memory_order_acquire))
998 {
999 m_logger.warning("HookManager: Shutdown in progress. Cannot apply VMT hook '{}'.", vmt_name);
1000 return false;
1001 }
1002
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 9 taken 2 times.
3 if (object == nullptr)
1003 {
1004
1/2
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 29 not taken.
1 m_logger.warning("HookManager: Cannot apply VMT hook '{}' to null object.", vmt_name);
1005 1 return false;
1006 }
1007
1008
1/2
✓ Branch 9 → 10 taken 2 times.
✗ Branch 9 → 39 not taken.
2 std::shared_lock<std::shared_mutex> mutator_gate(m_mutator_gate);
1009
1/2
✓ Branch 10 → 11 taken 2 times.
✗ Branch 10 → 37 not taken.
2 std::unique_lock<std::shared_mutex> lock(m_hooks_mutex);
1010
1/2
✓ Branch 11 → 12 taken 2 times.
✗ Branch 11 → 35 not taken.
2 auto it = m_vmt_hooks.find(vmt_name);
1011
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 17 taken 1 time.
2 if (it == m_vmt_hooks.end())
1012 {
1013
1/2
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 30 not taken.
1 m_logger.warning("HookManager: VMT hook '{}' not found for apply.", vmt_name);
1014 1 return false;
1015 }
1016
1017
1/2
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 35 not taken.
1 it->second.vmt_hook().apply(object);
1018
1/2
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 31 not taken.
1 m_logger.debug("HookManager: VMT hook '{}' applied to object {}.",
1019
1/2
✓ Branch 20 → 21 taken 1 time.
✗ Branch 20 → 34 not taken.
2 vmt_name, DetourModKit::Format::format_address(reinterpret_cast<uintptr_t>(object)));
1020 1 return true;
1021 2 }
1022
1023 3 bool HookManager::remove_vmt_from_object(std::string_view vmt_name, void *object)
1024 {
1025
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 3 times.
3 if (m_shutdown_called.load(std::memory_order_acquire))
1026 {
1027 m_logger.warning("HookManager: Shutdown in progress. Cannot remove VMT hook '{}' from object.", vmt_name);
1028 return false;
1029 }
1030
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 9 taken 2 times.
3 if (object == nullptr)
1031 {
1032
1/2
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 29 not taken.
1 m_logger.warning("HookManager: Cannot remove VMT hook '{}' from null object.", vmt_name);
1033 1 return false;
1034 }
1035
1036
1/2
✓ Branch 9 → 10 taken 2 times.
✗ Branch 9 → 39 not taken.
2 std::shared_lock<std::shared_mutex> mutator_gate(m_mutator_gate);
1037
1/2
✓ Branch 10 → 11 taken 2 times.
✗ Branch 10 → 37 not taken.
2 std::unique_lock<std::shared_mutex> lock(m_hooks_mutex);
1038
1/2
✓ Branch 11 → 12 taken 2 times.
✗ Branch 11 → 35 not taken.
2 auto it = m_vmt_hooks.find(vmt_name);
1039
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 17 taken 1 time.
2 if (it == m_vmt_hooks.end())
1040 {
1041
1/2
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 30 not taken.
1 m_logger.warning("HookManager: VMT hook '{}' not found for object removal.", vmt_name);
1042 1 return false;
1043 }
1044
1045
1/2
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 35 not taken.
1 it->second.vmt_hook().remove(object);
1046
1/2
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 31 not taken.
1 m_logger.debug("HookManager: VMT hook '{}' removed from object {}.",
1047
1/2
✓ Branch 20 → 21 taken 1 time.
✗ Branch 20 → 34 not taken.
2 vmt_name, DetourModKit::Format::format_address(reinterpret_cast<uintptr_t>(object)));
1048 1 return true;
1049 2 }
1050
1051 16 void HookManager::remove_all_vmt_hooks()
1052 {
1053
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 16 times.
16 if (m_shutdown_called.load(std::memory_order_acquire))
1054 return;
1055
1056
1/2
✓ Branch 5 → 6 taken 16 times.
✗ Branch 5 → 28 not taken.
16 std::shared_lock<std::shared_mutex> mutator_gate(m_mutator_gate);
1057
1/2
✓ Branch 6 → 7 taken 16 times.
✗ Branch 6 → 26 not taken.
16 std::unique_lock<std::shared_mutex> lock(m_hooks_mutex);
1058
2/2
✓ Branch 8 → 9 taken 14 times.
✓ Branch 8 → 14 taken 2 times.
16 if (!m_vmt_hooks.empty())
1059 {
1060 14 size_t num_hooks = m_vmt_hooks.size();
1061
1/2
✓ Branch 10 → 11 taken 14 times.
✗ Branch 10 → 20 not taken.
14 m_logger.debug("HookManager: Removing all {} VMT hooks...", num_hooks);
1062 14 m_vmt_hooks.clear();
1063
1/2
✓ Branch 12 → 13 taken 14 times.
✗ Branch 12 → 21 not taken.
14 m_logger.debug("HookManager: All {} VMT hooks have been removed.", num_hooks);
1064 }
1065 else
1066 {
1067
1/2
✓ Branch 14 → 15 taken 2 times.
✗ Branch 14 → 23 not taken.
2 m_logger.debug("HookManager: remove_all_vmt_hooks called, but no VMT hooks were active.");
1068 }
1069 16 }
1070
1071 10 std::vector<std::string> HookManager::get_vmt_hook_names() const
1072 {
1073
1/2
✓ Branch 2 → 3 taken 10 times.
✗ Branch 2 → 22 not taken.
10 std::shared_lock<std::shared_mutex> lock(m_hooks_mutex);
1074 10 std::vector<std::string> names;
1075
1/2
✓ Branch 4 → 5 taken 10 times.
✗ Branch 4 → 18 not taken.
10 names.reserve(m_vmt_hooks.size());
1076
2/2
✓ Branch 13 → 7 taken 5 times.
✓ Branch 13 → 14 taken 10 times.
15 for (const auto &[name, entry] : m_vmt_hooks)
1077 {
1078
1/2
✓ Branch 10 → 11 taken 5 times.
✗ Branch 10 → 17 not taken.
5 names.push_back(name);
1079 }
1080 10 return names;
1081 10 }
1082