src/scan_string_xref.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /** | ||
| 2 | * @file scan_string_xref.cpp | ||
| 3 | * @brief String-reference (xref) anchor resolver: locate an immutable string literal in a module image, then resolve | ||
| 4 | * the unique instruction that references it. | ||
| 5 | * @details Two fail-closed phases. Phase 1 locates the single occurrence of the query string in the image's readable | ||
| 6 | * pages (the page-gated readable scan). Phase 2 finds the single RIP-relative reference to that string: a | ||
| 7 | * fast, desync-immune shape scan for the dominant lea/mov forms by default, plus Zydis-verified candidate | ||
| 8 | * discovery for opt-in broad matches and derived-return uniqueness confirmation. Both phases resolve through | ||
| 9 | * the private engine page primitives. Zydis is confined to this TU: no public header exposes a Zydis type. | ||
| 10 | */ | ||
| 11 | |||
| 12 | #include "DetourModKit/scan.hpp" | ||
| 13 | |||
| 14 | #include "internal/memory_fault.hpp" | ||
| 15 | #include "internal/memory_guarded.hpp" | ||
| 16 | #include "internal/scan_engine.hpp" | ||
| 17 | #include "internal/scan_exclusions.hpp" | ||
| 18 | #include "internal/scan_fault_seam.hpp" | ||
| 19 | #include "internal/scan_pages.hpp" | ||
| 20 | #include "internal/scan_shared.hpp" | ||
| 21 | |||
| 22 | #include "DetourModKit/logger.hpp" | ||
| 23 | #include "DetourModKit/memory.hpp" | ||
| 24 | |||
| 25 | #include <windows.h> | ||
| 26 | |||
| 27 | #include <Zydis/Zydis.h> | ||
| 28 | |||
| 29 | #include <array> | ||
| 30 | #include <cstddef> | ||
| 31 | #include <cstdint> | ||
| 32 | #include <cstring> | ||
| 33 | #include <optional> | ||
| 34 | #include <span> | ||
| 35 | #include <string> | ||
| 36 | #include <vector> | ||
| 37 | |||
| 38 | namespace DetourModKit | ||
| 39 | { | ||
| 40 | namespace scan | ||
| 41 | { | ||
| 42 | namespace | ||
| 43 | { | ||
| 44 | struct ReferenceScanResult | ||
| 45 | { | ||
| 46 | std::uintptr_t site = 0; | ||
| 47 | // Address of the reference's disp32 field. This, not the instruction start, is what identifies a | ||
| 48 | // reference across the two phases: the field address is a property of the reference itself and is | ||
| 49 | // identical in both, while the instruction start is a framing verdict the leaf/JIT probe may place | ||
| 50 | // earlier than the narrow shape scan does when a byte the decoder absorbs precedes the instruction. | ||
| 51 | std::uintptr_t key = 0; | ||
| 52 | // One past the last byte of the reference instruction @ref site frames. The whole instruction is the | ||
| 53 | // evidence a resolved value rides on, so a quorum member that witnesses any byte of it shares this | ||
| 54 | // reference's failure domain; publishing only the first byte would let a selector matching the rest of | ||
| 55 | // the same instruction abut the span instead of overlapping it. | ||
| 56 | std::uintptr_t site_end = 0; | ||
| 57 | std::size_t count = 0; | ||
| 58 | // True when any execute-readable window faulted mid-sweep and was skipped under the TOCTOU guard, so | ||
| 59 | // the reference count is only a lower bound: a second reference to the string could hide in the skipped | ||
| 60 | // window. A uniqueness verdict must then fail closed to ambiguous rather than report the lone surviving | ||
| 61 | // reference as the unique site. Accumulated across the narrow and broad merges. | ||
| 62 | bool incomplete = false; | ||
| 63 | }; | ||
| 64 | |||
| 65 | // The matched narrow reference's lea destination register and instruction length, recovered alongside the | ||
| 66 | // unique-site search so the store-xref forward scan needs no second decode of the reference. Valid only | ||
| 67 | // when the unique reference is a REX.W `lea reg, [rip+string]` (is_lea == true); a `mov reg, [rip+string]` | ||
| 68 | // load already delivered the value to a register, so there is no store to model from the lea. reg is the | ||
| 69 | // 4-bit x86-64 register number (REX.R << 3 | ModRM.reg). | ||
| 70 | struct LeaReferenceInfo | ||
| 71 | { | ||
| 72 | std::uint8_t reg = 0; | ||
| 73 | std::size_t instr_len = 0; | ||
| 74 | std::uintptr_t window_end = 0; | ||
| 75 | bool is_lea = false; | ||
| 76 | }; | ||
| 77 | |||
| 78 | 629 | void merge_reference_scan( | |
| 79 | ReferenceScanResult &result, | ||
| 80 | std::uintptr_t site, | ||
| 81 | std::uintptr_t site_end, | ||
| 82 | std::uintptr_t key, | ||
| 83 | std::size_t count, | ||
| 84 | bool incomplete | ||
| 85 | ) noexcept | ||
| 86 | { | ||
| 87 | // Incompleteness is monotonic: once either sweep skipped a faulted window, the merged count is a lower | ||
| 88 | // bound regardless of what the other sweep found. | ||
| 89 |
4/4✓ Branch 2 → 3 taken 577 times.
✓ Branch 2 → 4 taken 52 times.
✓ Branch 3 → 4 taken 55 times.
✓ Branch 3 → 5 taken 522 times.
|
629 | result.incomplete = result.incomplete || incomplete; |
| 90 |
2/2✓ Branch 6 → 7 taken 23 times.
✓ Branch 6 → 8 taken 606 times.
|
629 | if (count == 0) |
| 91 | { | ||
| 92 | 23 | return; | |
| 93 | } | ||
| 94 |
2/2✓ Branch 8 → 9 taken 7 times.
✓ Branch 8 → 10 taken 599 times.
|
606 | if (count >= 2) |
| 95 | { | ||
| 96 | 7 | result.count = 2; | |
| 97 | 7 | result.site = 0; | |
| 98 | 7 | result.site_end = 0; | |
| 99 | 7 | result.key = 0; | |
| 100 | 7 | return; | |
| 101 | } | ||
| 102 |
2/2✓ Branch 10 → 11 taken 320 times.
✓ Branch 10 → 12 taken 279 times.
|
599 | if (result.count == 0) |
| 103 | { | ||
| 104 | 320 | result.site = site; | |
| 105 | 320 | result.site_end = site_end; | |
| 106 | 320 | result.key = key; | |
| 107 | 320 | result.count = 1; | |
| 108 | 320 | return; | |
| 109 | } | ||
| 110 |
1/2✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 279 times.
|
279 | if (result.key != key) |
| 111 | { | ||
| 112 | ✗ | result.site = 0; | |
| 113 | ✗ | result.site_end = 0; | |
| 114 | ✗ | result.key = 0; | |
| 115 | ✗ | result.count = 2; | |
| 116 | } | ||
| 117 | } | ||
| 118 | |||
| 119 | // Why a query cannot be compiled, so the caller can report a precise error instead of "not found". | ||
| 120 | enum class QueryTextStatus : std::uint8_t | ||
| 121 | { | ||
| 122 | Ok, | ||
| 123 | Malformed | ||
| 124 | }; | ||
| 125 | |||
| 126 | // Builds a literal-byte EnginePattern from a string query plus an optional trailing NUL, emitted as a hex | ||
| 127 | // AOB and run through parse_aob so the string scan reuses the exact same compiled-pattern path as every | ||
| 128 | // other AOB. | ||
| 129 | // | ||
| 130 | // StringEncoding::Utf8 searches for query.text byte for byte, so a caller may anchor on any byte sequence. | ||
| 131 | // StringEncoding::Utf16le searches for the UTF-16LE encoding of that text, which requires it to be | ||
| 132 | // well-formed UTF-8: each code point is transcoded, and a supplementary one becomes a surrogate pair. | ||
| 133 | // Byte-wise widening is correct only for ASCII and would search for a different literal otherwise. | ||
| 134 | // | ||
| 135 | // An embedded NUL is rejected on both routes: it contradicts require_terminator, cannot appear in the C | ||
| 136 | // string literals these anchors name, and would otherwise make the compiled pattern's terminator ambiguous. | ||
| 137 | std::optional<detail::EnginePattern> | ||
| 138 | 730 | compile_string_pattern(const StringRefQuery &query, QueryTextStatus &status) | |
| 139 | { | ||
| 140 | 730 | status = QueryTextStatus::Ok; | |
| 141 |
2/2✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 5 taken 725 times.
|
730 | if (query.text.find('\0') != std::string_view::npos) |
| 142 | { | ||
| 143 | 4 | status = QueryTextStatus::Malformed; | |
| 144 | 4 | return std::nullopt; | |
| 145 | } | ||
| 146 | |||
| 147 | 725 | const bool wide = (query.encoding == StringEncoding::Utf16le); | |
| 148 | 725 | std::string aob; | |
| 149 |
3/4✓ Branch 7 → 8 taken 16 times.
✓ Branch 7 → 9 taken 709 times.
✓ Branch 10 → 11 taken 726 times.
✗ Branch 10 → 42 not taken.
|
726 | aob.reserve(query.text.size() * (wide ? 6 : 3) + 6); |
| 150 | 14473 | const auto emit = [&aob](std::uint8_t byte) | |
| 151 | { | ||
| 152 | static constexpr char hex_digits[] = "0123456789ABCDEF"; | ||
| 153 | 14473 | aob.push_back(hex_digits[byte >> 4]); | |
| 154 | 14473 | aob.push_back(hex_digits[byte & 0x0F]); | |
| 155 | 14472 | aob.push_back(' '); | |
| 156 | 15199 | }; | |
| 157 | 23 | const auto emit_utf16_unit = [&emit](char16_t unit) | |
| 158 | { | ||
| 159 | 23 | emit(static_cast<std::uint8_t>(unit & 0xFFU)); | |
| 160 | 23 | emit(static_cast<std::uint8_t>((unit >> 8) & 0xFFU)); | |
| 161 | 749 | }; | |
| 162 | |||
| 163 |
2/2✓ Branch 11 → 12 taken 710 times.
✓ Branch 11 → 17 taken 16 times.
|
726 | if (!wide) |
| 164 | { | ||
| 165 |
2/2✓ Branch 16 → 14 taken 13708 times.
✓ Branch 16 → 30 taken 710 times.
|
14418 | for (const char ch : query.text) |
| 166 | { | ||
| 167 |
1/2✓ Branch 14 → 15 taken 13708 times.
✗ Branch 14 → 42 not taken.
|
13708 | emit(static_cast<std::uint8_t>(ch)); |
| 168 | } | ||
| 169 | } | ||
| 170 | else | ||
| 171 | { | ||
| 172 | 16 | std::size_t pos = 0; | |
| 173 |
2/2✓ Branch 28 → 18 taken 33 times.
✓ Branch 28 → 29 taken 5 times.
|
38 | while (pos < query.text.size()) |
| 174 | { | ||
| 175 | 33 | char32_t code_point = 0; | |
| 176 |
2/2✓ Branch 19 → 20 taken 11 times.
✓ Branch 19 → 22 taken 22 times.
|
33 | if (!detail::decode_utf8(query.text, pos, code_point)) |
| 177 | { | ||
| 178 | 11 | status = QueryTextStatus::Malformed; | |
| 179 | 11 | return std::nullopt; | |
| 180 | } | ||
| 181 |
2/2✓ Branch 22 → 23 taken 21 times.
✓ Branch 22 → 24 taken 1 time.
|
22 | if (code_point < 0x10000) |
| 182 | { | ||
| 183 |
1/2✓ Branch 23 → 26 taken 21 times.
✗ Branch 23 → 40 not taken.
|
21 | emit_utf16_unit(static_cast<char16_t>(code_point)); |
| 184 | } | ||
| 185 | else | ||
| 186 | { | ||
| 187 | 1 | const char32_t offset = code_point - 0x10000; | |
| 188 |
1/2✓ Branch 24 → 25 taken 1 time.
✗ Branch 24 → 40 not taken.
|
1 | emit_utf16_unit(static_cast<char16_t>(0xD800 + (offset >> 10))); |
| 189 |
1/2✓ Branch 25 → 26 taken 1 time.
✗ Branch 25 → 40 not taken.
|
1 | emit_utf16_unit(static_cast<char16_t>(0xDC00 + (offset & 0x3FF))); |
| 190 | } | ||
| 191 | } | ||
| 192 | } | ||
| 193 | |||
| 194 |
2/2✓ Branch 30 → 31 taken 714 times.
✓ Branch 30 → 34 taken 1 time.
|
715 | if (query.require_terminator) |
| 195 | { | ||
| 196 |
1/2✓ Branch 31 → 32 taken 714 times.
✗ Branch 31 → 42 not taken.
|
714 | emit(0x00); |
| 197 |
2/2✓ Branch 32 → 33 taken 5 times.
✓ Branch 32 → 34 taken 709 times.
|
714 | if (wide) |
| 198 | { | ||
| 199 |
1/2✓ Branch 33 → 34 taken 5 times.
✗ Branch 33 → 42 not taken.
|
5 | emit(0x00); |
| 200 | } | ||
| 201 | } | ||
| 202 |
1/2✓ Branch 35 → 36 taken 714 times.
✗ Branch 35 → 42 not taken.
|
715 | return detail::parse_aob(aob); |
| 203 | 725 | } | |
| 204 | |||
| 205 | // Best-effort diagnosis for executable windows skipped because they faulted mid-scan. A module image is | ||
| 206 | // rarely decommitted under a live resolve, but collect_executable_windows only proves readability at gate | ||
| 207 | // time, so the window scans below guard their reads and a faulted window is skipped, not fatal. try_log is | ||
| 208 | // level-gated and no-throw. | ||
| 209 | 629 | void log_faulted_windows(std::size_t faulted_windows) noexcept | |
| 210 | { | ||
| 211 |
2/2✓ Branch 2 → 3 taken 523 times.
✓ Branch 2 → 4 taken 106 times.
|
629 | if (faulted_windows == 0) |
| 212 | { | ||
| 213 | 523 | return; | |
| 214 | } | ||
| 215 | 106 | (void)log().try_log( | |
| 216 | LogLevel::Debug, | ||
| 217 | "scan::find_string_xref: skipped {} executable window(s) that faulted mid-scan (concurrent " | ||
| 218 | "decommit/reprotect).", | ||
| 219 | faulted_windows | ||
| 220 | ); | ||
| 221 | } | ||
| 222 | |||
| 223 | // Inner narrow scan of one already-gated executable window (no fault guard). Mutates found_count / | ||
| 224 | // first_site, returning once a second referencing site is seen (found_count == 2) so the caller fails | ||
| 225 | // closed on ambiguity. The recognized instruction shape is documented on scan_string_ref_narrow. | ||
| 226 | 408 | void scan_window_narrow_body( | |
| 227 | const detail::ExecutableWindow &window, | ||
| 228 | std::uintptr_t string_addr, | ||
| 229 | std::size_t instr_len, | ||
| 230 | std::size_t &found_count, | ||
| 231 | std::uintptr_t &first_site, | ||
| 232 | LeaReferenceInfo *info | ||
| 233 | ) noexcept | ||
| 234 | { | ||
| 235 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 236 | // Inside the guard's frame, so an armed address the gated window does not cover proves the guard | ||
| 237 | // screens the faulting address rather than only the exception class. | ||
| 238 | 408 | detail::fire_scan_fault_seam_for_test( | |
| 239 | detail::g_scan_window_fault_for_test, | ||
| 240 | &detail::g_scan_window_fault_preparation_for_test | ||
| 241 | ); | ||
| 242 | #endif | ||
| 243 | 407 | const auto *bytes = reinterpret_cast<const std::uint8_t *>(window.base); | |
| 244 |
2/2✓ Branch 19 → 4 taken 1563676 times.
✓ Branch 19 → 20 taken 403 times.
|
1564079 | for (std::size_t i = 0; i + instr_len <= window.span; ++i) |
| 245 | { | ||
| 246 | 1563676 | const std::uint8_t rex = bytes[i]; | |
| 247 | 1563676 | const std::uint8_t opcode = bytes[i + 1]; | |
| 248 | 1563676 | const std::uint8_t modrm = bytes[i + 2]; | |
| 249 |
10/10✓ Branch 4 → 5 taken 1026916 times.
✓ Branch 4 → 9 taken 536760 times.
✓ Branch 5 → 6 taken 408 times.
✓ Branch 5 → 9 taken 1026508 times.
✓ Branch 6 → 7 taken 90 times.
✓ Branch 6 → 8 taken 318 times.
✓ Branch 7 → 8 taken 16 times.
✓ Branch 7 → 9 taken 74 times.
✓ Branch 8 → 9 taken 13 times.
✓ Branch 8 → 10 taken 321 times.
|
1563676 | if (rex < 0x48 || rex > 0x4F || (opcode != 0x8D && opcode != 0x8B) || (modrm & 0xC7) != 0x05) |
| 250 | { | ||
| 251 | 1563356 | continue; | |
| 252 | } | ||
| 253 | 321 | std::int32_t disp = 0; | |
| 254 | 321 | std::memcpy(&disp, &bytes[i + 3], sizeof(disp)); | |
| 255 | 321 | const std::uintptr_t instr_addr = window.base + i; | |
| 256 | 321 | const std::uintptr_t target = | |
| 257 | 321 | instr_addr + instr_len + static_cast<std::uintptr_t>(static_cast<std::int64_t>(disp)); | |
| 258 |
2/2✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 320 times.
|
321 | if (target != string_addr) |
| 259 | { | ||
| 260 | 1 | continue; | |
| 261 | } | ||
| 262 | 320 | ++found_count; | |
| 263 |
2/2✓ Branch 12 → 13 taken 316 times.
✓ Branch 12 → 15 taken 4 times.
|
320 | if (found_count == 1) |
| 264 | { | ||
| 265 | 316 | first_site = instr_addr; | |
| 266 |
1/2✓ Branch 13 → 14 taken 316 times.
✗ Branch 13 → 16 not taken.
|
316 | if (info != nullptr) |
| 267 | { | ||
| 268 | // REX.R (bit 2 of the REX byte) is the high bit of the ModRM.reg field; the narrow shape | ||
| 269 | // accepts REX in 0x48..0x4F, so REX.R may be set for an r8..r15 destination. opcode 0x8D is | ||
| 270 | // lea (a load whose pointer a following store can cache); 0x8B is a mov load with no such | ||
| 271 | // store to model. | ||
| 272 | 316 | const std::uint8_t reg = | |
| 273 | 316 | static_cast<std::uint8_t>(((rex & 0x04) << 1) | ((modrm >> 3) & 0x07)); | |
| 274 | 316 | *info = LeaReferenceInfo{reg, instr_len, window.base + window.span, opcode == 0x8D}; | |
| 275 | } | ||
| 276 | } | ||
| 277 | else | ||
| 278 | { | ||
| 279 | // Ambiguous; caller maps found_count >= 2 to AmbiguousReference. | ||
| 280 | 4 | return; | |
| 281 | } | ||
| 282 | } | ||
| 283 | } | ||
| 284 | |||
| 285 | // Window-granular TOCTOU fault guard around scan_window_narrow_body. collect_executable_windows gated each | ||
| 286 | // window with one VirtualQuery; a concurrent decommit / reprotect before these unguarded byte reads | ||
| 287 | // complete would otherwise fault the host. Both arms claim a fault only inside the gated window bytes, | ||
| 288 | // [window.base, window.base + window.span): on MSVC through detail::guarded_range_fault_filter, on MinGW | ||
| 289 | // x64 through the process-wide vectored read guard the guarded_read paths use (detail::run_guarded_region), | ||
| 290 | // armed over the same span. A fault outside it did not come from this window's reads, so it reaches the | ||
| 291 | // host's handlers rather than being recorded as a faulted window, exactly as the broad sibling below | ||
| 292 | // already does. A 32-bit build is rejected by the defines.hpp architecture gate, so only the two x64 arms | ||
| 293 | // exist. Returns true when a fault was swallowed. | ||
| 294 | 408 | bool scan_window_narrow_guarded( | |
| 295 | const detail::ExecutableWindow &window, | ||
| 296 | std::uintptr_t string_addr, | ||
| 297 | std::size_t instr_len, | ||
| 298 | std::size_t &found_count, | ||
| 299 | std::uintptr_t &first_site, | ||
| 300 | LeaReferenceInfo *info | ||
| 301 | ) noexcept | ||
| 302 | { | ||
| 303 | #ifdef _MSC_VER | ||
| 304 | const std::size_t original_found_count = found_count; | ||
| 305 | const std::uintptr_t original_first_site = first_site; | ||
| 306 | const LeaReferenceInfo original_info = (info != nullptr) ? *info : LeaReferenceInfo{}; | ||
| 307 | __try | ||
| 308 | { | ||
| 309 | scan_window_narrow_body(window, string_addr, instr_len, found_count, first_site, info); | ||
| 310 | return false; | ||
| 311 | } | ||
| 312 | __except ( | ||
| 313 | detail::guarded_range_fault_filter( | ||
| 314 | GetExceptionInformation(), | ||
| 315 | window.base, | ||
| 316 | window.base + window.span | ||
| 317 | ) | ||
| 318 | ) | ||
| 319 | { | ||
| 320 | // The caller skips faulted windows, so discard any reference count (and recovered lea info) | ||
| 321 | // collected before the fault, or a partially-scanned window could leak a stale site/register. | ||
| 322 | found_count = original_found_count; | ||
| 323 | first_site = original_first_site; | ||
| 324 | if (info != nullptr) | ||
| 325 | { | ||
| 326 | *info = original_info; | ||
| 327 | } | ||
| 328 | return true; | ||
| 329 | } | ||
| 330 | #elif defined(_WIN64) | ||
| 331 | // MinGW x64: arm the process-wide vectored read guard over exactly the bytes the window gate proved | ||
| 332 | // readable, so a concurrent decommit / reprotect that faults the scan is swallowed and the window | ||
| 333 | // reported faulted. That is the same skip-the-window contract that the MSVC __except arm follows. | ||
| 334 | 408 | const std::size_t original_found_count = found_count; | |
| 335 | 408 | const std::uintptr_t original_first_site = first_site; | |
| 336 |
1/2✓ Branch 2 → 3 taken 408 times.
✗ Branch 2 → 4 not taken.
|
408 | const LeaReferenceInfo original_info = (info != nullptr) ? *info : LeaReferenceInfo{}; |
| 337 | struct NarrowScanContext | ||
| 338 | { | ||
| 339 | const detail::ExecutableWindow *window; | ||
| 340 | std::uintptr_t string_addr; | ||
| 341 | std::size_t instr_len; | ||
| 342 | std::size_t *found_count; | ||
| 343 | std::uintptr_t *first_site; | ||
| 344 | LeaReferenceInfo *info; | ||
| 345 | 408 | } scan_ctx{&window, string_addr, instr_len, &found_count, &first_site, info}; | |
| 346 | |||
| 347 | 408 | const auto run_scan = [](void *opaque) noexcept -> void | |
| 348 | { | ||
| 349 | 408 | auto *context = static_cast<NarrowScanContext *>(opaque); | |
| 350 | 408 | scan_window_narrow_body( | |
| 351 | 408 | *context->window, | |
| 352 | context->string_addr, | ||
| 353 | context->instr_len, | ||
| 354 | 408 | *context->found_count, | |
| 355 | 408 | *context->first_site, | |
| 356 | context->info | ||
| 357 | ); | ||
| 358 | 355 | }; | |
| 359 | |||
| 360 |
2/2✓ Branch 7 → 8 taken 355 times.
✓ Branch 7 → 9 taken 53 times.
|
408 | if (detail::run_guarded_region(window.base, window.base + window.span, run_scan, &scan_ctx)) |
| 361 | { | ||
| 362 | 355 | return false; | |
| 363 | } | ||
| 364 | // Faulted: discard any partial count / site / lea info so a partially-scanned window cannot leak a | ||
| 365 | // stale site or register, exactly as the MSVC arm does. | ||
| 366 | 53 | found_count = original_found_count; | |
| 367 | 53 | first_site = original_first_site; | |
| 368 |
1/2✓ Branch 9 → 10 taken 53 times.
✗ Branch 9 → 11 not taken.
|
53 | if (info != nullptr) |
| 369 | { | ||
| 370 | 53 | *info = original_info; | |
| 371 | } | ||
| 372 | 53 | return true; | |
| 373 | #endif | ||
| 374 | } | ||
| 375 | |||
| 376 | // The narrow phase sweeps execute-readable windows for dominant REX.W RIP-relative loads that resolve to | ||
| 377 | // string_addr. | ||
| 378 | // | ||
| 379 | // The accepted seven-byte shape uses REX.W LEA/MOV with a RIP-relative ModRM byte and disp32. The disp32 | ||
| 380 | // starts at offset 3. Exact target equality accepts a reference. The second hit marks ambiguity. `[B-63]` | ||
| 381 | // owns shapes outside this tier. | ||
| 382 | 335 | std::uintptr_t scan_string_ref_narrow( | |
| 383 | std::uintptr_t string_addr, | ||
| 384 | std::span<const detail::ExecutableWindow> windows, | ||
| 385 | std::size_t &found_count, | ||
| 386 | LeaReferenceInfo &info, | ||
| 387 | bool &incomplete | ||
| 388 | ) | ||
| 389 | { | ||
| 390 | 335 | found_count = 0; | |
| 391 | 335 | incomplete = false; | |
| 392 | 335 | std::uintptr_t first_site = 0; | |
| 393 | 335 | info = LeaReferenceInfo{}; | |
| 394 | // REX.W + opcode + ModRM + disp32. | ||
| 395 | 335 | constexpr std::size_t instr_len = 7; | |
| 396 | // scan_window_narrow_body reads bytes[i], bytes[i+1], bytes[i+2] and a disp32 at bytes[i+3..i+6], so | ||
| 397 | // the highest index it touches is i+6. The per-window loop only bounds i + instr_len <= span, so | ||
| 398 | // instr_len must cover that widest read or the disp32 fetch could run up to four bytes past the window. | ||
| 399 | // Pin the coupling here, beside the shape's byte count, so a future instr_len change cannot silently | ||
| 400 | // reopen it. | ||
| 401 | 335 | constexpr std::size_t narrow_max_read_index = 6; | |
| 402 | static_assert( | ||
| 403 | narrow_max_read_index < instr_len, | ||
| 404 | "instr_len must span scan_window_narrow_body's disp32 tail read at bytes[i+3..i+6]" | ||
| 405 | ); | ||
| 406 | 335 | std::size_t faulted_windows = 0; | |
| 407 | |||
| 408 | // Adjacent execute windows require back-carry from the prior window. Without it, a split instruction | ||
| 409 | // fits neither window and can create false uniqueness. When windows abut, the next scan includes at | ||
| 410 | // most instr_len - 1 prior bytes. The earlier window collection applied the readability gate to those | ||
| 411 | // bytes. A carried start never fits the prior sweep, so no duplicate count arises. The | ||
| 412 | // StringXrefTest.NarrowReferenceStraddlingProtectionSplitIsFound and | ||
| 413 | // NarrowTwoReferencesOneStraddlingSplitAreAmbiguous cases pin both outcomes. | ||
| 414 | 335 | std::uintptr_t prev_end = 0; | |
| 415 | 335 | std::size_t prev_span = 0; | |
| 416 | 335 | bool have_prev = false; | |
| 417 |
2/2✓ Branch 28 → 4 taken 407 times.
✓ Branch 28 → 29 taken 331 times.
|
1073 | for (const detail::ExecutableWindow &window : windows) |
| 418 | { | ||
| 419 | 407 | detail::ExecutableWindow effective = window; | |
| 420 |
4/4✓ Branch 6 → 7 taken 73 times.
✓ Branch 6 → 9 taken 334 times.
✓ Branch 7 → 8 taken 4 times.
✓ Branch 7 → 9 taken 69 times.
|
407 | if (have_prev && window.base == prev_end) |
| 421 | { | ||
| 422 | 4 | const std::size_t carry = (instr_len - 1 < prev_span) ? instr_len - 1 : prev_span; | |
| 423 | 4 | effective.base = window.base - carry; | |
| 424 | 4 | effective.span = window.span + carry; | |
| 425 | } | ||
| 426 | 407 | prev_end = window.base + window.span; | |
| 427 | 407 | prev_span = window.span; | |
| 428 | 407 | have_prev = true; | |
| 429 | |||
| 430 |
1/2✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 407 times.
|
407 | if (effective.span < instr_len) |
| 431 | { | ||
| 432 | 53 | continue; | |
| 433 | } | ||
| 434 |
2/2✓ Branch 12 → 13 taken 53 times.
✓ Branch 12 → 14 taken 355 times.
|
407 | if (scan_window_narrow_guarded(effective, string_addr, instr_len, found_count, first_site, &info)) |
| 435 | { | ||
| 436 | 53 | ++faulted_windows; | |
| 437 | 53 | continue; | |
| 438 | } | ||
| 439 |
2/2✓ Branch 14 → 15 taken 4 times.
✓ Branch 14 → 17 taken 351 times.
|
355 | if (found_count >= 2) |
| 440 | { | ||
| 441 | // Ambiguous; caller maps found_count >= 2 to AmbiguousReference. | ||
| 442 | 4 | incomplete = faulted_windows > 0; | |
| 443 | 4 | log_faulted_windows(faulted_windows); | |
| 444 | 4 | return 0; | |
| 445 | } | ||
| 446 | } | ||
| 447 | // A skipped faulted window makes the count a lower bound: surface it so a lone surviving reference is | ||
| 448 | // not committed as unique. The caller fails closed on the truncation itself. | ||
| 449 | 331 | incomplete = faulted_windows > 0; | |
| 450 | 331 | log_faulted_windows(faulted_windows); | |
| 451 |
2/2✓ Branch 30 → 31 taken 312 times.
✓ Branch 30 → 32 taken 19 times.
|
331 | return (found_count == 1) ? first_site : 0; |
| 452 | } | ||
| 453 | |||
| 454 | // `[B-76]` defines the terminal predicate for forward value-attribution scans. RET, unconditional branches, | ||
| 455 | // and the SYSRET category stop by category. INT3, UD2, and UIRET stop by mnemonic because Zydis places them | ||
| 456 | // elsewhere. None has a fall-through path. A conditional branch keeps its fall-through path. The | ||
| 457 | // StringXrefTest.StringPointerSlotStopsAt* and StringPointerSlotContinuesPastConditionalBranch cases pin | ||
| 458 | // these choices. | ||
| 459 | 270 | [[nodiscard]] bool is_non_fall_through_terminal(const ZydisDecodedInstruction &insn) noexcept | |
| 460 | { | ||
| 461 |
2/2✓ Branch 3 → 4 taken 268 times.
✓ Branch 3 → 8 taken 1 time.
|
269 | return insn.meta.category == ZYDIS_CATEGORY_RET || insn.meta.category == ZYDIS_CATEGORY_UNCOND_BR || |
| 462 |
4/4✓ Branch 4 → 5 taken 266 times.
✓ Branch 4 → 8 taken 2 times.
✓ Branch 5 → 6 taken 265 times.
✓ Branch 5 → 8 taken 1 time.
|
268 | insn.meta.category == ZYDIS_CATEGORY_SYSRET || insn.mnemonic == ZYDIS_MNEMONIC_INT3 || |
| 463 |
6/6✓ Branch 2 → 3 taken 269 times.
✓ Branch 2 → 8 taken 1 time.
✓ Branch 6 → 7 taken 264 times.
✓ Branch 6 → 8 taken 1 time.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 263 times.
|
539 | insn.mnemonic == ZYDIS_MNEMONIC_UD2 || insn.mnemonic == ZYDIS_MNEMONIC_UIRET; |
| 464 | } | ||
| 465 | |||
| 466 | // The store-slot scan decodes forward after `lea reg, [rip+string]`. | ||
| 467 | // It accepts the first `REX.W MOV [rip+disp32], reg64` store within the caller's tight window. | ||
| 468 | // | ||
| 469 | // store_end marks one past the accepted store. The complete decoded span forms the evidence for selector | ||
| 470 | // failure-domain overlap. | ||
| 471 | 17 | std::uintptr_t scan_store_slot_after_lea( | |
| 472 | std::uintptr_t lea_site, | ||
| 473 | std::size_t lea_len, | ||
| 474 | std::uintptr_t window_end, | ||
| 475 | std::uint8_t lea_reg, | ||
| 476 | detail::ModuleSpan range, | ||
| 477 | std::uintptr_t &store_end | ||
| 478 | ) noexcept | ||
| 479 | { | ||
| 480 | 17 | store_end = 0; | |
| 481 | // A cached pointer is stored very close to its load; bound the forward scan so a pathological region | ||
| 482 | // cannot scan unboundedly and the store cannot be attributed to a distant, unrelated reuse of the same | ||
| 483 | // register. | ||
| 484 | 17 | constexpr std::size_t forward_window = 0x80; // 128 bytes. | |
| 485 | 17 | const std::uintptr_t scan_lo = lea_site + lea_len; | |
| 486 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 17 times.
|
17 | if (scan_lo < lea_site) |
| 487 | { | ||
| 488 | ✗ | return 0; | |
| 489 | } | ||
| 490 |
2/2✓ Branch 4 → 5 taken 16 times.
✓ Branch 4 → 6 taken 1 time.
|
17 | const std::uintptr_t scan_end = (window_end < range.end) ? window_end : range.end; |
| 491 |
1/2✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 17 times.
|
17 | if (scan_lo >= scan_end) |
| 492 | { | ||
| 493 | ✗ | return 0; | |
| 494 | } | ||
| 495 | 17 | const std::uintptr_t scan_hi = | |
| 496 |
2/2✓ Branch 9 → 10 taken 16 times.
✓ Branch 9 → 11 taken 1 time.
|
17 | (scan_end - scan_lo < forward_window) ? scan_end : scan_lo + forward_window; |
| 497 | |||
| 498 | ZydisDecoder decoder; | ||
| 499 |
1/2✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 17 times.
|
17 | if (!ZYAN_SUCCESS(ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64))) |
| 500 | { | ||
| 501 | ✗ | return 0; | |
| 502 | } | ||
| 503 | // The lea wrote a 64-bit pointer, so the store source and any clobber are tested against the full | ||
| 504 | // 64-bit register. ZydisRegisterEncode maps the x86 register number (REX.R << 3 | ModRM.reg) to the | ||
| 505 | // GPR64 register. | ||
| 506 | 17 | const ZydisRegister target_reg = ZydisRegisterEncode(ZYDIS_REGCLASS_GPR64, lea_reg); | |
| 507 |
1/2✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 17 times.
|
17 | if (target_reg == ZYDIS_REGISTER_NONE) |
| 508 | { | ||
| 509 | ✗ | return 0; | |
| 510 | } | ||
| 511 | |||
| 512 | 17 | std::uintptr_t p = scan_lo; | |
| 513 |
2/2✓ Branch 60 → 19 taken 276 times.
✓ Branch 60 → 61 taken 3 times.
|
279 | while (p < scan_hi) |
| 514 | { | ||
| 515 | 276 | std::array<std::uint8_t, ZYDIS_MAX_INSTRUCTION_LENGTH> code{}; | |
| 516 | 276 | const std::size_t avail = | |
| 517 | 276 | (scan_hi - p < code.size()) ? static_cast<std::size_t>(scan_hi - p) : code.size(); | |
| 518 |
1/2✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 276 times.
|
276 | if (!detail::guarded_read_bytes(p, code.data(), avail)) |
| 519 | { | ||
| 520 | // Unreadable byte: the store sits in the same execute-readable window as the lea, so a fault | ||
| 521 | // here means it is not present in mapped code. | ||
| 522 | 14 | return 0; | |
| 523 | } | ||
| 524 | |||
| 525 | ZydisDecodedInstruction insn; | ||
| 526 | ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT]; | ||
| 527 |
2/2✓ Branch 29 → 30 taken 2 times.
✓ Branch 29 → 31 taken 274 times.
|
276 | if (!ZYAN_SUCCESS(ZydisDecoderDecodeFull(&decoder, code.data(), avail, &insn, operands))) |
| 528 | { | ||
| 529 | // The store must be instruction-aligned with the lea; a decode failure means alignment was lost | ||
| 530 | // or the bytes are not code. Fail closed rather than accept a misaligned match. | ||
| 531 | 2 | return 0; | |
| 532 | } | ||
| 533 | |||
| 534 | // The store: MOV with a RIP-relative memory destination (operand 0) and the matching 64-bit source | ||
| 535 | // register (operand 1). ZydisCalcAbsoluteAddress turns the destination into the slot's effective | ||
| 536 | // address (next-instruction address + sign-extended disp32). | ||
| 537 |
3/4✓ Branch 31 → 32 taken 7 times.
✓ Branch 31 → 42 taken 267 times.
✓ Branch 32 → 33 taken 7 times.
✗ Branch 32 → 42 not taken.
|
274 | if (insn.mnemonic == ZYDIS_MNEMONIC_MOV && insn.operand_count_visible >= 2 && |
| 538 |
3/4✓ Branch 33 → 34 taken 5 times.
✓ Branch 33 → 42 taken 2 times.
✓ Branch 34 → 35 taken 5 times.
✗ Branch 34 → 42 not taken.
|
7 | operands[0].type == ZYDIS_OPERAND_TYPE_MEMORY && operands[0].mem.base == ZYDIS_REGISTER_RIP && |
| 539 |
3/4✓ Branch 35 → 36 taken 5 times.
✗ Branch 35 → 42 not taken.
✓ Branch 36 → 37 taken 4 times.
✓ Branch 36 → 42 taken 1 time.
|
5 | operands[1].type == ZYDIS_OPERAND_TYPE_REGISTER && operands[1].reg.value == target_reg) |
| 540 | { | ||
| 541 | 4 | ZyanU64 absolute = 0; | |
| 542 |
1/2✓ Branch 38 → 39 taken 4 times.
✗ Branch 38 → 40 not taken.
|
4 | if (ZYAN_SUCCESS( |
| 543 | ZydisCalcAbsoluteAddress(&insn, &operands[0], static_cast<ZyanU64>(p), &absolute) | ||
| 544 | )) | ||
| 545 | { | ||
| 546 | 4 | store_end = p + insn.length; | |
| 547 | 4 | return static_cast<std::uintptr_t>(absolute); | |
| 548 | } | ||
| 549 | ✗ | return 0; | |
| 550 | } | ||
| 551 | |||
| 552 | // A CALL clobbers the caller-saved registers; a store after it cannot be trusted to cache this | ||
| 553 | // lea's pointer, so stop conservatively rather than attribute a post-call store to the wrong value. | ||
| 554 |
1/2✗ Branch 42 → 43 not taken.
✓ Branch 42 → 44 taken 270 times.
|
270 | if (insn.meta.category == ZYDIS_CATEGORY_CALL) |
| 555 | { | ||
| 556 | ✗ | return 0; | |
| 557 | } | ||
| 558 | // A store past a non-fall-through terminal belongs to a different function or an unreachable | ||
| 559 | // path, so fail closed rather than attribute it to this lea. | ||
| 560 |
2/2✓ Branch 45 → 46 taken 7 times.
✓ Branch 45 → 47 taken 263 times.
|
270 | if (is_non_fall_through_terminal(insn)) |
| 561 | { | ||
| 562 | 7 | return 0; | |
| 563 | } | ||
| 564 | // Any write to the loaded register (at any width: a 32-bit write zeroes the upper half) means a | ||
| 565 | // later store would cache a different value. Check every operand, including implicit ones. | ||
| 566 |
2/2✓ Branch 57 → 48 taken 785 times.
✓ Branch 57 → 58 taken 262 times.
|
1047 | for (std::size_t op = 0; op < insn.operand_count; ++op) |
| 567 | { | ||
| 568 | 785 | const ZydisDecodedOperand &operand = operands[op]; | |
| 569 | 2093 | if (operand.type == ZYDIS_OPERAND_TYPE_REGISTER && | |
| 570 |
6/6✓ Branch 48 → 49 taken 523 times.
✓ Branch 48 → 53 taken 262 times.
✓ Branch 49 → 50 taken 262 times.
✓ Branch 49 → 53 taken 261 times.
✓ Branch 54 → 55 taken 1 time.
✓ Branch 54 → 56 taken 784 times.
|
1047 | (operand.actions & ZYDIS_OPERAND_ACTION_MASK_WRITE) != 0 && |
| 571 |
2/2✓ Branch 51 → 52 taken 1 time.
✓ Branch 51 → 53 taken 261 times.
|
262 | ZydisRegisterGetLargestEnclosing(ZYDIS_MACHINE_MODE_LONG_64, operand.reg.value) == |
| 572 | target_reg) | ||
| 573 | { | ||
| 574 | 1 | return 0; | |
| 575 | } | ||
| 576 | } | ||
| 577 | |||
| 578 | 262 | p += insn.length; | |
| 579 | } | ||
| 580 | 3 | return 0; | |
| 581 | } | ||
| 582 | |||
| 583 | // Most bytes an x86-64 instruction can place before its disp32 field: up to four legacy prefixes, a REX | ||
| 584 | // byte, a three-byte opcode escape, and the ModRM byte the displacement directly follows. Bounding the | ||
| 585 | // leaf/JIT backward probe by this is what keeps candidate verification constant-cost. | ||
| 586 | constexpr std::size_t MAX_BYTES_BEFORE_DISP32 = 9; | ||
| 587 | |||
| 588 | // Instructions decoded while walking a trusted function stream forward to reach one candidate. A real | ||
| 589 | // function cannot exceed this before the walk is worth abandoning, and the bound stops a corrupt or hostile | ||
| 590 | // .pdata record from turning one candidate into an unbounded decode. | ||
| 591 | constexpr std::size_t MAX_TRUSTED_STREAM_STEPS = 8192; | ||
| 592 | |||
| 593 | // True when @p insn has a visible memory operand based on RIP whose absolute target is @p string_addr, and | ||
| 594 | // that operand's displacement field sits exactly at @p disp_field. Visible operands are ordered first in | ||
| 595 | // the array, so iterating the visible count covers every explicit operand a disassembler would show. The | ||
| 596 | // displacement-position check is what makes this a verification of a specific candidate rather than a | ||
| 597 | // second, independent search: a decode that happens to reference the string through some other field does | ||
| 598 | // not confirm the framing under test. | ||
| 599 | 3142 | bool instruction_references_at( | |
| 600 | const ZydisDecodedInstruction &insn, | ||
| 601 | const ZydisDecodedOperand *operands, | ||
| 602 | std::uintptr_t instr_addr, | ||
| 603 | std::uintptr_t disp_field, | ||
| 604 | std::uintptr_t string_addr | ||
| 605 | ) noexcept | ||
| 606 | { | ||
| 607 |
4/4✓ Branch 2 → 3 taken 308 times.
✓ Branch 2 → 4 taken 2834 times.
✓ Branch 3 → 4 taken 14 times.
✓ Branch 3 → 5 taken 294 times.
|
3142 | if (insn.raw.disp.size != 32 || instr_addr + insn.raw.disp.offset != disp_field) |
| 608 | { | ||
| 609 | 2848 | return false; | |
| 610 | } | ||
| 611 |
1/2✓ Branch 18 → 6 taken 580 times.
✗ Branch 18 → 19 not taken.
|
580 | for (std::size_t op = 0; op < insn.operand_count_visible; ++op) |
| 612 | { | ||
| 613 | 580 | const ZydisDecodedOperand &operand = operands[op]; | |
| 614 |
3/4✓ Branch 6 → 7 taken 294 times.
✓ Branch 6 → 8 taken 286 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 294 times.
|
580 | if (operand.type != ZYDIS_OPERAND_TYPE_MEMORY || operand.mem.base != ZYDIS_REGISTER_RIP) |
| 615 | { | ||
| 616 | 286 | continue; | |
| 617 | } | ||
| 618 | 294 | ZyanU64 absolute = 0; | |
| 619 | 294 | if (ZYAN_SUCCESS( | |
| 620 | ZydisCalcAbsoluteAddress(&insn, &operand, static_cast<ZyanU64>(instr_addr), &absolute) | ||
| 621 |
2/4✓ Branch 10 → 11 taken 294 times.
✗ Branch 10 → 13 not taken.
✓ Branch 14 → 15 taken 294 times.
✗ Branch 14 → 16 not taken.
|
588 | ) && |
| 622 |
1/2✓ Branch 11 → 12 taken 294 times.
✗ Branch 11 → 13 not taken.
|
294 | static_cast<std::uintptr_t>(absolute) == string_addr) |
| 623 | { | ||
| 624 | 294 | return true; | |
| 625 | } | ||
| 626 | } | ||
| 627 | ✗ | return false; | |
| 628 | } | ||
| 629 | |||
| 630 | // Resolves the candidate at @p disp_field against a decode stream that starts at a trusted boundary. The | ||
| 631 | // innermost .pdata RUNTIME_FUNCTION's BeginAddress is a real instruction boundary by construction, so | ||
| 632 | // decoding forward from it reaches @p disp_field synchronized with the compiler's own framing. | ||
| 633 | // | ||
| 634 | // Three outcomes, and the difference between the last two is load-bearing. A nonzero value is the | ||
| 635 | // referencing instruction's address. Zero means the stream REACHED the candidate and rejected it, so | ||
| 636 | // probing a shorter inner framing would contradict a boundary the compiler itself declared. No value means | ||
| 637 | // the stream produced no verdict at all: no record covers the candidate, the record is unreadable, the | ||
| 638 | // function begins outside the bytes this window proved readable, or the decode broke or ran out of budget | ||
| 639 | // before arriving. Treating no verdict as a rejection is what would let one undecodable byte (an embedded | ||
| 640 | // jump table is the common case) suppress every reference after it in the same function, which is the | ||
| 641 | // failure mode this whole discovery path exists to prevent, so those cases fall through to the probe. | ||
| 642 | 436 | std::optional<std::uintptr_t> resolve_candidate_from_trusted_origin( | |
| 643 | const ZydisDecoder &decoder, | ||
| 644 | const detail::ExecutableWindow &window, | ||
| 645 | std::uintptr_t disp_field, | ||
| 646 | std::uintptr_t string_addr | ||
| 647 | ) noexcept | ||
| 648 | { | ||
| 649 | 436 | DWORD64 image_base = 0; | |
| 650 | 436 | const PRUNTIME_FUNCTION entry = RtlLookupFunctionEntry(disp_field, &image_base, nullptr); | |
| 651 |
3/4✓ Branch 3 → 4 taken 27 times.
✓ Branch 3 → 5 taken 409 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 27 times.
|
436 | if (entry == nullptr || image_base == 0) |
| 652 | { | ||
| 653 | 409 | return std::nullopt; | |
| 654 | } | ||
| 655 | 27 | RUNTIME_FUNCTION record{}; | |
| 656 |
1/2✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 27 times.
|
27 | if (!detail::guarded_read_bytes(reinterpret_cast<std::uintptr_t>(entry), &record, sizeof(record))) |
| 657 | { | ||
| 658 | // The record exists but could not be read, so it adjudicates nothing. | ||
| 659 | ✗ | return std::nullopt; | |
| 660 | } | ||
| 661 | 27 | const std::uintptr_t origin = static_cast<std::uintptr_t>(image_base) + record.BeginAddress; | |
| 662 |
2/4✓ Branch 9 → 10 taken 27 times.
✗ Branch 9 → 11 not taken.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 27 times.
|
27 | if (origin < window.base || origin > disp_field) |
| 663 | { | ||
| 664 | // Decoding must start inside bytes this window already proved readable, and a record that does not | ||
| 665 | // actually precede the candidate cannot describe its stream. A function whose entry lies in an | ||
| 666 | // earlier window is ordinary, not suspicious, so this yields no verdict rather than a rejection. | ||
| 667 | ✗ | return std::nullopt; | |
| 668 | } | ||
| 669 | |||
| 670 | 27 | const auto *bytes = reinterpret_cast<const std::uint8_t *>(window.base); | |
| 671 | 27 | std::uintptr_t cursor = origin; | |
| 672 |
1/2✓ Branch 27 → 13 taken 53567 times.
✗ Branch 27 → 28 not taken.
|
53567 | for (std::size_t step = 0; step < MAX_TRUSTED_STREAM_STEPS; ++step) |
| 673 | { | ||
| 674 |
1/2✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 53567 times.
|
53567 | if (cursor >= window.base + window.span) |
| 675 | { | ||
| 676 | // The stream left the readable window before arriving, so it never saw the candidate. | ||
| 677 | 27 | return std::nullopt; | |
| 678 | } | ||
| 679 | ZydisDecodedInstruction insn; | ||
| 680 | ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT]; | ||
| 681 | 53567 | const std::size_t offset = static_cast<std::size_t>(cursor - window.base); | |
| 682 |
2/2✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 18 taken 53566 times.
|
53567 | if (!ZYAN_SUCCESS( |
| 683 | ZydisDecoderDecodeFull(&decoder, bytes + offset, window.span - offset, &insn, operands) | ||
| 684 | )) | ||
| 685 | { | ||
| 686 | // Real functions carry undecodable bytes: an embedded jump table, compiler padding, or data a | ||
| 687 | // switch lowered into .text. The stream stops there and adjudicates nothing beyond it. | ||
| 688 | 1 | return std::nullopt; | |
| 689 | } | ||
| 690 |
2/2✓ Branch 18 → 19 taken 26 times.
✓ Branch 18 → 25 taken 53540 times.
|
53566 | if (cursor + insn.length > disp_field) |
| 691 | { | ||
| 692 | // Arrived. This instruction is the compiler's own framing of the candidate's bytes, so its | ||
| 693 | // verdict is final in both directions. | ||
| 694 |
2/2✓ Branch 20 → 21 taken 8 times.
✓ Branch 20 → 22 taken 18 times.
|
26 | return instruction_references_at(insn, operands, cursor, disp_field, string_addr) ? cursor : 0; |
| 695 | } | ||
| 696 | 53540 | cursor += insn.length; | |
| 697 | } | ||
| 698 | ✗ | return std::nullopt; | |
| 699 | } | ||
| 700 | |||
| 701 | // Resolves the candidate at @p disp_field without a trusted origin: leaf functions carry no unwind data, | ||
| 702 | // and JIT or raw code buffers have no exception table at all. Every instruction start that could place a | ||
| 703 | // disp32 at the candidate is probed, so no single framing can suppress the reference. | ||
| 704 | // | ||
| 705 | // Probing runs from the earliest possible start toward the candidate, so the LONGEST accepted framing wins. | ||
| 706 | // That is the disambiguation rule, not a preference: an instruction's encoding includes its legacy prefixes | ||
| 707 | // and REX byte, so a start that skips one decodes a different instruction that merely happens to share the | ||
| 708 | // displacement. Reporting the shorter framing would put the site one byte past the instruction and disagree | ||
| 709 | // with the exact narrow shape scan for the very shapes both can see. | ||
| 710 | // | ||
| 711 | // The converse is possible too and is not a defect here: an unrelated byte before the true start that the | ||
| 712 | // decoder absorbs as a legacy prefix or a superseded REX yields an earlier accepted framing than the narrow | ||
| 713 | // shape scan reports. The two phases therefore identify a reference by its displacement FIELD, not by the | ||
| 714 | // instruction start, so a framing disagreement over one genuine reference cannot manufacture ambiguity. | ||
| 715 | 410 | std::uintptr_t resolve_candidate_by_probe( | |
| 716 | const ZydisDecoder &decoder, | ||
| 717 | const detail::ExecutableWindow &window, | ||
| 718 | std::uintptr_t disp_field, | ||
| 719 | std::uintptr_t string_addr | ||
| 720 | ) noexcept | ||
| 721 | { | ||
| 722 | 410 | const auto *bytes = reinterpret_cast<const std::uint8_t *>(window.base); | |
| 723 | 410 | const std::uintptr_t floor = (disp_field - window.base >= MAX_BYTES_BEFORE_DISP32) | |
| 724 |
2/2✓ Branch 2 → 3 taken 409 times.
✓ Branch 2 → 4 taken 1 time.
|
410 | ? disp_field - MAX_BYTES_BEFORE_DISP32 |
| 725 | : window.base; | ||
| 726 |
2/2✓ Branch 14 → 6 taken 3119 times.
✓ Branch 14 → 15 taken 124 times.
|
3243 | for (std::uintptr_t start = floor; start < disp_field; ++start) |
| 727 | { | ||
| 728 | ZydisDecodedInstruction insn; | ||
| 729 | ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT]; | ||
| 730 | 3119 | const std::size_t offset = static_cast<std::size_t>(start - window.base); | |
| 731 |
2/2✓ Branch 7 → 8 taken 3 times.
✓ Branch 7 → 9 taken 3116 times.
|
3119 | if (!ZYAN_SUCCESS( |
| 732 | ZydisDecoderDecodeFull(&decoder, bytes + offset, window.span - offset, &insn, operands) | ||
| 733 | )) | ||
| 734 | { | ||
| 735 | 3 | continue; | |
| 736 | } | ||
| 737 |
2/2✓ Branch 10 → 11 taken 286 times.
✓ Branch 10 → 12 taken 2830 times.
|
3116 | if (instruction_references_at(insn, operands, start, disp_field, string_addr)) |
| 738 | { | ||
| 739 | 286 | return start; | |
| 740 | } | ||
| 741 | } | ||
| 742 | 124 | return 0; | |
| 743 | } | ||
| 744 | |||
| 745 | // Inner broad scan of one already-gated executable window (no fault guard). Mutates found_count / | ||
| 746 | // first_site and returns once a second referencing site is seen. The discovery/verification contract is | ||
| 747 | // documented on scan_string_ref_broad. | ||
| 748 | 362 | void scan_window_broad_body( | |
| 749 | const ZydisDecoder &decoder, | ||
| 750 | const detail::ExecutableWindow &window, | ||
| 751 | std::uintptr_t string_addr, | ||
| 752 | std::uintptr_t count_floor, | ||
| 753 | std::size_t &found_count, | ||
| 754 | std::uintptr_t &first_site, | ||
| 755 | std::uintptr_t &first_end, | ||
| 756 | std::uintptr_t &first_key | ||
| 757 | ) noexcept | ||
| 758 | { | ||
| 759 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 362 times.
|
362 | if (window.span < sizeof(std::int32_t)) |
| 760 | { | ||
| 761 | ✗ | return; | |
| 762 | } | ||
| 763 | 362 | const auto *bytes = reinterpret_cast<const std::uint8_t *>(window.base); | |
| 764 | 362 | const std::size_t limit = window.span - sizeof(std::int32_t); | |
| 765 |
2/2✓ Branch 25 → 5 taken 1383029 times.
✓ Branch 25 → 26 taken 306 times.
|
1383335 | for (std::size_t i = 0; i <= limit; ++i) |
| 766 | { | ||
| 767 | // Candidate discovery, at every byte offset and independent of any decode. A RIP-relative operand | ||
| 768 | // computes target = end_of_instruction + disp32, and the instruction ends at the displacement field | ||
| 769 | // plus four plus whatever immediate follows, so a field can only reference the string when | ||
| 770 | // string_addr - (field + 4) - disp is one of the immediate widths an x86-64 instruction may place | ||
| 771 | // after a disp32. This is the property that makes the sweep desync-immune: a false boundary can | ||
| 772 | // mis-frame the bytes around a reference, but it cannot make this arithmetic stop holding. | ||
| 773 | 1383029 | std::int32_t disp = 0; | |
| 774 | 1383029 | std::memcpy(&disp, bytes + i, sizeof(disp)); | |
| 775 | 1383029 | const std::uintptr_t disp_field = window.base + i; | |
| 776 | 1383029 | const std::int64_t immediate_width = static_cast<std::int64_t>(string_addr) - | |
| 777 | 1383029 | static_cast<std::int64_t>(disp_field + sizeof(disp)) - disp; | |
| 778 |
8/8✓ Branch 5 → 6 taken 1382651 times.
✓ Branch 5 → 10 taken 378 times.
✓ Branch 6 → 7 taken 1382610 times.
✓ Branch 6 → 10 taken 41 times.
✓ Branch 7 → 8 taken 1382575 times.
✓ Branch 7 → 10 taken 35 times.
✓ Branch 8 → 9 taken 1382540 times.
✓ Branch 8 → 10 taken 35 times.
|
1383029 | if (immediate_width != 0 && immediate_width != 1 && immediate_width != 2 && immediate_width != 4) |
| 779 | { | ||
| 780 | 1382683 | continue; | |
| 781 | } | ||
| 782 | |||
| 783 | // Verification. The trusted .pdata stream decides wherever an exception record covers the | ||
| 784 | // candidate; otherwise the bounded probe covers leaf functions and code with no registered table. | ||
| 785 | const std::optional<std::uintptr_t> trusted_site = | ||
| 786 | 489 | resolve_candidate_from_trusted_origin(decoder, window, disp_field, string_addr); | |
| 787 | const std::uintptr_t site = | ||
| 788 |
2/2✓ Branch 12 → 13 taken 26 times.
✓ Branch 12 → 15 taken 410 times.
|
436 | trusted_site.has_value() ? *trusted_site |
| 789 | 436 | : resolve_candidate_by_probe(decoder, window, disp_field, string_addr); | |
| 790 |
2/2✓ Branch 16 → 17 taken 142 times.
✓ Branch 16 → 18 taken 294 times.
|
436 | if (site == 0) |
| 791 | { | ||
| 792 | 142 | continue; | |
| 793 | } | ||
| 794 | |||
| 795 | // Cross-window back-carry de-duplication: this window may have been extended backward into the | ||
| 796 | // previous window's tail so a boundary-straddling instruction is discovered whole (see the loop in | ||
| 797 | // scan_string_ref_broad). An instruction that ENDS at or before count_floor lies wholly in the | ||
| 798 | // previous window and was already counted there. count_floor equals this window's real base, so an | ||
| 799 | // un-extended window counts every reference it finds. | ||
| 800 | 294 | const std::uintptr_t site_end = | |
| 801 | 294 | disp_field + sizeof(disp) + static_cast<std::uintptr_t>(immediate_width); | |
| 802 |
2/2✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 20 taken 293 times.
|
294 | if (site_end <= count_floor) |
| 803 | { | ||
| 804 | 1 | continue; | |
| 805 | } | ||
| 806 | |||
| 807 | 293 | ++found_count; | |
| 808 |
2/2✓ Branch 20 → 21 taken 290 times.
✓ Branch 20 → 22 taken 3 times.
|
293 | if (found_count == 1) |
| 809 | { | ||
| 810 | 290 | first_site = site; | |
| 811 | 290 | first_end = site_end; | |
| 812 | 290 | first_key = disp_field; | |
| 813 | } | ||
| 814 | else | ||
| 815 | { | ||
| 816 | // Ambiguous; caller maps found_count >= 2 to AmbiguousReference. | ||
| 817 | 3 | return; | |
| 818 | } | ||
| 819 | } | ||
| 820 | } | ||
| 821 | |||
| 822 | // Window-granular TOCTOU fault guard around scan_window_broad_body; the narrow sibling | ||
| 823 | // scan_window_narrow_guarded documents the shared rationale. This body also leaves the window: | ||
| 824 | // resolve_candidate_from_trusted_origin calls RtlLookupFunctionEntry, whose dynamic-function-table walk is | ||
| 825 | // not the scanned range. A fault raised there is a defect or hostile registration, not the concurrent | ||
| 826 | // unmap this guard exists to absorb, so it must reach the host's handlers instead of being recorded as a | ||
| 827 | // faulted window. The nested guarded_read_bytes of the .pdata record carries its own inner range filter, so | ||
| 828 | // its faults never arrive here. Returns true when a fault was swallowed. | ||
| 829 | 362 | bool scan_window_broad_guarded( | |
| 830 | const ZydisDecoder &decoder, | ||
| 831 | const detail::ExecutableWindow &window, | ||
| 832 | std::uintptr_t string_addr, | ||
| 833 | std::uintptr_t count_floor, | ||
| 834 | std::size_t &found_count, | ||
| 835 | std::uintptr_t &first_site, | ||
| 836 | std::uintptr_t &first_end, | ||
| 837 | std::uintptr_t &first_key | ||
| 838 | ) noexcept | ||
| 839 | { | ||
| 840 | #ifdef _MSC_VER | ||
| 841 | const std::size_t original_found_count = found_count; | ||
| 842 | const std::uintptr_t original_first_site = first_site; | ||
| 843 | const std::uintptr_t original_first_end = first_end; | ||
| 844 | const std::uintptr_t original_first_key = first_key; | ||
| 845 | __try | ||
| 846 | { | ||
| 847 | scan_window_broad_body( | ||
| 848 | decoder, | ||
| 849 | window, | ||
| 850 | string_addr, | ||
| 851 | count_floor, | ||
| 852 | found_count, | ||
| 853 | first_site, | ||
| 854 | first_end, | ||
| 855 | first_key | ||
| 856 | ); | ||
| 857 | return false; | ||
| 858 | } | ||
| 859 | __except ( | ||
| 860 | detail::guarded_range_fault_filter( | ||
| 861 | GetExceptionInformation(), | ||
| 862 | window.base, | ||
| 863 | window.base + window.span | ||
| 864 | ) | ||
| 865 | ) | ||
| 866 | { | ||
| 867 | // The caller skips faulted windows, so discard any reference count collected before the fault. | ||
| 868 | found_count = original_found_count; | ||
| 869 | first_site = original_first_site; | ||
| 870 | first_end = original_first_end; | ||
| 871 | first_key = original_first_key; | ||
| 872 | return true; | ||
| 873 | } | ||
| 874 | #elif defined(_WIN64) | ||
| 875 | // MinGW x64: same vectored read guard as the narrow sibling, armed over the gated window bytes; a fault | ||
| 876 | // is swallowed and the window reported faulted. A 32-bit build is rejected by the defines.hpp | ||
| 877 | // architecture gate, so only the two x64 arms exist. | ||
| 878 | 362 | const std::size_t original_found_count = found_count; | |
| 879 | 362 | const std::uintptr_t original_first_site = first_site; | |
| 880 | 362 | const std::uintptr_t original_first_end = first_end; | |
| 881 | 362 | const std::uintptr_t original_first_key = first_key; | |
| 882 | struct BroadScanContext | ||
| 883 | { | ||
| 884 | const ZydisDecoder *decoder; | ||
| 885 | const detail::ExecutableWindow *window; | ||
| 886 | std::uintptr_t string_addr; | ||
| 887 | std::uintptr_t count_floor; | ||
| 888 | std::size_t *found_count; | ||
| 889 | std::uintptr_t *first_site; | ||
| 890 | std::uintptr_t *first_end; | ||
| 891 | std::uintptr_t *first_key; | ||
| 892 | } scan_ctx{ | ||
| 893 | &decoder, | ||
| 894 | &window, | ||
| 895 | string_addr, | ||
| 896 | count_floor, | ||
| 897 | &found_count, | ||
| 898 | &first_site, | ||
| 899 | &first_end, | ||
| 900 | &first_key | ||
| 901 | 362 | }; | |
| 902 | |||
| 903 | 362 | const auto run_scan = [](void *opaque) noexcept -> void | |
| 904 | { | ||
| 905 | 362 | auto *context = static_cast<BroadScanContext *>(opaque); | |
| 906 | 362 | scan_window_broad_body( | |
| 907 | 362 | *context->decoder, | |
| 908 | 362 | *context->window, | |
| 909 | context->string_addr, | ||
| 910 | context->count_floor, | ||
| 911 | 362 | *context->found_count, | |
| 912 | 362 | *context->first_site, | |
| 913 | 362 | *context->first_end, | |
| 914 | 362 | *context->first_key | |
| 915 | ); | ||
| 916 | 309 | }; | |
| 917 | |||
| 918 |
2/2✓ Branch 4 → 5 taken 309 times.
✓ Branch 4 → 6 taken 53 times.
|
362 | if (detail::run_guarded_region(window.base, window.base + window.span, run_scan, &scan_ctx)) |
| 919 | { | ||
| 920 | 309 | return false; | |
| 921 | } | ||
| 922 | // Faulted: discard any partial count / site so a partially-scanned window cannot leak a stale site. | ||
| 923 | 53 | found_count = original_found_count; | |
| 924 | 53 | first_site = original_first_site; | |
| 925 | 53 | first_end = original_first_end; | |
| 926 | 53 | first_key = original_first_key; | |
| 927 | 53 | return true; | |
| 928 | #endif | ||
| 929 | } | ||
| 930 | |||
| 931 | // The broad phase uses Zydis to verify every RIP-relative memory reference that resolves to string_addr. | ||
| 932 | // `[B-63]` owns when a derived return must use this phase. | ||
| 933 | // | ||
| 934 | // An arbitrary byte does not identify an x86-64 instruction boundary. Discovery therefore tests target | ||
| 935 | // arithmetic at every byte offset. Only survivors pay for decode and boundary validation. The .pdata entry | ||
| 936 | // supplies a registered boundary, while a bounded probe covers fallback code. The second hit marks | ||
| 937 | // ambiguity. StringXrefBoundaryProof.FalseBoundaryCannotSuppressBroadOnlyReference pins this order. | ||
| 938 | 294 | std::uintptr_t scan_string_ref_broad( | |
| 939 | std::uintptr_t string_addr, | ||
| 940 | std::span<const detail::ExecutableWindow> windows, | ||
| 941 | std::size_t &found_count, | ||
| 942 | std::uintptr_t &out_end, | ||
| 943 | std::uintptr_t &out_key, | ||
| 944 | bool &incomplete | ||
| 945 | ) | ||
| 946 | { | ||
| 947 | 294 | found_count = 0; | |
| 948 | 294 | out_end = 0; | |
| 949 | 294 | out_key = 0; | |
| 950 | 294 | incomplete = false; | |
| 951 | 294 | std::uintptr_t first_site = 0; | |
| 952 | 294 | std::uintptr_t first_end = 0; | |
| 953 | 294 | std::uintptr_t first_key = 0; | |
| 954 | |||
| 955 | ZydisDecoder decoder; | ||
| 956 |
2/4✓ Branch 2 → 3 taken 294 times.
✗ Branch 2 → 38 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 294 times.
|
294 | if (!ZYAN_SUCCESS(ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64))) |
| 957 | { | ||
| 958 | // A decoder that will not initialize cannot verify any reference; fail closed as "no reference" | ||
| 959 | // rather than guess a site. | ||
| 960 | ✗ | return 0; | |
| 961 | } | ||
| 962 | |||
| 963 | 294 | std::size_t faulted_windows = 0; | |
| 964 | // Cross-window back-carry, mirroring the narrow scan (and phase 1). A variable-length reference can | ||
| 965 | // straddle the split between two abutting execute-readable windows, decodable by neither window's own | ||
| 966 | // sweep (the previous window's decoder truncates at its end, and this window decodes from its base, | ||
| 967 | // mid-instruction). When this window abuts the previous, decode from ZYDIS_MAX_INSTRUCTION_LENGTH - 1 | ||
| 968 | // bytes earlier so the straddler is decoded whole. A count floor at this window's real base then | ||
| 969 | // de-duplicates: an instruction ending at or before the base was already counted by the previous | ||
| 970 | // window, so scan_window_broad_body skips it (see there). The carry is bounded by the previous window's | ||
| 971 | // span so it never reads before it; page-granular regions make that bound a formality. | ||
| 972 | 294 | constexpr std::size_t broad_carry = ZYDIS_MAX_INSTRUCTION_LENGTH - 1; | |
| 973 | 294 | std::uintptr_t prev_end = 0; | |
| 974 | 294 | std::size_t prev_span = 0; | |
| 975 | 294 | bool have_prev = false; | |
| 976 |
2/2✓ Branch 31 → 7 taken 362 times.
✓ Branch 31 → 32 taken 291 times.
|
947 | for (const detail::ExecutableWindow &window : windows) |
| 977 | { | ||
| 978 | 362 | detail::ExecutableWindow effective = window; | |
| 979 |
4/4✓ Branch 9 → 10 taken 68 times.
✓ Branch 9 → 15 taken 294 times.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 15 taken 66 times.
|
362 | if (have_prev && window.base == prev_end) |
| 980 | { | ||
| 981 |
1/2✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 2 times.
|
2 | const std::size_t carry = (broad_carry < prev_span) ? broad_carry : prev_span; |
| 982 | 2 | effective.base = window.base - carry; | |
| 983 | 2 | effective.span = window.span + carry; | |
| 984 | } | ||
| 985 | 362 | prev_end = window.base + window.span; | |
| 986 | 362 | prev_span = window.span; | |
| 987 | 362 | have_prev = true; | |
| 988 | |||
| 989 |
2/2✓ Branch 16 → 17 taken 53 times.
✓ Branch 16 → 18 taken 309 times.
|
362 | if (scan_window_broad_guarded( |
| 990 | decoder, | ||
| 991 | effective, | ||
| 992 | string_addr, | ||
| 993 | 362 | window.base, | |
| 994 | found_count, | ||
| 995 | first_site, | ||
| 996 | first_end, | ||
| 997 | first_key | ||
| 998 | )) | ||
| 999 | { | ||
| 1000 | 53 | ++faulted_windows; | |
| 1001 | 53 | continue; | |
| 1002 | } | ||
| 1003 |
2/2✓ Branch 18 → 19 taken 3 times.
✓ Branch 18 → 21 taken 306 times.
|
309 | if (found_count >= 2) |
| 1004 | { | ||
| 1005 | // Ambiguous; caller maps found_count >= 2 to AmbiguousReference. | ||
| 1006 | 3 | incomplete = faulted_windows > 0; | |
| 1007 | 3 | log_faulted_windows(faulted_windows); | |
| 1008 | 3 | return 0; | |
| 1009 | } | ||
| 1010 | } | ||
| 1011 | // Surface any skipped faulted window (see scan_string_ref_narrow): the caller fails closed on a | ||
| 1012 | // lower-bound count rather than reporting a lone surviving reference as unique. | ||
| 1013 | 291 | incomplete = faulted_windows > 0; | |
| 1014 | 291 | log_faulted_windows(faulted_windows); | |
| 1015 |
2/2✓ Branch 33 → 34 taken 4 times.
✓ Branch 33 → 35 taken 287 times.
|
291 | if (found_count != 1) |
| 1016 | { | ||
| 1017 | 4 | return 0; | |
| 1018 | } | ||
| 1019 | 287 | out_end = first_end; | |
| 1020 | 287 | out_key = first_key; | |
| 1021 | 287 | return first_site; | |
| 1022 | } | ||
| 1023 | |||
| 1024 | // `[B-64]` requires the x64 exception directory before the heuristic scan. RtlLookupFunctionEntry reports | ||
| 1025 | // the image base and innermost RUNTIME_FUNCTION for registered code. A null or invalid result permits the | ||
| 1026 | // fallback. | ||
| 1027 | // | ||
| 1028 | // UNW_FLAG_CHAININFO links a funclet or hot/cold fragment to its primary function. The chained record | ||
| 1029 | // follows the padded unwind-code array at UnwindData + 4 + 2 * ((CountOfCodes + 1) & ~1). Guarded reads and | ||
| 1030 | // a bounded hop count convert invalid metadata into fallback. The | ||
| 1031 | // StringXrefTest.EnclosingFunctionFollowsChainInfoToPrimaryFunction and | ||
| 1032 | // EnclosingFunctionFallsBackWhenChainInfoIsCyclic cases pin both outcomes. | ||
| 1033 | 16 | std::uintptr_t function_entry_via_pdata(std::uintptr_t instr_addr) noexcept | |
| 1034 | { | ||
| 1035 | 16 | DWORD64 image_base = 0; | |
| 1036 | 16 | PRUNTIME_FUNCTION entry = RtlLookupFunctionEntry(instr_addr, &image_base, nullptr); | |
| 1037 |
3/4✓ Branch 3 → 4 taken 9 times.
✓ Branch 3 → 5 taken 7 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 9 times.
|
16 | if (entry == nullptr || image_base == 0) |
| 1038 | { | ||
| 1039 | 7 | return 0; | |
| 1040 | } | ||
| 1041 | 9 | const std::uintptr_t base = static_cast<std::uintptr_t>(image_base); | |
| 1042 | |||
| 1043 | // The live image backing image_base bounds every RVA this walk dereferences. A dynamically registered | ||
| 1044 | // table (RtlAddFunctionTable over a non-image allocation) has no PE header, so the span stays invalid | ||
| 1045 | // and only the checked arithmetic and the fault guard apply; a normally loaded module always resolves, | ||
| 1046 | // and its unwind metadata is additionally required to stay inside [base, base + SizeOfImage) so a | ||
| 1047 | // malformed or wrapped RVA that merely happens to land on other mapped memory cannot escape the image. | ||
| 1048 | 9 | const detail::ModuleSpan image = detail::module_span(detail::module_image_region(Address{base})); | |
| 1049 | const auto resolve_rva_span = | ||
| 1050 | 67 | [base, image](std::uint64_t rva, std::size_t need) -> std::optional<std::uintptr_t> | |
| 1051 | { | ||
| 1052 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 67 times.
|
67 | if (rva > static_cast<std::uint64_t>(UINTPTR_MAX - base)) |
| 1053 | { | ||
| 1054 | ✗ | return std::nullopt; | |
| 1055 | } | ||
| 1056 | 67 | const std::uintptr_t address = base + static_cast<std::uintptr_t>(rva); | |
| 1057 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 67 times.
|
67 | if (need > UINTPTR_MAX - address) |
| 1058 | { | ||
| 1059 | ✗ | return std::nullopt; | |
| 1060 | } | ||
| 1061 |
9/10✓ Branch 7 → 8 taken 12 times.
✓ Branch 7 → 12 taken 55 times.
✓ Branch 8 → 9 taken 12 times.
✗ Branch 8 → 11 not taken.
✓ Branch 9 → 10 taken 10 times.
✓ Branch 9 → 11 taken 2 times.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 12 taken 8 times.
✓ Branch 13 → 14 taken 4 times.
✓ Branch 13 → 15 taken 63 times.
|
67 | if (image.valid() && (address < image.base || address >= image.end || image.end - address < need)) |
| 1062 | { | ||
| 1063 | 4 | return std::nullopt; | |
| 1064 | } | ||
| 1065 | 63 | return address; | |
| 1066 | 9 | }; | |
| 1067 | |||
| 1068 | // Copy the resolved record under the guard before walking .xdata by hand. RtlLookupFunctionEntry may | ||
| 1069 | // return a module .pdata record or a dynamically registered table record; both are still process memory | ||
| 1070 | // that must fail closed on an unexpected fault. | ||
| 1071 | 9 | RUNTIME_FUNCTION current{}; | |
| 1072 | 9 | const std::uintptr_t entry_address = reinterpret_cast<std::uintptr_t>(entry); | |
| 1073 | 9 | if ((image.valid() && | |
| 1074 |
7/8✓ Branch 10 → 11 taken 6 times.
✓ Branch 10 → 14 taken 3 times.
✓ Branch 12 → 13 taken 5 times.
✓ Branch 12 → 16 taken 1 time.
✓ Branch 13 → 14 taken 5 times.
✗ Branch 13 → 16 not taken.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 20 taken 8 times.
|
17 | (!image.contains(entry_address) || image.end - entry_address < sizeof(current))) || |
| 1075 |
1/2✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 8 times.
|
8 | !detail::guarded_read_bytes(entry_address, ¤t, sizeof(current))) |
| 1076 | { | ||
| 1077 | 1 | return 0; | |
| 1078 | } | ||
| 1079 | |||
| 1080 | 8 | constexpr int MAX_CHAIN_HOPS = 16; | |
| 1081 |
2/2✓ Branch 52 → 21 taken 25 times.
✓ Branch 52 → 53 taken 1 time.
|
26 | for (int hop = 0; hop < MAX_CHAIN_HOPS; ++hop) |
| 1082 | { | ||
| 1083 |
1/2✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 25 times.
|
25 | if (current.EndAddress <= current.BeginAddress) |
| 1084 | { | ||
| 1085 | 7 | return 0; | |
| 1086 | } | ||
| 1087 | const std::optional<std::uintptr_t> function_start = | ||
| 1088 | 25 | resolve_rva_span(current.BeginAddress, current.EndAddress - current.BeginAddress); | |
| 1089 |
2/2✓ Branch 25 → 26 taken 2 times.
✓ Branch 25 → 27 taken 23 times.
|
25 | if (!function_start) |
| 1090 | { | ||
| 1091 | 2 | return 0; | |
| 1092 | } | ||
| 1093 | |||
| 1094 | // UNWIND_INFO fixed header: byte 0 packs Version:3 | Flags:5 (so Flags = byte0 >> 3) and byte 2 is | ||
| 1095 | // CountOfCodes. Only these two fields drive chain traversal, so read the 4-byte header instead of | ||
| 1096 | // modelling the whole variable-length structure. | ||
| 1097 | 23 | std::uint8_t unwind_header[4] = {}; | |
| 1098 | const std::optional<std::uintptr_t> unwind_addr = | ||
| 1099 | 23 | resolve_rva_span(current.UnwindData, sizeof(unwind_header)); | |
| 1100 |
5/6✓ Branch 29 → 30 taken 22 times.
✓ Branch 29 → 33 taken 1 time.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 22 times.
✓ Branch 35 → 36 taken 1 time.
✓ Branch 35 → 37 taken 22 times.
|
23 | if (!unwind_addr || !detail::guarded_read_bytes(*unwind_addr, unwind_header, sizeof(unwind_header))) |
| 1101 | { | ||
| 1102 | 1 | return 0; | |
| 1103 | } | ||
| 1104 |
2/2✓ Branch 37 → 38 taken 3 times.
✓ Branch 37 → 40 taken 19 times.
|
22 | if (((unwind_header[0] >> 3) & UNW_FLAG_CHAININFO) == 0) |
| 1105 | { | ||
| 1106 | 3 | return *function_start; | |
| 1107 | } | ||
| 1108 | // The chained RUNTIME_FUNCTION follows the unwind-code array, padded up to an even entry count; | ||
| 1109 | // UnwindData is that structure's own RVA. Compute the offset in 64-bit so a large CountOfCodes or | ||
| 1110 | // UnwindData cannot wrap the 32-bit RVA before it is bounded against the image. | ||
| 1111 | 19 | const std::uint32_t count_of_codes = unwind_header[2]; | |
| 1112 | 19 | const std::uint64_t chained_rva = | |
| 1113 | 19 | static_cast<std::uint64_t>(current.UnwindData) + 4u + | |
| 1114 | 19 | 2u * ((static_cast<std::uint64_t>(count_of_codes) + 1u) & ~std::uint64_t{1}); | |
| 1115 | const std::optional<std::uintptr_t> chained_addr = | ||
| 1116 | 19 | resolve_rva_span(chained_rva, sizeof(RUNTIME_FUNCTION)); | |
| 1117 | 19 | RUNTIME_FUNCTION chained{}; | |
| 1118 |
5/6✓ Branch 42 → 43 taken 18 times.
✓ Branch 42 → 46 taken 1 time.
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 47 taken 18 times.
✓ Branch 48 → 49 taken 1 time.
✓ Branch 48 → 50 taken 18 times.
|
19 | if (!chained_addr || !detail::guarded_read_bytes(*chained_addr, &chained, sizeof(chained))) |
| 1119 | { | ||
| 1120 | 1 | return 0; | |
| 1121 | } | ||
| 1122 | 18 | current = chained; | |
| 1123 | } | ||
| 1124 | |||
| 1125 | 1 | return 0; | |
| 1126 | } | ||
| 1127 | |||
| 1128 | // `[B-64]` requires the authoritative lookup before this bounded heuristic. Absent or malformed metadata | ||
| 1129 | // permits the fallback. The scan finds the nearest 0xC3 RET or 0xCC INT3 boundary and then requires a | ||
| 1130 | // plausible prologue. It returns zero when the bounded window contains no boundary. The Windows x64 ABI | ||
| 1131 | // cleans the stack in the caller, so no epilogue uses RET imm16 and 0xC2 stays out of the boundary set. | ||
| 1132 | 16 | std::uintptr_t enclosing_function_start(std::uintptr_t instr_addr, std::uintptr_t window_lo) noexcept | |
| 1133 | { | ||
| 1134 |
2/2✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 5 taken 13 times.
|
16 | if (const std::uintptr_t via_pdata = function_entry_via_pdata(instr_addr); via_pdata != 0) |
| 1135 | { | ||
| 1136 | 3 | return via_pdata; | |
| 1137 | } | ||
| 1138 | |||
| 1139 | 13 | constexpr std::uintptr_t back_scan_window = 0x2000; // 8 KiB. | |
| 1140 | 13 | const std::uintptr_t floor = | |
| 1141 |
2/2✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 12 times.
|
13 | (instr_addr - window_lo > back_scan_window) ? instr_addr - back_scan_window : window_lo; |
| 1142 |
1/2✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 13 times.
|
13 | if (instr_addr <= floor) |
| 1143 | { | ||
| 1144 | ✗ | return 0; | |
| 1145 | } | ||
| 1146 | |||
| 1147 | // Buffer the back-scan window once instead of issuing a guarded read per byte. The window is read in | ||
| 1148 | // page-sized chunks from high address to low: a chunk that faults stops the read exactly where the | ||
| 1149 | // byte-at-a-time walk would have faulted going downward, so any boundary in the already-buffered higher | ||
| 1150 | // bytes is still found, and a fault below them returns 0 just as the per-byte version did. window[k] | ||
| 1151 | // holds the byte at floor + k; valid_lo is the lowest address actually buffered. | ||
| 1152 | // | ||
| 1153 | // Left deliberately uninitialized: the chunk-read loop fills exactly [valid_lo, instr_addr) via | ||
| 1154 | // guarded_read_bytes, and both probe loops below read only indices within that filled span (they are | ||
| 1155 | // bounded by valid_lo / instr_addr), so no unwritten byte is ever observed. Value-initializing the full | ||
| 1156 | // 8 KiB every call would be dead work on this control-plane path. | ||
| 1157 | // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init) filled before any read, see above | ||
| 1158 | std::array<std::uint8_t, static_cast<std::size_t>(back_scan_window)> window; | ||
| 1159 | 13 | std::uintptr_t valid_lo = instr_addr; | |
| 1160 | // Windows x86/x64 base page size; guarded_read_bytes faults at this granularity. Chunks are | ||
| 1161 | // page-aligned, not instr-aligned, so a single guarded read covers at most one page and never straddles | ||
| 1162 | // a readable/unreadable page boundary. | ||
| 1163 | 13 | constexpr std::uintptr_t page_size = 0x1000; | |
| 1164 |
2/2✓ Branch 20 → 11 taken 22 times.
✓ Branch 20 → 21 taken 12 times.
|
34 | for (std::uintptr_t hi = instr_addr; hi > floor;) |
| 1165 | { | ||
| 1166 | 22 | const std::uintptr_t page_lo = (hi - 1) & ~(page_size - 1); | |
| 1167 |
2/2✓ Branch 11 → 12 taken 9 times.
✓ Branch 11 → 13 taken 13 times.
|
22 | const std::uintptr_t lo = (page_lo > floor) ? page_lo : floor; |
| 1168 | 22 | const std::size_t len = static_cast<std::size_t>(hi - lo); | |
| 1169 |
2/2✓ Branch 17 → 18 taken 1 time.
✓ Branch 17 → 19 taken 21 times.
|
22 | if (!detail::guarded_read_bytes(lo, window.data() + (lo - floor), len)) |
| 1170 | { | ||
| 1171 | // This page is unreadable; stop. The byte-at-a-time walk would fault on the first byte here | ||
| 1172 | // too, after exhausting the readable bytes buffered above it. | ||
| 1173 | 1 | break; | |
| 1174 | } | ||
| 1175 | 21 | valid_lo = lo; | |
| 1176 | 21 | hi = lo; | |
| 1177 | } | ||
| 1178 |
1/2✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 13 times.
|
13 | if (valid_lo >= instr_addr) |
| 1179 | { | ||
| 1180 | // Not even the highest chunk was readable: the first probed byte would have faulted. | ||
| 1181 | ✗ | return 0; | |
| 1182 | } | ||
| 1183 | |||
| 1184 |
2/2✓ Branch 44 → 24 taken 33746 times.
✓ Branch 44 → 45 taken 6 times.
|
33752 | for (std::uintptr_t probe = instr_addr; probe > valid_lo; --probe) |
| 1185 | { | ||
| 1186 | 33746 | const std::uint8_t boundary_byte = window[static_cast<std::size_t>(probe - 1 - floor)]; | |
| 1187 |
3/4✓ Branch 25 → 26 taken 33739 times.
✓ Branch 25 → 28 taken 7 times.
✓ Branch 26 → 27 taken 33739 times.
✗ Branch 26 → 28 not taken.
|
33746 | if (boundary_byte != 0xCC && boundary_byte != 0xC3) |
| 1188 | { | ||
| 1189 | 33739 | continue; | |
| 1190 | } | ||
| 1191 | // The boundary byte ends the previous function (or its padding); the enclosing function begins at | ||
| 1192 | // the first non-INT3 byte after it. | ||
| 1193 | 7 | std::uintptr_t start = probe; | |
| 1194 |
3/6✓ Branch 30 → 31 taken 7 times.
✗ Branch 30 → 34 not taken.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 7 times.
✗ Branch 35 → 29 not taken.
✓ Branch 35 → 36 taken 7 times.
|
7 | while (start < instr_addr && window[static_cast<std::size_t>(start - floor)] == 0xCC) |
| 1195 | { | ||
| 1196 | ✗ | ++start; | |
| 1197 | } | ||
| 1198 |
1/2✓ Branch 38 → 39 taken 7 times.
✗ Branch 38 → 40 not taken.
|
7 | return is_likely_function_prologue(Address{start}) ? start : 0; |
| 1199 | } | ||
| 1200 | 6 | return 0; | |
| 1201 | } | ||
| 1202 | } // namespace | ||
| 1203 | |||
| 1204 | namespace | ||
| 1205 | { | ||
| 1206 | // The whole two-phase resolve. Both entry points below are thin wrappers over it: the public one passes no | ||
| 1207 | // exclusion set, the internal one carries the ladder resolver's. | ||
| 1208 | 736 | Result<Address> resolve_string_xref( | |
| 1209 | const StringRefQuery &query, | ||
| 1210 | Region scope, | ||
| 1211 | const detail::ScanExclusions *provided_exclusions, | ||
| 1212 | std::span<const Region> declared_exclusions, | ||
| 1213 | Region *physical_source = nullptr | ||
| 1214 | ) | ||
| 1215 | { | ||
| 1216 |
2/2✓ Branch 2 → 3 taken 20 times.
✓ Branch 2 → 5 taken 716 times.
|
736 | if (physical_source != nullptr) |
| 1217 | { | ||
| 1218 | 20 | *physical_source = Region{}; | |
| 1219 | } | ||
| 1220 |
6/6✓ Branch 6 → 7 taken 734 times.
✓ Branch 6 → 9 taken 2 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 732 times.
✓ Branch 11 → 12 taken 4 times.
✓ Branch 11 → 15 taken 732 times.
|
736 | if (!detail::valid_string_encoding(query.encoding) || !detail::valid_xref_return(query.return_mode)) |
| 1221 | { | ||
| 1222 | 4 | return std::unexpected(Error{ErrorCode::InvalidArg, "scan::find_string_xref"}); | |
| 1223 | } | ||
| 1224 |
2/2✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 20 taken 731 times.
|
732 | if (query.text.empty()) |
| 1225 | { | ||
| 1226 | 1 | return std::unexpected(Error{ErrorCode::EmptyQuery, "scan::find_string_xref"}); | |
| 1227 | } | ||
| 1228 | 731 | const detail::ModuleSpan range = detail::module_span(scope); | |
| 1229 |
2/2✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 26 taken 730 times.
|
731 | if (!range.valid()) |
| 1230 | { | ||
| 1231 | 1 | return std::unexpected(Error{ErrorCode::InvalidRange, "scan::find_string_xref"}); | |
| 1232 | } | ||
| 1233 | // Phase 1 is a readable sweep, so it inherits the same authority rule as every other readable scan: a | ||
| 1234 | // scope wider than one image or allocation also covers the caller's own copy of the literal, and a | ||
| 1235 | // located address there would be the query finding itself rather than the image's string. | ||
| 1236 |
1/2✗ Branch 27 → 28 not taken.
✓ Branch 27 → 31 taken 730 times.
|
730 | if (!detail::readable_scan_is_authoritative(range, Pages::Readable, declared_exclusions)) |
| 1237 | { | ||
| 1238 | ✗ | return std::unexpected(Error{ErrorCode::NotAuthoritative, "scan::find_string_xref"}); | |
| 1239 | } | ||
| 1240 | |||
| 1241 | 730 | detail::ScanExclusions direct_exclusions; | |
| 1242 |
2/2✓ Branch 31 → 32 taken 720 times.
✓ Branch 31 → 35 taken 10 times.
|
730 | if (provided_exclusions == nullptr) |
| 1243 | { | ||
| 1244 | 720 | direct_exclusions.restrict_to(range.base, range.end); | |
| 1245 | 720 | direct_exclusions.add_text(query.text); | |
| 1246 | 720 | provided_exclusions = &direct_exclusions; | |
| 1247 | } | ||
| 1248 |
1/2✗ Branch 36 → 37 not taken.
✓ Branch 36 → 40 taken 730 times.
|
730 | if (provided_exclusions->overflowed()) |
| 1249 | { | ||
| 1250 | ✗ | return std::unexpected(Error{ErrorCode::NotAuthoritative, "scan::find_string_xref"}); | |
| 1251 | } | ||
| 1252 | |||
| 1253 | // Phase 1: locate the single occurrence of the string in the image's readable pages. The linker pools | ||
| 1254 | // identical literals, so a second occurrence makes the anchor ambiguous and must fail closed. | ||
| 1255 | 730 | QueryTextStatus text_status = QueryTextStatus::Ok; | |
| 1256 |
1/2✓ Branch 40 → 41 taken 730 times.
✗ Branch 40 → 149 not taken.
|
730 | const auto pattern = compile_string_pattern(query, text_status); |
| 1257 |
2/2✓ Branch 42 → 43 taken 15 times.
✓ Branch 42 → 50 taken 714 times.
|
730 | if (!pattern) |
| 1258 | { | ||
| 1259 |
1/2✓ Branch 43 → 44 taken 15 times.
✗ Branch 43 → 47 not taken.
|
15 | if (text_status == QueryTextStatus::Malformed) |
| 1260 | { | ||
| 1261 | // The text is not encodable as asked, so no literal to search for was ever defined. Distinct | ||
| 1262 | // from "searched and absent" so the caller fixes the query rather than the signature. | ||
| 1263 | 15 | return std::unexpected(Error{ErrorCode::MalformedQueryText, "scan::find_string_xref"}); | |
| 1264 | } | ||
| 1265 | ✗ | return std::unexpected(Error{ErrorCode::StringNotFound, "scan::find_string_xref"}); | |
| 1266 | } | ||
| 1267 | // One traversal counts zero, one, or two-or-more occurrences, so the located address and the uniqueness | ||
| 1268 | // verdict describe the same view of memory; two independent passes could straddle a concurrent write | ||
| 1269 | // and certify a pairing that never existed. compile_string_pattern emits literal bytes only, so this | ||
| 1270 | // pattern carries no bounded jumps and a skipped faulted region is its only truncation channel. | ||
| 1271 | 714 | const detail::MatchResult located = detail::scan_module_readable( | |
| 1272 | *pattern, | ||
| 1273 | range, | ||
| 1274 | 714 | detail::ScanQuery{ | |
| 1275 | .occurrence = 1, | ||
| 1276 | .count_beyond = true, | ||
| 1277 | .exclusions = provided_exclusions, | ||
| 1278 | } | ||
| 1279 | ); | ||
| 1280 |
2/2✓ Branch 52 → 53 taken 7 times.
✓ Branch 52 → 61 taken 708 times.
|
715 | if (located.match == nullptr) |
| 1281 | { | ||
| 1282 |
1/2✗ Branch 54 → 55 not taken.
✓ Branch 54 → 58 taken 7 times.
|
7 | if (located.truncated()) |
| 1283 | { | ||
| 1284 | // A truncated sweep never read part of the image, so it cannot report the literal absent. | ||
| 1285 | ✗ | return std::unexpected(Error{ErrorCode::IncompleteScan, "scan::find_string_xref"}); | |
| 1286 | } | ||
| 1287 | 7 | return std::unexpected(Error{ErrorCode::StringNotFound, "scan::find_string_xref"}); | |
| 1288 | } | ||
| 1289 |
2/2✓ Branch 61 → 62 taken 2 times.
✓ Branch 61 → 65 taken 706 times.
|
708 | if (located.count > 1) |
| 1290 | { | ||
| 1291 | // A second pooled copy was actually observed, so the anchor is genuinely non-unique. That verdict | ||
| 1292 | // is authoritative and stays ambiguous even when the sweep was also truncated. | ||
| 1293 | 2 | return std::unexpected(Error{ErrorCode::StringAmbiguous, "scan::find_string_xref"}); | |
| 1294 | } | ||
| 1295 |
2/2✓ Branch 66 → 67 taken 371 times.
✓ Branch 66 → 70 taken 335 times.
|
706 | if (located.truncated()) |
| 1296 | { | ||
| 1297 | // One copy was seen, but a truncated sweep makes the count a lower bound: a second pooled copy | ||
| 1298 | // could hide in bytes that were never read, so uniqueness is unproven. Fail closed on the | ||
| 1299 | // truncation itself rather than on an ambiguity verdict, so the reason survives to the caller and | ||
| 1300 | // the ladder resolver's typed-failure latch cannot degrade it to NoMatch. | ||
| 1301 | 371 | return std::unexpected(Error{ErrorCode::IncompleteScan, "scan::find_string_xref"}); | |
| 1302 | } | ||
| 1303 | 335 | const auto string_addr = reinterpret_cast<std::uintptr_t>(located.match); | |
| 1304 | |||
| 1305 | // Phase 2: find the single RIP-relative reference whose target is the string. The narrow scan is the | ||
| 1306 | // fast, desync-immune default; broad_match keeps that coverage and adds a Zydis sweep for rarer | ||
| 1307 | // reference shapes. | ||
| 1308 | // | ||
| 1309 | // Both sweeps run over ONE enumeration of the execute-readable windows. Enumerating twice would | ||
| 1310 | // let a concurrent reprotect hide a window from the second sweep only, and a window that is absent is | ||
| 1311 | // indistinguishable from a window that agreed: the confirmation below would then certify a site it | ||
| 1312 | // never examined. Sharing the list routes any mid-sweep loss through the faulted-window channel, which | ||
| 1313 | // does fail closed. | ||
| 1314 |
1/2✓ Branch 70 → 71 taken 334 times.
✗ Branch 70 → 147 not taken.
|
335 | const std::vector<detail::ExecutableWindow> windows = detail::collect_executable_windows(range); |
| 1315 | |||
| 1316 | 334 | ReferenceScanResult references{}; | |
| 1317 | 334 | std::size_t narrow_count = 0; | |
| 1318 | 334 | LeaReferenceInfo lea_info{}; | |
| 1319 | 334 | bool narrow_incomplete = false; | |
| 1320 | const std::uintptr_t narrow_site = | ||
| 1321 | 334 | scan_string_ref_narrow(string_addr, windows, narrow_count, lea_info, narrow_incomplete); | |
| 1322 | // The narrow shape is REX + opcode + ModRM + disp32, so its disp32 field sits exactly three bytes into | ||
| 1323 | // the instruction and the reference key is derivable from the site alone. | ||
| 1324 |
4/4✓ Branch 73 → 74 taken 312 times.
✓ Branch 73 → 75 taken 23 times.
✓ Branch 76 → 77 taken 312 times.
✓ Branch 76 → 78 taken 23 times.
|
647 | merge_reference_scan( |
| 1325 | references, | ||
| 1326 | narrow_site, | ||
| 1327 | 312 | (narrow_site != 0) ? narrow_site + lea_info.instr_len : 0, | |
| 1328 | (narrow_site != 0) ? narrow_site + 3 : 0, | ||
| 1329 | narrow_count, | ||
| 1330 | narrow_incomplete | ||
| 1331 | ); | ||
| 1332 | |||
| 1333 | // `[B-63]` requires a broad second check before a derived return can certify one narrow hit. The | ||
| 1334 | // narrow shape accepts REX.W LEA and MOV, so the broad sweep re-counts whichever reference the | ||
| 1335 | // narrow phase returned. A rarer-shape twin raises the count to 2 and fails closed. lea_info stays | ||
| 1336 | // unchanged, so EnclosingFunction keeps that site and StringPointerSlot still requires the narrow | ||
| 1337 | // lea. ReferencingInstruction stays on the narrow path. | ||
| 1338 | // | ||
| 1339 | // A derived anchor that remains unique pays one complete executable-range sweep. | ||
| 1340 | // ReferencingInstruction avoids that confirmation unless broad_match requests the broad phase. | ||
| 1341 | 335 | const bool derived_return = query.return_mode != XrefReturn::ReferencingInstruction; | |
| 1342 |
4/4✓ Branch 80 → 81 taken 37 times.
✓ Branch 80 → 83 taken 298 times.
✓ Branch 81 → 82 taken 36 times.
✓ Branch 81 → 83 taken 1 time.
|
335 | const bool confirm_derived_uniqueness = derived_return && references.count == 1; |
| 1343 |
6/6✓ Branch 84 → 85 taken 331 times.
✓ Branch 84 → 91 taken 4 times.
✓ Branch 85 → 86 taken 73 times.
✓ Branch 85 → 87 taken 258 times.
✓ Branch 86 → 87 taken 36 times.
✓ Branch 86 → 91 taken 37 times.
|
335 | if (references.count < 2 && (query.broad_match || confirm_derived_uniqueness)) |
| 1344 | { | ||
| 1345 | 294 | std::size_t broad_count = 0; | |
| 1346 | 294 | std::uintptr_t broad_end = 0; | |
| 1347 | 294 | std::uintptr_t broad_key = 0; | |
| 1348 | 294 | bool broad_incomplete = false; | |
| 1349 |
1/2✓ Branch 88 → 89 taken 294 times.
✗ Branch 88 → 143 not taken.
|
294 | const std::uintptr_t broad_site = scan_string_ref_broad( |
| 1350 | string_addr, | ||
| 1351 | 294 | windows, | |
| 1352 | broad_count, | ||
| 1353 | broad_end, | ||
| 1354 | broad_key, | ||
| 1355 | broad_incomplete | ||
| 1356 | ); | ||
| 1357 | 294 | merge_reference_scan(references, broad_site, broad_end, broad_key, broad_count, broad_incomplete); | |
| 1358 | } | ||
| 1359 | |||
| 1360 |
2/2✓ Branch 91 → 92 taken 7 times.
✓ Branch 91 → 95 taken 328 times.
|
335 | if (references.count >= 2) |
| 1361 | { | ||
| 1362 | 7 | return std::unexpected(Error{ErrorCode::AmbiguousReference, "scan::find_string_xref"}); | |
| 1363 | } | ||
| 1364 |
2/2✓ Branch 95 → 96 taken 55 times.
✓ Branch 95 → 99 taken 273 times.
|
328 | if (references.incomplete) |
| 1365 | { | ||
| 1366 | // An execute-readable window faulted mid-sweep and was skipped, so the reference count is a lower | ||
| 1367 | // bound: a second reference to the string could hide in the skipped window. A lone surviving | ||
| 1368 | // reference (or none) is therefore not provably unique. Fail closed on the truncation itself, the | ||
| 1369 | // phase-2 twin of the phase-1 gate above; the count-driven AmbiguousReference just above stays the | ||
| 1370 | // authoritative multiplicity verdict. Only the faulted-window channel reaches here, because | ||
| 1371 | // merge_reference_scan carries no work budget. | ||
| 1372 | 55 | return std::unexpected(Error{ErrorCode::IncompleteScan, "scan::find_string_xref"}); | |
| 1373 | } | ||
| 1374 |
2/2✓ Branch 99 → 100 taken 9 times.
✓ Branch 99 → 103 taken 264 times.
|
273 | if (references.count == 0) |
| 1375 | { | ||
| 1376 | 9 | return std::unexpected(Error{ErrorCode::NoReference, "scan::find_string_xref"}); | |
| 1377 | } | ||
| 1378 | |||
| 1379 |
2/2✓ Branch 103 → 104 taken 13 times.
✓ Branch 103 → 109 taken 251 times.
|
264 | if (physical_source != nullptr) |
| 1380 | { | ||
| 1381 | // The referencing instruction is the byte evidence every return mode rides on. Publish its whole | ||
| 1382 | // extent, not its first byte: a co-voting selector that matches the reference from any later byte | ||
| 1383 | // would otherwise abut this span instead of overlapping it and would double-vote one instruction. | ||
| 1384 | // Both sweeps carry that extent (the narrow shape's decoded length, the broad sweep's framed end), | ||
| 1385 | // so the single-byte floor below is only a defensive fallback for a site with no recorded end. | ||
| 1386 | ✗ | const std::size_t reference_length = | |
| 1387 |
1/2✓ Branch 104 → 105 taken 13 times.
✗ Branch 104 → 106 not taken.
|
13 | (references.site_end > references.site) ? references.site_end - references.site : 1; |
| 1388 | 13 | *physical_source = Region{Address{references.site}, reference_length}; | |
| 1389 | } | ||
| 1390 | |||
| 1391 |
2/2✓ Branch 109 → 110 taken 19 times.
✓ Branch 109 → 127 taken 245 times.
|
264 | if (query.return_mode == XrefReturn::StringPointerSlot) |
| 1392 | { | ||
| 1393 | // Store-xref needs the unique reference to be the narrow `lea reg, [rip+string]` whose loaded | ||
| 1394 | // pointer a following `mov [rip+slot], reg` caches. A broad-only surviving reference never | ||
| 1395 | // populates lea_info, and a `mov reg, [rip+string]` load has no store to attribute, so is_lea is | ||
| 1396 | // the whole test: it is set only by the narrow sweep, and only for the site it returned. | ||
| 1397 |
2/2✓ Branch 110 → 111 taken 2 times.
✓ Branch 110 → 114 taken 17 times.
|
19 | if (!lea_info.is_lea) |
| 1398 | { | ||
| 1399 | 2 | return std::unexpected(Error{ErrorCode::StoreNotFound, "scan::find_string_xref"}); | |
| 1400 | } | ||
| 1401 | 17 | std::uintptr_t store_end = 0; | |
| 1402 | 17 | const std::uintptr_t slot = scan_store_slot_after_lea( | |
| 1403 | references.site, | ||
| 1404 | lea_info.instr_len, | ||
| 1405 | lea_info.window_end, | ||
| 1406 | 17 | lea_info.reg, | |
| 1407 | range, | ||
| 1408 | store_end | ||
| 1409 | ); | ||
| 1410 |
2/2✓ Branch 115 → 116 taken 13 times.
✓ Branch 115 → 119 taken 4 times.
|
17 | if (slot == 0) |
| 1411 | { | ||
| 1412 | 13 | return std::unexpected(Error{ErrorCode::StoreNotFound, "scan::find_string_xref"}); | |
| 1413 | } | ||
| 1414 |
3/4✓ Branch 119 → 120 taken 1 time.
✓ Branch 119 → 123 taken 3 times.
✓ Branch 120 → 121 taken 1 time.
✗ Branch 120 → 123 not taken.
|
4 | if (physical_source != nullptr && store_end > references.site) |
| 1415 | { | ||
| 1416 | // The slot address is the store's own disp32 resolved against its next-IP, so the store is the | ||
| 1417 | // evidence this mode returns; the reference only selects which store. The forward walk decoded | ||
| 1418 | // every instruction from the reference to it, so the run between them is evidence too and the | ||
| 1419 | // union is one contiguous extent rather than two disjoint spans. | ||
| 1420 | 1 | *physical_source = Region{Address{references.site}, store_end - references.site}; | |
| 1421 | } | ||
| 1422 | 4 | return Address{slot}; | |
| 1423 | } | ||
| 1424 | |||
| 1425 |
2/2✓ Branch 127 → 128 taken 16 times.
✓ Branch 127 → 136 taken 229 times.
|
245 | if (query.return_mode == XrefReturn::EnclosingFunction) |
| 1426 | { | ||
| 1427 | 16 | const std::uintptr_t function_start = enclosing_function_start(references.site, range.base); | |
| 1428 |
2/2✓ Branch 129 → 130 taken 6 times.
✓ Branch 129 → 133 taken 10 times.
|
16 | if (function_start == 0) |
| 1429 | { | ||
| 1430 | 6 | return std::unexpected(Error{ErrorCode::FunctionNotFound, "scan::find_string_xref"}); | |
| 1431 | } | ||
| 1432 | 10 | return Address{function_start}; | |
| 1433 | } | ||
| 1434 | 229 | return Address{references.site}; | |
| 1435 | 729 | } | |
| 1436 | |||
| 1437 | } // namespace | ||
| 1438 | |||
| 1439 | 716 | Result<Address> find_string_xref(const StringRefQuery &query, Region scope) | |
| 1440 | { | ||
| 1441 |
1/2✓ Branch 3 → 4 taken 716 times.
✗ Branch 3 → 7 not taken.
|
716 | return resolve_string_xref(query, scope, nullptr, {}); |
| 1442 | } | ||
| 1443 | } // namespace scan | ||
| 1444 | |||
| 1445 | 10 | Result<Address> detail::find_string_xref_with_exclusions( | |
| 1446 | const scan::StringRefQuery &query, | ||
| 1447 | Region scope, | ||
| 1448 | const ScanExclusions *exclusions, | ||
| 1449 | std::span<const Region> declared_exclusions, | ||
| 1450 | Region *physical_source | ||
| 1451 | ) | ||
| 1452 | { | ||
| 1453 | 10 | return scan::resolve_string_xref(query, scope, exclusions, declared_exclusions, physical_source); | |
| 1454 | } | ||
| 1455 | |||
| 1456 | Result<Address> | ||
| 1457 | 10 | detail::find_string_xref_with_provenance(const scan::StringRefQuery &query, Region scope, Region &physical_source) | |
| 1458 | { | ||
| 1459 |
1/2✓ Branch 3 → 4 taken 10 times.
✗ Branch 3 → 7 not taken.
|
10 | return scan::resolve_string_xref(query, scope, nullptr, {}, &physical_source); |
| 1460 | } | ||
| 1461 | } // namespace DetourModKit | ||
| 1462 |