src/internal/scan_shared.hpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #ifndef DETOURMODKIT_INTERNAL_SCAN_SHARED_HPP | ||
| 2 | #define DETOURMODKIT_INTERNAL_SCAN_SHARED_HPP | ||
| 3 | |||
| 4 | /** | ||
| 5 | * @file internal/scan_shared.hpp | ||
| 6 | * @brief Small shared helpers for scan-module TUs: anchor selection, resolution arithmetic, and strict UTF-8 decoding. | ||
| 7 | * @details Never installed. The scan_matching, scan_resolution, and scan_prologue_recovery TUs all turn a value Pattern | ||
| 8 | * into an EnginePattern with a haystack-chosen anchor and screen byte-tier resolutions through the same | ||
| 9 | * plausible-userspace floor, so those helpers live here in one place rather than being duplicated. The anchor | ||
| 10 | * override is correctness-neutral: it only changes which single byte the memchr prefilter sweeps for; the full | ||
| 11 | * masked compare still decides every accepted position. | ||
| 12 | */ | ||
| 13 | |||
| 14 | #include "internal/memory_guarded.hpp" | ||
| 15 | #include "internal/scan_engine.hpp" | ||
| 16 | #include "internal/scan_exclusions.hpp" | ||
| 17 | |||
| 18 | #include "DetourModKit/region.hpp" | ||
| 19 | #include "DetourModKit/scan.hpp" | ||
| 20 | |||
| 21 | #include <array> | ||
| 22 | #include <cstddef> | ||
| 23 | #include <cstdint> | ||
| 24 | #include <optional> | ||
| 25 | #include <span> | ||
| 26 | #include <string_view> | ||
| 27 | |||
| 28 | namespace DetourModKit | ||
| 29 | { | ||
| 30 | namespace detail | ||
| 31 | { | ||
| 32 | // Bounded haystack sampling budget. A byte-frequency ranking needs only a representative sample, not the whole | ||
| 33 | // image: 64 page reads (256 KiB) strided across the scope are ample to rank 256 byte values, and the fixed cap | ||
| 34 | // keeps anchor selection a constant cost regardless of scope size so it never approaches the cost of the scan | ||
| 35 | // it accelerates. Scopes larger than the budget ceiling (a whole-process window, terabytes of mostly-unmapped | ||
| 36 | // address space) are not sampled at all: the sample would be unrepresentative and would pay for dozens of | ||
| 37 | // guaranteed page faults, so those fall back to the compile-time anchor. | ||
| 38 | inline constexpr std::size_t SAMPLE_PAGE = 0x1000; | ||
| 39 | inline constexpr std::size_t SAMPLE_MAX_PAGES = 64; | ||
| 40 | inline constexpr std::size_t SAMPLE_MIN_BYTES = SAMPLE_PAGE; | ||
| 41 | inline constexpr std::size_t SAMPLE_MAX_SCOPE = std::size_t{512} * 1024 * 1024; | ||
| 42 | |||
| 43 | struct HaystackHistogram | ||
| 44 | { | ||
| 45 | std::array<std::uint32_t, 256> counts{}; | ||
| 46 | std::size_t sampled = 0; | ||
| 47 | }; | ||
| 48 | |||
| 49 | // Sample a bounded, strided set of pages from the scope and tally a 256-bin byte-frequency histogram. Reads go | ||
| 50 | // through guarded_read_bytes one page at a time, so an unmapped / guard page inside the scope is skipped rather | ||
| 51 | // than faulting the host; the stride spreads the sample across .text and .rdata / .data so the frequencies | ||
| 52 | // reflect the whole image, not just its first pages. Entirely stack-based (the histogram and the page buffer | ||
| 53 | // are locals), so it never allocates and is safe to call from the noexcept scan paths. | ||
| 54 | 390 | [[nodiscard]] inline HaystackHistogram sample_haystack(Region scope) noexcept | |
| 55 | { | ||
| 56 | 390 | HaystackHistogram histogram; | |
| 57 | 390 | const std::uintptr_t base = scope.base.raw(); | |
| 58 |
4/6✓ Branch 3 → 4 taken 390 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 390 times.
✗ Branch 4 → 6 not taken.
✓ Branch 5 → 6 taken 3 times.
✓ Branch 5 → 7 taken 387 times.
|
390 | if (base == 0 || scope.size == 0 || scope.size > SAMPLE_MAX_SCOPE) |
| 59 | { | ||
| 60 | 3 | return histogram; | |
| 61 | } | ||
| 62 | 387 | const std::size_t total_pages = (scope.size + SAMPLE_PAGE - 1) / SAMPLE_PAGE; | |
| 63 |
2/2✓ Branch 7 → 8 taken 29 times.
✓ Branch 7 → 9 taken 358 times.
|
387 | const std::size_t stride = (total_pages > SAMPLE_MAX_PAGES) ? (total_pages / SAMPLE_MAX_PAGES) : 1; |
| 64 | 387 | std::array<std::uint8_t, SAMPLE_PAGE> buffer{}; | |
| 65 | 387 | std::size_t pages_read = 0; | |
| 66 |
4/4✓ Branch 23 → 24 taken 3532 times.
✓ Branch 23 → 25 taken 359 times.
✓ Branch 24 → 11 taken 3503 times.
✓ Branch 24 → 25 taken 29 times.
|
3891 | for (std::size_t page = 0; page < total_pages && pages_read < SAMPLE_MAX_PAGES; page += stride) |
| 67 | { | ||
| 68 | 3503 | const std::uintptr_t page_address = base + static_cast<std::uintptr_t>(page) * SAMPLE_PAGE; | |
| 69 | 3503 | const std::size_t remaining = scope.size - page * SAMPLE_PAGE; | |
| 70 | 3503 | const std::size_t want = (remaining < SAMPLE_PAGE) ? remaining : SAMPLE_PAGE; | |
| 71 |
2/2✓ Branch 14 → 15 taken 4 times.
✓ Branch 14 → 16 taken 3499 times.
|
3503 | if (!guarded_read_bytes(page_address, buffer.data(), want)) |
| 72 | { | ||
| 73 | // A page that faults mid-read is skipped; the sample stays a valid (smaller) lower bound. | ||
| 74 | 4 | continue; | |
| 75 | } | ||
| 76 |
2/2✓ Branch 20 → 17 taken 12338859 times.
✓ Branch 20 → 21 taken 3500 times.
|
12342359 | for (std::size_t i = 0; i < want; ++i) |
| 77 | { | ||
| 78 | 12338859 | ++histogram.counts[buffer[i]]; | |
| 79 | } | ||
| 80 | 3500 | histogram.sampled += want; | |
| 81 | 3500 | ++pages_read; | |
| 82 | } | ||
| 83 | 388 | return histogram; | |
| 84 | } | ||
| 85 | |||
| 86 | // When a sufficient haystack sample exists, pick the pattern's fully-known byte whose value is rarest in this | ||
| 87 | // image (the most selective prefilter for this haystack), overriding the compile-time rarest-byte anchor the | ||
| 88 | // Pattern carries; otherwise fall back to that compile-time anchor. Returns the engine "no fully-known byte" | ||
| 89 | // sentinel (size()) when segment 0 has no full byte. Correctness-neutral: the anchor only selects which single | ||
| 90 | // byte the memchr prefilter sweeps for; the full masked compare still decides every accepted position. | ||
| 91 | // | ||
| 92 | // The override is confined to segment 0 (the fixed run before the first bounded jump), exactly like the | ||
| 93 | // compile-time anchor: the segmented matcher locates that first run and then walks the variable gaps, so a byte | ||
| 94 | // in a later segment sits at a gap-dependent address the memchr prefilter cannot sweep for. Choosing an anchor | ||
| 95 | // outside segment 0 would compute a wrong candidate start and silently miss real matches. | ||
| 96 | [[nodiscard]] inline std::size_t | ||
| 97 | 397 | choose_scan_anchor(const scan::Pattern &pattern, const HaystackHistogram &histogram) noexcept | |
| 98 | { | ||
| 99 | 397 | const std::size_t size = pattern.size(); | |
| 100 |
2/2✓ Branch 3 → 4 taken 55 times.
✓ Branch 3 → 9 taken 341 times.
|
396 | if (histogram.sampled < SAMPLE_MIN_BYTES) |
| 101 | { | ||
| 102 |
2/2✓ Branch 5 → 6 taken 55 times.
✓ Branch 5 → 7 taken 1 time.
|
55 | return pattern.has_anchor() ? pattern.anchor_index() : size; |
| 103 | } | ||
| 104 | 341 | const std::span<const std::byte> bytes = pattern.bytes(); | |
| 105 | 341 | const std::span<const std::byte> mask = pattern.mask(); | |
| 106 | 341 | const detail::PatternBuffer &data = detail::pattern_buffer(pattern); | |
| 107 |
2/2✓ Branch 12 → 13 taken 15 times.
✓ Branch 12 → 15 taken 326 times.
|
341 | const std::size_t segment0_end = (data.jump_count == 0) ? size : data.jumps[0].position; |
| 108 | 341 | std::size_t best_index = size; | |
| 109 | 341 | std::uint32_t best_count = 0; | |
| 110 |
2/2✓ Branch 28 → 17 taken 3845 times.
✓ Branch 28 → 29 taken 341 times.
|
4186 | for (std::size_t i = 0; i < segment0_end; ++i) |
| 111 | { | ||
| 112 |
2/2✓ Branch 18 → 19 taken 116 times.
✓ Branch 18 → 20 taken 3729 times.
|
3845 | if (mask[i] != std::byte{0xFF}) |
| 113 | { | ||
| 114 | // Only a fully-known byte gives the prefilter one exact value to memchr for; nibble / wildcard | ||
| 115 | // positions cannot anchor. | ||
| 116 | 116 | continue; | |
| 117 | } | ||
| 118 | 7458 | const std::uint32_t count = histogram.counts[std::to_integer<std::uint8_t>(bytes[i])]; | |
| 119 |
4/4✓ Branch 24 → 25 taken 3388 times.
✓ Branch 24 → 26 taken 341 times.
✓ Branch 25 → 26 taken 332 times.
✓ Branch 25 → 27 taken 3056 times.
|
3729 | if (best_index == size || count < best_count) |
| 120 | { | ||
| 121 | 673 | best_index = i; | |
| 122 | 673 | best_count = count; | |
| 123 | } | ||
| 124 | } | ||
| 125 | 341 | return best_index; | |
| 126 | } | ||
| 127 | |||
| 128 | // Builds the engine pattern for a value Pattern with the haystack-chosen anchor. Allocates two small vectors, | ||
| 129 | // so every caller on a noexcept path guards this against std::bad_alloc. | ||
| 130 | [[nodiscard]] inline EnginePattern | ||
| 131 | 397 | to_engine_pattern(const scan::Pattern &pattern, const HaystackHistogram &histogram) | |
| 132 | { | ||
| 133 | 397 | const std::size_t anchor = choose_scan_anchor(pattern, histogram); | |
| 134 | 397 | return engine_pattern_from(pattern, anchor); | |
| 135 | } | ||
| 136 | |||
| 137 | /** | ||
| 138 | * @brief Decodes one UTF-8 code point and advances @p pos. | ||
| 139 | * @details Runtime and persistence share this strict decoder. A replacement character changes the requested | ||
| 140 | * literal. The caller must pass pos less than text.size(). | ||
| 141 | * @param text Input bytes. | ||
| 142 | * @param pos Input and output byte position. | ||
| 143 | * @param out Decoded scalar value on success. | ||
| 144 | * @return True on success. False for an invalid leader, tail, scalar value, overlong form, or truncated | ||
| 145 | * sequence. | ||
| 146 | */ | ||
| 147 | 259 | [[nodiscard]] inline bool decode_utf8(std::string_view text, std::size_t &pos, char32_t &out) noexcept | |
| 148 | { | ||
| 149 | 340 | const auto byte_at = [&text](std::size_t index) noexcept { return static_cast<std::uint8_t>(text[index]); }; | |
| 150 | |||
| 151 | 259 | const std::uint8_t lead = byte_at(pos); | |
| 152 | 259 | std::size_t extra = 0; | |
| 153 | 259 | char32_t value = 0; | |
| 154 |
2/2✓ Branch 3 → 4 taken 199 times.
✓ Branch 3 → 5 taken 60 times.
|
259 | if (lead < 0x80) |
| 155 | { | ||
| 156 | 199 | out = lead; | |
| 157 | 199 | ++pos; | |
| 158 | 199 | return true; | |
| 159 | } | ||
| 160 |
4/4✓ Branch 5 → 6 taken 54 times.
✓ Branch 5 → 8 taken 6 times.
✓ Branch 6 → 7 taken 19 times.
✓ Branch 6 → 8 taken 35 times.
|
60 | if (lead >= 0xC2 && lead <= 0xDF) |
| 161 | { | ||
| 162 | 19 | extra = 1; | |
| 163 | 19 | value = lead & 0x1FU; | |
| 164 | } | ||
| 165 |
4/4✓ Branch 8 → 9 taken 35 times.
✓ Branch 8 → 11 taken 6 times.
✓ Branch 9 → 10 taken 19 times.
✓ Branch 9 → 11 taken 16 times.
|
41 | else if (lead >= 0xE0 && lead <= 0xEF) |
| 166 | { | ||
| 167 | 19 | extra = 2; | |
| 168 | 19 | value = lead & 0x0FU; | |
| 169 | } | ||
| 170 |
4/4✓ Branch 11 → 12 taken 16 times.
✓ Branch 11 → 14 taken 6 times.
✓ Branch 12 → 13 taken 14 times.
✓ Branch 12 → 14 taken 2 times.
|
22 | else if (lead >= 0xF0 && lead <= 0xF4) |
| 171 | { | ||
| 172 | 14 | extra = 3; | |
| 173 | 14 | value = lead & 0x07U; | |
| 174 | } | ||
| 175 | else | ||
| 176 | { | ||
| 177 | 8 | return false; | |
| 178 | } | ||
| 179 | |||
| 180 | // The caller guarantees pos < size. The subtraction keeps this bounds check free from pointer overflow. | ||
| 181 |
2/2✓ Branch 16 → 17 taken 11 times.
✓ Branch 16 → 18 taken 41 times.
|
52 | if (extra >= text.size() - pos) |
| 182 | { | ||
| 183 | 11 | return false; | |
| 184 | } | ||
| 185 |
2/2✓ Branch 23 → 19 taken 81 times.
✓ Branch 23 → 24 taken 30 times.
|
111 | for (std::size_t i = 1; i <= extra; ++i) |
| 186 | { | ||
| 187 | 81 | const std::uint8_t continuation = byte_at(pos + i); | |
| 188 |
2/2✓ Branch 20 → 21 taken 11 times.
✓ Branch 20 → 22 taken 70 times.
|
81 | if ((continuation & 0xC0U) != 0x80U) |
| 189 | { | ||
| 190 | 11 | return false; | |
| 191 | } | ||
| 192 | 70 | value = (value << 6) | (continuation & 0x3FU); | |
| 193 | } | ||
| 194 | |||
| 195 | // The leader ranges above exclude two-byte overlongs. These checks reject the remaining invalid values. | ||
| 196 |
10/10✓ Branch 24 → 25 taken 14 times.
✓ Branch 24 → 26 taken 16 times.
✓ Branch 25 → 26 taken 9 times.
✓ Branch 25 → 31 taken 5 times.
✓ Branch 26 → 27 taken 13 times.
✓ Branch 26 → 28 taken 12 times.
✓ Branch 27 → 28 taken 8 times.
✓ Branch 27 → 31 taken 5 times.
✓ Branch 28 → 29 taken 13 times.
✓ Branch 28 → 30 taken 7 times.
|
30 | if ((extra == 2 && value < 0x800) || (extra == 3 && value < 0x10000) || |
| 197 |
4/4✓ Branch 29 → 30 taken 8 times.
✓ Branch 29 → 31 taken 5 times.
✓ Branch 30 → 31 taken 5 times.
✓ Branch 30 → 32 taken 10 times.
|
20 | (value >= 0xD800 && value <= 0xDFFF) || value > 0x10FFFF) |
| 198 | { | ||
| 199 | 20 | return false; | |
| 200 | } | ||
| 201 | 10 | pos += extra + 1; | |
| 202 | 10 | out = value; | |
| 203 | 10 | return true; | |
| 204 | } | ||
| 205 | |||
| 206 | /// Reports whether every byte of @p text belongs to a well-formed UTF-8 sequence (decode_utf8's rule). | ||
| 207 | 48 | [[nodiscard]] inline bool utf8_is_well_formed(std::string_view text) noexcept | |
| 208 | { | ||
| 209 | 48 | std::size_t pos = 0; | |
| 210 |
2/2✓ Branch 8 → 3 taken 226 times.
✓ Branch 8 → 9 taken 9 times.
|
235 | while (pos < text.size()) |
| 211 | { | ||
| 212 | 226 | char32_t code_point = 0; | |
| 213 |
2/2✓ Branch 4 → 5 taken 39 times.
✓ Branch 4 → 6 taken 187 times.
|
226 | if (!decode_utf8(text, pos, code_point)) |
| 214 | { | ||
| 215 | 39 | return false; | |
| 216 | } | ||
| 217 | } | ||
| 218 | 9 | return true; | |
| 219 | } | ||
| 220 | |||
| 221 | // Exact-membership validity checks for the string-xref facet enums, shared by the direct find_string_xref | ||
| 222 | // boundary and resolve()'s whole-ladder prepass so both fail closed on the same vocabulary. | ||
| 223 | 748 | [[nodiscard]] inline constexpr bool valid_string_encoding(scan::StringEncoding encoding) noexcept | |
| 224 | { | ||
| 225 |
4/4✓ Branch 2 → 3 taken 21 times.
✓ Branch 2 → 4 taken 727 times.
✓ Branch 3 → 4 taken 18 times.
✓ Branch 3 → 5 taken 3 times.
|
748 | return encoding == scan::StringEncoding::Utf8 || encoding == scan::StringEncoding::Utf16le; |
| 226 | } | ||
| 227 | |||
| 228 | 745 | [[nodiscard]] inline constexpr bool valid_xref_return(scan::XrefReturn mode) noexcept | |
| 229 | { | ||
| 230 |
2/2✓ Branch 2 → 3 taken 741 times.
✓ Branch 2 → 4 taken 4 times.
|
745 | switch (mode) |
| 231 | { | ||
| 232 | 741 | case scan::XrefReturn::ReferencingInstruction: | |
| 233 | case scan::XrefReturn::EnclosingFunction: | ||
| 234 | case scan::XrefReturn::StringPointerSlot: | ||
| 235 | 741 | return true; | |
| 236 | } | ||
| 237 | 4 | return false; | |
| 238 | } | ||
| 239 | |||
| 240 | // Zero keeps the decoder's natural width; a positive width may narrow at most the 64-bit result type. | ||
| 241 | 1166 | [[nodiscard]] inline constexpr bool valid_code_constant_byte_width(std::uint8_t byte_width) noexcept | |
| 242 | { | ||
| 243 | 1166 | return byte_width <= sizeof(std::int64_t); | |
| 244 | } | ||
| 245 | |||
| 246 | /** | ||
| 247 | * @struct ResolvedScanHit | ||
| 248 | * @brief A public scan hit plus the physical matched span that supplied its byte evidence. | ||
| 249 | * @details @ref winning_index and @ref match_span identify the selected rung for fresh epoch validation. | ||
| 250 | * @ref match_span is the raw matched extent. @ref physical_source can also cover a RIP instruction | ||
| 251 | * suffix that extends past that match (T-CODE-EPOCH). | ||
| 252 | */ | ||
| 253 | struct ResolvedScanHit | ||
| 254 | { | ||
| 255 | scan::Hit hit; | ||
| 256 | Region physical_source; | ||
| 257 | /// Ladder index of the selected candidate. SIZE_MAX denotes a prologue-recovery hit. | ||
| 258 | std::size_t winning_index = static_cast<std::size_t>(-1); | ||
| 259 | /// The selected byte rung's raw matched span. Text tiers and recovery leave it empty. | ||
| 260 | Region match_span; | ||
| 261 | }; | ||
| 262 | |||
| 263 | /// Resolves a request and retains private physical-source provenance for anchor quorum checks. | ||
| 264 | [[nodiscard]] Result<ResolvedScanHit> resolve_scan_with_provenance(const scan::ScanRequest &request); | ||
| 265 | |||
| 266 | /// Returns the compiled byte Pattern of a Direct or RipRelative candidate, or nullptr for a text tier. | ||
| 267 | 666 | [[nodiscard]] inline const scan::Pattern *byte_pattern_of(const scan::Candidate &candidate) noexcept | |
| 268 | { | ||
| 269 |
2/2✓ Branch 3 → 4 taken 638 times.
✓ Branch 3 → 5 taken 28 times.
|
666 | if (const scan::DirectPattern *direct = candidate.as_direct()) |
| 270 | { | ||
| 271 | 638 | return &direct->pattern; | |
| 272 | } | ||
| 273 |
1/2✓ Branch 6 → 7 taken 28 times.
✗ Branch 6 → 8 not taken.
|
28 | if (const scan::RipRelativePattern *rip = candidate.as_rip_relative()) |
| 274 | { | ||
| 275 | 28 | return &rip->pattern; | |
| 276 | } | ||
| 277 | ✗ | return nullptr; | |
| 278 | } | ||
| 279 | |||
| 280 | /** | ||
| 281 | * @struct ResolvedCodeConstant | ||
| 282 | * @brief A decoded constant plus the instruction and matched byte span that supplied it. | ||
| 283 | * @details @ref instruction_span covers the whole decoded instruction, not just its first byte: every byte of | ||
| 284 | * it is evidence this constant depends on, so a co-voting selector that witnesses any of those bytes | ||
| 285 | * shares the failure domain and must not corroborate. | ||
| 286 | */ | ||
| 287 | struct ResolvedCodeConstant | ||
| 288 | { | ||
| 289 | std::int64_t value = 0; | ||
| 290 | Region instruction_span; | ||
| 291 | Region physical_source; | ||
| 292 | }; | ||
| 293 | |||
| 294 | /// Reads a code constant while retaining private physical-source provenance for anchor quorum checks. | ||
| 295 | [[nodiscard]] Result<ResolvedCodeConstant> | ||
| 296 | read_code_constant_with_provenance(const scan::CodeConstant &code_constant, Region scope); | ||
| 297 | |||
| 298 | /** | ||
| 299 | * @brief Resolves a string xref while retaining the referencing instruction's span for quorum checks. | ||
| 300 | * @param physical_source Receives the half-open span of the bytes the result rides on; empty on failure. | ||
| 301 | * @details The span covers the referencing instruction whole, and for @ref scan::XrefReturn::StringPointerSlot | ||
| 302 | * it reaches through the store the slot address was decoded from, since that store's own disp32 is | ||
| 303 | * what the mode returns. A co-voting selector that witnesses any of those bytes shares the failure | ||
| 304 | * domain and must not corroborate. | ||
| 305 | */ | ||
| 306 | [[nodiscard]] Result<Address> | ||
| 307 | find_string_xref_with_provenance(const scan::StringRefQuery &query, Region scope, Region &physical_source); | ||
| 308 | |||
| 309 | /** | ||
| 310 | * @brief Resolves a string xref while honouring an exclusion set the caller already assembled. | ||
| 311 | * @param query String literal and reference-selection facets. | ||
| 312 | * @param scope Address range to search. | ||
| 313 | * @param exclusions Combined DMK-owned and caller-declared query storage; null builds the set from @p query | ||
| 314 | * alone. | ||
| 315 | * @param declared_exclusions Caller-declared spans, used only to establish readable-scan authority. | ||
| 316 | * @param physical_source When non-null, receives the half-open span of the bytes the result rides on; empty on | ||
| 317 | * failure. See @ref find_string_xref_with_provenance for what that span covers. | ||
| 318 | * @return The resolved address, or a typed scan error. | ||
| 319 | * @details Exists so the ladder resolver can carry its own query storage into phase 1 instead of re-deriving | ||
| 320 | * it. Internal on purpose, so no consumer depends on DMK's exclusion representation. | ||
| 321 | */ | ||
| 322 | [[nodiscard]] Result<Address> find_string_xref_with_exclusions( | ||
| 323 | const scan::StringRefQuery &query, | ||
| 324 | Region scope, | ||
| 325 | const ScanExclusions *exclusions, | ||
| 326 | std::span<const Region> declared_exclusions, | ||
| 327 | Region *physical_source = nullptr | ||
| 328 | ); | ||
| 329 | |||
| 330 | // Direct-tier resolution: the resolved address is the match plus the signed walk-back. Screened through the | ||
| 331 | // plausible-userspace floor so a pathological walk-back that underflows to a near-null / kernel-range address | ||
| 332 | // is a miss, never a hit. | ||
| 333 | [[nodiscard]] inline std::optional<std::uintptr_t> | ||
| 334 | 464 | resolve_direct(std::uintptr_t match, const scan::DirectPattern &direct) noexcept | |
| 335 | { | ||
| 336 | 464 | const std::uintptr_t resolved = match + static_cast<std::uintptr_t>(direct.walk_back); | |
| 337 |
2/2✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 462 times.
|
464 | if (!is_plausible_ptr(resolved)) |
| 338 | { | ||
| 339 | 2 | return std::nullopt; | |
| 340 | } | ||
| 341 | 462 | return resolved; | |
| 342 | } | ||
| 343 | |||
| 344 | // Adds a sign-extended disp32 to the next-instruction address with defined modular arithmetic. | ||
| 345 | 51 | [[nodiscard]] inline constexpr std::uintptr_t add_rip_displacement( | |
| 346 | std::uintptr_t instruction, | ||
| 347 | std::size_t instruction_length, | ||
| 348 | std::int32_t displacement | ||
| 349 | ) noexcept | ||
| 350 | { | ||
| 351 | 51 | const std::uintptr_t displacement_offset = | |
| 352 | static_cast<std::uintptr_t>(static_cast<std::int64_t>(displacement)); | ||
| 353 | 51 | return instruction + static_cast<std::uintptr_t>(instruction_length) + displacement_offset; | |
| 354 | } | ||
| 355 | |||
| 356 | /** | ||
| 357 | * @brief Decodes one guarded instruction snapshot and returns its declared RIP-relative disp32. | ||
| 358 | * @param instruction Immutable bytes captured at the matched instruction during the page sweep. | ||
| 359 | * @param displacement_offset Declared byte offset of the disp32 field. | ||
| 360 | * @param instruction_length Declared total instruction length. | ||
| 361 | * @return The displacement when the decoded instruction has the declared length and disp32 location on a | ||
| 362 | * RIP-relative memory operand; otherwise std::nullopt. | ||
| 363 | * @details Defined in scan_rip_relative.cpp so Zydis stays confined to that TU. The displacement comes from the | ||
| 364 | * same guarded byte snapshot that is decoded, so a second live-memory read cannot diverge from it. | ||
| 365 | */ | ||
| 366 | [[nodiscard]] std::optional<std::int32_t> decode_rip_displacement( | ||
| 367 | std::span<const std::byte> instruction, | ||
| 368 | std::size_t displacement_offset, | ||
| 369 | std::size_t instruction_length | ||
| 370 | ) noexcept; | ||
| 371 | |||
| 372 | // RipRelative-tier resolution: decode-verify the immutable sweep snapshot, then compute next-IP plus its | ||
| 373 | // sign-extended disp32. A drifted layout, an absent snapshot, or an implausible target is a miss. | ||
| 374 | 24 | [[nodiscard]] inline std::optional<std::uintptr_t> resolve_rip_relative_candidate( | |
| 375 | std::uintptr_t match, | ||
| 376 | const scan::RipRelativePattern &rip, | ||
| 377 | std::span<const std::byte> instruction | ||
| 378 | ) noexcept | ||
| 379 | { | ||
| 380 | // A wildcarded pattern can byte-match an instruction whose opcode, addressing form, or length drifted from | ||
| 381 | // the declared layout; applying the declared displacement_at to that instruction would read a | ||
| 382 | // plausible-but-wrong disp32 and resolve a wrong target. Decode-verify gates the resolution so a drift is a | ||
| 383 | // miss, not a silently wrong hit. | ||
| 384 | const std::optional<std::int32_t> displacement = | ||
| 385 | 24 | decode_rip_displacement(instruction, rip.displacement_at, rip.instruction_length); | |
| 386 |
2/2✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 22 times.
|
24 | if (!displacement) |
| 387 | { | ||
| 388 | 2 | return std::nullopt; | |
| 389 | } | ||
| 390 | 22 | const std::uintptr_t resolved = add_rip_displacement(match, rip.instruction_length, *displacement); | |
| 391 |
1/2✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 22 times.
|
22 | if (!is_plausible_ptr(resolved)) |
| 392 | { | ||
| 393 | ✗ | return std::nullopt; | |
| 394 | } | ||
| 395 | 22 | return resolved; | |
| 396 | } | ||
| 397 | } // namespace detail | ||
| 398 | } // namespace DetourModKit | ||
| 399 | |||
| 400 | #endif // DETOURMODKIT_INTERNAL_SCAN_SHARED_HPP | ||
| 401 |