GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 93.9% 245 / 0 / 261
Functions: 100.0% 15 / 0 / 15
Branches: 80.4% 172 / 0 / 214

src/string_xref.cpp
Line Branch Exec Source
1 /**
2 * @file string_xref.cpp
3 * @brief String-reference (xref) anchor backend: locate an immutable string literal in a module image, then resolve the
4 * unique instruction that references it.
5 *
6 * Two fail-closed phases. Phase 1 locates the single occurrence of the query string in the image's readable pages
7 * (reusing the scanner's module-scoped readable scan). Phase 2 finds the single RIP-relative reference to that string:
8 * a fast, desync-immune shape scan for the dominant lea/mov forms by default, or that same shape scan plus a
9 * Zydis-verified linear sweep (broad_match) that also recognizes the rarer shapes (cmp/push [rip+d], no-REX lea/mov,
10 * ...).
11 *
12 * Zydis is confined to this translation unit, exactly as code_constant.cpp confines
13 * it: no public DetourModKit header exposes a Zydis type, so an installed-package
14 * consumer links DetourModKit (which already links Zydis statically) without ever needing Zydis headers on its own
15 * include path. Keeping the broad sweep here also keeps scanner.cpp -- the core scan engine -- free of the decoder.
16 */
17
18 #include "DetourModKit/scanner.hpp"
19 #include "DetourModKit/memory.hpp"
20 #include "DetourModKit/logger.hpp"
21 #include "scanner_internal.hpp"
22 #include "memory_internal.hpp"
23
24 #include <windows.h>
25 #include <Zydis/Zydis.h>
26
27 #include <array>
28 #include <cstddef>
29 #include <cstdint>
30 #include <cstring>
31 #include <optional>
32 #include <string>
33
34 namespace DetourModKit
35 {
36 namespace
37 {
38 struct ReferenceScanResult
39 {
40 std::uintptr_t site = 0;
41 std::size_t count = 0;
42 };
43
44 // The matched narrow reference's lea destination register and instruction length, recovered alongside the
45 // unique-site search so the store-xref forward scan needs no second decode of the reference. Valid only when
46 // the unique reference is a REX.W `lea reg, [rip+string]` (is_lea == true); a `mov reg, [rip+string]` load
47 // already delivered the value to a register, so there is no store to model from the lea. reg is the 4-bit
48 // x86-64 register number (REX.R << 3 | ModRM.reg).
49 struct LeaReferenceInfo
50 {
51 std::uint8_t reg = 0;
52 std::size_t instr_len = 0;
53 std::uintptr_t window_end = 0;
54 bool is_lea = false;
55 };
56
57 1256 void merge_reference_scan(ReferenceScanResult &result, std::uintptr_t site, std::size_t count) noexcept
58 {
59
2/2
✓ Branch 2 → 3 taken 16 times.
✓ Branch 2 → 4 taken 1240 times.
1256 if (count == 0)
60 {
61 16 return;
62 }
63
2/2
✓ Branch 4 → 5 taken 5 times.
✓ Branch 4 → 6 taken 1235 times.
1240 if (count >= 2)
64 {
65 5 result.count = 2;
66 5 result.site = 0;
67 5 return;
68 }
69
2/2
✓ Branch 6 → 7 taken 632 times.
✓ Branch 6 → 8 taken 603 times.
1235 if (result.count == 0)
70 {
71 632 result.site = site;
72 632 result.count = 1;
73 632 return;
74 }
75
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 603 times.
603 if (result.site != site)
76 {
77 result.site = 0;
78 result.count = 2;
79 }
80 }
81
82 // Builds a literal-byte CompiledPattern from a string query: the raw bytes of the text (UTF-8, or each code
83 // unit widened to UTF-16LE) plus an optional trailing NUL. The query content is emitted as a hex AOB and run
84 // through parse_aob so the string scan reuses the exact same compiled-pattern path as every other AOB. Returns
85 // nullopt on an empty query. Non-ASCII UTF-16 is out of scope: each byte is widened verbatim (Latin-1), which
86 // covers the ASCII identifiers that anchor strings almost always are.
87 652 std::optional<Scanner::CompiledPattern> compile_string_pattern(const Scanner::StringRefQuery &query)
88 {
89
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 652 times.
652 if (query.text.empty())
90 {
91 return std::nullopt;
92 }
93 652 const bool wide = (query.encoding == Scanner::StringEncoding::Utf16le);
94 652 std::string aob;
95
3/4
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 651 times.
✓ Branch 10 → 11 taken 652 times.
✗ Branch 10 → 28 not taken.
652 aob.reserve(query.text.size() * (wide ? 6 : 3) + 6);
96 13079 const auto emit = [&aob](std::uint8_t byte)
97 {
98 static constexpr char hex_digits[] = "0123456789ABCDEF";
99 13079 aob.push_back(hex_digits[byte >> 4]);
100 13079 aob.push_back(hex_digits[byte & 0x0F]);
101 13079 aob.push_back(' ');
102 13731 };
103
2/2
✓ Branch 17 → 13 taken 12419 times.
✓ Branch 17 → 18 taken 652 times.
13071 for (const char ch : query.text)
104 {
105
1/2
✓ Branch 13 → 14 taken 12419 times.
✗ Branch 13 → 28 not taken.
12419 emit(static_cast<std::uint8_t>(ch));
106
2/2
✓ Branch 14 → 15 taken 8 times.
✓ Branch 14 → 16 taken 12411 times.
12419 if (wide)
107 {
108 // High byte of a Latin-1 code unit in UTF-16LE.
109
1/2
✓ Branch 15 → 16 taken 8 times.
✗ Branch 15 → 28 not taken.
8 emit(0x00);
110 }
111 }
112
2/2
✓ Branch 18 → 19 taken 651 times.
✓ Branch 18 → 22 taken 1 time.
652 if (query.require_terminator)
113 {
114
1/2
✓ Branch 19 → 20 taken 651 times.
✗ Branch 19 → 28 not taken.
651 emit(0x00);
115
2/2
✓ Branch 20 → 21 taken 1 time.
✓ Branch 20 → 22 taken 650 times.
651 if (wide)
116 {
117
1/2
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 28 not taken.
1 emit(0x00);
118 }
119 }
120
1/2
✓ Branch 23 → 24 taken 652 times.
✗ Branch 23 → 28 not taken.
652 return Scanner::parse_aob(aob);
121 652 }
122
123 // Best-effort diagnosis for executable windows skipped because they faulted mid-scan. A module image is rarely
124 // decommitted under a live resolve, but collect_executable_windows only proves readability at gate time, so the
125 // window scans below guard their reads and a faulted window is skipped, not fatal. try_log is level-gated and
126 // no-throw.
127 1256 void log_faulted_windows(std::size_t faulted_windows) noexcept
128 {
129
2/2
✓ Branch 2 → 3 taken 794 times.
✓ Branch 2 → 4 taken 462 times.
1256 if (faulted_windows == 0)
130 {
131 794 return;
132 }
133 462 (void)Logger::get_instance().try_log(
134 LogLevel::Debug,
135 "Scanner::find_string_xref: skipped {} executable window(s) that faulted mid-scan (concurrent "
136 "decommit/reprotect).",
137 faulted_windows);
138 }
139
140 // Inner narrow scan of one already-gated executable window (no fault guard). Mutates found_count / first_site,
141 // returning once a second referencing site is seen (found_count == 2) so the caller fails closed on ambiguity.
142 // The recognized instruction shape is documented on scan_string_ref_narrow.
143 916 void scan_window_narrow_body(const Scanner::detail::ExecutableWindow &window, std::uintptr_t string_addr,
144 std::size_t instr_len, std::size_t &found_count, std::uintptr_t &first_site,
145 LeaReferenceInfo *info) noexcept
146 {
147 916 const auto *bytes = reinterpret_cast<const std::uint8_t *>(window.base);
148
2/2
✓ Branch 18 → 3 taken 2706287 times.
✓ Branch 18 → 19 taken 912 times.
2707199 for (std::size_t i = 0; i + instr_len <= window.span; ++i)
149 {
150 2706287 const std::uint8_t rex = bytes[i];
151 2706287 const std::uint8_t opcode = bytes[i + 1];
152 2706287 const std::uint8_t modrm = bytes[i + 2];
153
10/10
✓ Branch 3 → 4 taken 2463762 times.
✓ Branch 3 → 8 taken 242525 times.
✓ Branch 4 → 5 taken 656 times.
✓ Branch 4 → 8 taken 2463106 times.
✓ Branch 5 → 6 taken 23 times.
✓ Branch 5 → 7 taken 633 times.
✓ Branch 6 → 7 taken 5 times.
✓ Branch 6 → 8 taken 18 times.
✓ Branch 7 → 8 taken 3 times.
✓ Branch 7 → 9 taken 635 times.
2706287 if (rex < 0x48 || rex > 0x4F || (opcode != 0x8D && opcode != 0x8B) || (modrm & 0xC7) != 0x05)
154 {
155 2705652 continue;
156 }
157 635 std::int32_t disp = 0;
158 635 std::memcpy(&disp, &bytes[i + 3], sizeof(disp));
159 635 const std::uintptr_t instr_addr = window.base + i;
160 635 const std::uintptr_t target =
161 635 instr_addr + instr_len + static_cast<std::uintptr_t>(static_cast<std::int64_t>(disp));
162
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 635 times.
635 if (target != string_addr)
163 {
164 continue;
165 }
166 635 ++found_count;
167
2/2
✓ Branch 11 → 12 taken 631 times.
✓ Branch 11 → 14 taken 4 times.
635 if (found_count == 1)
168 {
169 631 first_site = instr_addr;
170
1/2
✓ Branch 12 → 13 taken 631 times.
✗ Branch 12 → 15 not taken.
631 if (info != nullptr)
171 {
172 // REX.R (bit 2 of the REX byte) is the high bit of the ModRM.reg field; the narrow shape
173 // accepts REX in 0x48..0x4F, so REX.R may be set for an r8..r15 destination. opcode 0x8D is lea
174 // (a load whose pointer a following store can cache); 0x8B is a mov load with no such store to
175 // model.
176 631 const std::uint8_t reg = static_cast<std::uint8_t>(((rex & 0x04) << 1) | ((modrm >> 3) & 0x07));
177 631 *info = LeaReferenceInfo{reg, instr_len, window.base + window.span, opcode == 0x8D};
178 }
179 }
180 else
181 {
182 // Ambiguous; caller maps found_count >= 2 to AmbiguousReference.
183 4 return;
184 }
185 }
186 }
187
188 // Window-granular TOCTOU fault guard around scan_window_narrow_body. collect_executable_windows gated each
189 // window with one VirtualQuery; a concurrent decommit / reprotect before these unguarded byte reads complete
190 // would otherwise fault the host. On MSVC the body runs inside a __try / __except that swallows exactly the
191 // foreign-read faults (Memory::detail::is_guarded_read_fault) and reports the window faulted so the sweep skips
192 // it. On MinGW x64 the body runs through the same process-wide vectored read guard the seh_read paths use
193 // (Memory::detail::run_guarded_region), so the fault is swallowed and the window skipped + counted there too;
194 // only on 32-bit MinGW, where that x64-only vectored guard is unavailable, does the body run directly behind
195 // just the VirtualQuery gate. Mirrors scan_region_guarded in scanner.cpp. Returns true when a fault was
196 // swallowed.
197 916 bool scan_window_narrow_guarded(const Scanner::detail::ExecutableWindow &window, std::uintptr_t string_addr,
198 std::size_t instr_len, std::size_t &found_count, std::uintptr_t &first_site,
199 LeaReferenceInfo *info) noexcept
200 {
201 #ifdef _MSC_VER
202 const std::size_t original_found_count = found_count;
203 const std::uintptr_t original_first_site = first_site;
204 const LeaReferenceInfo original_info = (info != nullptr) ? *info : LeaReferenceInfo{};
205 __try
206 {
207 scan_window_narrow_body(window, string_addr, instr_len, found_count, first_site, info);
208 return false;
209 }
210 __except (Memory::detail::is_guarded_read_fault(GetExceptionCode()) ? EXCEPTION_EXECUTE_HANDLER
211 : EXCEPTION_CONTINUE_SEARCH)
212 {
213 // The caller skips faulted windows, so discard any reference count (and recovered lea info) collected
214 // before the fault, or a partially-scanned window could leak a stale site/register.
215 found_count = original_found_count;
216 first_site = original_first_site;
217 if (info != nullptr)
218 {
219 *info = original_info;
220 }
221 return true;
222 }
223 #elif defined(_WIN64)
224 // MinGW x64: arm the process-wide vectored read guard over exactly the bytes the window gate proved
225 // readable, so a concurrent decommit / reprotect that faults the scan is swallowed and the window reported
226 // faulted -- the same skip-the-window contract the MSVC __except arm and scanner.cpp's scan_region_guarded
227 // follow.
228 916 const std::size_t original_found_count = found_count;
229 916 const std::uintptr_t original_first_site = first_site;
230
1/2
✓ Branch 2 → 3 taken 916 times.
✗ Branch 2 → 4 not taken.
916 const LeaReferenceInfo original_info = (info != nullptr) ? *info : LeaReferenceInfo{};
231 struct NarrowScanContext
232 {
233 const Scanner::detail::ExecutableWindow *window;
234 std::uintptr_t string_addr;
235 std::size_t instr_len;
236 std::size_t *found_count;
237 std::uintptr_t *first_site;
238 LeaReferenceInfo *info;
239 916 } scan_ctx{&window, string_addr, instr_len, &found_count, &first_site, info};
240
241 916 const auto run_scan = [](void *opaque) noexcept -> void
242 {
243 916 auto *context = static_cast<NarrowScanContext *>(opaque);
244 916 scan_window_narrow_body(*context->window, context->string_addr, context->instr_len,
245 916 *context->found_count, *context->first_site, context->info);
246 654 };
247
248
2/2
✓ Branch 7 → 8 taken 654 times.
✓ Branch 7 → 9 taken 262 times.
916 if (Memory::detail::run_guarded_region(window.base, window.base + window.span, run_scan, &scan_ctx))
249 {
250 654 return false;
251 }
252 // Faulted: discard any partial count / site / lea info so a partially-scanned window cannot leak a stale
253 // site or register, exactly as the MSVC arm does.
254 262 found_count = original_found_count;
255 262 first_site = original_first_site;
256
1/2
✓ Branch 9 → 10 taken 262 times.
✗ Branch 9 → 11 not taken.
262 if (info != nullptr)
257 {
258 262 *info = original_info;
259 }
260 262 return true;
261 #else
262 scan_window_narrow_body(window, string_addr, instr_len, found_count, first_site, info);
263 return false;
264 #endif
265 }
266
267 // Phase 2 (default, "narrow") of find_string_xref: scan the image's execute-readable windows for the dominant
268 // 64-bit string-load forms whose resolved absolute target is string_addr.
269 //
270 // Recognizes a mandatory REX.W prefix (0x48..0x4F), opcode 8D (lea) or 8B (mov), and a ModRM byte in the
271 // RIP-relative form -- mod == 00b and rm == 101b, i.e. (modrm & 0xC7) == 0x05 -- followed by a 4-byte
272 // displacement. That required REX.W byte makes the instruction exactly 7 bytes and the disp32 sits at offset 3,
273 // so the candidate is self-delimiting from its shape: this needs no instruction-aligned linear sweep and
274 // therefore cannot desync on data or jump tables embedded in .text.
275 //
276 // The resolved target is next-instruction-address + sign-extended disp32, done in unsigned modular arithmetic
277 // so it is well-defined for every input. Only a target that exactly equals string_addr is accepted, which is an
278 // extremely strong filter: string_addr is the already-located, plausible
279 // .rdata address, so this equality subsumes the plausible_userspace_ptr floor (an implausible target cannot
280 // equal it) and a coincidental byte sequence resolving to precisely string_addr is not a realistic false
281 // positive. Counting stops at the second hit so the caller can fail closed on ambiguity.
282 644 std::uintptr_t scan_string_ref_narrow(std::uintptr_t string_addr, Memory::ModuleRange range,
283 std::size_t &found_count, LeaReferenceInfo &info)
284 {
285 644 found_count = 0;
286 644 std::uintptr_t first_site = 0;
287 644 info = LeaReferenceInfo{};
288 // REX.W + opcode + ModRM + disp32.
289 644 constexpr std::size_t instr_len = 7;
290 // scan_window_narrow_body reads bytes[i], bytes[i+1], bytes[i+2] and a disp32 at bytes[i+3..i+6], so the
291 // highest index it touches is i+6. The per-window loop only bounds i + instr_len <= span, so instr_len
292 // must cover that widest read or the disp32 fetch could run up to four bytes past the window. Pin the
293 // coupling here, beside the shape's byte count, so a future instr_len change cannot silently reopen it.
294 644 constexpr std::size_t narrow_max_read_index = 6;
295 static_assert(narrow_max_read_index < instr_len,
296 "instr_len must span scan_window_narrow_body's disp32 tail read at bytes[i+3..i+6]");
297 644 std::size_t faulted_windows = 0;
298
299
3/4
✓ Branch 2 → 3 taken 644 times.
✗ Branch 2 → 37 not taken.
✓ Branch 24 → 5 taken 916 times.
✓ Branch 24 → 25 taken 640 times.
2200 for (const auto &window : Scanner::detail::collect_executable_windows(range))
300 {
301
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 916 times.
916 if (window.span < instr_len)
302 {
303 continue;
304 }
305
2/2
✓ Branch 10 → 11 taken 262 times.
✓ Branch 10 → 12 taken 654 times.
916 if (scan_window_narrow_guarded(window, string_addr, instr_len, found_count, first_site, &info))
306 {
307 262 ++faulted_windows;
308 262 continue;
309 }
310
2/2
✓ Branch 12 → 13 taken 4 times.
✓ Branch 12 → 15 taken 650 times.
654 if (found_count >= 2)
311 {
312 // Ambiguous; caller maps found_count >= 2 to AmbiguousReference.
313 4 log_faulted_windows(faulted_windows);
314 4 return 0;
315 }
316
2/2
✓ Branch 27 → 28 taken 640 times.
✓ Branch 27 → 31 taken 4 times.
644 }
317 640 log_faulted_windows(faulted_windows);
318
2/2
✓ Branch 30 → 32 taken 627 times.
✓ Branch 30 → 33 taken 13 times.
640 return (found_count == 1) ? first_site : 0;
319 }
320
321 // Store-xref forward scan: starting just past a `lea reg, [rip+string]`, decode forward instruction by
322 // instruction (Zydis, the same decoder scan_string_ref_broad uses) and return the slot a `mov [rip+slot], reg`
323 // store caches the loaded pointer into, where reg is the lea destination. Decoding rather than a raw byte sweep
324 // keeps the match instruction-aligned -- a `48 89 05 ...` byte run buried inside another instruction's
325 // immediate or displacement is never mistaken for a store -- and lets the scan stop the moment the loaded
326 // register is overwritten, since a store after that would cache a different value, not this lea's pointer. The
327 // store shape is REX.W MOV [rip+disp32], reg64: a RIP-relative memory destination plus the matching 64-bit
328 // source register (the mirror of the REX.W load). The match is first-within-window, not uniqueness-checked, so
329 // the window is kept tight. Returns 0 (the caller maps it to StoreNotFound) on no store, a write to the loaded
330 // register, a CALL (which clobbers the caller-saved set), a decode failure (alignment lost / non-code), an
331 // unreadable byte, or the bound being hit. Reads go through Memory::seh_read_bytes so a truncated or unmapped
332 // tail cannot fault the host. The scan is bounded to the lea's own executable window (window_end), the module
333 // range, and a small forward window.
334 8 std::uintptr_t scan_store_slot_after_lea(std::uintptr_t lea_site, std::size_t lea_len,
335 std::uintptr_t window_end, std::uint8_t lea_reg,
336 Memory::ModuleRange range) noexcept
337 {
338 // A cached pointer is stored very close to its load; bound the forward scan so a pathological region cannot
339 // scan unboundedly and the store cannot be attributed to a distant, unrelated reuse of the same register.
340 8 constexpr std::size_t forward_window = 0x80; // 128 bytes.
341 8 const std::uintptr_t scan_lo = lea_site + lea_len;
342
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 8 times.
8 if (scan_lo < lea_site)
343 {
344 return 0;
345 }
346
2/2
✓ Branch 4 → 5 taken 7 times.
✓ Branch 4 → 6 taken 1 time.
8 const std::uintptr_t scan_end = (window_end < range.end) ? window_end : range.end;
347
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 8 times.
8 if (scan_lo >= scan_end)
348 {
349 return 0;
350 }
351
2/2
✓ Branch 9 → 10 taken 7 times.
✓ Branch 9 → 11 taken 1 time.
8 const std::uintptr_t scan_hi = (scan_end - scan_lo < forward_window) ? scan_end : scan_lo + forward_window;
352
353 ZydisDecoder decoder;
354
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 8 times.
8 if (!ZYAN_SUCCESS(ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64)))
355 {
356 return 0;
357 }
358 // The lea wrote a 64-bit pointer, so the store source and any clobber are tested against the full 64-bit
359 // register. ZydisRegisterEncode maps the x86 register number (REX.R << 3 | ModRM.reg) to the GPR64
360 // register.
361 8 const ZydisRegister target_reg = ZydisRegisterEncode(ZYDIS_REGCLASS_GPR64, lea_reg);
362
1/2
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 8 times.
8 if (target_reg == ZYDIS_REGISTER_NONE)
363 {
364 return 0;
365 }
366
367 8 std::uintptr_t p = scan_lo;
368
2/2
✓ Branch 57 → 19 taken 266 times.
✓ Branch 57 → 58 taken 3 times.
269 while (p < scan_hi)
369 {
370 266 std::array<std::uint8_t, ZYDIS_MAX_INSTRUCTION_LENGTH> code{};
371 266 const std::size_t avail =
372 266 (scan_hi - p < code.size()) ? static_cast<std::size_t>(scan_hi - p) : code.size();
373
1/2
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 266 times.
266 if (!Memory::seh_read_bytes(p, code.data(), avail))
374 {
375 // Unreadable byte: the store sits in the same execute-readable window as the lea, so a fault here
376 // means it is not present in mapped code.
377 5 return 0;
378 }
379
380 ZydisDecodedInstruction insn;
381 ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT];
382
2/2
✓ Branch 29 → 30 taken 2 times.
✓ Branch 29 → 31 taken 264 times.
266 if (!ZYAN_SUCCESS(ZydisDecoderDecodeFull(&decoder, code.data(), avail, &insn, operands)))
383 {
384 // The store must be instruction-aligned with the lea; a decode failure means alignment was lost or
385 // the bytes are not code. Fail closed rather than accept a misaligned match.
386 2 return 0;
387 }
388
389 // The store: MOV with a RIP-relative memory destination (operand 0) and the matching 64-bit source
390 // register (operand 1). ZydisCalcAbsoluteAddress turns the destination into the slot's effective
391 // address (next-instruction address + sign-extended disp32).
392
3/4
✓ Branch 31 → 32 taken 5 times.
✓ Branch 31 → 42 taken 259 times.
✓ Branch 32 → 33 taken 5 times.
✗ Branch 32 → 42 not taken.
264 if (insn.mnemonic == ZYDIS_MNEMONIC_MOV && insn.operand_count_visible >= 2 &&
393
3/4
✓ Branch 33 → 34 taken 3 times.
✓ Branch 33 → 42 taken 2 times.
✓ Branch 34 → 35 taken 3 times.
✗ Branch 34 → 42 not taken.
5 operands[0].type == ZYDIS_OPERAND_TYPE_MEMORY && operands[0].mem.base == ZYDIS_REGISTER_RIP &&
394
3/4
✓ Branch 35 → 36 taken 3 times.
✗ Branch 35 → 42 not taken.
✓ Branch 36 → 37 taken 2 times.
✓ Branch 36 → 42 taken 1 time.
3 operands[1].type == ZYDIS_OPERAND_TYPE_REGISTER && operands[1].reg.value == target_reg)
395 {
396 2 ZyanU64 absolute = 0;
397
1/2
✓ Branch 38 → 39 taken 2 times.
✗ Branch 38 → 40 not taken.
2 if (ZYAN_SUCCESS(ZydisCalcAbsoluteAddress(&insn, &operands[0], static_cast<ZyanU64>(p), &absolute)))
398 {
399 2 return static_cast<std::uintptr_t>(absolute);
400 }
401 return 0;
402 }
403
404 // A CALL clobbers the caller-saved registers; a store after it cannot be trusted to cache this lea's
405 // pointer, so stop conservatively rather than attribute a post-call store to the wrong value.
406
1/2
✗ Branch 42 → 43 not taken.
✓ Branch 42 → 44 taken 262 times.
262 if (insn.meta.category == ZYDIS_CATEGORY_CALL)
407 {
408 return 0;
409 }
410 // Any write to the loaded register (at any width -- a 32-bit write zeroes the upper half) means a later
411 // store would cache a different value. Check every operand, including implicit ones.
412
2/2
✓ Branch 54 → 45 taken 782 times.
✓ Branch 54 → 55 taken 261 times.
1043 for (std::size_t op = 0; op < insn.operand_count; ++op)
413 {
414 782 const ZydisDecodedOperand &operand = operands[op];
415 2085 if (operand.type == ZYDIS_OPERAND_TYPE_REGISTER &&
416
6/6
✓ Branch 45 → 46 taken 521 times.
✓ Branch 45 → 50 taken 261 times.
✓ Branch 46 → 47 taken 261 times.
✓ Branch 46 → 50 taken 260 times.
✓ Branch 51 → 52 taken 1 time.
✓ Branch 51 → 53 taken 781 times.
1043 (operand.actions & ZYDIS_OPERAND_ACTION_MASK_WRITE) != 0 &&
417
2/2
✓ Branch 48 → 49 taken 1 time.
✓ Branch 48 → 50 taken 260 times.
261 ZydisRegisterGetLargestEnclosing(ZYDIS_MACHINE_MODE_LONG_64, operand.reg.value) == target_reg)
418 {
419 1 return 0;
420 }
421 }
422
423 261 p += insn.length;
424 }
425 3 return 0;
426 }
427
428 // Inner broad scan of one already-gated executable window (no fault guard). Decodes each position with Zydis,
429 // counting RIP-relative operands whose absolute target equals string_addr; mutates found_count / first_site and
430 // returns once a second referencing site is seen. The decode/recovery contract is documented on
431 // scan_string_ref_broad.
432 814 void scan_window_broad_body(const ZydisDecoder &decoder, const Scanner::detail::ExecutableWindow &window,
433 std::uintptr_t string_addr, std::size_t &found_count,
434 std::uintptr_t &first_site) noexcept
435 {
436 814 const auto *bytes = reinterpret_cast<const std::uint8_t *>(window.base);
437 814 std::size_t offset = 0;
438
2/2
✓ Branch 26 → 3 taken 2477584 times.
✓ Branch 26 → 27 taken 613 times.
2478197 while (offset < window.span)
439 {
440 ZydisDecodedInstruction insn;
441 ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT];
442 2477584 const std::uintptr_t instr_addr = window.base + offset;
443
2/2
✓ Branch 4 → 5 taken 5 times.
✓ Branch 4 → 6 taken 2477379 times.
2477584 if (!ZYAN_SUCCESS(
444 ZydisDecoderDecodeFull(&decoder, bytes + offset, window.span - offset, &insn, operands)))
445 {
446 // Byte-restart recovery: realign past data / jump tables.
447 5 ++offset;
448 5 continue;
449 }
450
451 // A referencing instruction has a visible memory operand based on
452 // RIP whose absolute target is the string. Visible operands are ordered first in the array, so
453 // iterating the visible count covers every explicit operand a disassembler would show.
454 2477379 bool references_string = false;
455
2/2
✓ Branch 19 → 7 taken 52016 times.
✓ Branch 19 → 20 taken 2476769 times.
2528785 for (std::size_t op = 0; op < insn.operand_count_visible; ++op)
456 {
457 52016 const ZydisDecodedOperand &operand = operands[op];
458
4/4
✓ Branch 7 → 8 taken 24207 times.
✓ Branch 7 → 9 taken 27809 times.
✓ Branch 8 → 9 taken 23597 times.
✓ Branch 8 → 10 taken 610 times.
52016 if (operand.type != ZYDIS_OPERAND_TYPE_MEMORY || operand.mem.base != ZYDIS_REGISTER_RIP)
459 {
460 51406 continue;
461 }
462 610 ZyanU64 absolute = 0;
463 610 if (ZYAN_SUCCESS(
464
2/4
✓ Branch 11 → 12 taken 610 times.
✗ Branch 11 → 14 not taken.
✓ Branch 15 → 16 taken 610 times.
✗ Branch 15 → 17 not taken.
1220 ZydisCalcAbsoluteAddress(&insn, &operand, static_cast<ZyanU64>(instr_addr), &absolute)) &&
465
1/2
✓ Branch 12 → 13 taken 610 times.
✗ Branch 12 → 14 not taken.
610 static_cast<std::uintptr_t>(absolute) == string_addr)
466 {
467 610 references_string = true;
468 610 break;
469 }
470 }
471
472
2/2
✓ Branch 20 → 21 taken 610 times.
✓ Branch 20 → 24 taken 2476769 times.
2477379 if (references_string)
473 {
474 610 ++found_count;
475
2/2
✓ Branch 21 → 22 taken 609 times.
✓ Branch 21 → 23 taken 1 time.
610 if (found_count == 1)
476 {
477 609 first_site = instr_addr;
478 }
479 else
480 {
481 // Ambiguous; caller maps found_count >= 2 to AmbiguousReference.
482 1 return;
483 }
484 }
485
486 2477378 offset += insn.length;
487 }
488 }
489
490 // Window-granular TOCTOU fault guard around scan_window_broad_body; the narrow sibling
491 // scan_window_narrow_guarded documents the rationale. Returns true when a fault was swallowed.
492 814 bool scan_window_broad_guarded(const ZydisDecoder &decoder, const Scanner::detail::ExecutableWindow &window,
493 std::uintptr_t string_addr, std::size_t &found_count,
494 std::uintptr_t &first_site) noexcept
495 {
496 #ifdef _MSC_VER
497 const std::size_t original_found_count = found_count;
498 const std::uintptr_t original_first_site = first_site;
499 __try
500 {
501 scan_window_broad_body(decoder, window, string_addr, found_count, first_site);
502 return false;
503 }
504 __except (Memory::detail::is_guarded_read_fault(GetExceptionCode()) ? EXCEPTION_EXECUTE_HANDLER
505 : EXCEPTION_CONTINUE_SEARCH)
506 {
507 // The caller skips faulted windows, so discard any reference count collected before the fault.
508 found_count = original_found_count;
509 first_site = original_first_site;
510 return true;
511 }
512 #elif defined(_WIN64)
513 // MinGW x64: same vectored read guard as the narrow sibling, armed over the gated window bytes; a fault is
514 // swallowed and the window reported faulted. Only 32-bit MinGW falls back to the bare VirtualQuery gate.
515 814 const std::size_t original_found_count = found_count;
516 814 const std::uintptr_t original_first_site = first_site;
517 struct BroadScanContext
518 {
519 const ZydisDecoder *decoder;
520 const Scanner::detail::ExecutableWindow *window;
521 std::uintptr_t string_addr;
522 std::size_t *found_count;
523 std::uintptr_t *first_site;
524 814 } scan_ctx{&decoder, &window, string_addr, &found_count, &first_site};
525
526 814 const auto run_scan = [](void *opaque) noexcept -> void
527 {
528 814 auto *context = static_cast<BroadScanContext *>(opaque);
529 814 scan_window_broad_body(*context->decoder, *context->window, context->string_addr, *context->found_count,
530 814 *context->first_site);
531 614 };
532
533
2/2
✓ Branch 4 → 5 taken 614 times.
✓ Branch 4 → 6 taken 200 times.
814 if (Memory::detail::run_guarded_region(window.base, window.base + window.span, run_scan, &scan_ctx))
534 {
535 614 return false;
536 }
537 // Faulted: discard any partial count / site so a partially-scanned window cannot leak a stale site.
538 200 found_count = original_found_count;
539 200 first_site = original_first_site;
540 200 return true;
541 #else
542 scan_window_broad_body(decoder, window, string_addr, found_count, first_site);
543 return false;
544 #endif
545 }
546
547 // Phase 2 ("broad") add-on for find_string_xref: a Zydis-verified linear sweep that recognizes the rarer
548 // RIP-relative reference shapes the narrow
549 // scan does not model -- cmp [rip+d], imm; push [rip+d]; a no-REX lea/mov;
550 // any instruction whose memory operand is [rip+disp] and resolves to string_addr. The caller merges this with
551 // the narrow scan so broad_match cannot lose coverage for the default lea/mov anchors.
552 //
553 // The sweep decodes each position with the same decoder code_constant.cpp uses (ZydisDecoderDecodeFull +
554 // ZydisCalcAbsoluteAddress). x86-64 is not self-synchronizing, so on a decode failure the cursor advances one
555 // byte to realign past embedded data or jump tables (recovery); on success it advances by the decoded
556 // instruction length, which is always >= 1 so the sweep cannot stall. As in the narrow scan, only a
557 // RIP-relative operand whose absolute target exactly equals string_addr counts. That exact-target filter
558 // subsumes the plausibility floor and neutralizes a mid-.text desync:
559 // a bogus instruction decoded out of embedded data almost never carries a RIP operand resolving to precisely
560 // string_addr. The one residual miss is a reference truncated at the very end of a window (too few bytes to
561 // decode, so the sweep byte-restarts past it); that cannot happen mid-image, since a
562 // PE's executable section is one contiguous window and a real reference never sits in its final few bytes.
563 // Counting stops at the second referencing instruction so the caller fails closed on ambiguity.
564 612 std::uintptr_t scan_string_ref_broad(std::uintptr_t string_addr, Memory::ModuleRange range,
565 std::size_t &found_count)
566 {
567 612 found_count = 0;
568 612 std::uintptr_t first_site = 0;
569
570 ZydisDecoder decoder;
571
2/4
✓ Branch 2 → 3 taken 612 times.
✗ Branch 2 → 38 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 612 times.
612 if (!ZYAN_SUCCESS(ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64)))
572 {
573 // A decoder that will not initialize cannot verify any reference;
574 // fail closed as "no reference" rather than guess a site.
575 return 0;
576 }
577
578 612 std::size_t faulted_windows = 0;
579
3/4
✓ Branch 5 → 6 taken 612 times.
✗ Branch 5 → 37 not taken.
✓ Branch 25 → 8 taken 814 times.
✓ Branch 25 → 26 taken 611 times.
2037 for (const auto &window : Scanner::detail::collect_executable_windows(range))
580 {
581
2/2
✓ Branch 11 → 12 taken 200 times.
✓ Branch 11 → 13 taken 614 times.
814 if (scan_window_broad_guarded(decoder, window, string_addr, found_count, first_site))
582 {
583 200 ++faulted_windows;
584 200 continue;
585 }
586
2/2
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 16 taken 613 times.
614 if (found_count >= 2)
587 {
588 // Ambiguous; caller maps found_count >= 2 to AmbiguousReference.
589 1 log_faulted_windows(faulted_windows);
590 1 return 0;
591 }
592 }
593 611 log_faulted_windows(faulted_windows);
594
2/2
✓ Branch 30 → 32 taken 608 times.
✓ Branch 30 → 33 taken 3 times.
611 return (found_count == 1) ? first_site : 0;
595 }
596
597 // Best-effort enclosing-function entry for a referencing instruction. Walks backward for the nearest function
598 // boundary -- a terminal RET (0xC3) or a run of INT3 (0xCC) alignment padding -- and returns the first byte
599 // after it that passes is_likely_function_prologue, skipping any further INT3 padding. The back-scan is bounded
600 // so a pathological region cannot scan unboundedly, and it fails closed (returns 0) when no boundary is found
601 // in the window. This is a heuristic, not control-flow analysis: a 0xC3 / 0xCC byte that is actually operand
602 // data inside an instruction can mark a false boundary, which is why the default return mode is the exact
603 // referencing instruction.
604 4 std::uintptr_t enclosing_function_start(std::uintptr_t instr_addr, std::uintptr_t window_lo) noexcept
605 {
606 4 constexpr std::uintptr_t back_scan_window = 0x2000; // 8 KiB.
607 4 const std::uintptr_t floor =
608
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 3 times.
4 (instr_addr - window_lo > back_scan_window) ? instr_addr - back_scan_window : window_lo;
609
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 4 times.
4 if (instr_addr <= floor)
610 {
611 return 0;
612 }
613
614 // Buffer the back-scan window once instead of issuing a guarded read per byte. The window is read in
615 // page-sized chunks from high address to low: a chunk that faults stops the read exactly where the
616 // byte-at-a-time walk would have faulted going downward, so any boundary in the already-buffered higher
617 // bytes is still found, and a fault below them returns 0 just as the per-byte version did. This replaces up
618 // to ~8 KiB of guarded reads (a VirtualQuery / SEH-frame each on MinGW) with at most a few. window[k] holds
619 // the byte at floor + k; valid_lo is the lowest address actually buffered, so the scan and the forward
620 // INT3-padding skip operate only over the buffered range [valid_lo, instr_addr) (the skip only moves toward
621 // instr_addr, i.e. into already-buffered higher addresses).
622 4 std::array<std::uint8_t, static_cast<std::size_t>(back_scan_window)> window{};
623 4 std::uintptr_t valid_lo = instr_addr;
624 // Windows x86/x64 base page size; seh_read_bytes faults at this granularity. Chunks are page-aligned, not
625 // instr-aligned, so a single guarded read covers at most one page and never straddles a readable/unreadable
626 // page boundary: a fault in a lower page then cannot discard bytes already buffered from a readable higher
627 // page (which is exactly where the byte-at-a-time walk would have found a boundary before reaching the
628 // fault going downward).
629 4 constexpr std::uintptr_t page_size = 0x1000;
630
2/2
✓ Branch 17 → 8 taken 7 times.
✓ Branch 17 → 18 taken 3 times.
10 for (std::uintptr_t hi = instr_addr; hi > floor;)
631 {
632 7 const std::uintptr_t page_lo = (hi - 1) & ~(page_size - 1);
633
2/2
✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 10 taken 4 times.
7 const std::uintptr_t lo = (page_lo > floor) ? page_lo : floor;
634 7 const std::size_t len = static_cast<std::size_t>(hi - lo);
635
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 16 taken 6 times.
7 if (!Memory::seh_read_bytes(lo, window.data() + (lo - floor), len))
636 {
637 // This page is unreadable; stop. The byte-at-a-time walk would fault on the first byte here too,
638 // after exhausting the readable bytes buffered above it.
639 1 break;
640 }
641 6 valid_lo = lo;
642 6 hi = lo;
643 }
644
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 4 times.
4 if (valid_lo >= instr_addr)
645 {
646 // Not even the highest chunk was readable: the first probed byte would have faulted.
647 return 0;
648 }
649
650
2/2
✓ Branch 39 → 21 taken 8615 times.
✓ Branch 39 → 40 taken 1 time.
8616 for (std::uintptr_t probe = instr_addr; probe > valid_lo; --probe)
651 {
652 8615 const std::uint8_t boundary_byte = window[static_cast<std::size_t>(probe - 1 - floor)];
653
3/4
✓ Branch 22 → 23 taken 8612 times.
✓ Branch 22 → 25 taken 3 times.
✓ Branch 23 → 24 taken 8612 times.
✗ Branch 23 → 25 not taken.
8615 if (boundary_byte != 0xCC && boundary_byte != 0xC3)
654 {
655 8612 continue;
656 }
657 // The boundary byte ends the previous function (or its padding); the enclosing function begins at the
658 // first non-INT3 byte after it.
659 3 std::uintptr_t start = probe;
660
3/6
✓ Branch 27 → 28 taken 3 times.
✗ Branch 27 → 31 not taken.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 3 times.
✗ Branch 32 → 26 not taken.
✓ Branch 32 → 33 taken 3 times.
3 while (start < instr_addr && window[static_cast<std::size_t>(start - floor)] == 0xCC)
661 {
662 ++start;
663 }
664
1/2
✓ Branch 34 → 35 taken 3 times.
✗ Branch 34 → 36 not taken.
3 return Scanner::is_likely_function_prologue(start) ? start : 0;
665 }
666 1 return 0;
667 }
668 } // anonymous namespace
669
670 654 std::expected<std::uintptr_t, Scanner::StringXrefError> Scanner::find_string_xref(const StringRefQuery &query,
671 Memory::ModuleRange range)
672 {
673
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 7 taken 653 times.
654 if (query.text.empty())
674 {
675 1 return std::unexpected(StringXrefError::EmptyQuery);
676 }
677
2/2
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 12 taken 652 times.
653 if (!range.valid())
678 {
679 1 return std::unexpected(StringXrefError::InvalidRange);
680 }
681
682 // Phase 1: locate the single occurrence of the string in the image's readable pages. The linker pools identical
683 // literals, so a second occurrence makes the anchor ambiguous and must fail closed.
684
1/2
✓ Branch 12 → 13 taken 652 times.
✗ Branch 12 → 73 not taken.
652 const auto pattern = compile_string_pattern(query);
685
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 18 taken 652 times.
652 if (!pattern)
686 {
687 // Non-empty text that does not compile to a pattern cannot be located;
688 // report not-found rather than guess.
689 return std::unexpected(StringXrefError::StringNotFound);
690 }
691 652 const std::byte *first = detail::scan_module_readable(*pattern, range, 1);
692
2/2
✓ Branch 20 → 21 taken 6 times.
✓ Branch 20 → 24 taken 646 times.
652 if (first == nullptr)
693 {
694 6 return std::unexpected(StringXrefError::StringNotFound);
695 }
696
2/2
✓ Branch 26 → 27 taken 2 times.
✓ Branch 26 → 30 taken 644 times.
646 if (detail::scan_module_readable(*pattern, range, 2) != nullptr)
697 {
698 2 return std::unexpected(StringXrefError::StringAmbiguous);
699 }
700 644 const auto string_addr = reinterpret_cast<std::uintptr_t>(first);
701
702 // Phase 2: find the single RIP-relative reference whose target is the string. The narrow scan is the fast,
703 // desync-immune default; broad_match keeps that coverage and adds a Zydis sweep for rarer reference shapes.
704 644 ReferenceScanResult references{};
705 644 std::size_t narrow_count = 0;
706 644 LeaReferenceInfo lea_info{};
707
1/2
✓ Branch 30 → 31 taken 644 times.
✗ Branch 30 → 71 not taken.
644 const std::uintptr_t narrow_site = scan_string_ref_narrow(string_addr, range, narrow_count, lea_info);
708 644 merge_reference_scan(references, narrow_site, narrow_count);
709
4/4
✓ Branch 32 → 33 taken 613 times.
✓ Branch 32 → 37 taken 31 times.
✓ Branch 33 → 34 taken 612 times.
✓ Branch 33 → 37 taken 1 time.
644 if (query.broad_match && references.count < 2)
710 {
711 612 std::size_t broad_count = 0;
712
1/2
✓ Branch 34 → 35 taken 612 times.
✗ Branch 34 → 70 not taken.
612 const std::uintptr_t broad_site = scan_string_ref_broad(string_addr, range, broad_count);
713 612 merge_reference_scan(references, broad_site, broad_count);
714 }
715
716
2/2
✓ Branch 37 → 38 taken 7 times.
✓ Branch 37 → 41 taken 637 times.
644 if (references.count == 0)
717 {
718 7 return std::unexpected(StringXrefError::NoReference);
719 }
720
2/2
✓ Branch 41 → 42 taken 5 times.
✓ Branch 41 → 45 taken 632 times.
637 if (references.count >= 2)
721 {
722 5 return std::unexpected(StringXrefError::AmbiguousReference);
723 }
724
725
2/2
✓ Branch 45 → 46 taken 10 times.
✓ Branch 45 → 58 taken 622 times.
632 if (query.return_mode == XrefReturn::StringPointerSlot)
726 {
727 // Store-xref needs the unique reference to be the narrow `lea reg, [rip+string]` whose loaded pointer a
728 // following `mov [rip+slot], reg` caches. A broad-only surviving reference (references.site differs from
729 // the narrow site, so lea_info was never populated for it) or a `mov reg, [rip+string]` load has no such
730 // store to attribute. With broad_match false, references.site == narrow_site whenever count == 1, so the
731 // second guard is a no-op there.
732
3/4
✓ Branch 46 → 47 taken 8 times.
✓ Branch 46 → 48 taken 2 times.
✗ Branch 47 → 48 not taken.
✓ Branch 47 → 51 taken 8 times.
10 if (!lea_info.is_lea || references.site != narrow_site)
733 {
734 2 return std::unexpected(StringXrefError::StoreNotFound);
735 }
736 8 const std::uintptr_t slot = scan_store_slot_after_lea(references.site, lea_info.instr_len,
737 8 lea_info.window_end, lea_info.reg, range);
738
2/2
✓ Branch 52 → 53 taken 6 times.
✓ Branch 52 → 56 taken 2 times.
8 if (slot == 0)
739 {
740 6 return std::unexpected(StringXrefError::StoreNotFound);
741 }
742 2 return slot;
743 }
744
745
2/2
✓ Branch 58 → 59 taken 4 times.
✓ Branch 58 → 66 taken 618 times.
622 if (query.return_mode == XrefReturn::EnclosingFunction)
746 {
747 4 const std::uintptr_t function_start = enclosing_function_start(references.site, range.base);
748
2/2
✓ Branch 60 → 61 taken 1 time.
✓ Branch 60 → 64 taken 3 times.
4 if (function_start == 0)
749 {
750 1 return std::unexpected(StringXrefError::FunctionNotFound);
751 }
752 3 return function_start;
753 }
754 618 return references.site;
755 652 }
756 } // namespace DetourModKit
757