GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 79.7% 459 / 0 / 576
Functions: 97.7% 42 / 0 / 43
Branches: 54.3% 266 / 0 / 490

src/memory.cpp
Line Branch Exec Source
1 /**
2 * @file memory.cpp
3 * @brief Implementation of memory manipulation and validation utilities.
4 *
5 * Provides functions for checking memory readability and writability, writing bytes to memory,
6 * and managing a memory region cache for performance optimization.
7 * The cache uses sharded locks with SRWLOCK for high-concurrency read-heavy access.
8 * Uses monotonic counter-keyed map for O(log n) LRU eviction instead of O(n) scan.
9 * In-flight query coalescing prevents cache stampede under high concurrency.
10 * On-demand cleanup handles expired entry removal to avoid polluting the miss path.
11 * Epoch-based reader tracking prevents use-after-free during shutdown.
12 */
13
14 #include "DetourModKit/memory.hpp"
15 #include "DetourModKit/format.hpp"
16 #include "DetourModKit/logger.hpp"
17 #include "platform.hpp"
18
19 #include <windows.h>
20 #include <shared_mutex>
21 #include <unordered_map>
22 #include <map>
23 #include <vector>
24 #include <chrono>
25 #include <atomic>
26 #include <cstdlib>
27 #include <sstream>
28 #include <iomanip>
29 #include <algorithm>
30 #include <stdexcept>
31 #include <cstddef>
32 #include <thread>
33 #include <condition_variable>
34
35 using namespace DetourModKit;
36
37 // Permission flags as constexpr for compile-time constants
38 namespace CachePermissions
39 {
40 constexpr DWORD READ_PERMISSION_FLAGS = PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY |
41 PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
42 constexpr DWORD WRITE_PERMISSION_FLAGS = PAGE_READWRITE | PAGE_WRITECOPY |
43 PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
44 constexpr DWORD NOACCESS_GUARD_FLAGS = PAGE_NOACCESS | PAGE_GUARD;
45 }
46
47 using DetourModKit::detail::is_loader_lock_held;
48 using DetourModKit::detail::pin_current_module;
49
50 // Anonymous namespace for internal helpers and storage
51 namespace
52 {
53 /**
54 * @class SrwSharedMutex
55 * @brief Shared mutex backed by Windows SRWLOCK instead of pthread_rwlock_t.
56 * @details MinGW/winpthreads' pthread_rwlock_t corrupts internal state under
57 * high reader contention, causing assertion failures in lock_shared().
58 * SRWLOCK is kernel-level, lock-free for uncontended cases, and does
59 * not suffer from this bug.
60 */
61 class SrwSharedMutex
62 {
63 public:
64 2744 SrwSharedMutex() noexcept { InitializeSRWLock(&srw_); }
65
66 SrwSharedMutex(const SrwSharedMutex &) = delete;
67 SrwSharedMutex &operator=(const SrwSharedMutex &) = delete;
68
69 3279 void lock() noexcept { AcquireSRWLockExclusive(&srw_); }
70 51 bool try_lock() noexcept { return TryAcquireSRWLockExclusive(&srw_) != 0; }
71 3330 void unlock() noexcept { ReleaseSRWLockExclusive(&srw_); }
72
73 93678 void lock_shared() noexcept { AcquireSRWLockShared(&srw_); }
74 21 bool try_lock_shared() noexcept { return TryAcquireSRWLockShared(&srw_) != 0; }
75 102163 void unlock_shared() noexcept { ReleaseSRWLockShared(&srw_); }
76
77 private:
78 SRWLOCK srw_;
79 };
80
81 /**
82 * @struct CachedMemoryRegionInfo
83 * @brief Structure to hold cached memory region information.
84 * @details Uses timestamp for thread-safe updates and reduced memory footprint.
85 */
86 struct CachedMemoryRegionInfo
87 {
88 uintptr_t baseAddress;
89 size_t regionSize;
90 DWORD protection;
91 DWORD state;
92 uint64_t timestamp_ns;
93 uint64_t lru_key;
94 bool valid;
95
96 139 CachedMemoryRegionInfo()
97 139 : baseAddress(0), regionSize(0), protection(0), state(0), timestamp_ns(0), lru_key(0), valid(false)
98 {
99 139 }
100 };
101
102 /**
103 * @struct CacheShard
104 * @brief Individual cache shard with O(1) address lookup and O(log n) LRU eviction.
105 * @details Uses unordered_map keyed by region base address for fast lookup.
106 * std::map keyed by monotonic counter for efficient oldest-entry eviction.
107 * SrwSharedMutex allows multiple concurrent readers.
108 * in_flight flag prevents cache stampede by coalescing concurrent VirtualQuery calls.
109 * Mutex is stored separately to allow vector resize operations.
110 */
111 struct CacheShard
112 {
113 // Map from baseAddress -> CachedMemoryRegionInfo for O(1) lookup by address
114 std::unordered_map<uintptr_t, CachedMemoryRegionInfo> entries;
115 // Map from monotonic counter -> baseAddress for O(log n) oldest-entry lookup (LRU)
116 // Monotonic counter guarantees insertion-order uniqueness for correct eviction
117 std::map<uint64_t, uintptr_t> lru_index;
118 // Sorted by base address for O(log n) containment lookup
119 std::vector<std::pair<uintptr_t, uintptr_t>> sorted_ranges; // {base, base+size}
120 uint64_t entry_counter{0};
121 size_t capacity;
122 size_t max_capacity;
123
124 2744 CacheShard() : capacity(0), max_capacity(0)
125 {
126
1/2
✓ Branch 5 → 6 taken 2744 times.
✗ Branch 5 → 8 not taken.
2744 entries.reserve(64);
127
1/2
✓ Branch 6 → 7 taken 2744 times.
✗ Branch 6 → 8 not taken.
2744 sorted_ranges.reserve(64);
128 2744 }
129 };
130
131 /**
132 * @brief Returns current time in nanoseconds.
133 */
134 97577 inline uint64_t current_time_ns() noexcept
135 {
136 103791 return std::chrono::duration_cast<std::chrono::nanoseconds>(
137 200524 std::chrono::steady_clock::now().time_since_epoch())
138 103575 .count();
139 }
140
141 /**
142 * @brief Computes the shard index for a given address.
143 * @param address The address to hash.
144 * @param shard_count Total number of shards.
145 * @return The shard index.
146 * @note Uses golden ratio bit-mixing to spread adjacent addresses across shards.
147 */
148 98982 constexpr inline size_t compute_shard_index(uintptr_t address, size_t shard_count) noexcept
149 {
150 98982 return (static_cast<size_t>((address * 0x9E3779B97F4A7C15ULL) >> 48)) % shard_count;
151 }
152 }
153
154 // Internal static variables and helper functions for memory cache.
155 // Anonymous namespace ensures internal linkage, preventing ODR violations
156 // if this translation unit's declarations were ever duplicated.
157 namespace
158 {
159 std::vector<CacheShard> s_cacheShards;
160 std::vector<std::unique_ptr<SrwSharedMutex>> s_shardMutexes;
161 std::unique_ptr<std::atomic<char>[]> s_inFlight;
162 std::atomic<size_t> s_shardCount{0};
163 std::atomic<size_t> s_maxEntriesPerShard{0};
164 std::atomic<unsigned int> s_configuredExpiryMs{0};
165 std::atomic<bool> s_cacheInitialized{false};
166
167 // Global cache state mutex to serialize init/clear/shutdown transitions
168 // Protects against concurrent state changes that could leave vectors in invalid state
169 std::mutex s_cacheStateMutex;
170
171 // Epoch-based reader tracking to prevent use-after-free during shutdown.
172 // Readers increment on entry to is_readable/is_writable and decrement on exit.
173 // shutdown_cache waits for this to reach zero before destroying data structures.
174 std::atomic<int32_t> s_activeReaders{0};
175
176 /**
177 * @class ActiveReaderGuard
178 * @brief RAII guard that increments s_activeReaders on construction and
179 * decrements on destruction, ensuring correct pairing on all exit paths.
180 */
181 class ActiveReaderGuard
182 {
183 public:
184 101138 ActiveReaderGuard() noexcept
185 {
186 s_activeReaders.fetch_add(1, std::memory_order_acq_rel);
187 101138 }
188
189 107617 ~ActiveReaderGuard() noexcept
190 {
191 s_activeReaders.fetch_sub(1, std::memory_order_release);
192 107617 }
193
194 ActiveReaderGuard(const ActiveReaderGuard &) = delete;
195 ActiveReaderGuard &operator=(const ActiveReaderGuard &) = delete;
196 };
197
198 // Background cleanup thread.
199 // Uses std::thread (not jthread) because these are namespace-scope statics:
200 // jthread's auto-join destructor would run after s_cleanupCv/s_cleanupMutex
201 // are destroyed (reverse declaration order), causing UB. Manual join in
202 // shutdown_cache() avoids this. DMK_Shutdown() calls shutdown_cache()
203 // which joins this thread before any other cleanup proceeds, ensuring
204 // the thread is fully stopped before static destruction begins.
205 std::atomic<bool> s_cleanupThreadRunning{false};
206 std::thread s_cleanupThread;
207 std::mutex s_cleanupMutex;
208 std::condition_variable s_cleanupCv;
209 std::atomic<bool> s_cleanupRequested{false};
210
211 // On-demand cleanup fallback timer (used when background thread is disabled)
212 std::atomic<uint64_t> s_lastCleanupTimeNs{0};
213 constexpr uint64_t CLEANUP_INTERVAL_NS = 1'000'000'000ULL; // 1 second in nanoseconds
214
215 // Always-available cache statistics
216 struct CacheStats
217 {
218 std::atomic<uint64_t> cacheHits{0};
219 std::atomic<uint64_t> cacheMisses{0};
220 std::atomic<uint64_t> invalidations{0};
221 std::atomic<uint64_t> coalescedQueries{0};
222 std::atomic<uint64_t> onDemandCleanups{0};
223 };
224 CacheStats s_stats;
225
226 /**
227 * @brief Checks if a cache entry covers the requested address range and is valid.
228 * @param entry The cache entry to check.
229 * @param address Start address of the query.
230 * @param size Size of the query range.
231 * @param current_time_ns Current timestamp in nanoseconds.
232 * @param expiry_ns Expiry time in nanoseconds.
233 * @return true if the entry is valid and covers the range.
234 */
235 100662 constexpr inline bool is_entry_valid_and_covers(const CachedMemoryRegionInfo &entry,
236 uintptr_t address,
237 size_t size,
238 uint64_t current_time_ns,
239 uint64_t expiry_ns) noexcept
240 {
241
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 100662 times.
100662 if (!entry.valid)
242 return false;
243
244 100662 const uint64_t entry_age = current_time_ns - entry.timestamp_ns;
245
2/2
✓ Branch 4 → 5 taken 4 times.
✓ Branch 4 → 6 taken 100658 times.
100662 if (entry_age > expiry_ns)
246 4 return false;
247
248 100658 const uintptr_t endAddress = address + size;
249
2/2
✓ Branch 6 → 7 taken 4 times.
✓ Branch 6 → 8 taken 100654 times.
100658 if (endAddress < address)
250 4 return false;
251
252 100654 const uintptr_t entryEndAddress = entry.baseAddress + entry.regionSize;
253
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 100654 times.
100654 if (entryEndAddress < entry.baseAddress)
254 return false;
255
256
2/4
✓ Branch 10 → 11 taken 102975 times.
✗ Branch 10 → 13 not taken.
✓ Branch 11 → 12 taken 103665 times.
✗ Branch 11 → 13 not taken.
100654 return address >= entry.baseAddress && endAddress <= entryEndAddress;
257 }
258
259 /**
260 * @brief Checks protection flags for read permission.
261 */
262 100860 constexpr inline bool check_read_permission(DWORD protection) noexcept
263 {
264
1/2
✓ Branch 2 → 3 taken 101840 times.
✗ Branch 2 → 5 not taken.
202700 return (protection & CachePermissions::READ_PERMISSION_FLAGS) != 0 &&
265
1/2
✓ Branch 3 → 4 taken 102147 times.
✗ Branch 3 → 5 not taken.
202700 (protection & CachePermissions::NOACCESS_GUARD_FLAGS) == 0;
266 }
267
268 /**
269 * @brief Checks protection flags for write permission.
270 */
271 3786 constexpr inline bool check_write_permission(DWORD protection) noexcept
272 {
273
1/2
✓ Branch 2 → 3 taken 3802 times.
✗ Branch 2 → 5 not taken.
7588 return (protection & CachePermissions::WRITE_PERMISSION_FLAGS) != 0 &&
274
2/2
✓ Branch 3 → 4 taken 3777 times.
✓ Branch 3 → 5 taken 25 times.
7588 (protection & CachePermissions::NOACCESS_GUARD_FLAGS) == 0;
275 }
276
277 /**
278 * @brief Inserts a range into the shard's sorted auxiliary vector.
279 * @note Must be called with shard mutex held (exclusive).
280 */
281 139 void insert_sorted_range(CacheShard &shard, uintptr_t base_addr, size_t region_size) noexcept
282 {
283 139 auto range = std::make_pair(base_addr, base_addr + region_size);
284 139 auto pos = std::lower_bound(shard.sorted_ranges.begin(),
285 shard.sorted_ranges.end(), range);
286 278 shard.sorted_ranges.insert(pos, range);
287 139 }
288
289 /**
290 * @brief Removes a range from the shard's sorted auxiliary vector.
291 * @note Must be called with shard mutex held (exclusive).
292 */
293 14 void remove_sorted_range(CacheShard &shard, uintptr_t base_addr) noexcept
294 {
295 14 auto it = std::lower_bound(shard.sorted_ranges.begin(),
296 shard.sorted_ranges.end(),
297 14 std::make_pair(base_addr, uintptr_t{0}));
298
3/6
✓ Branch 13 → 14 taken 14 times.
✗ Branch 13 → 18 not taken.
✓ Branch 16 → 17 taken 14 times.
✗ Branch 16 → 18 not taken.
✓ Branch 19 → 20 taken 14 times.
✗ Branch 19 → 25 not taken.
42 if (it != shard.sorted_ranges.end() && it->first == base_addr)
299 28 shard.sorted_ranges.erase(it);
300 14 }
301
302 /**
303 * @brief Finds and validates a cache entry in a shard by scanning for range containment.
304 * @param shard The cache shard to search.
305 * @param address Address to look up.
306 * @param size Size of the query range.
307 * @param current_time_ns Current timestamp in nanoseconds.
308 * @param expiry_ns Expiry time in nanoseconds.
309 * @return Pointer to the matching entry, or nullptr if not found or expired.
310 * @note Must be called with shard mutex held (shared or exclusive).
311 * @note First attempts direct lookup by page-aligned base address for O(1) fast path,
312 * then falls back to O(log n) binary search via sorted_ranges for addresses
313 * within larger regions.
314 */
315 107515 CachedMemoryRegionInfo *find_in_shard(CacheShard &shard,
316 uintptr_t address,
317 size_t size,
318 uint64_t current_time_ns,
319 uint64_t expiry_ns) noexcept
320 {
321 // Fast path: direct lookup by page-aligned base address
322 107515 const uintptr_t base_addr = address & ~static_cast<uintptr_t>(0xFFF);
323 107515 auto it = shard.entries.find(base_addr);
324
1/2
✓ Branch 5 → 6 taken 103592 times.
✗ Branch 5 → 10 not taken.
101532 if (it != shard.entries.end())
325 {
326 103592 CachedMemoryRegionInfo &entry = it->second;
327
1/2
✓ Branch 8 → 9 taken 102628 times.
✗ Branch 8 → 10 not taken.
102192 if (is_entry_valid_and_covers(entry, address, size, current_time_ns, expiry_ns))
328 {
329 102628 return &entry;
330 }
331 }
332
333 // Slow path: O(log n) containment lookup via sorted ranges.
334 // Finds the last range starting at or before the queried address,
335 // then verifies containment and entry validity.
336 159 auto range_it = std::upper_bound(shard.sorted_ranges.begin(),
337 shard.sorted_ranges.end(),
338 std::make_pair(address, UINTPTR_MAX));
339
2/2
✓ Branch 21 → 22 taken 34 times.
✓ Branch 21 → 44 taken 125 times.
318 if (range_it != shard.sorted_ranges.begin())
340 {
341 --range_it;
342
5/6
✓ Branch 26 → 27 taken 34 times.
✗ Branch 26 → 31 not taken.
✓ Branch 29 → 30 taken 6 times.
✓ Branch 29 → 31 taken 28 times.
✓ Branch 32 → 33 taken 6 times.
✓ Branch 32 → 44 taken 28 times.
68 if (address >= range_it->first && address < range_it->second)
343 {
344 12 auto entry_it = shard.entries.find(range_it->first);
345
1/2
✓ Branch 38 → 39 taken 6 times.
✗ Branch 38 → 43 not taken.
6 if (entry_it != shard.entries.end())
346 {
347 6 CachedMemoryRegionInfo &entry = entry_it->second;
348
2/2
✓ Branch 41 → 42 taken 2 times.
✓ Branch 41 → 43 taken 4 times.
6 if (is_entry_valid_and_covers(entry, address, size, current_time_ns, expiry_ns))
349 {
350 2 return &entry;
351 }
352 }
353 }
354 }
355
356 157 return nullptr;
357 }
358
359 /**
360 * @brief Evicts the oldest entry from the shard using O(log n) LRU lookup.
361 * @note Must be called with shard mutex held (exclusive).
362 * @return true if an entry was evicted, false if shard is empty.
363 */
364 8 bool evict_oldest_entry(CacheShard &shard) noexcept
365 {
366
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 8 times.
8 if (shard.lru_index.empty())
367 return false;
368
369 8 const auto lru_it = shard.lru_index.begin();
370 8 const uintptr_t oldest_base = lru_it->second;
371
372 8 shard.lru_index.erase(lru_it);
373
374 8 const auto entry_it = shard.entries.find(oldest_base);
375
1/2
✓ Branch 11 → 12 taken 8 times.
✗ Branch 11 → 15 not taken.
8 if (entry_it != shard.entries.end())
376 {
377 8 shard.entries.erase(entry_it);
378 8 remove_sorted_range(shard, oldest_base);
379 8 return true;
380 }
381 return false;
382 }
383
384 /**
385 * @brief Force-evicts entries until shard is at or below max_capacity.
386 * @note Must be called with shard mutex held (exclusive).
387 * @param shard The cache shard to trim.
388 */
389 36 void trim_to_max_capacity(CacheShard &shard) noexcept
390 {
391
2/6
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 9 taken 36 times.
✗ Branch 7 → 8 not taken.
✗ Branch 7 → 9 not taken.
✗ Branch 10 → 3 not taken.
✓ Branch 10 → 11 taken 36 times.
36 while (shard.entries.size() > shard.max_capacity && !shard.lru_index.empty())
392 {
393 evict_oldest_entry(shard);
394 }
395 36 }
396
397 /**
398 * @brief Updates or inserts a cache entry in a specific shard.
399 * @param shard The cache shard to update.
400 * @param mbi Memory basic information from VirtualQuery.
401 * @param current_time_ns Current timestamp in nanoseconds.
402 * @note Must be called with shard mutex held (exclusive).
403 */
404 143 void update_shard_with_region(CacheShard &shard, const MEMORY_BASIC_INFORMATION &mbi, uint64_t current_time_ns) noexcept
405 {
406 143 const uintptr_t base_addr = reinterpret_cast<uintptr_t>(mbi.BaseAddress);
407
408 143 auto it = shard.entries.find(base_addr);
409
2/2
✓ Branch 5 → 6 taken 4 times.
✓ Branch 5 → 22 taken 139 times.
143 if (it != shard.entries.end())
410 {
411 // Remove old entry from LRU index using stored lru_key
412 4 CachedMemoryRegionInfo &old_entry = it->second;
413 4 const auto lru_it = shard.lru_index.find(old_entry.lru_key);
414
3/6
✓ Branch 10 → 11 taken 4 times.
✗ Branch 10 → 14 not taken.
✓ Branch 12 → 13 taken 4 times.
✗ Branch 12 → 14 not taken.
✓ Branch 15 → 16 taken 4 times.
✗ Branch 15 → 17 not taken.
4 if (lru_it != shard.lru_index.end() && lru_it->second == base_addr)
415 {
416 4 shard.lru_index.erase(lru_it);
417 }
418
419 // Update sorted range if region size changed
420
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 20 taken 4 times.
4 if (old_entry.regionSize != mbi.RegionSize)
421 {
422 remove_sorted_range(shard, base_addr);
423 insert_sorted_range(shard, base_addr, mbi.RegionSize);
424 }
425
426 // Update existing entry with new monotonic LRU key
427 4 const uint64_t new_lru_key = shard.entry_counter++;
428 4 old_entry.baseAddress = base_addr;
429 4 old_entry.regionSize = mbi.RegionSize;
430 4 old_entry.protection = mbi.Protect;
431 4 old_entry.state = mbi.State;
432 4 old_entry.timestamp_ns = current_time_ns;
433 4 old_entry.lru_key = new_lru_key;
434 4 old_entry.valid = true;
435
436 // Insert new composite key into LRU index
437 4 shard.lru_index.emplace(new_lru_key, base_addr);
438 }
439 else
440 {
441 // Evict oldest if at capacity - O(log n) via map
442
2/2
✓ Branch 23 → 24 taken 8 times.
✓ Branch 23 → 25 taken 131 times.
139 if (shard.entries.size() >= shard.capacity)
443 {
444 8 evict_oldest_entry(shard);
445 }
446
447 // Hard upper bound: trim if exceeding max_capacity
448
1/2
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 139 times.
139 if (shard.entries.size() >= shard.max_capacity)
449 {
450 trim_to_max_capacity(shard);
451 }
452
453 // Generate unique monotonic LRU key
454 139 const uint64_t new_lru_key = shard.entry_counter++;
455
456 139 CachedMemoryRegionInfo new_entry;
457 139 new_entry.baseAddress = base_addr;
458 139 new_entry.regionSize = mbi.RegionSize;
459 139 new_entry.protection = mbi.Protect;
460 139 new_entry.state = mbi.State;
461 139 new_entry.timestamp_ns = current_time_ns;
462 139 new_entry.lru_key = new_lru_key;
463 139 new_entry.valid = true;
464
465 278 shard.entries.insert_or_assign(base_addr, std::move(new_entry));
466 139 shard.lru_index.emplace(new_lru_key, base_addr);
467 139 insert_sorted_range(shard, base_addr, mbi.RegionSize);
468 }
469 143 }
470
471 /**
472 * @brief Removes expired entries from a shard.
473 * @note Must be called with shard mutex held (exclusive).
474 * @return Number of entries removed from this shard.
475 */
476 36 size_t cleanup_expired_entries_in_shard(CacheShard &shard,
477 uint64_t current_time_ns,
478 uint64_t expiry_ns) noexcept
479 {
480 36 size_t removed = 0;
481 36 auto it = shard.entries.begin();
482
2/2
✓ Branch 25 → 4 taken 1 time.
✓ Branch 25 → 26 taken 36 times.
37 while (it != shard.entries.end())
483 {
484 1 const CachedMemoryRegionInfo &entry = it->second;
485 1 const uint64_t entry_age = current_time_ns - entry.timestamp_ns;
486
487
2/4
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 7 not taken.
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 21 not taken.
1 if (!entry.valid || entry_age > expiry_ns)
488 {
489 // Remove from LRU index using stored lru_key
490 1 const auto lru_it = shard.lru_index.find(entry.lru_key);
491
3/6
✓ Branch 10 → 11 taken 1 time.
✗ Branch 10 → 15 not taken.
✓ Branch 13 → 14 taken 1 time.
✗ Branch 13 → 15 not taken.
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 18 not taken.
1 if (lru_it != shard.lru_index.end() && lru_it->second == it->first)
492 {
493 1 shard.lru_index.erase(lru_it);
494 }
495
496 1 remove_sorted_range(shard, entry.baseAddress);
497 1 it = shard.entries.erase(it);
498 1 ++removed;
499 1 }
500 else
501 {
502 ++it;
503 }
504 }
505 36 return removed;
506 }
507
508 /**
509 * @brief Performs cleanup of expired cache entries across all shards.
510 * @details Called by the background cleanup thread or on-demand timer.
511 * @param force Force cleanup regardless of timing.
512 */
513 3 void cleanup_expired_entries(bool force) noexcept
514 {
515 // Always hold state mutex to prevent racing with shutdown_cache()
516 // which clears the shard vectors. try_lock for on-demand to avoid
517 // blocking the hot path; forced cleanup blocks to guarantee progress.
518 3 std::unique_lock<std::mutex> lock(s_cacheStateMutex, std::defer_lock);
519
1/2
✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 5 not taken.
3 if (force)
520 {
521 3 lock.lock();
522 }
523 else if (!lock.try_lock())
524 {
525 return; // Shutdown or forced cleanup in progress, skip
526 }
527
528
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 3 times.
3 if (s_cacheShards.empty())
529 return;
530
531 3 const size_t shard_count = s_shardCount.load(std::memory_order_acquire);
532
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 3 times.
3 if (shard_count == 0)
533 return;
534
535 3 const uint64_t current_ts = current_time_ns();
536 3 const uint64_t expiry_ns = static_cast<uint64_t>(s_configuredExpiryMs.load(std::memory_order_acquire)) * 1'000'000ULL;
537
538
2/2
✓ Branch 40 → 29 taken 36 times.
✓ Branch 40 → 41 taken 3 times.
39 for (size_t i = 0; i < shard_count; ++i)
539 {
540 36 std::unique_lock<SrwSharedMutex> shard_lock(*s_shardMutexes[i], std::try_to_lock);
541
1/2
✓ Branch 33 → 34 taken 36 times.
✗ Branch 33 → 38 not taken.
36 if (shard_lock.owns_lock())
542 {
543 36 cleanup_expired_entries_in_shard(s_cacheShards[i], current_ts, expiry_ns);
544 // Also trim to hard upper bound
545 36 trim_to_max_capacity(s_cacheShards[i]);
546 }
547 36 }
548
1/2
✓ Branch 43 → 44 taken 3 times.
✗ Branch 43 → 46 not taken.
3 }
549
550 /**
551 * @brief Checks if on-demand cleanup should run based on elapsed time.
552 * @return true if cleanup was performed, false otherwise.
553 */
554 bool try_trigger_on_demand_cleanup() noexcept
555 {
556 if (!s_cacheInitialized.load(std::memory_order_acquire))
557 return false;
558
559 const uint64_t now_ns = current_time_ns();
560 const uint64_t last_cleanup = s_lastCleanupTimeNs.load(std::memory_order_acquire);
561 const uint64_t elapsed_ns = now_ns - last_cleanup;
562
563 if (elapsed_ns >= CLEANUP_INTERVAL_NS)
564 {
565 // Atomically update last cleanup time to prevent multiple threads triggering
566 uint64_t expected = last_cleanup;
567 if (s_lastCleanupTimeNs.compare_exchange_strong(expected, now_ns, std::memory_order_acq_rel))
568 {
569 cleanup_expired_entries(false);
570 s_stats.onDemandCleanups.fetch_add(1, std::memory_order_relaxed);
571 return true;
572 }
573 }
574 return false;
575 }
576
577 /**
578 * @brief Background cleanup thread function.
579 * @details Runs periodically to clean up expired entries without impacting the miss path.
580 */
581 191 void cleanup_thread_func() noexcept
582 {
583
2/2
✓ Branch 13 → 3 taken 16 times.
✓ Branch 13 → 14 taken 178 times.
194 while (s_cleanupThreadRunning.load(std::memory_order_acquire))
584 {
585 {
586 16 std::unique_lock<std::mutex> lock(s_cleanupMutex);
587 16 s_cleanupCv.wait_for(lock, std::chrono::seconds(1), [&]()
588
4/4
✓ Branch 3 → 4 taken 28 times.
✓ Branch 3 → 6 taken 2 times.
✓ Branch 5 → 6 taken 13 times.
✓ Branch 5 → 7 taken 15 times.
30 { return s_cleanupRequested.load(std::memory_order_acquire) || !s_cleanupThreadRunning.load(std::memory_order_acquire); });
589 16 }
590
591
2/2
✓ Branch 8 → 9 taken 13 times.
✓ Branch 8 → 10 taken 3 times.
16 if (!s_cleanupThreadRunning.load(std::memory_order_acquire))
592 13 break;
593
594 3 cleanup_expired_entries(true); // force=true to hold state mutex during vector iteration
595 3 s_cleanupRequested.store(false, std::memory_order_relaxed);
596 }
597 191 }
598
599 /**
600 * @brief Signals the cleanup thread to run or triggers on-demand cleanup.
601 */
602 15 void request_cleanup() noexcept
603 {
604
1/2
✓ Branch 3 → 4 taken 15 times.
✗ Branch 3 → 6 not taken.
15 if (s_cleanupThreadRunning.load(std::memory_order_acquire))
605 {
606 15 s_cleanupRequested.store(true, std::memory_order_relaxed);
607 15 s_cleanupCv.notify_one();
608 }
609 else
610 {
611 // Background thread disabled (MinGW) - use on-demand timer-based cleanup
612 try_trigger_on_demand_cleanup();
613 }
614 15 }
615
616 /**
617 * @brief Invalidates cache entries in shards that overlap with the given range.
618 * @details Only invalidates specific entries that overlap, not entire shards.
619 * Uses retry loop to handle locked shards gracefully.
620 */
621 15 void invalidate_range_internal(uintptr_t address, size_t size) noexcept
622 {
623
3/6
✓ Branch 3 → 4 taken 15 times.
✗ Branch 3 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 15 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 15 times.
15 if (s_cacheShards.empty() || size == 0)
624 return;
625
626 // Guard against address + size wrapping around the address space
627
2/2
✓ Branch 9 → 10 taken 14 times.
✓ Branch 9 → 11 taken 1 time.
15 const uintptr_t endAddress = (address + size < address) ? UINTPTR_MAX : address + size;
628 15 const size_t shard_count = s_shardCount.load(std::memory_order_acquire);
629
630 15 const uintptr_t start_page = address >> 12;
631
1/2
✓ Branch 19 → 20 taken 15 times.
✗ Branch 19 → 21 not taken.
15 const uintptr_t end_page = (endAddress == 0 ? address : endAddress - 1) >> 12;
632
633 15 constexpr size_t MAX_INVALIDATION_RETRIES = 3;
634
635
1/2
✓ Branch 74 → 23 taken 15 times.
✗ Branch 74 → 75 not taken.
15 for (uintptr_t page = start_page; page <= end_page; ++page)
636 {
637 15 const size_t shard_idx = compute_shard_index(page << 12, shard_count);
638
639 15 bool invalidated = false;
640
3/4
✓ Branch 69 → 70 taken 30 times.
✗ Branch 69 → 71 not taken.
✓ Branch 70 → 25 taken 15 times.
✓ Branch 70 → 71 taken 15 times.
30 for (size_t retry = 0; retry < MAX_INVALIDATION_RETRIES && !invalidated; ++retry)
641 {
642 15 std::unique_lock<SrwSharedMutex> lock(*s_shardMutexes[shard_idx], std::try_to_lock);
643
1/2
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 33 taken 15 times.
15 if (!lock.owns_lock())
644 {
645 // Shard is locked by another writer - yield and retry
646 if (retry < MAX_INVALIDATION_RETRIES - 1)
647 {
648 std::this_thread::yield();
649 }
650 continue;
651 }
652
653 15 CacheShard &shard = s_cacheShards[shard_idx];
654 15 const uintptr_t page_base = page << 12;
655
656 15 auto it = shard.entries.find(page_base);
657
2/2
✓ Branch 37 → 38 taken 5 times.
✓ Branch 37 → 61 taken 10 times.
15 if (it != shard.entries.end())
658 {
659 5 CachedMemoryRegionInfo &entry = it->second;
660
1/2
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 41 taken 5 times.
5 if (!entry.valid)
661 {
662 invalidated = true;
663 continue;
664 }
665
666 5 const uintptr_t entryEndAddress = entry.baseAddress + entry.regionSize;
667
2/4
✓ Branch 41 → 42 taken 5 times.
✗ Branch 41 → 44 not taken.
✓ Branch 42 → 43 taken 5 times.
✗ Branch 42 → 44 not taken.
5 const bool overlaps = address < entryEndAddress && endAddress > entry.baseAddress;
668
1/2
✓ Branch 45 → 46 taken 5 times.
✗ Branch 45 → 62 not taken.
5 if (overlaps)
669 {
670 // Remove from LRU index using stored lru_key to avoid tombstone accumulation
671 5 const auto lru_it = shard.lru_index.find(entry.lru_key);
672
3/6
✓ Branch 49 → 50 taken 5 times.
✗ Branch 49 → 53 not taken.
✓ Branch 51 → 52 taken 5 times.
✗ Branch 51 → 53 not taken.
✓ Branch 54 → 55 taken 5 times.
✗ Branch 54 → 56 not taken.
5 if (lru_it != shard.lru_index.end() && lru_it->second == page_base)
673 {
674 5 shard.lru_index.erase(lru_it);
675 }
676 // Erase entry immediately instead of leaving tombstone
677 5 remove_sorted_range(shard, entry.baseAddress);
678 5 shard.entries.erase(it);
679 s_stats.invalidations.fetch_add(1, std::memory_order_relaxed);
680 5 invalidated = true;
681 }
682 }
683 else
684 {
685 10 invalidated = true;
686 }
687
1/2
✓ Branch 64 → 65 taken 15 times.
✗ Branch 64 → 67 not taken.
15 }
688
689
1/2
✓ Branch 71 → 72 taken 15 times.
✗ Branch 71 → 73 not taken.
15 if (start_page == end_page)
690 15 break;
691 }
692 }
693
694 /**
695 * @brief Performs one-time cache initialization.
696 */
697 191 bool perform_cache_initialization(size_t cache_size, unsigned int expiry_ms, size_t shard_count)
698 {
699
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 191 times.
191 if (cache_size == 0)
700 cache_size = 1;
701
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 191 times.
191 if (shard_count == 0)
702 shard_count = 1;
703
704 191 const size_t entries_per_shard = (cache_size + shard_count - 1) / shard_count;
705 191 const size_t hard_max_per_shard = entries_per_shard * 2; // Hard upper bound: 2x capacity
706
707 try
708 {
709
1/2
✓ Branch 6 → 7 taken 191 times.
✗ Branch 6 → 73 not taken.
191 s_cacheShards.resize(shard_count);
710
1/2
✓ Branch 7 → 8 taken 191 times.
✗ Branch 7 → 73 not taken.
191 s_shardMutexes.resize(shard_count);
711
1/2
✓ Branch 8 → 9 taken 191 times.
✗ Branch 8 → 71 not taken.
191 s_inFlight = std::make_unique<std::atomic<char>[]>(shard_count);
712
2/2
✓ Branch 32 → 12 taken 2744 times.
✓ Branch 32 → 33 taken 191 times.
2935 for (size_t i = 0; i < shard_count; ++i)
713 {
714
1/2
✓ Branch 13 → 14 taken 2744 times.
✗ Branch 13 → 73 not taken.
2744 s_cacheShards[i].entries.reserve(entries_per_shard * 2);
715
1/2
✓ Branch 15 → 16 taken 2744 times.
✗ Branch 15 → 73 not taken.
2744 s_cacheShards[i].sorted_ranges.reserve(entries_per_shard * 2);
716 2744 s_cacheShards[i].capacity = entries_per_shard;
717 2744 s_cacheShards[i].max_capacity = hard_max_per_shard;
718
1/2
✓ Branch 18 → 19 taken 2744 times.
✗ Branch 18 → 72 not taken.
2744 s_shardMutexes[i] = std::make_unique<SrwSharedMutex>();
719 2744 s_inFlight[i].store(0, std::memory_order_relaxed);
720 }
721 }
722 catch (const std::bad_alloc &)
723 {
724 Logger::get_instance().error("MemoryCache: Failed to allocate memory for cache shards.");
725 s_cacheShards.clear();
726 s_shardMutexes.clear();
727 s_inFlight.reset();
728 // Reset initialization flag so retry can work
729 s_cacheInitialized.store(false, std::memory_order_relaxed);
730 return false;
731 }
732
733 191 s_shardCount.store(shard_count, std::memory_order_release);
734 191 s_maxEntriesPerShard.store(entries_per_shard, std::memory_order_release);
735 191 s_configuredExpiryMs.store(expiry_ms, std::memory_order_release);
736 191 s_lastCleanupTimeNs.store(current_time_ns(), std::memory_order_release);
737
738
2/4
✓ Branch 66 → 67 taken 191 times.
✗ Branch 66 → 87 not taken.
✓ Branch 67 → 68 taken 191 times.
✗ Branch 67 → 86 not taken.
191 Logger::get_instance().debug("MemoryCache: Initialized with {} shards ({} entries/shard, {}ms expiry, {} max).",
739 shard_count, entries_per_shard, expiry_ms, hard_max_per_shard);
740
741 191 return true;
742 }
743
744 /**
745 * @brief Performs VirtualQuery and updates cache with coalescing support.
746 * @param shard_idx Index of the shard to update.
747 * @param address Address to query.
748 * @param mbi_out Output buffer for VirtualQuery result.
749 * @return true if VirtualQuery succeeded.
750 */
751 143 bool query_and_update_cache(size_t shard_idx, LPCVOID address, MEMORY_BASIC_INFORMATION &mbi_out) noexcept
752 {
753 143 CacheShard &shard = s_cacheShards[shard_idx];
754
755 // Try to claim in-flight status (stampede coalescing)
756 143 char expected = 0;
757
1/2
✓ Branch 12 → 13 taken 143 times.
✗ Branch 12 → 32 not taken.
286 if (s_inFlight[shard_idx].compare_exchange_strong(expected, 1, std::memory_order_acq_rel))
758 {
759 // We are the leader - perform VirtualQuery
760 143 const bool result = VirtualQuery(address, &mbi_out, sizeof(mbi_out)) != 0;
761 143 const uint64_t now_ns = current_time_ns();
762
763
1/2
✓ Branch 15 → 16 taken 143 times.
✗ Branch 15 → 22 not taken.
143 if (result)
764 {
765 143 std::unique_lock<SrwSharedMutex> lock(*s_shardMutexes[shard_idx]);
766 143 update_shard_with_region(shard, mbi_out, now_ns);
767 143 }
768
769 // Release in-flight status
770 143 s_inFlight[shard_idx].store(0, std::memory_order_release);
771 143 return result;
772 }
773 else
774 {
775 // We are a follower - VirtualQuery already in progress by another thread.
776 // Bounded wait to avoid stalling game threads on render-critical paths.
777 const uint64_t expiry_ns = static_cast<uint64_t>(s_configuredExpiryMs.load(std::memory_order_acquire)) * 1'000'000ULL;
778 constexpr size_t MAX_FOLLOWER_YIELDS = 8;
779
780 for (size_t yield_count = 0; yield_count < MAX_FOLLOWER_YIELDS; ++yield_count)
781 {
782 if (s_inFlight[shard_idx].load(std::memory_order_acquire) == 0)
783 {
784 // Query completed, check cache
785 const uintptr_t addr_val = reinterpret_cast<uintptr_t>(address);
786 std::shared_lock<SrwSharedMutex> lock(*s_shardMutexes[shard_idx]);
787 CachedMemoryRegionInfo *cached = find_in_shard(shard, addr_val, 1, current_time_ns(), expiry_ns);
788 if (cached)
789 {
790 s_stats.coalescedQueries.fetch_add(1, std::memory_order_relaxed);
791 // Copy cached info to output for consistency
792 mbi_out.BaseAddress = reinterpret_cast<PVOID>(cached->baseAddress);
793 mbi_out.RegionSize = cached->regionSize;
794 mbi_out.Protect = cached->protection;
795 mbi_out.State = cached->state;
796 return true;
797 }
798 // Cache not populated, break to retry as leader
799 break;
800 }
801
802 // Yield to allow the leader thread to complete
803 std::this_thread::yield();
804 }
805
806 // Retry as leader if follower wait timed out
807 expected = 0;
808 if (s_inFlight[shard_idx].compare_exchange_strong(expected, 1, std::memory_order_acq_rel))
809 {
810 const bool result = VirtualQuery(address, &mbi_out, sizeof(mbi_out)) != 0;
811 if (result)
812 {
813 std::unique_lock<SrwSharedMutex> lock(*s_shardMutexes[shard_idx]);
814 const uint64_t now_ns = current_time_ns();
815 update_shard_with_region(shard, mbi_out, now_ns);
816 }
817 s_inFlight[shard_idx].store(0, std::memory_order_release);
818 return result;
819 }
820
821 // Last resort: just do VirtualQuery without cache update
822 return VirtualQuery(address, &mbi_out, sizeof(mbi_out)) != 0;
823 }
824 }
825
826 } // anonymous namespace (cache internals)
827
828 193 bool DetourModKit::Memory::init_cache(size_t cache_size, unsigned int expiry_ms, size_t shard_count)
829 {
830 // Hold state mutex to prevent concurrent clear_cache or shutdown_cache
831 // This serializes init/clear/shutdown transitions to ensure vectors are not accessed while being resized or cleared
832
1/2
✓ Branch 2 → 3 taken 193 times.
✗ Branch 2 → 38 not taken.
193 std::lock_guard<std::mutex> state_lock(s_cacheStateMutex);
833
834 // Fast path: already initialized
835
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 191 times.
193 if (s_cacheInitialized.load(std::memory_order_acquire))
836 2 return true;
837
838 // Try to initialize
839 191 bool expected = false;
840
1/2
✓ Branch 7 → 8 taken 191 times.
✗ Branch 7 → 21 not taken.
191 if (s_cacheInitialized.compare_exchange_strong(expected, true, std::memory_order_acq_rel))
841 {
842
2/4
✓ Branch 8 → 9 taken 191 times.
✗ Branch 8 → 36 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 191 times.
191 if (!perform_cache_initialization(cache_size, expiry_ms, shard_count))
843 {
844 // Initialization failed - s_cacheInitialized already reset to false in perform_cache_initialization
845 return false;
846 }
847
848 // Try to start background cleanup thread (may fail silently on MinGW)
849 191 s_cleanupThreadRunning.store(true, std::memory_order_release);
850 try
851 {
852
1/2
✓ Branch 12 → 13 taken 191 times.
✗ Branch 12 → 25 not taken.
191 s_cleanupThread = std::thread(cleanup_thread_func);
853 }
854 catch (const std::system_error &)
855 {
856 // Background thread creation failed (MinGW pthreads issue) - use on-demand cleanup
857 s_cleanupThreadRunning.store(false, std::memory_order_release);
858 Logger::get_instance().debug("MemoryCache: Background cleanup thread unavailable, using on-demand cleanup.");
859 }
860
861 // Register atexit handler as a last-resort safety net in case the
862 // consumer forgets to call shutdown_cache() / DMK_Shutdown().
863 // Prevents std::terminate from the joinable std::thread destructor.
864 // The handler detects loader-lock context (FreeLibrary) and skips
865 // the thread join to avoid deadlock.
866 static bool atexit_registered = false;
867
2/2
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 20 taken 190 times.
191 if (!atexit_registered)
868 {
869 1 std::atexit([]()
870 {
871
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 15 taken 1 time.
1 if (s_cacheInitialized.load(std::memory_order_acquire))
872 {
873 if (is_loader_lock_held())
874 {
875 // Under loader lock (FreeLibrary path): pin the module
876 // so code pages remain valid for the detached thread,
877 // then signal it to stop and detach.
878 s_cleanupThreadRunning.store(false, std::memory_order_release);
879 s_cleanupCv.notify_one();
880 if (s_cleanupThread.joinable())
881 {
882 pin_current_module();
883 s_cleanupThread.detach();
884 }
885 s_cacheInitialized.store(false, std::memory_order_release);
886 return;
887 }
888 Memory::shutdown_cache();
889 } });
890 1 atexit_registered = true;
891 }
892
893 191 return true;
894 }
895
896 // Another thread initialized while we were waiting
897 return true;
898 193 }
899
900 26 void DetourModKit::Memory::clear_cache()
901 {
902 // Hold state mutex to serialize with shutdown and cleanup thread
903
1/2
✓ Branch 2 → 3 taken 26 times.
✗ Branch 2 → 102 not taken.
26 std::lock_guard<std::mutex> state_lock(s_cacheStateMutex);
904
905
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 26 times.
26 if (!s_cacheInitialized.load(std::memory_order_acquire))
906 return;
907
908 26 const size_t shard_count = s_shardCount.load(std::memory_order_acquire);
909
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 26 times.
26 if (shard_count == 0)
910 return;
911
912 // Acquire exclusive lock on each shard and clear entries.
913 // Uses blocking lock to guarantee all entries are cleared.
914 // The background cleanup thread uses try_to_lock on shard mutexes,
915 // so it will skip shards we hold without deadlocking.
916
2/2
✓ Branch 39 → 16 taken 392 times.
✓ Branch 39 → 40 taken 26 times.
418 for (size_t i = 0; i < shard_count; ++i)
917 {
918 392 auto &mutex_ptr = s_shardMutexes[i];
919
1/2
✓ Branch 18 → 19 taken 392 times.
✗ Branch 18 → 38 not taken.
392 if (mutex_ptr)
920 {
921
1/2
✓ Branch 20 → 21 taken 392 times.
✗ Branch 20 → 98 not taken.
392 std::unique_lock<SrwSharedMutex> shard_lock(*mutex_ptr);
922 392 s_cacheShards[i].entries.clear();
923 392 s_cacheShards[i].lru_index.clear();
924 392 s_cacheShards[i].sorted_ranges.clear();
925 392 s_inFlight[i].store(0, std::memory_order_relaxed);
926 392 }
927 }
928
929 s_stats.cacheHits.store(0, std::memory_order_relaxed);
930 s_stats.cacheMisses.store(0, std::memory_order_relaxed);
931 s_stats.invalidations.store(0, std::memory_order_relaxed);
932 s_stats.coalescedQueries.store(0, std::memory_order_relaxed);
933 s_stats.onDemandCleanups.store(0, std::memory_order_relaxed);
934
935 26 s_lastCleanupTimeNs.store(current_time_ns(), std::memory_order_relaxed);
936
937
2/4
✓ Branch 89 → 90 taken 26 times.
✗ Branch 89 → 100 not taken.
✓ Branch 90 → 91 taken 26 times.
✗ Branch 90 → 99 not taken.
26 Logger::get_instance().debug("MemoryCache: All entries cleared.");
938
1/2
✓ Branch 93 → 94 taken 26 times.
✗ Branch 93 → 96 not taken.
26 }
939
940 213 void DetourModKit::Memory::shutdown_cache()
941 {
942 // Signal and join cleanup thread BEFORE acquiring state mutex.
943 // The cleanup thread acquires s_cacheStateMutex in cleanup_expired_entries(force=true),
944 // so joining while holding the state mutex would deadlock.
945 213 s_cleanupThreadRunning.store(false, std::memory_order_release);
946 213 s_cleanupCv.notify_one();
947
948
2/2
✓ Branch 5 → 6 taken 191 times.
✓ Branch 5 → 11 taken 22 times.
213 if (s_cleanupThread.joinable())
949 {
950
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 10 taken 191 times.
191 if (is_loader_lock_held())
951 {
952 // Under loader lock (DllMain / FreeLibrary): thread join would
953 // deadlock because the cleanup thread cannot exit while the
954 // loader lock is held. Pin the module so code and static data
955 // remain valid, then detach. The thread will observe the stop
956 // flag and exit on its own.
957 pin_current_module();
958 s_cleanupThread.detach();
959 }
960 else
961 {
962
1/2
✓ Branch 10 → 11 taken 191 times.
✗ Branch 10 → 132 not taken.
191 s_cleanupThread.join();
963 }
964 }
965
966 // Acquire state mutex to serialize with clear_cache and protect data teardown
967
1/2
✓ Branch 11 → 12 taken 213 times.
✗ Branch 11 → 132 not taken.
213 std::lock_guard<std::mutex> state_lock(s_cacheStateMutex);
968
969 // Mark as not initialized and zero shard count.
970 // This prevents new readers from entering the critical section.
971 // acquire/release is sufficient here because the state mutex provides the
972 // cross-thread ordering guarantee. Readers that observe shard_count == 0
973 // immediately exit without touching data structures.
974 213 s_cacheInitialized.store(false, std::memory_order_release);
975 s_shardCount.store(0, std::memory_order_release);
976
977 // Wait for in-flight readers to finish before destroying data structures.
978 // Readers increment s_activeReaders on entry and decrement on exit.
979 // ActiveReaderGuard is RAII so readers always decrement; this loop is
980 // bounded by the maximum time a single cache lookup can take.
981 // Escalate from yield to sleep to avoid burning CPU if a reader is
982 // preempted by the OS scheduler.
983 213 constexpr int yield_spins = 4096;
984 213 int spins = 0;
985
1/2
✗ Branch 35 → 22 not taken.
✓ Branch 35 → 36 taken 213 times.
426 while (s_activeReaders.load(std::memory_order_acquire) > 0)
986 {
987 if (spins < yield_spins)
988 {
989 std::this_thread::yield();
990 }
991 else
992 {
993 std::this_thread::sleep_for(std::chrono::microseconds(100));
994 }
995 ++spins;
996 }
997
998 // All readers have exited - safe to destroy data structures
999 213 const size_t shard_count = s_cacheShards.size();
1000
2/2
✓ Branch 53 → 38 taken 2744 times.
✓ Branch 53 → 54 taken 213 times.
2957 for (size_t i = 0; i < shard_count; ++i)
1001 {
1002
1/2
✓ Branch 40 → 41 taken 2744 times.
✗ Branch 40 → 52 not taken.
2744 if (s_shardMutexes[i])
1003 {
1004
1/2
✓ Branch 43 → 44 taken 2744 times.
✗ Branch 43 → 128 not taken.
2744 std::unique_lock<SrwSharedMutex> shard_lock(*s_shardMutexes[i]);
1005 2744 s_cacheShards[i].entries.clear();
1006 2744 s_cacheShards[i].lru_index.clear();
1007 2744 s_cacheShards[i].sorted_ranges.clear();
1008 2744 }
1009 }
1010
1011 213 s_cacheShards.clear();
1012 213 s_shardMutexes.clear();
1013 213 s_inFlight.reset();
1014
1015 // Reset all stats and config so a subsequent init_cache starts from a clean state
1016 s_stats.cacheHits.store(0, std::memory_order_relaxed);
1017 s_stats.cacheMisses.store(0, std::memory_order_relaxed);
1018 s_stats.invalidations.store(0, std::memory_order_relaxed);
1019 s_stats.coalescedQueries.store(0, std::memory_order_relaxed);
1020 s_stats.onDemandCleanups.store(0, std::memory_order_relaxed);
1021 s_lastCleanupTimeNs.store(0, std::memory_order_relaxed);
1022 s_configuredExpiryMs.store(0, std::memory_order_relaxed);
1023 s_maxEntriesPerShard.store(0, std::memory_order_relaxed);
1024 213 s_cleanupRequested.store(false, std::memory_order_relaxed);
1025
1026
2/4
✓ Branch 122 → 123 taken 213 times.
✗ Branch 122 → 130 not taken.
✓ Branch 123 → 124 taken 213 times.
✗ Branch 123 → 129 not taken.
213 Logger::get_instance().debug("MemoryCache: Shutdown complete.");
1027 213 }
1028
1029 22 std::string DetourModKit::Memory::get_cache_stats()
1030 {
1031 22 const uint64_t hits = s_stats.cacheHits.load(std::memory_order_relaxed);
1032 22 const uint64_t misses = s_stats.cacheMisses.load(std::memory_order_relaxed);
1033 22 const uint64_t invalidations = s_stats.invalidations.load(std::memory_order_relaxed);
1034 22 const uint64_t coalesced = s_stats.coalescedQueries.load(std::memory_order_relaxed);
1035 22 const uint64_t on_demand_cleanups = s_stats.onDemandCleanups.load(std::memory_order_relaxed);
1036 22 const uint64_t total_queries = hits + misses;
1037
1038 22 const size_t shard_count = s_shardCount.load(std::memory_order_acquire);
1039 22 const size_t max_entries_per_shard = s_maxEntriesPerShard.load(std::memory_order_acquire);
1040 22 const unsigned int expiry_ms = s_configuredExpiryMs.load(std::memory_order_acquire);
1041
1042 // Calculate total entries and hard max with reader guard
1043 22 size_t total_entries = 0;
1044 22 size_t total_hard_max = 0;
1045
1046 {
1047 22 ActiveReaderGuard reader_guard;
1048 22 const size_t active_shard_count = s_shardCount.load(std::memory_order_acquire);
1049
2/2
✓ Branch 78 → 67 taken 115 times.
✓ Branch 78 → 79 taken 22 times.
137 for (size_t i = 0; i < active_shard_count; ++i)
1050 {
1051 115 auto &mutex_ptr = s_shardMutexes[i];
1052
1/2
✓ Branch 69 → 70 taken 115 times.
✗ Branch 69 → 77 not taken.
115 if (mutex_ptr)
1053 {
1054 115 std::shared_lock<SrwSharedMutex> shard_lock(*mutex_ptr);
1055 115 total_entries += s_cacheShards[i].entries.size();
1056 115 total_hard_max += s_cacheShards[i].max_capacity;
1057 115 }
1058 }
1059 22 }
1060
1061
1/2
✓ Branch 80 → 81 taken 22 times.
✗ Branch 80 → 120 not taken.
22 std::ostringstream oss;
1062
2/4
✓ Branch 81 → 82 taken 22 times.
✗ Branch 81 → 118 not taken.
✓ Branch 82 → 83 taken 22 times.
✗ Branch 82 → 118 not taken.
22 oss << "MemoryCache Stats (Shards: " << shard_count
1063
2/4
✓ Branch 83 → 84 taken 22 times.
✗ Branch 83 → 118 not taken.
✓ Branch 84 → 85 taken 22 times.
✗ Branch 84 → 118 not taken.
22 << ", Entries/Shard: " << max_entries_per_shard
1064
3/6
✓ Branch 85 → 86 taken 22 times.
✗ Branch 85 → 118 not taken.
✓ Branch 86 → 87 taken 22 times.
✗ Branch 86 → 88 not taken.
✓ Branch 89 → 90 taken 22 times.
✗ Branch 89 → 118 not taken.
22 << ", HardMax/Shard: " << (shard_count > 0 ? total_hard_max / shard_count : 0)
1065
2/4
✓ Branch 90 → 91 taken 22 times.
✗ Branch 90 → 118 not taken.
✓ Branch 91 → 92 taken 22 times.
✗ Branch 91 → 118 not taken.
22 << ", Expiry: " << expiry_ms << "ms) - "
1066
5/10
✓ Branch 92 → 93 taken 22 times.
✗ Branch 92 → 118 not taken.
✓ Branch 93 → 94 taken 22 times.
✗ Branch 93 → 118 not taken.
✓ Branch 94 → 95 taken 22 times.
✗ Branch 94 → 118 not taken.
✓ Branch 95 → 96 taken 22 times.
✗ Branch 95 → 118 not taken.
✓ Branch 96 → 97 taken 22 times.
✗ Branch 96 → 118 not taken.
22 << "Hits: " << hits << ", Misses: " << misses
1067
2/4
✓ Branch 97 → 98 taken 22 times.
✗ Branch 97 → 118 not taken.
✓ Branch 98 → 99 taken 22 times.
✗ Branch 98 → 118 not taken.
22 << ", Invalidations: " << invalidations
1068
2/4
✓ Branch 99 → 100 taken 22 times.
✗ Branch 99 → 118 not taken.
✓ Branch 100 → 101 taken 22 times.
✗ Branch 100 → 118 not taken.
22 << ", Coalesced: " << coalesced
1069
2/4
✓ Branch 101 → 102 taken 22 times.
✗ Branch 101 → 118 not taken.
✓ Branch 102 → 103 taken 22 times.
✗ Branch 102 → 118 not taken.
22 << ", OnDemandCleanups: " << on_demand_cleanups
1070
2/4
✓ Branch 103 → 104 taken 22 times.
✗ Branch 103 → 118 not taken.
✓ Branch 104 → 105 taken 22 times.
✗ Branch 104 → 118 not taken.
22 << ", TotalEntries: " << total_entries;
1071
1072
2/2
✓ Branch 105 → 106 taken 16 times.
✓ Branch 105 → 112 taken 6 times.
22 if (total_queries > 0)
1073 {
1074 16 const double hit_rate_percent = (static_cast<double>(hits) / static_cast<double>(total_queries)) * 100.0;
1075
4/8
✓ Branch 106 → 107 taken 16 times.
✗ Branch 106 → 118 not taken.
✓ Branch 107 → 108 taken 16 times.
✗ Branch 107 → 118 not taken.
✓ Branch 110 → 111 taken 16 times.
✗ Branch 110 → 118 not taken.
✓ Branch 111 → 113 taken 16 times.
✗ Branch 111 → 118 not taken.
16 oss << ", Hit Rate: " << std::fixed << std::setprecision(2) << hit_rate_percent << "%";
1076 }
1077 else
1078 {
1079
1/2
✓ Branch 112 → 113 taken 6 times.
✗ Branch 112 → 118 not taken.
6 oss << ", Hit Rate: N/A (no queries tracked)";
1080 }
1081
1/2
✓ Branch 113 → 114 taken 22 times.
✗ Branch 113 → 118 not taken.
44 return oss.str();
1082 22 }
1083
1084 17 void DetourModKit::Memory::invalidate_range(const void *address, size_t size)
1085 {
1086
4/4
✓ Branch 2 → 3 taken 16 times.
✓ Branch 2 → 4 taken 1 time.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 15 times.
17 if (!address || size == 0)
1087 2 return;
1088
1089 // Construct reader guard BEFORE checking s_cacheInitialized to prevent
1090 // shutdown_cache from destroying data structures between the check and access.
1091 15 ActiveReaderGuard reader_guard;
1092
1093
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 15 times.
15 if (!s_cacheInitialized.load(std::memory_order_acquire))
1094 return;
1095
1096 15 const size_t shard_count = s_shardCount.load(std::memory_order_acquire);
1097
1/2
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 15 times.
15 if (shard_count == 0)
1098 return;
1099
1100 15 const uintptr_t addr_val = reinterpret_cast<uintptr_t>(address);
1101 15 invalidate_range_internal(addr_val, size);
1102
1103 // request_cleanup may trigger on-demand cleanup_expired_entries(force=false)
1104 // which iterates shards without s_cacheStateMutex. Keep s_activeReaders > 0
1105 // so shutdown_cache cannot destroy shards during the cleanup pass.
1106 15 request_cleanup();
1107
1/2
✓ Branch 22 → 23 taken 15 times.
✗ Branch 22 → 25 not taken.
15 }
1108
1109 namespace
1110 {
1111 /**
1112 * @brief Unified permission check for is_readable/is_writable.
1113 * @details Parameterized by permission checker to avoid duplicating the
1114 * cache lookup, VirtualQuery fallback, and range validation logic.
1115 * @param address Starting address of the memory region.
1116 * @param size Number of bytes in the memory region to check.
1117 * @param check_permission Function that validates protection flags.
1118 * @return true if the entire region has the requested permission.
1119 */
1120 100063 bool check_memory_permission(const void *address, size_t size,
1121 bool (*check_permission)(DWORD) noexcept) noexcept
1122 {
1123
2/4
✓ Branch 2 → 3 taken 102266 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 103178 times.
100063 if (!address || size == 0)
1124 return false;
1125
1126 // Construct reader guard BEFORE checking s_cacheInitialized to prevent
1127 // shutdown_cache from destroying data structures between the check and access.
1128 103178 ActiveReaderGuard reader_guard;
1129
1130
2/2
✓ Branch 7 → 8 taken 118 times.
✓ Branch 7 → 24 taken 103285 times.
110510 if (!s_cacheInitialized.load(std::memory_order_acquire))
1131 {
1132 // Cache not initialized -- fall back to direct VirtualQuery
1133 MEMORY_BASIC_INFORMATION mbi;
1134
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 118 times.
118 if (!VirtualQuery(address, &mbi, sizeof(mbi)))
1135 return false;
1136
2/2
✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 13 taken 115 times.
118 if (mbi.State != MEM_COMMIT)
1137 3 return false;
1138
2/2
✓ Branch 14 → 15 taken 3 times.
✓ Branch 14 → 16 taken 112 times.
115 if (!check_permission(mbi.Protect))
1139 3 return false;
1140 112 const uintptr_t query_addr_val = reinterpret_cast<uintptr_t>(address);
1141 112 const uintptr_t region_start = reinterpret_cast<uintptr_t>(mbi.BaseAddress);
1142 112 const uintptr_t query_end = query_addr_val + size;
1143
2/2
✓ Branch 16 → 17 taken 3 times.
✓ Branch 16 → 18 taken 109 times.
112 if (query_end < query_addr_val)
1144 3 return false;
1145
2/4
✓ Branch 18 → 19 taken 109 times.
✗ Branch 18 → 21 not taken.
✓ Branch 19 → 20 taken 109 times.
✗ Branch 19 → 21 not taken.
109 return query_addr_val >= region_start && query_end <= region_start + mbi.RegionSize;
1146 }
1147
1148 // Reader guard already active -- safe to access cache data structures
1149
1150 100223 const size_t shard_count = s_shardCount.load(std::memory_order_acquire);
1151
1/2
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 100223 times.
100223 if (shard_count == 0)
1152 return false;
1153
1154 100223 const uintptr_t query_addr_val = reinterpret_cast<uintptr_t>(address);
1155 100223 const size_t shard_idx = compute_shard_index(query_addr_val, shard_count);
1156 98317 const uint64_t now_ns = current_time_ns();
1157 100317 const uint64_t expiry_ns = static_cast<uint64_t>(s_configuredExpiryMs.load(std::memory_order_acquire)) * 1'000'000ULL;
1158
1159 // Fast path: blocking shared lock for concurrent read access (multiple readers allowed)
1160 {
1161 100317 std::shared_lock<SrwSharedMutex> lock(*s_shardMutexes[shard_idx]);
1162 108597 CachedMemoryRegionInfo *cached_info = find_in_shard(
1163 112546 s_cacheShards[shard_idx],
1164 query_addr_val, size, now_ns, expiry_ns);
1165
2/2
✓ Branch 47 → 48 taken 106199 times.
✓ Branch 47 → 52 taken 143 times.
106342 if (cached_info)
1166 {
1167 s_stats.cacheHits.fetch_add(1, std::memory_order_relaxed);
1168 106199 return check_permission(cached_info->protection);
1169 }
1170
2/2
✓ Branch 54 → 55 taken 143 times.
✓ Branch 54 → 60 taken 108819 times.
103999 }
1171
1172 s_stats.cacheMisses.fetch_add(1, std::memory_order_relaxed);
1173
1174 // Cache miss: call VirtualQuery with stampede coalescing
1175 MEMORY_BASIC_INFORMATION mbi;
1176
1/2
✗ Branch 59 → 61 not taken.
✓ Branch 59 → 62 taken 143 times.
143 if (!query_and_update_cache(shard_idx, address, mbi))
1177 return false;
1178
1179
2/2
✓ Branch 62 → 63 taken 4 times.
✓ Branch 62 → 64 taken 139 times.
143 if (mbi.State != MEM_COMMIT)
1180 4 return false;
1181
1182
2/2
✓ Branch 65 → 66 taken 8 times.
✓ Branch 65 → 67 taken 131 times.
139 if (!check_permission(mbi.Protect))
1183 8 return false;
1184
1185 131 const uintptr_t region_start_addr = reinterpret_cast<uintptr_t>(mbi.BaseAddress);
1186 131 const uintptr_t region_end_addr = region_start_addr + mbi.RegionSize;
1187 131 const uintptr_t query_end_addr = query_addr_val + size;
1188
1189
2/2
✓ Branch 67 → 68 taken 4 times.
✓ Branch 67 → 69 taken 127 times.
131 if (query_end_addr < query_addr_val)
1190 4 return false;
1191
1192
3/4
✓ Branch 69 → 70 taken 127 times.
✗ Branch 69 → 72 not taken.
✓ Branch 70 → 71 taken 126 times.
✓ Branch 70 → 72 taken 1 time.
127 return query_addr_val >= region_start_addr && query_end_addr <= region_end_addr;
1193 109080 }
1194 } // anonymous namespace
1195
1196 99840 bool DetourModKit::Memory::is_readable(const void *address, size_t size)
1197 {
1198 99840 return check_memory_permission(address, size, check_read_permission);
1199 }
1200
1201 3755 bool DetourModKit::Memory::is_writable(void *address, size_t size)
1202 {
1203 3755 return check_memory_permission(address, size, check_write_permission);
1204 }
1205
1206 16 std::expected<void, MemoryError> DetourModKit::Memory::write_bytes(std::byte *targetAddress, const std::byte *sourceBytes, size_t numBytes)
1207 {
1208
1/2
✓ Branch 2 → 3 taken 16 times.
✗ Branch 2 → 92 not taken.
16 auto &logger = Logger::get_instance();
1209
1210
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 9 taken 14 times.
16 if (!targetAddress)
1211 {
1212
1/2
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 66 not taken.
2 logger.error("write_bytes: Target address is null.");
1213 2 return std::unexpected(MemoryError::NullTargetAddress);
1214 }
1215
3/4
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 16 taken 12 times.
✓ Branch 10 → 11 taken 2 times.
✗ Branch 10 → 16 not taken.
14 if (!sourceBytes && numBytes > 0)
1216 {
1217
1/2
✓ Branch 11 → 12 taken 2 times.
✗ Branch 11 → 67 not taken.
2 logger.error("write_bytes: Source bytes pointer is null for non-zero numBytes.");
1218 2 return std::unexpected(MemoryError::NullSourceBytes);
1219 }
1220
2/2
✓ Branch 16 → 17 taken 2 times.
✓ Branch 16 → 21 taken 10 times.
12 if (numBytes == 0)
1221 {
1222
1/2
✓ Branch 17 → 18 taken 2 times.
✗ Branch 17 → 68 not taken.
2 logger.warning("write_bytes: Number of bytes to write is zero. Operation has no effect.");
1223 2 return {};
1224 }
1225
2/2
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 27 taken 9 times.
10 if (numBytes > MAX_WRITE_SIZE)
1226 {
1227
1/2
✓ Branch 22 → 23 taken 1 time.
✗ Branch 22 → 69 not taken.
1 logger.error("write_bytes: Requested size {} exceeds MAX_WRITE_SIZE ({}).", numBytes, MAX_WRITE_SIZE);
1228 1 return std::unexpected(MemoryError::SizeTooLarge);
1229 }
1230
1231 DWORD old_protection_flags;
1232
2/4
✓ Branch 27 → 28 taken 9 times.
✗ Branch 27 → 92 not taken.
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 37 taken 9 times.
9 if (!VirtualProtect(reinterpret_cast<LPVOID>(targetAddress), numBytes, PAGE_EXECUTE_READWRITE, &old_protection_flags))
1233 {
1234 logger.error("write_bytes: VirtualProtect failed to set PAGE_EXECUTE_READWRITE at address {}. Windows Error: {}",
1235 DetourModKit::Format::format_address(reinterpret_cast<uintptr_t>(targetAddress)), GetLastError());
1236 return std::unexpected(MemoryError::ProtectionChangeFailed);
1237 }
1238
1239 9 memcpy(reinterpret_cast<void *>(targetAddress), reinterpret_cast<const void *>(sourceBytes), numBytes);
1240
1241 DWORD temp_old_protect;
1242
2/4
✓ Branch 37 → 38 taken 9 times.
✗ Branch 37 → 92 not taken.
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 49 taken 9 times.
9 if (!VirtualProtect(reinterpret_cast<LPVOID>(targetAddress), numBytes, old_protection_flags, &temp_old_protect))
1243 {
1244 logger.error("write_bytes: VirtualProtect failed to restore original protection ({}) at address {}. Windows Error: {}. Memory may remain writable!",
1245 DetourModKit::Format::format_hex(static_cast<int>(old_protection_flags)),
1246 DetourModKit::Format::format_address(reinterpret_cast<uintptr_t>(targetAddress)), GetLastError());
1247 return std::unexpected(MemoryError::ProtectionRestoreFailed);
1248 }
1249
1250
3/6
✓ Branch 49 → 50 taken 9 times.
✗ Branch 49 → 92 not taken.
✓ Branch 50 → 51 taken 9 times.
✗ Branch 50 → 92 not taken.
✗ Branch 51 → 52 not taken.
✓ Branch 51 → 57 taken 9 times.
9 if (!FlushInstructionCache(GetCurrentProcess(), reinterpret_cast<LPCVOID>(targetAddress), numBytes))
1251 {
1252 logger.warning("write_bytes: FlushInstructionCache failed for address {}. Windows Error: {}",
1253 DetourModKit::Format::format_address(reinterpret_cast<uintptr_t>(targetAddress)), GetLastError());
1254 }
1255
1256 9 Memory::invalidate_range(targetAddress, numBytes);
1257
1258
1/2
✓ Branch 59 → 60 taken 9 times.
✗ Branch 59 → 88 not taken.
9 logger.debug("write_bytes: Successfully wrote {} bytes to address {}.",
1259
1/2
✓ Branch 58 → 59 taken 9 times.
✗ Branch 58 → 91 not taken.
18 numBytes, DetourModKit::Format::format_address(reinterpret_cast<uintptr_t>(targetAddress)));
1260 9 return {};
1261 }
1262
1263 14 Memory::ReadableStatus DetourModKit::Memory::is_readable_nonblocking(const void *address, size_t size)
1264 {
1265
4/4
✓ Branch 2 → 3 taken 13 times.
✓ Branch 2 → 4 taken 1 time.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 12 times.
14 if (!address || size == 0)
1266 2 return ReadableStatus::NotReadable;
1267
1268 12 ActiveReaderGuard reader_guard;
1269
1270
2/2
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 23 taken 10 times.
12 if (!s_cacheInitialized.load(std::memory_order_acquire))
1271 {
1272 // Cache not initialized - fall back to direct VirtualQuery (blocking)
1273 MEMORY_BASIC_INFORMATION mbi;
1274
2/4
✓ Branch 8 → 9 taken 2 times.
✗ Branch 8 → 62 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 2 times.
2 if (!VirtualQuery(address, &mbi, sizeof(mbi)))
1275 return ReadableStatus::NotReadable;
1276
2/2
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 1 time.
2 if (mbi.State != MEM_COMMIT)
1277 1 return ReadableStatus::NotReadable;
1278
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 1 time.
1 if (!check_read_permission(mbi.Protect))
1279 return ReadableStatus::NotReadable;
1280 1 const uintptr_t query_addr_val = reinterpret_cast<uintptr_t>(address);
1281 1 const uintptr_t region_start = reinterpret_cast<uintptr_t>(mbi.BaseAddress);
1282 1 const uintptr_t query_end = query_addr_val + size;
1283
1/2
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 1 time.
1 if (query_end < query_addr_val)
1284 return ReadableStatus::NotReadable;
1285
2/4
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 21 not taken.
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 21 not taken.
1 if (query_addr_val >= region_start && query_end <= region_start + mbi.RegionSize)
1286 1 return ReadableStatus::Readable;
1287 return ReadableStatus::NotReadable;
1288 }
1289
1290 10 const size_t shard_count = s_shardCount.load(std::memory_order_acquire);
1291
1/2
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 10 times.
10 if (shard_count == 0)
1292 return ReadableStatus::Unknown;
1293
1294 10 const uintptr_t query_addr_val = reinterpret_cast<uintptr_t>(address);
1295 10 const size_t shard_idx = compute_shard_index(query_addr_val, shard_count);
1296 10 const uint64_t now_ns = current_time_ns();
1297 10 const uint64_t expiry_ns = static_cast<uint64_t>(s_configuredExpiryMs.load(std::memory_order_acquire)) * 1'000'000ULL;
1298
1299 // Non-blocking: try_lock_shared to avoid stalling latency-sensitive threads
1300 10 std::shared_lock<SrwSharedMutex> lock(*s_shardMutexes[shard_idx], std::try_to_lock);
1301
1/2
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 47 taken 10 times.
10 if (!lock.owns_lock())
1302 return ReadableStatus::Unknown;
1303
1304 10 CachedMemoryRegionInfo *cached_info = find_in_shard(
1305 10 s_cacheShards[shard_idx],
1306 query_addr_val, size, now_ns, expiry_ns);
1307
2/2
✓ Branch 49 → 50 taken 6 times.
✓ Branch 49 → 57 taken 4 times.
10 if (cached_info)
1308 {
1309 s_stats.cacheHits.fetch_add(1, std::memory_order_relaxed);
1310 6 return check_read_permission(cached_info->protection)
1311
2/2
✓ Branch 53 → 54 taken 3 times.
✓ Branch 53 → 55 taken 3 times.
6 ? ReadableStatus::Readable
1312 6 : ReadableStatus::NotReadable;
1313 }
1314
1315 // Cache miss with non-blocking semantics: return Unknown rather than issuing VirtualQuery
1316 4 return ReadableStatus::Unknown;
1317 12 }
1318
1319 11 uintptr_t DetourModKit::Memory::read_ptr_unsafe(uintptr_t base, ptrdiff_t offset) noexcept
1320 {
1321 #ifdef _MSC_VER
1322 __try
1323 {
1324 return *reinterpret_cast<const uintptr_t *>(base + offset);
1325 }
1326 __except ((GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ||
1327 GetExceptionCode() == STATUS_GUARD_PAGE_VIOLATION)
1328 ? EXCEPTION_EXECUTE_HANDLER
1329 : EXCEPTION_CONTINUE_SEARCH)
1330 {
1331 return 0;
1332 }
1333 #else
1334 // MinGW/GCC lacks __try/__except. Probe the cache with a trylock
1335 // to avoid a VirtualQuery syscall when the region is already cached.
1336 // Falls back to VirtualQuery on cache miss or when cache is off.
1337 // ActiveReaderGuard is required to prevent shutdown_cache() from
1338 // destroying shard vectors between our check and access.
1339 11 const auto src = base + static_cast<uintptr_t>(offset);
1340
1341 {
1342 11 ActiveReaderGuard reader_guard;
1343
1344
1/2
✓ Branch 4 → 5 taken 11 times.
✗ Branch 4 → 40 not taken.
11 if (s_cacheInitialized.load(std::memory_order_acquire))
1345 {
1346 11 const size_t shard_count = s_shardCount.load(std::memory_order_acquire);
1347
1/2
✓ Branch 12 → 13 taken 11 times.
✗ Branch 12 → 40 not taken.
11 if (shard_count != 0)
1348 {
1349 11 const size_t shard_idx = compute_shard_index(src, shard_count);
1350 11 std::shared_lock<SrwSharedMutex> lock(*s_shardMutexes[shard_idx], std::try_to_lock);
1351
1/2
✓ Branch 18 → 19 taken 11 times.
✗ Branch 18 → 34 not taken.
11 if (lock.owns_lock())
1352 {
1353 11 const uint64_t now_ns = current_time_ns();
1354 11 const uint64_t expiry_ns = static_cast<uint64_t>(
1355 11 s_configuredExpiryMs.load(std::memory_order_acquire)) * 1'000'000ULL;
1356 11 CachedMemoryRegionInfo *cached = find_in_shard(
1357 11 s_cacheShards[shard_idx],
1358 src, sizeof(uintptr_t), now_ns, expiry_ns);
1359
2/2
✓ Branch 29 → 30 taken 1 time.
✓ Branch 29 → 34 taken 10 times.
11 if (cached)
1360 {
1361
1/2
✓ Branch 31 → 32 taken 1 time.
✗ Branch 31 → 33 not taken.
1 if (check_read_permission(cached->protection))
1362 1 return *reinterpret_cast<const uintptr_t *>(src);
1363 return 0;
1364 }
1365 }
1366
2/2
✓ Branch 36 → 37 taken 10 times.
✓ Branch 36 → 39 taken 1 time.
11 }
1367 }
1368
2/2
✓ Branch 42 → 43 taken 10 times.
✓ Branch 42 → 46 taken 1 time.
11 }
1369
1370 // Cache miss, lock contention, or cache not initialized
1371 MEMORY_BASIC_INFORMATION mbi;
1372
1/2
✗ Branch 45 → 47 not taken.
✓ Branch 45 → 48 taken 10 times.
10 if (!VirtualQuery(reinterpret_cast<const void *>(src), &mbi, sizeof(mbi)))
1373 return 0;
1374
2/2
✓ Branch 48 → 49 taken 3 times.
✓ Branch 48 → 50 taken 7 times.
10 if (mbi.State != MEM_COMMIT)
1375 3 return 0;
1376
2/2
✓ Branch 50 → 51 taken 6 times.
✓ Branch 50 → 52 taken 1 time.
7 if ((mbi.Protect & CachePermissions::READ_PERMISSION_FLAGS) == 0 ||
1377
2/2
✓ Branch 51 → 52 taken 1 time.
✓ Branch 51 → 53 taken 5 times.
6 (mbi.Protect & CachePermissions::NOACCESS_GUARD_FLAGS) != 0)
1378 2 return 0;
1379 // Verify the full read fits within the committed region (overflow-safe)
1380 5 const uintptr_t region_start = reinterpret_cast<uintptr_t>(mbi.BaseAddress);
1381 5 const uintptr_t region_end = region_start + mbi.RegionSize;
1382 5 const uintptr_t read_end = src + sizeof(uintptr_t);
1383
3/6
✓ Branch 53 → 54 taken 5 times.
✗ Branch 53 → 56 not taken.
✓ Branch 54 → 55 taken 5 times.
✗ Branch 54 → 56 not taken.
✗ Branch 55 → 56 not taken.
✓ Branch 55 → 57 taken 5 times.
5 if (read_end < src || src < region_start || read_end > region_end)
1384 return 0;
1385 5 return *reinterpret_cast<const uintptr_t *>(src);
1386 #endif
1387 }
1388