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