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 |