GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 85.4% 399 / 0 / 467
Functions: 96.6% 28 / 0 / 29
Branches: 68.9% 325 / 0 / 472

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