GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 3 / 0 / 3
Functions: 100.0% 1 / 0 / 1
Branches: 100.0% 4 / 0 / 4

include/DetourModKit/anchor.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_ANCHOR_HPP
2 #define DETOURMODKIT_ANCHOR_HPP
3
4 /**
5 * @file anchor.hpp
6 * @brief Declarative anchor registry: one table that resolves a mod's patch-fragile constants and reports drift.
7 * @details Declares patch-fragile values once, resolves them through the existing scan/RTTI backends, and reports
8 * one machine-readable drift table. Missing or contradictory evidence fails closed; Quorum can require
9 * N-of-M independent agreement, while Manual remains the explicit pinned fallback.
10 * @warning `[B-100]` Under the loader lock, call only the Callback-safe trust and quality queries. Resolution can
11 * allocate, query loader state, scan memory, or create threads.
12 */
13
14 #include "DetourModKit/error.hpp"
15 #include "DetourModKit/region.hpp"
16 #include "DetourModKit/scan.hpp"
17
18 #include <array>
19 #include <cstddef>
20 #include <cstdint>
21 #include <span>
22 #include <string_view>
23
24 namespace DetourModKit
25 {
26 namespace anchor
27 {
28 /**
29 * @enum AnchorKind
30 * @brief Which backend resolves an anchor, and therefore how update-resilient it is.
31 * @details When a target can be expressed more than one way, prefer the most update-resilient backend:
32 * ExportName > StringXref > VtableIdentity > RipGlobal > CodeOperand, with a Quorum voting over
33 * several of those raising confidence further and Manual as the last resort. A named export is a
34 * module's documented ABI; a string literal and a mangled type name survive game patches far better
35 * than the code bytes and addresses around them.
36 */
37 enum class AnchorKind : std::uint8_t
38 {
39 /// A class vtable address, keyed on its mangled name, via @ref rtti::vtable_for_type.
40 VtableIdentity,
41 /// An absolute address (Direct or RIP-relative candidate cascade), via @ref scan::resolve.
42 RipGlobal,
43 /// An in-code immediate or `[reg + disp]` displacement, via @ref scan::read_code_constant.
44 CodeOperand,
45 /**
46 * @brief The instruction (or enclosing function) that references an immutable string literal, via
47 * @ref scan::find_string_xref.
48 */
49 StringXref,
50 /// A pinned literal with no backend; reported as at-risk because it cannot self-heal.
51 Manual,
52 /**
53 * @brief Reserved for a future prologue-dataflow backend (a call argument to its register/stack home).
54 * Declaring it now keeps a registry table forward-compatible; it currently reports
55 * @ref AnchorStatus::Unsupported.
56 */
57 CallArgHome,
58 /**
59 * @brief A corroborated value accepted only when at least N of M independent sub-anchors resolve and agree
60 * (N-of-M voting).
61 */
62 Quorum,
63 /**
64 * @brief A named export resolved by walking its module's PE Export Address Table, via
65 * @ref scan::resolve_export. Uses @ref Anchor::export_name and the optional
66 * @ref Anchor::export_module (an empty module resolves within the resolve scope).
67 */
68 ExportName,
69 /**
70 * @brief No backend: the fail-closed default for an anchor whose @ref Anchor::kind was never set. An
71 * aggregate table entry that omits the kind reports @ref AnchorStatus::Failed instead of a trusted
72 * address 0.
73 */
74 Unset
75 };
76
77 /// The number of @ref AnchorKind enumerators; sizes the per-kind deny-list in @ref ScanProfile.
78 inline constexpr std::size_t ANCHOR_KIND_COUNT = 9;
79 static_assert(
80 static_cast<std::size_t>(AnchorKind::Unset) + 1 == ANCHOR_KIND_COUNT,
81 "ANCHOR_KIND_COUNT must track the AnchorKind enumerator count."
82 );
83
84 /**
85 * @enum QuorumMatch
86 * @brief The agreement policy a @ref AnchorKind::Quorum applies when deciding whether two resolved member
87 * values count as one vote for the same target.
88 */
89 enum class QuorumMatch : std::uint8_t
90 {
91 /// Two member values agree only when identical (the default, strongest policy).
92 ExactValue,
93 /**
94 * @brief Two member values agree when their gap is at most @ref Anchor::quorum_tolerance; a negative
95 * tolerance fails closed (never accepts). The pairwise-independence gate prevents a false
96 * near-value cluster from two members that decode adjacent bytes.
97 */
98 WithinTolerance
99 };
100
101 /**
102 * @enum AnchorStatus
103 * @brief The outcome of resolving one anchor.
104 */
105 enum class AnchorStatus : std::uint8_t
106 {
107 /// The initial state written into an untouched slot; a resolved report never leaves this in place.
108 Unresolved,
109 /// The backend resolved a value and every applicable validator/corroboration check passed.
110 Resolved,
111 /**
112 * @brief The backend missed, a validator rejected the value, a denied backend was requested, or a quorum
113 * disagreed; no value is invented (fail closed).
114 */
115 Failed,
116 /// The kind has no resolver yet (@ref AnchorKind::CallArgHome).
117 Unsupported,
118 /// A quorum's members were not all pairwise-independent evidence, so corroboration would be meaningless.
119 QuorumNotIndependent,
120 /**
121 * @brief A quorum reached its threshold for two or more values that do not agree with each other, so no
122 * single value is corroborated. Declaration order must not silently pick a winner, so the vote
123 * fails closed.
124 */
125 QuorumAmbiguous
126 };
127
128 /**
129 * @brief A post-resolve validator predicate: returns false to fail an otherwise-resolved anchor closed.
130 * @details Runs on the resolved value just before it is accepted. Returning false resets the value to 0 and
131 * sets @ref AnchorStatus::Failed, identical to a backend miss, so the caller re-heals by re-resolving.
132 * Use it to assert a domain invariant a generic backend cannot know (the target lies in an expected
133 * sub-range, a displacement points into `.rdata`, the site begins with a plausible prologue).
134 * @param value The resolved value (an address cast to int64, an in-code constant, or the manual literal).
135 * @param context The opaque @ref Anchor::validator_context pointer, forwarded verbatim (nullptr if unused).
136 */
137 using AnchorValidator = bool (*)(std::int64_t value, const void *context) noexcept;
138
139 /**
140 * @struct Anchor
141 * @brief One declarative registry entry: what to resolve, how, and how to verify it. Authored as a static
142 * table.
143 * @details A flat aggregate authored with designated initializers, so a table lists only the fields its kind
144 * uses and leaves the rest defaulted. The active field set depends on @ref kind; other kinds' fields
145 * are ignored. All views (@ref label, @ref mangled, @ref site, @ref xref_text) are non-owning and must
146 * outlive the resolve call. The canonical use is a `static constexpr`/`static const` table whose
147 * storage lives for the process.
148 */
149 struct Anchor
150 {
151 /// Identifier echoed into the @ref ResolvedAnchor; excluded from @ref anchor_fingerprint.
152 std::string_view label;
153 /// Which backend resolves this anchor. Defaults to @ref AnchorKind::Unset so an omitted kind fails closed.
154 AnchorKind kind = AnchorKind::Unset;
155
156 /// VtableIdentity: the MSVC mangled type name, e.g. ".?AVGameAudioEffect@engine@@".
157 std::string_view mangled;
158
159 /**
160 * @brief RipGlobal / CodeOperand: the candidate ladder resolving to the address or the instruction site.
161 * Borrowed.
162 */
163 std::span<const scan::Candidate> site;
164 /// CodeOperand: whether to read an immediate or a memory-operand displacement.
165 scan::OperandKind operand_kind = scan::OperandKind::Immediate;
166 /// CodeOperand: index into the instruction's VISIBLE operands.
167 std::uint8_t operand_index = 0;
168 /// CodeOperand: 0 preserves the decoded value; 1 through 8 narrows non-RIP low bytes and sign-extends.
169 std::uint8_t byte_width = 0;
170
171 /// StringXref: the exact literal content to anchor on (no quotes). Borrowed.
172 std::string_view xref_text;
173 /// StringXref: byte encoding of the literal in the image (Utf16le for wchar_t literals).
174 scan::StringEncoding xref_encoding = scan::StringEncoding::Utf8;
175 /// StringXref: whether to return the referencing instruction, its enclosing function, or the pointer slot.
176 scan::XrefReturn xref_return = scan::XrefReturn::ReferencingInstruction;
177 /// StringXref: match a trailing NUL so a prefix of a longer literal is not matched.
178 bool xref_require_terminator = true;
179 /// StringXref: keep the lea/mov shape scan and add the broad Zydis sweep for rarer reference shapes.
180 bool xref_broad_match = false;
181
182 /// Manual: the pinned literal value, taken as-is (unless @ref validate_manual runs the validator on it).
183 std::int64_t manual_value = 0;
184
185 /**
186 * @brief Optional post-resolve predicate; nullptr skips validation. Never applied to Manual unless
187 * @ref validate_manual, nor to CallArgHome; for a Quorum it runs once on the corroborated value.
188 */
189 AnchorValidator validator = nullptr;
190 /// Opaque pointer forwarded verbatim to @ref validator.
191 const void *validator_context = nullptr;
192 /// Run @ref validator on a Manual anchor too, instead of taking the pinned literal unchecked.
193 bool validate_manual = false;
194 /**
195 * @brief Reject a backend-resolvable anchor that carries no @ref validator (status Failed). Only the five
196 * backend kinds (VtableIdentity, RipGlobal, CodeOperand, StringXref, ExportName) are subject to
197 * this:
198 * a pinned Manual literal and a Quorum are both exempt. A Manual is not a resolved target, and a
199 * Quorum's N-of-M corroboration is already the verification.
200 */
201 bool require_validator = false;
202
203 /**
204 * @brief Quorum: the M candidate sub-anchors that vote on the target. Non-owning pointers into the caller's
205 * own anchor storage; every member must outlive the resolve call. The quorum fails closed (status
206 * @ref AnchorStatus::Failed) on a malformed declaration - fewer than two members, a null member, or
207 * a member that is itself a Quorum (nesting is bounded to one level).
208 */
209 std::span<const Anchor *const> quorum_members;
210 /**
211 * @brief Quorum: N, the minimum number of members that must resolve AND agree for the quorum to accept
212 * (N-of-M voting). 0 (the default) means unanimous: every member in @ref quorum_members must
213 * agree, so a two-member quorum with the default is the strict 2-of-2 corroboration. A quorum is
214 * corroboration, so an explicit N below 2 or above the member count is a malformed vote and fails
215 * the quorum closed rather than degrading to a single signal.
216 * @details Vote semantics: each resolved member value is a candidate center, and a center qualifies when
217 * at least N resolved votes agree with it. Two qualified centers that disagree yield
218 * @ref AnchorStatus::QuorumAmbiguous. Otherwise the vote commits the smallest qualified center,
219 * independent of member order. Under @ref QuorumMatch::WithinTolerance agreement is measured
220 * against that center, so the accepted members can span up to two tolerances.
221 */
222 std::size_t quorum_threshold = 0;
223 /// Quorum: how two resolved member values must relate for a vote to count them as agreeing.
224 QuorumMatch quorum_match = QuorumMatch::ExactValue;
225 /// Quorum: the tolerance for @ref QuorumMatch::WithinTolerance (a negative tolerance fails closed).
226 std::int64_t quorum_tolerance = 0;
227
228 /**
229 * @brief RipGlobal: page-protection class the byte-tier ladder scans. The @ref scan::Pages::Readable
230 * default lets a Direct rung resolve a plain global in `.rdata` / `.data`. Set
231 * @ref scan::Pages::Executable when every rung anchors on an in-image instruction. A byte twin in a
232 * data page then cannot demote a unique resolve to a fail-closed ambiguity. Ignored by CodeOperand
233 * and non-scan kinds.
234 */
235 scan::Pages pages = scan::Pages::Readable;
236
237 /**
238 * @brief ExportName: the module whose Export Address Table holds the export, e.g. "kernel32.dll". Empty
239 * (the default) resolves the export within the same @p scope the anchor is resolved against, so an
240 * anchor on the scanned module's own export needs no module name. A non-empty name is looked up
241 * through @ref Region::module_named at resolve time, so an ExportName in a foreign module (one
242 * independent of the table's shared scan scope) resolves correctly. Borrowed; ignored by every other
243 * kind.
244 */
245 std::string_view export_module;
246 /// ExportName: the exact, case-sensitive export symbol name (no decoration), e.g. "Sleep". Borrowed.
247 std::string_view export_name;
248 };
249
250 /**
251 * @enum ResultDomain
252 * @brief What a resolved anchor's value IS, so a binding cannot mutate through an incompatible target.
253 * @details From @ref declared_domain: a mid-hook binding needs a @ref CodeSite, a VMT binding a
254 * @ref VtableAddress, an address / pointer-chain write a real address (CodeSite or DataAddress). A
255 * @ref Scalar is a constant, not an address, and authorizes no write; @ref Unknown is the fail-closed
256 * default an unresolved or unsupported entry keeps.
257 */
258 enum class ResultDomain : std::uint8_t
259 {
260 /// A failed, unresolved, or unsupported entry: no resolved target. Authorizes no mutation.
261 Unknown,
262 /// An executable instruction site (an inline-hook or mid-hook target).
263 CodeSite,
264 /// A non-executable data address (a global variable or a resolved pointer slot).
265 DataAddress,
266 /// A class vtable base, keyed on its type identity (a VMT-hook target).
267 VtableAddress,
268 /// A decoded constant, not an address (a code immediate / displacement or a pinned Manual literal).
269 Scalar
270 };
271
272 /**
273 * @enum PhysicalSource
274 * @brief The normalized evidence backend that produced a resolved value.
275 */
276 enum class PhysicalSource : std::uint8_t
277 {
278 /// No resolved value (failed, unsupported, or unresolved).
279 None,
280 /// A Direct / RIP-relative byte-signature cascade (an @ref AnchorKind::RipGlobal).
281 ByteSignature,
282 /// A string-literal cross-reference (an @ref AnchorKind::StringXref).
283 StringLiteral,
284 /// A reverse-RTTI vtable identity (an @ref AnchorKind::VtableIdentity).
285 TypeIdentity,
286 /// A PE export-table walk (an @ref AnchorKind::ExportName).
287 ExportTable,
288 /// A decoded in-code immediate or displacement (an @ref AnchorKind::CodeOperand).
289 CodeOperand,
290 /// A pinned Manual literal with no backend (an @ref AnchorKind::Manual).
291 ManualPin,
292 /// A value corroborated by N-of-M voting (an @ref AnchorKind::Quorum), with no single physical source.
293 Corroborated
294 };
295
296 /**
297 * @enum WitnessCompleteness
298 * @brief Whether a resolved value came from a complete, authoritative view of its scope.
299 * @details A truncated or unauthoritative sweep fails before it can produce @ref Complete.
300 */
301 enum class WitnessCompleteness : std::uint8_t
302 {
303 /// No assessable completeness (the entry did not resolve).
304 Unknown,
305 /// Resolved over a complete, authoritative view of the scope.
306 Complete
307 };
308
309 /**
310 * @struct ResolvedWitness
311 * @brief The image, source, and completeness evidence carried by a resolved anchor.
312 * @details Populated only on @ref AnchorStatus::Resolved. Scalar values carry no image identity.
313 */
314 struct ResolvedWitness
315 {
316 /**
317 * @brief Identity of the module owning the resolved address; absent for a Scalar or synthetic address.
318 * @details Copies the accepted value-owner identity. Missing identity or owner/mapping drift through
319 * validation, quorum voting, or commit yields @ref AnchorStatus::Failed with no witness.
320 */
321 scan::ImageIdentity image{};
322 /// The normalized backend that produced the value.
323 PhysicalSource source = PhysicalSource::None;
324 /// For a @ref PhysicalSource::CodeOperand, which operand field was decoded; otherwise unused.
325 scan::OperandKind operand_kind = scan::OperandKind::Immediate;
326 /// Whether the resolve saw a complete, authoritative view.
327 WitnessCompleteness completeness = WitnessCompleteness::Unknown;
328 /**
329 * @brief The literal bytes of the span the winning byte-pattern rung matched; absent for every other kind.
330 * @details Present only when @ref AnchorKind::RipGlobal wins on a byte-pattern rung. Structural rungs
331 * and scalar results carry no matched span.
332 */
333 scan::WinningEvidence evidence{};
334 };
335
336 /**
337 * @struct ResolvedAnchor
338 * @brief One resolved entry in the drift report: the anchor's identity plus its outcome and value.
339 * @details The report array is the drift report itself: walk it once at init to log what resolved, what failed,
340 * and what is a pinned Manual literal and therefore at risk. @ref value is meaningful only when
341 * @ref status is @ref AnchorStatus::Resolved, and carries the quantity interpreted per @ref kind (a
342 * vtable or global address cast to int64, an in-code constant, or the manual literal).
343 */
344 struct ResolvedAnchor
345 {
346 /**
347 * @brief A borrowed view of @ref Anchor::label, not an owned copy: it aliases the source anchor's storage
348 * and shares its lifetime. Valid only while that anchor (canonically a `static` table entry that
349 * lives for the process) outlives the report; copy into owned storage before the source can end.
350 */
351 std::string_view label;
352 /// Copied from @ref Anchor::kind.
353 AnchorKind kind = AnchorKind::Unset;
354 /// The resolution outcome.
355 AnchorStatus status = AnchorStatus::Unresolved;
356 /// The resolved quantity, meaningful only when @ref status is @ref AnchorStatus::Resolved.
357 std::int64_t value = 0;
358 /**
359 * @brief What @ref value is, for binding-compatibility gating (see @ref ResultDomain). Set from
360 * @ref declared_domain when the entry resolves; @ref ResultDomain::Unknown otherwise.
361 */
362 ResultDomain domain = ResultDomain::Unknown;
363 /**
364 * @brief The resolved value's semantic-site witness; empty unless @ref status is Resolved.
365 */
366 ResolvedWitness witness{};
367 };
368
369 /**
370 * @struct AnchorQuality
371 * @brief A one-pass robustness summary of a drift report, for gating "is this manifest healthy enough to run?".
372 */
373 struct AnchorQuality
374 {
375 /// Total entries in the report.
376 std::size_t total = 0;
377 /// Entries that resolved.
378 std::size_t resolved = 0;
379 /// Entries that failed closed.
380 std::size_t failed = 0;
381 /// Entries whose kind has no resolver yet (CallArgHome).
382 std::size_t unsupported = 0;
383 /// Quorum entries rejected because their sub-anchors were not independent.
384 std::size_t not_independent = 0;
385 /// Pinned Manual literals that cannot self-heal (counted regardless of status).
386 std::size_t manual_at_risk = 0;
387 /// Corroborated quorums that resolved (the strongest evidence).
388 std::size_t corroborated = 0;
389 };
390
391 /**
392 * @enum GateVerdict
393 * @brief The startup decision a drift report yields: enable, enable-with-caution, or safe-disable.
394 * @details `[B-51]` The verdict lets a mod disable a feature before it uses unverified addresses. It prevents a
395 * low-quality log followed by a game-memory patch.
396 */
397 enum class GateVerdict : std::uint8_t
398 {
399 /// Healthy enough to enable outright: the resolve ratio met the threshold and no at-risk signal fired.
400 Pass,
401 /**
402 * @brief Resolved above the threshold, but a soft signal (a pinned Manual literal that cannot self-heal,
403 * or a report with nothing assessable) marks the resolution at-risk. The caller decides how to
404 * treat the risk.
405 */
406 Degraded,
407 /**
408 * @brief Below the threshold - too few anchors resolved, or too many failed. Safe-disable the feature
409 * rather than run it on addresses the manifest could not verify.
410 */
411 Fail
412 };
413
414 /**
415 * @struct GatePolicy
416 * @brief The thresholds that turn an @ref AnchorQuality summary into a @ref GateVerdict. Defaults fail closed.
417 * @details A plain value with no global state, so a mod can hold one policy per feature (a cosmetic overlay can
418 * tolerate a lower ratio than a frame-time camera patch that writes a live pointer). The defaults are
419 * the strictest: every resolvable anchor must heal and nothing may fail.
420 */
421 struct GatePolicy
422 {
423 /**
424 * @brief Minimum fraction, in [0, 1], of RESOLVABLE anchors (@ref AnchorQuality::total minus the
425 * unsupported @ref AnchorKind::CallArgHome kind) that must resolve for the gate to pass. A
426 * caller-supplied value outside [0, 1] is clamped; NaN is treated as the strict default. The default
427 * 1.0 requires every resolvable anchor to heal.
428 */
429 double min_resolved_ratio = 1.0;
430 /**
431 * @brief Hard cap on non-resolving failures (@ref AnchorQuality::failed plus @ref
432 * AnchorQuality::not_independent); exceeding it fails the gate regardless of the ratio. The default
433 * 0 tolerates no failure.
434 */
435 std::size_t max_failed = 0;
436 /**
437 * @brief When true (the default), any counted @ref AnchorQuality::manual_at_risk entry downgrades an
438 * otherwise-passing verdict to @ref GateVerdict::Degraded, because a Manual literal cannot
439 * self-heal across a patch.
440 */
441 bool manual_at_risk_degrades = true;
442 };
443
444 /**
445 * @brief Turns a drift-report robustness summary into a startup enable/disable decision.
446 * @param quality The summary from @ref assess_quality (or @ref diagnostics::Snapshot::anchor_quality).
447 * @param policy The thresholds; the default policy fails closed (every resolvable anchor must heal, zero
448 * failures tolerated).
449 * @return @ref GateVerdict::Fail when the report is below the threshold (safe-disable the feature), @ref
450 * GateVerdict::Degraded when it resolved but carries a soft risk, else @ref GateVerdict::Pass.
451 * @details For feature-granular gating, gate a sub-span of a shared report. The ratio denominator excludes
452 * the unsupported @ref AnchorKind::CallArgHome kind, which has no resolver. Every resolvable entry
453 * that did not resolve (a Failed anchor, a QuorumNotIndependent one, an untouched Unresolved slot)
454 * stays in the denominator, so a partial resolve fails closed. A report with nothing to assess is
455 * @ref GateVerdict::Degraded, never a false Pass. A hand-built @ref AnchorQuality whose status
456 * counts exceed @ref AnchorQuality::total fails closed to @ref GateVerdict::Fail.
457 * @note Callback-safe: pure threshold arithmetic over @p quality, allocation-free and side-effect-free.
458 */
459 [[nodiscard]] GateVerdict evaluate_gate(const AnchorQuality &quality, const GatePolicy &policy = {}) noexcept;
460
461 /**
462 * @brief Summarizes a drift report and gates it in one call.
463 * @param report The @ref ResolvedAnchor array (or a per-feature sub-span) produced by a resolve_all variant.
464 * @param policy The gate thresholds.
465 * @return The gate verdict for @p report under @p policy; equivalent to
466 * `evaluate_gate(assess_quality(report), policy)`.
467 * @note Callback-safe: one allocation-free tally pass plus the threshold arithmetic.
468 */
469 [[nodiscard]] GateVerdict
470 evaluate_gate(std::span<const ResolvedAnchor> report, const GatePolicy &policy = {}) noexcept;
471
472 /**
473 * @brief Maps a @ref GateVerdict to a short human-readable label.
474 * @param verdict The verdict.
475 * @return A static string view naming the verdict.
476 */
477 [[nodiscard]] std::string_view gate_verdict_to_string(GateVerdict verdict) noexcept;
478
479 /**
480 * @struct ScanProfile
481 * @brief A per-game bundle of setup-only scan-tuning DEFAULTS, applied as a plain value with no global state.
482 * @details It supplies defaults only: an explicit per-anchor choice still wins, so wiring a profile never
483 * overrides an explicit setting. The plain @ref resolve / @ref resolve_all are equivalent to resolving
484 * with an empty profile.
485 */
486 struct ScanProfile
487 {
488 /**
489 * @brief Widen the broad string-xref sweep on for StringXref anchors. It can only widen: a per-anchor
490 * @ref Anchor::xref_broad_match still wins, so this never forces broad mode off.
491 */
492 bool default_broad_string_xref = false;
493 /// The candidate ordering applied to RipGlobal / CodeOperand ladders (reuses the scan module's policy).
494 scan::CandidateOrder candidate_order = scan::CandidateOrder::AsDeclared;
495 /// A per-@ref AnchorKind deny-list. A denied backend fails closed (never silently replaced by another).
496 std::array<bool, ANCHOR_KIND_COUNT> deny_backend{};
497
498 /**
499 * @brief Reports whether @p kind's backend is denied by this profile.
500 * @param kind The anchor kind to test.
501 * @return true when the kind is in range and its deny-list slot is set.
502 */
503 411 [[nodiscard]] bool is_denied(AnchorKind kind) const noexcept
504 {
505 411 const auto index = static_cast<std::size_t>(kind);
506
4/4
✓ Branch 4 → 5 taken 410 times.
✓ Branch 4 → 8 taken 1 time.
✓ Branch 6 → 7 taken 4 times.
✓ Branch 6 → 8 taken 406 times.
822 return index < deny_backend.size() && deny_backend[index];
507 }
508 };
509
510 /**
511 * @brief Applies a profile's string-xref defaults to a query, widening broad-match only.
512 * @param profile The profile whose defaults to apply.
513 * @param query The base query (typically built from an anchor's xref_* fields).
514 * @return The query with @ref ScanProfile::default_broad_string_xref folded in (widen-only: an already-broad
515 * query stays broad, never downgraded).
516 */
517 [[nodiscard]] scan::StringRefQuery
518 apply_profile(const ScanProfile &profile, scan::StringRefQuery query) noexcept;
519
520 /**
521 * @brief Resolves one anchor through its backend, fail-closed.
522 * @param anchor The anchor to resolve.
523 * @param scope One module image or reserved allocation to resolve within; defaults to the host executable.
524 * Scoping is load-bearing: the same vtable name or instruction shape can exist in several loaded
525 * modules, so a scope-backed anchor fails closed when the range crosses allocation boundaries.
526 * @return A @ref ResolvedAnchor carrying the outcome and (on success) the value.
527 * @details Rechecks the scope's single-allocation identity through commit, including scope-backed quorum
528 * members. An explicit ExportName module may differ from the common scope.
529 * @note Setup/control-plane only: the resolve runs its backend scan, which can allocate and walk pages.
530 */
531 [[nodiscard]] ResolvedAnchor resolve(const Anchor &anchor, Region scope = Region::host());
532
533 /**
534 * @brief Resolves a table of anchors serially, writing one @ref ResolvedAnchor per input.
535 * @param anchors The anchor table.
536 * @param out The report buffer; at most `min(anchors.size(), out.size())` entries are written.
537 * @param scope The module image to resolve within.
538 * @return The number of entries written.
539 * @note Setup/control-plane only (see @ref resolve).
540 */
541 [[nodiscard]] std::size_t
542 resolve_all(std::span<const Anchor> anchors, std::span<ResolvedAnchor> out, Region scope = Region::host());
543
544 /**
545 * @brief Resolves a table of independent anchors concurrently through a fork-join worker pool.
546 * @param anchors The anchor table.
547 * @param out The report buffer; at most `min(anchors.size(), out.size())` entries are written, in input order.
548 * @param scope The module image to resolve within.
549 * @param max_workers Upper bound on worker threads (0 = auto-select from hardware_concurrency, clamped).
550 * @return The number of entries written.
551 * @details Each anchor still goes through the single-anchor @ref resolve path, so backend failures, validators,
552 * quorum checks, and result ordering all match @ref resolve_all. It is opt-in because validators run
553 * concurrently; use the serial @ref resolve_all when a validator context is order-dependent or must be
554 * externally serialized.
555 * @note Setup/control-plane only: spawns a worker pool. Never call it from a hook or under the loader lock.
556 */
557 [[nodiscard]] std::size_t resolve_all_parallel(
558 std::span<const Anchor> anchors,
559 std::span<ResolvedAnchor> out,
560 Region scope = Region::host(),
561 std::size_t max_workers = 0
562 );
563
564 /**
565 * @brief Rolls a drift report into an @ref AnchorQuality summary in one allocation-free pass (no re-resolve).
566 * @param report The @ref ResolvedAnchor array produced by a resolve_all variant.
567 * @return The tallied summary.
568 * @note Callback-safe: one allocation-free tally pass over @p report.
569 */
570 [[nodiscard]] AnchorQuality assess_quality(std::span<const ResolvedAnchor> report) noexcept;
571
572 /**
573 * @brief Hashes an anchor's resolution EVIDENCE into a stable 64-bit diff key, excluding the resolved address.
574 * @param anchor The anchor to fingerprint.
575 * @return A 64-bit FNV-1a hash of the declarative inputs the backend uses.
576 * @details The fingerprint excludes the resolved address, the cosmetic @ref Anchor::label, and the candidate
577 * names, so it stays stable when only the address drifts. Persist it next to each resolved value. A
578 * moved value with the same fingerprint is self-healed drift. A changed fingerprint means that the
579 * signature itself changed and needs a new review. A byte tier hashes the
580 * compiled Pattern's bytes, mask, and decode parameters. A Quorum combines every member's evidence
581 * order-independently and folds in the effective vote threshold, agreement mode, and tolerance. It
582 * reads only the declarative views, resolves nothing, and allocates nothing.
583 * @note Callback-safe: allocation-free and side-effect-free (see @ref anchor_trust_fingerprint).
584 */
585 [[nodiscard]] std::uint64_t anchor_fingerprint(const Anchor &anchor) noexcept;
586
587 /**
588 * @brief Hashes an anchor's definition evidence together with its effective live-image identity.
589 * @param anchor The anchor to fingerprint.
590 * @param scope_identity The @ref scan::ImageIdentity of the module the anchor effectively resolves against
591 * (@ref scan::image_identity of that module).
592 * @return A 64-bit scope-bound trust key.
593 * @details ASLR does not affect the key. For @ref AnchorKind::ExportName, the effective identity replaces the
594 * declared module spelling so inherited and explicit spellings of the same module agree.
595 * @note Callback-safe: allocation-free and side-effect-free.
596 */
597 [[nodiscard]] std::uint64_t
598 anchor_trust_fingerprint(const Anchor &anchor, scan::ImageIdentity scope_identity) noexcept;
599
600 /**
601 * @brief Maps an @ref AnchorStatus to a short human-readable label.
602 * @param status The status.
603 * @return A static string view naming the status.
604 */
605 [[nodiscard]] std::string_view anchor_status_to_string(AnchorStatus status) noexcept;
606
607 /**
608 * @brief The @ref ResultDomain an anchor is declared to resolve, for binding-compatibility gating.
609 * @param anchor The anchor.
610 * @return The domain implied by @ref Anchor::kind: a VtableIdentity is a VtableAddress, a CodeOperand or Manual
611 * a Scalar, a StringXref a CodeSite (a DataAddress for a StringPointerSlot return), an ExportName
612 * provisionally a CodeSite, a RipGlobal a CodeSite only when @ref Anchor::pages narrows it to
613 * executable pages (else a DataAddress), and a Quorum the single specific domain its members agree on
614 * (Unknown when they conflict, or for CallArgHome / Unset). @ref ResolvedAnchor::domain follows the
615 * live page class instead: a code-site kind committed at a non-executable address is stamped
616 * @ref ResultDomain::DataAddress. Allocation-free and side-effect-free.
617 */
618 [[nodiscard]] ResultDomain declared_domain(const Anchor &anchor) noexcept;
619
620 /**
621 * @brief Maps a @ref ResultDomain to a short human-readable label.
622 * @param domain The domain.
623 * @return A static string view naming the domain.
624 */
625 [[nodiscard]] std::string_view result_domain_to_string(ResultDomain domain) noexcept;
626
627 /**
628 * @brief Maps a @ref PhysicalSource to a short human-readable label.
629 * @param source The physical source.
630 * @return A static string view naming the source.
631 */
632 [[nodiscard]] std::string_view physical_source_to_string(PhysicalSource source) noexcept;
633
634 /**
635 * @brief Resolves one anchor with a profile's defaults applied (deny-list, candidate order, broad-string
636 * widen).
637 * @param anchor The anchor to resolve.
638 * @param profile The per-game defaults. A denied backend fails closed; the profile threads into Quorum
639 * sub-anchors, so a denied sub-anchor kind fails the quorum closed.
640 * @param scope The module image to resolve within.
641 * @return A @ref ResolvedAnchor carrying the outcome and (on success) the value.
642 * @note Setup/control-plane only (see @ref resolve).
643 */
644 [[nodiscard]] ResolvedAnchor
645 resolve_with_profile(const Anchor &anchor, const ScanProfile &profile, Region scope = Region::host());
646
647 /**
648 * @brief Resolves a table serially with a profile's defaults applied.
649 * @param anchors The anchor table.
650 * @param out The report buffer; at most `min(anchors.size(), out.size())` entries are written.
651 * @param profile The per-game defaults.
652 * @param scope The module image to resolve within.
653 * @return The number of entries written.
654 * @note Setup/control-plane only (see @ref resolve).
655 */
656 [[nodiscard]] std::size_t resolve_all_with_profile(
657 std::span<const Anchor> anchors,
658 std::span<ResolvedAnchor> out,
659 const ScanProfile &profile,
660 Region scope = Region::host()
661 );
662
663 /**
664 * @brief Resolves a table concurrently with a profile's defaults applied.
665 * @param anchors The anchor table.
666 * @param out The report buffer; at most `min(anchors.size(), out.size())` entries are written, in input order.
667 * @param profile The per-game defaults.
668 * @param scope The module image to resolve within.
669 * @param max_workers Upper bound on worker threads (0 = auto-select).
670 * @return The number of entries written.
671 * @note Setup/control-plane only: spawns a worker pool. Never call it from a hook or under the loader lock.
672 */
673 [[nodiscard]] std::size_t resolve_all_with_profile_parallel(
674 std::span<const Anchor> anchors,
675 std::span<ResolvedAnchor> out,
676 const ScanProfile &profile,
677 Region scope = Region::host(),
678 std::size_t max_workers = 0
679 );
680 } // namespace anchor
681 } // namespace DetourModKit
682
683 #endif // DETOURMODKIT_ANCHOR_HPP
684