./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
473 / 499
94.8%
Branches:
326 / 388
84.0%
Functions:
29 / 29
100.0%

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