GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 99.1% 111 / 0 / 112
Functions: 100.0% 38 / 0 / 38
Branches: 83.1% 59 / 0 / 71

include/DetourModKit/scan.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_SCAN_HPP
2 #define DETOURMODKIT_SCAN_HPP
3
4 /**
5 * @file scan.hpp
6 * @brief The public scan surface: pattern matching, candidate-ladder resolution, and the
7 * standalone RIP-relative, string-xref, code-constant, and export resolvers.
8 * @details `Pattern` compiles an AOB mini-DSL string and holds its bytes and mask inline.
9 * @ref scan locates one `Pattern` in a `Region`. @ref resolve runs an ordered
10 * @ref Candidate ladder until one strategy produces a confident address. The free
11 * resolvers expose a single backend for a caller that holds one piece of evidence.
12 * The resolution note in docs/design/resolution.md owns the mechanism.
13 * @warning `[B-100]` Under the loader lock, call only Callback-safe entry points and supply each required Region from
14 * setup. Ladder construction and resolution can allocate, scan memory, or create threads.
15 */
16
17 #include "DetourModKit/address.hpp"
18 #include "DetourModKit/defines.hpp"
19 #include "DetourModKit/detail/pattern_core.hpp"
20 #include "DetourModKit/error.hpp"
21 #include "DetourModKit/region.hpp"
22
23 #include <array>
24 #include <cstddef>
25 #include <cstdint>
26 #include <span>
27 #include <stdexcept>
28 #include <string>
29 #include <string_view>
30 #include <variant>
31 #include <vector>
32
33 namespace DetourModKit::scan
34 {
35 class Pattern;
36 } // namespace DetourModKit::scan
37
38 namespace DetourModKit::detail
39 {
40 /// Returns @p pattern's immutable compiled buffer for internal scan adapters.
41 [[nodiscard]] constexpr const PatternBuffer &pattern_buffer(const scan::Pattern &pattern) noexcept;
42 } // namespace DetourModKit::detail
43
44 namespace DetourModKit::scan
45 {
46 /**
47 * @class Pattern
48 * @brief A value-semantic compiled AOB pattern that owns its bytes and mask inline.
49 * @details Construct with compile() (runtime, returns Result) or literal() (compile-time, returns by value). The
50 * compiled form exposes its bytes, mask, result offset, and the cached compile-time anchor so the scan
51 * engine can prefilter and verify without re-parsing, and matches_at() applies the same masked compare
52 * the engine uses for a single position. Copyable and trivially comparable in cost to its inline arrays.
53 * @note The compile() / literal() factories are setup/control-plane; size(), the byte/mask/anchor accessors, and
54 * matches_at() are callback-safe (pure value reads with no allocation, I/O, or locking).
55 */
56 class Pattern
57 {
58 public:
59 /**
60 * @brief Compiles a runtime AOB DSL string.
61 * @param dsl The whitespace-separated pattern, e.g. "48 8B 05 ?? ?? ?? ??".
62 * @return A Pattern on success, or Error{ErrorCode::BadPattern} when the string is malformed/empty/over-cap.
63 * @details Never undefined behaviour on bad input: a parse failure becomes a recoverable Error, with the
64 * specific parse status in the Error's extra slot.
65 * @note Setup/control-plane only: compile patterns at init, not inside a hot callback.
66 */
67 399 [[nodiscard]] static Result<Pattern> compile(std::string_view dsl)
68 {
69 399 const detail::PatternParse parsed = detail::parse_pattern(dsl);
70
2/2
✓ Branch 3 → 4 taken 22 times.
✓ Branch 3 → 7 taken 377 times.
399 if (parsed.status != detail::PatternStatus::Ok)
71 {
72 22 return std::unexpected(
73 22 Error{ErrorCode::BadPattern, "scan::compile", 0, static_cast<std::uint32_t>(parsed.status)}
74 22 );
75 }
76 377 return Pattern{parsed.buffer};
77 }
78
79 /**
80 * @brief Compiles an in-source AOB DSL literal at compile time.
81 * @param dsl A constant-expression pattern string.
82 * @return The compiled Pattern by value.
83 * @details consteval, so a malformed literal causes a compile error at that literal. The throw below becomes a
84 * non-constant expression during constant evaluation, instead of a runtime Result to deref.
85 * @note Compile-time only: consteval, so it runs during compilation and has no runtime call site to classify.
86 */
87 [[nodiscard]] static consteval Pattern literal(std::string_view dsl)
88 {
89 const detail::PatternParse parsed = detail::parse_pattern(dsl);
90 if (parsed.status != detail::PatternStatus::Ok)
91 {
92 throw "DetourModKit: scan::Pattern::literal() received a malformed AOB pattern";
93 }
94 return Pattern{parsed.buffer};
95 }
96
97 /// Number of bytes in the compiled pattern.
98 536 [[nodiscard]] constexpr std::size_t size() const noexcept { return m_data.length; }
99
100 /**
101 * @brief The `|` result offset as an index into the fixed byte stream (0 when there is no offset marker).
102 * @details For a pattern with bounded jumps the resolver adds the actual gap bytes at match time, so the
103 * returned address still points at the intended run; this reports only the fixed-byte portion.
104 */
105 804 [[nodiscard]] constexpr std::size_t offset() const noexcept { return m_data.offset; }
106
107 /**
108 * @brief View over the compiled fixed pattern bytes, all segments concatenated (length == size()).
109 * @details Gap bytes are not stored, so for a jump-bearing pattern this is the fixed bytes only, not the span.
110 */
111 1230 [[nodiscard]] constexpr std::span<const std::byte> bytes() const noexcept
112 {
113 1230 return std::span<const std::byte>(m_data.bytes.data(), m_data.length);
114 }
115
116 /// View over the per-byte match mask paralleling bytes() (length == size()).
117 1239 [[nodiscard]] constexpr std::span<const std::byte> mask() const noexcept
118 {
119 1239 return std::span<const std::byte>(m_data.mask.data(), m_data.length);
120 }
121
122 /// True when the pattern carries at least one bounded jump (and therefore more than one segment).
123 72 [[nodiscard]] constexpr bool has_jumps() const noexcept { return m_data.jump_count > 0; }
124
125 /// Number of fixed segments the pattern splits into (1 for a plain pattern; one more than the jump count).
126 5 [[nodiscard]] constexpr std::size_t segment_count() const noexcept { return m_data.jump_count + 1; }
127
128 /// Fewest bytes any match can occupy: the fixed byte count plus every gap's minimum skip.
129 5 [[nodiscard]] constexpr std::size_t min_match_length() const noexcept
130 {
131 5 return detail::min_match_length(m_data);
132 }
133
134 /// Most bytes any match can occupy: the fixed byte count plus every gap's maximum skip.
135 4 [[nodiscard]] constexpr std::size_t max_match_length() const noexcept
136 {
137 4 return detail::max_match_length(m_data);
138 }
139
140 /// True when the pattern has at least one fully-known byte the prefilter can anchor on.
141 226 [[nodiscard]] constexpr bool has_anchor() const noexcept { return m_data.anchor < m_data.length; }
142
143 /// Index of the rarest fully-known byte; only meaningful when has_anchor() is true.
144 207 [[nodiscard]] constexpr std::size_t anchor_index() const noexcept { return m_data.anchor; }
145
146 /// The anchor byte value, or a zero byte when has_anchor() is false.
147 5 [[nodiscard]] constexpr std::byte anchor_byte() const noexcept
148 {
149
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 6 taken 1 time.
5 return has_anchor() ? m_data.bytes[m_data.anchor] : std::byte{0x00};
150 }
151
152 /**
153 * @brief Tests whether the pattern matches at the start of @p window and honors each bounded jump.
154 * @param window The candidate byte window. A match requires at least min_match_length() bytes.
155 * @return True only when the bounded search finds a complete placement at the window start.
156 * It returns false when the search finds no placement or exhausts its per-position budget before it
157 * checks every candidate placement. A false result is not proof of absence.
158 * @details A byte agrees when (memory ^ pattern) & mask is zero. A mask of 0x00 accepts every byte.
159 * Masks 0xF0 and 0x0F compare only the fixed nibble. The search tries gaps from smallest to largest.
160 * @note Callback-safe: the bounded search allocates no memory, performs no I/O, and takes no lock.
161 */
162 26 [[nodiscard]] constexpr bool matches_at(std::span<const std::byte> window) const noexcept
163 {
164 26 return detail::matches_buffer_at(m_data, window);
165 }
166
167 private:
168 friend constexpr const detail::PatternBuffer &detail::pattern_buffer(const Pattern &pattern) noexcept;
169
170 // Private so the only ways to obtain a Pattern are the validating factories; a default-constructed or
171 // arbitrary-buffer Pattern can never exist.
172 377 constexpr explicit Pattern(const detail::PatternBuffer &data) noexcept : m_data{data} {}
173
174 detail::PatternBuffer m_data{};
175 };
176
177 } // namespace DetourModKit::scan
178
179 namespace DetourModKit::detail
180 {
181 /// Returns @p pattern's immutable compiled buffer for internal scan adapters.
182 1218 [[nodiscard]] constexpr const PatternBuffer &pattern_buffer(const scan::Pattern &pattern) noexcept
183 {
184 1218 return pattern.m_data;
185 }
186 } // namespace DetourModKit::detail
187
188 namespace DetourModKit::scan
189 {
190
191 /**
192 * @enum Pages
193 * @brief Which page-protection class a page-gated scan accepts.
194 * @details Readable accepts every committed readable page, so one pass covers both code and data candidates.
195 * Executable narrows to committed execute-readable code pages only, the lower-false-positive choice when
196 * a signature must land on code.
197 */
198 enum class Pages : std::uint8_t
199 {
200 /// Every committed readable page (a superset of Executable); the default for a data-capable sweep.
201 Readable,
202 /// Committed execute-readable code pages only.
203 Executable
204 };
205
206 /**
207 * @brief Largest architectural x86-64 instruction length, in bytes.
208 * @details x86-64 instructions are at most 15 bytes long. RIP-relative helpers validate their disp32 layout against
209 * this bound before using the instruction length as the next-instruction base.
210 */
211 inline constexpr std::size_t MAX_X86_INSTRUCTION_LENGTH = 15;
212
213 /**
214 * @brief Tests whether a disp32 field fits within an x86-64 instruction of the given length.
215 * @param displacement_offset Byte offset of the signed 4-byte displacement field.
216 * @param instruction_length Total instruction length in bytes.
217 * @return True when the field lies entirely within a non-empty instruction no longer than
218 * @ref MAX_X86_INSTRUCTION_LENGTH.
219 * @details This checks structural bounds only; the caller remains responsible for supplying an opcode whose operand
220 * is actually RIP-relative. Subtraction after the offset comparison avoids unsigned-overflow arithmetic.
221 * @note Callback-safe: pure constexpr arithmetic.
222 */
223 [[nodiscard]] constexpr bool
224 130 is_valid_rip_relative_layout(std::size_t displacement_offset, std::size_t instruction_length) noexcept
225 {
226
3/4
✓ Branch 2 → 3 taken 126 times.
✓ Branch 2 → 6 taken 4 times.
✓ Branch 3 → 4 taken 126 times.
✗ Branch 3 → 6 not taken.
256 return instruction_length <= MAX_X86_INSTRUCTION_LENGTH && displacement_offset <= instruction_length &&
227
2/2
✓ Branch 4 → 5 taken 116 times.
✓ Branch 4 → 6 taken 10 times.
256 instruction_length - displacement_offset >= sizeof(std::int32_t);
228 }
229
230 /**
231 * @enum SimdLevel
232 * @brief The highest SIMD verification tier the engine selects at runtime.
233 */
234 enum class SimdLevel : std::uint8_t
235 {
236 /// Byte-by-byte verification (no SIMD).
237 Scalar,
238 /// SSE2 (16 bytes per iteration).
239 Sse2,
240 /// AVX2 (32 bytes per iteration, with an SSE2 + scalar tail).
241 Avx2,
242 /// AVX-512F + AVX-512BW (64 bytes per iteration). Opt-in: a DMK_ENABLE_AVX512 build on an AVX-512 host.
243 Avx512
244 };
245
246 /**
247 * @brief Returns the enumerator name for a SimdLevel.
248 * @param level The tier.
249 * @return A static string view; "Unknown" for an out-of-range value.
250 * @note Callback-safe: a pure constexpr value map.
251 */
252 [[nodiscard]] constexpr std::string_view to_string(SimdLevel level) noexcept
253 {
254 switch (level)
255 {
256 case SimdLevel::Scalar:
257 return "Scalar";
258 case SimdLevel::Sse2:
259 return "Sse2";
260 case SimdLevel::Avx2:
261 return "Avx2";
262 case SimdLevel::Avx512:
263 return "Avx512";
264 }
265 return "Unknown";
266 }
267
268 /**
269 * @brief Reports the SIMD tier find-pattern matching uses at runtime.
270 * @details Reflects both compile-time support (which intrinsics were built) and runtime CPU detection (CPUID plus
271 * OS XGETBV). Reports Avx512 only when the library was built with the opt-in DMK_ENABLE_AVX512 option and
272 * the host has AVX-512F + AVX-512BW; otherwise it reports the highest available lower tier.
273 * @note Callback-safe: pure CPU-feature read, no allocation or locking.
274 */
275 [[nodiscard]] SimdLevel active_simd_level() noexcept;
276
277 /**
278 * @enum StringEncoding
279 * @brief Byte encoding of an anchor string as it is stored in the image.
280 */
281 enum class StringEncoding : std::uint8_t
282 {
283 /**
284 * @brief The literal is stored as the query's bytes verbatim (char / std::string literals).
285 * @details Byte-transparent: the query text is searched for exactly as given, so a caller may anchor on a byte
286 * sequence that is not well-formed UTF-8.
287 */
288 Utf8,
289 /**
290 * @brief The literal is stored as UTF-16LE (wchar_t / L"" on Windows).
291 * @details The query text is UTF-8 and is transcoded, so it must be well-formed: a supplementary code point
292 * becomes a surrogate pair, and ill-formed input returns @ref ErrorCode::MalformedQueryText rather
293 * than searching for something else.
294 */
295 Utf16le
296 };
297
298 /**
299 * @enum XrefReturn
300 * @brief What a resolved string cross-reference returns.
301 */
302 enum class XrefReturn : std::uint8_t
303 {
304 /// Exact address of the instruction that loads the string.
305 ReferencingInstruction,
306 /**
307 * @brief Enclosing-function entry of the referencing instruction.
308 * @details Authoritative x64 `.pdata` bounds via RtlLookupFunctionEntry (following chained fragments to the
309 * primary function), with a bounded RET/INT3 prologue back-scan as the fallback for leaf functions and
310 * code regions with no registered exception table.
311 */
312 EnclosingFunction,
313 /**
314 * @brief Address of the global data slot a `mov [rip+slot], reg` stores the loaded string pointer into.
315 * @details Applies when the unique reference is a `lea reg, [rip+string]` shortly followed by that store; it
316 * resolves a cached global string pointer rather than the load site. Reports
317 * ErrorCode::StoreNotFound when no such store follows the reference.
318 */
319 StringPointerSlot
320 };
321
322 /**
323 * @struct StringRefQuery
324 * @brief A string-reference anchor query, with the string text borrowed for the duration of the call.
325 * @details Anchors a target on an immutable string literal in the image's read-only data, then resolves the unique
326 * RIP-relative reference to it. @ref text is a non-owning view into caller storage. This query is for the
327 * immediate find_string_xref() call. A stored string-xref Candidate owns its literal independently (see
328 * Candidate::string_xref).
329 */
330 struct StringRefQuery
331 {
332 /**
333 * @brief Literal content (no quotes); borrowed for the call.
334 * @details Must not contain an embedded NUL on either encoding: it would contradict @ref require_terminator and
335 * cannot occur in the C string literals these anchors name, so it returns
336 * @ref ErrorCode::MalformedQueryText.
337 */
338 std::string_view text;
339 /// How it is stored in the image, and therefore how @ref text is interpreted (see @ref StringEncoding).
340 StringEncoding encoding = StringEncoding::Utf8;
341 /**
342 * @brief Match a trailing NUL so a prefix of a longer literal is not matched (e.g. "Player" inside
343 * "PlayerController").
344 */
345 bool require_terminator = true;
346 /// Selects the exact instruction site, the enclosing-function heuristic, or the cached global pointer slot.
347 XrefReturn return_mode = XrefReturn::ReferencingInstruction;
348 /**
349 * @brief Selects the phase-2 reference scan breadth.
350 * @details false (default) runs the fast, desync-immune all-offset shape scan that recognizes the REX.W
351 * `lea`/`mov reg, [rip+disp32]` forms. true keeps that scan and also runs a Zydis-verified linear
352 * sweep that recognizes the rarer RIP-relative shapes (`cmp [rip+d], imm`, `push [rip+d]`, a no-REX
353 * `lea`/`mov`, ...), at the cost of a full decode per instruction. Derived return modes may still run
354 * that broad sweep as a confirmation pass when this flag is false, so a shape-local narrow hit is not
355 * certified while a rarer second reference exists.
356 */
357 bool broad_match = false;
358 };
359
360 /**
361 * @brief Resolves a string-reference anchor inside one mapped image.
362 * @param query The string and how to interpret its reference.
363 * @param scope Module image to search; defaults to the host executable. A scope confined to neither one mapped
364 * image nor one reserved allocation returns @ref ErrorCode::NotAuthoritative. This entry point takes no
365 * exclusion span and no page selector, so a confined scope is the only remedy.
366 * @return The referencing-instruction (or enclosing-function, or pointer-slot) address, or an Error.
367 * @details Two fail-closed phases. Phase 1 locates the single occurrence of @p query.text in the scope's readable
368 * pages. Zero returns @ref ErrorCode::StringNotFound, and more than one returns
369 * @ref ErrorCode::StringAmbiguous. Phase 2 finds the single RIP-relative reference whose resolved target
370 * equals that string address. Zero returns @ref ErrorCode::NoReference, and more than one returns
371 * @ref ErrorCode::AmbiguousReference. An observed second copy or second reference stays ambiguous even
372 * when the sweep was also truncated. A truncated sweep that observed no multiplicity certifies neither
373 * absence nor uniqueness and returns @ref ErrorCode::IncompleteScan. Text that cannot be encoded as
374 * asked returns
375 * @ref ErrorCode::MalformedQueryText. An out-of-range @ref StringRefQuery::encoding or
376 * @ref StringRefQuery::return_mode returns @ref ErrorCode::InvalidArg before phase 1 starts. The result
377 * is ASLR-correct because the reference is RIP-relative.
378 * @note Phase 1 excludes @p query.text's own storage when that storage lies inside @p scope. If the caller's
379 * buffer is the only copy in scope, the result is @ref ErrorCode::StringNotFound. Anchor on a literal that
380 * the scanned image owns.
381 * @note With @ref StringRefQuery::broad_match false, @ref XrefReturn::ReferencingInstruction reports uniqueness
382 * among the fast REX.W `lea`/`mov reg, [rip+disp32]` shapes only. A derived return
383 * (@ref XrefReturn::EnclosingFunction or @ref XrefReturn::StringPointerSlot) runs a broad confirmation
384 * sweep after a single narrow hit, so a rarer second reference fails closed as
385 * @ref ErrorCode::AmbiguousReference. That sweep does not promote a broad-only reference into a hit. Set
386 * @ref StringRefQuery::broad_match to accept the rarer shapes. @ref anchor::Anchor::xref_broad_match and
387 * @ref anchor::ScanProfile::default_broad_string_xref expose the same knob.
388 * @note Not noexcept: the broad-match phase may allocate during its decode. Setup/control-plane only.
389 */
390 [[nodiscard]] Result<Address> find_string_xref(const StringRefQuery &query, Region scope = Region::host());
391
392 /**
393 * @brief Resolves a named export to its address through one module's PE Export Address Table.
394 * @param export_name The exact export symbol, for example "Sleep". PE export names are case-sensitive.
395 * @param module The mapped image whose export directory to search; defaults to the host executable. An export
396 * usually lives in a different module from the code a mod scans, so pass the export's own module,
397 * for example @ref Region::module_named("kernel32.dll").
398 * @return The absolute address of the exported symbol, or an Error.
399 * @details The walk parses the mapped image's own IMAGE_EXPORT_DIRECTORY. It never calls GetProcAddress, so it
400 * never enters the loader and never runs a DllMain. Every RVA is bound-checked against the image and
401 * every read is guarded, so a truncated or hostile export section returns an Error, never a host fault.
402 * A missing export directory, an absent name, an ordinal-only export, an out-of-image RVA, and an empty
403 * @p export_name all return @ref ErrorCode::ExportNotFound. A null or invalid module image returns
404 * @ref ErrorCode::InvalidRange. A forwarded export returns @ref ErrorCode::ExportForwarded instead of
405 * the address of the forwarder string, because only the loader can follow a forwarder.
406 * @note Setup/control-plane only by convention, because it queries a module image, not a per-frame quantity.
407 * Allocation-free and noexcept: the name compare runs against a fixed-length window.
408 */
409 [[nodiscard]] Result<Address> resolve_export(std::string_view export_name, Region module = Region::host()) noexcept;
410
411 /**
412 * @enum OperandKind
413 * @brief Which operand field @ref read_code_constant extracts.
414 */
415 enum class OperandKind : std::uint8_t
416 {
417 /// An immediate operand (e.g. the imm of `add reg, imm`).
418 Immediate,
419 /// A memory operand's displacement (e.g. the disp of `[reg + disp]`).
420 MemoryDisplacement
421 };
422
423 /**
424 * @enum Mode
425 * @brief The resolution strategy a Candidate uses to turn a signature into an address.
426 * @details The mode is data on the Candidate (the active std::variant alternative), so a ladder can interleave
427 * the tiers freely. The two byte tiers (Direct, RipRelative) scan a compiled Pattern. The two text tiers
428 * (RttiVtable, StringXref) resolve a name or literal through a dedicated backend. They are unique-only by
429 * construction and fail closed on ambiguity regardless of the request's require_unique.
430 */
431 enum class Mode : std::uint8_t
432 {
433 /// Scan for the Pattern, then add a fixed signed walk-back to the hit (the address IS at the match site).
434 Direct,
435 /// Scan for the Pattern, then read the RIP-relative disp32 it spans and compute the absolute target.
436 RipRelative,
437 /// Resolve the primary vtable of an MSVC-mangled type name through the reverse-RTTI walk.
438 RttiVtable,
439 /// Anchor on an immutable string literal and resolve the unique RIP-relative reference to it.
440 StringXref
441 };
442
443 /**
444 * @enum CandidateOrder
445 * @brief How a ScanRequest's ladder is ordered before the resolver tries it.
446 * @details AsDeclared preserves the caller's array order. UniqueFirst promotes the unique-only text tiers, then
447 * anchored byte patterns, then the other byte patterns. @ref resolve returns the first candidate that
448 * resolves successfully. The changed order can alter the returned @ref Hit when valid candidates resolve
449 * to different addresses. Every candidate retains the same verification and validity rules.
450 */
451 enum class CandidateOrder : std::uint8_t
452 {
453 /// Try candidates in the order the caller wrote them.
454 AsDeclared,
455 /// Try unique-only text tiers, then anchored byte patterns, then the rest; declared order kept within a group.
456 UniqueFirst
457 };
458
459 /**
460 * @brief Returns the enumerator name for a CandidateOrder.
461 * @param order The ordering policy.
462 * @return A static string view; "Unknown" for an out-of-range value.
463 * @note Callback-safe: a pure constexpr value map with no allocation, I/O, or locking.
464 */
465 5 [[nodiscard]] constexpr std::string_view candidate_order_to_string(CandidateOrder order) noexcept
466 {
467
3/3
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 2 times.
✓ Branch 2 → 5 taken 1 time.
5 switch (order)
468 {
469 2 case CandidateOrder::AsDeclared:
470 2 return "AsDeclared";
471 2 case CandidateOrder::UniqueFirst:
472 2 return "UniqueFirst";
473 }
474 1 return "Unknown";
475 }
476
477 /**
478 * @struct DirectPattern
479 * @brief The Direct-tier payload: a compiled Pattern plus the signed walk-back applied to the match.
480 */
481 struct DirectPattern
482 {
483 /// The compiled signature to scan for.
484 Pattern pattern;
485 /// Signed byte delta added to the match (negative walks backward); 0 returns the match itself.
486 std::ptrdiff_t walk_back{0};
487 };
488
489 /**
490 * @struct RipRelativePattern
491 * @brief The RipRelative-tier payload: a compiled Pattern plus the disp32 location and instruction length.
492 * @details The resolved target is `(match + instruction_length) + sign_extend(disp32 @ (match + displacement_at))`,
493 * read under a fault guard, so a corrupt displacement is a miss rather than a host fault.
494 */
495 struct RipRelativePattern
496 {
497 /// The compiled signature to scan for.
498 Pattern pattern;
499 /// Byte offset from the match to the signed 4-byte displacement field.
500 std::ptrdiff_t displacement_at{0};
501 /// Total length of the referencing instruction (the next-IP base for the disp).
502 std::size_t instruction_length{0};
503 };
504
505 /**
506 * @struct RttiVtable
507 * @brief The RttiVtable-tier payload: the MSVC-mangled type name to resolve through the reverse-RTTI walk. Owned.
508 * @details Unique-only: an ambiguous name (two primaries) fails closed and the ladder falls through.
509 */
510 struct RttiVtable
511 {
512 /// The MSVC decorated type name, e.g. ".?AVCameraManager@@". Owned.
513 std::string mangled;
514 };
515
516 /**
517 * @struct StringXref
518 * @brief The StringXref-tier payload: an OWNED string literal plus the reference-resolution facets.
519 * @details The literal is held as an owned std::string (not the borrowed std::string_view of StringRefQuery)
520 * because a Candidate is stored and resolved later, long after the expression that built it; the resolver
521 * rebuilds a StringRefQuery view over this owned text at resolve time. Unique-only: a pooled literal or a
522 * second reference fails closed.
523 */
524 struct StringXref
525 {
526 /// The exact string content to anchor on (no quotes). Owned.
527 std::string text;
528 /// How the literal is stored in the image.
529 StringEncoding encoding{StringEncoding::Utf8};
530 /// Match a trailing NUL so a prefix of a longer literal is not matched.
531 bool require_terminator{true};
532 /// Instruction site, enclosing function, or cached global pointer slot.
533 XrefReturn return_mode{XrefReturn::ReferencingInstruction};
534 /// Keep the lea/mov shape scan and add the Zydis broad sweep for rarer reference shapes.
535 bool broad_match{false};
536 };
537
538 /**
539 * @class Candidate
540 * @brief One resilience tier in a resolution ladder: a strategy plus the signature it resolves, owning its strings.
541 * @details The payload is a std::variant over the four typed tiers, so the (mode, payload) pairing is coherent by
542 * construction. The four factories are the only way to build one, so a Candidate can only exist with a
543 * valid alternative. The candidate copies every owned string: the name, the RttiVtable mangled name, and
544 * the StringXref literal. A returned Hit or a stored ladder therefore never aliases caller storage.
545 */
546 class Candidate
547 {
548 public:
549 /// The active variant payload type. The alternative order matches the Mode enumerator order.
550 using Payload = std::variant<DirectPattern, RipRelativePattern, RttiVtable, StringXref>;
551
552 /**
553 * @brief A Direct byte-scan candidate: the resolved address is at the match site plus a fixed walk-back.
554 * @note Setup/control-plane only: builds an owned Candidate (string + Pattern copy); assemble ladders at init.
555 */
556 335 [[nodiscard]] static Candidate direct(std::string name, Pattern pattern, std::ptrdiff_t walk_back = 0)
557 {
558 670 return Candidate{std::move(name), DirectPattern{std::move(pattern), walk_back}};
559 }
560
561 /**
562 * @brief A RIP-relative byte-scan candidate: the resolved address is read from a disp32 the match spans.
563 * @param displacement_at Byte offset from the match to the signed 4-byte displacement field; must be >= 0.
564 * @param instruction_length Total length of the referencing instruction; must be no more than 15 bytes and
565 * contain the disp32 field.
566 * @throws std::invalid_argument when the declared layout is invalid or the matched suffix does not span the
567 * complete disp32 field.
568 * @details Setup/control-plane only: it builds an owned Candidate. Assemble each ladder at init. The matched
569 * evidence must cover the disp32 it authorizes. The resolver computes
570 * match + instruction_length + disp from one immutable sweep snapshot, never from a post-sweep
571 * reread.
572 * @note The manifest loader validates the same bound through @ref is_valid_rip_relative_layout, so a bad
573 * manifest fails closed with an error value instead of a throw.
574 */
575 [[nodiscard]] static Candidate
576 36 rip_relative(std::string name, Pattern pattern, std::ptrdiff_t displacement_at, std::size_t instruction_length)
577 {
578 71 if (displacement_at < 0 ||
579
6/6
✓ Branch 2 → 3 taken 35 times.
✓ Branch 2 → 8 taken 1 time.
✓ Branch 4 → 5 taken 32 times.
✓ Branch 4 → 8 taken 3 times.
✓ Branch 10 → 11 taken 8 times.
✓ Branch 10 → 14 taken 28 times.
68 !is_valid_rip_relative_layout(static_cast<std::size_t>(displacement_at), instruction_length) ||
580 32 detail::min_match_suffix_length(detail::pattern_buffer(pattern)) <
581
2/2
✓ Branch 7 → 8 taken 4 times.
✓ Branch 7 → 9 taken 28 times.
32 static_cast<std::size_t>(displacement_at) + sizeof(std::int32_t))
582 {
583 throw std::invalid_argument(
584 "scan::Candidate::rip_relative: the matched suffix must span a valid x86-64 RIP disp32 "
585 "(0 <= displacement_at, displacement_at + 4 <= instruction_length <= 15, and the pattern's "
586 "shortest suffix from the result marker covers displacement_at + 4 bytes)"
587
1/2
✓ Branch 12 → 13 taken 8 times.
✗ Branch 12 → 25 not taken.
8 );
588 }
589 return Candidate{
590 28 std::move(name),
591 56 RipRelativePattern{std::move(pattern), displacement_at, instruction_length}
592 84 };
593 }
594
595 /**
596 * @brief An RTTI-vtable candidate: resolves the primary vtable of an MSVC-mangled type name.
597 * @note Setup/control-plane only: copies the name and mangled query strings.
598 */
599 7 [[nodiscard]] static Candidate rtti_vtable(std::string name, std::string mangled)
600 {
601 14 return Candidate{std::move(name), RttiVtable{std::move(mangled)}};
602 }
603
604 /**
605 * @brief A string-xref candidate with default facets (UTF-8, referencing-instruction, terminator-required).
606 * @note Setup/control-plane only: copies the name and literal query strings.
607 */
608 10 [[nodiscard]] static Candidate string_xref(std::string name, std::string literal)
609 {
610
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 10 times.
20 return Candidate{std::move(name), StringXref{std::move(literal)}};
611 }
612
613 /**
614 * @brief A string-xref candidate carrying explicit facets (encoding, return mode, terminator, broad match).
615 * @details The query's borrowed text is copied into the Candidate's owned StringXref payload, so the Candidate
616 * outlives the StringRefQuery and its backing storage. The remaining facets are taken verbatim.
617 * @note Setup/control-plane only: copies the name and the query literal.
618 */
619 11 [[nodiscard]] static Candidate string_xref(std::string name, StringRefQuery query)
620 {
621 return Candidate{
622 11 std::move(name),
623
2/4
✓ Branch 7 → 8 taken 11 times.
✗ Branch 7 → 18 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 11 times.
22 StringXref{
624 std::string{query.text},
625 11 query.encoding,
626 11 query.require_terminator,
627 11 query.return_mode,
628 11 query.broad_match
629 }
630 33 };
631 }
632
633 /// Human-readable label; carried verbatim into the winning Hit.
634 348 [[nodiscard]] const std::string &name() const noexcept { return m_name; }
635
636 /// The resolution strategy this tier uses (a cast of the active variant alternative index).
637 636 [[nodiscard]] Mode mode() const noexcept { return static_cast<Mode>(m_payload.index()); }
638
639 /// The full variant payload, for the resolver's std::visit dispatch.
640 [[nodiscard]] const Payload &payload() const noexcept { return m_payload; }
641
642 /// Returns the Direct payload, or nullptr when this is not a Direct candidate.
643 1402 [[nodiscard]] const DirectPattern *as_direct() const noexcept { return std::get_if<DirectPattern>(&m_payload); }
644
645 /// Returns the RipRelative payload, or nullptr when this is not a RipRelative candidate.
646 450 [[nodiscard]] const RipRelativePattern *as_rip_relative() const noexcept
647 {
648 450 return std::get_if<RipRelativePattern>(&m_payload);
649 }
650
651 /// Returns the RttiVtable payload, or nullptr when this is not an RTTI-vtable candidate.
652 806 [[nodiscard]] const RttiVtable *as_rtti_vtable() const noexcept { return std::get_if<RttiVtable>(&m_payload); }
653
654 /// Returns the StringXref payload, or nullptr when this is not a string-xref candidate.
655 1226 [[nodiscard]] const StringXref *as_string_xref() const noexcept { return std::get_if<StringXref>(&m_payload); }
656
657 private:
658 // Private so the four validating factories are the only construction path; a stray `Candidate{...}` does not
659 // compile, and the (name, payload) coherence is established at the factory.
660 1173 Candidate(std::string name, Payload payload) : m_name{std::move(name)}, m_payload{std::move(payload)} {}
661
662 std::string m_name;
663 Payload m_payload;
664 };
665
666 // The resolver derives Mode from the active variant index, so the alternative order MUST track the Mode order. Pin
667 // it here: a future reorder of either list that breaks the mapping fails the build rather than silently misrouting
668 // a candidate to the wrong backend.
669 static_assert(std::is_same_v<
670 std::variant_alternative_t<static_cast<std::size_t>(Mode::Direct), Candidate::Payload>,
671 DirectPattern>);
672 static_assert(std::is_same_v<
673 std::variant_alternative_t<static_cast<std::size_t>(Mode::RipRelative), Candidate::Payload>,
674 RipRelativePattern>);
675 static_assert(std::is_same_v<
676 std::variant_alternative_t<static_cast<std::size_t>(Mode::RttiVtable), Candidate::Payload>,
677 RttiVtable>);
678 static_assert(std::is_same_v<
679 std::variant_alternative_t<static_cast<std::size_t>(Mode::StringXref), Candidate::Payload>,
680 StringXref>);
681
682 /**
683 * @struct CodeConstant
684 * @brief Declares a constant encoded in the engine's machine code so DMK can re-derive it after a patch.
685 * @details The code-side twin of the RTTI self-heal: where a struct stride or field displacement is an immediate or
686 * `[reg + disp]` in a dispatch loop, declare the candidate ladder that lands ON the instruction plus which
687 * operand to read, and read_code_constant() decodes the live instruction and returns the current value, so
688 * a consumer stops hand-reading the immediate every patch.
689 */
690 struct CodeConstant
691 {
692 /// Candidate ladder that resolves to an execute-readable instruction site. Borrowed.
693 std::span<const Candidate> site;
694 /// Which operand field to read: an immediate or a memory displacement.
695 OperandKind kind = OperandKind::Immediate;
696 /// Index into the instruction's VISIBLE operands, as counted in a disassembler.
697 std::uint8_t operand_index = 0;
698 /// 0 preserves the decoded value; 1 through 8 narrows a non-RIP constant to low bytes and sign-extends.
699 std::uint8_t byte_width = 0;
700 /// Last-known value, for telemetry/baseline ONLY; never returned in place of a live decode.
701 std::int64_t nominal = 0;
702 /// Set true to make @ref nominal meaningful (do not overload nominal == 0 as "unset").
703 bool has_nominal = false;
704 };
705
706 /**
707 * @brief Resolves @p code_constant.site, decodes the instruction there, and returns the requested operand's value.
708 * @param code_constant The code-constant declaration.
709 * @param scope Module image to resolve the site in; defaults to the host executable.
710 * @return The decoded value (sign-extended), or an Error.
711 * @details Always decodes and returns the LIVE operand; @c nominal is never a short-circuit, so a same-shape
712 * different-value drift (e.g. a stride 232 -> 240) is reported as the new value, which is the point.
713 * Fail-closed: a candidate whose final site is not execute-readable is skipped so a later ladder rung can
714 * resolve; if the selected site loses executable protection before decoding, or its decoded instruction
715 * crosses into a non-executable page, it returns DecodeFailed. A site that no longer decodes
716 * (DecodeFailed), whose operand is the wrong kind (UnexpectedShape), or whose operand index is out of
717 * range (OperandOutOfRange) also returns a typed error rather than a guess. An out-of-range
718 * @ref CodeConstant::kind or @ref CodeConstant::byte_width returns @ref ErrorCode::InvalidArg before site
719 * resolution. A RIP-relative memory operand is resolved to its absolute target without narrowing.
720 * The value decodes from a fresh snapshot after site resolution.
721 * A byte rung must still match its physical span and resolve the decoded site at that epoch (`[B-75]`).
722 * Otherwise, the function returns @ref ErrorCode::EvidenceMismatch.
723 * A wildcarded operand byte at the selected site may drift. The function returns its current value.
724 * @note Not noexcept: resolving the site allocates. Setup/control-plane only.
725 */
726 [[nodiscard]] Result<std::int64_t>
727 read_code_constant(const CodeConstant &code_constant, Region scope = Region::host());
728
729 /**
730 * @brief Ceiling on the winning-span bytes a @ref WinningEvidence can carry.
731 * @details A mutation baseline is compared byte for byte, so it must hold the WHOLE winning span or it would
732 * authorize a write on partial evidence. Evidence longer than this is reported truncated: still valid for
733 * read-only resolution, never usable to authorize a mutation or to seed a strict baseline.
734 */
735 inline constexpr std::size_t MAX_MUTATION_WITNESS_BYTES = 256;
736
737 /**
738 * @struct WinningEvidence
739 * @brief The literal bytes present at the winning match span, captured during the match that produced them.
740 * @details Content evidence, as opposed to the layout evidence in @ref ImageIdentity: it records what the target
741 * actually contained, including the concrete values that matched wildcard positions and the bytes a
742 * variable-length gap skipped over. That is what lets a caller distinguish a same-layout image whose code
743 * was changed under it from one that is genuinely unchanged, which no PE-header identity can do.
744 *
745 * Captured from the same traversal that matched, never a re-read, so it witnesses the span the resolver
746 * actually accepted rather than whatever occupies that address later.
747 */
748 struct WinningEvidence
749 {
750 /// The captured span, valid over the first @ref length elements; trailing elements are zero.
751 std::array<std::byte, MAX_MUTATION_WITNESS_BYTES> bytes{};
752 /// How many leading elements of @ref bytes are meaningful; 0 when nothing was captured.
753 std::uint16_t length = 0;
754 /**
755 * @brief True when the winning span exceeded @ref MAX_MUTATION_WITNESS_BYTES and was not captured.
756 * @details Set with @ref length 0: a partial prefix would compare equal against a prefix baseline and silently
757 * weaken the gate, so an over-long span carries no evidence at all rather than misleading evidence.
758 */
759 bool truncated = false;
760
761 /// True when a complete, internally valid winning span was captured.
762 263 [[nodiscard]] constexpr bool present() const noexcept
763 {
764
6/6
✓ Branch 2 → 3 taken 62 times.
✓ Branch 2 → 6 taken 201 times.
✓ Branch 3 → 4 taken 60 times.
✓ Branch 3 → 6 taken 2 times.
✓ Branch 4 → 5 taken 58 times.
✓ Branch 4 → 6 taken 2 times.
263 return length != 0 && length <= MAX_MUTATION_WITNESS_BYTES && !truncated;
765 }
766
767 /// The captured bytes as a span; empty unless @ref present.
768 6 [[nodiscard]] constexpr std::span<const std::byte> span() const noexcept
769 {
770
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 4 times.
6 if (!present())
771 {
772 2 return {};
773 }
774 4 return std::span<const std::byte>{bytes.data(), length};
775 }
776
777 /**
778 * @brief Value equality over the captured prefix and the truncation flag.
779 * @details Fails closed on a malformed value rather than walking it: @ref length is public and is not
780 * clamped on assignment, so a hand-built evidence whose length exceeds
781 * @ref MAX_MUTATION_WITNESS_BYTES, or which claims bytes while @ref truncated, would otherwise read
782 * past @ref bytes. Such a value compares equal to nothing, including a copy of itself, so it can
783 * never satisfy a baseline comparison.
784 */
785 13 [[nodiscard]] constexpr bool operator==(const WinningEvidence &other) const noexcept
786 {
787
4/6
✓ Branch 2 → 3 taken 13 times.
✗ Branch 2 → 5 not taken.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 6 taken 12 times.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 6 not taken.
13 const bool malformed = length > MAX_MUTATION_WITNESS_BYTES || (truncated && length != 0);
788 13 const bool other_malformed =
789
4/6
✓ Branch 7 → 8 taken 13 times.
✗ Branch 7 → 10 not taken.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 11 taken 12 times.
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 11 not taken.
13 other.length > MAX_MUTATION_WITNESS_BYTES || (other.truncated && other.length != 0);
790
3/4
✓ Branch 12 → 13 taken 12 times.
✓ Branch 12 → 14 taken 1 time.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 12 times.
13 if (malformed || other_malformed)
791 {
792 1 return false;
793 }
794
3/4
✓ Branch 15 → 16 taken 11 times.
✓ Branch 15 → 17 taken 1 time.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 11 times.
12 if (length != other.length || truncated != other.truncated)
795 {
796 1 return false;
797 }
798
2/2
✓ Branch 24 → 19 taken 1030 times.
✓ Branch 24 → 25 taken 9 times.
1039 for (std::size_t i = 0; i < length; ++i)
799 {
800
2/2
✓ Branch 21 → 22 taken 2 times.
✓ Branch 21 → 23 taken 1028 times.
1030 if (bytes[i] != other.bytes[i])
801 {
802 2 return false;
803 }
804 }
805 9 return true;
806 }
807 };
808
809 /**
810 * @struct Hit
811 * @brief A resolved address paired with the owning name and mode of the candidate that produced it.
812 * @details @ref winning_name remains valid for the lifetime of the Hit.
813 */
814 struct Hit
815 {
816 /// The resolved absolute address.
817 Address address;
818 /// A copy of the winning candidate's name.
819 std::string winning_name;
820 /// The resolution mode of the winning candidate.
821 Mode winning_mode = Mode::Direct;
822 /**
823 * @brief The literal bytes at the span this candidate matched; absent for a backend that matches no span.
824 * @details Only a byte-pattern tier witnesses a span. An RTTI, export, or string-xref rung resolves through a
825 * structure rather than a literal run, so it leaves this absent and cannot seed a mutation baseline.
826 */
827 WinningEvidence evidence{};
828 };
829
830 /**
831 * @enum FallbackPolicy
832 * @brief How strictly hooked-prologue recovery confirms the identity of a recovered target.
833 * @details `[B-53]` Hooked-prologue recovery rebuilds a Direct candidate's prologue as an inline-hook jump shape
834 * and resolves the single site that uniquely matches. That structural gate is strong but address-blind: a
835 * game reshape can leave an inline-hooked near-twin with a coincidental match. Pair a
836 * @ref FallbackWitness with RequireIdentity to fail closed on an unconfirmed site.
837 */
838 enum class FallbackPolicy : std::uint8_t
839 {
840 /// Recovery disabled: a full direct miss stays a miss.
841 Off,
842 /// Recover structurally; log a rejecting @ref FallbackWitness but still return the address.
843 WarnOnly,
844 /// Recover structurally, then require a @ref FallbackWitness to confirm the recovered address.
845 RequireIdentity,
846 };
847
848 /**
849 * @brief A post-recovery identity check for hooked-prologue recovery.
850 * @details Signature-compatible with @ref anchor::AnchorValidator. The recovered absolute address is passed as
851 * @p value. Return false to reject it as a coincidental near-twin.
852 * @param value The recovered absolute address, as a signed integer (a hook target is a code address).
853 * @param context The opaque @ref FallbackWitness::context pointer, forwarded verbatim (nullptr if unused).
854 */
855 using FallbackValidator = bool (*)(std::int64_t value, const void *context) noexcept;
856
857 /**
858 * @struct FallbackWitness
859 * @brief The witness a @ref FallbackPolicy runs against a recovered prologue-fallback target.
860 * @details A null @ref predicate means "no witness": WarnOnly then behaves as a plain structural recovery, and
861 * RequireIdentity fails closed (it has nothing to confirm the site with). A typical witness corroborates
862 * the recovered address against an independently resolved landmark, or reads a distinguishing byte past
863 * the overwritten prologue.
864 */
865 struct FallbackWitness
866 {
867 /// Predicate run on the recovered address; nullptr for no identity check.
868 FallbackValidator predicate = nullptr;
869 /// Opaque pointer forwarded verbatim to @ref predicate.
870 const void *context = nullptr;
871 };
872
873 /**
874 * @struct ScanRequest
875 * @brief A non-owning resolution request: a candidate ladder plus the scope and policy to resolve it under.
876 * @details ladder, label, and exclusions are NON-owning views, so a ScanRequest is for a request built and consumed
877 * in one expression (a temporary handed straight to resolve() outlives the call). To store or pass a
878 * request around, use OwnedScanRequest, or build a borrowed one through borrow() so the lifetime-bound
879 * diagnostic can ride its parameters (the attribute cannot annotate a data member).
880 */
881 struct ScanRequest
882 {
883 /// Candidates tried in order (after applying @ref order); the first that resolves uniquely wins.
884 std::span<const Candidate> ladder;
885 /// Optional label for diagnostics; non-owning.
886 std::string_view label{};
887 /// The memory range to resolve within; defaults to the host process image.
888 Region scope = Region::host();
889 /// Hooked-prologue recovery mode for a full direct miss (see @ref FallbackPolicy).
890 FallbackPolicy fallback_policy = FallbackPolicy::Off;
891 /// The identity witness the fallback runs on a recovered site (see @ref FallbackPolicy). Unused when Off.
892 FallbackWitness fallback_witness{};
893 /// Fail closed on an ambiguous byte match (a second occurrence in scope) rather than taking the first.
894 bool require_unique = true;
895 /// How the ladder is ordered before it is tried.
896 CandidateOrder order = CandidateOrder::AsDeclared;
897 /// Page-protection class the Direct / RipRelative byte tiers scan.
898 Pages pages = Pages::Readable;
899 /**
900 * @brief Rejects a candidate whose final resolved address is not on a committed execute-readable page.
901 * @details Applies after each byte, RTTI, string-xref, or prologue-recovery backend resolves its final address.
902 * Use it for a hook target that must be executable even when a byte candidate matches code then
903 * transforms its match into a data address. Defaults false because a RipGlobal may intentionally
904 * resolve a data global from an executable instruction reference.
905 */
906 bool require_executable_result = false;
907 /**
908 * @brief Caller-owned copies of the ladder's query bytes a match may not come from.
909 * @details Only needed for a @ref Pages::Readable scope confined to neither one mapped image nor one reserved
910 * allocation, where a match could otherwise be the query finding its own storage; such a scope
911 * resolves to @ref ErrorCode::NotAuthoritative while this is empty. DMK's own representations (the
912 * ladder, its Patterns, and the compiled forms) are excluded regardless. Non-owning, like @ref ladder.
913 */
914 std::span<const Region> exclusions{};
915 };
916
917 /**
918 * @brief Builds a borrowed ScanRequest whose lifetime-bound diagnostic rides its view parameters.
919 * @details DMK_LIFETIMEBOUND on the borrowed parameters lets Clang/MSVC warn when a temporary ladder/label is
920 * passed. MinGW GCC has no such attribute, so the build there relies on the owning/borrowed split plus
921 * `-Wdangling-reference`. For a stored or deferred request, prefer OwnedScanRequest.
922 * @note Callback-safe with an explicit Region: packs the borrowed views into a ScanRequest; noexcept, no
923 * allocation. The default scope query is setup/control-plane only.
924 */
925 [[nodiscard]] ScanRequest borrow(
926 std::span<const Candidate> ladder DMK_LIFETIMEBOUND,
927 std::string_view label DMK_LIFETIMEBOUND = {},
928 Region scope = Region::host(),
929 FallbackPolicy fallback_policy = FallbackPolicy::Off,
930 FallbackWitness fallback_witness = {},
931 bool require_unique = true,
932 CandidateOrder order = CandidateOrder::AsDeclared,
933 Pages pages = Pages::Readable
934 ) noexcept;
935
936 /**
937 * @brief Builds a borrowed ScanRequest preset for resolving a CODE (hook) target.
938 * @param ladder Candidates tried in order; borrowed for the call.
939 * @param label Optional diagnostic label; borrowed.
940 * @param scope Module image to resolve within; defaults to the host process image.
941 * @param fallback_policy Hooked-prologue recovery strictness (see @ref FallbackPolicy). Defaults to WarnOnly:
942 * recover a target another mod inline-hooked, structurally. Pass RequireIdentity with @p fallback_witness to
943 * fail closed on a recovered site the witness cannot confirm.
944 * @param fallback_witness The identity witness the fallback runs on a recovered site (see @ref FallbackWitness).
945 * @return A ScanRequest carrying the code-target resolution policy.
946 * @details A hook target must land on an instruction, so this preset differs from the default data-capable
947 * request in four ways: `Pages::Executable`, `require_executable_result`,
948 * @ref CandidateOrder::UniqueFirst, and an enabled @p fallback_policy. `require_unique` stays true.
949 * `Pages::Executable` narrows the Direct and RipRelative byte scans only. The final-result gate also
950 * rejects a byte tier that resolves code bytes to a data address, and any RTTI or string-xref result
951 * that is not executable. For a data, RTTI, or string target, use the default ScanRequest or
952 * @ref borrow.
953 * @note Callback-safe with an explicit Region: packs the borrowed views into a ScanRequest; noexcept, no
954 * allocation. The default scope query is setup/control-plane only. For a stored or deferred request, copy
955 * the same fields onto an OwnedScanRequest so the ladder is owned.
956 */
957 [[nodiscard]] ScanRequest borrow_code_target(
958 std::span<const Candidate> ladder DMK_LIFETIMEBOUND,
959 std::string_view label DMK_LIFETIMEBOUND = {},
960 Region scope = Region::host(),
961 FallbackPolicy fallback_policy = FallbackPolicy::WarnOnly,
962 FallbackWitness fallback_witness = {}
963 ) noexcept;
964
965 /**
966 * @brief Builds a borrowed ScanRequest preset for a CODE (hook) target that fails closed on unconfirmed recovery.
967 * @param ladder Candidates tried in order; borrowed for the call.
968 * @param label Optional diagnostic label; borrowed. Required positionally because the witness that follows has no
969 * default. Pass {} for none.
970 * @param fallback_witness The identity witness a recovered hooked-prologue site must satisfy. It has no default.
971 * @param scope Module image to resolve within; defaults to the host process image.
972 * @return A ScanRequest carrying the code-target policy under @ref FallbackPolicy::RequireIdentity.
973 * @details The strict counterpart to @ref borrow_code_target. Every field is identical except the fallback
974 * strictness. Recovery runs under @ref FallbackPolicy::RequireIdentity, so a Direct candidate recovered
975 * from an already inline-hooked target resolves only when @p fallback_witness confirms it. A
976 * coincidental near-twin fails closed instead. The witness has no default because RequireIdentity
977 * without a witness fails closed on every recovery, which is a silent always-miss.
978 * @note Callback-safe with an explicit Region: packs the borrowed views into a ScanRequest; noexcept, no
979 * allocation. The default scope query is setup/control-plane only.
980 */
981 [[nodiscard]] ScanRequest borrow_code_target_strict(
982 std::span<const Candidate> ladder DMK_LIFETIMEBOUND,
983 std::string_view label DMK_LIFETIMEBOUND,
984 FallbackWitness fallback_witness,
985 Region scope = Region::host()
986 ) noexcept;
987
988 /**
989 * @struct OwnedScanRequest
990 * @brief An owning resolution request for stored or deferred resolution.
991 * @details Owns its ladder, label, and exclusions, so it is the safe shape to keep inside a registration or any
992 * structure that outlives the expression that built it. The structural guarantee that stored entry points
993 * take OwnedScanRequest (never a borrowed ScanRequest) is the primary defense against dangling views;
994 * @ref view rebuilds a borrowed ScanRequest over this object's storage on demand.
995 */
996 struct OwnedScanRequest
997 {
998 /// Owned candidate ladder.
999 std::vector<Candidate> ladder;
1000 /// Owned diagnostic label.
1001 std::string label;
1002 /// The resolution scope; defaults to the host image.
1003 Region scope = Region::host();
1004 /// Hooked-prologue recovery strictness on a full direct miss (see @ref FallbackPolicy).
1005 FallbackPolicy fallback_policy = FallbackPolicy::Off;
1006 /// The identity witness the fallback runs on a recovered site (see @ref FallbackPolicy). Unused when Off.
1007 FallbackWitness fallback_witness{};
1008 /// Fail closed on an ambiguous byte match.
1009 bool require_unique = true;
1010 /// Ladder ordering policy.
1011 CandidateOrder order = CandidateOrder::AsDeclared;
1012 /// Page-protection class the byte tiers scan (see @ref ScanRequest::pages).
1013 Pages pages = Pages::Readable;
1014 /// Whether the final resolved address must be execute-readable.
1015 bool require_executable_result = false;
1016 /// Owned copies of the caller-declared query exclusions (see @ref ScanRequest::exclusions).
1017 std::vector<Region> exclusions;
1018
1019 /**
1020 * @brief Returns a borrowed ScanRequest viewing this object's owned storage.
1021 * @return A ScanRequest whose ladder/label/exclusions alias *this; valid only while this object lives.
1022 */
1023 12 [[nodiscard]] ScanRequest view() const noexcept DMK_LIFETIMEBOUND
1024 {
1025 return ScanRequest{
1026 12 .ladder = ladder,
1027 12 .label = label,
1028 .scope = scope,
1029 12 .fallback_policy = fallback_policy,
1030 .fallback_witness = fallback_witness,
1031 12 .require_unique = require_unique,
1032 12 .order = order,
1033 12 .pages = pages,
1034 12 .require_executable_result = require_executable_result,
1035 12 .exclusions = exclusions,
1036 24 };
1037 }
1038 };
1039
1040 /**
1041 * @brief Writes the index permutation @p order implies for @p ladder into @p out.
1042 * @param order The ordering policy.
1043 * @param ladder The candidate ladder to order.
1044 * @param out Destination for the permutation; receives up to min(ladder.size(), out.size()) indices.
1045 * @return The number of indices written.
1046 * @details Pure index math, no allocation. UniqueFirst is a stable three-pass partition (unique-only text tiers,
1047 * then anchored byte patterns, then the rest), declared order preserved within each group. Every other
1048 * value, including an out-of-range one, yields the identity permutation, so a mis-declared order can never
1049 * select the UniqueFirst promotion; the fallible @ref resolve boundary rejects it with
1050 * @ref ErrorCode::InvalidArg.
1051 * @note Callback-safe: pure index math, noexcept, no allocation.
1052 */
1053 [[nodiscard]] std::size_t
1054 order_candidates(CandidateOrder order, std::span<const Candidate> ladder, std::span<std::size_t> out) noexcept;
1055
1056 /**
1057 * @brief Resolves a candidate ladder to a single address, trying each tier until one resolves uniquely.
1058 * @param request The ladder, scope, and policy to resolve.
1059 * @return The resolved Hit, or an Error describing why no candidate resolved.
1060 * @details The whole resolver surface in one call. Candidates are tried in @ref ScanRequest::order order; the first
1061 * that (for a byte tier) matches in scope, passes the uniqueness gate when required, and resolves to an
1062 * in-scope plausible address, or (for a text tier) resolves through its unique-only backend, wins. When
1063 * @ref ScanRequest::require_executable_result is true, every final address must also be execute-readable.
1064 * On a full direct miss with a non-Off fallback_policy, each Direct candidate's prologue is rebuilt as a
1065 * near/far JMP and retried to recover a target another mod already inline-hooked, subject to the policy's
1066 * identity witness. A byte tier whose own sweep was truncated (@ref ErrorCode::BudgetExceeded or
1067 * @ref ErrorCode::IncompleteScan) preempts that recovery, because "the direct candidates fully missed" is
1068 * the premise recovery rests on and a partly-read scope does not establish it; a recovery sweep that
1069 * itself skips a faulted region reports @ref ErrorCode::IncompleteScan rather than a miss. A text tier's
1070 * failure (@ref ErrorCode::MalformedQueryText, or @ref ErrorCode::NotAuthoritative or
1071 * @ref ErrorCode::IncompleteScan from its own readable sweep) does not preempt recovery, but is reported
1072 * in place of the generic miss when nothing resolves. An unconfined Pages::Readable scope that declares
1073 * no @ref ScanRequest::exclusions refuses the whole request with @ref ErrorCode::NotAuthoritative before
1074 * any candidate is graded. An out-of-range @ref ScanRequest::pages, @ref ScanRequest::order,
1075 * @ref ScanRequest::fallback_policy, or StringXref candidate encoding/return mode fails closed with
1076 * @ref ErrorCode::InvalidArg rather than selecting a permissive default. May allocate, so it is NOT
1077 * noexcept; the only throwing path is allocation failure.
1078 * @note Setup/control-plane only: a cascade resolve walks the image and is a startup-time operation.
1079 */
1080 [[nodiscard]] Result<Hit> resolve(const ScanRequest &request);
1081
1082 /**
1083 * @brief Resolves a batch of requests concurrently, returning one Result per request in input order.
1084 * @param requests The requests to resolve.
1085 * @param max_workers Upper bound on worker threads (0 = auto-select from hardware concurrency).
1086 * @return On success, the inner vector holds one @ref Hit-or-Error per input request, in order. On a WHOLE-BATCH
1087 * failure (the per-request result container itself could not be allocated under true out-of-memory) the
1088 * OUTER Result carries Error{OutOfMemory} and there is no inner vector.
1089 * @details noexcept by contract, and the two failure layers are distinct so no failure is ever silent. A
1090 * PER-REQUEST allocation failure is reported as that slot's Error{OutOfMemory}, and any other per-request
1091 * exception leaves that slot at the seeded Error{NoMatch}, so one failing request never sinks the batch.
1092 * A WHOLE-BATCH allocation failure, when even the seeded result vector cannot be built, is reported on
1093 * the outer Result instead of an easily-ignored empty vector, so a caller must unwrap the outer Result
1094 * before indexing and cannot silently proceed on a truncated batch. This mirrors @ref hook::install_all,
1095 * whose outer Result is likewise the whole-batch signal.
1096 * @note Setup/control-plane only: spawns a worker pool and allocates; a startup-time batch, not a per-frame call.
1097 */
1098 [[nodiscard]] Result<std::vector<Result<Hit>>>
1099 resolve_batch(std::span<const ScanRequest> requests, std::size_t max_workers = 0) noexcept;
1100
1101 /**
1102 * @brief Scans one Pattern over a known scope and returns the Nth match address.
1103 * @param pattern The compiled signature.
1104 * @param scope The memory range to search.
1105 * @param occurrence Which match to return (1-based). 1 = first match. 0 yields NoMatch.
1106 * @param pages Which page-protection class to accept (Readable superset by default, or Executable code-only).
1107 * @return The address of the Nth match (adjusted by the Pattern's `|` offset), or an Error.
1108 * @details Page-gated. The sweep walks @p scope through the OS page map and reads only committed pages of the
1109 * requested class under a fault guard, so an unmapped or guard page inside the scope is skipped instead
1110 * of a host fault. A match that straddles two adjacent accepted regions is still found. For the raw
1111 * primitive where the caller guarantees readability, use @ref unchecked::find_pattern.
1112 *
1113 * A miss is typed, because "not found" and "not searched" are different answers.
1114 * @ref ErrorCode::NoMatch means the sweep traversed the whole scope and the pattern is absent.
1115 * @ref ErrorCode::IncompleteScan means a region faulted mid-scan and was skipped, so the pattern can
1116 * live in bytes that the sweep never read. @ref ErrorCode::BudgetExceeded means a bounded-jump pattern
1117 * spent its backtracking budget before the traversal was exhaustive. Neither truncation is a miss. An
1118 * out-of-range @p pages value returns @ref ErrorCode::InvalidArg before the sweep starts.
1119 *
1120 * A @ref Pages::Readable scan returns @ref ErrorCode::NotAuthoritative when its scope is confined to
1121 * neither one mapped image nor one reserved allocation and declares no exclusions. Such a scope covers
1122 * the caller's own copies of the pattern bytes, which DMK cannot enumerate, so a match can be the query
1123 * that finds itself. Confine the scope, scan @ref Pages::Executable, or use the exclusion-taking
1124 * overload. DMK always excludes its own query representations, on every scope.
1125 * @note Setup/control-plane only: walks the scope through the OS page map; a startup-time scan, not a per-frame
1126 * call. noexcept; an allocation failure while preparing the scan surfaces as Error{OutOfMemory}.
1127 */
1128 [[nodiscard]] Result<Address>
1129 scan(const Pattern &pattern, Region scope, std::size_t occurrence = 1, Pages pages = Pages::Readable) noexcept;
1130
1131 /**
1132 * @brief Scans one Pattern over a known scope while excluding caller-owned copies of the query bytes.
1133 * @param pattern The compiled signature.
1134 * @param scope The memory range to search.
1135 * @param exclusions Spans holding the caller's own copies of the query material; a match intersecting one is not
1136 * counted. Passing a non-empty span is what makes an otherwise unprovable readable scope authoritative, so
1137 * it must actually name every live copy the caller holds.
1138 * @param occurrence Which match to return (1-based). 1 = first match. 0 yields NoMatch.
1139 * @param pages Which page-protection class to accept.
1140 * @return The address of the Nth non-excluded match, or an Error.
1141 * @details Identical to the four-argument overload except that @p exclusions is added to the set DMK already
1142 * excludes for its own query storage. The combined set has 32 slots after merging touching spans; if it
1143 * cannot hold every span, the scan fails closed with @ref ErrorCode::NotAuthoritative.
1144 * @note Setup/control-plane only, same constraints as the four-argument overload.
1145 */
1146 [[nodiscard]] Result<Address> scan(
1147 const Pattern &pattern,
1148 Region scope,
1149 std::span<const Region> exclusions,
1150 std::size_t occurrence = 1,
1151 Pages pages = Pages::Readable
1152 ) noexcept;
1153
1154 /// Common x86-64 RIP-relative opcode prefixes (the bytes preceding the disp32 field), for find_and_resolve.
1155 inline constexpr std::array<std::byte, 3> PREFIX_MOV_RAX_RIP = {std::byte{0x48}, std::byte{0x8B}, std::byte{0x05}};
1156 inline constexpr std::array<std::byte, 3> PREFIX_MOV_RCX_RIP = {std::byte{0x48}, std::byte{0x8B}, std::byte{0x0D}};
1157 inline constexpr std::array<std::byte, 3> PREFIX_MOV_RDX_RIP = {std::byte{0x48}, std::byte{0x8B}, std::byte{0x15}};
1158 inline constexpr std::array<std::byte, 3> PREFIX_MOV_RBX_RIP = {std::byte{0x48}, std::byte{0x8B}, std::byte{0x1D}};
1159 inline constexpr std::array<std::byte, 3> PREFIX_LEA_RAX_RIP = {std::byte{0x48}, std::byte{0x8D}, std::byte{0x05}};
1160 inline constexpr std::array<std::byte, 3> PREFIX_LEA_RCX_RIP = {std::byte{0x48}, std::byte{0x8D}, std::byte{0x0D}};
1161 inline constexpr std::array<std::byte, 3> PREFIX_LEA_RDX_RIP = {std::byte{0x48}, std::byte{0x8D}, std::byte{0x15}};
1162 inline constexpr std::array<std::byte, 1> PREFIX_CALL_REL32 = {std::byte{0xE8}};
1163 inline constexpr std::array<std::byte, 1> PREFIX_JMP_REL32 = {std::byte{0xE9}};
1164
1165 /**
1166 * @brief Resolves an absolute address from an x86-64 RIP-relative instruction at a known address.
1167 * @param instruction Address of the first byte of the instruction.
1168 * @param displacement_offset Byte offset from @p instruction to the disp32 field.
1169 * @param instruction_length Total length of the instruction in bytes; must be at most 15 and contain the disp32.
1170 * @return The resolved absolute address (`instruction + instruction_length + disp32`), or an Error.
1171 * @details The displacement is read under an SEH fault guard. A resolved address that is not a plausible user-mode
1172 * pointer is rejected with ErrorCode::ImplausibleTarget rather than returned. For `FF 15`/`FF 25` forms
1173 * the resolved value is the pointer slot, itself an in-image address. A malformed field layout returns
1174 * ErrorCode::InvalidArg before any read.
1175 * @note Callback-safe: a guarded read plus pointer arithmetic, no allocation.
1176 */
1177 [[nodiscard]] Result<Address>
1178 resolve_rip_relative(Address instruction, std::size_t displacement_offset, std::size_t instruction_length) noexcept;
1179
1180 /**
1181 * @brief Scans forward in @p search for an opcode prefix, then resolves the RIP-relative target that follows it.
1182 * @param search The region to scan; the disp32 is assumed to immediately follow the matched prefix.
1183 * @param opcode_prefix The opcode byte sequence to search for.
1184 * @param instruction_length Total length of the instruction in bytes; must be at most 15 and contain the disp32
1185 * that follows @p opcode_prefix.
1186 * @return The resolved absolute address, or an Error.
1187 * @details The first resolvable prefix wins. A matched occurrence is a coincidental decoy when its disp32
1188 * resolves to an implausible target, or when the first byte of the resolved target is not readable at
1189 * scan time. The scan skips each decoy and continues. The scan fails only after it exhausts the region,
1190 * and then reports the last concrete failure, for example @ref ErrorCode::ImplausibleTarget or
1191 * @ref ErrorCode::UnreadableTarget for a plausible target on an unreadable page. The prefix search
1192 * reads @p search directly with no page filter, so the caller must guarantee that the region is
1193 * committed and readable.
1194 * To resolve one instruction whose address is uncertain, use @ref resolve_rip_relative, whose
1195 * displacement read is guarded. For the `FF 15` and `FF 25` indirect forms the returned address is the
1196 * pointer slot, not the final target. The same ImplausibleTarget gate applies. For an ambiguous
1197 * signature, anchor through @ref resolve, which enforces per-candidate uniqueness. A malformed field
1198 * layout returns @ref ErrorCode::InvalidArg before the sweep starts.
1199 * @note The prefix scan reads @p search unguarded (caller-guaranteed readable); the displacement read is guarded.
1200 * No allocation.
1201 * @note Setup/control-plane only: the sweep cost scales with @p search, so resolve at init, not per frame.
1202 */
1203 [[nodiscard]] Result<Address> find_and_resolve_rip_relative(
1204 Region search,
1205 std::span<const std::byte> opcode_prefix,
1206 std::size_t instruction_length
1207 ) noexcept;
1208
1209 /**
1210 * @brief Cheap heuristic: does @p addr look like the first byte of a real function body?
1211 * @param addr Absolute address to probe. A null @p addr returns false without reading memory.
1212 * @return true if the byte at @p addr is readable and not on the poison list; false otherwise.
1213 * @details Reads exactly one byte from @p addr under an SEH fault guard and rejects a small blacklist of bytes that
1214 * are never the first opcode of a callable x86-64 function: 0x00 (zero-fill / NULL page), 0xCC (int3 pad),
1215 * and 0xC2 / 0xC3 (bare RET stub). It returns true for 0xE9 / 0xEB / the 0xFF 0x25 prefix of an indirect
1216 * JMP, so a target whose prologue is already overwritten by another inline hook still passes, which is
1217 * required for nested-hook scenarios. This is the negative complement to the resolve() prologue-recovery
1218 * fallback: use it to filter scan poison (a zero page or an alignment pad) after a resolve.
1219 * @note Callback-safe: a single guarded byte read, no allocation.
1220 */
1221 [[nodiscard]] bool is_likely_function_prologue(Address addr) noexcept;
1222
1223 /**
1224 * @struct ImageIdentity
1225 * @brief An ASLR-insensitive fingerprint of a loaded module's PE build identity.
1226 * @details Folds the PE timestamp, image size, and section-table layout. The module base is excluded; a malformed
1227 * or incomplete header read yields an absent identity.
1228 * @warning Layout identity, NOT content identity. Every input is an
1229 * @c IMAGE_FILE_HEADER / @c IMAGE_OPTIONAL_HEADER / @c IMAGE_SECTION_HEADER field; no section body is
1230 * ever read. Executable content patched in place, leaving the timestamp, @c SizeOfImage, and the section
1231 * table equal, produces a bit-identical identity. Use @ref WinningEvidence to witness content.
1232 */
1233 struct ImageIdentity
1234 {
1235 /// PE @c IMAGE_FILE_HEADER::TimeDateStamp of the resolved image (0 when the read failed).
1236 470 std::uint32_t timestamp = 0;
1237 /// PE @c IMAGE_OPTIONAL_HEADER::SizeOfImage of the resolved image (0 when the read failed).
1238 468 std::uint32_t size_of_image = 0;
1239 /// A fold of every section header's name, RVA, virtual size, and characteristics.
1240 468 std::uint64_t section_digest = 0;
1241
1242 /// True when a live image was read (@ref size_of_image is non-zero); a default value is absent.
1243 493 [[nodiscard]] constexpr bool present() const noexcept { return size_of_image != 0; }
1244
1245 /// A single 64-bit token folding all three fields, for a fingerprint or an equality key.
1246 15 [[nodiscard]] constexpr std::uint64_t token() const noexcept
1247 {
1248 15 std::uint64_t seed = section_digest;
1249 15 seed ^= static_cast<std::uint64_t>(timestamp) + 0x9E3779B97F4A7C15ULL + (seed << 6) + (seed >> 2);
1250 15 seed ^= static_cast<std::uint64_t>(size_of_image) + 0x9E3779B97F4A7C15ULL + (seed << 6) + (seed >> 2);
1251 15 return seed;
1252 }
1253
1254 /// Value equality across all three fields.
1255
5/6
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 468 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 468 times.
✓ Branch 6 → 7 taken 12 times.
✓ Branch 6 → 8 taken 456 times.
470 [[nodiscard]] constexpr bool operator==(const ImageIdentity &other) const noexcept = default;
1256 };
1257
1258 /**
1259 * @brief Reads the ASLR-insensitive @ref ImageIdentity of the module mapped at @p range's base.
1260 * @param range The module image to identify; defaults to the host executable. Only @p range.base is used, since
1261 * the live PE headers there carry the authoritative SizeOfImage and section table.
1262 * @return The identity, or an absent value when @p range is empty or its PE headers do not validate completely.
1263 * @details Uses guarded reads and consults no loader.
1264 * @note Callback-safe with an explicit Region: bounded guarded reads of the PE headers, no allocation or loader
1265 * call. The default scope query is setup/control-plane only.
1266 */
1267 [[nodiscard]] ImageIdentity image_identity(Region range = Region::host()) noexcept;
1268
1269 /**
1270 * @brief Flattens a resolve Result to its address, or a null Address on failure.
1271 * @details A convenience adapter, not the primary contract: new code resolves through Result and handles the
1272 * Error.
1273 * @note Callback-safe: a pure noexcept Result read with no allocation, I/O, or locking.
1274 */
1275 2 [[nodiscard]] inline Address or_null(const Result<Hit> &result) noexcept
1276 {
1277
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 6 taken 1 time.
2 return result ? result->address : Address{};
1278 }
1279
1280 /**
1281 * @brief Flattens a resolve Result to its address, or a caller-chosen fallback on failure.
1282 * @details The general, non-default form of or_null (`or_null(r)` is `address_or(r, Address{})`). Same
1283 * convenience-adapter status: prefer handling the Error in new code.
1284 * @note Callback-safe: a pure noexcept Result read with no allocation, I/O, or locking.
1285 */
1286 3 [[nodiscard]] inline Address address_or(const Result<Hit> &result, Address fallback = Address{}) noexcept
1287 {
1288
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 6 taken 2 times.
3 return result ? result->address : fallback;
1289 }
1290
1291 namespace unchecked
1292 {
1293 /**
1294 * @brief Raw single-pattern scan over a region the caller guarantees is fully readable.
1295 * @param region The byte range to scan; every byte MUST be committed and readable.
1296 * @param pattern The compiled signature.
1297 * @param occurrence Which match to return (1-based). 1 = first match. 0 returns nullptr.
1298 * @return A pointer to the Nth match (adjusted by the Pattern's `|` offset), or nullptr if not found.
1299 * @details The unsafe twin of scan(): it performs no page filtering and uses raw SIMD/memchr loads, so an
1300 * unreadable byte in @p region faults the host. The return is a raw pointer, not a Result, because
1301 * there is no recoverable error to report. noexcept. A pattern allocation failure returns nullptr.
1302 * @note Setup/control-plane only: prepares the engine pattern and performs a raw, page-unfiltered scan.
1303 */
1304 [[nodiscard]] const std::byte *
1305 find_pattern(Region region, const Pattern &pattern, std::size_t occurrence = 1) noexcept;
1306 } // namespace unchecked
1307
1308 } // namespace DetourModKit::scan
1309
1310 #endif // DETOURMODKIT_SCAN_HPP
1311