GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 79.9% 666 / 0 / 834
Functions: 97.0% 64 / 0 / 66
Branches: 58.9% 377 / 0 / 640

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, and managing a memory
6 * region cache for performance optimization. The cache uses sharded locks with SRWLOCK for high-concurrency read-heavy
7 * access. Uses monotonic counter-keyed map for O(log n) LRU eviction instead of O(n) scan. In-flight query coalescing
8 * prevents cache stampede under high concurrency. On-demand cleanup handles expired entry removal to avoid polluting
9 * the miss path. Epoch-based reader tracking prevents use-after-free during shutdown.
10 */
11
12 #include "DetourModKit/memory.hpp"
13 #include "DetourModKit/diagnostics.hpp"
14 #include "DetourModKit/format.hpp"
15 #include "DetourModKit/logger.hpp"
16 #include "DetourModKit/srw_shared_mutex.hpp"
17 #include "platform.hpp"
18 #include "memory_internal.hpp"
19
20 #include <windows.h>
21 #if defined(_MSC_VER) && defined(__SANITIZE_ADDRESS__)
22 #include <intrin.h> // __movsb -- ASan-safe copy in the SEH probe read
23 #endif
24 #include <shared_mutex>
25 #include <unordered_map>
26 #include <map>
27 #include <vector>
28 #include <deque>
29 #include <type_traits>
30 #include <chrono>
31 #include <array>
32 #include <atomic>
33 #include <mutex>
34 #include <cstdlib>
35 #include <sstream>
36 #include <iomanip>
37 #include <algorithm>
38 #include <stdexcept>
39 #include <cstddef>
40 #include <thread>
41 #include <condition_variable>
42
43 namespace DetourModKit
44 {
45 using DetourModKit::detail::is_loader_lock_held;
46 using DetourModKit::detail::pin_current_module;
47 using DetourModKit::detail::SrwSharedMutex;
48
49 // Anonymous namespace for internal helpers and storage
50 namespace
51 {
52 // Page-protection flag groups for the cache permission checks. Grouped in a struct rather than a named
53 // namespace so the constants keep internal linkage through the enclosing anonymous namespace, per the .cpp
54 // internal-linkage convention.
55 struct CachePermissions
56 {
57 static constexpr DWORD READ_PERMISSION_FLAGS = PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY |
58 PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE |
59 PAGE_EXECUTE_WRITECOPY;
60 static constexpr DWORD WRITE_PERMISSION_FLAGS =
61 PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
62 static constexpr DWORD NOACCESS_GUARD_FLAGS = PAGE_NOACCESS | PAGE_GUARD;
63 };
64
65 /**
66 * @struct CachedMemoryRegionInfo
67 * @brief Structure to hold cached memory region information.
68 * @details Uses timestamp for thread-safe updates and reduced memory footprint.
69 */
70 struct CachedMemoryRegionInfo
71 {
72 uintptr_t base_address;
73 size_t region_size;
74 DWORD protection;
75 DWORD state;
76 uint64_t timestamp_ns;
77 uint64_t lru_key;
78 bool valid;
79
80 145 CachedMemoryRegionInfo()
81 145 : base_address(0), region_size(0), protection(0), state(0), timestamp_ns(0), lru_key(0), valid(false)
82 {
83 145 }
84 };
85
86 /**
87 * @struct CacheShard
88 * @brief Individual cache shard with O(1) address lookup and O(log n) LRU eviction.
89 * @details Uses unordered_map keyed by region base address for fast lookup. std::map keyed by monotonic counter
90 * for efficient oldest-entry eviction. The per-shard SrwSharedMutex (multiple concurrent readers) and
91 * the in_flight stampede-coalescing flag are stored inline, and the whole struct is aligned to a cache
92 * line, so one shard's lock word and in_flight flag never share a line with another shard's:
93 * concurrent access to different shards stays off each other's cache lines. Inlining the mutex (rather
94 * than a separate heap-allocated lock per shard) is what fixes the cache-line layout, and it makes the
95 * shard non-movable -- SrwSharedMutex and std::atomic are non-movable -- so the shards are allocated
96 * once as a fixed-size array that never relocates, not a resizable std::vector.
97 */
98 #if defined(_MSC_VER)
99 #pragma warning(push)
100 // C4324: CacheShard is intentionally padded to a full cache line by alignas(64) for cache-line hygiene.
101 #pragma warning(disable : 4324)
102 #endif
103 struct alignas(64) CacheShard
104 {
105 // Map from base_address -> CachedMemoryRegionInfo for O(1) lookup by address
106 std::unordered_map<uintptr_t, CachedMemoryRegionInfo> entries;
107 // Map from monotonic counter -> base_address for O(log n) oldest-entry lookup (LRU)
108 // Monotonic counter guarantees insertion-order uniqueness for correct eviction
109 std::map<uint64_t, uintptr_t> lru_index;
110 // Sorted by base address for O(log n) containment lookup. All sorted_ranges access is serialized by the
111 // shard SRW lock (shared for lookups, exclusive for mutation), so iterators never outlive a critical
112 // section. The std::deque is defense-in-depth that prevents wholesale buffer relocation on growth, though
113 // interior insert/erase still invalidates deque iterators per the standard. The hit path is dominated by
114 // the direct entries lookup; the sorted-ranges path is the slow-path fallback for regions larger than one
115 // page, and the deque's extra indirection on random-access iterators is in the noise at the bounded size
116 // (hard_max_per_shard).
117 // {base, base+size}
118 std::deque<std::pair<uintptr_t, uintptr_t>> sorted_ranges;
119 // Per-shard reader/writer lock (shared for lookups, exclusive for mutation). Inline so it lives in the
120 // shard's own cache line(s) rather than behind a separately heap-allocated lock word, and is kept off other
121 // shards' lines by the struct's alignas(64).
122 SrwSharedMutex mtx;
123 // Stampede-coalescing flag: the first thread to CAS it from 0 to 1 becomes the VirtualQuery leader for this
124 // shard and the rest coalesce onto its result. Stored inline (not in a shared global array) so it never
125 // shares a cache line with a neighbouring shard's flag.
126 std::atomic<char> in_flight{0};
127 uint64_t entry_counter{0};
128 size_t capacity;
129 size_t max_capacity;
130
131
2/4
✓ Branch 4 → 5 taken 5657 times.
✗ Branch 4 → 11 not taken.
✓ Branch 7 → 8 taken 5657 times.
✗ Branch 7 → 9 not taken.
5657 CacheShard() : capacity(0), max_capacity(0) { entries.reserve(64); }
132 };
133 #if defined(_MSC_VER)
134 #pragma warning(pop)
135 #endif
136
137 // The sorted-ranges container type is pinned as a deliberate refactor tripwire: with a deque, mutation never
138 // relocates the whole buffer under the lock. This is not iterator stability (deque insert/erase invalidates
139 // iterators); the lock is what excludes concurrent access.
140 static_assert(
141 std::is_same_v<decltype(CacheShard::sorted_ranges), std::deque<std::pair<uintptr_t, uintptr_t>>>,
142 "CacheShard::sorted_ranges is pinned to std::deque so mutation never relocates the whole buffer.");
143
144 /**
145 * @brief Returns current time in nanoseconds.
146 */
147 227923 inline uint64_t current_time_ns() noexcept
148 {
149 233980 return std::chrono::duration_cast<std::chrono::nanoseconds>(
150 458666 std::chrono::steady_clock::now().time_since_epoch())
151 231444 .count();
152 }
153
154 /**
155 * @brief Computes the shard index for a given address.
156 * @param address The address to hash.
157 * @param shard_count Total number of shards.
158 * @return The shard index.
159 * @note Uses golden ratio bit-mixing to spread adjacent addresses across shards.
160 */
161 227821 constexpr inline size_t compute_shard_index(uintptr_t address, size_t shard_count) noexcept
162 {
163 227821 return (static_cast<size_t>((address * 0x9E3779B97F4A7C15ULL) >> 48)) % shard_count;
164 }
165 } // anonymous namespace
166
167 // Internal static variables and helper functions for memory cache. Anonymous namespace ensures internal linkage,
168 // preventing ODR violations if this translation unit's declarations were ever duplicated.
169 namespace
170 {
171 // Fixed-size shard array, allocated once by perform_cache_initialization and never resized: CacheShard owns its
172 // SrwSharedMutex and in_flight atomic inline and so is non-movable, which rules out a resizable std::vector.
173 // null until init, reset on shutdown. The per-shard lock and in_flight flag are reached through
174 // s_cache_shards[i].mtx and s_cache_shards[i].in_flight.
175 std::unique_ptr<CacheShard[]> s_cache_shards;
176 std::atomic<size_t> s_shard_count{0};
177 std::atomic<size_t> s_max_entries_per_shard{0};
178 std::atomic<unsigned int> s_configured_expiry_ms{0};
179 std::atomic<bool> s_cache_initialized{false};
180
181 /// Configured cache-entry expiry converted from milliseconds to nanoseconds.
182 226657 [[nodiscard]] inline uint64_t configured_expiry_ns() noexcept
183 {
184 226841 return static_cast<uint64_t>(s_configured_expiry_ms.load(std::memory_order_acquire)) * 1'000'000ULL;
185 }
186
187 // Global cache state mutex to serialize init/clear/shutdown transitions
188 // Protects against concurrent state changes that could leave vectors in invalid state
189 std::mutex s_cache_state_mutex;
190
191 // Epoch-based reader tracking to prevent use-after-free during shutdown. Readers increment on entry to
192 // is_readable/is_writable and decrement on exit; shutdown_cache waits for the count to reach zero before
193 // destroying data structures. The count is striped across many cache-line-padded counters rather than a single
194 // global atomic: a single counter is a shared-cache-line hotspot that re-serializes readers despite the sharded
195 // SRWLOCKs, so each thread increments its own stripe and shutdown sums them. Distributing the increment does
196 // not weaken the shutdown drain -- see active_reader_total() and the ActiveReaderGuard Dekker note below.
197 constexpr size_t READER_STRIPE_COUNT = 64;
198
199 #if defined(_MSC_VER)
200 #pragma warning(push)
201 // C4324: ReaderStripe is intentionally padded to a full cache line by alignas(64) so stripes never share a line.
202 #pragma warning(disable : 4324)
203 #endif
204 struct alignas(64) ReaderStripe
205 {
206 std::atomic<int32_t> count{0};
207 };
208 #if defined(_MSC_VER)
209 #pragma warning(pop)
210 #endif
211
212 std::array<ReaderStripe, READER_STRIPE_COUNT> s_reader_stripes{};
213
214 /**
215 * @brief Returns this thread's reader stripe, assigned round-robin on first use so concurrent readers spread
216 * across distinct cache lines instead of contending on one counter.
217 */
218 229646 [[nodiscard]] inline size_t reader_stripe_index() noexcept
219 {
220 static std::atomic<size_t> s_next_stripe{0};
221 thread_local const size_t stripe =
222
2/2
✓ Branch 2 → 3 taken 140 times.
✓ Branch 2 → 6 taken 229506 times.
229786 s_next_stripe.fetch_add(1, std::memory_order_relaxed) % READER_STRIPE_COUNT;
223 229646 return stripe;
224 }
225
226 /**
227 * @brief Sum of all reader stripes: the number of readers currently inside an ActiveReaderGuard.
228 * @details shutdown_cache spins on this reaching zero (under seq_cst) after publishing
229 * s_cache_initialized=false,
230 * before freeing shard storage.
231 */
232 434 [[nodiscard]] inline int64_t active_reader_total() noexcept
233 {
234 434 int64_t total = 0;
235
2/2
✓ Branch 11 → 3 taken 27776 times.
✓ Branch 11 → 12 taken 434 times.
28210 for (const ReaderStripe &stripe : s_reader_stripes)
236 {
237 55552 total += stripe.count.load(std::memory_order_seq_cst);
238 }
239 434 return total;
240 }
241
242 /**
243 * @class ActiveReaderGuard
244 * @brief RAII guard that increments this thread's reader stripe on construction and decrements it on
245 * destruction,
246 * ensuring correct pairing on all exit paths.
247 */
248 class ActiveReaderGuard
249 {
250 public:
251 230557 ActiveReaderGuard() noexcept : m_stripe(reader_stripe_index())
252 {
253 // seq_cst (not acq_rel) so this increment and the reader's subsequent seq_cst load of
254 // s_cache_initialized share the single total order that forbids the store-buffering (Dekker) outcome
255 // with shutdown_cache: shutdown stores s_cache_initialized=false then sums every stripe, while a reader
256 // increments its stripe then loads s_cache_initialized. Under seq_cst a reader that observes the cache
257 // live was necessarily counted on its stripe before shutdown reads that stripe, so shutdown cannot free
258 // shard data out from under it -- the per-stripe argument is identical to the single-counter one
259 // because each reader touches exactly one stripe for its whole lifetime. On x86-64 this is the same
260 // lock xadd as acq_rel, so the hot path pays nothing beyond landing on a per-thread cache line instead
261 // of one shared line.
262 248036 s_reader_stripes[m_stripe].count.fetch_add(1, std::memory_order_seq_cst);
263 246240 }
264
265 232686 ~ActiveReaderGuard() noexcept { s_reader_stripes[m_stripe].count.fetch_sub(1, std::memory_order_release); }
266
267 ActiveReaderGuard(const ActiveReaderGuard &) = delete;
268 ActiveReaderGuard &operator=(const ActiveReaderGuard &) = delete;
269
270 private:
271 const size_t m_stripe;
272 };
273
274 // Background cleanup thread. Uses std::thread (not jthread) because these are namespace-scope statics:
275 // jthread's auto-join destructor would run after s_cleanup_cv/s_cleanup_mutex are destroyed (reverse
276 // declaration order), causing UB. Manual join in shutdown_cache() avoids this. DMK_Shutdown() calls
277 // shutdown_cache() which joins this thread before any other cleanup proceeds, ensuring the thread is fully
278 // stopped before static destruction begins.
279 std::atomic<bool> s_cleanup_thread_running{false};
280 std::thread s_cleanup_thread;
281 std::mutex s_cleanup_mutex;
282 std::condition_variable s_cleanup_cv;
283 std::atomic<bool> s_cleanup_requested{false};
284
285 // On-demand cleanup fallback timer (used when background thread is disabled)
286 std::atomic<uint64_t> s_last_cleanup_time_ns{0};
287 // 1 second in nanoseconds
288 constexpr uint64_t CLEANUP_INTERVAL_NS = 1'000'000'000ULL;
289
290 // Always-available cache statistics
291 struct CacheStats
292 {
293 std::atomic<uint64_t> cache_hits{0};
294 std::atomic<uint64_t> cache_misses{0};
295 std::atomic<uint64_t> invalidations{0};
296 std::atomic<uint64_t> coalesced_queries{0};
297 std::atomic<uint64_t> on_demand_cleanups{0};
298 };
299 CacheStats s_stats;
300
301 /**
302 * @brief Checks if a cache entry covers the requested address range and is valid.
303 * @param entry The cache entry to check.
304 * @param address Start address of the query.
305 * @param size Size of the query range.
306 * @param current_time_ns Current timestamp in nanoseconds.
307 * @param expiry_ns Expiry time in nanoseconds.
308 * @return true if the entry is valid and covers the range.
309 */
310 221502 constexpr inline bool is_entry_valid_and_covers(const CachedMemoryRegionInfo &entry, uintptr_t address,
311 size_t size, uint64_t current_time_ns,
312 uint64_t expiry_ns) noexcept
313 {
314
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 221502 times.
221502 if (!entry.valid)
315 return false;
316
317 221502 const uint64_t entry_age = current_time_ns - entry.timestamp_ns;
318
2/2
✓ Branch 4 → 5 taken 6 times.
✓ Branch 4 → 6 taken 221496 times.
221502 if (entry_age > expiry_ns)
319 6 return false;
320
321 221496 const uintptr_t end_address = address + size;
322
2/2
✓ Branch 6 → 7 taken 4 times.
✓ Branch 6 → 8 taken 221492 times.
221496 if (end_address < address)
323 4 return false;
324
325 221492 const uintptr_t entry_end_address = entry.base_address + entry.region_size;
326
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 221492 times.
221492 if (entry_end_address < entry.base_address)
327 return false;
328
329
2/4
✓ Branch 10 → 11 taken 224483 times.
✗ Branch 10 → 13 not taken.
✓ Branch 11 → 12 taken 224809 times.
✗ Branch 11 → 13 not taken.
221492 return address >= entry.base_address && end_address <= entry_end_address;
330 }
331
332 /**
333 * @brief Checks protection flags for read permission.
334 */
335 224841 constexpr inline bool check_read_permission(DWORD protection) noexcept
336 {
337
1/2
✓ Branch 2 → 3 taken 226935 times.
✗ Branch 2 → 5 not taken.
451776 return (protection & CachePermissions::READ_PERMISSION_FLAGS) != 0 &&
338
1/2
✓ Branch 3 → 4 taken 227446 times.
✗ Branch 3 → 5 not taken.
451776 (protection & CachePermissions::NOACCESS_GUARD_FLAGS) == 0;
339 }
340
341 /**
342 * @brief Checks protection flags for write permission.
343 */
344 3786 constexpr inline bool check_write_permission(DWORD protection) noexcept
345 {
346
1/2
✓ Branch 2 → 3 taken 3799 times.
✗ Branch 2 → 5 not taken.
7585 return (protection & CachePermissions::WRITE_PERMISSION_FLAGS) != 0 &&
347
1/2
✓ Branch 3 → 4 taken 3804 times.
✗ Branch 3 → 5 not taken.
7585 (protection & CachePermissions::NOACCESS_GUARD_FLAGS) == 0;
348 }
349
350 /**
351 * @brief Inserts a range into the shard's sorted auxiliary container.
352 * @note Must be called with shard mutex held (exclusive).
353 */
354 145 void insert_sorted_range(CacheShard &shard, uintptr_t base_addr, size_t region_size) noexcept
355 {
356 145 auto range = std::make_pair(base_addr, base_addr + region_size);
357 145 auto pos = std::lower_bound(shard.sorted_ranges.begin(), shard.sorted_ranges.end(), range);
358 145 shard.sorted_ranges.insert(pos, range);
359 145 }
360
361 /**
362 * @brief Removes a range from the shard's sorted auxiliary container.
363 * @note Must be called with shard mutex held (exclusive).
364 */
365 19 void remove_sorted_range(CacheShard &shard, uintptr_t base_addr) noexcept
366 {
367 19 auto it = std::lower_bound(shard.sorted_ranges.begin(), shard.sorted_ranges.end(),
368 38 std::make_pair(base_addr, uintptr_t{0}));
369
3/6
✓ Branch 8 → 9 taken 19 times.
✗ Branch 8 → 12 not taken.
✓ Branch 10 → 11 taken 19 times.
✗ Branch 10 → 12 not taken.
✓ Branch 13 → 14 taken 19 times.
✗ Branch 13 → 17 not taken.
19 if (it != shard.sorted_ranges.end() && it->first == base_addr)
370 19 shard.sorted_ranges.erase(it);
371 19 }
372
373 /**
374 * @brief Finds and validates a cache entry in a shard by scanning for range containment.
375 * @param shard The cache shard to search.
376 * @param address Address to look up.
377 * @param size Size of the query range.
378 * @param current_time_ns Current timestamp in nanoseconds.
379 * @param expiry_ns Expiry time in nanoseconds.
380 * @return Pointer to the matching entry, or nullptr if not found or expired.
381 * @note Must be called with shard mutex held (shared or exclusive).
382 * @note First attempts direct lookup by page-aligned base address for O(1) fast path, then falls back to O(log
383 * n)
384 * binary search via sorted_ranges for addresses within larger regions.
385 */
386 233532 CachedMemoryRegionInfo *find_in_shard(CacheShard &shard, uintptr_t address, size_t size,
387 uint64_t current_time_ns, uint64_t expiry_ns) noexcept
388 {
389 // Fast path: direct lookup by page-aligned base address
390 233532 const uintptr_t base_addr = address & ~static_cast<uintptr_t>(0xFFF);
391 233532 auto it = shard.entries.find(base_addr);
392
2/2
✓ Branch 5 → 6 taken 224945 times.
✓ Branch 5 → 10 taken 1587 times.
226672 if (it != shard.entries.end())
393 {
394 224945 CachedMemoryRegionInfo &entry = it->second;
395
1/2
✓ Branch 8 → 9 taken 223694 times.
✗ Branch 8 → 10 not taken.
223294 if (is_entry_valid_and_covers(entry, address, size, current_time_ns, expiry_ns))
396 {
397 223694 return &entry;
398 }
399 }
400
401 // Slow path: O(log n) containment lookup via sorted ranges. Finds the last range starting at or before the
402 // queried address, then verifies containment and entry validity.
403 271 auto range_it = std::upper_bound(shard.sorted_ranges.begin(), shard.sorted_ranges.end(),
404 1105 std::make_pair(address, UINTPTR_MAX));
405
2/2
✓ Branch 16 → 17 taken 148 times.
✓ Branch 16 → 35 taken 124 times.
273 if (range_it != shard.sorted_ranges.begin())
406 {
407 148 --range_it;
408
5/6
✓ Branch 19 → 20 taken 148 times.
✗ Branch 19 → 23 not taken.
✓ Branch 21 → 22 taken 124 times.
✓ Branch 21 → 23 taken 25 times.
✓ Branch 24 → 25 taken 124 times.
✓ Branch 24 → 35 taken 25 times.
147 if (address >= range_it->first && address < range_it->second)
409 {
410 124 auto entry_it = shard.entries.find(range_it->first);
411
1/2
✓ Branch 29 → 30 taken 123 times.
✗ Branch 29 → 34 not taken.
123 if (entry_it != shard.entries.end())
412 {
413 123 CachedMemoryRegionInfo &entry = entry_it->second;
414
2/2
✓ Branch 32 → 33 taken 117 times.
✓ Branch 32 → 34 taken 5 times.
122 if (is_entry_valid_and_covers(entry, address, size, current_time_ns, expiry_ns))
415 {
416 117 return &entry;
417 }
418 }
419 }
420 }
421
422 154 return nullptr;
423 }
424
425 /**
426 * @brief Evicts the oldest entry from the shard using O(log n) LRU lookup.
427 * @note Must be called with shard mutex held (exclusive).
428 * @return true if an entry was evicted, false if shard is empty.
429 */
430 8 bool evict_oldest_entry(CacheShard &shard) noexcept
431 {
432
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 8 times.
8 if (shard.lru_index.empty())
433 return false;
434
435 8 const auto lru_it = shard.lru_index.begin();
436 8 const uintptr_t oldest_base = lru_it->second;
437
438 8 shard.lru_index.erase(lru_it);
439
440 8 const auto entry_it = shard.entries.find(oldest_base);
441
1/2
✓ Branch 11 → 12 taken 8 times.
✗ Branch 11 → 15 not taken.
8 if (entry_it != shard.entries.end())
442 {
443 8 shard.entries.erase(entry_it);
444 8 remove_sorted_range(shard, oldest_base);
445 8 return true;
446 }
447 return false;
448 }
449
450 /**
451 * @brief Force-evicts entries until shard is at or below max_capacity.
452 * @note Must be called with shard mutex held (exclusive).
453 * @param shard The cache shard to trim.
454 */
455 59 void trim_to_max_capacity(CacheShard &shard) noexcept
456 {
457
2/6
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 9 taken 59 times.
✗ Branch 7 → 8 not taken.
✗ Branch 7 → 9 not taken.
✗ Branch 10 → 3 not taken.
✓ Branch 10 → 11 taken 59 times.
59 while (shard.entries.size() > shard.max_capacity && !shard.lru_index.empty())
458 {
459 evict_oldest_entry(shard);
460 }
461 59 }
462
463 /**
464 * @brief Updates or inserts a cache entry in a specific shard.
465 * @param shard The cache shard to update.
466 * @param mbi Memory basic information from VirtualQuery.
467 * @param current_time_ns Current timestamp in nanoseconds.
468 * @note Must be called with shard mutex held (exclusive).
469 */
470 150 void update_shard_with_region(CacheShard &shard, const MEMORY_BASIC_INFORMATION &mbi,
471 uint64_t current_time_ns) noexcept
472 {
473 150 const uintptr_t base_addr = reinterpret_cast<uintptr_t>(mbi.BaseAddress);
474
475 150 auto it = shard.entries.find(base_addr);
476
2/2
✓ Branch 5 → 6 taken 5 times.
✓ Branch 5 → 22 taken 145 times.
150 if (it != shard.entries.end())
477 {
478 // Remove old entry from LRU index using stored lru_key
479 5 CachedMemoryRegionInfo &old_entry = it->second;
480 5 const auto lru_it = shard.lru_index.find(old_entry.lru_key);
481
3/6
✓ Branch 10 → 11 taken 5 times.
✗ Branch 10 → 14 not taken.
✓ Branch 12 → 13 taken 5 times.
✗ Branch 12 → 14 not taken.
✓ Branch 15 → 16 taken 5 times.
✗ Branch 15 → 17 not taken.
5 if (lru_it != shard.lru_index.end() && lru_it->second == base_addr)
482 {
483 5 shard.lru_index.erase(lru_it);
484 }
485
486 // Update sorted range if region size changed
487
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 20 taken 5 times.
5 if (old_entry.region_size != mbi.RegionSize)
488 {
489 remove_sorted_range(shard, base_addr);
490 insert_sorted_range(shard, base_addr, mbi.RegionSize);
491 }
492
493 // Update existing entry with new monotonic LRU key
494 5 const uint64_t new_lru_key = shard.entry_counter++;
495 5 old_entry.base_address = base_addr;
496 5 old_entry.region_size = mbi.RegionSize;
497 5 old_entry.protection = mbi.Protect;
498 5 old_entry.state = mbi.State;
499 5 old_entry.timestamp_ns = current_time_ns;
500 5 old_entry.lru_key = new_lru_key;
501 5 old_entry.valid = true;
502
503 // Insert new composite key into LRU index
504 5 shard.lru_index.emplace(new_lru_key, base_addr);
505 }
506 else
507 {
508 // Evict oldest if at capacity - O(log n) via map
509
2/2
✓ Branch 23 → 24 taken 8 times.
✓ Branch 23 → 25 taken 137 times.
145 if (shard.entries.size() >= shard.capacity)
510 {
511 8 evict_oldest_entry(shard);
512 }
513
514 // Hard upper bound: trim if exceeding max_capacity
515
1/2
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 145 times.
145 if (shard.entries.size() >= shard.max_capacity)
516 {
517 trim_to_max_capacity(shard);
518 }
519
520 // Generate unique monotonic LRU key
521 145 const uint64_t new_lru_key = shard.entry_counter++;
522
523 145 CachedMemoryRegionInfo new_entry;
524 145 new_entry.base_address = base_addr;
525 145 new_entry.region_size = mbi.RegionSize;
526 145 new_entry.protection = mbi.Protect;
527 145 new_entry.state = mbi.State;
528 145 new_entry.timestamp_ns = current_time_ns;
529 145 new_entry.lru_key = new_lru_key;
530 145 new_entry.valid = true;
531
532 290 shard.entries.insert_or_assign(base_addr, std::move(new_entry));
533 145 shard.lru_index.emplace(new_lru_key, base_addr);
534 145 insert_sorted_range(shard, base_addr, mbi.RegionSize);
535 }
536 150 }
537
538 /**
539 * @brief Removes expired entries from a shard.
540 * @note Must be called with shard mutex held (exclusive).
541 * @return Number of entries removed from this shard.
542 */
543 59 size_t cleanup_expired_entries_in_shard(CacheShard &shard, uint64_t current_time_ns,
544 uint64_t expiry_ns) noexcept
545 {
546 59 size_t removed = 0;
547 59 auto it = shard.entries.begin();
548
2/2
✓ Branch 25 → 4 taken 47 times.
✓ Branch 25 → 26 taken 59 times.
106 while (it != shard.entries.end())
549 {
550 47 const CachedMemoryRegionInfo &entry = it->second;
551 47 const uint64_t entry_age = current_time_ns - entry.timestamp_ns;
552
553
3/4
✓ Branch 5 → 6 taken 47 times.
✗ Branch 5 → 7 not taken.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 21 taken 46 times.
47 if (!entry.valid || entry_age > expiry_ns)
554 {
555 // Remove from LRU index using stored lru_key
556 1 const auto lru_it = shard.lru_index.find(entry.lru_key);
557
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)
558 {
559 1 shard.lru_index.erase(lru_it);
560 }
561
562 1 remove_sorted_range(shard, entry.base_address);
563 1 it = shard.entries.erase(it);
564 1 ++removed;
565 1 }
566 else
567 {
568 46 ++it;
569 }
570 }
571 59 return removed;
572 }
573
574 /**
575 * @brief Performs cleanup of expired cache entries across all shards.
576 * @details Called by the background cleanup thread or on-demand timer.
577 * @param force Force cleanup regardless of timing.
578 */
579 149 void cleanup_expired_entries(bool force) noexcept
580 {
581 // Always hold state mutex to prevent racing with shutdown_cache() which clears the shard vectors. try_lock
582 // for on-demand to avoid blocking the hot path; forced cleanup blocks to guarantee progress.
583 149 std::unique_lock<std::mutex> lock(s_cache_state_mutex, std::defer_lock);
584
1/2
✓ Branch 3 → 4 taken 149 times.
✗ Branch 3 → 5 not taken.
149 if (force)
585 {
586 149 lock.lock();
587 }
588 else if (!lock.try_lock())
589 {
590 return; // Shutdown or forced cleanup in progress, skip
591 }
592
593
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 149 times.
149 if (!s_cache_shards)
594 return;
595
596 149 const size_t shard_count = s_shard_count.load(std::memory_order_acquire);
597
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 149 times.
149 if (shard_count == 0)
598 return;
599
600 149 const uint64_t current_ts = current_time_ns();
601 149 const uint64_t expiry_ns = configured_expiry_ns();
602
603
2/2
✓ Branch 33 → 23 taken 161 times.
✓ Branch 33 → 34 taken 149 times.
310 for (size_t i = 0; i < shard_count; ++i)
604 {
605 161 std::unique_lock<SrwSharedMutex> shard_lock(s_cache_shards[i].mtx, std::try_to_lock);
606
2/2
✓ Branch 26 → 27 taken 59 times.
✓ Branch 26 → 31 taken 102 times.
161 if (shard_lock.owns_lock())
607 {
608 59 cleanup_expired_entries_in_shard(s_cache_shards[i], current_ts, expiry_ns);
609 // Also trim to hard upper bound
610 59 trim_to_max_capacity(s_cache_shards[i]);
611 }
612 161 }
613
1/2
✓ Branch 36 → 37 taken 149 times.
✗ Branch 36 → 39 not taken.
149 }
614
615 /**
616 * @brief Checks if on-demand cleanup should run based on elapsed time.
617 * @return true if cleanup was performed, false otherwise.
618 */
619 bool try_trigger_on_demand_cleanup() noexcept
620 {
621 if (!s_cache_initialized.load(std::memory_order_seq_cst))
622 return false;
623
624 const uint64_t now_ns = current_time_ns();
625 const uint64_t last_cleanup = s_last_cleanup_time_ns.load(std::memory_order_acquire);
626 const uint64_t elapsed_ns = now_ns - last_cleanup;
627
628 if (elapsed_ns >= CLEANUP_INTERVAL_NS)
629 {
630 // Atomically update last cleanup time to prevent multiple threads triggering
631 uint64_t expected = last_cleanup;
632 if (s_last_cleanup_time_ns.compare_exchange_strong(expected, now_ns, std::memory_order_acq_rel))
633 {
634 cleanup_expired_entries(false);
635 s_stats.on_demand_cleanups.fetch_add(1, std::memory_order_relaxed);
636 return true;
637 }
638 }
639 return false;
640 }
641
642 /**
643 * @brief Background cleanup thread function.
644 * @details Runs periodically to clean up expired entries without impacting the miss path.
645 */
646 374 void cleanup_thread_func() noexcept
647 {
648
2/2
✓ Branch 13 → 3 taken 186 times.
✓ Branch 13 → 14 taken 337 times.
523 while (s_cleanup_thread_running.load(std::memory_order_acquire))
649 {
650 {
651 186 std::unique_lock<std::mutex> lock(s_cleanup_mutex);
652 186 s_cleanup_cv.wait_for(lock, std::chrono::seconds(1),
653 325 [&]()
654 {
655
2/2
✓ Branch 3 → 4 taken 175 times.
✓ Branch 3 → 6 taken 150 times.
500 return s_cleanup_requested.load(std::memory_order_acquire) ||
656
2/2
✓ Branch 5 → 6 taken 35 times.
✓ Branch 5 → 7 taken 140 times.
500 !s_cleanup_thread_running.load(std::memory_order_acquire);
657 });
658 186 }
659
660
2/2
✓ Branch 8 → 9 taken 37 times.
✓ Branch 8 → 10 taken 149 times.
186 if (!s_cleanup_thread_running.load(std::memory_order_acquire))
661 37 break;
662
663 // force=true to hold state mutex during vector iteration
664 149 cleanup_expired_entries(true);
665 149 s_cleanup_requested.store(false, std::memory_order_relaxed);
666 }
667 374 }
668
669 /**
670 * @brief Signals the cleanup thread to run or triggers on-demand cleanup.
671 */
672 516 void request_cleanup() noexcept
673 {
674
1/2
✓ Branch 3 → 4 taken 516 times.
✗ Branch 3 → 6 not taken.
516 if (s_cleanup_thread_running.load(std::memory_order_acquire))
675 {
676 516 s_cleanup_requested.store(true, std::memory_order_relaxed);
677 516 s_cleanup_cv.notify_one();
678 }
679 else
680 {
681 // Background thread disabled (MinGW) - use on-demand timer-based cleanup
682 try_trigger_on_demand_cleanup();
683 }
684 516 }
685
686 /**
687 * @brief Evicts every entry in a shard whose region overlaps [address, end_address).
688 * @param shard The cache shard to scan.
689 * @param address Inclusive start of the invalidated range.
690 * @param end_address Exclusive end of the invalidated range (wrap-clamped by the caller).
691 * @return Number of entries evicted.
692 * @note Must be called with the shard mutex held (exclusive).
693 * @note Scans the whole shard rather than probing a single key. An entry is keyed by its VirtualQuery region
694 * base, but it is stored in the shard chosen from the original query address (compute_shard_index mixes
695 * the full address, not the region base), so one region can be cached in several shards under the same
696 * base key. A single key/shard probe therefore cannot locate every covering entry; only a per-shard
697 * containment scan can. The shard is bounded by max_capacity and invalidation runs only after a write, so
698 * this linear scan is never on a read hot path.
699 */
700 636 size_t evict_overlapping_entries_in_shard(CacheShard &shard, uintptr_t address, uintptr_t end_address) noexcept
701 {
702 636 size_t evicted = 0;
703 636 auto it = shard.entries.begin();
704
2/2
✓ Branch 34 → 4 taken 462 times.
✓ Branch 34 → 35 taken 636 times.
1098 while (it != shard.entries.end())
705 {
706 462 const CachedMemoryRegionInfo &entry = it->second;
707 462 const uintptr_t entry_end_address = entry.base_address + entry.region_size;
708 // A VirtualQuery region cannot extend past the address space, but a corrupt cached size could; treat a
709 // wrapped end as covering the top of the space so a poisoned entry is still evicted rather than
710 // skipped.
711 462 const uintptr_t clamped_entry_end =
712
1/2
✓ Branch 5 → 6 taken 462 times.
✗ Branch 5 → 7 not taken.
462 (entry_end_address < entry.base_address) ? UINTPTR_MAX : entry_end_address;
713
4/6
✓ Branch 8 → 9 taken 462 times.
✗ Branch 8 → 12 not taken.
✓ Branch 9 → 10 taken 462 times.
✗ Branch 9 → 12 not taken.
✓ Branch 10 → 11 taken 10 times.
✓ Branch 10 → 12 taken 452 times.
462 const bool overlaps = entry.valid && address < clamped_entry_end && end_address > entry.base_address;
714
2/2
✓ Branch 13 → 14 taken 10 times.
✓ Branch 13 → 30 taken 452 times.
462 if (overlaps)
715 {
716 // Drop the LRU back-reference by the stored key so no tombstone accumulates.
717 10 const auto lru_it = shard.lru_index.find(entry.lru_key);
718
3/6
✓ Branch 17 → 18 taken 10 times.
✗ Branch 17 → 22 not taken.
✓ Branch 20 → 21 taken 10 times.
✗ Branch 20 → 22 not taken.
✓ Branch 23 → 24 taken 10 times.
✗ Branch 23 → 25 not taken.
10 if (lru_it != shard.lru_index.end() && lru_it->second == it->first)
719 {
720 10 shard.lru_index.erase(lru_it);
721 }
722 10 remove_sorted_range(shard, entry.base_address);
723 10 it = shard.entries.erase(it);
724 s_stats.invalidations.fetch_add(1, std::memory_order_relaxed);
725 10 ++evicted;
726 }
727 else
728 {
729 452 ++it;
730 }
731 }
732 636 return evicted;
733 }
734
735 /**
736 * @brief Invalidates cache entries overlapping [address, address + size) across all shards.
737 * @details Every shard is scanned because an entry's storage shard is derived from the original query address,
738 * not its region base, so a covering entry may live in any shard (see
739 * evict_overlapping_entries_in_shard). A bounded try-lock retry per shard keeps the writer that
740 * triggered the invalidation from blocking on a momentarily contended shard.
741 */
742 516 void invalidate_range_internal(uintptr_t address, size_t size) noexcept
743 {
744
3/6
✓ Branch 3 → 4 taken 516 times.
✗ Branch 3 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 516 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 516 times.
516 if (!s_cache_shards || size == 0)
745 return;
746
747 // Guard against address + size wrapping around the address space.
748
2/2
✓ Branch 9 → 10 taken 515 times.
✓ Branch 9 → 11 taken 1 time.
516 const uintptr_t end_address = (address + size < address) ? UINTPTR_MAX : address + size;
749 516 const size_t shard_count = s_shard_count.load(std::memory_order_acquire);
750
751 516 constexpr size_t MAX_INVALIDATION_RETRIES = 3;
752
753 516 size_t skipped_shards = 0;
754
2/2
✓ Branch 39 → 20 taken 684 times.
✓ Branch 39 → 40 taken 516 times.
1200 for (size_t shard_idx = 0; shard_idx < shard_count; ++shard_idx)
755 {
756 684 bool invalidated = false;
757
2/2
✓ Branch 35 → 21 taken 1041 times.
✓ Branch 35 → 36 taken 48 times.
1089 for (size_t retry = 0; retry < MAX_INVALIDATION_RETRIES; ++retry)
758 {
759 1041 std::unique_lock<SrwSharedMutex> lock(s_cache_shards[shard_idx].mtx, std::try_to_lock);
760
2/2
✓ Branch 24 → 25 taken 405 times.
✓ Branch 24 → 28 taken 636 times.
1041 if (!lock.owns_lock())
761 {
762 // Shard is held by another writer - yield and retry rather than block.
763
2/2
✓ Branch 25 → 26 taken 357 times.
✓ Branch 25 → 27 taken 48 times.
405 if (retry < MAX_INVALIDATION_RETRIES - 1)
764 {
765 357 std::this_thread::yield();
766 }
767 405 continue;
768 }
769
770 636 evict_overlapping_entries_in_shard(s_cache_shards[shard_idx], address, end_address);
771 636 invalidated = true;
772 636 break;
773
2/2
✓ Branch 32 → 33 taken 405 times.
✓ Branch 32 → 34 taken 636 times.
1041 }
774
2/2
✓ Branch 36 → 37 taken 48 times.
✓ Branch 36 → 38 taken 636 times.
684 if (!invalidated)
775 {
776 48 ++skipped_shards;
777 }
778 }
779
780
2/2
✓ Branch 40 → 41 taken 48 times.
✓ Branch 40 → 44 taken 468 times.
516 if (skipped_shards > 0)
781 {
782 // A shard that stayed contended across every retry keeps its entries in place, so they answer from the
783 // pre-write protection state until the configured expiry sweeps them. This matters chiefly when the
784 // caller could not restore protection (write_bytes restores on success), so surface it for diagnosis
785 // instead of skipping silently. try_log keeps this noexcept path honest when the level is enabled.
786 48 (void)Logger::get_instance().try_log(
787 LogLevel::Debug,
788 "MemoryCache: invalidate_range left {} contended shard(s) unswept; "
789 "stale entries persist until expiry.",
790 skipped_shards);
791 }
792 }
793
794 /**
795 * @brief Performs one-time cache initialization.
796 */
797 374 bool perform_cache_initialization(size_t cache_size, unsigned int expiry_ms, size_t shard_count)
798 {
799
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 374 times.
374 if (cache_size == 0)
800 cache_size = MIN_CACHE_SIZE;
801
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 374 times.
374 if (shard_count == 0)
802 shard_count = 1;
803
804 374 const size_t entries_per_shard = (cache_size + shard_count - 1) / shard_count;
805 // Hard upper bound: nominal per-shard capacity scaled by the multiplier
806 374 const size_t hard_max_per_shard = entries_per_shard * DEFAULT_MAX_CACHE_SIZE_MULTIPLIER;
807
808 try
809 {
810 // One allocation for the whole fixed-size array; each shard default-constructs its inline mutex and
811 // in_flight flag in place (no per-shard heap allocation, no relocation).
812
1/2
✓ Branch 6 → 7 taken 374 times.
✗ Branch 6 → 54 not taken.
374 s_cache_shards = std::make_unique<CacheShard[]>(shard_count);
813
2/2
✓ Branch 15 → 10 taken 5657 times.
✓ Branch 15 → 16 taken 374 times.
6031 for (size_t i = 0; i < shard_count; ++i)
814 {
815
1/2
✓ Branch 11 → 12 taken 5657 times.
✗ Branch 11 → 55 not taken.
5657 s_cache_shards[i].entries.reserve(hard_max_per_shard);
816 5657 s_cache_shards[i].capacity = entries_per_shard;
817 5657 s_cache_shards[i].max_capacity = hard_max_per_shard;
818 }
819 }
820 catch (const std::bad_alloc &)
821 {
822 Logger::get_instance().error("MemoryCache: Failed to allocate memory for cache shards.");
823 s_cache_shards.reset();
824 // Reset initialization flag so retry can work
825 s_cache_initialized.store(false, std::memory_order_relaxed);
826 return false;
827 }
828
829 374 s_shard_count.store(shard_count, std::memory_order_release);
830 374 s_max_entries_per_shard.store(entries_per_shard, std::memory_order_release);
831 374 s_configured_expiry_ms.store(expiry_ms, std::memory_order_release);
832 374 s_last_cleanup_time_ns.store(current_time_ns(), std::memory_order_release);
833
834
2/4
✓ Branch 49 → 50 taken 374 times.
✗ Branch 49 → 67 not taken.
✓ Branch 50 → 51 taken 374 times.
✗ Branch 50 → 66 not taken.
374 Logger::get_instance().debug(
835 "MemoryCache: Initialized with {} shards ({} entries/shard, {}ms expiry, {} max).", shard_count,
836 entries_per_shard, expiry_ms, hard_max_per_shard);
837
838 374 return true;
839 }
840
841 /**
842 * @brief Performs VirtualQuery and updates cache with coalescing support.
843 * @param shard_idx Index of the shard to update.
844 * @param address Address to query.
845 * @param mbi_out Output buffer for VirtualQuery result.
846 * @return true if VirtualQuery succeeded.
847 */
848 150 bool query_and_update_cache(size_t shard_idx, LPCVOID address, MEMORY_BASIC_INFORMATION &mbi_out) noexcept
849 {
850 150 CacheShard &shard = s_cache_shards[shard_idx];
851
852 // Try to claim in-flight status (stampede coalescing)
853 150 char expected = 0;
854
1/2
✓ Branch 11 → 12 taken 150 times.
✗ Branch 11 → 29 not taken.
300 if (shard.in_flight.compare_exchange_strong(expected, 1, std::memory_order_acq_rel))
855 {
856 // We are the leader - perform VirtualQuery
857 150 const bool result = VirtualQuery(address, &mbi_out, sizeof(mbi_out)) != 0;
858 150 const uint64_t now_ns = current_time_ns();
859
860
1/2
✓ Branch 14 → 15 taken 150 times.
✗ Branch 14 → 20 not taken.
150 if (result)
861 {
862 150 std::unique_lock<SrwSharedMutex> lock(s_cache_shards[shard_idx].mtx);
863 150 update_shard_with_region(shard, mbi_out, now_ns);
864 150 }
865
866 // Release in-flight status
867 150 shard.in_flight.store(0, std::memory_order_release);
868 150 return result;
869 }
870 else
871 {
872 // We are a follower - VirtualQuery already in progress by another thread. Bounded wait to avoid
873 // stalling game threads on render-critical paths.
874 const uint64_t expiry_ns = configured_expiry_ns();
875 constexpr size_t MAX_FOLLOWER_YIELDS = 8;
876
877 for (size_t yield_count = 0; yield_count < MAX_FOLLOWER_YIELDS; ++yield_count)
878 {
879 if (shard.in_flight.load(std::memory_order_acquire) == 0)
880 {
881 // Query completed, check cache
882 const uintptr_t addr_val = reinterpret_cast<uintptr_t>(address);
883 std::shared_lock<SrwSharedMutex> lock(s_cache_shards[shard_idx].mtx);
884 CachedMemoryRegionInfo *cached =
885 find_in_shard(shard, addr_val, 1, current_time_ns(), expiry_ns);
886 if (cached)
887 {
888 s_stats.coalesced_queries.fetch_add(1, std::memory_order_relaxed);
889 // Copy cached info to output for consistency
890 mbi_out.BaseAddress = reinterpret_cast<PVOID>(cached->base_address);
891 mbi_out.RegionSize = cached->region_size;
892 mbi_out.Protect = cached->protection;
893 mbi_out.State = cached->state;
894 return true;
895 }
896 // Cache not populated, break to retry as leader
897 break;
898 }
899
900 // Yield to allow the leader thread to complete
901 std::this_thread::yield();
902 }
903
904 // Retry as leader if follower wait timed out
905 expected = 0;
906 if (shard.in_flight.compare_exchange_strong(expected, 1, std::memory_order_acq_rel))
907 {
908 const bool result = VirtualQuery(address, &mbi_out, sizeof(mbi_out)) != 0;
909 if (result)
910 {
911 std::unique_lock<SrwSharedMutex> lock(s_cache_shards[shard_idx].mtx);
912 const uint64_t now_ns = current_time_ns();
913 update_shard_with_region(shard, mbi_out, now_ns);
914 }
915 shard.in_flight.store(0, std::memory_order_release);
916 return result;
917 }
918
919 // Last resort: just do VirtualQuery without cache update
920 return VirtualQuery(address, &mbi_out, sizeof(mbi_out)) != 0;
921 }
922 }
923
924 } // anonymous namespace
925
926 // Lower bound on a valid usermode pointer on x64 Windows. The null page plus the standard NoAccess guard region
927 // cover [0, 0x10000); rejecting addresses in this range without a memory access keeps stale or sentinel pointers
928 // from raising first-chance exceptions in callers' debuggers. Shared by seh_read_bytes and the MinGW guarded-read
929 // entry point.
930 namespace
931 {
932 inline constexpr uintptr_t SEH_READ_MIN_VALID_ADDR = 0x10000;
933 } // anonymous namespace
934
935 #ifndef _MSC_VER
936 // MinGW/GCC has no __try / __except, so the foreign-memory probes in this file cannot wrap their accesses in
937 // frame-based SEH the way the MSVC paths do. A single process-wide vectored exception handler provides the
938 // equivalent fault guard: each guarded access marks the foreign range it is about to touch in a thread-local slot,
939 // and a fault inside that range is intercepted and turned into a clean failure instead of terminating the host. The
940 // guarded path avoids a per-call VirtualQuery on successful terminal reads/writes and keeps stale cache entries
941 // from authorizing unguarded dereferences after a page is reprotected.
942 namespace
943 {
944 // VirtualQuery-validated read. On x64 it is the fallback used only when the vectored handler could not be
945 // installed; on a 32-bit MinGW build, where the handler's x64 register redirect is unavailable, it is the only
946 // guard. The copy itself goes through ReadProcessMemory so a page that changes after the query fails as an API
947 // result rather than as a user-mode fault.
948 9 bool virtualquery_validated_copy(uintptr_t addr, void *out, size_t bytes) noexcept
949 {
950 9 size_t copied = 0;
951
2/2
✓ Branch 29 → 3 taken 9 times.
✓ Branch 29 → 30 taken 8 times.
17 while (copied < bytes)
952 {
953 9 const uintptr_t cur = addr + copied;
954 9 MEMORY_BASIC_INFORMATION mbi{};
955
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 11 times.
9 if (!VirtualQuery(reinterpret_cast<const void *>(cur), &mbi, sizeof(mbi)))
956 3 return false;
957
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 11 times.
11 if (mbi.State != MEM_COMMIT)
958 return false;
959
2/2
✓ Branch 8 → 9 taken 8 times.
✓ Branch 8 → 10 taken 3 times.
11 if ((mbi.Protect & CachePermissions::READ_PERMISSION_FLAGS) == 0 ||
960
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 8 times.
8 (mbi.Protect & CachePermissions::NOACCESS_GUARD_FLAGS) != 0)
961 3 return false;
962
963 8 const uintptr_t region_start = reinterpret_cast<uintptr_t>(mbi.BaseAddress);
964 8 const uintptr_t region_end = region_start + mbi.RegionSize;
965
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 8 times.
8 if (region_end < region_start)
966 return false;
967
2/4
✓ Branch 13 → 14 taken 8 times.
✗ Branch 13 → 15 not taken.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 8 times.
8 if (cur < region_start || cur >= region_end)
968 return false;
969
970 8 const size_t available = static_cast<size_t>(region_end - cur);
971 8 const size_t remaining = bytes - copied;
972
1/2
✓ Branch 16 → 17 taken 8 times.
✗ Branch 16 → 18 not taken.
8 const size_t to_copy = (remaining < available) ? remaining : available;
973 8 SIZE_T copied_now = 0;
974 8 if (!ReadProcessMemory(GetCurrentProcess(), reinterpret_cast<const void *>(cur),
975
2/4
✓ Branch 21 → 22 taken 8 times.
✗ Branch 21 → 23 not taken.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 8 times.
16 static_cast<std::byte *>(out) + copied, to_copy, &copied_now) ||
976
1/2
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 8 times.
8 copied_now != to_copy)
977 return false;
978 8 copied += to_copy;
979 }
980 8 return true;
981 }
982
983 // VirtualQuery-validated write fallback for MinGW when no frame/vectored fault guard is available. It never
984 // changes page protection: if the current protection is not writable, the write fails closed. The copy itself
985 // goes through WriteProcessMemory so a page that changes after the query fails as an API result rather than as
986 // a user-mode fault.
987 bool virtualquery_validated_write(uintptr_t addr, const void *source, size_t bytes) noexcept
988 {
989 size_t copied = 0;
990 while (copied < bytes)
991 {
992 const uintptr_t cur = addr + copied;
993 MEMORY_BASIC_INFORMATION mbi{};
994 if (!VirtualQuery(reinterpret_cast<const void *>(cur), &mbi, sizeof(mbi)))
995 return false;
996 if (mbi.State != MEM_COMMIT)
997 return false;
998 if ((mbi.Protect & CachePermissions::WRITE_PERMISSION_FLAGS) == 0 ||
999 (mbi.Protect & CachePermissions::NOACCESS_GUARD_FLAGS) != 0)
1000 return false;
1001
1002 const uintptr_t region_start = reinterpret_cast<uintptr_t>(mbi.BaseAddress);
1003 const uintptr_t region_end = region_start + mbi.RegionSize;
1004 if (region_end < region_start)
1005 return false;
1006 if (cur < region_start || cur >= region_end)
1007 return false;
1008
1009 const size_t available = static_cast<size_t>(region_end - cur);
1010 const size_t remaining = bytes - copied;
1011 const size_t to_copy = (remaining < available) ? remaining : available;
1012 SIZE_T copied_now = 0;
1013 if (!WriteProcessMemory(GetCurrentProcess(), reinterpret_cast<void *>(cur),
1014 static_cast<const std::byte *>(source) + copied, to_copy, &copied_now) ||
1015 copied_now != to_copy)
1016 return false;
1017 copied += to_copy;
1018 }
1019 return true;
1020 }
1021
1022 #if defined(_WIN64)
1023 // Per-access record describing the foreign range and the recovery snapshot. It lives on the guarded access's
1024 // own stack (one per nested-free synchronous access) and is published to the thread's Win32 TLS slot for the
1025 // duration of the access; the handler reads that slot. A Win32 TLS slot is used rather than a thread_local /
1026 // __thread because mingw lowers thread-locals to __emutls_get_address, which allocates and locks on a thread's
1027 // first access -- forbidden in the exception-dispatch context the handler runs in. TlsGetValue is documented to
1028 // be callable there: it reads the thread's TLS array with no allocation and no lock, and returns null on any
1029 // thread that has not armed an access.
1030 struct VehAccessGuard
1031 {
1032 void *env[5]; // __builtin_setjmp buffer; the recovery stub longjmps through it (5 words is the GCC ABI)
1033 uintptr_t guard_lo; // first byte of the foreign range being accessed
1034 uintptr_t guard_hi; // one past the last byte of that range
1035 };
1036
1037 std::mutex s_veh_mutex;
1038 std::atomic<void *> s_veh_handle{nullptr};
1039 // Process-lifetime TLS index, allocated once and reused across install/remove cycles (never freed so a removal
1040 // can never invalidate an index a concurrent access still holds). The handler reads it with an acquire load.
1041 std::atomic<DWORD> s_veh_tls_index{TLS_OUT_OF_INDEXES};
1042 // Count of accesses currently on the guarded path. shutdown_cache drains this to zero before unregistering the
1043 // handler so a fault can never arrive after the handler is gone.
1044 std::atomic<int> s_veh_in_flight{0};
1045
1046 // Recovery stub the handler redirects a faulting thread into. __builtin_longjmp restores the stack pointer,
1047 // frame pointer and program counter from the snapshot the matching __builtin_setjmp captured before the access,
1048 // so recovery is correct no matter which frame the fault occurred in and without invoking SEH unwinding (which
1049 // can abort when unwound from a vectored-handler-resumed context). The handler passes the buffer in the
1050 // first-argument register so the stub touches no thread-local itself. noinline gives it a stable address for
1051 // the handler to target.
1052 8564 [[noreturn]] __attribute__((noinline)) void veh_perform_longjmp(void *env) noexcept
1053 {
1054 8564 __builtin_longjmp(env, 1);
1055 }
1056
1057 // Vectored exception handler, installed at the front of the list. It claims a fault only when the current
1058 // thread is inside a guarded access (the TLS slot is non-null), the code is one a guarded probe owns
1059 // (is_guarded_read_fault -- the same set the MSVC __except filters use), the record carries a faulting address,
1060 // and that address falls inside the foreign range being accessed. Every other fault is passed straight through,
1061 // so a host software exception reusing one of these codes, or any code running outside a guarded access, still
1062 // reaches the host's own handlers unchanged. On a claimed fault it redirects the thread into
1063 // veh_perform_longjmp, which reports the access as failed.
1064 8552 LONG NTAPI dmk_veh_read_handler(PEXCEPTION_POINTERS info) noexcept
1065 {
1066 8537 const DWORD slot = s_veh_tls_index.load(std::memory_order_acquire);
1067
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 8537 times.
8537 if (slot == TLS_OUT_OF_INDEXES)
1068 return EXCEPTION_CONTINUE_SEARCH;
1069
1070 8537 auto *const guard = static_cast<VehAccessGuard *>(TlsGetValue(slot));
1071
2/2
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 14 taken 8538 times.
8539 if (guard == nullptr)
1072 1 return EXCEPTION_CONTINUE_SEARCH;
1073
1074 8538 const EXCEPTION_RECORD *const record = info->ExceptionRecord;
1075
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 8456 times.
8538 if (!Memory::detail::is_guarded_read_fault(record->ExceptionCode))
1076 return EXCEPTION_CONTINUE_SEARCH;
1077
1078 // A guarded foreign access can only fault with a hardware access-violation, guard-page or in-page-error,
1079 // all of which carry the faulting data address in ExceptionInformation[1]. Refuse to claim a record without
1080 // it: that rules out a host RaiseException reusing one of these NTSTATUS codes with no address from being
1081 // hijacked out of the host's control flow while a guarded access happens to be in flight on this thread.
1082
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 8456 times.
8456 if (record->NumberParameters < 2)
1083 return EXCEPTION_CONTINUE_SEARCH;
1084
1085 // Confine the claim to the foreign range this operation explicitly armed. A bug that faults outside the
1086 // range reaches the host's handlers instead of being silently swallowed.
1087 8456 const uintptr_t fault_address = static_cast<uintptr_t>(record->ExceptionInformation[1]);
1088
2/4
✓ Branch 19 → 20 taken 8470 times.
✗ Branch 19 → 21 not taken.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 8472 times.
8456 if (fault_address < guard->guard_lo || fault_address >= guard->guard_hi)
1089 return EXCEPTION_CONTINUE_SEARCH;
1090
1091 // Disarm before resuming so a fault inside the longjmp stub would pass through rather than recurse.
1092 8472 TlsSetValue(slot, nullptr);
1093
1094 // Resume the faulting thread in veh_perform_longjmp(env): instruction pointer to the stub, setjmp buffer in
1095 // the Win64 first-argument register (RCX). The stub is entered by an injected RIP change, not a CALL, so
1096 // the fault-point RSP is not the ABI-required call alignment; pre-align it (the stub reloads RSP from the
1097 // snapshot anyway, so this only protects the stub's own prologue) to keep the resume robust against future
1098 // codegen that might touch an aligned stack slot before the reload.
1099 8454 CONTEXT *const ctx = info->ContextRecord;
1100 8454 ctx->Rsp = (ctx->Rsp & ~static_cast<DWORD64>(15)) - 8;
1101 8454 ctx->Rcx = reinterpret_cast<DWORD64>(&guard->env);
1102 8454 ctx->Rip = reinterpret_cast<DWORD64>(&veh_perform_longjmp);
1103 8454 return EXCEPTION_CONTINUE_EXECUTION;
1104 }
1105
1106 // Install the handler once, lazily. Re-installable across an init/shutdown cycle: shutdown_cache removes it and
1107 // clears the handle, so a later guarded access or re-init installs a fresh one. Best-effort: if either TlsAlloc
1108 // or AddVectoredExceptionHandler fails (realistic only under exhaustion) the handle stays null; byte-copy
1109 // guards fall back to VirtualQuery plus ReadProcessMemory / WriteProcessMemory, while in-place region guards
1110 // fail closed without touching the foreign range.
1111 504825 void ensure_veh_installed() noexcept
1112 {
1113
2/2
✓ Branch 3 → 4 taken 504241 times.
✓ Branch 3 → 5 taken 693 times.
504825 if (s_veh_handle.load(std::memory_order_acquire) != nullptr)
1114 504266 return;
1115
1116 // Names the step that left the guard unavailable, read after the lock is released so the best-effort Logger
1117 // call never runs while s_veh_mutex is held (the deferred-logging discipline: Logger takes its own locks).
1118 693 const char *unavailable_reason = nullptr;
1119 {
1120 693 std::lock_guard<std::mutex> lock(s_veh_mutex);
1121
2/2
✓ Branch 7 → 8 taken 25 times.
✓ Branch 7 → 9 taken 698 times.
723 if (s_veh_handle.load(std::memory_order_relaxed) != nullptr)
1122 25 return;
1123
2/2
✓ Branch 16 → 17 taken 638 times.
✓ Branch 16 → 29 taken 60 times.
698 if (s_veh_tls_index.load(std::memory_order_relaxed) == TLS_OUT_OF_INDEXES)
1124 {
1125 638 const DWORD slot = TlsAlloc();
1126
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 638 times.
638 if (slot == TLS_OUT_OF_INDEXES)
1127 unavailable_reason =
1128 "TLS slot exhausted"; // cannot guard; access paths take their fail-closed fallback
1129 else
1130 s_veh_tls_index.store(slot, std::memory_order_release);
1131 }
1132
1/2
✓ Branch 29 → 30 taken 698 times.
✗ Branch 29 → 34 not taken.
698 if (unavailable_reason == nullptr)
1133 {
1134 // First in the list (FirstHandler = 1): a guarded access always resolves through this handler
1135 // before any consumer VEH or frame-based SEH. Every fault that is not this thread's own in-flight
1136 // guarded access is passed through with EXCEPTION_CONTINUE_SEARCH, so being first never starves the
1137 // host's handlers.
1138 698 void *const handle = AddVectoredExceptionHandler(1, dmk_veh_read_handler);
1139 698 s_veh_handle.store(handle, std::memory_order_release);
1140
1/2
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 698 times.
698 if (handle == nullptr)
1141 unavailable_reason = "AddVectoredExceptionHandler failed";
1142 }
1143
2/2
✓ Branch 36 → 37 taken 698 times.
✓ Branch 36 → 39 taken 25 times.
723 }
1144
1145 // Surface a guard-unavailable condition once. The guarded access paths stay correct by failing closed or
1146 // using kernel-mediated byte copies, so this is observability only; a single latched emission keeps a
1147 // retried path -- which re-enters here while the handle stays null -- from spamming the sink. Realistic
1148 // only under resource exhaustion.
1149
1/2
✗ Branch 38 → 40 not taken.
✓ Branch 38 → 45 taken 698 times.
698 if (unavailable_reason != nullptr)
1150 {
1151 static std::atomic<bool> s_veh_unavailable_warned{false};
1152 if (!s_veh_unavailable_warned.exchange(true, std::memory_order_relaxed))
1153 {
1154 (void)Logger::get_instance().try_log(
1155 LogLevel::Warning,
1156 "MemoryCache: vectored access guard unavailable ({}); guarded byte copies use kernel-mediated "
1157 "fallbacks and guarded region scans fail closed.",
1158 unavailable_reason);
1159 }
1160 }
1161 }
1162
1163 402 void remove_veh_handler() noexcept
1164 {
1165 402 std::lock_guard<std::mutex> lock(s_veh_mutex);
1166 402 void *const handle = s_veh_handle.load(std::memory_order_relaxed);
1167
2/2
✓ Branch 4 → 5 taken 28 times.
✓ Branch 4 → 6 taken 374 times.
402 if (handle == nullptr)
1168 28 return;
1169 // Stop new guarded accesses from taking the handler path, then wait for any access already committed to it
1170 // to finish before unregistering, so a fault cannot arrive after the handler is gone. The seq_cst store
1171 // pairs with the seq_cst fetch_add / handle-load in the guarded access helpers (the Dekker protocol the
1172 // cache reader epoch uses): an access that observed a live handle is necessarily counted in s_veh_in_flight
1173 // before this store is observed.
1174 374 s_veh_handle.store(nullptr, std::memory_order_seq_cst);
1175 374 int spins = 0;
1176
2/2
✓ Branch 21 → 8 taken 131 times.
✓ Branch 21 → 22 taken 374 times.
879 while (s_veh_in_flight.load(std::memory_order_seq_cst) > 0)
1177 {
1178
1/2
✓ Branch 8 → 9 taken 131 times.
✗ Branch 8 → 10 not taken.
131 if (spins < 4096)
1179 131 std::this_thread::yield();
1180 else
1181 std::this_thread::sleep_for(std::chrono::microseconds(100));
1182 131 ++spins;
1183 }
1184 374 RemoveVectoredExceptionHandler(handle);
1185
2/2
✓ Branch 25 → 26 taken 374 times.
✓ Branch 25 → 28 taken 28 times.
402 }
1186
1187 // Copy [src, src + len) into out under the vectored handler. The copy is a single rep movsb emitted as raw
1188 // inline assembly: inline asm is invisible to AddressSanitizer, which instruments only compiler-emitted loads,
1189 // so this deliberate cross-region read cannot raise an ASan false positive (the same reason the MSVC probe
1190 // copies via the
1191 // __movsb intrinsic). __builtin_setjmp records the recovery point; the guard is then published to this thread's
1192 // TLS slot so a read fault is claimable, and the handler longjmps back here so the setjmp expression returns
1193 // non-zero and the function reports failure. noinline keeps the read and its setjmp anchor in one
1194 // self-contained frame.
1195 485808 __attribute__((noinline)) bool veh_guarded_copy(void *out, const void *src, size_t len) noexcept
1196 {
1197 485897 const DWORD slot = s_veh_tls_index.load(std::memory_order_acquire);
1198 VehAccessGuard guard;
1199 485897 guard.guard_lo = reinterpret_cast<uintptr_t>(src);
1200 485897 guard.guard_hi = guard.guard_lo + len;
1201
1202
2/2
✓ Branch 13 → 14 taken 4581 times.
✓ Branch 13 → 15 taken 485778 times.
490478 if (__builtin_setjmp(guard.env) != 0)
1203 {
1204 // Reached only when the handler longjmped here after swallowing a read fault; the handler already
1205 // cleared the TLS slot. Report the failure.
1206 4581 return false;
1207 }
1208
1209 // Arm after the setjmp captures env and before the read, so a fault in the rep movsb below is claimable
1210 // while a fault before the buffer is valid is not. TlsSetValue writes the thread's TLS array with no
1211 // allocation.
1212 485778 TlsSetValue(slot, &guard);
1213
1214 485848 void *dst = out;
1215 485848 const void *cur = src;
1216 485848 size_t n = len;
1217 485848 __asm__ __volatile__("rep movsb" : "+D"(dst), "+S"(cur), "+c"(n) : : "memory");
1218
1219 481649 TlsSetValue(slot, nullptr);
1220 481981 return true;
1221 }
1222
1223 // Runs fn(ctx) with the vectored handler armed over [lo, hi). Used for in-place accesses where the operation is
1224 // not the simple rep movsb read that veh_guarded_copy performs. __builtin_setjmp records the recovery point,
1225 // the guard is published to this thread's TLS slot so a fault inside [lo, hi) is claimable, and the handler
1226 // longjmps back here so the setjmp expression returns non-zero and the function reports failure. fn must touch
1227 // only [lo, hi); a fault outside that range (e.g. a bug in fn) is not claimed and reaches the host's handlers.
1228 // fn is abandoned on a fault via __builtin_longjmp without running destructors, so it must hold no resources
1229 // that need unwinding -- the scanner sweep and write wrapper use only POD locals. noinline keeps the setjmp
1230 // anchor and the fn call in one self-contained frame.
1231 18580 __attribute__((noinline)) bool veh_guarded_region(uintptr_t lo, uintptr_t hi, void (*fn)(void *) noexcept,
1232 void *ctx) noexcept
1233 {
1234 18580 const DWORD slot = s_veh_tls_index.load(std::memory_order_acquire);
1235 VehAccessGuard guard;
1236 18580 guard.guard_lo = lo;
1237 18580 guard.guard_hi = hi;
1238
1239
2/2
✓ Branch 13 → 14 taken 3972 times.
✓ Branch 13 → 15 taken 18580 times.
22552 if (__builtin_setjmp(guard.env) != 0)
1240 {
1241 // Reached only when the handler longjmped here after swallowing a fault inside [lo, hi); the
1242 // handler already cleared the TLS slot. Report the failure.
1243 3972 return false;
1244 }
1245
1246 // Arm after the setjmp captures env and before invoking fn, so a fault in the guarded access is claimable
1247 // while a fault before the buffer is valid is not.
1248 18580 TlsSetValue(slot, &guard);
1249 18580 fn(ctx);
1250 14608 TlsSetValue(slot, nullptr);
1251 14608 return true;
1252 }
1253
1254 // Single entry point the MinGW read paths share. Rejects a wrapping or low source range first (a wrapped
1255 // addr + bytes would invert the handler's [guard_lo, guard_hi) check and let a real fault escape the guard);
1256 // mirrors seh_read_bytes' own precheck so read_ptr_unsafe, which has no precheck of its own, is covered too.
1257 // Counts the read in the drain epoch around the path decision so a read on the guarded path is always visible
1258 // to remove_veh_handler's drain. Falls back to a VirtualQuery plus ReadProcessMemory copy when the handler is
1259 // unavailable.
1260 485874 bool veh_read_bytes(uintptr_t addr, void *out, size_t bytes) noexcept
1261 {
1262
2/4
✓ Branch 2 → 3 taken 485913 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 485932 times.
485874 if (addr < SEH_READ_MIN_VALID_ADDR || addr + bytes < addr)
1263 return false;
1264
1265 485932 ensure_veh_installed();
1266
1267 s_veh_in_flight.fetch_add(1, std::memory_order_seq_cst);
1268 485938 const bool armed = s_veh_handle.load(std::memory_order_seq_cst) != nullptr;
1269
1/2
✓ Branch 9 → 10 taken 486181 times.
✗ Branch 9 → 12 not taken.
486163 const bool ok = armed ? veh_guarded_copy(out, reinterpret_cast<const void *>(addr), bytes)
1270 486629 : virtualquery_validated_copy(addr, out, bytes);
1271 s_veh_in_flight.fetch_sub(1, std::memory_order_release);
1272 486658 return ok;
1273 }
1274
1275 9 bool veh_write_bytes(uintptr_t addr, const void *source, size_t bytes) noexcept
1276 {
1277
2/4
✓ Branch 2 → 3 taken 9 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 9 times.
9 if (addr < SEH_READ_MIN_VALID_ADDR || addr + bytes < addr)
1278 return false;
1279
1280 struct WriteContext
1281 {
1282 uintptr_t dst;
1283 const void *src;
1284 size_t bytes;
1285 9 } ctx{addr, source, bytes};
1286
1287 9 const auto do_write = [](void *opaque) noexcept -> void
1288 {
1289 9 auto *context = static_cast<WriteContext *>(opaque);
1290 9 std::memcpy(reinterpret_cast<void *>(context->dst), context->src, context->bytes);
1291 9 };
1292
1293 9 ensure_veh_installed();
1294
1295 s_veh_in_flight.fetch_add(1, std::memory_order_seq_cst);
1296 9 const bool armed = s_veh_handle.load(std::memory_order_seq_cst) != nullptr;
1297
1/2
✓ Branch 9 → 10 taken 9 times.
✗ Branch 9 → 13 not taken.
9 const bool ok = armed ? veh_guarded_region(addr, addr + bytes, do_write, &ctx)
1298 9 : virtualquery_validated_write(addr, source, bytes);
1299 s_veh_in_flight.fetch_sub(1, std::memory_order_release);
1300 9 return ok;
1301 }
1302 #else // !_WIN64
1303 // 32-bit MinGW: the handler's recovery redirect rewrites x64 CONTEXT registers (Rcx/Rip) and the longjmp buffer
1304 // is x64-sized, so the vectored guard is x64-only. A guarded read here validates every region with VirtualQuery
1305 // before copying through ReadProcessMemory instead.
1306 bool veh_read_bytes(uintptr_t addr, void *out, size_t bytes) noexcept
1307 {
1308 if (addr < SEH_READ_MIN_VALID_ADDR || addr + bytes < addr)
1309 return false;
1310 return virtualquery_validated_copy(addr, out, bytes);
1311 }
1312
1313 bool veh_write_bytes(uintptr_t addr, const void *source, size_t bytes) noexcept
1314 {
1315 if (addr < SEH_READ_MIN_VALID_ADDR || addr + bytes < addr)
1316 return false;
1317 return virtualquery_validated_write(addr, source, bytes);
1318 }
1319 #endif // _WIN64
1320 } // anonymous namespace
1321 #endif // !_MSC_VER
1322
1323 #if !defined(_MSC_VER) && defined(_WIN64)
1324 18571 bool DetourModKit::Memory::detail::run_guarded_region(uintptr_t lo, uintptr_t hi, void (*fn)(void *) noexcept,
1325 void *ctx) noexcept
1326 {
1327 // An empty or wrapping range has nothing to guard; run the access directly. A wrapped [lo, hi) would also
1328 // invert the handler's range check, the same input veh_read_bytes rejects up front.
1329
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 5 taken 18571 times.
18571 if (hi <= lo)
1330 {
1331 fn(ctx);
1332 return true;
1333 }
1334
1335 18571 ensure_veh_installed();
1336
1337 // Count the call in the drain epoch around the path decision (mirroring veh_read_bytes) so a guarded access is
1338 // always visible to remove_veh_handler's drain.
1339 s_veh_in_flight.fetch_add(1, std::memory_order_seq_cst);
1340 18571 const bool armed = s_veh_handle.load(std::memory_order_seq_cst) != nullptr;
1341 18571 bool completed = true;
1342
1/2
✓ Branch 9 → 10 taken 18571 times.
✗ Branch 9 → 11 not taken.
18571 if (armed)
1343 {
1344 18571 completed = veh_guarded_region(lo, hi, fn, ctx);
1345 }
1346 else
1347 {
1348 // Handler unavailable (install failed, realistic only under resource exhaustion): do not run an in-place
1349 // scan unguarded. The caller treats false as a skipped/faulted region and fails uniqueness-sensitive work
1350 // closed.
1351 completed = false;
1352 }
1353 s_veh_in_flight.fetch_sub(1, std::memory_order_release);
1354 18571 return completed;
1355 }
1356 #endif // !_MSC_VER && _WIN64
1357
1358 376 bool DetourModKit::Memory::init_cache(size_t cache_size, unsigned int expiry_ms, size_t shard_count)
1359 {
1360 // Hold state mutex to prevent concurrent clear_cache or shutdown_cache
1361 // This serializes init/clear/shutdown transitions to ensure vectors are not accessed while being resized or
1362 // cleared
1363
1/2
✓ Branch 2 → 3 taken 376 times.
✗ Branch 2 → 39 not taken.
376 std::lock_guard<std::mutex> state_lock(s_cache_state_mutex);
1364
1365 // Fast path: already initialized
1366
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 374 times.
376 if (s_cache_initialized.load(std::memory_order_seq_cst))
1367 2 return true;
1368
1369 // Try to initialize
1370 374 bool expected = false;
1371
1/2
✓ Branch 7 → 8 taken 374 times.
✗ Branch 7 → 22 not taken.
374 if (s_cache_initialized.compare_exchange_strong(expected, true, std::memory_order_acq_rel))
1372 {
1373
2/4
✓ Branch 8 → 9 taken 374 times.
✗ Branch 8 → 37 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 374 times.
374 if (!perform_cache_initialization(cache_size, expiry_ms, shard_count))
1374 {
1375 // Initialization failed - s_cache_initialized already reset to false in perform_cache_initialization
1376 return false;
1377 }
1378
1379 #if !defined(_MSC_VER) && defined(_WIN64)
1380 // MinGW has no frame-based SEH; install the process-wide vectored fault handler the seh_read paths rely on
1381 // so a guarded read never has to fall back to a per-call VirtualQuery. Best-effort and independent of cache
1382 // success: a failed install only costs the guarded reads their VirtualQuery fallback.
1383 374 ensure_veh_installed();
1384 #endif
1385
1386 // Try to start background cleanup thread (may fail silently on MinGW)
1387 374 s_cleanup_thread_running.store(true, std::memory_order_release);
1388 try
1389 {
1390
1/2
✓ Branch 13 → 14 taken 374 times.
✗ Branch 13 → 26 not taken.
374 s_cleanup_thread = std::thread(cleanup_thread_func);
1391 }
1392 catch (const std::system_error &)
1393 {
1394 // Background thread creation failed (MinGW pthreads issue) - use on-demand cleanup
1395 s_cleanup_thread_running.store(false, std::memory_order_release);
1396 Logger::get_instance().debug(
1397 "MemoryCache: Background cleanup thread unavailable, using on-demand cleanup.");
1398 }
1399
1400 // Register atexit handler as a last-resort safety net in case the consumer forgets to call shutdown_cache()
1401 // / DMK_Shutdown(). Prevents std::terminate from the joinable std::thread destructor. The handler detects
1402 // loader-lock context (FreeLibrary) and skips the thread join to avoid deadlock.
1403 static bool atexit_registered = false;
1404
2/2
✓ Branch 17 → 18 taken 314 times.
✓ Branch 17 → 21 taken 60 times.
374 if (!atexit_registered)
1405 {
1406 314 std::atexit(
1407 628 []()
1408 {
1409
2/2
✓ Branch 3 → 4 taken 8 times.
✓ Branch 3 → 17 taken 306 times.
314 if (s_cache_initialized.load(std::memory_order_seq_cst))
1410 {
1411
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 16 taken 8 times.
8 if (is_loader_lock_held())
1412 {
1413 #if !defined(_MSC_VER) && defined(_WIN64)
1414 // Remove the vectored fault handler before the module can be unloaded: a list removal
1415 // is safe under loader lock, and leaving the handler registered against
1416 // soon-to-be-freed code is worse than the pinned-thread leak below (it would actively
1417 // claim faults on a defunct subsystem). shutdown_cache is not reached on this branch,
1418 // so do it here.
1419 remove_veh_handler();
1420 #endif
1421 // Under loader lock (FreeLibrary path): pin the module so code pages remain valid for
1422 // the detached thread, then signal it to stop and detach.
1423 s_cleanup_thread_running.store(false, std::memory_order_release);
1424 s_cleanup_cv.notify_one();
1425 if (s_cleanup_thread.joinable())
1426 {
1427 pin_current_module();
1428 s_cleanup_thread.detach();
1429 DetourModKit::Diagnostics::record_intentional_leak(
1430 DetourModKit::Diagnostics::LeakSubsystem::MemoryCache);
1431 }
1432 s_cache_initialized.store(false, std::memory_order_release);
1433 return;
1434 }
1435 8 Memory::shutdown_cache();
1436 }
1437 });
1438 314 atexit_registered = true;
1439 }
1440
1441 374 return true;
1442 }
1443
1444 // Another thread initialized while we were waiting
1445 return true;
1446 376 }
1447
1448 27 void DetourModKit::Memory::clear_cache() noexcept
1449 {
1450 // Hold state mutex to serialize with shutdown and cleanup thread
1451 27 std::lock_guard<std::mutex> state_lock(s_cache_state_mutex);
1452
1453
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 27 times.
27 if (!s_cache_initialized.load(std::memory_order_seq_cst))
1454 return;
1455
1456 27 const size_t shard_count = s_shard_count.load(std::memory_order_acquire);
1457
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 27 times.
27 if (shard_count == 0)
1458 return;
1459
1460 // Acquire exclusive lock on each shard and clear entries. Uses blocking lock to guarantee all entries are
1461 // cleared. The background cleanup thread uses try_to_lock on shard mutexes, so it will skip shards we hold
1462 // without deadlocking.
1463
2/2
✓ Branch 35 → 16 taken 408 times.
✓ Branch 35 → 36 taken 27 times.
435 for (size_t i = 0; i < shard_count; ++i)
1464 {
1465 408 std::unique_lock<SrwSharedMutex> shard_lock(s_cache_shards[i].mtx);
1466 408 s_cache_shards[i].entries.clear();
1467 408 s_cache_shards[i].lru_index.clear();
1468 408 s_cache_shards[i].sorted_ranges.clear();
1469 408 s_cache_shards[i].in_flight.store(0, std::memory_order_relaxed);
1470 408 }
1471
1472 s_stats.cache_hits.store(0, std::memory_order_relaxed);
1473 s_stats.cache_misses.store(0, std::memory_order_relaxed);
1474 s_stats.invalidations.store(0, std::memory_order_relaxed);
1475 s_stats.coalesced_queries.store(0, std::memory_order_relaxed);
1476 s_stats.on_demand_cleanups.store(0, std::memory_order_relaxed);
1477
1478 27 s_last_cleanup_time_ns.store(current_time_ns(), std::memory_order_relaxed);
1479
1480 // Diagnostic-only tail. The method is noexcept (a cache teardown must not throw), and Logger::debug can format
1481 // and flush a sink, so fail closed: a sink or format failure drops the line rather than escaping clear_cache.
1482 try
1483 {
1484
2/4
✓ Branch 85 → 86 taken 27 times.
✗ Branch 85 → 96 not taken.
✓ Branch 86 → 87 taken 27 times.
✗ Branch 86 → 95 not taken.
27 Logger::get_instance().debug("MemoryCache: All entries cleared.");
1485 }
1486 catch (...)
1487 {
1488 }
1489
1/2
✓ Branch 90 → 91 taken 27 times.
✗ Branch 90 → 93 not taken.
27 }
1490
1491 402 void DetourModKit::Memory::shutdown_cache() noexcept
1492 {
1493 // Signal and join cleanup thread BEFORE acquiring state mutex. The cleanup thread acquires s_cache_state_mutex
1494 // in cleanup_expired_entries(force=true), so joining while holding the state mutex would deadlock.
1495 402 s_cleanup_thread_running.store(false, std::memory_order_release);
1496 402 s_cleanup_cv.notify_one();
1497
1498
2/2
✓ Branch 5 → 6 taken 374 times.
✓ Branch 5 → 12 taken 28 times.
402 if (s_cleanup_thread.joinable())
1499 {
1500
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 11 taken 374 times.
374 if (is_loader_lock_held())
1501 {
1502 // Under loader lock (DllMain / FreeLibrary): thread join would deadlock because the cleanup thread
1503 // cannot exit while the loader lock is held. Pin the module so code and static data remain valid, then
1504 // detach. The thread will observe the stop flag and exit on its own.
1505 pin_current_module();
1506 s_cleanup_thread.detach();
1507 DetourModKit::Diagnostics::record_intentional_leak(
1508 DetourModKit::Diagnostics::LeakSubsystem::MemoryCache);
1509 }
1510 else
1511 {
1512 374 s_cleanup_thread.join();
1513 }
1514 }
1515
1516 // Acquire state mutex to serialize with clear_cache and protect data teardown
1517 402 std::lock_guard<std::mutex> state_lock(s_cache_state_mutex);
1518
1519 // Mark as not initialized and zero shard count so new readers do not enter the critical section. The
1520 // s_cache_initialized store is seq_cst (not just
1521 // release): it pairs with the reader's seq_cst load in the ActiveReaderGuard
1522 // protocol so the store-buffering (Dekker) race against the reader-count load below is forbidden by the single
1523 // total order. s_shard_count stays release because readers only read it after passing the seq_cst
1524 // s_cache_initialized gate.
1525 // Capture the shard count before zeroing it: the destroy loop below needs the array length, and s_cache_shards
1526 // is a fixed-size array (no size() of its own). The array is never resized between here and the reset() below.
1527 402 const size_t shard_count = s_shard_count.load(std::memory_order_acquire);
1528
1529 402 s_cache_initialized.store(false, std::memory_order_seq_cst);
1530 s_shard_count.store(0, std::memory_order_release);
1531
1532 // Wait for in-flight readers to finish before destroying data structures. Readers increment their reader stripe
1533 // on entry and decrement on exit; active_reader_total() sums the stripes. ActiveReaderGuard is RAII so readers
1534 // always decrement; this loop is bounded by the maximum time a single cache lookup can take. Escalate from
1535 // yield to sleep to avoid burning CPU if a reader is preempted by the OS scheduler.
1536 402 constexpr int yield_spins = 4096;
1537 402 int spins = 0;
1538
2/2
✓ Branch 37 → 30 taken 32 times.
✓ Branch 37 → 38 taken 402 times.
434 while (active_reader_total() > 0)
1539 {
1540
1/2
✓ Branch 30 → 31 taken 32 times.
✗ Branch 30 → 32 not taken.
32 if (spins < yield_spins)
1541 {
1542 32 std::this_thread::yield();
1543 }
1544 else
1545 {
1546 std::this_thread::sleep_for(std::chrono::microseconds(100));
1547 }
1548 32 ++spins;
1549 }
1550
1551 // All readers have exited - safe to destroy data structures
1552
2/2
✓ Branch 49 → 39 taken 5657 times.
✓ Branch 49 → 50 taken 402 times.
6059 for (size_t i = 0; i < shard_count; ++i)
1553 {
1554 5657 std::unique_lock<SrwSharedMutex> shard_lock(s_cache_shards[i].mtx);
1555 5657 s_cache_shards[i].entries.clear();
1556 5657 s_cache_shards[i].lru_index.clear();
1557 5657 s_cache_shards[i].sorted_ranges.clear();
1558 5657 }
1559
1560 402 s_cache_shards.reset();
1561
1562 // Reset all stats and config so a subsequent init_cache starts from a clean state
1563 s_stats.cache_hits.store(0, std::memory_order_relaxed);
1564 s_stats.cache_misses.store(0, std::memory_order_relaxed);
1565 s_stats.invalidations.store(0, std::memory_order_relaxed);
1566 s_stats.coalesced_queries.store(0, std::memory_order_relaxed);
1567 s_stats.on_demand_cleanups.store(0, std::memory_order_relaxed);
1568 s_last_cleanup_time_ns.store(0, std::memory_order_relaxed);
1569 s_configured_expiry_ms.store(0, std::memory_order_relaxed);
1570 s_max_entries_per_shard.store(0, std::memory_order_relaxed);
1571 402 s_cleanup_requested.store(false, std::memory_order_relaxed);
1572
1573 #if !defined(_MSC_VER) && defined(_WIN64)
1574 // Remove the vectored fault handler installed for the MinGW seh_read paths so it cannot dangle into freed code
1575 // if the DMK module is unloaded after teardown. remove_veh_handler drains guarded reads still on the handler
1576 // path before unregistering, so an in-flight read cannot fault into a missing handler. Idempotent: a no-op when
1577 // the handler was never installed (cache used without any guarded read) or already removed. A later guarded
1578 // read re-installs it.
1579 402 remove_veh_handler();
1580 #endif
1581
1582 // Diagnostic-only tail. shutdown_cache() is noexcept, so a sink or format failure must not escape teardown.
1583 try
1584 {
1585
2/4
✓ Branch 117 → 118 taken 402 times.
✗ Branch 117 → 123 not taken.
✓ Branch 118 → 119 taken 402 times.
✗ Branch 118 → 122 not taken.
402 Logger::get_instance().debug("MemoryCache: Shutdown complete.");
1586 }
1587 catch (...)
1588 {
1589 }
1590 402 }
1591
1592 26 DetourModKit::Memory::MemoryStats DetourModKit::Memory::get_memory_stats() noexcept
1593 {
1594 26 MemoryStats stats{};
1595 26 stats.hits = s_stats.cache_hits.load(std::memory_order_relaxed);
1596 26 stats.misses = s_stats.cache_misses.load(std::memory_order_relaxed);
1597 26 stats.invalidations = s_stats.invalidations.load(std::memory_order_relaxed);
1598 26 stats.coalesced_queries = s_stats.coalesced_queries.load(std::memory_order_relaxed);
1599 26 stats.on_demand_cleanups = s_stats.on_demand_cleanups.load(std::memory_order_relaxed);
1600
1601 26 stats.shard_count = s_shard_count.load(std::memory_order_acquire);
1602 26 stats.max_entries_per_shard = s_max_entries_per_shard.load(std::memory_order_acquire);
1603 26 stats.expiry_ms = s_configured_expiry_ms.load(std::memory_order_acquire);
1604
1605 // Sum live entries and hard-max capacity across shards under the reader guard. A non-zero shard count implies
1606 // s_cache_shards is allocated (init publishes the count after the array) and the reader guard keeps it alive;
1607 // when the count is zero the loop simply does not run.
1608 26 size_t total_hard_max = 0;
1609 {
1610 26 ActiveReaderGuard reader_guard;
1611 26 const size_t active_shard_count = s_shard_count.load(std::memory_order_acquire);
1612
2/2
✓ Branch 74 → 67 taken 179 times.
✓ Branch 74 → 75 taken 26 times.
205 for (size_t i = 0; i < active_shard_count; ++i)
1613 {
1614 179 std::shared_lock<SrwSharedMutex> shard_lock(s_cache_shards[i].mtx);
1615 179 stats.total_entries += s_cache_shards[i].entries.size();
1616 179 total_hard_max += s_cache_shards[i].max_capacity;
1617 179 }
1618 26 }
1619
1/2
✓ Branch 76 → 77 taken 26 times.
✗ Branch 76 → 78 not taken.
26 stats.hard_max_per_shard = (stats.shard_count > 0) ? total_hard_max / stats.shard_count : 0;
1620
1621 26 const uint64_t total_queries = stats.hits + stats.misses;
1622 26 stats.hit_rate_percent =
1623
2/2
✓ Branch 79 → 80 taken 16 times.
✓ Branch 79 → 81 taken 10 times.
26 (total_queries > 0) ? (static_cast<double>(stats.hits) / static_cast<double>(total_queries)) * 100.0 : -1.0;
1624 26 return stats;
1625 }
1626
1627 24 std::string DetourModKit::Memory::get_cache_stats()
1628 {
1629 24 const MemoryStats s = get_memory_stats();
1630
1631
1/2
✓ Branch 3 → 4 taken 24 times.
✗ Branch 3 → 40 not taken.
24 std::ostringstream oss;
1632
4/8
✓ Branch 4 → 5 taken 24 times.
✗ Branch 4 → 38 not taken.
✓ Branch 5 → 6 taken 24 times.
✗ Branch 5 → 38 not taken.
✓ Branch 6 → 7 taken 24 times.
✗ Branch 6 → 38 not taken.
✓ Branch 7 → 8 taken 24 times.
✗ Branch 7 → 38 not taken.
24 oss << "MemoryCache Stats (Shards: " << s.shard_count << ", Entries/Shard: " << s.max_entries_per_shard
1633
4/8
✓ Branch 8 → 9 taken 24 times.
✗ Branch 8 → 38 not taken.
✓ Branch 9 → 10 taken 24 times.
✗ Branch 9 → 38 not taken.
✓ Branch 10 → 11 taken 24 times.
✗ Branch 10 → 38 not taken.
✓ Branch 11 → 12 taken 24 times.
✗ Branch 11 → 38 not taken.
24 << ", HardMax/Shard: " << s.hard_max_per_shard << ", Expiry: " << s.expiry_ms << "ms) - "
1634
7/14
✓ Branch 12 → 13 taken 24 times.
✗ Branch 12 → 38 not taken.
✓ Branch 13 → 14 taken 24 times.
✗ Branch 13 → 38 not taken.
✓ Branch 14 → 15 taken 24 times.
✗ Branch 14 → 38 not taken.
✓ Branch 15 → 16 taken 24 times.
✗ Branch 15 → 38 not taken.
✓ Branch 16 → 17 taken 24 times.
✗ Branch 16 → 38 not taken.
✓ Branch 17 → 18 taken 24 times.
✗ Branch 17 → 38 not taken.
✓ Branch 18 → 19 taken 24 times.
✗ Branch 18 → 38 not taken.
24 << "Hits: " << s.hits << ", Misses: " << s.misses << ", Invalidations: " << s.invalidations
1635
4/8
✓ Branch 19 → 20 taken 24 times.
✗ Branch 19 → 38 not taken.
✓ Branch 20 → 21 taken 24 times.
✗ Branch 20 → 38 not taken.
✓ Branch 21 → 22 taken 24 times.
✗ Branch 21 → 38 not taken.
✓ Branch 22 → 23 taken 24 times.
✗ Branch 22 → 38 not taken.
24 << ", Coalesced: " << s.coalesced_queries << ", OnDemandCleanups: " << s.on_demand_cleanups
1636
2/4
✓ Branch 23 → 24 taken 24 times.
✗ Branch 23 → 38 not taken.
✓ Branch 24 → 25 taken 24 times.
✗ Branch 24 → 38 not taken.
24 << ", TotalEntries: " << s.total_entries;
1637
1638
2/2
✓ Branch 25 → 26 taken 16 times.
✓ Branch 25 → 32 taken 8 times.
24 if (s.hit_rate_percent >= 0.0)
1639 {
1640
4/8
✓ Branch 26 → 27 taken 16 times.
✗ Branch 26 → 38 not taken.
✓ Branch 27 → 28 taken 16 times.
✗ Branch 27 → 38 not taken.
✓ Branch 30 → 31 taken 16 times.
✗ Branch 30 → 38 not taken.
✓ Branch 31 → 33 taken 16 times.
✗ Branch 31 → 38 not taken.
16 oss << ", Hit Rate: " << std::fixed << std::setprecision(2) << s.hit_rate_percent << "%";
1641 }
1642 else
1643 {
1644
1/2
✓ Branch 32 → 33 taken 8 times.
✗ Branch 32 → 38 not taken.
8 oss << ", Hit Rate: N/A (no queries tracked)";
1645 }
1646
1/2
✓ Branch 33 → 34 taken 24 times.
✗ Branch 33 → 38 not taken.
48 return oss.str();
1647 24 }
1648
1649 518 void DetourModKit::Memory::invalidate_range(const void *address, size_t size)
1650 {
1651
4/4
✓ Branch 2 → 3 taken 517 times.
✓ Branch 2 → 4 taken 1 time.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 516 times.
518 if (!address || size == 0)
1652 2 return;
1653
1654 // Construct reader guard BEFORE checking s_cache_initialized to prevent shutdown_cache from destroying data
1655 // structures between the check and access.
1656 516 ActiveReaderGuard reader_guard;
1657
1658
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 516 times.
516 if (!s_cache_initialized.load(std::memory_order_seq_cst))
1659 return;
1660
1661 516 const size_t shard_count = s_shard_count.load(std::memory_order_acquire);
1662
1/2
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 516 times.
516 if (shard_count == 0)
1663 return;
1664
1665 516 const uintptr_t addr_val = reinterpret_cast<uintptr_t>(address);
1666 516 invalidate_range_internal(addr_val, size);
1667
1668 // request_cleanup may trigger on-demand cleanup_expired_entries(force=false) which iterates shards without
1669 // s_cache_state_mutex. The ActiveReaderGuard held by this call keeps a reader stripe non-zero so shutdown_cache
1670 // cannot destroy shards during the cleanup pass.
1671 516 request_cleanup();
1672
1/2
✓ Branch 22 → 23 taken 516 times.
✗ Branch 22 → 25 not taken.
516 }
1673
1674 namespace
1675 {
1676 /**
1677 * @brief Unified permission check for is_readable/is_writable.
1678 * @details Parameterized by permission checker to avoid duplicating the cache lookup, VirtualQuery fallback,
1679 * and
1680 * range validation logic.
1681 * @param address Starting address of the memory region.
1682 * @param size Number of bytes in the memory region to check.
1683 * @param check_permission Function that validates protection flags.
1684 * @return true if the entire region has the requested permission.
1685 */
1686 230988 bool check_memory_permission(const void *address, size_t size,
1687 bool (*check_permission)(DWORD) noexcept) noexcept
1688 {
1689
2/4
✓ Branch 2 → 3 taken 233540 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 234058 times.
230988 if (!address || size == 0)
1690 return false;
1691
1692 // Construct reader guard BEFORE loading s_cache_initialized to prevent shutdown_cache from destroying data
1693 // structures between the check and access.
1694 234058 ActiveReaderGuard reader_guard;
1695
1696 // Snapshot cache readiness once, under the guard. The guard's seq_cst increment is ordered before this
1697 // seq_cst load of s_cache_initialized (the Dekker protocol that lets shutdown_cache drain readers safely).
1698 // shard_count is loaded only when initialized, so the cache path below operates on a single consistent
1699 // value.
1700 240505 const bool cache_initialized = s_cache_initialized.load(std::memory_order_seq_cst);
1701
2/2
✓ Branch 7 → 8 taken 230295 times.
✓ Branch 7 → 16 taken 371 times.
460873 const size_t shard_count = cache_initialized ? s_shard_count.load(std::memory_order_acquire) : 0;
1702
1703 // Fall back to a direct VirtualQuery whenever the cache is unavailable: never initialized, observed in the
1704 // brief init publication window where s_cache_initialized is already true but s_shard_count is still 0
1705 // (init_cache publishes the flag before perform_cache_initialization stores the count), or a concurrent
1706 // shutdown that has already zeroed the count. Treating shard_count == 0 as "use the direct query" --
1707 // matching read_ptr_unsafe and invalidate_range -- avoids wrongly reporting a readable region as
1708 // non-readable during that window.
1709
2/2
✓ Branch 17 → 18 taken 371 times.
✓ Branch 17 → 34 taken 230207 times.
230578 if (shard_count == 0)
1710 {
1711 371 MEMORY_BASIC_INFORMATION mbi{};
1712
1/2
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 371 times.
371 if (!VirtualQuery(address, &mbi, sizeof(mbi)))
1713 return false;
1714
2/2
✓ Branch 21 → 22 taken 2 times.
✓ Branch 21 → 23 taken 369 times.
371 if (mbi.State != MEM_COMMIT)
1715 2 return false;
1716
2/2
✓ Branch 24 → 25 taken 3 times.
✓ Branch 24 → 26 taken 366 times.
369 if (!check_permission(mbi.Protect))
1717 3 return false;
1718 366 const uintptr_t query_addr_val = reinterpret_cast<uintptr_t>(address);
1719 366 const uintptr_t region_start = reinterpret_cast<uintptr_t>(mbi.BaseAddress);
1720 366 const uintptr_t query_end = query_addr_val + size;
1721
2/2
✓ Branch 26 → 27 taken 3 times.
✓ Branch 26 → 28 taken 363 times.
366 if (query_end < query_addr_val)
1722 3 return false;
1723
2/4
✓ Branch 28 → 29 taken 363 times.
✗ Branch 28 → 31 not taken.
✓ Branch 29 → 30 taken 363 times.
✗ Branch 29 → 31 not taken.
363 return query_addr_val >= region_start && query_end <= region_start + mbi.RegionSize;
1724 }
1725
1726 // Cache is live and shard_count is non-zero -- the reader guard keeps the shard vectors alive for the
1727 // access.
1728 230207 const uintptr_t query_addr_val = reinterpret_cast<uintptr_t>(address);
1729 230207 const size_t shard_idx = compute_shard_index(query_addr_val, shard_count);
1730 227517 const uint64_t now_ns = current_time_ns();
1731 228729 const uint64_t expiry_ns = configured_expiry_ns();
1732
1733 // Fast path: blocking shared lock for concurrent read access (multiple readers allowed)
1734 {
1735 223362 std::shared_lock<SrwSharedMutex> lock(s_cache_shards[shard_idx].mtx);
1736 CachedMemoryRegionInfo *cached_info =
1737 246290 find_in_shard(s_cache_shards[shard_idx], query_addr_val, size, now_ns, expiry_ns);
1738
2/2
✓ Branch 41 → 42 taken 230347 times.
✓ Branch 41 → 46 taken 150 times.
230497 if (cached_info)
1739 {
1740 s_stats.cache_hits.fetch_add(1, std::memory_order_relaxed);
1741 230347 return check_permission(cached_info->protection);
1742 }
1743
2/2
✓ Branch 48 → 49 taken 150 times.
✓ Branch 48 → 54 taken 234655 times.
228893 }
1744
1745 s_stats.cache_misses.fetch_add(1, std::memory_order_relaxed);
1746
1747 // Cache miss: call VirtualQuery with stampede coalescing
1748 150 MEMORY_BASIC_INFORMATION mbi{};
1749
1/2
✗ Branch 53 → 55 not taken.
✓ Branch 53 → 56 taken 150 times.
150 if (!query_and_update_cache(shard_idx, address, mbi))
1750 return false;
1751
1752
2/2
✓ Branch 56 → 57 taken 4 times.
✓ Branch 56 → 58 taken 146 times.
150 if (mbi.State != MEM_COMMIT)
1753 4 return false;
1754
1755
2/2
✓ Branch 59 → 60 taken 9 times.
✓ Branch 59 → 61 taken 137 times.
146 if (!check_permission(mbi.Protect))
1756 9 return false;
1757
1758 137 const uintptr_t region_start_addr = reinterpret_cast<uintptr_t>(mbi.BaseAddress);
1759 137 const uintptr_t region_end_addr = region_start_addr + mbi.RegionSize;
1760 137 const uintptr_t query_end_addr = query_addr_val + size;
1761
1762
2/2
✓ Branch 61 → 62 taken 4 times.
✓ Branch 61 → 63 taken 133 times.
137 if (query_end_addr < query_addr_val)
1763 4 return false;
1764
1765
3/4
✓ Branch 63 → 64 taken 133 times.
✗ Branch 63 → 66 not taken.
✓ Branch 64 → 65 taken 132 times.
✓ Branch 64 → 66 taken 1 time.
133 return query_addr_val >= region_start_addr && query_end_addr <= region_end_addr;
1766 235176 }
1767 } // anonymous namespace
1768
1769 227790 bool DetourModKit::Memory::is_readable(const void *address, size_t size)
1770 {
1771 227790 return check_memory_permission(address, size, check_read_permission);
1772 }
1773
1774 3809 bool DetourModKit::Memory::is_writable(void *address, size_t size)
1775 {
1776 3809 return check_memory_permission(address, size, check_write_permission);
1777 }
1778
1779 16 std::expected<void, MemoryError> DetourModKit::Memory::write_bytes(std::byte *target_address,
1780 const std::byte *source_bytes, size_t num_bytes)
1781 {
1782
1/2
✓ Branch 2 → 3 taken 16 times.
✗ Branch 2 → 97 not taken.
16 auto &logger = Logger::get_instance();
1783
1784
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 9 taken 14 times.
16 if (!target_address)
1785 {
1786
1/2
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 71 not taken.
2 logger.error("write_bytes: Target address is null.");
1787 2 return std::unexpected(MemoryError::NullTargetAddress);
1788 }
1789
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 (!source_bytes && num_bytes > 0)
1790 {
1791
1/2
✓ Branch 11 → 12 taken 2 times.
✗ Branch 11 → 72 not taken.
2 logger.error("write_bytes: Source bytes pointer is null for non-zero num_bytes.");
1792 2 return std::unexpected(MemoryError::NullSourceBytes);
1793 }
1794
2/2
✓ Branch 16 → 17 taken 2 times.
✓ Branch 16 → 21 taken 10 times.
12 if (num_bytes == 0)
1795 {
1796
1/2
✓ Branch 17 → 18 taken 2 times.
✗ Branch 17 → 73 not taken.
2 logger.warning("write_bytes: Number of bytes to write is zero. Operation has no effect.");
1797 2 return {};
1798 }
1799
2/2
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 27 taken 9 times.
10 if (num_bytes > MAX_WRITE_SIZE)
1800 {
1801
1/2
✓ Branch 22 → 23 taken 1 time.
✗ Branch 22 → 74 not taken.
1 logger.error("write_bytes: Requested size {} exceeds MAX_WRITE_SIZE ({}).", num_bytes, MAX_WRITE_SIZE);
1802 1 return std::unexpected(MemoryError::SizeTooLarge);
1803 }
1804
1805 DWORD old_protection_flags;
1806
2/4
✓ Branch 27 → 28 taken 9 times.
✗ Branch 27 → 97 not taken.
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 37 taken 9 times.
9 if (!VirtualProtect(reinterpret_cast<LPVOID>(target_address), num_bytes, PAGE_EXECUTE_READWRITE,
1807 &old_protection_flags))
1808 {
1809 logger.error(
1810 "write_bytes: VirtualProtect failed to set PAGE_EXECUTE_READWRITE at address {}. Windows Error: {}",
1811 DetourModKit::Format::format_address(reinterpret_cast<uintptr_t>(target_address)), GetLastError());
1812 return std::unexpected(MemoryError::ProtectionChangeFailed);
1813 }
1814
1815 9 memcpy(reinterpret_cast<void *>(target_address), reinterpret_cast<const void *>(source_bytes), num_bytes);
1816
1817 // The bytes are now modified. The instruction-cache flush and the DMK cache-range invalidation must run on
1818 // every path from here -- they are promised unconditionally, and skipping them after a write would leave stale
1819 // cached state for bytes that have already changed. Restore the original page protection first so its outcome
1820 // can be reported, but keep that out of an early return: the cleanup below is a single unconditional block, so
1821 // the restore-failure path runs exactly the same maintenance as the success path by construction and cannot
1822 // diverge.
1823 DWORD temp_old_protect;
1824
1/2
✓ Branch 37 → 38 taken 9 times.
✗ Branch 37 → 97 not taken.
9 const bool restore_succeeded = VirtualProtect(reinterpret_cast<LPVOID>(target_address), num_bytes,
1825 9 old_protection_flags, &temp_old_protect) != FALSE;
1826
1/2
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 46 taken 9 times.
9 if (!restore_succeeded)
1827 {
1828 logger.error("write_bytes: VirtualProtect failed to restore original protection ({}) at address {}. "
1829 "Windows Error: {}. Memory may remain writable!",
1830 DetourModKit::Format::format_hex(static_cast<int>(old_protection_flags)),
1831 DetourModKit::Format::format_address(reinterpret_cast<uintptr_t>(target_address)),
1832 GetLastError());
1833 }
1834
1835
3/6
✓ Branch 46 → 47 taken 9 times.
✗ Branch 46 → 97 not taken.
✓ Branch 47 → 48 taken 9 times.
✗ Branch 47 → 97 not taken.
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 54 taken 9 times.
9 if (!FlushInstructionCache(GetCurrentProcess(), reinterpret_cast<LPCVOID>(target_address), num_bytes))
1836 {
1837 logger.warning("write_bytes: FlushInstructionCache failed for address {}. Windows Error: {}",
1838 DetourModKit::Format::format_address(reinterpret_cast<uintptr_t>(target_address)),
1839 GetLastError());
1840 }
1841
1842 9 Memory::invalidate_range(target_address, num_bytes);
1843
1844 // Surface the restore failure only after cache maintenance has run.
1845
1/2
✗ Branch 55 → 56 not taken.
✓ Branch 55 → 60 taken 9 times.
9 if (!restore_succeeded)
1846 {
1847 return std::unexpected(MemoryError::ProtectionRestoreFailed);
1848 }
1849
1850 // Gate the success log behind the level check so the format_address call is skipped entirely when Debug is off
1851 // (the shipping default). write_bytes is setup/patch-only, but a consumer that does drive it frequently should
1852 // not pay a hex format per call just to discard the line.
1853
1/2
✗ Branch 61 → 62 not taken.
✓ Branch 61 → 66 taken 9 times.
9 if (logger.is_enabled(LogLevel::Debug))
1854 {
1855 logger.debug("write_bytes: Successfully wrote {} bytes to address {}.", num_bytes,
1856 DetourModKit::Format::format_address(reinterpret_cast<uintptr_t>(target_address)));
1857 }
1858 9 return {};
1859 }
1860
1861 14 Memory::ReadableStatus DetourModKit::Memory::is_readable_nonblocking(const void *address, size_t size)
1862 {
1863
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)
1864 2 return ReadableStatus::NotReadable;
1865
1866 12 ActiveReaderGuard reader_guard;
1867
1868
2/2
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 23 taken 10 times.
12 if (!s_cache_initialized.load(std::memory_order_seq_cst))
1869 {
1870 // Cache not initialized - fall back to direct VirtualQuery (blocking)
1871 2 MEMORY_BASIC_INFORMATION mbi{};
1872
2/4
✓ Branch 8 → 9 taken 2 times.
✗ Branch 8 → 55 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 2 times.
2 if (!VirtualQuery(address, &mbi, sizeof(mbi)))
1873 return ReadableStatus::NotReadable;
1874
2/2
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 1 time.
2 if (mbi.State != MEM_COMMIT)
1875 1 return ReadableStatus::NotReadable;
1876
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 1 time.
1 if (!check_read_permission(mbi.Protect))
1877 return ReadableStatus::NotReadable;
1878 1 const uintptr_t query_addr_val = reinterpret_cast<uintptr_t>(address);
1879 1 const uintptr_t region_start = reinterpret_cast<uintptr_t>(mbi.BaseAddress);
1880 1 const uintptr_t query_end = query_addr_val + size;
1881
1/2
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 1 time.
1 if (query_end < query_addr_val)
1882 return ReadableStatus::NotReadable;
1883
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)
1884 1 return ReadableStatus::Readable;
1885 return ReadableStatus::NotReadable;
1886 }
1887
1888 10 const size_t shard_count = s_shard_count.load(std::memory_order_acquire);
1889
1/2
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 10 times.
10 if (shard_count == 0)
1890 return ReadableStatus::Unknown;
1891
1892 10 const uintptr_t query_addr_val = reinterpret_cast<uintptr_t>(address);
1893 10 const size_t shard_idx = compute_shard_index(query_addr_val, shard_count);
1894 10 const uint64_t now_ns = current_time_ns();
1895 10 const uint64_t expiry_ns = configured_expiry_ns();
1896
1897 // Non-blocking: try_lock_shared to avoid stalling latency-sensitive threads
1898 10 std::shared_lock<SrwSharedMutex> lock(s_cache_shards[shard_idx].mtx, std::try_to_lock);
1899
1/2
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 40 taken 10 times.
10 if (!lock.owns_lock())
1900 return ReadableStatus::Unknown;
1901
1902 CachedMemoryRegionInfo *cached_info =
1903 10 find_in_shard(s_cache_shards[shard_idx], query_addr_val, size, now_ns, expiry_ns);
1904
2/2
✓ Branch 42 → 43 taken 6 times.
✓ Branch 42 → 50 taken 4 times.
10 if (cached_info)
1905 {
1906 s_stats.cache_hits.fetch_add(1, std::memory_order_relaxed);
1907
2/2
✓ Branch 46 → 47 taken 3 times.
✓ Branch 46 → 48 taken 3 times.
6 return check_read_permission(cached_info->protection) ? ReadableStatus::Readable
1908 6 : ReadableStatus::NotReadable;
1909 }
1910
1911 // Cache miss with non-blocking semantics: return Unknown rather than issuing VirtualQuery
1912 4 return ReadableStatus::Unknown;
1913 12 }
1914
1915 19283 uintptr_t DetourModKit::Memory::read_ptr_unsafe(uintptr_t base, ptrdiff_t offset) noexcept
1916 {
1917 // Route the pointer-sized read through the shared guarded-read entry point rather than a private __try/memcpy
1918 // so a single implementation owns every safety property: the low/null source floor, the (src + size) wrap
1919 // rejection, and
1920 // -- under AddressSanitizer on MSVC -- the __movsb copy that seh_read_bytes uses because the compiler otherwise
1921 // routes std::memcpy through the ASan interceptor, which both false-positives on the foreign mapped memory
1922 // these probes legitimately read and aborts on a wrapping source range before the SEH guard can turn the fault
1923 // into a 0 return. seh_read_bytes' byte-wise copy also keeps a misaligned foreign pointer free of cast-deref
1924 // undefined behavior. A rejected or faulting read yields 0; on a hit, value holds the bytes.
1925 19283 uintptr_t value = 0;
1926
2/2
✓ Branch 3 → 4 taken 4307 times.
✓ Branch 3 → 5 taken 15458 times.
19283 if (!seh_read_bytes(base + static_cast<uintptr_t>(offset), &value, sizeof(value)))
1927 4307 return 0;
1928 15458 return value;
1929 }
1930
1931 485933 bool DetourModKit::Memory::seh_read_bytes(uintptr_t addr, void *out, size_t bytes) noexcept
1932 {
1933
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 485932 times.
485933 if (bytes == 0)
1934 1 return true;
1935
2/4
✓ Branch 4 → 5 taken 485984 times.
✗ Branch 4 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 486007 times.
485932 if (!out || addr < SEH_READ_MIN_VALID_ADDR)
1936 return false;
1937
1938 // Overflow guard on (addr + bytes); a wraparound source range can never be a valid mapped image.
1939
2/2
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 486005 times.
486007 if (addr + bytes < addr)
1940 2 return false;
1941
1942 #ifdef _MSC_VER
1943 __try
1944 {
1945 #if defined(__SANITIZE_ADDRESS__)
1946 // Copy via __movsb (rep movsb) under ASan: MSVC routes std::memcpy through the ASan interceptor, which
1947 // inspects the source against ASan's shadow and false-positives on the foreign mapped memory this probe
1948 // legitimately reads (e.g. a module's data section during the RTTI walk). __movsb emits the copy inline
1949 // with no interceptable call. Release keeps std::memcpy.
1950 __movsb(static_cast<unsigned char *>(out), reinterpret_cast<const unsigned char *>(addr), bytes);
1951 #else
1952 std::memcpy(out, reinterpret_cast<const void *>(addr), bytes);
1953 #endif
1954 return true;
1955 }
1956 __except (Memory::detail::is_guarded_read_fault(GetExceptionCode()) ? EXCEPTION_EXECUTE_HANDLER
1957 : EXCEPTION_CONTINUE_SEARCH)
1958 {
1959 return false;
1960 }
1961 #else
1962 // MinGW lacks __try/__except. Read through the process-wide vectored fault guard (see veh_read_bytes): the
1963 // success path is a single rep movsb with no syscall, and any read fault across the span -- including a
1964 // multi-region read that crosses into unmapped or protected memory -- is swallowed and reported as failure.
1965 486005 return veh_read_bytes(addr, out, bytes);
1966 #endif
1967 }
1968
1969 namespace
1970 {
1971 // Walk a Cheat-Engine-style pointer chain inside a single fault guard. Every offset except the last is added
1972 // and dereferenced to obtain the next link; the last offset is added but not dereferenced, yielding the target
1973 // field address in out_addr. Each intermediate link is screened with plausible_userspace_ptr so a torn or
1974 // sentinel pointer aborts the walk before the next dereference faults. Returns false on any fault or
1975 // implausible intermediate link.
1976 18 bool resolve_chain_guarded(uintptr_t base, const ptrdiff_t *offsets, size_t count, uintptr_t &out_addr) noexcept
1977 {
1978 #ifdef _MSC_VER
1979 __try
1980 {
1981 uintptr_t cur = base;
1982 for (size_t i = 0; i + 1 < count; ++i)
1983 {
1984 uintptr_t next = 0;
1985 std::memcpy(&next, reinterpret_cast<const void *>(cur + static_cast<uintptr_t>(offsets[i])),
1986 sizeof(next));
1987 if (!Memory::plausible_userspace_ptr(next))
1988 return false;
1989 cur = next;
1990 }
1991 out_addr = (count == 0) ? cur : cur + static_cast<uintptr_t>(offsets[count - 1]);
1992 return true;
1993 }
1994 __except (Memory::detail::is_guarded_read_fault(GetExceptionCode()) ? EXCEPTION_EXECUTE_HANDLER
1995 : EXCEPTION_CONTINUE_SEARCH)
1996 {
1997 return false;
1998 }
1999 #else
2000 // MinGW lacks __try/__except. Each intermediate link is read through read_ptr_unsafe, which returns 0 on
2001 // fault; the plausibility screen also rejects that 0.
2002 18 uintptr_t cur = base;
2003
2/2
✓ Branch 8 → 3 taken 19 times.
✓ Branch 8 → 9 taken 15 times.
34 for (size_t i = 0; i + 1 < count; ++i)
2004 {
2005 19 const uintptr_t next = Memory::read_ptr_unsafe(cur, offsets[i]);
2006
2/2
✓ Branch 5 → 6 taken 3 times.
✓ Branch 5 → 7 taken 16 times.
19 if (!Memory::plausible_userspace_ptr(next))
2007 3 return false;
2008 16 cur = next;
2009 }
2010
2/2
✓ Branch 9 → 10 taken 12 times.
✓ Branch 9 → 11 taken 3 times.
15 out_addr = (count == 0) ? cur : cur + static_cast<uintptr_t>(offsets[count - 1]);
2011 15 return true;
2012 #endif
2013 }
2014 } // anonymous namespace
2015
2016 7 std::optional<uintptr_t> DetourModKit::Memory::seh_resolve_chain(uintptr_t base,
2017 std::span<const ptrdiff_t> offsets) noexcept
2018 {
2019 7 uintptr_t addr = 0;
2020
2/2
✓ Branch 5 → 6 taken 6 times.
✓ Branch 5 → 7 taken 1 time.
7 if (resolve_chain_guarded(base, offsets.data(), offsets.size(), addr))
2021 6 return addr;
2022 1 return std::nullopt;
2023 }
2024
2025 7 bool DetourModKit::Memory::seh_read_chain_bytes(uintptr_t base, std::span<const ptrdiff_t> offsets, void *out,
2026 size_t bytes) noexcept
2027 {
2028
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 6 times.
7 if (bytes == 0)
2029 1 return true;
2030
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 5 times.
6 if (!out)
2031 1 return false;
2032
2033 #ifdef _MSC_VER
2034 // The walk is inlined here rather than reusing resolve_chain_guarded so the resolve and the terminal read sit
2035 // in one __try region. On x64 the __try is table-driven and free on the no-fault path, so this is a structural
2036 // choice (one uniform failure path for the whole operation) and not a measurable saving over two adjacent
2037 // guarded regions.
2038 const ptrdiff_t *const offs = offsets.data();
2039 const size_t count = offsets.size();
2040 __try
2041 {
2042 uintptr_t cur = base;
2043 for (size_t i = 0; i + 1 < count; ++i)
2044 {
2045 uintptr_t next = 0;
2046 std::memcpy(&next, reinterpret_cast<const void *>(cur + static_cast<uintptr_t>(offs[i])), sizeof(next));
2047 if (!Memory::plausible_userspace_ptr(next))
2048 return false;
2049 cur = next;
2050 }
2051 const uintptr_t final_addr = (count == 0) ? cur : cur + static_cast<uintptr_t>(offs[count - 1]);
2052 // Apply seh_read_bytes' own prechecks on the terminal address so a low or wrapping final address fails
2053 // identically on both toolchains. The
2054 // MinGW branch below already routes through seh_read_bytes, which rejects these; matching here keeps a
2055 // stale or sentinel final address from raising a (benign but debugger-visible) first-chance exception.
2056 if (final_addr < SEH_READ_MIN_VALID_ADDR || final_addr + bytes < final_addr)
2057 return false;
2058 std::memcpy(out, reinterpret_cast<const void *>(final_addr), bytes);
2059 return true;
2060 }
2061 __except (Memory::detail::is_guarded_read_fault(GetExceptionCode()) ? EXCEPTION_EXECUTE_HANDLER
2062 : EXCEPTION_CONTINUE_SEARCH)
2063 {
2064 return false;
2065 }
2066 #else
2067 // MinGW: resolve through the shared guarded-link helper, then read the terminal range with seh_read_bytes so
2068 // the same vectored fault guard covers the final byte span.
2069 5 uintptr_t final_addr = 0;
2070
2/2
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 4 times.
5 if (!resolve_chain_guarded(base, offsets.data(), offsets.size(), final_addr))
2071 1 return false;
2072 4 return Memory::seh_read_bytes(final_addr, out, bytes);
2073 #endif
2074 }
2075
2076 12 bool DetourModKit::Memory::seh_write_bytes(uintptr_t addr, const void *source, size_t bytes) noexcept
2077 {
2078
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 11 times.
12 if (bytes == 0)
2079 1 return true;
2080
4/4
✓ Branch 4 → 5 taken 10 times.
✓ Branch 4 → 6 taken 1 time.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 9 times.
11 if (!source || addr < SEH_READ_MIN_VALID_ADDR)
2081 2 return false;
2082
2083 // Overflow guard on (addr + bytes); a wraparound destination range can never be a valid mapped target.
2084
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 9 times.
9 if (addr + bytes < addr)
2085 return false;
2086
2087 #ifdef _MSC_VER
2088 __try
2089 {
2090 #if defined(__SANITIZE_ADDRESS__)
2091 // Copy via __movsb (rep movsb) under ASan for the same reason seh_read_bytes does: MSVC routes std::memcpy
2092 // through the ASan interceptor, which inspects the operands against ASan's shadow and false-positives on
2093 // the foreign mapped memory this primitive legitimately writes. __movsb emits the copy inline with no
2094 // interceptable call. Release keeps std::memcpy.
2095 __movsb(reinterpret_cast<unsigned char *>(addr), static_cast<const unsigned char *>(source), bytes);
2096 #else
2097 std::memcpy(reinterpret_cast<void *>(addr), source, bytes);
2098 #endif
2099 return true;
2100 }
2101 __except (Memory::detail::is_guarded_read_fault(GetExceptionCode()) ? EXCEPTION_EXECUTE_HANDLER
2102 : EXCEPTION_CONTINUE_SEARCH)
2103 {
2104 return false;
2105 }
2106 #else
2107 // MinGW: write through the same guard/fallback split as seh_read_bytes. x64 uses the process-wide vectored
2108 // guard when available; if the handler cannot be installed, and on 32-bit builds, the fallback validates the
2109 // destination and writes through WriteProcessMemory.
2110 9 return veh_write_bytes(addr, source, bytes);
2111 #endif
2112 }
2113
2114 8 bool DetourModKit::Memory::seh_write_chain_bytes(uintptr_t base, std::span<const ptrdiff_t> offsets,
2115 const void *source, size_t bytes) noexcept
2116 {
2117
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 7 times.
8 if (bytes == 0)
2118 1 return true;
2119
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 6 times.
7 if (!source)
2120 1 return false;
2121
2122 #ifdef _MSC_VER
2123 // The walk is inlined here rather than reusing resolve_chain_guarded so the resolve and the terminal write sit
2124 // in one __try region, mirroring seh_read_chain_bytes: one uniform failure path for the whole operation.
2125 const ptrdiff_t *const offs = offsets.data();
2126 const size_t count = offsets.size();
2127 // The copies below use plain std::memcpy, not the __movsb ASan shim seh_write_bytes uses. The single-shot
2128 // primitive writes arbitrary scan/code memory that can abut an ASan-poisoned redzone; a chain write instead
2129 // lands on a plausibility-screened, resolved address, so plain memcpy stays correct and still lets ASan flag a
2130 // genuinely out-of-bounds chain target. This matches seh_read_chain_bytes, the design mirror of this function.
2131 __try
2132 {
2133 uintptr_t cur = base;
2134 for (size_t i = 0; i + 1 < count; ++i)
2135 {
2136 uintptr_t next = 0;
2137 std::memcpy(&next, reinterpret_cast<const void *>(cur + static_cast<uintptr_t>(offs[i])), sizeof(next));
2138 if (!Memory::plausible_userspace_ptr(next))
2139 return false;
2140 cur = next;
2141 }
2142 const uintptr_t final_addr = (count == 0) ? cur : cur + static_cast<uintptr_t>(offs[count - 1]);
2143 // Same terminal-address prechecks as seh_read_chain_bytes so a low or wrapping final address fails
2144 // identically on both toolchains before the store is attempted.
2145 if (final_addr < SEH_READ_MIN_VALID_ADDR || final_addr + bytes < final_addr)
2146 return false;
2147 std::memcpy(reinterpret_cast<void *>(final_addr), source, bytes);
2148 return true;
2149 }
2150 __except (Memory::detail::is_guarded_read_fault(GetExceptionCode()) ? EXCEPTION_EXECUTE_HANDLER
2151 : EXCEPTION_CONTINUE_SEARCH)
2152 {
2153 return false;
2154 }
2155 #else
2156 // MinGW: resolve through the shared guarded-link helper (its intermediate reads use the vectored guard), then
2157 // write the terminal range through seh_write_bytes so the same guard covers the final store.
2158 6 uintptr_t final_addr = 0;
2159
2/2
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 5 times.
6 if (!resolve_chain_guarded(base, offsets.data(), offsets.size(), final_addr))
2160 1 return false;
2161 5 return Memory::seh_write_bytes(final_addr, source, bytes);
2162 #endif
2163 }
2164
2165 namespace
2166 {
2167 // PE header layout for module range resolution. Pulled into an anonymous namespace so the helper is internal to
2168 // memory.cpp and shared between module_range_for, own_module_range, and host_module_range.
2169 101 DetourModKit::Memory::ModuleRange module_range_from_handle(HMODULE mod) noexcept
2170 {
2171
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 101 times.
101 if (!mod)
2172 return {};
2173
2174 101 const uintptr_t base = reinterpret_cast<uintptr_t>(mod);
2175
2176 101 IMAGE_DOS_HEADER dos{};
2177
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 101 times.
101 if (!DetourModKit::Memory::seh_read_bytes(base, &dos, sizeof(dos)))
2178 return {};
2179
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 101 times.
101 if (dos.e_magic != IMAGE_DOS_SIGNATURE)
2180 return {};
2181
2182 // Bound e_lfanew. A genuine PE places NT headers within the first few
2183 // KiB; anything beyond a generous 1 MiB cap is corrupt or hostile.
2184
2/4
✓ Branch 9 → 10 taken 101 times.
✗ Branch 9 → 11 not taken.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 101 times.
101 if (dos.e_lfanew <= 0 || static_cast<uint32_t>(dos.e_lfanew) > 0x100000U)
2185 return {};
2186
2187 101 IMAGE_NT_HEADERS nt{};
2188
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 101 times.
101 if (!DetourModKit::Memory::seh_read_bytes(base + static_cast<uintptr_t>(dos.e_lfanew), &nt, sizeof(nt)))
2189 return {};
2190
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 101 times.
101 if (nt.Signature != IMAGE_NT_SIGNATURE)
2191 return {};
2192
2193 101 const uintptr_t size_of_image = nt.OptionalHeader.SizeOfImage;
2194
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 101 times.
101 if (size_of_image == 0)
2195 return {};
2196
2197 101 return {base, base + size_of_image};
2198 }
2199
2200 // Per-process ModuleRange cache shared by module_range_for. Constructed on first use; survives until process
2201 // exit. Static-storage destruction is deliberately a non-issue here because the cache is consulted only by
2202 // DetourModKit code that has already shut down its own subsystems via
2203 // DMK_Shutdown() (callers do not query ranges from atexit handlers).
2204 struct ModuleRangeCache
2205 {
2206 SrwSharedMutex mtx;
2207 std::unordered_map<HMODULE, DetourModKit::Memory::ModuleRange> entries;
2208 };
2209
2210 230776 ModuleRangeCache &get_module_range_cache() noexcept
2211 {
2212
3/4
✓ Branch 2 → 3 taken 80 times.
✓ Branch 2 → 8 taken 230696 times.
✓ Branch 4 → 5 taken 80 times.
✗ Branch 4 → 8 not taken.
230776 static ModuleRangeCache cache;
2213 230776 return cache;
2214 }
2215 } // anonymous namespace
2216
2217 std::optional<DetourModKit::Memory::ModuleRange>
2218 230789 DetourModKit::Memory::module_range_for(const void *address) noexcept
2219 {
2220
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 230788 times.
230789 if (!address)
2221 1 return std::nullopt;
2222
2223 230788 HMODULE mod = nullptr;
2224 230788 if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
2225
4/4
✓ Branch 5 → 6 taken 230776 times.
✓ Branch 5 → 7 taken 12 times.
✓ Branch 9 → 10 taken 12 times.
✓ Branch 9 → 11 taken 230776 times.
461564 reinterpret_cast<LPCWSTR>(address), &mod) ||
2226
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 230776 times.
230776 mod == nullptr)
2227 {
2228 12 return std::nullopt;
2229 }
2230
2231 230776 auto &cache = get_module_range_cache();
2232 {
2233 230776 std::shared_lock<SrwSharedMutex> lock(cache.mtx);
2234 230776 const auto it = cache.entries.find(mod);
2235
2/2
✓ Branch 16 → 17 taken 230696 times.
✓ Branch 16 → 20 taken 80 times.
230776 if (it != cache.entries.end())
2236 230696 return it->second;
2237
2/2
✓ Branch 22 → 23 taken 80 times.
✓ Branch 22 → 27 taken 230696 times.
230776 }
2238
2239 80 const auto range = module_range_from_handle(mod);
2240
1/2
✗ Branch 26 → 28 not taken.
✓ Branch 26 → 29 taken 80 times.
80 if (!range.valid())
2241 return std::nullopt;
2242
2243 {
2244 80 std::unique_lock<SrwSharedMutex> lock(cache.mtx);
2245 // Another thread may have inserted between our shared/unique transition;
2246 // emplace skips on collision so we keep the first-resolved entry.
2247 80 cache.entries.emplace(mod, range);
2248 80 }
2249 80 return range;
2250 }
2251
2252 3 DetourModKit::Memory::ModuleRange DetourModKit::Memory::own_module_range() noexcept
2253 {
2254 // Magic-static initialization: the lambda runs exactly once per module, guarded by the C++23
2255 // thread-safe-initialization rules. Taking the address of own_module_range itself anchors the lookup in
2256 // whichever DLL/EXE statically linked this translation unit.
2257 2 static const ModuleRange cached = []
2258 {
2259 2 HMODULE mod = nullptr;
2260
1/2
✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 12 not taken.
2 if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
2261 GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
2262
2/4
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 5 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 2 times.
4 reinterpret_cast<LPCWSTR>(&DetourModKit::Memory::own_module_range), &mod) ||
2263
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 2 times.
2 mod == nullptr)
2264 {
2265 return ModuleRange{};
2266 }
2267 2 return module_range_from_handle(mod);
2268
3/4
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 8 taken 1 time.
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 8 not taken.
3 }();
2269 3 return cached;
2270 }
2271
2272 24 DetourModKit::Memory::ModuleRange DetourModKit::Memory::host_module_range() noexcept
2273 {
2274 19 static const ModuleRange cached = []
2275 {
2276 19 HMODULE mod = GetModuleHandleW(nullptr);
2277
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 19 times.
19 if (!mod)
2278 return ModuleRange{};
2279 19 return module_range_from_handle(mod);
2280
3/4
✓ Branch 2 → 3 taken 19 times.
✓ Branch 2 → 8 taken 5 times.
✓ Branch 4 → 5 taken 19 times.
✗ Branch 4 → 8 not taken.
24 }();
2281 24 return cached;
2282 }
2283 } // namespace DetourModKit
2284