GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 93.9% 428 / 0 / 456
Functions: 100.0% 37 / 0 / 37
Branches: 81.4% 338 / 0 / 415

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 "DetourModKit/diagnostics.hpp"
11
12 #include "scanner_internal.hpp"
13 #include "memory_internal.hpp"
14
15 #include <windows.h>
16 #include <vector>
17 #include <string>
18 #include <cctype>
19 #include <stdexcept>
20 #include <cstddef>
21 #include <cstdint>
22 #include <cassert>
23 #include <cstring>
24 #include <optional>
25
26 #if defined(__SSE2__) || defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2)
27 #define DMK_HAS_SSE2 1
28 #include <emmintrin.h>
29 #endif
30
31 // AVX2 support: compile-time header + runtime CPUID detection. On GCC/Clang, AVX2 intrinsics require either -mavx2
32 // globally or
33 // __attribute__((target("avx2"))) per function. We use the latter so the rest of the TU stays SSE2-only and runs on any
34 // x86-64 CPU. On MSVC, intrinsics are always available; runtime CPUID gates usage.
35 #if defined(__GNUC__) && (defined(__x86_64__) || defined(__i386__))
36 #define DMK_HAS_AVX2 1
37 #include <immintrin.h>
38 #include <cpuid.h>
39 #define DMK_AVX2_TARGET __attribute__((target("avx2")))
40 #elif defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86))
41 #define DMK_HAS_AVX2 1
42 #include <immintrin.h>
43 #include <intrin.h>
44 #define DMK_AVX2_TARGET
45 #endif
46
47 // AVX-512 verify tier: opt-in, off by default. Gated behind the DMK_ENABLE_AVX512 build option rather than a global
48 // /arch:AVX512 or -mavx512 flag, because enabling it must NOT let the compiler emit AVX-512 across the whole TU -- that
49 // would fault with #UD on the majority of CPUs that lack AVX-512. When the option is on, the verify tier is compiled
50 // with a per-function target attribute on GCC/Clang (exactly like the AVX2 tier), so the rest of the TU stays
51 // AVX2-only and runs anywhere; the tier is reached only after the runtime cpu_has_avx512() gate confirms both the CPU
52 // and the OS support it. Byte-granular masked compare (_mm512_test_epi8_mask) is an AVX-512BW instruction, so the gate
53 // requires AVX-512F + AVX-512BW, not F alone.
54 #if defined(DMK_ENABLE_AVX512) && defined(__GNUC__) && (defined(__x86_64__) || defined(__i386__))
55 #define DMK_HAS_AVX512 1
56 #include <immintrin.h>
57 #define DMK_AVX512_TARGET __attribute__((target("avx512f,avx512bw")))
58 #elif defined(DMK_ENABLE_AVX512) && defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86))
59 #define DMK_HAS_AVX512 1
60 #include <immintrin.h>
61 #define DMK_AVX512_TARGET
62 #endif
63
64 // AddressSanitizer poisons the shadow of this process's own committed, readable memory -- the redzones around stack
65 // locals and instrumented globals. The AOB scanner deliberately reads across whole readable regions, so under ASan its
66 // in-bounds, never-faulting reads land on poisoned shadow and are reported as overflows. DMK_NO_SANITIZE_ADDRESS
67 // removes the compiler's load instrumentation from such a function, so the read runs exactly as a release build does.
68 // It does NOT stop ASan's libc interceptors (memchr/memcpy are hot-patched at runtime); the scanner therefore routes
69 // the prefilter through a self-provided dmk_memchr that does its own byte comparisons and never calls into libc. The
70 // attribute also covers the verify path's instrumented SIMD/scalar loads. ASan links only under MSVC here (mingw-w64
71 // ships no sanitizer runtime), so the attribute is the MSVC __declspec form; the macro is empty in every other build,
72 // leaving release codegen unchanged.
73 #if defined(_MSC_VER) && defined(__SANITIZE_ADDRESS__)
74 #define DMK_NO_SANITIZE_ADDRESS __declspec(no_sanitize_address)
75 #else
76 #define DMK_NO_SANITIZE_ADDRESS
77 #endif
78
79 namespace DetourModKit
80 {
81 namespace
82 {
83 #if defined(DMK_HAS_AVX2) || defined(DMK_HAS_AVX512)
84 constexpr unsigned int CPUID_ECX_XSAVE = 1u << 26;
85 constexpr unsigned int CPUID_ECX_OSXSAVE = 1u << 27;
86 constexpr unsigned int CPUID_ECX_AVX = 1u << 28;
87 constexpr unsigned int XCR0_SSE = 1u << 1;
88 constexpr unsigned int XCR0_AVX = 1u << 2;
89 constexpr unsigned int XCR0_OPMASK = 1u << 5;
90 constexpr unsigned int XCR0_ZMM_HI256 = 1u << 6;
91 constexpr unsigned int XCR0_HI16_ZMM = 1u << 7;
92 constexpr unsigned int XCR0_AVX512_STATE = XCR0_SSE | XCR0_AVX | XCR0_OPMASK | XCR0_ZMM_HI256 | XCR0_HI16_ZMM;
93
94 /**
95 * @brief Tests CPUID leaf 1 ECX feature bits.
96 * @param required_bits Bit mask that must be present in ECX.
97 * @return True when every requested leaf 1 ECX feature bit is set.
98 */
99 402 bool cpu_leaf1_ecx_has(unsigned int required_bits) noexcept
100 {
101 #if defined(__GNUC__) || defined(__clang__)
102 402 unsigned int eax = 0, ebx = 0, ecx = 0, edx = 0;
103
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 402 times.
402 if (!__get_cpuid(1, &eax, &ebx, &ecx, &edx))
104 return false;
105 402 return (ecx & required_bits) == required_bits;
106 #elif defined(_MSC_VER)
107 int cpui[4]{};
108 __cpuidex(cpui, 1, 0);
109 const unsigned int ecx = static_cast<unsigned int>(cpui[2]);
110 return (ecx & required_bits) == required_bits;
111 #else
112 return false;
113 #endif
114 }
115
116 /**
117 * @brief Tests whether the OS has enabled the requested XCR0 SIMD register state.
118 * @param required_mask XCR0 bit mask that must be enabled by the OS.
119 * @return True when XGETBV is legal to execute and XCR0 contains every requested bit.
120 */
121 201 bool xcr0_has_enabled_state(unsigned int required_mask) noexcept
122 {
123
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 201 times.
201 if (!cpu_leaf1_ecx_has(CPUID_ECX_XSAVE | CPUID_ECX_OSXSAVE))
124 {
125 return false;
126 }
127
128 #if defined(__GNUC__) || defined(__clang__)
129 201 unsigned int xcr0_lo = 0, xcr0_hi = 0;
130 201 __asm__ volatile("xgetbv" : "=a"(xcr0_lo), "=d"(xcr0_hi) : "c"(0));
131 (void)xcr0_hi;
132 201 return (xcr0_lo & required_mask) == required_mask;
133 #elif defined(_MSC_VER)
134 const unsigned long long xcr0 = _xgetbv(0);
135 return (xcr0 & required_mask) == required_mask;
136 #else
137 return false;
138 #endif
139 }
140 #endif
141
142 #ifdef DMK_HAS_AVX2
143 /**
144 * @brief Detects AVX2 support at runtime via CPUID.
145 * @details Checks CPUID leaf 1 ECX bit 28 (AVX) plus CPUID leaf 7 subleaf 0 EBX bit 5 (AVX2), then verifies
146 * that
147 * the OS has enabled SSE and AVX register state in XCR0. Result is cached in a function-local static.
148 */
149 17830 bool cpu_has_avx2() noexcept
150 {
151 201 static const bool result = []() -> bool
152 {
153 #if defined(__GNUC__) || defined(__clang__)
154 201 unsigned int eax = 0, ebx = 0, ecx = 0, edx = 0;
155
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 201 times.
201 if (!__get_cpuid_count(7, 0, &eax, &ebx, &ecx, &edx))
156 return false;
157 201 const bool avx2_flag = (ebx & (1u << 5)) != 0;
158
159
3/6
✓ Branch 6 → 7 taken 201 times.
✗ Branch 6 → 11 not taken.
✓ Branch 7 → 8 taken 201 times.
✗ Branch 7 → 11 not taken.
✓ Branch 9 → 10 taken 201 times.
✗ Branch 9 → 11 not taken.
201 return cpu_leaf1_ecx_has(CPUID_ECX_AVX) && avx2_flag && xcr0_has_enabled_state(XCR0_SSE | XCR0_AVX);
160 #elif defined(_MSC_VER)
161 int cpui[4]{};
162 __cpuidex(cpui, 7, 0);
163 const bool avx2_flag = (cpui[1] & (1 << 5)) != 0;
164
165 return cpu_leaf1_ecx_has(CPUID_ECX_AVX) && avx2_flag && xcr0_has_enabled_state(XCR0_SSE | XCR0_AVX);
166 #else
167 return false;
168 #endif
169
3/4
✓ Branch 2 → 3 taken 201 times.
✓ Branch 2 → 8 taken 17629 times.
✓ Branch 4 → 5 taken 201 times.
✗ Branch 4 → 8 not taken.
17830 }();
170 17830 return result;
171 }
172 /**
173 * @brief Verifies a pattern match using AVX2 (32 bytes per iteration).
174 * @param pattern_start Start of the candidate region in memory.
175 * @param pattern The compiled pattern to verify against.
176 * @param start_offset Byte offset to start verification from (may be non-zero if a previous tier partially
177 * verified).
178 * @return The next byte offset to resume verification from on success (equal to pattern.size() when the AVX2
179 * tier
180 * covered the whole pattern), or std::nullopt when a 32-byte chunk did not match and the caller must
181 * abandon this candidate position.
182 * @note This function is compiled with AVX2 codegen via target attribute on
183 * GCC/Clang. On MSVC, intrinsics are always available.
184 */
185 DMK_AVX2_TARGET
186 DMK_NO_SANITIZE_ADDRESS
187 4558774 std::optional<size_t> verify_pattern_avx2(const std::byte *pattern_start,
188 const Scanner::CompiledPattern &pattern, size_t start_offset) noexcept
189 {
190 4558774 const size_t pattern_size = pattern.size();
191 4558775 size_t j = start_offset;
192
193
2/2
✓ Branch 25 → 4 taken 20 times.
✓ Branch 25 → 26 taken 4558773 times.
4558793 for (; j + 32 <= pattern_size; j += 32)
194 {
195 20 const __m256i mem = _mm256_loadu_si256(reinterpret_cast<const __m256i *>(pattern_start + j));
196 20 const __m256i pat = _mm256_loadu_si256(reinterpret_cast<const __m256i *>(pattern.bytes.data() + j));
197 40 const __m256i msk = _mm256_loadu_si256(reinterpret_cast<const __m256i *>(pattern.mask.data() + j));
198
199 20 const __m256i xored = _mm256_xor_si256(mem, pat);
200 20 const __m256i masked = _mm256_and_si256(xored, msk);
201 40 const __m256i cmp = _mm256_cmpeq_epi8(masked, _mm256_setzero_si256());
202
203
2/2
✓ Branch 22 → 23 taken 2 times.
✓ Branch 22 → 24 taken 18 times.
20 if (static_cast<unsigned int>(_mm256_movemask_epi8(cmp)) != 0xFFFFFFFFu)
204 {
205 2 return std::nullopt;
206 }
207 }
208
209 4558773 return j;
210 }
211 #endif // DMK_HAS_AVX2
212
213 #ifdef DMK_HAS_AVX512
214 /**
215 * @brief Detects AVX-512F + AVX-512BW support at runtime via CPUID and XGETBV.
216 * @details Checks CPUID leaf 7 subleaf 0, EBX bit 16 (AVX-512F) and bit 30 (AVX-512BW). Byte-granular masked
217 * compare is a BW instruction, so both are required. Also verifies the OS has enabled the full opmask
218 * / ZMM register state via XGETBV (XCR0 bits 1,2,5,6,7); a CPU that reports AVX-512 while the OS has
219 * not enabled the state must fail closed. Result is cached in a function-local static.
220 */
221 bool cpu_has_avx512() noexcept
222 {
223 static const bool result = []() -> bool
224 {
225 #if defined(__GNUC__) || defined(__clang__)
226 unsigned int eax = 0, ebx = 0, ecx = 0, edx = 0;
227 if (!__get_cpuid_count(7, 0, &eax, &ebx, &ecx, &edx))
228 return false;
229 const bool avx512f = (ebx & (1u << 16)) != 0;
230 const bool avx512bw = (ebx & (1u << 30)) != 0;
231
232 return cpu_leaf1_ecx_has(CPUID_ECX_AVX) && avx512f && avx512bw &&
233 xcr0_has_enabled_state(XCR0_AVX512_STATE);
234 #elif defined(_MSC_VER)
235 int cpui[4]{};
236 __cpuidex(cpui, 7, 0);
237 const bool avx512f = (cpui[1] & (1 << 16)) != 0;
238 const bool avx512bw = (cpui[1] & (1 << 30)) != 0;
239
240 return cpu_leaf1_ecx_has(CPUID_ECX_AVX) && avx512f && avx512bw &&
241 xcr0_has_enabled_state(XCR0_AVX512_STATE);
242 #else
243 return false;
244 #endif
245 }();
246 return result;
247 }
248
249 /**
250 * @brief Verifies a pattern match using AVX-512 (64 bytes per iteration).
251 * @param pattern_start Start of the candidate region in memory.
252 * @param pattern The compiled pattern to verify against.
253 * @param start_offset Byte offset to start verification from (may be non-zero if a previous tier partially
254 * verified).
255 * @return The next byte offset to resume verification from on success (equal to start_offset plus a multiple of
256 * 64 once the AVX-512 tier covered whole 64-byte chunks), or std::nullopt when a 64-byte chunk did not
257 * match and the caller must abandon this candidate position.
258 * @note Compiled with AVX-512F + AVX-512BW codegen via target attribute on GCC/Clang; on MSVC the intrinsics
259 * are
260 * always available. Only entered after cpu_has_avx512() has confirmed CPU and OS support.
261 */
262 DMK_AVX512_TARGET
263 DMK_NO_SANITIZE_ADDRESS
264 std::optional<size_t> verify_pattern_avx512(const std::byte *pattern_start,
265 const Scanner::CompiledPattern &pattern,
266 size_t start_offset) noexcept
267 {
268 const size_t pattern_size = pattern.size();
269 size_t j = start_offset;
270
271 for (; j + 64 <= pattern_size; j += 64)
272 {
273 const __m512i mem = _mm512_loadu_si512(reinterpret_cast<const void *>(pattern_start + j));
274 const __m512i pat = _mm512_loadu_si512(reinterpret_cast<const void *>(pattern.bytes.data() + j));
275 const __m512i msk = _mm512_loadu_si512(reinterpret_cast<const void *>(pattern.mask.data() + j));
276
277 // (mem ^ pat) & mask is zero in every matching byte: a wildcard lane (mask 0x00) clears to zero, and a
278 // literal lane (mask 0xFF) keeps the xor, which is zero only on an exact byte match. test_epi8_mask
279 // sets a bit per byte whose masked value is nonzero -- i.e. a mismatch -- so any nonzero result fails
280 // the chunk.
281 const __m512i xored = _mm512_xor_si512(mem, pat);
282 const __m512i masked = _mm512_and_si512(xored, msk);
283 if (_mm512_test_epi8_mask(masked, masked) != 0)
284 {
285 return std::nullopt;
286 }
287 }
288
289 return j;
290 }
291 #endif // DMK_HAS_AVX512
292
293 /**
294 * @brief Returns a commonality score for a byte value in typical x64 PE code sections.
295 * @details Higher scores indicate bytes that appear more frequently, making them poor candidates for
296 * anchor-based
297 * scanning.
298 */
299 1286 constexpr uint8_t byte_frequency_class(uint8_t byte_value) noexcept
300 {
301
10/13
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 4 taken 12 times.
✓ Branch 2 → 5 taken 14 times.
✓ Branch 2 → 6 taken 78 times.
✓ Branch 2 → 7 taken 103 times.
✓ Branch 2 → 8 taken 39 times.
✓ Branch 2 → 9 taken 39 times.
✓ Branch 2 → 10 taken 2 times.
✗ Branch 2 → 11 not taken.
✓ Branch 2 → 12 taken 15 times.
✗ Branch 2 → 13 not taken.
✗ Branch 2 → 14 not taken.
✓ Branch 2 → 15 taken 980 times.
1286 switch (byte_value)
302 {
303 4 case 0x00:
304 4 return 10; // null padding, very common
305 12 case 0xCC:
306 12 return 9; // INT3, debug padding
307 14 case 0x90:
308 14 return 9; // NOP
309 78 case 0xFF:
310 78 return 8; // call/jmp indirect, common
311 103 case 0x48:
312 103 return 8; // REX.W prefix, ubiquitous in x64
313 39 case 0x8B:
314 39 return 7; // MOV reg, r/m
315 39 case 0x89:
316 39 return 7; // MOV r/m, reg
317 2 case 0x0F:
318 2 return 7; // two-byte opcode escape
319 case 0xE8:
320 return 6; // CALL rel32
321 15 case 0xE9:
322 15 return 6; // JMP rel32
323 case 0x83:
324 return 6; // arithmetic imm8
325 case 0xC3:
326 return 5; // RET
327 980 default:
328 980 return 0; // uncommon, ideal anchor
329 }
330 }
331
332 /**
333 * @brief Picks the rarest fully-known byte's index in a compiled pattern.
334 * @return The byte index in `[0, pattern.size())` with the lowest score, or `pattern.size()` when no position
335 * is a
336 * fully-known literal byte (every position is a wildcard or only partially masked).
337 */
338 1025 size_t select_pattern_anchor(const Scanner::CompiledPattern &pattern) noexcept
339 {
340 1025 const size_t pattern_size = pattern.size();
341 1025 size_t best = pattern_size;
342 1025 uint8_t best_score = UINT8_MAX;
343
2/2
✓ Branch 14 → 4 taken 1399 times.
✓ Branch 14 → 15 taken 45 times.
1444 for (size_t i = 0; i < pattern_size; ++i)
344 {
345 // Only a fully-known byte (mask 0xFF) can anchor the memchr / SIMD prefilter, which searches for one
346 // exact byte value. A wildcard (mask 0x00) or a partially-masked nibble byte (0xF0 / 0x0F) carries no
347 // single byte value to scan for, so it is never an anchor candidate.
348
2/2
✓ Branch 5 → 6 taken 113 times.
✓ Branch 5 → 7 taken 1286 times.
1399 if (pattern.mask[i] != std::byte{0xFF})
349 {
350 113 continue;
351 }
352 1286 const uint8_t score = byte_frequency_class(static_cast<uint8_t>(pattern.bytes[i]));
353
4/4
✓ Branch 9 → 10 taken 280 times.
✓ Branch 9 → 11 taken 1006 times.
✓ Branch 10 → 11 taken 204 times.
✓ Branch 10 → 13 taken 76 times.
1286 if (best == pattern_size || score < best_score)
354 {
355 1210 best = i;
356 1210 best_score = score;
357
2/2
✓ Branch 11 → 12 taken 980 times.
✓ Branch 11 → 13 taken 230 times.
1210 if (score == 0)
358 {
359 980 break;
360 }
361 }
362 }
363 1025 return best;
364 }
365 } // anonymous namespace
366
367 1016 void DetourModKit::Scanner::CompiledPattern::compile_anchor() noexcept
368 {
369 1016 anchor = select_pattern_anchor(*this);
370 1016 }
371
372 namespace
373 {
374 /**
375 * @brief Converts a single hex character to its numeric value.
376 * @return The value 0-15, or -1 if not a valid hex digit.
377 */
378 35549 constexpr int hex_char_to_int(char c) noexcept
379 {
380
3/4
✓ Branch 2 → 3 taken 35551 times.
✗ Branch 2 → 5 not taken.
✓ Branch 3 → 4 taken 30178 times.
✓ Branch 3 → 5 taken 5373 times.
35549 if (c >= '0' && c <= '9')
381 30178 return c - '0';
382
3/4
✓ Branch 5 → 6 taken 5377 times.
✗ Branch 5 → 8 not taken.
✓ Branch 6 → 7 taken 5358 times.
✓ Branch 6 → 8 taken 19 times.
5371 if (c >= 'A' && c <= 'F')
383 5358 return c - 'A' + 10;
384
3/4
✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 11 taken 10 times.
✓ Branch 9 → 10 taken 3 times.
✗ Branch 9 → 11 not taken.
13 if (c >= 'a' && c <= 'f')
385 3 return c - 'a' + 10;
386 10 return -1;
387 }
388 } // anonymous namespace
389
390 1031 std::optional<Scanner::CompiledPattern> DetourModKit::Scanner::parse_aob(std::string_view aob_str)
391 {
392
1/2
✓ Branch 2 → 3 taken 1031 times.
✗ Branch 2 → 132 not taken.
1031 Logger &logger = Logger::get_instance();
393
394 90661 auto is_ws = [](char c) noexcept
395
9/12
✓ Branch 2 → 3 taken 56002 times.
✓ Branch 2 → 8 taken 34659 times.
✓ Branch 3 → 4 taken 55994 times.
✓ Branch 3 → 8 taken 8 times.
✓ Branch 4 → 5 taken 55995 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 55994 times.
✓ Branch 5 → 8 taken 1 time.
✓ Branch 6 → 7 taken 55994 times.
✗ Branch 6 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 55995 times.
90661 { return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\f' || c == '\v'; };
396
397 // Trim leading/trailing whitespace without allocating
398 1031 std::string_view input = aob_str;
399
6/6
✓ Branch 6 → 7 taken 1038 times.
✓ Branch 6 → 11 taken 6 times.
✓ Branch 9 → 10 taken 13 times.
✓ Branch 9 → 11 taken 1025 times.
✓ Branch 12 → 4 taken 13 times.
✓ Branch 12 → 13 taken 1031 times.
1044 while (!input.empty() && is_ws(input.front()))
400 13 input.remove_prefix(1);
401
6/6
✓ Branch 16 → 17 taken 1785 times.
✓ Branch 16 → 21 taken 6 times.
✓ Branch 19 → 20 taken 760 times.
✓ Branch 19 → 21 taken 1026 times.
✓ Branch 22 → 14 taken 760 times.
✓ Branch 22 → 23 taken 1032 times.
1791 while (!input.empty() && is_ws(input.back()))
402 760 input.remove_suffix(1);
403
404
2/2
✓ Branch 24 → 25 taken 6 times.
✓ Branch 24 → 30 taken 1025 times.
1032 if (input.empty())
405 {
406
2/2
✓ Branch 26 → 27 taken 3 times.
✓ Branch 26 → 29 taken 3 times.
6 if (!aob_str.empty())
407 {
408
1/2
✓ Branch 27 → 28 taken 3 times.
✗ Branch 27 → 116 not taken.
3 logger.debug("AOB Parser: Input string became empty after trimming.");
409 }
410 6 return std::nullopt;
411 }
412
413 1025 CompiledPattern result;
414 1025 size_t token_idx = 0;
415 1025 bool offset_set = false;
416
417 1025 size_t pos = 0;
418
2/2
✓ Branch 104 → 31 taken 17974 times.
✓ Branch 104 → 105 taken 1014 times.
18988 while (pos < input.size())
419 {
420 // Skip whitespace between tokens
421
5/6
✓ Branch 34 → 35 taken 34929 times.
✗ Branch 34 → 39 not taken.
✓ Branch 37 → 38 taken 16956 times.
✓ Branch 37 → 39 taken 17977 times.
✓ Branch 40 → 32 taken 16956 times.
✓ Branch 40 → 41 taken 17976 times.
34930 while (pos < input.size() && is_ws(input[pos]))
422 16956 ++pos;
423
1/2
✗ Branch 42 → 43 not taken.
✓ Branch 42 → 44 taken 17977 times.
17976 if (pos >= input.size())
424 break;
425
426 // Find token end
427 17977 const size_t token_start = pos;
428
6/6
✓ Branch 47 → 48 taken 52927 times.
✓ Branch 47 → 52 taken 1013 times.
✓ Branch 50 → 51 taken 35972 times.
✓ Branch 50 → 52 taken 16963 times.
✓ Branch 53 → 45 taken 35971 times.
✓ Branch 53 → 54 taken 17977 times.
53948 while (pos < input.size() && !is_ws(input[pos]))
429 35971 ++pos;
430
1/2
✓ Branch 54 → 55 taken 17973 times.
✗ Branch 54 → 128 not taken.
17977 const std::string_view token = input.substr(token_start, pos - token_start);
431
432 17973 token_idx++;
433
2/2
✓ Branch 57 → 58 taken 16 times.
✓ Branch 57 → 63 taken 17959 times.
17973 if (token == "|")
434 {
435
2/2
✓ Branch 58 → 59 taken 1 time.
✓ Branch 58 → 61 taken 15 times.
16 if (offset_set)
436 {
437
1/2
✓ Branch 59 → 60 taken 1 time.
✗ Branch 59 → 117 not taken.
1 logger.error("AOB Parser: Multiple '|' offset markers at position {}.", token_idx);
438 13 return std::nullopt;
439 }
440 15 result.offset = static_cast<std::ptrdiff_t>(result.bytes.size());
441 15 offset_set = true;
442 }
443
6/6
✓ Branch 65 → 66 taken 17790 times.
✓ Branch 65 → 69 taken 172 times.
✓ Branch 68 → 69 taken 2 times.
✓ Branch 68 → 70 taken 17780 times.
✓ Branch 71 → 72 taken 174 times.
✓ Branch 71 → 75 taken 17780 times.
17959 else if (token == "??" || token == "?")
444 {
445
1/2
✓ Branch 72 → 73 taken 174 times.
✗ Branch 72 → 118 not taken.
174 result.bytes.push_back(std::byte{0x00});
446
1/2
✓ Branch 73 → 74 taken 174 times.
✗ Branch 73 → 119 not taken.
174 result.mask.push_back(std::byte{0x00});
447 }
448
2/2
✓ Branch 76 → 77 taken 17776 times.
✓ Branch 76 → 99 taken 5 times.
17780 else if (token.length() == 2)
449 {
450 17776 const char hi_char = token[0];
451 17776 const char lo_char = token[1];
452 17776 const int hi = hex_char_to_int(hi_char);
453 17776 const int lo = hex_char_to_int(lo_char);
454
4/4
✓ Branch 81 → 82 taken 17766 times.
✓ Branch 81 → 86 taken 13 times.
✓ Branch 82 → 83 taken 17761 times.
✓ Branch 82 → 86 taken 5 times.
17779 if (hi >= 0 && lo >= 0)
455 {
456
1/2
✓ Branch 83 → 84 taken 17760 times.
✗ Branch 83 → 120 not taken.
17761 result.bytes.push_back(static_cast<std::byte>((hi << 4) | lo));
457
1/2
✓ Branch 84 → 85 taken 17763 times.
✗ Branch 84 → 121 not taken.
17760 result.mask.push_back(std::byte{0xFF});
458 }
459
3/4
✓ Branch 86 → 87 taken 6 times.
✓ Branch 86 → 91 taken 12 times.
✓ Branch 87 → 88 taken 6 times.
✗ Branch 87 → 91 not taken.
18 else if (hi >= 0 && lo_char == '?')
460 {
461 // High-nibble token (e.g. "4?"): the high nibble is fixed and the low nibble is a wildcard. Store
462 // the known nibble in place with a zeroed wildcard nibble and a 0xF0 mask, so the masked compare
463 // (mem ^ pat) & mask checks only the high nibble.
464
1/2
✓ Branch 88 → 89 taken 6 times.
✗ Branch 88 → 122 not taken.
6 result.bytes.push_back(static_cast<std::byte>(hi << 4));
465
1/2
✓ Branch 89 → 90 taken 6 times.
✗ Branch 89 → 123 not taken.
6 result.mask.push_back(std::byte{0xF0});
466 }
467
3/4
✓ Branch 91 → 92 taken 5 times.
✓ Branch 91 → 96 taken 7 times.
✓ Branch 92 → 93 taken 5 times.
✗ Branch 92 → 96 not taken.
12 else if (hi_char == '?' && lo >= 0)
468 {
469 // Low-nibble token (e.g. "?5"): the low nibble is fixed and the high nibble is a wildcard. A 0x0F
470 // mask checks only the low nibble.
471
1/2
✓ Branch 93 → 94 taken 5 times.
✗ Branch 93 → 124 not taken.
5 result.bytes.push_back(static_cast<std::byte>(lo));
472
1/2
✓ Branch 94 → 95 taken 5 times.
✗ Branch 94 → 125 not taken.
5 result.mask.push_back(std::byte{0x0F});
473 }
474 else
475 {
476 // Split the literal around '??' to dodge the C++ trigraph
477 // ??' (interpreted as a `|`), which trips -Wtrigraphs on
478 // GCC and would otherwise require disabling the warning TU-wide.
479
1/2
✓ Branch 96 → 97 taken 8 times.
✗ Branch 96 → 126 not taken.
7 logger.error("AOB Parser: Invalid token '{}' at position {}. "
480 "Expected hex byte (e.g., FF), a per-nibble form (e.g. '4?' or '?5'), '?', or '?"
481 "?'.",
482 token, token_idx);
483 8 return std::nullopt;
484 }
485 }
486 else
487 {
488
1/2
✓ Branch 99 → 100 taken 4 times.
✗ Branch 99 → 127 not taken.
5 logger.error("AOB Parser: Invalid token '{}' at position {}. "
489 "Expected hex byte (e.g., FF), a per-nibble form (e.g. '4?' or '?5'), '?', or '?"
490 "?'.",
491 token, token_idx);
492 4 return std::nullopt;
493 }
494 }
495
496
1/2
✗ Branch 106 → 107 not taken.
✓ Branch 106 → 111 taken 1013 times.
1014 if (result.empty())
497 {
498 if (token_idx > 0)
499 {
500 logger.error("AOB Parser: Processed tokens but resulting pattern is empty.");
501 }
502 return std::nullopt;
503 }
504
505 1013 result.compile_anchor();
506 1013 return result;
507 1025 }
508
509 namespace
510 {
511 // Internal scan primitive: returns the match *start* without applying pattern.offset. The public find_pattern
512 // wrappers apply the offset exactly once on top of this result; scan_executable_regions also calls this
513 // directly so its own final offset-application remains correct.
514 DMK_NO_SANITIZE_ADDRESS
515 const std::byte *find_pattern_raw(const std::byte *start_address, size_t region_size,
516 const Scanner::CompiledPattern &pattern) noexcept;
517
518 // Shared guard for "pattern has no literal bytes". Returning start_address preserves backwards compatibility
519 // for callers that rely on the degenerate "all wildcards matches anywhere" behaviour, but the call site is
520 // almost always a bug. Logging once per public entry (rather than per internal find_pattern_raw iteration)
521 // keeps the warning visible without flooding logs when the Nth-occurrence overload or scan_executable_regions
522 // loops.
523 273 bool pattern_has_literal_byte(const Scanner::CompiledPattern &pattern) noexcept
524 {
525
2/2
✓ Branch 17 → 4 taken 306 times.
✓ Branch 17 → 18 taken 15 times.
594 for (const std::byte mask_byte : pattern.mask)
526 {
527
2/2
✓ Branch 6 → 7 taken 258 times.
✓ Branch 6 → 8 taken 48 times.
306 if (mask_byte != std::byte{0x00})
528 258 return true;
529 }
530 15 return false;
531 }
532
533 // Shared precondition check for the public find_pattern overloads. Returns false when the caller must
534 // short-circuit with nullptr (empty pattern or null start_address). Emits the all-wildcard warning itself so
535 // callers do not duplicate it; in that case the caller still continues scanning.
536 102 bool validate_find_pattern_inputs(const std::byte *start_address,
537 const Scanner::CompiledPattern &pattern) noexcept
538 {
539 102 Logger &logger = Logger::get_instance();
540
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 7 taken 100 times.
102 if (pattern.empty())
541 {
542 2 logger.error("find_pattern: Pattern is empty. Cannot scan.");
543 2 return false;
544 }
545
2/2
✓ Branch 7 → 8 taken 4 times.
✓ Branch 7 → 10 taken 96 times.
100 if (!start_address)
546 {
547 4 logger.error("find_pattern: Start address is null. Cannot scan.");
548 4 return false;
549 }
550
2/2
✓ Branch 11 → 12 taken 6 times.
✓ Branch 11 → 14 taken 90 times.
96 if (!pattern_has_literal_byte(pattern))
551 {
552 6 logger.warning("find_pattern: pattern contains no literal bytes "
553 "(all wildcards); returning region start unchanged");
554 }
555 96 return true;
556 }
557 } // anonymous namespace
558
559 86 const std::byte *DetourModKit::Scanner::find_pattern(const std::byte *start_address, size_t region_size,
560 const CompiledPattern &pattern)
561 {
562
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 5 taken 82 times.
86 if (!validate_find_pattern_inputs(start_address, pattern))
563 {
564 4 return nullptr;
565 }
566
567 82 const std::byte *match = find_pattern_raw(start_address, region_size, pattern);
568
2/2
✓ Branch 6 → 7 taken 14 times.
✓ Branch 6 → 8 taken 68 times.
82 if (!match)
569 14 return nullptr;
570 68 return match + pattern.offset;
571 }
572
573 namespace
574 {
575 // Self-provided memchr over [haystack, haystack + n) for the anchor byte. Routing the prefilter through libc
576 // memchr works in release, but under AddressSanitizer the runtime interceptor inspects the whole range against
577 // ASan's shadow and reports a false overflow when the scanner walks this process's own committed, readable
578 // memory (the poisoned shadow around stack locals and instrumented globals). The runtime interceptor bypasses
579 // any no_sanitize_address attribute on the caller, so a per-function escape hatch is not enough: the function
580 // itself must do the byte comparisons.
581 //
582 // The needle search is tiered the same way the verify path is. On x86-64 the SSE2 body (16 bytes per iteration)
583 // is always available -- SSE2 is part of the x86-64 baseline, so no runtime gate is needed -- and an AVX2 body
584 // (32 bytes per iteration) is selected at runtime through the same cpu_has_avx2() gate the verify tier uses.
585 // Each SIMD body broadcasts the needle into every lane, compares a whole vector against it with one PCMPEQB,
586 // and collapses the per-byte result to a movemask bitmask; count-trailing-zeros on the first nonzero mask gives
587 // the lane index of the first match, so the search keeps libc memchr's "lowest address wins" contract. A scalar
588 // byte loop finishes the sub-vector tail and is the only body on targets without SSE2 (32-bit x86 built without
589 // it). None of the tiers call into libc, so the ASan interceptor never sees the read; the explicit intrinsics
590 // also use unaligned loads, so there is no type-punned qword load for clang-cl's strict-aliasing TBAA to
591 // miscompile.
592
593 #if defined(DMK_HAS_SSE2) || defined(DMK_HAS_AVX2)
594 /// Count-trailing-zeros over a known-nonzero movemask result; yields the first matching byte's lane index.
595 4558732 inline unsigned dmk_movemask_first_index(unsigned int mask) noexcept
596 {
597 #if defined(_MSC_VER) && !defined(__clang__)
598 unsigned long index = 0;
599 _BitScanForward(&index, mask);
600 return static_cast<unsigned>(index);
601 #else
602 4558732 return static_cast<unsigned>(__builtin_ctz(mask));
603 #endif
604 }
605 #endif // DMK_HAS_SSE2 || DMK_HAS_AVX2
606
607 #ifdef DMK_HAS_SSE2
608 // SSE2 needle search over [p, p + n): a 16-byte body plus a scalar tail. No runtime gate -- DMK_HAS_SSE2
609 // implies the target is x86-64 (or x86 built with SSE2), where these instructions are always legal.
610 DMK_NO_SANITIZE_ADDRESS
611 87 const unsigned char *dmk_memchr_sse2(const unsigned char *p, unsigned char needle, size_t n) noexcept
612 {
613 87 const __m128i needle_vec = _mm_set1_epi8(static_cast<char>(needle));
614
2/2
✓ Branch 17 → 7 taken 21 times.
✓ Branch 17 → 18 taken 73 times.
94 for (; n >= 16; p += 16, n -= 16)
615 {
616 21 const __m128i chunk = _mm_loadu_si128(reinterpret_cast<const __m128i *>(p));
617 const unsigned int mask =
618 21 static_cast<unsigned int>(_mm_movemask_epi8(_mm_cmpeq_epi8(chunk, needle_vec)));
619
2/2
✓ Branch 13 → 14 taken 14 times.
✓ Branch 13 → 16 taken 7 times.
21 if (mask != 0)
620 {
621 14 return p + dmk_movemask_first_index(mask);
622 }
623 }
624
2/2
✓ Branch 22 → 19 taken 376 times.
✓ Branch 22 → 23 taken 48 times.
424 for (; n > 0; ++p, --n)
625 {
626
2/2
✓ Branch 19 → 20 taken 25 times.
✓ Branch 19 → 21 taken 351 times.
376 if (*p == needle)
627 {
628 25 return p;
629 }
630 }
631 48 return nullptr;
632 }
633 #endif // DMK_HAS_SSE2
634
635 #ifdef DMK_HAS_AVX2
636 // AVX2 needle search over [p, p + n): a 32-byte body plus a scalar tail. Compiled with AVX2 codegen via the
637 // target attribute on GCC/Clang so the rest of the TU stays SSE2-only, and only entered after cpu_has_avx2()
638 // has confirmed both the CPU and the OS support the instructions. The tail is scalar rather than an SSE2 call
639 // so the body emits no legacy-SSE encoding and the compiler has no VEX/legacy transition to reconcile on the
640 // way out.
641 DMK_AVX2_TARGET
642 DMK_NO_SANITIZE_ADDRESS
643 4574599 const unsigned char *dmk_memchr_avx2(const unsigned char *p, unsigned char needle, size_t n) noexcept
644 {
645 4574599 const __m256i needle_vec = _mm256_set1_epi8(static_cast<char>(needle));
646
2/2
✓ Branch 17 → 7 taken 70933766 times.
✓ Branch 17 → 18 taken 15881 times.
70949647 for (; n >= 32; p += 32, n -= 32)
647 {
648 70933766 const __m256i chunk = _mm256_loadu_si256(reinterpret_cast<const __m256i *>(p));
649 const unsigned int mask =
650 70933766 static_cast<unsigned int>(_mm256_movemask_epi8(_mm256_cmpeq_epi8(chunk, needle_vec)));
651
2/2
✓ Branch 13 → 14 taken 4558718 times.
✓ Branch 13 → 16 taken 66375048 times.
70933766 if (mask != 0)
652 {
653 4558718 return p + dmk_movemask_first_index(mask);
654 }
655 }
656
2/2
✓ Branch 22 → 19 taken 208254 times.
✓ Branch 22 → 23 taken 15864 times.
224118 for (; n > 0; ++p, --n)
657 {
658
2/2
✓ Branch 19 → 20 taken 17 times.
✓ Branch 19 → 21 taken 208237 times.
208254 if (*p == needle)
659 {
660 17 return p;
661 }
662 }
663 15864 return nullptr;
664 }
665 #endif // DMK_HAS_AVX2
666
667 // use_avx2 is hoisted by find_pattern_raw so the per-anchor-hit sweep never re-reads the cpu_has_avx2() static.
668 DMK_NO_SANITIZE_ADDRESS
669 4574686 const void *dmk_memchr(const void *haystack, unsigned char needle, size_t n,
670 [[maybe_unused]] bool use_avx2) noexcept
671 {
672
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 4574686 times.
4574686 if (n == 0)
673 {
674 return nullptr;
675 }
676 4574686 const auto *p = static_cast<const unsigned char *>(haystack);
677
678 #ifdef DMK_HAS_AVX2
679 // The 32-byte body only pays for itself once a full vector is in play; shorter spans skip straight to the
680 // SSE2/scalar bodies, which avoids the target-switch on the tail of a sweep that has nearly run out.
681
3/4
✓ Branch 4 → 5 taken 4574686 times.
✗ Branch 4 → 7 not taken.
✓ Branch 5 → 6 taken 4574599 times.
✓ Branch 5 → 7 taken 87 times.
4574686 if (use_avx2 && n >= 32)
682 {
683 4574599 return dmk_memchr_avx2(p, needle, n);
684 }
685 #endif
686 #ifdef DMK_HAS_SSE2
687 87 return dmk_memchr_sse2(p, needle, n);
688 #else
689 for (; n > 0; ++p, --n)
690 {
691 if (*p == needle)
692 {
693 return p;
694 }
695 }
696 return nullptr;
697 #endif
698 }
699
700 // memchr over [begin, end] for the anchor byte. Routes through the self-provided dmk_memchr above so the ASan
701 // runtime cannot intercept the call. dmk_memchr returns a pointer into the range or nullptr; the wrapper
702 // re-establishes the [begin, end] inclusive contract the scanner expects. use_avx2 is the caller's hoisted
703 // cpu_has_avx2() result, threaded through so the prefilter does not re-read the static on every anchor hit.
704 DMK_NO_SANITIZE_ADDRESS
705 4574686 const std::byte *scan_for_byte(const std::byte *begin, const std::byte *end, unsigned char target,
706 bool use_avx2) noexcept
707 {
708 4574686 const size_t n = static_cast<size_t>(end - begin + 1);
709 4574686 return static_cast<const std::byte *>(dmk_memchr(begin, target, n, use_avx2));
710 }
711
712 DMK_NO_SANITIZE_ADDRESS
713 17844 const std::byte *find_pattern_raw(const std::byte *start_address, size_t region_size,
714 const Scanner::CompiledPattern &pattern) noexcept
715 {
716 17844 const size_t pattern_size = pattern.size();
717
718
4/6
✓ Branch 3 → 4 taken 17844 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 17844 times.
✗ Branch 4 → 6 not taken.
✓ Branch 5 → 6 taken 4 times.
✓ Branch 5 → 7 taken 17840 times.
17844 if (pattern_size == 0 || !start_address || region_size < pattern_size)
719 {
720 4 return nullptr;
721 }
722
723 // Anchor selection: parse_aob() pre-populates pattern.anchor, so the common path is a single load. Manually
724 // constructed patterns fall back to inline selection without mutating the input (preserves the
725 // const-by-design contract).
726 const size_t best_anchor =
727
2/2
✓ Branch 7 → 8 taken 17831 times.
✓ Branch 7 → 9 taken 9 times.
17840 (pattern.anchor <= pattern_size) ? pattern.anchor : select_pattern_anchor(pattern);
728
729 // No fully-known byte to anchor on. Two sub-cases:
730 // - The pattern is entirely wildcards (no mask bit set anywhere): the search degenerates to
731 // "always match at region start", preserved for backward compatibility. The public wrappers log the
732 // warning once per call.
733 // - The pattern carries only partially-masked (nibble) bytes: there is no exact byte for the
734 // memchr / SIMD prefilter, so fall back to a masked compare at every candidate position. This
735 // path is rare -- a real signature almost always carries at least one full literal byte -- so a
736 // scalar verify is acceptable; correctness, not throughput, is the concern here.
737
2/2
✓ Branch 10 → 11 taken 14 times.
✓ Branch 10 → 33 taken 17826 times.
17840 if (best_anchor == pattern_size)
738 {
739
2/2
✓ Branch 12 → 13 taken 9 times.
✓ Branch 12 → 14 taken 5 times.
14 if (!pattern_has_literal_byte(pattern))
740 {
741 9 return start_address;
742 }
743 5 const std::byte *const last_start = start_address + (region_size - pattern_size);
744
2/2
✓ Branch 31 → 15 taken 815 times.
✓ Branch 31 → 32 taken 2 times.
817 for (const std::byte *pos = start_address; pos <= last_start; ++pos)
745 {
746 815 bool match_found = true;
747
2/2
✓ Branch 27 → 16 taken 815 times.
✓ Branch 27 → 28 taken 3 times.
818 for (size_t j = 0; j < pattern_size; ++j)
748 {
749 815 const auto mem = std::to_integer<unsigned>(pos[j]);
750 815 const auto pat = std::to_integer<unsigned>(pattern.bytes[j]);
751 815 const auto msk = std::to_integer<unsigned>(pattern.mask[j]);
752
2/2
✓ Branch 24 → 25 taken 812 times.
✓ Branch 24 → 26 taken 3 times.
815 if (((mem ^ pat) & msk) != 0)
753 {
754 812 match_found = false;
755 812 break;
756 }
757 }
758
2/2
✓ Branch 28 → 29 taken 3 times.
✓ Branch 28 → 30 taken 812 times.
815 if (match_found)
759 {
760 3 return pos;
761 }
762 }
763 2 return nullptr;
764 }
765
766 17826 const std::byte target_byte = pattern.bytes[best_anchor];
767 17826 const unsigned char target_val = static_cast<unsigned char>(target_byte);
768
769 17826 const std::byte *search_start = start_address + best_anchor;
770 17826 const std::byte *const search_end = start_address + (region_size - pattern_size) + best_anchor;
771
772 // Hoist runtime CPU detection. The query itself is a function-local static behind a one-shot init, but
773 // reading it on every memchr hit and every verify adds an indirect load per false candidate. Caching it
774 // once here lets both the prefilter sweep and the per-candidate verify branch use a register-resident bool.
775 // It is defined unconditionally (false without an AVX2 build) because the prefilter takes it on every call.
776 #ifdef DMK_HAS_AVX2
777 17826 const bool use_avx2 = cpu_has_avx2();
778 #else
779 const bool use_avx2 = false;
780 #endif
781 #ifdef DMK_HAS_AVX512
782 const bool use_avx512 = cpu_has_avx512();
783 #endif
784
785
2/2
✓ Branch 89 → 36 taken 4574686 times.
✓ Branch 89 → 90 taken 4 times.
4574690 while (search_start <= search_end)
786 {
787 4574686 const std::byte *current_scan_ptr = scan_for_byte(search_start, search_end, target_val, use_avx2);
788
789
2/2
✓ Branch 37 → 38 taken 12403 times.
✓ Branch 37 → 39 taken 4558774 times.
4571177 if (!current_scan_ptr)
790 {
791 12403 break;
792 }
793 4558774 const std::byte *pattern_start = current_scan_ptr - best_anchor;
794
795 // Verify the full pattern at this position. SIMD tiers run widest-first: AVX-512 (64B) -> AVX2 (32B) ->
796 // SSE2 (16B) -> scalar (1B). Each tier resumes from the offset the previous one reached (start_offset
797 // j), so the widest available tiers cover the bulk and the scalar loop only ever finishes a sub-16-byte
798 // tail.
799 4558774 bool match_found = true;
800 4558774 size_t j = 0;
801
802 #ifdef DMK_HAS_AVX512
803 if (use_avx512)
804 {
805 const auto next_j = verify_pattern_avx512(pattern_start, pattern, j);
806 if (next_j.has_value())
807 {
808 j = *next_j;
809 }
810 else
811 {
812 match_found = false;
813 }
814 }
815 #endif // DMK_HAS_AVX512
816
817 #ifdef DMK_HAS_AVX2
818
2/4
✓ Branch 39 → 40 taken 4558774 times.
✗ Branch 39 → 48 not taken.
✓ Branch 40 → 41 taken 4558774 times.
✗ Branch 40 → 48 not taken.
4558774 if (match_found && use_avx2)
819 {
820 4558774 const auto next_j = verify_pattern_avx2(pattern_start, pattern, j);
821
2/2
✓ Branch 43 → 44 taken 4558773 times.
✓ Branch 43 → 46 taken 2 times.
4558775 if (next_j.has_value())
822 {
823 4558773 j = *next_j;
824 }
825 else
826 {
827 2 match_found = false;
828 }
829 }
830 #endif // DMK_HAS_AVX2
831
832 #ifdef DMK_HAS_SSE2
833
4/4
✓ Branch 70 → 71 taken 4560491 times.
✓ Branch 70 → 72 taken 2 times.
✓ Branch 71 → 49 taken 4081028 times.
✓ Branch 71 → 72 taken 479463 times.
4560493 for (; match_found && j + 16 <= pattern_size; j += 16)
834 {
835 4081028 const __m128i mem = _mm_loadu_si128(reinterpret_cast<const __m128i *>(pattern_start + j));
836 4081028 const __m128i pat = _mm_loadu_si128(reinterpret_cast<const __m128i *>(pattern.bytes.data() + j));
837 8162056 const __m128i msk = _mm_loadu_si128(reinterpret_cast<const __m128i *>(pattern.mask.data() + j));
838
839 4081028 const __m128i xored = _mm_xor_si128(mem, pat);
840 4081028 const __m128i masked = _mm_and_si128(xored, msk);
841 8162056 const __m128i cmp = _mm_cmpeq_epi8(masked, _mm_setzero_si128());
842
843
2/2
✓ Branch 67 → 68 taken 4079310 times.
✓ Branch 67 → 69 taken 1718 times.
4081028 if (_mm_movemask_epi8(cmp) != 0xFFFF)
844 {
845 4079310 match_found = false;
846 4079310 break;
847 }
848 }
849 #endif // DMK_HAS_SSE2
850
851
4/4
✓ Branch 84 → 85 taken 1176035 times.
✓ Branch 84 → 86 taken 4556864 times.
✓ Branch 85 → 73 taken 1174124 times.
✓ Branch 85 → 86 taken 1911 times.
5732899 for (; match_found && j < pattern_size; ++j)
852 {
853 // Masked compare so a partially-masked nibble byte checks only its known nibble: (mem ^ pat) & mask
854 // is zero exactly when every bit the mask selects agrees. A wildcard (mask 0x00) is trivially
855 // satisfied, a full literal (0xFF) compares the whole byte, and a nibble (0xF0 / 0x0F) compares one
856 // nibble.
857 1174124 const auto mem = std::to_integer<unsigned>(pattern_start[j]);
858 1174124 const auto pat = std::to_integer<unsigned>(pattern.bytes[j]);
859 1174124 const auto msk = std::to_integer<unsigned>(pattern.mask[j]);
860
2/2
✓ Branch 81 → 82 taken 477552 times.
✓ Branch 81 → 83 taken 696572 times.
1174124 if (((mem ^ pat) & msk) != 0)
861 {
862 477552 match_found = false;
863 }
864 }
865
866
2/2
✓ Branch 86 → 87 taken 1911 times.
✓ Branch 86 → 88 taken 4556864 times.
4558775 if (match_found)
867 {
868 1911 return pattern_start;
869 }
870
871 // No match, continue searching from next position
872 4556864 search_start = current_scan_ptr + 1;
873 }
874
875 12407 return nullptr;
876 }
877 } // anonymous namespace
878
879 18 const std::byte *DetourModKit::Scanner::find_pattern(const std::byte *start_address, size_t region_size,
880 const CompiledPattern &pattern, size_t occurrence)
881 {
882
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 16 times.
18 if (occurrence == 0)
883 {
884 2 return nullptr;
885 }
886
887
2/2
✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 7 taken 14 times.
16 if (!validate_find_pattern_inputs(start_address, pattern))
888 {
889 2 return nullptr;
890 }
891
892 14 const std::byte *cursor = start_address;
893 14 size_t remaining = region_size;
894 14 size_t found_count = 0;
895
896 // Iterate via the raw helper so the `match + 1` continuation stays correct regardless of the pattern's offset
897 // marker. Offset is applied exactly once when we return the Nth hit.
898
2/2
✓ Branch 15 → 8 taken 29 times.
✓ Branch 15 → 16 taken 1 time.
30 while (remaining >= pattern.size())
899 {
900 29 const std::byte *match = find_pattern_raw(cursor, remaining, pattern);
901
2/2
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 28 times.
29 if (!match)
902 {
903 1 break;
904 }
905
2/2
✓ Branch 11 → 12 taken 12 times.
✓ Branch 11 → 13 taken 16 times.
28 if (++found_count == occurrence)
906 {
907 12 return match + pattern.offset;
908 }
909 16 const size_t advance = static_cast<size_t>(match - cursor) + 1;
910 16 cursor += advance;
911 16 remaining -= advance;
912 }
913
914 2 return nullptr;
915 }
916
917 std::expected<uintptr_t, DetourModKit::RipResolveError>
918 28 DetourModKit::Scanner::resolve_rip_relative(const std::byte *instruction_address, size_t displacement_offset,
919 size_t instruction_length)
920 {
921
2/2
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 6 taken 25 times.
28 if (!instruction_address)
922 {
923 3 return std::unexpected(RipResolveError::NullInput);
924 }
925
926 25 const std::byte *disp_ptr = instruction_address + displacement_offset;
927 // Read the displacement under a single SEH fault guard instead of is_readable + raw memcpy. is_readable is a
928 // time-of-check/time-of-use illusion -- the page can change protection or unmap between the check and the copy
929 // -- so an unguarded memcpy could fault the host.
930 25 const auto displacement = Memory::seh_read<int32_t>(reinterpret_cast<uintptr_t>(disp_ptr));
931
2/2
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 12 taken 24 times.
25 if (!displacement)
932 {
933 1 return std::unexpected(RipResolveError::UnreadableDisplacement);
934 }
935
936 // Compute the target in unsigned modular arithmetic so the math stays well-defined on every input, including
937 // kernel-range instruction addresses (where intptr_t would be negative and signed overflow is UB). The
938 // displacement is sign-extended first so negative disp32 values wrap to the correct 64-bit offset.
939 24 const uintptr_t base = reinterpret_cast<uintptr_t>(instruction_address);
940 24 const uintptr_t disp_sext = static_cast<uintptr_t>(static_cast<int64_t>(*displacement));
941 24 const uintptr_t target = base + instruction_length + disp_sext;
942
943 // Fail closed on a target that cannot be a real in-process address. A corrupt or hostile displacement can
944 // resolve to 0, a low guard-page address, or a kernel-range value; returning that as "success" would hand the
945 // caller a pointer that faults on first use. plausible_userspace_ptr is pure arithmetic, so this guard adds no
946 // syscall and no memory access.
947
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 18 taken 23 times.
24 if (!Memory::plausible_userspace_ptr(target))
948 {
949 1 return std::unexpected(RipResolveError::ImplausibleTarget);
950 }
951 23 return target;
952 }
953
954 std::expected<uintptr_t, DetourModKit::RipResolveError>
955 20 DetourModKit::Scanner::find_and_resolve_rip_relative(const std::byte *search_start, size_t search_length,
956 std::span<const std::byte> opcode_prefix,
957 size_t instruction_length)
958 {
959
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())
960 {
961 3 return std::unexpected(RipResolveError::NullInput);
962 }
963
964 17 const size_t prefix_len = opcode_prefix.size();
965 17 const size_t min_bytes = prefix_len + sizeof(int32_t);
966
2/2
✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 16 taken 15 times.
17 if (search_length < min_bytes)
967 {
968 2 return std::unexpected(RipResolveError::RegionTooSmall);
969 }
970
971 15 const size_t scan_limit = search_length - min_bytes;
972 15 const std::byte first = opcode_prefix[0];
973
974
2/2
✓ Branch 29 → 18 taken 141 times.
✓ Branch 29 → 30 taken 2 times.
143 for (size_t i = 0; i <= scan_limit; ++i)
975 {
976
2/2
✓ Branch 18 → 19 taken 127 times.
✓ Branch 18 → 20 taken 14 times.
141 if (search_start[i] != first)
977 {
978 127 continue;
979 }
980
981
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)
982 {
983 1 continue;
984 }
985
986 13 return resolve_rip_relative(&search_start[i], prefix_len, instruction_length);
987 }
988
989 2 return std::unexpected(RipResolveError::PrefixNotFound);
990 }
991
992 // Per-thread "the most recently measured sweep window skipped a faulted region" flag. scan_regions_filtered ORs it
993 // true (never clears it) whenever it skips a region that faulted mid-scan, so a faulted sweep is observable to the
994 // cascade layer in its own TU without changing any scan return type. It is thread_local because two threads
995 // scanning concurrently must not see each other's fault state; the cascade clears it before a measurement window
996 // and reads it after, so a stale value from an unrelated earlier scan on the same thread cannot leak into a later
997 // verdict. The flag is advisory accounting for the fail-closed uniqueness check -- it never alters which address a
998 // scan returns.
999 3558 bool &Scanner::detail::scan_incomplete_flag() noexcept
1000 {
1001 thread_local bool incomplete = false;
1002 3558 return incomplete;
1003 }
1004
1005 namespace
1006 {
1007 // Scan one protection-gated region for the next needed match, decrementing matches_remaining for each non-self
1008 // match. Returns the resolved address (match + pattern.offset) when the Nth match lands in this region, or
1009 // nullptr when the region is exhausted first. This is the body the TOCTOU fault guard wraps (see
1010 // scan_region_guarded): it performs the unguarded find_pattern_raw reads (memchr prefilter + SIMD verify)
1011 // across [region_start, +scan_size).
1012 16841 const std::byte *scan_region_for_match(const std::byte *region_start, size_t scan_size,
1013 const Scanner::CompiledPattern &pattern, uintptr_t needle_lo,
1014 uintptr_t needle_hi, size_t &matches_remaining) noexcept
1015 {
1016 16841 const std::byte *match = find_pattern_raw(region_start, scan_size, pattern);
1017
2/2
✓ Branch 15 → 4 taken 1827 times.
✓ Branch 15 → 16 taken 12398 times.
14225 while (match != nullptr)
1018 {
1019 1827 const auto match_addr = reinterpret_cast<uintptr_t>(match);
1020
4/4
✓ Branch 4 → 5 taken 1701 times.
✓ Branch 4 → 8 taken 126 times.
✓ Branch 6 → 7 taken 7 times.
✓ Branch 6 → 8 taken 1694 times.
1827 const bool self_match = match_addr < needle_hi && (match_addr + pattern.size()) > needle_lo;
1021
2/2
✓ Branch 9 → 10 taken 1820 times.
✓ Branch 9 → 12 taken 7 times.
1827 if (!self_match)
1022 {
1023 1820 --matches_remaining;
1024
2/2
✓ Branch 10 → 11 taken 935 times.
✓ Branch 10 → 12 taken 885 times.
1820 if (matches_remaining == 0)
1025 935 return match + pattern.offset;
1026 }
1027
1028 // Continue scanning past the current match.
1029 892 const size_t consumed = static_cast<size_t>(match - region_start) + 1;
1030
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 892 times.
892 if (consumed >= scan_size)
1031 break;
1032 892 match = find_pattern_raw(match + 1, scan_size - consumed, pattern);
1033 }
1034 12398 return nullptr;
1035 }
1036
1037 // Region-granular TOCTOU fault guard around scan_region_for_match. The caller's per-region VirtualQuery only
1038 // proves the region was committed and readable at gate time; a concurrent decommit / reprotect before these
1039 // unguarded reads complete would otherwise fault the host. On MSVC the body runs inside a __try / __except that
1040 // swallows exactly the foreign-read faults (Memory::detail::is_guarded_read_fault) and reports the region as
1041 // faulted, so the sweep skips it and continues -- the same skip-the-region contract seh_read_bytes follows. On
1042 // MinGW x64 the same scan runs through the process-wide vectored read guard
1043 // (Memory::detail::run_guarded_region) that the seh_read paths use, so a fault inside the scanned span is
1044 // swallowed and the region is skipped + counted there too. On 32-bit MinGW that x64-only vectored guard is
1045 // unavailable, so the body runs directly and the per-region VirtualQuery gate is the only guard. *out_faulted
1046 // is set true only when a fault was swallowed.
1047 16840 const std::byte *scan_region_guarded(const std::byte *region_start, size_t scan_size,
1048 const Scanner::CompiledPattern &pattern, uintptr_t needle_lo,
1049 uintptr_t needle_hi, size_t &matches_remaining, bool &out_faulted) noexcept
1050 {
1051 // The 64-bit-only contract, asserted locally so the unguarded 32-bit MinGW arm of this function (the bare
1052 // #else below, which runs scan_region_for_match with no vectored fault guard) is provably unreachable in
1053 // any build that compiles: a 32-bit build fails here at compile time rather than silently shipping a sweep
1054 // whose only TOCTOU protection is the per-region VirtualQuery gate.
1055 static_assert(sizeof(void *) == 8, "scan_region_guarded requires a 64-bit target: the MinGW fault guard "
1056 "(run_guarded_region) is x64-only and the 32-bit arm is unguarded.");
1057 16840 out_faulted = false;
1058 #ifdef _MSC_VER
1059 const size_t original_matches_remaining = matches_remaining;
1060 __try
1061 {
1062 return scan_region_for_match(region_start, scan_size, pattern, needle_lo, needle_hi, matches_remaining);
1063 }
1064 __except (Memory::detail::is_guarded_read_fault(GetExceptionCode()) ? EXCEPTION_EXECUTE_HANDLER
1065 : EXCEPTION_CONTINUE_SEARCH)
1066 {
1067 // Treat a faulted region as skipped, not partially scanned. Matches observed before the fault cannot be
1068 // trusted for Nth-occurrence accounting because unreadable tail bytes may hide additional matches.
1069 matches_remaining = original_matches_remaining;
1070 out_faulted = true;
1071 return nullptr;
1072 }
1073 #elif defined(_WIN64)
1074 // MinGW x64: route the unguarded find_pattern_raw sweep through the same vectored fault guard the foreign-
1075 // read primitives use. The guard is armed over exactly the bytes the per-region gate proved readable; a
1076 // concurrent decommit / reprotect that faults the sweep is swallowed and the region is skipped + counted,
1077 // closing the TOCTOU window the bare gate cannot.
1078 struct ScanContext
1079 {
1080 const std::byte *region_start;
1081 size_t scan_size;
1082 const Scanner::CompiledPattern *pattern;
1083 uintptr_t needle_lo;
1084 uintptr_t needle_hi;
1085 size_t *matches_remaining;
1086 const std::byte *result;
1087 16840 } scan_ctx{region_start, scan_size, &pattern, needle_lo, needle_hi, &matches_remaining, nullptr};
1088
1089 16840 const size_t original_matches_remaining = matches_remaining;
1090 16841 const auto run_scan = [](void *opaque) noexcept -> void
1091 {
1092 16841 auto *context = static_cast<ScanContext *>(opaque);
1093 13333 context->result =
1094 16841 scan_region_for_match(context->region_start, context->scan_size, *context->pattern,
1095 16841 context->needle_lo, context->needle_hi, *context->matches_remaining);
1096 13333 };
1097
1098 16840 const auto span_lo = reinterpret_cast<uintptr_t>(region_start);
1099
2/2
✓ Branch 4 → 5 taken 13333 times.
✓ Branch 4 → 6 taken 3508 times.
16840 if (Memory::detail::run_guarded_region(span_lo, span_lo + scan_size, run_scan, &scan_ctx))
1100 {
1101 13333 return scan_ctx.result;
1102 }
1103 // A faulted region is skipped, not partially scanned: restore the count so unreadable tail bytes that may
1104 // hide additional matches cannot corrupt Nth-occurrence accounting -- the same contract as the MSVC path.
1105 3508 matches_remaining = original_matches_remaining;
1106 3508 out_faulted = true;
1107 3508 return nullptr;
1108 #else
1109 return scan_region_for_match(region_start, scan_size, pattern, needle_lo, needle_hi, matches_remaining);
1110 #endif
1111 }
1112
1113 // Region-walking AOB scan shared by scan_executable_regions, scan_readable_regions, and the module-scoped
1114 // detail::scan_module_* entry points. Walks the committed regions of [window_lo, window_hi) via VirtualQuery
1115 // and runs the per-region scan (scan_region_for_match, behind the fault guard) against every region whose base
1116 // protection is present in accept_mask, returning the Nth match (1-based, adjusted by pattern.offset) or
1117 // nullptr. The whole-process scanners pass [0, UINTPTR_MAX); the module-scoped scan passes the image's [base,
1118 // end) so only one contiguous image is searched.
1119 //
1120 // Guard, no-access, and uncommitted regions are always skipped: PAGE_GUARD raises STATUS_GUARD_PAGE_VIOLATION
1121 // on the first touch and PAGE_NOACCESS faults even for reads, so neither is safe to dereference. The Windows
1122 // base protections (PAGE_READONLY, PAGE_READWRITE, ... , PAGE_EXECUTE_WRITECOPY) are mutually exclusive single
1123 // bits, so a bitwise-AND against a mask of the acceptable bases is a sound membership test. PAGE_GUARD is a
1124 // modifier bit OR-ed onto a base value (a guarded read-only page reads as PAGE_READONLY | PAGE_GUARD), so it
1125 // must be excluded separately or it would satisfy the mask and be scanned.
1126 //
1127 // Each region is scanned through the raw helper so the final `+ pattern.offset` applies exactly once (the
1128 // public find_pattern already applies offset; calling it here would double-apply). To find a signature that
1129 // straddles a protection split -- two adjacent accepted regions VirtualQuery reports separately because their
1130 // base protections differ (a sibling VirtualProtect carving part of .text into PAGE_EXECUTE_READWRITE is the
1131 // canonical case; VirtualQuery never coalesces regions with differing attributes) -- each accepted region's
1132 // scan is extended back by up to pattern_size - 1 bytes into the contiguous run of already-accepted regions it
1133 // abuts. The overlap is capped at pattern_size - 1 so a match lying wholly inside the previous region (already
1134 // counted there) can never be re-counted here, and bounded by the run start so it never reads past the bytes
1135 // the per-region gate proved readable.
1136 6789 const std::byte *scan_regions_filtered(const Scanner::CompiledPattern &pattern, size_t occurrence,
1137 DWORD accept_mask, uintptr_t window_lo, uintptr_t window_hi) noexcept
1138 {
1139 // The compiled pattern's own bytes buffer lives in readable heap memory, so a whole-process readable sweep
1140 // would match the needle against itself and could return the caller's pattern storage instead of the
1141 // intended target. Exclude any match that overlaps that buffer. The executable sweep never reaches
1142 // pattern.bytes (the heap is not executable), so this is a no-op there and keeps both scanners consistent:
1143 // a scan never matches the needle's own storage. The needle is the caller's allocation, so no real target
1144 // can share its range.
1145 6789 const auto needle_lo = reinterpret_cast<uintptr_t>(pattern.bytes.data());
1146 6787 const auto needle_hi = needle_lo + pattern.size();
1147
1148 6783 size_t matches_remaining = occurrence;
1149 6783 size_t faulted_regions = 0;
1150 6783 MEMORY_BASIC_INFORMATION mbi{};
1151 6783 uintptr_t addr = window_lo;
1152
1153 // Contiguous-accepted-run tracking for the cross-boundary overlap (see the function comment).
1154 // prev_accept_hi is the end of the previous accepted region; run_lo is the start of the run of contiguous
1155 // accepted regions the current region belongs to. A gap (a skipped, guarded, or non-readable region) breaks
1156 // the run because the bytes across it are not proven readable.
1157 6783 bool prev_accepted = false;
1158 6783 uintptr_t prev_accept_hi = 0;
1159 6783 uintptr_t run_lo = 0;
1160 6789 auto report_faulted_regions = [&]() noexcept
1161 {
1162
2/2
✓ Branch 2 → 3 taken 3281 times.
✓ Branch 2 → 4 taken 3508 times.
6789 if (faulted_regions == 0)
1163 3281 return;
1164
1165 // Surface the incomplete-scan state to the cascade layer (its own TU) so a uniqueness / occurrence
1166 // count run over a window that skipped a faulted region can fail closed: a hidden match could live in
1167 // the skipped bytes, so a count taken here is a lower bound, not a proof. OR-set (never clear) so a
1168 // fault in any one of several scans within a caller's measurement window stays observable until the
1169 // caller reads and clears it.
1170 3508 Scanner::detail::scan_incomplete_flag() = true;
1171
1172 // Best-effort diagnosis only; the sweep already skipped each faulted region and continued.
1173 try
1174 {
1175
1/2
✓ Branch 5 → 6 taken 3508 times.
✗ Branch 5 → 13 not taken.
3508 (void)Logger::get_instance().try_log(
1176 LogLevel::Debug,
1177 "Scanner: skipped {} region(s) that faulted mid-scan (concurrent decommit/reprotect).",
1178 faulted_regions);
1179 }
1180 catch (...)
1181 {
1182 }
1183
1184 // Surface the same skipped-region count to subscribers as a typed event. The dispatcher is lazy and can
1185 // allocate on first use, so diagnostics must never change the scan result.
1186 try
1187 {
1188
1/2
✓ Branch 8 → 9 taken 3508 times.
✗ Branch 8 → 16 not taken.
7016 Diagnostics::scanner_faults().emit_safe(Diagnostics::ScannerFaultEvent{
1189 3508 .faulted_regions = faulted_regions, .window_low = window_lo, .window_high = window_hi});
1190 }
1191 catch (...)
1192 {
1193 }
1194 3508 faulted_regions = 0;
1195 6783 };
1196
1197
6/6
✓ Branch 46 → 47 taken 44114 times.
✓ Branch 46 → 50 taken 5820 times.
✓ Branch 48 → 49 taken 44087 times.
✓ Branch 48 → 50 taken 34 times.
✓ Branch 51 → 5 taken 44086 times.
✓ Branch 51 → 52 taken 5855 times.
49934 while (addr < window_hi && VirtualQuery(reinterpret_cast<LPCVOID>(addr), &mbi, sizeof(mbi)))
1198 {
1199 44086 const bool protection_unsafe = (mbi.Protect & (PAGE_GUARD | PAGE_NOACCESS)) != 0;
1200 44086 const auto region_base = reinterpret_cast<uintptr_t>(mbi.BaseAddress);
1201 44086 const uintptr_t region_end = region_base + mbi.RegionSize;
1202
1203 // Clamp the region to the requested window so a region that straddles window_lo / window_hi is
1204 // inspected only where it intersects. For a whole-process sweep the window is [0, UINTPTR_MAX), so the
1205 // clamp is a no-op and the scanned span equals the region. For a module-scoped sweep this is what keeps
1206 // the scan inside [base, end) even when a VirtualQuery region (e.g. a section straddling the image
1207 // boundary) extends past it.
1208
2/2
✓ Branch 5 → 6 taken 8 times.
✓ Branch 5 → 7 taken 44078 times.
44086 const uintptr_t scan_lo = region_base < window_lo ? window_lo : region_base;
1209
2/2
✓ Branch 8 → 9 taken 12 times.
✓ Branch 8 → 10 taken 44074 times.
44086 const uintptr_t scan_hi = region_end > window_hi ? window_hi : region_end;
1210
1211
7/8
✓ Branch 11 → 12 taken 33613 times.
✓ Branch 11 → 39 taken 10473 times.
✓ Branch 12 → 13 taken 16951 times.
✓ Branch 12 → 39 taken 16662 times.
✓ Branch 13 → 14 taken 16843 times.
✓ Branch 13 → 39 taken 108 times.
✓ Branch 14 → 15 taken 16843 times.
✗ Branch 14 → 39 not taken.
44086 if (mbi.State == MEM_COMMIT && (mbi.Protect & accept_mask) != 0 && !protection_unsafe &&
1212 scan_hi > scan_lo)
1213 {
1214 // Continue the accepted run only when this region begins exactly where the previous accepted one
1215 // ended; otherwise restart it here. Done before computing the overlap so run_lo reflects the run
1216 // scan_lo joins.
1217
3/4
✓ Branch 15 → 16 taken 6122 times.
✓ Branch 15 → 17 taken 10721 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 6122 times.
16843 if (!prev_accepted || prev_accept_hi != scan_lo)
1218 {
1219 10721 run_lo = scan_lo;
1220 }
1221
1222 // Extend the scan back by up to pattern_size - 1 bytes into the contiguous accepted run so a match
1223 // that begins in the previous region's tail and ends in this one is found. Bounded by run_lo so the
1224 // read stays inside already-gated bytes; capped at pattern_size - 1 so an interior match is not
1225 // re-counted.
1226 16843 uintptr_t effective_scan_lo = scan_lo;
1227
6/6
✓ Branch 19 → 20 taken 16836 times.
✓ Branch 19 → 22 taken 7 times.
✓ Branch 20 → 21 taken 6122 times.
✓ Branch 20 → 22 taken 10714 times.
✓ Branch 23 → 24 taken 6122 times.
✓ Branch 23 → 29 taken 10721 times.
16843 if (pattern.size() > 1 && scan_lo > run_lo)
1228 {
1229 6122 const uintptr_t max_overlap = static_cast<uintptr_t>(pattern.size() - 1);
1230 6122 const uintptr_t available = scan_lo - run_lo;
1231
1/2
✓ Branch 25 → 26 taken 6122 times.
✗ Branch 25 → 27 not taken.
6122 effective_scan_lo = scan_lo - ((max_overlap < available) ? max_overlap : available);
1232 }
1233
1234 16843 const size_t scan_size = static_cast<size_t>(scan_hi - effective_scan_lo);
1235
2/2
✓ Branch 30 → 31 taken 16841 times.
✓ Branch 30 → 38 taken 2 times.
16843 if (scan_size >= pattern.size())
1236 {
1237 16841 const auto *region_start = reinterpret_cast<const std::byte *>(effective_scan_lo);
1238
1239 // The protection gate above proved the region readable at gate time; scan_region_guarded
1240 // backstops a concurrent decommit / reprotect that could fault the read after the gate (a
1241 // TOCTOU the gate cannot close). A faulted region is skipped and counted, not fatal.
1242 16841 bool region_faulted = false;
1243 16841 const std::byte *result = scan_region_guarded(region_start, scan_size, pattern, needle_lo,
1244 needle_hi, matches_remaining, region_faulted);
1245
2/2
✓ Branch 32 → 33 taken 935 times.
✓ Branch 32 → 35 taken 15906 times.
16841 if (result != nullptr)
1246 {
1247 935 report_faulted_regions();
1248 935 return result;
1249 }
1250
2/2
✓ Branch 35 → 36 taken 3508 times.
✓ Branch 35 → 37 taken 12398 times.
15906 if (region_faulted)
1251 3508 ++faulted_regions;
1252 }
1253
1254 15908 prev_accepted = true;
1255 15908 prev_accept_hi = scan_hi;
1256 15908 }
1257 else
1258 {
1259 27243 prev_accepted = false;
1260 }
1261
1262
1/2
✗ Branch 40 → 41 not taken.
✓ Branch 40 → 42 taken 43151 times.
43151 assert(region_end > addr && "VirtualQuery returned a non-advancing region");
1263
1/2
✗ Branch 43 → 44 not taken.
✓ Branch 43 → 45 taken 43151 times.
43151 if (region_end <= addr)
1264 break; // Overflow guard.
1265 43151 addr = region_end;
1266 }
1267
1268 5855 report_faulted_regions();
1269 5854 return nullptr;
1270 }
1271
1272 // Base protections accepted by the executable-only sweeps: the three page variants that grant execute *and*
1273 // read. Bare PAGE_EXECUTE (execute without a read bit) is excluded because dereferencing it raises an access
1274 // violation; PAGE_GUARD / PAGE_NOACCESS are filtered separately inside scan_regions_filtered. This is the scope
1275 // for code-only scans: the whole-process scan_executable_regions and the prologue-recovery fallback, whose
1276 // rebuilt near-JMP can only ever overwrite a code prologue.
1277 constexpr DWORD EXECUTABLE_PAGE_FLAGS = PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
1278
1279 // Base protections accepted by the readable sweep and the data-capable module-scoped cascade: the
1280 // executable-readable set plus the non-executable readable pages (.rdata / .data and read-only heaps). This
1281 // reaches C++ vtables, RTTI type descriptors, and other read-only metadata the executable-only sweep cannot
1282 // see.
1283 constexpr DWORD READABLE_PAGE_FLAGS = EXECUTABLE_PAGE_FLAGS | PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY;
1284
1285 } // anonymous namespace
1286
1287 // Module-scoped siblings of scan_executable_regions / scan_readable_regions:
1288 // each searches only the mapped image [range.base, range.end) and returns the
1289 // Nth match (1-based, adjusted by pattern.offset) or nullptr. They are the internal entry points the cascade
1290 // resolver (its own TU) calls instead of reaching the page-protection masks directly. Both reuse
1291 // scan_regions_filtered's per-region VirtualQuery protection gate, so a non-readable interior page (a
1292 // section-alignment gap, a guard page, a sibling VirtualProtect on part of the image) is skipped instead of
1293 // dereferenced. find_pattern_raw itself does an unguarded memchr / SIMD compare; on the no-fault path the gate is
1294 // what makes that safe, and scan_region_guarded backstops the gate against a concurrent decommit / reprotect that
1295 // would fault the read after the gate passed.
1296 31 const std::byte *Scanner::detail::scan_module_executable(const Scanner::CompiledPattern &pattern,
1297 Memory::ModuleRange range, std::size_t occurrence) noexcept
1298 {
1299 // EXECUTABLE_PAGE_FLAGS confines the match to code: the prologue-recovery fallback's rebuilt near-JMP can only
1300 // ever overwrite a code prologue, so a data-page hit would be a false positive.
1301
4/8
✓ Branch 3 → 4 taken 31 times.
✗ Branch 3 → 7 not taken.
✓ Branch 4 → 5 taken 31 times.
✗ Branch 4 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 31 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 31 times.
31 if (pattern.empty() || occurrence == 0 || !range.valid())
1302 {
1303 return nullptr;
1304 }
1305 31 return scan_regions_filtered(pattern, occurrence, EXECUTABLE_PAGE_FLAGS, range.base, range.end);
1306 }
1307
1308 6598 const std::byte *Scanner::detail::scan_module_readable(const Scanner::CompiledPattern &pattern,
1309 Memory::ModuleRange range, std::size_t occurrence) noexcept
1310 {
1311 // READABLE_PAGE_FLAGS lets one pass cover both .text and .rdata / .data candidates, which is why the in-module
1312 // cascade needs no ScannerKind split.
1313
6/8
✓ Branch 3 → 4 taken 6598 times.
✗ Branch 3 → 7 not taken.
✓ Branch 4 → 5 taken 6598 times.
✗ Branch 4 → 7 not taken.
✓ Branch 6 → 7 taken 4 times.
✓ Branch 6 → 8 taken 6594 times.
✓ Branch 9 → 10 taken 4 times.
✓ Branch 9 → 11 taken 6594 times.
6598 if (pattern.empty() || occurrence == 0 || !range.valid())
1314 {
1315 4 return nullptr;
1316 }
1317 6594 return scan_regions_filtered(pattern, occurrence, READABLE_PAGE_FLAGS, range.base, range.end);
1318 }
1319
1320 // Centralizes the executable-page protection gate for out-of-TU callers (the string-xref backend): one VirtualQuery
1321 // walk over [range.base, range.end) that returns each committed, execute-readable region clamped to the range,
1322 // using the identical mask scan_module_executable applies. The per-region gate (MEM_COMMIT, EXECUTABLE_PAGE_FLAGS,
1323 // not PAGE_GUARD / PAGE_NOACCESS) guarantees the window is readable at gate time; the caller still wraps its reads
1324 // of the window in a fault guard so a concurrent decommit / reprotect between gate and read cannot fault the host,
1325 // exactly as scan_region_guarded backstops the in-TU sweeps.
1326 std::vector<Scanner::detail::ExecutableWindow>
1327 1256 Scanner::detail::collect_executable_windows(Memory::ModuleRange range)
1328 {
1329 1256 std::vector<ExecutableWindow> windows;
1330
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1256 times.
1256 if (!range.valid())
1331 {
1332 return windows;
1333 }
1334
1335 1256 MEMORY_BASIC_INFORMATION mbi{};
1336 1256 uintptr_t addr = range.base;
1337
6/8
✓ Branch 21 → 22 taken 3613 times.
✓ Branch 21 → 25 taken 1256 times.
✓ Branch 22 → 23 taken 3613 times.
✗ Branch 22 → 31 not taken.
✓ Branch 23 → 24 taken 3613 times.
✗ Branch 23 → 25 not taken.
✓ Branch 26 → 6 taken 3613 times.
✓ Branch 26 → 27 taken 1256 times.
4869 while (addr < range.end && VirtualQuery(reinterpret_cast<LPCVOID>(addr), &mbi, sizeof(mbi)))
1338 {
1339 3613 const bool protection_unsafe = (mbi.Protect & (PAGE_GUARD | PAGE_NOACCESS)) != 0;
1340 3613 const auto region_base = reinterpret_cast<uintptr_t>(mbi.BaseAddress);
1341 3613 const uintptr_t region_end = region_base + mbi.RegionSize;
1342
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 3613 times.
3613 const uintptr_t scan_lo = region_base < range.base ? range.base : region_base;
1343
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 3613 times.
3613 const uintptr_t scan_hi = region_end > range.end ? range.end : region_end;
1344
1345
6/8
✓ Branch 12 → 13 taken 1761 times.
✓ Branch 12 → 18 taken 1852 times.
✓ Branch 13 → 14 taken 1730 times.
✓ Branch 13 → 18 taken 31 times.
✓ Branch 14 → 15 taken 1730 times.
✗ Branch 14 → 18 not taken.
✓ Branch 15 → 16 taken 1730 times.
✗ Branch 15 → 18 not taken.
3613 if (mbi.State == MEM_COMMIT && (mbi.Protect & EXECUTABLE_PAGE_FLAGS) != 0 && !protection_unsafe &&
1346 scan_hi > scan_lo)
1347 {
1348
1/2
✓ Branch 16 → 17 taken 1730 times.
✗ Branch 16 → 30 not taken.
1730 windows.push_back(ExecutableWindow{scan_lo, static_cast<std::size_t>(scan_hi - scan_lo)});
1349 }
1350
1351
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 3613 times.
3613 if (region_end <= addr)
1352 {
1353 break; // Overflow guard, mirroring scan_regions_filtered.
1354 }
1355 3613 addr = region_end;
1356 }
1357 1256 return windows;
1358 }
1359
1360 // Single-address sibling of the executable-page gate scan_regions_filtered applies per region. One VirtualQuery,
1361 // matched against the identical mask (MEM_COMMIT, EXECUTABLE_PAGE_FLAGS, not PAGE_GUARD / PAGE_NOACCESS), so the
1362 // prologue-recovery fallback can vet a decoded E9 destination without re-deriving the Windows page masks or
1363 // constraining it to a loaded module (a sibling mod's trampoline is VirtualAlloc'd outside every image).
1364 11 bool Scanner::detail::is_executable_address(std::uintptr_t address) noexcept
1365 {
1366 11 MEMORY_BASIC_INFORMATION mbi{};
1367
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 11 times.
11 if (VirtualQuery(reinterpret_cast<LPCVOID>(address), &mbi, sizeof(mbi)) == 0)
1368 {
1369 return false;
1370 }
1371 11 const bool protection_unsafe = (mbi.Protect & (PAGE_GUARD | PAGE_NOACCESS)) != 0;
1372
5/6
✓ Branch 5 → 6 taken 10 times.
✓ Branch 5 → 9 taken 1 time.
✓ Branch 6 → 7 taken 8 times.
✓ Branch 6 → 9 taken 2 times.
✓ Branch 7 → 8 taken 8 times.
✗ Branch 7 → 9 not taken.
11 return mbi.State == MEM_COMMIT && (mbi.Protect & EXECUTABLE_PAGE_FLAGS) != 0 && !protection_unsafe;
1373 }
1374
1375 126 const std::byte *DetourModKit::Scanner::scan_executable_regions(const CompiledPattern &pattern, size_t occurrence)
1376 {
1377
6/6
✓ Branch 3 → 4 taken 124 times.
✓ Branch 3 → 5 taken 2 times.
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 122 times.
✓ Branch 7 → 8 taken 4 times.
✓ Branch 7 → 9 taken 122 times.
126 if (pattern.empty() || occurrence == 0)
1378 4 return nullptr;
1379
1380 122 Logger &logger = Logger::get_instance();
1381
1382
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 14 taken 122 times.
122 if (!pattern_has_literal_byte(pattern))
1383 {
1384 logger.warning("scan_executable_regions: pattern contains no literal "
1385 "bytes (all wildcards); returning first readable region "
1386 "start unchanged");
1387 }
1388
1389 // EXECUTABLE_PAGE_FLAGS keeps the sweep to pages we can actually *read*; bare
1390 // PAGE_EXECUTE grants execute without read, so dereferencing such a page would raise an access violation.
1391 // Whole-process sweep: the window spans the entire user address space, so the clamp in scan_regions_filtered is
1392 // a no-op and the walk stops only when VirtualQuery runs off the end of the address space.
1393 122 return scan_regions_filtered(pattern, occurrence, EXECUTABLE_PAGE_FLAGS, 0, UINTPTR_MAX);
1394 }
1395
1396 43 const std::byte *DetourModKit::Scanner::scan_readable_regions(const CompiledPattern &pattern, size_t occurrence)
1397 {
1398
6/6
✓ Branch 3 → 4 taken 42 times.
✓ Branch 3 → 5 taken 1 time.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 41 times.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 41 times.
43 if (pattern.empty() || occurrence == 0)
1399 2 return nullptr;
1400
1401 41 Logger &logger = Logger::get_instance();
1402
1403
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 14 taken 41 times.
41 if (!pattern_has_literal_byte(pattern))
1404 {
1405 logger.warning("scan_readable_regions: pattern contains no literal "
1406 "bytes (all wildcards); returning first readable region "
1407 "start unchanged");
1408 }
1409
1410 // READABLE_PAGE_FLAGS is a superset of the executable-only mask: every committed region we can read, including
1411 // .rdata / .data (PAGE_READONLY /
1412 // PAGE_READWRITE / PAGE_WRITECOPY) and read-only heaps, plus the execute-readable variants. The semantic is
1413 // "find this pattern anywhere readable", so execute-readable code pages are intentionally included rather than
1414 // deduplicated against scan_executable_regions; callers wanting non-code matches post-filter. The window spans
1415 // the whole address space.
1416 41 return scan_regions_filtered(pattern, occurrence, READABLE_PAGE_FLAGS, 0, UINTPTR_MAX);
1417 }
1418
1419 4 Scanner::SimdLevel DetourModKit::Scanner::active_simd_level() noexcept
1420 {
1421 #ifdef DMK_HAS_AVX512
1422 if (cpu_has_avx512())
1423 return SimdLevel::Avx512;
1424 #endif
1425 #ifdef DMK_HAS_AVX2
1426
1/2
✓ Branch 3 → 4 taken 4 times.
✗ Branch 3 → 5 not taken.
4 if (cpu_has_avx2())
1427 4 return SimdLevel::Avx2;
1428 #endif
1429 #ifdef DMK_HAS_SSE2
1430 return SimdLevel::Sse2;
1431 #else
1432 return SimdLevel::Scalar;
1433 #endif
1434 }
1435
1436 13 bool DetourModKit::Scanner::is_likely_function_prologue(std::uintptr_t addr) noexcept
1437 {
1438
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 12 times.
13 if (addr == 0)
1439 {
1440 1 return false;
1441 }
1442
1443 // Read the first opcode byte under a fault guard rather than is_readable + a raw dereference. is_readable is a
1444 // TOCTOU illusion (the page can change or unmap between the check and the read), and the bare dereference would
1445 // then fault the host. seh_read returns nullopt on any fault.
1446 12 const auto b0 = Memory::seh_read<std::uint8_t>(addr);
1447
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 11 times.
12 if (!b0)
1448 {
1449 1 return false;
1450 }
1451
1452 // Reject bytes that never begin a real function prologue, so an AOB match that landed in inter-function padding
1453 // or past a function's end is filtered out instead of accepted as a target:
1454 // 0x00 -- zero fill / uninitialized page (decodes as `add [rax], al`)
1455 // 0xCC -- INT3, the alignment padding linkers insert between functions
1456 // 0xC3 -- RET (near return): a function epilogue, not a prologue
1457 // 0xC2 -- RET imm16: likewise a return, not a prologue
1458
8/8
✓ Branch 9 → 10 taken 10 times.
✓ Branch 9 → 17 taken 1 time.
✓ Branch 11 → 12 taken 9 times.
✓ Branch 11 → 17 taken 1 time.
✓ Branch 13 → 14 taken 8 times.
✓ Branch 13 → 17 taken 1 time.
✓ Branch 15 → 16 taken 7 times.
✓ Branch 15 → 17 taken 1 time.
11 return *b0 != 0x00 && *b0 != 0xCC && *b0 != 0xC2 && *b0 != 0xC3;
1459 }
1460 } // namespace DetourModKit
1461