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 |