GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 94.7% 460 / 0 / 486
Functions: 100.0% 25 / 0 / 25
Branches: 83.0% 317 / 0 / 382

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, &current, 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