src/scanner.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /** | ||
| 2 | * @file scanner.cpp | ||
| 3 | * @brief Implementation of Array-of-Bytes (AOB) parsing, scanning, and RIP-relative resolution. | ||
| 4 | */ | ||
| 5 | |||
| 6 | #include "DetourModKit/scanner.hpp" | ||
| 7 | #include "DetourModKit/memory.hpp" | ||
| 8 | #include "DetourModKit/logger.hpp" | ||
| 9 | #include "DetourModKit/format.hpp" | ||
| 10 | #include "x86_decode.hpp" | ||
| 11 | |||
| 12 | #include <windows.h> | ||
| 13 | #include <vector> | ||
| 14 | #include <string> | ||
| 15 | #include <cctype> | ||
| 16 | #include <stdexcept> | ||
| 17 | #include <cstddef> | ||
| 18 | #include <cstdint> | ||
| 19 | #include <cassert> | ||
| 20 | #include <cstring> | ||
| 21 | #include <optional> | ||
| 22 | |||
| 23 | #if defined(__SSE2__) || defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2) | ||
| 24 | #define DMK_HAS_SSE2 1 | ||
| 25 | #include <emmintrin.h> | ||
| 26 | #endif | ||
| 27 | |||
| 28 | // AVX2 support: compile-time header + runtime CPUID detection. | ||
| 29 | // On GCC/Clang, AVX2 intrinsics require either -mavx2 globally or | ||
| 30 | // __attribute__((target("avx2"))) per function. We use the latter so | ||
| 31 | // the rest of the TU stays SSE2-only and runs on any x86-64 CPU. | ||
| 32 | // On MSVC, intrinsics are always available; runtime CPUID gates usage. | ||
| 33 | #if defined(__GNUC__) && (defined(__x86_64__) || defined(__i386__)) | ||
| 34 | #define DMK_HAS_AVX2 1 | ||
| 35 | #include <immintrin.h> | ||
| 36 | #include <cpuid.h> | ||
| 37 | #define DMK_AVX2_TARGET __attribute__((target("avx2"))) | ||
| 38 | #elif defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86)) | ||
| 39 | #define DMK_HAS_AVX2 1 | ||
| 40 | #include <immintrin.h> | ||
| 41 | #include <intrin.h> | ||
| 42 | #define DMK_AVX2_TARGET | ||
| 43 | #endif | ||
| 44 | |||
| 45 | using namespace DetourModKit; | ||
| 46 | using namespace DetourModKit::String; | ||
| 47 | |||
| 48 | namespace | ||
| 49 | { | ||
| 50 | #ifdef DMK_HAS_AVX2 | ||
| 51 | /** | ||
| 52 | * @brief Detects AVX2 support at runtime via CPUID. | ||
| 53 | * @details Checks CPUID leaf 7 subleaf 0, EBX bit 5 (AVX2) and also | ||
| 54 | * verifies that the OS has enabled AVX state saving (XGETBV). | ||
| 55 | * Result is cached in a function-local static for zero-cost | ||
| 56 | * repeated queries. | ||
| 57 | */ | ||
| 58 | 785 | bool cpu_has_avx2() noexcept | |
| 59 | { | ||
| 60 | 1 | static const bool result = []() -> bool | |
| 61 | { | ||
| 62 | #if defined(__GNUC__) || defined(__clang__) | ||
| 63 | // Check CPUID is supported and query leaf 7 | ||
| 64 | 1 | unsigned int eax = 0, ebx = 0, ecx = 0, edx = 0; | |
| 65 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1 time.
|
1 | if (!__get_cpuid_count(7, 0, &eax, &ebx, &ecx, &edx)) |
| 66 | ✗ | return false; | |
| 67 | 1 | const bool avx2_flag = (ebx & (1u << 5)) != 0; | |
| 68 | |||
| 69 | // Verify OS has enabled AVX state saving via XGETBV (ECX=0, bit 2) | ||
| 70 | 1 | unsigned int xcr0_lo = 0, xcr0_hi = 0; | |
| 71 | 1 | __asm__ volatile("xgetbv" : "=a"(xcr0_lo), "=d"(xcr0_hi) : "c"(0)); | |
| 72 | 1 | const bool os_avx = (xcr0_lo & 0x06) == 0x06; // SSE + AVX state | |
| 73 | |||
| 74 |
2/4✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 9 not taken.
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 9 not taken.
|
1 | return avx2_flag && os_avx; |
| 75 | #elif defined(_MSC_VER) | ||
| 76 | int cpui[4]{}; | ||
| 77 | __cpuidex(cpui, 7, 0); | ||
| 78 | const bool avx2_flag = (cpui[1] & (1 << 5)) != 0; | ||
| 79 | |||
| 80 | // Verify OS has enabled AVX state saving | ||
| 81 | const unsigned long long xcr0 = _xgetbv(0); | ||
| 82 | const bool os_avx = (xcr0 & 0x06) == 0x06; | ||
| 83 | |||
| 84 | return avx2_flag && os_avx; | ||
| 85 | #else | ||
| 86 | return false; | ||
| 87 | #endif | ||
| 88 |
3/4✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 8 taken 784 times.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 8 not taken.
|
785 | }(); |
| 89 | 785 | return result; | |
| 90 | } | ||
| 91 | /** | ||
| 92 | * @brief Verifies a pattern match using AVX2 (32 bytes per iteration). | ||
| 93 | * @param pattern_start Start of the candidate region in memory. | ||
| 94 | * @param pattern The compiled pattern to verify against. | ||
| 95 | * @param start_offset Byte offset to start verification from (may be non-zero | ||
| 96 | * if a previous tier partially verified). | ||
| 97 | * @return The next byte offset to resume verification from on success | ||
| 98 | * (equal to pattern.size() when the AVX2 tier covered the whole | ||
| 99 | * pattern), or std::nullopt when a 32-byte chunk did not match | ||
| 100 | * and the caller must abandon this candidate position. | ||
| 101 | * @note This function is compiled with AVX2 codegen via target attribute on | ||
| 102 | * GCC/Clang. On MSVC, intrinsics are always available. | ||
| 103 | */ | ||
| 104 | DMK_AVX2_TARGET | ||
| 105 | 846494 | std::optional<size_t> verify_pattern_avx2(const std::byte *pattern_start, | |
| 106 | const Scanner::CompiledPattern &pattern, | ||
| 107 | size_t start_offset) noexcept | ||
| 108 | { | ||
| 109 | 846494 | const size_t pattern_size = pattern.size(); | |
| 110 | 846494 | size_t j = start_offset; | |
| 111 | |||
| 112 |
2/2✓ Branch 25 → 4 taken 7 times.
✓ Branch 25 → 26 taken 846493 times.
|
846500 | for (; j + 32 <= pattern_size; j += 32) |
| 113 | { | ||
| 114 | 7 | const __m256i mem = _mm256_loadu_si256( | |
| 115 | 7 | reinterpret_cast<const __m256i *>(pattern_start + j)); | |
| 116 | 7 | const __m256i pat = _mm256_loadu_si256( | |
| 117 | 7 | reinterpret_cast<const __m256i *>(pattern.bytes.data() + j)); | |
| 118 | 7 | const __m256i msk = _mm256_loadu_si256( | |
| 119 | 7 | reinterpret_cast<const __m256i *>(pattern.mask.data() + j)); | |
| 120 | |||
| 121 | 7 | const __m256i xored = _mm256_xor_si256(mem, pat); | |
| 122 | 7 | const __m256i masked = _mm256_and_si256(xored, msk); | |
| 123 | 14 | const __m256i cmp = _mm256_cmpeq_epi8(masked, _mm256_setzero_si256()); | |
| 124 | |||
| 125 |
2/2✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 24 taken 6 times.
|
7 | if (static_cast<unsigned int>(_mm256_movemask_epi8(cmp)) != 0xFFFFFFFFu) |
| 126 | { | ||
| 127 | 1 | return std::nullopt; | |
| 128 | } | ||
| 129 | } | ||
| 130 | |||
| 131 | 846493 | return j; | |
| 132 | } | ||
| 133 | #endif // DMK_HAS_AVX2 | ||
| 134 | |||
| 135 | /** | ||
| 136 | * @brief Returns a commonality score for a byte value in typical x64 PE code sections. | ||
| 137 | * @details Higher scores indicate bytes that appear more frequently, making them | ||
| 138 | * poor candidates for anchor-based scanning. | ||
| 139 | */ | ||
| 140 | 248 | static constexpr uint8_t byte_frequency_class(uint8_t b) noexcept | |
| 141 | { | ||
| 142 |
9/13✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 4 taken 9 times.
✓ Branch 2 → 5 taken 12 times.
✓ Branch 2 → 6 taken 69 times.
✓ Branch 2 → 7 taken 40 times.
✓ Branch 2 → 8 taken 32 times.
✓ Branch 2 → 9 taken 8 times.
✗ Branch 2 → 10 not taken.
✗ Branch 2 → 11 not taken.
✓ Branch 2 → 12 taken 3 times.
✗ Branch 2 → 13 not taken.
✗ Branch 2 → 14 not taken.
✓ Branch 2 → 15 taken 71 times.
|
248 | switch (b) |
| 143 | { | ||
| 144 | 4 | case 0x00: | |
| 145 | 4 | return 10; // null padding, very common | |
| 146 | 9 | case 0xCC: | |
| 147 | 9 | return 9; // INT3, debug padding | |
| 148 | 12 | case 0x90: | |
| 149 | 12 | return 9; // NOP | |
| 150 | 69 | case 0xFF: | |
| 151 | 69 | return 8; // call/jmp indirect, common | |
| 152 | 40 | case 0x48: | |
| 153 | 40 | return 8; // REX.W prefix, ubiquitous in x64 | |
| 154 | 32 | case 0x8B: | |
| 155 | 32 | return 7; // MOV reg, r/m | |
| 156 | 8 | case 0x89: | |
| 157 | 8 | return 7; // MOV r/m, reg | |
| 158 | ✗ | case 0x0F: | |
| 159 | ✗ | return 7; // two-byte opcode escape | |
| 160 | ✗ | case 0xE8: | |
| 161 | ✗ | return 6; // CALL rel32 | |
| 162 | 3 | case 0xE9: | |
| 163 | 3 | return 6; // JMP rel32 | |
| 164 | ✗ | case 0x83: | |
| 165 | ✗ | return 6; // arithmetic imm8 | |
| 166 | ✗ | case 0xC3: | |
| 167 | ✗ | return 5; // RET | |
| 168 | 71 | default: | |
| 169 | 71 | return 0; // uncommon, ideal anchor | |
| 170 | } | ||
| 171 | } | ||
| 172 | |||
| 173 | /** | ||
| 174 | * @brief Picks the rarest literal byte's index in a compiled pattern. | ||
| 175 | * @return The byte index in `[0, pattern.size())` with the lowest score, | ||
| 176 | * or `pattern.size()` when every position is a wildcard. | ||
| 177 | */ | ||
| 178 | 108 | size_t select_pattern_anchor(const Scanner::CompiledPattern &pattern) noexcept | |
| 179 | { | ||
| 180 | 108 | const size_t pattern_size = pattern.size(); | |
| 181 | 108 | size_t best = pattern_size; | |
| 182 | 108 | uint8_t best_score = UINT8_MAX; | |
| 183 |
2/2✓ Branch 14 → 4 taken 304 times.
✓ Branch 14 → 15 taken 37 times.
|
341 | for (size_t i = 0; i < pattern_size; ++i) |
| 184 | { | ||
| 185 |
2/2✓ Branch 5 → 6 taken 56 times.
✓ Branch 5 → 7 taken 248 times.
|
304 | if (pattern.mask[i] == std::byte{0x00}) |
| 186 | { | ||
| 187 | 56 | continue; | |
| 188 | } | ||
| 189 | const uint8_t score = | ||
| 190 | 248 | byte_frequency_class(static_cast<uint8_t>(pattern.bytes[i])); | |
| 191 |
4/4✓ Branch 9 → 10 taken 154 times.
✓ Branch 9 → 11 taken 94 times.
✓ Branch 10 → 11 taken 82 times.
✓ Branch 10 → 13 taken 72 times.
|
248 | if (best == pattern_size || score < best_score) |
| 192 | { | ||
| 193 | 176 | best = i; | |
| 194 | 176 | best_score = score; | |
| 195 |
2/2✓ Branch 11 → 12 taken 71 times.
✓ Branch 11 → 13 taken 105 times.
|
176 | if (score == 0) |
| 196 | { | ||
| 197 | 71 | break; | |
| 198 | } | ||
| 199 | } | ||
| 200 | } | ||
| 201 | 108 | return best; | |
| 202 | } | ||
| 203 | } // anonymous namespace | ||
| 204 | |||
| 205 | 99 | void DetourModKit::Scanner::CompiledPattern::compile_anchor() noexcept | |
| 206 | { | ||
| 207 | 99 | anchor = select_pattern_anchor(*this); | |
| 208 | 99 | } | |
| 209 | |||
| 210 | namespace | ||
| 211 | { | ||
| 212 | /** | ||
| 213 | * @brief Converts a single hex character to its numeric value. | ||
| 214 | * @return The value 0-15, or -1 if not a valid hex digit. | ||
| 215 | */ | ||
| 216 | 1752 | constexpr int hex_char_to_int(char c) noexcept | |
| 217 | { | ||
| 218 |
3/4✓ Branch 2 → 3 taken 1752 times.
✗ Branch 2 → 5 not taken.
✓ Branch 3 → 4 taken 1125 times.
✓ Branch 3 → 5 taken 627 times.
|
1752 | if (c >= '0' && c <= '9') |
| 219 | 1125 | return c - '0'; | |
| 220 |
3/4✓ Branch 5 → 6 taken 627 times.
✗ Branch 5 → 8 not taken.
✓ Branch 6 → 7 taken 608 times.
✓ Branch 6 → 8 taken 19 times.
|
627 | if (c >= 'A' && c <= 'F') |
| 221 | 608 | return c - 'A' + 10; | |
| 222 |
3/4✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 11 taken 16 times.
✓ Branch 9 → 10 taken 3 times.
✗ Branch 9 → 11 not taken.
|
19 | if (c >= 'a' && c <= 'f') |
| 223 | 3 | return c - 'a' + 10; | |
| 224 | 16 | return -1; | |
| 225 | } | ||
| 226 | } // anonymous namespace | ||
| 227 | |||
| 228 | 114 | std::optional<Scanner::CompiledPattern> DetourModKit::Scanner::parse_aob(std::string_view aob_str) | |
| 229 | { | ||
| 230 |
1/2✓ Branch 2 → 3 taken 114 times.
✗ Branch 2 → 118 not taken.
|
114 | Logger &logger = Logger::get_instance(); |
| 231 | |||
| 232 | 4771 | auto is_ws = [](char c) noexcept | |
| 233 |
9/12✓ Branch 2 → 3 taken 3071 times.
✓ Branch 2 → 8 taken 1700 times.
✓ Branch 3 → 4 taken 3063 times.
✓ Branch 3 → 8 taken 8 times.
✓ Branch 4 → 5 taken 3063 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 3062 times.
✓ Branch 5 → 8 taken 1 time.
✓ Branch 6 → 7 taken 3062 times.
✗ Branch 6 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 3062 times.
|
4771 | { return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\f' || c == '\v'; }; |
| 234 | |||
| 235 | // Trim leading/trailing whitespace without allocating | ||
| 236 | 114 | std::string_view input = aob_str; | |
| 237 |
6/6✓ Branch 6 → 7 taken 121 times.
✓ Branch 6 → 11 taken 6 times.
✓ Branch 9 → 10 taken 13 times.
✓ Branch 9 → 11 taken 108 times.
✓ Branch 12 → 4 taken 13 times.
✓ Branch 12 → 13 taken 114 times.
|
127 | while (!input.empty() && is_ws(input.front())) |
| 238 | 13 | input.remove_prefix(1); | |
| 239 |
6/6✓ Branch 16 → 17 taken 112 times.
✓ Branch 16 → 21 taken 6 times.
✓ Branch 19 → 20 taken 4 times.
✓ Branch 19 → 21 taken 108 times.
✓ Branch 22 → 14 taken 4 times.
✓ Branch 22 → 23 taken 114 times.
|
118 | while (!input.empty() && is_ws(input.back())) |
| 240 | 4 | input.remove_suffix(1); | |
| 241 | |||
| 242 |
2/2✓ Branch 24 → 25 taken 6 times.
✓ Branch 24 → 30 taken 108 times.
|
114 | if (input.empty()) |
| 243 | { | ||
| 244 |
2/2✓ Branch 26 → 27 taken 3 times.
✓ Branch 26 → 29 taken 3 times.
|
6 | if (!aob_str.empty()) |
| 245 | { | ||
| 246 |
1/2✓ Branch 27 → 28 taken 3 times.
✗ Branch 27 → 106 not taken.
|
3 | logger.debug("AOB Parser: Input string became empty after trimming."); |
| 247 | } | ||
| 248 | 6 | return std::nullopt; | |
| 249 | } | ||
| 250 | |||
| 251 | 108 | CompiledPattern result; | |
| 252 | 108 | size_t token_idx = 0; | |
| 253 | 108 | bool offset_set = false; | |
| 254 | |||
| 255 | 108 | size_t pos = 0; | |
| 256 |
2/2✓ Branch 94 → 31 taken 947 times.
✓ Branch 94 → 95 taken 96 times.
|
1043 | while (pos < input.size()) |
| 257 | { | ||
| 258 | // Skip whitespace between tokens | ||
| 259 |
5/6✓ Branch 34 → 35 taken 1790 times.
✗ Branch 34 → 39 not taken.
✓ Branch 37 → 38 taken 843 times.
✓ Branch 37 → 39 taken 947 times.
✓ Branch 40 → 32 taken 843 times.
✓ Branch 40 → 41 taken 947 times.
|
1790 | while (pos < input.size() && is_ws(input[pos])) |
| 260 | 843 | ++pos; | |
| 261 |
1/2✗ Branch 42 → 43 not taken.
✓ Branch 42 → 44 taken 947 times.
|
947 | if (pos >= input.size()) |
| 262 | ✗ | break; | |
| 263 | |||
| 264 | // Find token end | ||
| 265 | 947 | const size_t token_start = pos; | |
| 266 |
6/6✓ Branch 47 → 48 taken 2748 times.
✓ Branch 47 → 52 taken 98 times.
✓ Branch 50 → 51 taken 1899 times.
✓ Branch 50 → 52 taken 849 times.
✓ Branch 53 → 45 taken 1899 times.
✓ Branch 53 → 54 taken 947 times.
|
2846 | while (pos < input.size() && !is_ws(input[pos])) |
| 267 | 1899 | ++pos; | |
| 268 |
1/2✓ Branch 54 → 55 taken 947 times.
✗ Branch 54 → 114 not taken.
|
947 | const std::string_view token = input.substr(token_start, pos - token_start); |
| 269 | |||
| 270 | 947 | token_idx++; | |
| 271 |
2/2✓ Branch 57 → 58 taken 11 times.
✓ Branch 57 → 63 taken 936 times.
|
947 | if (token == "|") |
| 272 | { | ||
| 273 |
2/2✓ Branch 58 → 59 taken 1 time.
✓ Branch 58 → 61 taken 10 times.
|
11 | if (offset_set) |
| 274 | { | ||
| 275 |
1/2✓ Branch 59 → 60 taken 1 time.
✗ Branch 59 → 107 not taken.
|
1 | logger.error("AOB Parser: Multiple '|' offset markers at position {}.", token_idx); |
| 276 | 12 | return std::nullopt; | |
| 277 | } | ||
| 278 | 10 | result.offset = static_cast<std::ptrdiff_t>(result.bytes.size()); | |
| 279 | 10 | offset_set = true; | |
| 280 | } | ||
| 281 |
6/6✓ Branch 65 → 66 taken 881 times.
✓ Branch 65 → 69 taken 55 times.
✓ Branch 68 → 69 taken 2 times.
✓ Branch 68 → 70 taken 879 times.
✓ Branch 71 → 72 taken 57 times.
✓ Branch 71 → 75 taken 879 times.
|
936 | else if (token == "??" || token == "?") |
| 282 | { | ||
| 283 |
1/2✓ Branch 72 → 73 taken 57 times.
✗ Branch 72 → 108 not taken.
|
57 | result.bytes.push_back(std::byte{0x00}); |
| 284 |
1/2✓ Branch 73 → 74 taken 57 times.
✗ Branch 73 → 109 not taken.
|
57 | result.mask.push_back(std::byte{0x00}); |
| 285 | } | ||
| 286 |
2/2✓ Branch 76 → 77 taken 876 times.
✓ Branch 76 → 89 taken 3 times.
|
879 | else if (token.length() == 2) |
| 287 | { | ||
| 288 | 876 | const int hi = hex_char_to_int(token[0]); | |
| 289 | 876 | const int lo = hex_char_to_int(token[1]); | |
| 290 |
3/4✓ Branch 81 → 82 taken 868 times.
✓ Branch 81 → 86 taken 8 times.
✓ Branch 82 → 83 taken 868 times.
✗ Branch 82 → 86 not taken.
|
876 | if (hi >= 0 && lo >= 0) |
| 291 | { | ||
| 292 |
1/2✓ Branch 83 → 84 taken 868 times.
✗ Branch 83 → 110 not taken.
|
868 | result.bytes.push_back(static_cast<std::byte>((hi << 4) | lo)); |
| 293 |
1/2✓ Branch 84 → 85 taken 868 times.
✗ Branch 84 → 111 not taken.
|
868 | result.mask.push_back(std::byte{0xFF}); |
| 294 | } | ||
| 295 | else | ||
| 296 | { | ||
| 297 | // Split the literal around '??' to dodge the C++ trigraph | ||
| 298 | // ??' (interpreted as a `|`), which trips -Wtrigraphs on | ||
| 299 | // GCC and would otherwise require disabling the warning TU-wide. | ||
| 300 |
1/2✓ Branch 86 → 87 taken 8 times.
✗ Branch 86 → 112 not taken.
|
8 | logger.error("AOB Parser: Invalid token '{}' at position {}. " |
| 301 | "Expected hex byte (e.g., FF), '?', or '?" | ||
| 302 | "?'.", | ||
| 303 | token, token_idx); | ||
| 304 | 8 | return std::nullopt; | |
| 305 | } | ||
| 306 | } | ||
| 307 | else | ||
| 308 | { | ||
| 309 |
1/2✓ Branch 89 → 90 taken 3 times.
✗ Branch 89 → 113 not taken.
|
3 | logger.error("AOB Parser: Invalid token '{}' at position {}. " |
| 310 | "Expected hex byte (e.g., FF), '?', or '?" | ||
| 311 | "?'.", | ||
| 312 | token, token_idx); | ||
| 313 | 3 | return std::nullopt; | |
| 314 | } | ||
| 315 | } | ||
| 316 | |||
| 317 |
1/2✗ Branch 96 → 97 not taken.
✓ Branch 96 → 101 taken 96 times.
|
96 | if (result.empty()) |
| 318 | { | ||
| 319 | ✗ | if (token_idx > 0) | |
| 320 | { | ||
| 321 | ✗ | logger.error("AOB Parser: Processed tokens but resulting pattern is empty."); | |
| 322 | } | ||
| 323 | ✗ | return std::nullopt; | |
| 324 | } | ||
| 325 | |||
| 326 | 96 | result.compile_anchor(); | |
| 327 | 96 | return result; | |
| 328 | 108 | } | |
| 329 | |||
| 330 | namespace | ||
| 331 | { | ||
| 332 | // Internal scan primitive: returns the match *start* without applying | ||
| 333 | // pattern.offset. The public find_pattern wrappers apply the offset | ||
| 334 | // exactly once on top of this result; scan_executable_regions also calls | ||
| 335 | // this directly so its own final offset-application remains correct. | ||
| 336 | const std::byte *find_pattern_raw(const std::byte *start_address, size_t region_size, | ||
| 337 | const Scanner::CompiledPattern &pattern) noexcept; | ||
| 338 | |||
| 339 | // Shared guard for "pattern has no literal bytes". Returning start_address | ||
| 340 | // preserves backwards compatibility for callers that rely on the degenerate | ||
| 341 | // "all wildcards matches anywhere" behaviour, but the call site is almost | ||
| 342 | // always a bug. Logging once per public entry (rather than per internal | ||
| 343 | // find_pattern_raw iteration) keeps the warning visible without flooding | ||
| 344 | // logs when the Nth-occurrence overload or scan_executable_regions loops. | ||
| 345 | 83 | bool pattern_has_literal_byte(const Scanner::CompiledPattern &pattern) noexcept | |
| 346 | { | ||
| 347 |
2/2✓ Branch 17 → 4 taken 104 times.
✓ Branch 17 → 18 taken 6 times.
|
193 | for (const std::byte m : pattern.mask) |
| 348 | { | ||
| 349 |
2/2✓ Branch 6 → 7 taken 77 times.
✓ Branch 6 → 8 taken 27 times.
|
104 | if (m != std::byte{0x00}) |
| 350 | 77 | return true; | |
| 351 | } | ||
| 352 | 6 | return false; | |
| 353 | } | ||
| 354 | |||
| 355 | // Shared precondition check for the public find_pattern overloads. Returns | ||
| 356 | // false when the caller must short-circuit with nullptr (empty pattern or | ||
| 357 | // null start_address). Emits the all-wildcard warning itself so callers | ||
| 358 | // do not duplicate it; in that case the caller still continues scanning. | ||
| 359 | 65 | bool validate_find_pattern_inputs(const std::byte *start_address, | |
| 360 | const Scanner::CompiledPattern &pattern, | ||
| 361 | Logger &logger) noexcept | ||
| 362 | { | ||
| 363 |
2/2✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 6 taken 63 times.
|
65 | if (pattern.empty()) |
| 364 | { | ||
| 365 | 2 | logger.error("find_pattern: Pattern is empty. Cannot scan."); | |
| 366 | 2 | return false; | |
| 367 | } | ||
| 368 |
2/2✓ Branch 6 → 7 taken 5 times.
✓ Branch 6 → 9 taken 58 times.
|
63 | if (!start_address) |
| 369 | { | ||
| 370 | 5 | logger.error("find_pattern: Start address is null. Cannot scan."); | |
| 371 | 5 | return false; | |
| 372 | } | ||
| 373 |
2/2✓ Branch 10 → 11 taken 6 times.
✓ Branch 10 → 13 taken 52 times.
|
58 | if (!pattern_has_literal_byte(pattern)) |
| 374 | { | ||
| 375 | 6 | logger.warning("find_pattern: pattern contains no literal bytes " | |
| 376 | "(all wildcards); returning region start unchanged"); | ||
| 377 | } | ||
| 378 | 58 | return true; | |
| 379 | } | ||
| 380 | } // anonymous namespace | ||
| 381 | |||
| 382 | 51 | const std::byte *DetourModKit::Scanner::find_pattern(const std::byte *start_address, size_t region_size, | |
| 383 | const CompiledPattern &pattern) | ||
| 384 | { | ||
| 385 | 51 | Logger &logger = Logger::get_instance(); | |
| 386 |
2/2✓ Branch 4 → 5 taken 5 times.
✓ Branch 4 → 6 taken 46 times.
|
51 | if (!validate_find_pattern_inputs(start_address, pattern, logger)) |
| 387 | { | ||
| 388 | 5 | return nullptr; | |
| 389 | } | ||
| 390 | |||
| 391 | 46 | const std::byte *match = find_pattern_raw(start_address, region_size, pattern); | |
| 392 |
2/2✓ Branch 7 → 8 taken 10 times.
✓ Branch 7 → 9 taken 36 times.
|
46 | if (!match) |
| 393 | 10 | return nullptr; | |
| 394 | 36 | return match + pattern.offset; | |
| 395 | } | ||
| 396 | |||
| 397 | namespace | ||
| 398 | { | ||
| 399 | 794 | const std::byte *find_pattern_raw(const std::byte *start_address, size_t region_size, | |
| 400 | const Scanner::CompiledPattern &pattern) noexcept | ||
| 401 | { | ||
| 402 | 794 | const size_t pattern_size = pattern.size(); | |
| 403 | |||
| 404 |
4/6✓ Branch 3 → 4 taken 794 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 794 times.
✗ Branch 4 → 6 not taken.
✓ Branch 5 → 6 taken 4 times.
✓ Branch 5 → 7 taken 790 times.
|
794 | if (pattern_size == 0 || !start_address || region_size < pattern_size) |
| 405 | { | ||
| 406 | 4 | return nullptr; | |
| 407 | } | ||
| 408 | |||
| 409 | // Anchor selection: parse_aob() pre-populates pattern.anchor, so the | ||
| 410 | // common path is a single load. Manually constructed patterns fall | ||
| 411 | // back to inline selection without mutating the input (preserves the | ||
| 412 | // const-by-design contract). | ||
| 413 | 790 | const size_t best_anchor = (pattern.anchor <= pattern_size) | |
| 414 |
2/2✓ Branch 7 → 8 taken 781 times.
✓ Branch 7 → 9 taken 9 times.
|
790 | ? pattern.anchor |
| 415 | 790 | : select_pattern_anchor(pattern); | |
| 416 | |||
| 417 | // All wildcards: the pattern has no literal bytes to anchor on, so the | ||
| 418 | // search degenerates to "always match at region start". The public | ||
| 419 | // wrappers log the warning exactly once per call; repeated internal | ||
| 420 | // iterations (Nth occurrence, per-region scans) stay quiet. | ||
| 421 |
2/2✓ Branch 10 → 11 taken 9 times.
✓ Branch 10 → 12 taken 781 times.
|
790 | if (best_anchor == pattern_size) |
| 422 | { | ||
| 423 | 9 | return start_address; | |
| 424 | } | ||
| 425 | |||
| 426 | 781 | const std::byte target_byte = pattern.bytes[best_anchor]; | |
| 427 | 781 | const unsigned char target_val = static_cast<unsigned char>(target_byte); | |
| 428 | |||
| 429 | 781 | const std::byte *search_start = start_address + best_anchor; | |
| 430 | 781 | const std::byte *const search_end = start_address + (region_size - pattern_size) + best_anchor; | |
| 431 | |||
| 432 | // Hoist runtime CPU detection. The query itself is a function-local | ||
| 433 | // static behind a one-shot init, but reading it on every memchr hit | ||
| 434 | // adds an indirect load per false candidate. Caching it once here | ||
| 435 | // lets the per-hit branch use a register-resident bool. | ||
| 436 | #ifdef DMK_HAS_AVX2 | ||
| 437 | 781 | const bool use_avx2 = cpu_has_avx2(); | |
| 438 | #endif | ||
| 439 | |||
| 440 |
1/2✓ Branch 64 → 15 taken 847207 times.
✗ Branch 64 → 65 not taken.
|
847207 | while (search_start <= search_end) |
| 441 | { | ||
| 442 | 847207 | const void *found = memchr(search_start, static_cast<int>(target_val), | |
| 443 | 847207 | static_cast<size_t>(search_end - search_start + 1)); | |
| 444 | |||
| 445 |
2/2✓ Branch 15 → 16 taken 713 times.
✓ Branch 15 → 17 taken 846494 times.
|
847207 | if (!found) |
| 446 | { | ||
| 447 | 713 | break; | |
| 448 | } | ||
| 449 | |||
| 450 | 846494 | const std::byte *current_scan_ptr = static_cast<const std::byte *>(found); | |
| 451 | 846494 | const std::byte *pattern_start = current_scan_ptr - best_anchor; | |
| 452 | |||
| 453 | // Verify the full pattern at this position. | ||
| 454 | // Three-tier SIMD: AVX2 (32B) -> SSE2 (16B) -> scalar (1B). | ||
| 455 | 846494 | bool match_found = true; | |
| 456 | 846494 | size_t j = 0; | |
| 457 | |||
| 458 | #ifdef DMK_HAS_AVX2 | ||
| 459 |
1/2✓ Branch 17 → 18 taken 846494 times.
✗ Branch 17 → 25 not taken.
|
846494 | if (use_avx2) |
| 460 | { | ||
| 461 | 846494 | const auto next_j = verify_pattern_avx2(pattern_start, pattern, 0); | |
| 462 |
2/2✓ Branch 20 → 21 taken 846493 times.
✓ Branch 20 → 23 taken 1 time.
|
846494 | if (next_j.has_value()) |
| 463 | { | ||
| 464 | 846493 | j = *next_j; | |
| 465 | } | ||
| 466 | else | ||
| 467 | { | ||
| 468 | 1 | match_found = false; | |
| 469 | } | ||
| 470 | } | ||
| 471 | #endif // DMK_HAS_AVX2 | ||
| 472 | |||
| 473 | #ifdef DMK_HAS_SSE2 | ||
| 474 |
4/4✓ Branch 47 → 48 taken 846512 times.
✓ Branch 47 → 49 taken 1 time.
✓ Branch 48 → 26 taken 451512 times.
✓ Branch 48 → 49 taken 395000 times.
|
846513 | for (; match_found && j + 16 <= pattern_size; j += 16) |
| 475 | { | ||
| 476 | 451512 | const __m128i mem = _mm_loadu_si128( | |
| 477 | 451512 | reinterpret_cast<const __m128i *>(pattern_start + j)); | |
| 478 | 451512 | const __m128i pat = _mm_loadu_si128( | |
| 479 | 451512 | reinterpret_cast<const __m128i *>(pattern.bytes.data() + j)); | |
| 480 | 451512 | const __m128i msk = _mm_loadu_si128( | |
| 481 | 451512 | reinterpret_cast<const __m128i *>(pattern.mask.data() + j)); | |
| 482 | |||
| 483 | 451512 | const __m128i xored = _mm_xor_si128(mem, pat); | |
| 484 | 451512 | const __m128i masked = _mm_and_si128(xored, msk); | |
| 485 | 903024 | const __m128i cmp = _mm_cmpeq_epi8(masked, _mm_setzero_si128()); | |
| 486 | |||
| 487 |
2/2✓ Branch 44 → 45 taken 451493 times.
✓ Branch 44 → 46 taken 19 times.
|
451512 | if (_mm_movemask_epi8(cmp) != 0xFFFF) |
| 488 | { | ||
| 489 | 451493 | match_found = false; | |
| 490 | 451493 | break; | |
| 491 | } | ||
| 492 | } | ||
| 493 | #endif // DMK_HAS_SSE2 | ||
| 494 | |||
| 495 |
4/4✓ Branch 59 → 60 taken 998469 times.
✓ Branch 59 → 61 taken 846426 times.
✓ Branch 60 → 50 taken 998401 times.
✓ Branch 60 → 61 taken 68 times.
|
1844895 | for (; match_found && j < pattern_size; ++j) |
| 496 | { | ||
| 497 |
6/6✓ Branch 51 → 52 taken 998335 times.
✓ Branch 51 → 55 taken 66 times.
✓ Branch 53 → 54 taken 394932 times.
✓ Branch 53 → 55 taken 603403 times.
✓ Branch 56 → 57 taken 394932 times.
✓ Branch 56 → 58 taken 603469 times.
|
998401 | if (pattern.mask[j] != std::byte{0x00} && pattern_start[j] != pattern.bytes[j]) |
| 498 | { | ||
| 499 | 394932 | match_found = false; | |
| 500 | } | ||
| 501 | } | ||
| 502 | |||
| 503 |
2/2✓ Branch 61 → 62 taken 68 times.
✓ Branch 61 → 63 taken 846426 times.
|
846494 | if (match_found) |
| 504 | { | ||
| 505 | 68 | return pattern_start; | |
| 506 | } | ||
| 507 | |||
| 508 | // No match, continue searching from next position | ||
| 509 | 846426 | search_start = current_scan_ptr + 1; | |
| 510 | } | ||
| 511 | |||
| 512 | 713 | return nullptr; | |
| 513 | } | ||
| 514 | } // anonymous namespace | ||
| 515 | |||
| 516 | 16 | const std::byte *DetourModKit::Scanner::find_pattern(const std::byte *start_address, size_t region_size, | |
| 517 | const CompiledPattern &pattern, size_t occurrence) | ||
| 518 | { | ||
| 519 |
2/2✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 14 times.
|
16 | if (occurrence == 0) |
| 520 | { | ||
| 521 | 2 | return nullptr; | |
| 522 | } | ||
| 523 | |||
| 524 | 14 | Logger &logger = Logger::get_instance(); | |
| 525 |
2/2✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 8 taken 12 times.
|
14 | if (!validate_find_pattern_inputs(start_address, pattern, logger)) |
| 526 | { | ||
| 527 | 2 | return nullptr; | |
| 528 | } | ||
| 529 | |||
| 530 | 12 | const std::byte *cursor = start_address; | |
| 531 | 12 | size_t remaining = region_size; | |
| 532 | 12 | size_t found_count = 0; | |
| 533 | |||
| 534 | // Iterate via the raw helper so the `match + 1` continuation stays | ||
| 535 | // correct regardless of the pattern's offset marker. Offset is applied | ||
| 536 | // exactly once when we return the Nth hit. | ||
| 537 |
2/2✓ Branch 16 → 9 taken 25 times.
✓ Branch 16 → 17 taken 1 time.
|
26 | while (remaining >= pattern.size()) |
| 538 | { | ||
| 539 | 25 | const std::byte *match = find_pattern_raw(cursor, remaining, pattern); | |
| 540 |
2/2✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 24 times.
|
25 | if (!match) |
| 541 | { | ||
| 542 | 1 | break; | |
| 543 | } | ||
| 544 |
2/2✓ Branch 12 → 13 taken 10 times.
✓ Branch 12 → 14 taken 14 times.
|
24 | if (++found_count == occurrence) |
| 545 | { | ||
| 546 | 10 | return match + pattern.offset; | |
| 547 | } | ||
| 548 | 14 | const size_t advance = static_cast<size_t>(match - cursor) + 1; | |
| 549 | 14 | cursor += advance; | |
| 550 | 14 | remaining -= advance; | |
| 551 | } | ||
| 552 | |||
| 553 | 2 | return nullptr; | |
| 554 | } | ||
| 555 | |||
| 556 | 27 | std::expected<uintptr_t, DetourModKit::RipResolveError> DetourModKit::Scanner::resolve_rip_relative( | |
| 557 | const std::byte *instruction_address, | ||
| 558 | size_t displacement_offset, | ||
| 559 | size_t instruction_length) | ||
| 560 | { | ||
| 561 |
2/2✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 6 taken 24 times.
|
27 | if (!instruction_address) |
| 562 | { | ||
| 563 | 3 | return std::unexpected(RipResolveError::NullInput); | |
| 564 | } | ||
| 565 | |||
| 566 | 24 | const std::byte *disp_ptr = instruction_address + displacement_offset; | |
| 567 |
3/4✓ Branch 6 → 7 taken 24 times.
✗ Branch 6 → 15 not taken.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 11 taken 23 times.
|
24 | if (!Memory::is_readable(disp_ptr, sizeof(int32_t))) |
| 568 | { | ||
| 569 | 1 | return std::unexpected(RipResolveError::UnreadableDisplacement); | |
| 570 | } | ||
| 571 | |||
| 572 | int32_t displacement; | ||
| 573 | 23 | std::memcpy(&displacement, disp_ptr, sizeof(int32_t)); | |
| 574 | |||
| 575 | // Compute the target in unsigned modular arithmetic so the math stays | ||
| 576 | // well-defined on every input, including kernel-range instruction | ||
| 577 | // addresses (where intptr_t would be negative and signed overflow is UB). | ||
| 578 | // The displacement is sign-extended first so negative disp32 values wrap | ||
| 579 | // to the correct 64-bit offset. | ||
| 580 | 23 | const uintptr_t base = reinterpret_cast<uintptr_t>(instruction_address); | |
| 581 | 23 | const uintptr_t disp_sext = static_cast<uintptr_t>(static_cast<int64_t>(displacement)); | |
| 582 | 23 | return base + instruction_length + disp_sext; | |
| 583 | } | ||
| 584 | |||
| 585 | 20 | std::expected<uintptr_t, DetourModKit::RipResolveError> DetourModKit::Scanner::find_and_resolve_rip_relative( | |
| 586 | const std::byte *search_start, | ||
| 587 | size_t search_length, | ||
| 588 | std::span<const std::byte> opcode_prefix, | ||
| 589 | size_t instruction_length) | ||
| 590 | { | ||
| 591 |
6/6✓ Branch 2 → 3 taken 18 times.
✓ Branch 2 → 5 taken 2 times.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 17 times.
✓ Branch 7 → 8 taken 3 times.
✓ Branch 7 → 11 taken 17 times.
|
20 | if (!search_start || opcode_prefix.empty()) |
| 592 | { | ||
| 593 | 3 | return std::unexpected(RipResolveError::NullInput); | |
| 594 | } | ||
| 595 | |||
| 596 | 17 | const size_t prefix_len = opcode_prefix.size(); | |
| 597 | 17 | const size_t min_bytes = prefix_len + sizeof(int32_t); | |
| 598 |
2/2✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 16 taken 15 times.
|
17 | if (search_length < min_bytes) |
| 599 | { | ||
| 600 | 2 | return std::unexpected(RipResolveError::RegionTooSmall); | |
| 601 | } | ||
| 602 | |||
| 603 | 15 | const size_t scan_limit = search_length - min_bytes; | |
| 604 | 15 | const std::byte first = opcode_prefix[0]; | |
| 605 | |||
| 606 |
2/2✓ Branch 29 → 18 taken 141 times.
✓ Branch 29 → 30 taken 2 times.
|
143 | for (size_t i = 0; i <= scan_limit; ++i) |
| 607 | { | ||
| 608 |
2/2✓ Branch 18 → 19 taken 127 times.
✓ Branch 18 → 20 taken 14 times.
|
141 | if (search_start[i] != first) |
| 609 | { | ||
| 610 | 127 | continue; | |
| 611 | } | ||
| 612 | |||
| 613 |
6/6✓ Branch 20 → 21 taken 7 times.
✓ Branch 20 → 24 taken 7 times.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 24 taken 6 times.
✓ Branch 25 → 26 taken 1 time.
✓ Branch 25 → 27 taken 13 times.
|
14 | if (prefix_len > 1 && std::memcmp(&search_start[i + 1], opcode_prefix.data() + 1, prefix_len - 1) != 0) |
| 614 | { | ||
| 615 | 1 | continue; | |
| 616 | } | ||
| 617 | |||
| 618 | 13 | return resolve_rip_relative(&search_start[i], prefix_len, instruction_length); | |
| 619 | } | ||
| 620 | |||
| 621 | 2 | return std::unexpected(RipResolveError::PrefixNotFound); | |
| 622 | } | ||
| 623 | |||
| 624 | 27 | const std::byte *DetourModKit::Scanner::scan_executable_regions(const CompiledPattern &pattern, size_t occurrence) | |
| 625 | { | ||
| 626 |
6/6✓ Branch 3 → 4 taken 26 times.
✓ Branch 3 → 5 taken 1 time.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 25 times.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 25 times.
|
27 | if (pattern.empty() || occurrence == 0) |
| 627 | 2 | return nullptr; | |
| 628 | |||
| 629 |
1/2✓ Branch 9 → 10 taken 25 times.
✗ Branch 9 → 61 not taken.
|
25 | Logger &logger = Logger::get_instance(); |
| 630 | |||
| 631 |
1/2✗ Branch 11 → 12 not taken.
✓ Branch 11 → 14 taken 25 times.
|
25 | if (!pattern_has_literal_byte(pattern)) |
| 632 | { | ||
| 633 | ✗ | logger.warning("scan_executable_regions: pattern contains no literal " | |
| 634 | "bytes (all wildcards); returning first readable region " | ||
| 635 | "start unchanged"); | ||
| 636 | } | ||
| 637 | |||
| 638 | // Only scan pages we can actually *read*. Bare PAGE_EXECUTE grants execute | ||
| 639 | // rights without read, so dereferencing such a page raises an access | ||
| 640 | // violation. Omitting it keeps find_pattern safe on all walked regions. | ||
| 641 | 25 | constexpr DWORD READABLE_EXEC_FLAGS = PAGE_EXECUTE_READ | | |
| 642 | PAGE_EXECUTE_READWRITE | | ||
| 643 | PAGE_EXECUTE_WRITECOPY; | ||
| 644 | |||
| 645 | 25 | size_t matches_remaining = occurrence; | |
| 646 | 25 | MEMORY_BASIC_INFORMATION mbi{}; | |
| 647 | 25 | uintptr_t addr = 0; | |
| 648 | |||
| 649 |
3/4✓ Branch 51 → 52 taken 8579 times.
✗ Branch 51 → 61 not taken.
✓ Branch 52 → 15 taken 8565 times.
✓ Branch 52 → 53 taken 14 times.
|
8579 | while (VirtualQuery(reinterpret_cast<LPCVOID>(addr), &mbi, sizeof(mbi))) |
| 650 | { | ||
| 651 | // Skip non-readable / hostile protection states regardless of the | ||
| 652 | // execute bits: guard pages trigger STATUS_GUARD_PAGE_VIOLATION on | ||
| 653 | // access, and PAGE_NOACCESS will AV even for reads. | ||
| 654 | 8565 | const bool protection_unsafe = (mbi.Protect & (PAGE_GUARD | PAGE_NOACCESS)) != 0; | |
| 655 |
1/2✗ Branch 15 → 16 not taken.
✓ Branch 15 → 18 taken 8565 times.
|
8565 | const bool execute_only = (mbi.Protect & PAGE_EXECUTE) != 0 && |
| 656 | ✗ | (mbi.Protect & READABLE_EXEC_FLAGS) == 0; | |
| 657 | |||
| 658 |
1/6✗ Branch 19 → 20 not taken.
✓ Branch 19 → 28 taken 8565 times.
✗ Branch 20 → 21 not taken.
✗ Branch 20 → 28 not taken.
✗ Branch 21 → 22 not taken.
✗ Branch 21 → 28 not taken.
|
8565 | if (execute_only && !protection_unsafe && mbi.State == MEM_COMMIT) |
| 659 | { | ||
| 660 | ✗ | if (logger.is_enabled(LogLevel::Trace)) | |
| 661 | { | ||
| 662 | ✗ | logger.trace("scan_executable_regions: skipping pure-execute " | |
| 663 | "region at {} (size {}) - not readable", | ||
| 664 | ✗ | Format::format_address(reinterpret_cast<uintptr_t>(mbi.BaseAddress)), | |
| 665 | mbi.RegionSize); | ||
| 666 | } | ||
| 667 | } | ||
| 668 | |||
| 669 |
2/2✓ Branch 29 → 30 taken 718 times.
✓ Branch 29 → 34 taken 5948 times.
|
6666 | if (mbi.State == MEM_COMMIT && (mbi.Protect & READABLE_EXEC_FLAGS) != 0 && |
| 670 |
7/8✓ Branch 28 → 29 taken 6666 times.
✓ Branch 28 → 34 taken 1899 times.
✓ Branch 30 → 31 taken 717 times.
✓ Branch 30 → 34 taken 1 time.
✓ Branch 32 → 33 taken 717 times.
✗ Branch 32 → 34 not taken.
✓ Branch 35 → 36 taken 717 times.
✓ Branch 35 → 45 taken 7848 times.
|
15231 | !protection_unsafe && mbi.RegionSize >= pattern.size()) |
| 671 | { | ||
| 672 | 717 | const auto *region_start = reinterpret_cast<const std::byte *>(mbi.BaseAddress); | |
| 673 | |||
| 674 | // Use the raw helper so our own `+ pattern.offset` at the final | ||
| 675 | // return applies exactly once (the public find_pattern already | ||
| 676 | // applies offset; calling it here would double-apply). | ||
| 677 | 717 | const std::byte *match = find_pattern_raw(region_start, mbi.RegionSize, pattern); | |
| 678 |
2/2✓ Branch 43 → 38 taken 17 times.
✓ Branch 43 → 44 taken 706 times.
|
723 | while (match != nullptr) |
| 679 | { | ||
| 680 | 17 | --matches_remaining; | |
| 681 |
2/2✓ Branch 38 → 39 taken 11 times.
✓ Branch 38 → 40 taken 6 times.
|
17 | if (matches_remaining == 0) |
| 682 | 11 | return match + pattern.offset; | |
| 683 | |||
| 684 | // Continue scanning past the current match | ||
| 685 | 6 | const size_t consumed = static_cast<size_t>(match - region_start) + 1; | |
| 686 |
1/2✗ Branch 40 → 41 not taken.
✓ Branch 40 → 42 taken 6 times.
|
6 | if (consumed >= mbi.RegionSize) |
| 687 | ✗ | break; | |
| 688 | 6 | match = find_pattern_raw(match + 1, mbi.RegionSize - consumed, pattern); | |
| 689 | } | ||
| 690 | } | ||
| 691 | |||
| 692 | 8554 | const uintptr_t next = reinterpret_cast<uintptr_t>(mbi.BaseAddress) + mbi.RegionSize; | |
| 693 |
1/2✗ Branch 45 → 46 not taken.
✓ Branch 45 → 47 taken 8554 times.
|
8554 | assert(next > addr && "VirtualQuery returned a non-advancing region"); |
| 694 |
1/2✗ Branch 48 → 49 not taken.
✓ Branch 48 → 50 taken 8554 times.
|
8554 | if (next <= addr) |
| 695 | ✗ | break; // Overflow guard | |
| 696 | 8554 | addr = next; | |
| 697 | } | ||
| 698 | |||
| 699 | 14 | return nullptr; | |
| 700 | } | ||
| 701 | |||
| 702 | 4 | Scanner::SimdLevel DetourModKit::Scanner::active_simd_level() noexcept | |
| 703 | { | ||
| 704 | #ifdef DMK_HAS_AVX2 | ||
| 705 |
1/2✓ Branch 3 → 4 taken 4 times.
✗ Branch 3 → 5 not taken.
|
4 | if (cpu_has_avx2()) |
| 706 | 4 | return SimdLevel::Avx2; | |
| 707 | #endif | ||
| 708 | #ifdef DMK_HAS_SSE2 | ||
| 709 | ✗ | return SimdLevel::Sse2; | |
| 710 | #else | ||
| 711 | return SimdLevel::Scalar; | ||
| 712 | #endif | ||
| 713 | } | ||
| 714 | |||
| 715 | namespace | ||
| 716 | { | ||
| 717 | ✗ | std::uintptr_t resolve_candidate_match(std::uintptr_t match_addr, | |
| 718 | const DetourModKit::Scanner::AddrCandidate &c) noexcept | ||
| 719 | { | ||
| 720 | using DetourModKit::Scanner::ResolveMode; | ||
| 721 | ✗ | if (c.mode == ResolveMode::Direct) | |
| 722 | { | ||
| 723 | ✗ | return match_addr + static_cast<std::uintptr_t>(c.disp_offset); | |
| 724 | } | ||
| 725 | ✗ | const auto disp_addr = match_addr + static_cast<std::uintptr_t>(c.disp_offset); | |
| 726 | ✗ | if (!DetourModKit::Memory::is_readable(reinterpret_cast<const void *>(disp_addr), sizeof(std::int32_t))) | |
| 727 | { | ||
| 728 | ✗ | return 0; | |
| 729 | } | ||
| 730 | ✗ | std::int32_t disp = 0; | |
| 731 | ✗ | std::memcpy(&disp, reinterpret_cast<const void *>(disp_addr), sizeof(disp)); | |
| 732 | return static_cast<std::uintptr_t>( | ||
| 733 | ✗ | static_cast<std::int64_t>(match_addr + static_cast<std::uintptr_t>(c.instr_end_offset)) + | |
| 734 | ✗ | disp); | |
| 735 | } | ||
| 736 | |||
| 737 | // Minimum number of literal (non-wildcard) bytes the tail of the pattern | ||
| 738 | // must contain after dropping the first 5 prologue tokens. Five literal | ||
| 739 | // bytes still leave the rebuilt pattern shaped like a generic near-JMP | ||
| 740 | // plus a short common-instruction tail, which collides with thousands of | ||
| 741 | // unrelated E9 sites in a multi-megabyte .text section. Ten literal bytes | ||
| 742 | // is roughly two to four real instructions of context and reduces the | ||
| 743 | // false-positive rate to near zero on real binaries while staying inside | ||
| 744 | // the 12 to 20 byte sweet spot documented for fallback signatures. | ||
| 745 | constexpr int kPrologueFallbackMinTailLiterals = 10; | ||
| 746 | |||
| 747 | // Upper bound on hits the rebuilt fallback pattern may produce across the | ||
| 748 | // process's executable regions before we reject it as ambiguous. The | ||
| 749 | // fallback only exists to recover the single site where a sibling mod | ||
| 750 | // inline-hooked the target function, so the legitimate rewritten pattern | ||
| 751 | // must match exactly once: the unique JMP into that mod's trampoline. | ||
| 752 | // Any value above 1 admits a false positive whose blast radius (a hook | ||
| 753 | // installed at an unrelated function) far outweighs the benefit of | ||
| 754 | // tolerating duplicate matches. | ||
| 755 | constexpr std::size_t kPrologueFallbackMaxHits = 1; | ||
| 756 | |||
| 757 | 47 | bool is_wildcard_token(std::string_view token) noexcept | |
| 758 | { | ||
| 759 |
3/4✓ Branch 4 → 5 taken 47 times.
✗ Branch 4 → 8 not taken.
✓ Branch 7 → 8 taken 4 times.
✓ Branch 7 → 9 taken 43 times.
|
47 | return token == "?" || token == "??"; |
| 760 | } | ||
| 761 | |||
| 762 | // Walks the AOB token stream and splits it into (first 5 byte-tokens, tail). | ||
| 763 | // The `|` anchor marker is stripped because the rebuilt pattern targets | ||
| 764 | // the hooked-prologue start. Returns false if the source has fewer than | ||
| 765 | // 5 byte-tokens. | ||
| 766 | struct PrologueSplit | ||
| 767 | { | ||
| 768 | std::vector<std::string_view> tail_tokens; | ||
| 769 | int literal_tail_count{0}; | ||
| 770 | }; | ||
| 771 | |||
| 772 | 6 | bool split_prologue(std::string_view orig, PrologueSplit &out) noexcept | |
| 773 | { | ||
| 774 | 6 | std::size_t i = 0; | |
| 775 | 6 | int byte_tokens = 0; | |
| 776 |
2/2✓ Branch 54 → 3 taken 77 times.
✓ Branch 54 → 55 taken 6 times.
|
83 | while (i < orig.size()) |
| 777 | { | ||
| 778 |
6/8✓ Branch 6 → 7 taken 148 times.
✗ Branch 6 → 16 not taken.
✓ Branch 8 → 9 taken 77 times.
✓ Branch 8 → 15 taken 71 times.
✓ Branch 10 → 11 taken 77 times.
✗ Branch 10 → 15 not taken.
✓ Branch 17 → 4 taken 71 times.
✓ Branch 17 → 18 taken 77 times.
|
225 | while (i < orig.size() && (orig[i] == ' ' || orig[i] == '\t' || |
| 779 |
2/4✓ Branch 12 → 13 taken 77 times.
✗ Branch 12 → 15 not taken.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 77 times.
|
77 | orig[i] == '\n' || orig[i] == '\r')) |
| 780 | { | ||
| 781 | 71 | ++i; | |
| 782 | } | ||
| 783 |
1/2✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 77 times.
|
77 | if (i >= orig.size()) |
| 784 | { | ||
| 785 | ✗ | break; | |
| 786 | } | ||
| 787 |
1/2✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 77 times.
|
77 | if (orig[i] == '|') |
| 788 | { | ||
| 789 | ✗ | ++i; | |
| 790 | ✗ | continue; | |
| 791 | } | ||
| 792 | 77 | const std::size_t tok_start = i; | |
| 793 |
3/4✓ Branch 29 → 30 taken 154 times.
✓ Branch 29 → 39 taken 71 times.
✓ Branch 31 → 32 taken 154 times.
✗ Branch 31 → 39 not taken.
|
456 | while (i < orig.size() && orig[i] != ' ' && orig[i] != '\t' && |
| 794 |
7/10✓ Branch 27 → 28 taken 225 times.
✓ Branch 27 → 39 taken 6 times.
✓ Branch 33 → 34 taken 154 times.
✗ Branch 33 → 39 not taken.
✓ Branch 35 → 36 taken 154 times.
✗ Branch 35 → 39 not taken.
✓ Branch 37 → 38 taken 154 times.
✗ Branch 37 → 39 not taken.
✓ Branch 40 → 25 taken 154 times.
✓ Branch 40 → 41 taken 77 times.
|
456 | orig[i] != '\n' && orig[i] != '\r' && orig[i] != '|') |
| 795 | { | ||
| 796 | 154 | ++i; | |
| 797 | } | ||
| 798 | 77 | const std::string_view tok = orig.substr(tok_start, i - tok_start); | |
| 799 |
1/2✗ Branch 43 → 44 not taken.
✓ Branch 43 → 45 taken 77 times.
|
77 | if (tok.empty()) |
| 800 | { | ||
| 801 | ✗ | continue; | |
| 802 | } | ||
| 803 |
2/2✓ Branch 45 → 46 taken 47 times.
✓ Branch 45 → 50 taken 30 times.
|
77 | if (byte_tokens >= 5) |
| 804 | { | ||
| 805 | 47 | out.tail_tokens.push_back(tok); | |
| 806 |
2/2✓ Branch 48 → 49 taken 43 times.
✓ Branch 48 → 50 taken 4 times.
|
47 | if (!is_wildcard_token(tok)) |
| 807 | { | ||
| 808 | 43 | ++out.literal_tail_count; | |
| 809 | } | ||
| 810 | } | ||
| 811 | 77 | ++byte_tokens; | |
| 812 | } | ||
| 813 | 6 | return byte_tokens >= 5; | |
| 814 | } | ||
| 815 | |||
| 816 | 6 | std::string build_hooked_prologue_pattern(std::string_view orig) | |
| 817 | { | ||
| 818 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 6 times.
|
6 | if (orig.empty()) |
| 819 | { | ||
| 820 | ✗ | return {}; | |
| 821 | } | ||
| 822 | 6 | PrologueSplit split; | |
| 823 |
1/2✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 6 times.
|
6 | if (!split_prologue(orig, split)) |
| 824 | { | ||
| 825 | ✗ | return {}; | |
| 826 | } | ||
| 827 |
2/2✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 10 taken 3 times.
|
6 | if (split.literal_tail_count < kPrologueFallbackMinTailLiterals) |
| 828 | { | ||
| 829 | 3 | return {}; | |
| 830 | } | ||
| 831 |
1/2✓ Branch 12 → 13 taken 3 times.
✗ Branch 12 → 35 not taken.
|
3 | std::string out = "E9 ?? ?? ?? ??"; |
| 832 |
2/2✓ Branch 29 → 16 taken 32 times.
✓ Branch 29 → 30 taken 3 times.
|
38 | for (const auto &tok : split.tail_tokens) |
| 833 | { | ||
| 834 |
1/2✓ Branch 18 → 19 taken 32 times.
✗ Branch 18 → 38 not taken.
|
32 | out.push_back(' '); |
| 835 |
1/2✓ Branch 19 → 20 taken 32 times.
✗ Branch 19 → 38 not taken.
|
32 | out.append(tok); |
| 836 | } | ||
| 837 | 3 | return out; | |
| 838 | 6 | } | |
| 839 | |||
| 840 | // Returns true if `addr` lies inside any currently loaded module's | ||
| 841 | // executable image range. Used to reject E9-rel32 destinations that | ||
| 842 | // resolve into unmapped or data-only memory. | ||
| 843 | 1 | bool is_address_in_module(std::uintptr_t addr) noexcept | |
| 844 | { | ||
| 845 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1 time.
|
1 | if (addr == 0) |
| 846 | { | ||
| 847 | ✗ | return false; | |
| 848 | } | ||
| 849 | 1 | HMODULE mod = nullptr; | |
| 850 | 1 | if (!GetModuleHandleExW( | |
| 851 | GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | | ||
| 852 | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, | ||
| 853 |
2/4✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 11 not taken.
|
1 | reinterpret_cast<LPCWSTR>(addr), &mod) || |
| 854 | ✗ | mod == nullptr) | |
| 855 | { | ||
| 856 | 1 | return false; | |
| 857 | } | ||
| 858 | ✗ | return true; | |
| 859 | } | ||
| 860 | |||
| 861 | // Counts up to (max_hits + 1) occurrences of `pattern` across executable | ||
| 862 | // regions. Returning max_hits+1 signals "too many to be unique". | ||
| 863 | 3 | std::size_t count_pattern_hits_bounded(const DetourModKit::Scanner::CompiledPattern &pattern, | |
| 864 | std::size_t max_hits) noexcept | ||
| 865 | { | ||
| 866 | 3 | std::size_t hits = 0; | |
| 867 |
2/2✓ Branch 7 → 3 taken 6 times.
✓ Branch 7 → 8 taken 2 times.
|
8 | for (std::size_t n = 1; n <= max_hits + 1; ++n) |
| 868 | { | ||
| 869 | 6 | const auto *match = DetourModKit::Scanner::scan_executable_regions(pattern, n); | |
| 870 |
2/2✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 5 times.
|
6 | if (match == nullptr) |
| 871 | { | ||
| 872 | 1 | break; | |
| 873 | } | ||
| 874 | 5 | ++hits; | |
| 875 | } | ||
| 876 | 3 | return hits; | |
| 877 | } | ||
| 878 | |||
| 879 | struct CascadeAttempt | ||
| 880 | { | ||
| 881 | std::uintptr_t address{0}; | ||
| 882 | size_t index{0}; | ||
| 883 | bool success{false}; | ||
| 884 | }; | ||
| 885 | |||
| 886 | 9 | CascadeAttempt scan_candidates(std::span<const DetourModKit::Scanner::AddrCandidate> candidates, | |
| 887 | bool &all_parse_failed, | ||
| 888 | DetourModKit::Logger &logger) | ||
| 889 | { | ||
| 890 | 9 | all_parse_failed = true; | |
| 891 |
2/2✓ Branch 27 → 3 taken 9 times.
✓ Branch 27 → 28 taken 9 times.
|
18 | for (size_t i = 0; i < candidates.size(); ++i) |
| 892 | { | ||
| 893 | 9 | const auto &c = candidates[i]; | |
| 894 |
1/2✓ Branch 4 → 5 taken 9 times.
✗ Branch 4 → 34 not taken.
|
9 | auto compiled = DetourModKit::Scanner::parse_aob(c.pattern); |
| 895 |
2/2✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 13 taken 8 times.
|
9 | if (!compiled) |
| 896 | { | ||
| 897 |
1/2✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 30 not taken.
|
1 | logger.warning("Scanner: Failed to parse AOB for candidate '{}'.", |
| 898 |
1/2✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1 time.
|
1 | c.name.empty() ? std::string_view{"<unnamed>"} : c.name); |
| 899 | 1 | continue; | |
| 900 | } | ||
| 901 | 8 | all_parse_failed = false; | |
| 902 |
1/2✓ Branch 14 → 15 taken 8 times.
✗ Branch 14 → 32 not taken.
|
8 | const auto *match = DetourModKit::Scanner::scan_executable_regions(*compiled); |
| 903 |
1/2✗ Branch 15 → 16 not taken.
✓ Branch 15 → 18 taken 8 times.
|
8 | if (match != nullptr) |
| 904 | { | ||
| 905 | ✗ | const auto addr = resolve_candidate_match( | |
| 906 | reinterpret_cast<std::uintptr_t>(match), c); | ||
| 907 | ✗ | return CascadeAttempt{addr, i, true}; | |
| 908 | } | ||
| 909 |
2/3✓ Branch 20 → 21 taken 8 times.
✓ Branch 20 → 23 taken 1 time.
✗ Branch 20 → 24 not taken.
|
9 | } |
| 910 | 9 | return CascadeAttempt{0, 0, false}; | |
| 911 | } | ||
| 912 | |||
| 913 | struct PrologueFallbackResult | ||
| 914 | { | ||
| 915 | CascadeAttempt attempt{}; | ||
| 916 | bool not_applicable{true}; | ||
| 917 | }; | ||
| 918 | |||
| 919 | 6 | PrologueFallbackResult scan_candidates_hooked_prologue( | |
| 920 | std::span<const DetourModKit::Scanner::AddrCandidate> candidates, | ||
| 921 | DetourModKit::Logger &logger) | ||
| 922 | { | ||
| 923 | using DetourModKit::Scanner::ResolveMode; | ||
| 924 | 6 | PrologueFallbackResult out; | |
| 925 |
2/2✓ Branch 59 → 3 taken 6 times.
✓ Branch 59 → 60 taken 6 times.
|
12 | for (size_t i = 0; i < candidates.size(); ++i) |
| 926 | { | ||
| 927 | 6 | const auto &c = candidates[i]; | |
| 928 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 6 times.
|
6 | if (c.mode != ResolveMode::Direct) |
| 929 | { | ||
| 930 | 6 | continue; | |
| 931 | } | ||
| 932 |
1/2✓ Branch 6 → 7 taken 6 times.
✗ Branch 6 → 72 not taken.
|
6 | auto hooked = build_hooked_prologue_pattern(c.pattern); |
| 933 |
2/2✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 15 taken 3 times.
|
6 | if (hooked.empty()) |
| 934 | { | ||
| 935 |
1/2✓ Branch 13 → 14 taken 3 times.
✗ Branch 13 → 62 not taken.
|
3 | logger.debug("Scanner: prologue fallback skipped for '{}' (insufficient literal tail bytes)", |
| 936 |
1/2✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 3 times.
|
3 | c.name.empty() ? std::string_view{"<unnamed>"} : c.name); |
| 937 | 3 | continue; | |
| 938 | } | ||
| 939 |
1/2✓ Branch 16 → 17 taken 3 times.
✗ Branch 16 → 70 not taken.
|
3 | auto compiled = DetourModKit::Scanner::parse_aob(hooked); |
| 940 |
1/2✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 3 times.
|
3 | if (!compiled) |
| 941 | { | ||
| 942 | ✗ | continue; | |
| 943 | } | ||
| 944 | 3 | out.not_applicable = false; | |
| 945 | const std::size_t hits = | ||
| 946 | 3 | count_pattern_hits_bounded(*compiled, kPrologueFallbackMaxHits); | |
| 947 |
1/2✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 3 times.
|
3 | if (hits == 0) |
| 948 | { | ||
| 949 | ✗ | continue; | |
| 950 | } | ||
| 951 |
2/2✓ Branch 24 → 25 taken 2 times.
✓ Branch 24 → 31 taken 1 time.
|
3 | if (hits > kPrologueFallbackMaxHits) |
| 952 | { | ||
| 953 |
1/2✓ Branch 29 → 30 taken 2 times.
✗ Branch 29 → 64 not taken.
|
2 | logger.debug( |
| 954 | "Scanner: prologue fallback rejected for '{}': {} hits exceed uniqueness ceiling ({})", | ||
| 955 |
1/2✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 2 times.
|
2 | c.name.empty() ? std::string_view{"<unnamed>"} : c.name, |
| 956 | hits, kPrologueFallbackMaxHits); | ||
| 957 | 2 | continue; | |
| 958 | } | ||
| 959 |
1/2✓ Branch 32 → 33 taken 1 time.
✗ Branch 32 → 68 not taken.
|
1 | const auto *match = DetourModKit::Scanner::scan_executable_regions(*compiled); |
| 960 |
1/2✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 1 time.
|
1 | if (match == nullptr) |
| 961 | { | ||
| 962 | ✗ | continue; | |
| 963 | } | ||
| 964 | |||
| 965 | 1 | const auto match_addr = reinterpret_cast<std::uintptr_t>(match); | |
| 966 | 1 | const auto decoded = DetourModKit::detail::decode_e9_rel32(match_addr); | |
| 967 |
1/2✗ Branch 37 → 38 not taken.
✓ Branch 37 → 39 taken 1 time.
|
1 | if (!decoded) |
| 968 | { | ||
| 969 | ✗ | continue; | |
| 970 | } | ||
| 971 | 1 | const auto jmp_destination = *decoded; | |
| 972 |
1/2✓ Branch 41 → 42 taken 1 time.
✗ Branch 41 → 48 not taken.
|
1 | if (!is_address_in_module(jmp_destination)) |
| 973 | { | ||
| 974 |
1/2✓ Branch 46 → 47 taken 1 time.
✗ Branch 46 → 66 not taken.
|
1 | logger.debug( |
| 975 | "Scanner: prologue fallback rejected for '{}': E9 destination {} not in any module", | ||
| 976 |
1/2✗ Branch 43 → 44 not taken.
✓ Branch 43 → 45 taken 1 time.
|
1 | c.name.empty() ? std::string_view{"<unnamed>"} : c.name, |
| 977 | jmp_destination); | ||
| 978 | 1 | continue; | |
| 979 | } | ||
| 980 | |||
| 981 | ✗ | const auto addr = resolve_candidate_match(match_addr, c); | |
| 982 | ✗ | out.attempt = CascadeAttempt{addr, i, true}; | |
| 983 | ✗ | return out; | |
| 984 |
2/4✓ Branch 51 → 52 taken 3 times.
✗ Branch 51 → 53 not taken.
✓ Branch 55 → 56 taken 6 times.
✗ Branch 55 → 57 not taken.
|
9 | } |
| 985 | 6 | return out; | |
| 986 | } | ||
| 987 | } // anonymous namespace | ||
| 988 | |||
| 989 | std::expected<DetourModKit::Scanner::ResolveHit, DetourModKit::Scanner::ResolveError> | ||
| 990 | 4 | DetourModKit::Scanner::resolve_cascade(std::span<const AddrCandidate> candidates, | |
| 991 | std::string_view label) | ||
| 992 | { | ||
| 993 |
1/2✓ Branch 2 → 3 taken 4 times.
✗ Branch 2 → 40 not taken.
|
4 | auto &logger = Logger::get_instance(); |
| 994 | |||
| 995 |
2/2✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 9 taken 3 times.
|
4 | if (candidates.empty()) |
| 996 | { | ||
| 997 |
1/2✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 32 not taken.
|
1 | logger.warning("Scanner: resolve_cascade for '{}' called with no candidates.", label); |
| 998 | 1 | return std::unexpected(ResolveError::EmptyCandidates); | |
| 999 | } | ||
| 1000 | |||
| 1001 | 3 | bool all_parse_failed = true; | |
| 1002 |
1/2✓ Branch 9 → 10 taken 3 times.
✗ Branch 9 → 40 not taken.
|
3 | const auto attempt = scan_candidates(candidates, all_parse_failed, logger); |
| 1003 |
1/2✗ Branch 10 → 11 not taken.
✓ Branch 10 → 21 taken 3 times.
|
3 | if (attempt.success) |
| 1004 | { | ||
| 1005 | ✗ | const auto &winner = candidates[attempt.index]; | |
| 1006 | ✗ | logger.debug("{} resolved via '{}' at {}", label, | |
| 1007 | ✗ | winner.name.empty() ? std::string_view{"<unnamed>"} : winner.name, | |
| 1008 | ✗ | Format::format_address(attempt.address)); | |
| 1009 | ✗ | return ResolveHit{attempt.address, winner.name}; | |
| 1010 | } | ||
| 1011 | |||
| 1012 |
2/2✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 26 taken 2 times.
|
3 | if (all_parse_failed) |
| 1013 | { | ||
| 1014 |
1/2✓ Branch 22 → 23 taken 1 time.
✗ Branch 22 → 38 not taken.
|
1 | logger.error("{}: every candidate pattern failed to parse.", label); |
| 1015 | 1 | return std::unexpected(ResolveError::AllPatternsInvalid); | |
| 1016 | } | ||
| 1017 | |||
| 1018 |
1/2✓ Branch 26 → 27 taken 2 times.
✗ Branch 26 → 39 not taken.
|
2 | logger.warning("{}: cascade AOB scan failed (no candidate matched).", label); |
| 1019 | 2 | return std::unexpected(ResolveError::NoMatch); | |
| 1020 | } | ||
| 1021 | |||
| 1022 | std::expected<DetourModKit::Scanner::ResolveHit, DetourModKit::Scanner::ResolveError> | ||
| 1023 | 6 | DetourModKit::Scanner::resolve_cascade_with_prologue_fallback( | |
| 1024 | std::span<const AddrCandidate> candidates, std::string_view label) | ||
| 1025 | { | ||
| 1026 |
1/2✓ Branch 2 → 3 taken 6 times.
✗ Branch 2 → 63 not taken.
|
6 | auto &logger = Logger::get_instance(); |
| 1027 | |||
| 1028 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 9 taken 6 times.
|
6 | if (candidates.empty()) |
| 1029 | { | ||
| 1030 | ✗ | logger.warning("Scanner: resolve_cascade_with_prologue_fallback for '{}' called with no candidates.", label); | |
| 1031 | ✗ | return std::unexpected(ResolveError::EmptyCandidates); | |
| 1032 | } | ||
| 1033 | |||
| 1034 | 6 | bool all_parse_failed = true; | |
| 1035 |
1/2✓ Branch 9 → 10 taken 6 times.
✗ Branch 9 → 63 not taken.
|
6 | auto attempt = scan_candidates(candidates, all_parse_failed, logger); |
| 1036 |
1/2✗ Branch 10 → 11 not taken.
✓ Branch 10 → 21 taken 6 times.
|
6 | if (attempt.success) |
| 1037 | { | ||
| 1038 | ✗ | const auto &winner = candidates[attempt.index]; | |
| 1039 | ✗ | logger.debug("{} resolved via '{}' at {}", label, | |
| 1040 | ✗ | winner.name.empty() ? std::string_view{"<unnamed>"} : winner.name, | |
| 1041 | ✗ | Format::format_address(attempt.address)); | |
| 1042 | ✗ | return ResolveHit{attempt.address, winner.name}; | |
| 1043 | } | ||
| 1044 | |||
| 1045 |
1/2✓ Branch 21 → 22 taken 6 times.
✗ Branch 21 → 63 not taken.
|
6 | const auto hooked = scan_candidates_hooked_prologue(candidates, logger); |
| 1046 |
1/2✗ Branch 22 → 23 not taken.
✓ Branch 22 → 33 taken 6 times.
|
6 | if (hooked.attempt.success) |
| 1047 | { | ||
| 1048 | ✗ | const auto &winner = candidates[hooked.attempt.index]; | |
| 1049 | ✗ | logger.debug( | |
| 1050 | "{} resolved via '{}' at {} (pre-hooked prologue; reusing target)", | ||
| 1051 | label, | ||
| 1052 | ✗ | winner.name.empty() ? std::string_view{"<unnamed>"} : winner.name, | |
| 1053 | ✗ | Format::format_address(hooked.attempt.address)); | |
| 1054 | ✗ | return ResolveHit{hooked.attempt.address, winner.name}; | |
| 1055 | } | ||
| 1056 | |||
| 1057 |
1/2✗ Branch 33 → 34 not taken.
✓ Branch 33 → 38 taken 6 times.
|
6 | if (all_parse_failed) |
| 1058 | { | ||
| 1059 | ✗ | logger.error("{}: every candidate pattern failed to parse.", label); | |
| 1060 | ✗ | return std::unexpected(ResolveError::AllPatternsInvalid); | |
| 1061 | } | ||
| 1062 | |||
| 1063 |
2/2✓ Branch 38 → 39 taken 3 times.
✓ Branch 38 → 43 taken 3 times.
|
6 | if (hooked.not_applicable) |
| 1064 | { | ||
| 1065 |
1/2✓ Branch 39 → 40 taken 3 times.
✗ Branch 39 → 61 not taken.
|
3 | logger.warning("{}: cascade AOB scan failed; prologue fallback not applicable (insufficient literal tail bytes).", |
| 1066 | label); | ||
| 1067 | 3 | return std::unexpected(ResolveError::PrologueFallbackNotApplicable); | |
| 1068 | } | ||
| 1069 | |||
| 1070 |
1/2✓ Branch 43 → 44 taken 3 times.
✗ Branch 43 → 62 not taken.
|
3 | logger.warning("{}: cascade AOB scan failed (including prologue fallback).", label); |
| 1071 | 3 | return std::unexpected(ResolveError::NoMatch); | |
| 1072 | } | ||
| 1073 | |||
| 1074 | 10 | bool DetourModKit::Scanner::is_likely_function_prologue(std::uintptr_t addr) noexcept | |
| 1075 | { | ||
| 1076 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 9 times.
|
10 | if (addr == 0) |
| 1077 | { | ||
| 1078 | 1 | return false; | |
| 1079 | } | ||
| 1080 | |||
| 1081 | 9 | const auto *probe = reinterpret_cast<const void *>(addr); | |
| 1082 |
2/2✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 8 times.
|
9 | if (!Memory::is_readable(probe, 1)) |
| 1083 | { | ||
| 1084 | 1 | return false; | |
| 1085 | } | ||
| 1086 | |||
| 1087 | 8 | const auto b0 = *reinterpret_cast<const std::uint8_t *>(addr); | |
| 1088 |
8/8✓ Branch 7 → 8 taken 7 times.
✓ Branch 7 → 12 taken 1 time.
✓ Branch 8 → 9 taken 6 times.
✓ Branch 8 → 12 taken 1 time.
✓ Branch 9 → 10 taken 5 times.
✓ Branch 9 → 12 taken 1 time.
✓ Branch 10 → 11 taken 4 times.
✓ Branch 10 → 12 taken 1 time.
|
8 | return b0 != 0x00 && b0 != 0xCC && b0 != 0xC2 && b0 != 0xC3; |
| 1089 | } | ||
| 1090 |