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 | * @warning `[B-100]` Under the loader lock, call only the Callback-safe trust and quality queries. Resolution can | ||
| 8 | * allocate, query loader state, scan memory, or create threads. | ||
| 9 | */ | ||
| 10 | |||
| 11 | #include "DetourModKit/error.hpp" | ||
| 12 | #include "DetourModKit/region.hpp" | ||
| 13 | #include "DetourModKit/scan.hpp" | ||
| 14 | |||
| 15 | #include <array> | ||
| 16 | #include <cstddef> | ||
| 17 | #include <cstdint> | ||
| 18 | #include <span> | ||
| 19 | #include <string_view> | ||
| 20 | |||
| 21 | namespace DetourModKit | ||
| 22 | { | ||
| 23 | namespace anchor | ||
| 24 | { | ||
| 25 | /** @brief Which backend resolves an anchor. `docs/guides/scanning/anchors.md` ranks the kinds by resilience. */ | ||
| 26 | enum class AnchorKind : std::uint8_t | ||
| 27 | { | ||
| 28 | /// A class vtable address keyed on its mangled name (@ref rtti::vtable_for_type). | ||
| 29 | VtableIdentity, | ||
| 30 | /// An absolute address from a Direct or RIP-relative candidate cascade (@ref scan::resolve). | ||
| 31 | RipGlobal, | ||
| 32 | /// An in-code immediate or `[reg + disp]` displacement (@ref scan::read_code_constant). | ||
| 33 | CodeOperand, | ||
| 34 | /// A reference to an immutable string literal (@ref scan::find_string_xref). | ||
| 35 | StringXref, | ||
| 36 | /// A pinned literal with no backend. It cannot self-heal, so @ref AnchorQuality counts it as at risk. | ||
| 37 | Manual, | ||
| 38 | /// A reserved prologue-dataflow kind with no resolver, which reports @ref AnchorStatus::Unsupported. | ||
| 39 | CallArgHome, | ||
| 40 | /// A value accepted only when at least N of M independent sub-anchors resolve and agree. | ||
| 41 | Quorum, | ||
| 42 | /// A named export read from its module's PE Export Address Table (@ref scan::resolve_export). | ||
| 43 | ExportName, | ||
| 44 | /// The default for an omitted kind, which reports @ref AnchorStatus::Failed instead of a trusted address 0. | ||
| 45 | Unset | ||
| 46 | }; | ||
| 47 | |||
| 48 | /// The number of @ref AnchorKind enumerators, which sizes the per-kind deny-list in @ref ScanProfile. | ||
| 49 | inline constexpr std::size_t ANCHOR_KIND_COUNT = 9; | ||
| 50 | static_assert( | ||
| 51 | static_cast<std::size_t>(AnchorKind::Unset) + 1 == ANCHOR_KIND_COUNT, | ||
| 52 | "ANCHOR_KIND_COUNT must track the AnchorKind enumerator count." | ||
| 53 | ); | ||
| 54 | |||
| 55 | /** @brief How a @ref AnchorKind::Quorum decides that two resolved member values agree. */ | ||
| 56 | enum class QuorumMatch : std::uint8_t | ||
| 57 | { | ||
| 58 | /// Two member values agree only when identical. | ||
| 59 | ExactValue, | ||
| 60 | /// Two member values agree when their gap is at most @ref Anchor::quorum_tolerance. | ||
| 61 | WithinTolerance | ||
| 62 | }; | ||
| 63 | |||
| 64 | /** @brief The resolution outcome of one anchor. */ | ||
| 65 | enum class AnchorStatus : std::uint8_t | ||
| 66 | { | ||
| 67 | /// The initial state of an untouched slot, which a resolve never returns. | ||
| 68 | Unresolved, | ||
| 69 | /// The backend resolved a value, and every applicable validator and corroboration check passed. | ||
| 70 | Resolved, | ||
| 71 | /// A backend miss, validator rejection, denied backend, or failed quorum, with no invented value. | ||
| 72 | Failed, | ||
| 73 | /// The kind has no resolver (@ref AnchorKind::CallArgHome). | ||
| 74 | Unsupported, | ||
| 75 | /// A quorum's members were not all pairwise-independent evidence. | ||
| 76 | QuorumNotIndependent, | ||
| 77 | /** @brief Values that disagree each reached the quorum threshold. Member order never picks a winner. */ | ||
| 78 | QuorumAmbiguous | ||
| 79 | }; | ||
| 80 | |||
| 81 | /** @brief A post-resolve predicate. A false return fails the anchor (@ref AnchorStatus::Failed, value 0). */ | ||
| 82 | using AnchorValidator = bool (*)(std::int64_t value, const void *context) noexcept; | ||
| 83 | |||
| 84 | /** @brief A registry entry. A kind-labeled field applies only to that kind. Views must outlive the resolve. */ | ||
| 85 | struct Anchor | ||
| 86 | { | ||
| 87 | /// An identifier echoed into the @ref ResolvedAnchor. | ||
| 88 | std::string_view label; | ||
| 89 | /// Which backend resolves this anchor. | ||
| 90 | AnchorKind kind = AnchorKind::Unset; | ||
| 91 | |||
| 92 | /// VtableIdentity: the MSVC mangled type name, for example ".?AVGameAudioEffect@engine@@". | ||
| 93 | std::string_view mangled; | ||
| 94 | |||
| 95 | /// RipGlobal / CodeOperand: the candidate ladder that resolves to the address or the instruction site. | ||
| 96 | std::span<const scan::Candidate> site; | ||
| 97 | /// CodeOperand: whether to read an immediate or a memory-operand displacement. | ||
| 98 | scan::OperandKind operand_kind = scan::OperandKind::Immediate; | ||
| 99 | /// CodeOperand: the index into the instruction's visible operands. | ||
| 100 | std::uint8_t operand_index = 0; | ||
| 101 | /// CodeOperand: 0 keeps the decoded value, and 1 through 8 narrows non-RIP low bytes and sign-extends. | ||
| 102 | std::uint8_t byte_width = 0; | ||
| 103 | |||
| 104 | /// StringXref: the exact literal content to anchor on, without quotes. | ||
| 105 | std::string_view xref_text; | ||
| 106 | /// StringXref: the byte encoding of the literal in the image (Utf16le for wchar_t literals). | ||
| 107 | scan::StringEncoding xref_encoding = scan::StringEncoding::Utf8; | ||
| 108 | /// StringXref: whether to return the referencing instruction, its enclosing function, or the pointer slot. | ||
| 109 | scan::XrefReturn xref_return = scan::XrefReturn::ReferencingInstruction; | ||
| 110 | /// StringXref: if true, match a trailing NUL so that a prefix of a longer literal does not match. | ||
| 111 | bool xref_require_terminator = true; | ||
| 112 | /// StringXref: if true, add the broad sweep for rarer reference shapes to the lea/mov shape scan. | ||
| 113 | bool xref_broad_match = false; | ||
| 114 | |||
| 115 | /// Manual: the pinned literal value, taken as-is unless @ref validate_manual is set. | ||
| 116 | std::int64_t manual_value = 0; | ||
| 117 | |||
| 118 | /** @brief An optional post-resolve predicate. For a Quorum, it runs once on the corroborated value. */ | ||
| 119 | AnchorValidator validator = nullptr; | ||
| 120 | /// An opaque pointer forwarded verbatim to @ref validator. | ||
| 121 | const void *validator_context = nullptr; | ||
| 122 | /// If true, @ref validator also checks a Manual literal. | ||
| 123 | bool validate_manual = false; | ||
| 124 | /// If true, a backend-resolved anchor with no @ref validator reports Failed. Manual and Quorum are exempt. | ||
| 125 | bool require_validator = false; | ||
| 126 | |||
| 127 | /** | ||
| 128 | * @brief Quorum: the M sub-anchors that vote. Each member must outlive the resolve call. Fewer than two | ||
| 129 | * members, a null member, or a nested Quorum fails the quorum (@ref AnchorStatus::Failed). | ||
| 130 | */ | ||
| 131 | std::span<const Anchor *const> quorum_members; | ||
| 132 | /** | ||
| 133 | * @brief Quorum: N, the minimum number of resolved members that must agree. The default 0 means all M | ||
| 134 | * members, and an explicit N below 2 or above M fails the quorum. | ||
| 135 | * @details A resolved member value qualifies as a center when at least N resolved votes agree with it. | ||
| 136 | * Qualified centers that disagree give @ref AnchorStatus::QuorumAmbiguous. Otherwise the smallest | ||
| 137 | * qualified center wins. @ref QuorumMatch::WithinTolerance measures agreement against that | ||
| 138 | * center, so the accepted members can span up to two tolerances. | ||
| 139 | */ | ||
| 140 | std::size_t quorum_threshold = 0; | ||
| 141 | /// Quorum: the rule that decides whether two resolved member values agree. | ||
| 142 | QuorumMatch quorum_match = QuorumMatch::ExactValue; | ||
| 143 | /// Quorum: the tolerance for @ref QuorumMatch::WithinTolerance, where a negative value fails closed. | ||
| 144 | std::int64_t quorum_tolerance = 0; | ||
| 145 | |||
| 146 | /** | ||
| 147 | * @brief RipGlobal: the page class the byte-tier ladder scans. The @ref scan::Pages::Readable default also | ||
| 148 | * matches a global in `.rdata` / `.data`. If every rung anchors on an in-image instruction, set | ||
| 149 | * @ref scan::Pages::Executable. A data-page byte twin then cannot make a unique resolve ambiguous. | ||
| 150 | */ | ||
| 151 | scan::Pages pages = scan::Pages::Readable; | ||
| 152 | |||
| 153 | /// ExportName: the module name for @ref Region::module_named, or empty for the resolve scope. | ||
| 154 | std::string_view export_module; | ||
| 155 | /// ExportName: the exact, case-sensitive export symbol name without decoration, for example "Sleep". | ||
| 156 | std::string_view export_name; | ||
| 157 | }; | ||
| 158 | |||
| 159 | /** @brief What a resolved value is. See @ref manifest::GatePolicy::require_mutation_safe_binding. */ | ||
| 160 | enum class ResultDomain : std::uint8_t | ||
| 161 | { | ||
| 162 | /// An entry that did not resolve, or a Quorum with conflicting member domains. It authorizes no mutation. | ||
| 163 | Unknown, | ||
| 164 | /// An executable instruction site (an inline-hook or mid-hook target). | ||
| 165 | CodeSite, | ||
| 166 | /// A non-executable data address (a global variable or a resolved pointer slot). | ||
| 167 | DataAddress, | ||
| 168 | /// A class vtable base, keyed on its type identity (a VMT-hook target). | ||
| 169 | VtableAddress, | ||
| 170 | /// A CodeOperand value or pinned Manual literal. It authorizes no write, even when it holds an address. | ||
| 171 | Scalar | ||
| 172 | }; | ||
| 173 | |||
| 174 | /** @brief The normalized evidence backend that produced a resolved value. */ | ||
| 175 | enum class PhysicalSource : std::uint8_t | ||
| 176 | { | ||
| 177 | /// No resolved value (failed, unsupported, or unresolved). | ||
| 178 | None, | ||
| 179 | /// A Direct / RIP-relative byte-signature cascade (an @ref AnchorKind::RipGlobal). | ||
| 180 | ByteSignature, | ||
| 181 | /// A string-literal cross-reference (an @ref AnchorKind::StringXref). | ||
| 182 | StringLiteral, | ||
| 183 | /// A reverse-RTTI vtable identity (an @ref AnchorKind::VtableIdentity). | ||
| 184 | TypeIdentity, | ||
| 185 | /// A PE export-table walk (an @ref AnchorKind::ExportName). | ||
| 186 | ExportTable, | ||
| 187 | /// A decoded in-code immediate or displacement (an @ref AnchorKind::CodeOperand). | ||
| 188 | CodeOperand, | ||
| 189 | /// A pinned Manual literal with no backend (an @ref AnchorKind::Manual). | ||
| 190 | ManualPin, | ||
| 191 | /// A value corroborated by N-of-M voting (an @ref AnchorKind::Quorum), with no single physical source. | ||
| 192 | Corroborated | ||
| 193 | }; | ||
| 194 | |||
| 195 | /** @brief Whether a resolved value came from a complete, authoritative view. A partial sweep fails instead. */ | ||
| 196 | enum class WitnessCompleteness : std::uint8_t | ||
| 197 | { | ||
| 198 | /// No assessable completeness, because the entry did not resolve. | ||
| 199 | Unknown, | ||
| 200 | /// Resolved over a complete, authoritative view of the scope. | ||
| 201 | Complete | ||
| 202 | }; | ||
| 203 | |||
| 204 | /** @brief The evidence behind a resolved value, populated only on @ref AnchorStatus::Resolved. */ | ||
| 205 | struct ResolvedWitness | ||
| 206 | { | ||
| 207 | /** | ||
| 208 | * @brief Identity of the module that owns the resolved address. Absent for a Scalar or synthetic address. | ||
| 209 | * An unreadable identity, or owner or mapping drift at validation, vote, or commit, gives | ||
| 210 | * @ref AnchorStatus::Failed with no witness. | ||
| 211 | */ | ||
| 212 | scan::ImageIdentity image{}; | ||
| 213 | /// The normalized backend that produced the value. | ||
| 214 | PhysicalSource source = PhysicalSource::None; | ||
| 215 | /// For a @ref PhysicalSource::CodeOperand, the decoded operand field, and otherwise unused. | ||
| 216 | scan::OperandKind operand_kind = scan::OperandKind::Immediate; | ||
| 217 | /// Whether the resolve saw a complete, authoritative view. | ||
| 218 | WitnessCompleteness completeness = WitnessCompleteness::Unknown; | ||
| 219 | /// The bytes that the byte-pattern rung matched, present only when a RipGlobal resolves on a byte rung. | ||
| 220 | scan::WinningEvidence evidence{}; | ||
| 221 | }; | ||
| 222 | |||
| 223 | /** @brief One entry in the drift report: the anchor's identity, outcome, and value. */ | ||
| 224 | struct ResolvedAnchor | ||
| 225 | { | ||
| 226 | /// A copy of the @ref Anchor::label view, valid only while the characters it borrows live. | ||
| 227 | std::string_view label; | ||
| 228 | /// Copied from @ref Anchor::kind. | ||
| 229 | AnchorKind kind = AnchorKind::Unset; | ||
| 230 | /// The resolution outcome. | ||
| 231 | AnchorStatus status = AnchorStatus::Unresolved; | ||
| 232 | /** @brief An address cast to int64, or a constant. Meaningful only when @ref status is Resolved. */ | ||
| 233 | std::int64_t value = 0; | ||
| 234 | /** | ||
| 235 | * @brief What @ref value is. A resolved entry gets @ref declared_domain, but a CodeSite at a non-executable | ||
| 236 | * address becomes @ref ResultDomain::DataAddress. Any other entry stays Unknown. | ||
| 237 | */ | ||
| 238 | ResultDomain domain = ResultDomain::Unknown; | ||
| 239 | /// The evidence behind @ref value (see @ref ResolvedWitness). | ||
| 240 | ResolvedWitness witness{}; | ||
| 241 | }; | ||
| 242 | |||
| 243 | /** @brief A robustness summary of a drift report, the input to @ref evaluate_gate. */ | ||
| 244 | struct AnchorQuality | ||
| 245 | { | ||
| 246 | /// Total entries in the report. | ||
| 247 | std::size_t total = 0; | ||
| 248 | /// Entries that resolved. | ||
| 249 | std::size_t resolved = 0; | ||
| 250 | /// Entries that failed closed (Failed or QuorumAmbiguous). | ||
| 251 | std::size_t failed = 0; | ||
| 252 | /// Entries whose kind has no resolver (CallArgHome). | ||
| 253 | std::size_t unsupported = 0; | ||
| 254 | /// Quorum entries rejected because their sub-anchors were not independent. | ||
| 255 | std::size_t not_independent = 0; | ||
| 256 | /// Pinned Manual literals that cannot self-heal (counted regardless of status). | ||
| 257 | std::size_t manual_at_risk = 0; | ||
| 258 | /// Corroborated quorums that resolved. | ||
| 259 | std::size_t corroborated = 0; | ||
| 260 | }; | ||
| 261 | |||
| 262 | /** @brief `[B-51]` The startup decision for a drift report: enable, enable with caution, or safe-disable. */ | ||
| 263 | enum class GateVerdict : std::uint8_t | ||
| 264 | { | ||
| 265 | /// Healthy enough to enable: within the thresholds, with no at-risk signal. | ||
| 266 | Pass, | ||
| 267 | /// Within the thresholds, but a pinned Manual literal or a report with nothing assessable adds risk. | ||
| 268 | Degraded, | ||
| 269 | /** @brief Too few anchors resolved, or too many failed. Safe-disable the feature. */ | ||
| 270 | Fail | ||
| 271 | }; | ||
| 272 | |||
| 273 | /** @brief The thresholds for @ref evaluate_gate. The defaults are the strictest and fail closed. */ | ||
| 274 | struct GatePolicy | ||
| 275 | { | ||
| 276 | /** @brief Minimum resolved fraction of all non-Unsupported entries, clamped to [0, 1]. NaN means 1.0. */ | ||
| 277 | double min_resolved_ratio = 1.0; | ||
| 278 | /** @brief Cap on failed plus not_independent entries. A count above it fails the gate at any ratio. */ | ||
| 279 | std::size_t max_failed = 0; | ||
| 280 | /// If true, a nonzero @ref AnchorQuality::manual_at_risk count turns Pass into @ref GateVerdict::Degraded. | ||
| 281 | bool manual_at_risk_degrades = true; | ||
| 282 | }; | ||
| 283 | |||
| 284 | /** | ||
| 285 | * @brief Turns a drift-report summary into a startup verdict. | ||
| 286 | * @details A report with nothing to assess is @ref GateVerdict::Degraded. Status counts above | ||
| 287 | * @ref AnchorQuality::total yield @ref GateVerdict::Fail. | ||
| 288 | * @note Callback-safe: allocation-free and side-effect-free. | ||
| 289 | */ | ||
| 290 | [[nodiscard]] GateVerdict evaluate_gate(const AnchorQuality &quality, const GatePolicy &policy = {}) noexcept; | ||
| 291 | |||
| 292 | /** | ||
| 293 | * @brief Returns `evaluate_gate(assess_quality(report), policy)`. @p report can be a per-feature sub-span. | ||
| 294 | * @note Callback-safe: one allocation-free tally pass plus the threshold arithmetic. | ||
| 295 | */ | ||
| 296 | [[nodiscard]] GateVerdict | ||
| 297 | evaluate_gate(std::span<const ResolvedAnchor> report, const GatePolicy &policy = {}) noexcept; | ||
| 298 | |||
| 299 | /** @brief Maps a @ref GateVerdict to a short static label. */ | ||
| 300 | [[nodiscard]] std::string_view gate_verdict_to_string(GateVerdict verdict) noexcept; | ||
| 301 | |||
| 302 | /** @brief Per-game scan tuning that applies to every anchor in a profiled resolve. */ | ||
| 303 | struct ScanProfile | ||
| 304 | { | ||
| 305 | /// Enables the broad sweep for every StringXref anchor, but never turns broad mode off. | ||
| 306 | bool default_broad_string_xref = false; | ||
| 307 | /// The candidate order for RipGlobal and CodeOperand ladders. | ||
| 308 | scan::CandidateOrder candidate_order = scan::CandidateOrder::AsDeclared; | ||
| 309 | /// A per-@ref AnchorKind deny-list. A denied backend fails closed, and no other backend replaces it. | ||
| 310 | std::array<bool, ANCHOR_KIND_COUNT> deny_backend{}; | ||
| 311 | |||
| 312 | /** @brief Reports whether @p kind is in range and denied by this profile. */ | ||
| 313 | 414 | [[nodiscard]] bool is_denied(AnchorKind kind) const noexcept | |
| 314 | { | ||
| 315 | 414 | const auto index = static_cast<std::size_t>(kind); | |
| 316 | 828 | return index < deny_backend.size() && deny_backend[index]; | |
| 317 | } | ||
| 318 | }; | ||
| 319 | |||
| 320 | /** @brief Returns @p query with @ref ScanProfile::default_broad_string_xref applied. It only widens. */ | ||
| 321 | [[nodiscard]] scan::StringRefQuery | ||
| 322 | apply_profile(const ScanProfile &profile, scan::StringRefQuery query) noexcept; | ||
| 323 | |||
| 324 | /** | ||
| 325 | * @brief Resolves one anchor through its backend, fail-closed. | ||
| 326 | * @param scope One module image or reserved allocation. A VtableIdentity, RipGlobal, CodeOperand, or StringXref | ||
| 327 | * anchor, quorum members included, fails closed if the range spans several allocations. It also | ||
| 328 | * fails closed if the owner or mapping at commit differs from the one captured before the scan. | ||
| 329 | * @note Setup/control-plane only: the backend scan can allocate and walk pages. | ||
| 330 | */ | ||
| 331 | [[nodiscard]] ResolvedAnchor resolve(const Anchor &anchor, Region scope = Region::host()); | ||
| 332 | |||
| 333 | /** | ||
| 334 | * @brief Serially resolves `min(anchors.size(), out.size())` anchors into @p out and returns that count. | ||
| 335 | * @note Setup/control-plane only (see @ref resolve). | ||
| 336 | */ | ||
| 337 | [[nodiscard]] std::size_t | ||
| 338 | resolve_all(std::span<const Anchor> anchors, std::span<ResolvedAnchor> out, Region scope = Region::host()); | ||
| 339 | |||
| 340 | /** | ||
| 341 | * @brief @ref resolve_all on a fork-join worker pool, with results in input order. | ||
| 342 | * @param max_workers Upper bound on threads. 0 auto-selects from `hardware_concurrency`, clamped to the count. | ||
| 343 | * @details Validators run concurrently. If one is order-dependent or thread-unsafe, use @ref resolve_all. If a | ||
| 344 | * resolve throws, this call writes a Failed entry for that anchor and does not rethrow. | ||
| 345 | * @note Setup/control-plane only: spawns and joins a worker pool. | ||
| 346 | * @warning Never call it under the loader lock, where the worker join hangs. | ||
| 347 | */ | ||
| 348 | [[nodiscard]] std::size_t resolve_all_parallel( | ||
| 349 | std::span<const Anchor> anchors, | ||
| 350 | std::span<ResolvedAnchor> out, | ||
| 351 | Region scope = Region::host(), | ||
| 352 | std::size_t max_workers = 0 | ||
| 353 | ); | ||
| 354 | |||
| 355 | /** | ||
| 356 | * @brief Rolls a drift report into an @ref AnchorQuality without a new resolve. | ||
| 357 | * @note Callback-safe: one allocation-free tally pass over @p report. | ||
| 358 | */ | ||
| 359 | [[nodiscard]] AnchorQuality assess_quality(std::span<const ResolvedAnchor> report) noexcept; | ||
| 360 | |||
| 361 | /** | ||
| 362 | * @brief Hashes an anchor's declared resolution evidence into a 64-bit FNV-1a diff key. | ||
| 363 | * @details The key excludes the resolved address, @ref Anchor::label, and candidate names. It is identical on | ||
| 364 | * every load of the same declaration, so a persisted key can serve as a drift baseline. A Quorum | ||
| 365 | * folds in its members independent of order, plus its effective threshold, match rule, and tolerance. | ||
| 366 | * @note Callback-safe: reads only the declaration, resolves nothing, and allocates nothing. | ||
| 367 | */ | ||
| 368 | [[nodiscard]] std::uint64_t anchor_fingerprint(const Anchor &anchor) noexcept; | ||
| 369 | |||
| 370 | /** | ||
| 371 | * @brief Hashes the @ref anchor_fingerprint evidence with @p scope_identity, the identity of the module that | ||
| 372 | * the anchor resolves against. ASLR does not change the key. For ExportName, @p scope_identity | ||
| 373 | * replaces the declared module name, so inherited and explicit spellings of one module give one key. | ||
| 374 | * @note Callback-safe: allocation-free and side-effect-free. | ||
| 375 | */ | ||
| 376 | [[nodiscard]] std::uint64_t | ||
| 377 | anchor_trust_fingerprint(const Anchor &anchor, scan::ImageIdentity scope_identity) noexcept; | ||
| 378 | |||
| 379 | /** @brief Maps an @ref AnchorStatus to a short static label. */ | ||
| 380 | [[nodiscard]] std::string_view anchor_status_to_string(AnchorStatus status) noexcept; | ||
| 381 | |||
| 382 | /** | ||
| 383 | * @brief The @ref ResultDomain that @p anchor declares. Allocation-free and side-effect-free. | ||
| 384 | * @return VtableIdentity gives VtableAddress, and CodeOperand or Manual gives Scalar. StringXref gives | ||
| 385 | * CodeSite, or DataAddress for a StringPointerSlot return. ExportName gives a provisional CodeSite (see | ||
| 386 | * @ref ResolvedAnchor::domain). RipGlobal gives CodeSite only on executable @ref Anchor::pages, else | ||
| 387 | * DataAddress. CallArgHome, Unset, and an out-of-range enum or @ref Anchor::byte_width value in a field | ||
| 388 | * that the kind reads give Unknown. A Quorum gives the one address domain that its members declare, | ||
| 389 | * Scalar if none does, or Unknown if two members declare different ones. | ||
| 390 | */ | ||
| 391 | [[nodiscard]] ResultDomain declared_domain(const Anchor &anchor) noexcept; | ||
| 392 | |||
| 393 | /** @brief Maps a @ref ResultDomain to a short static label. */ | ||
| 394 | [[nodiscard]] std::string_view result_domain_to_string(ResultDomain domain) noexcept; | ||
| 395 | |||
| 396 | /** @brief Maps a @ref PhysicalSource to a short static label. */ | ||
| 397 | [[nodiscard]] std::string_view physical_source_to_string(PhysicalSource source) noexcept; | ||
| 398 | |||
| 399 | /** | ||
| 400 | * @brief @ref resolve with the deny-list, candidate order, and broad-string default of @p profile applied. | ||
| 401 | * @param profile Quorum members use the same profile, so a denied member fails and casts no vote. An empty | ||
| 402 | * profile gives the @ref resolve result. | ||
| 403 | * @note Setup/control-plane only (see @ref resolve). | ||
| 404 | */ | ||
| 405 | [[nodiscard]] ResolvedAnchor | ||
| 406 | resolve_with_profile(const Anchor &anchor, const ScanProfile &profile, Region scope = Region::host()); | ||
| 407 | |||
| 408 | /** | ||
| 409 | * @brief @ref resolve_all with @p profile applied (see @ref resolve_with_profile). | ||
| 410 | * @note Setup/control-plane only (see @ref resolve). | ||
| 411 | */ | ||
| 412 | [[nodiscard]] std::size_t resolve_all_with_profile( | ||
| 413 | std::span<const Anchor> anchors, | ||
| 414 | std::span<ResolvedAnchor> out, | ||
| 415 | const ScanProfile &profile, | ||
| 416 | Region scope = Region::host() | ||
| 417 | ); | ||
| 418 | |||
| 419 | /** | ||
| 420 | * @brief @ref resolve_all_parallel with @p profile applied (see @ref resolve_with_profile). | ||
| 421 | * @note Setup/control-plane only (see @ref resolve_all_parallel). | ||
| 422 | * @warning Never call it under the loader lock, where the worker join hangs. | ||
| 423 | */ | ||
| 424 | [[nodiscard]] std::size_t resolve_all_with_profile_parallel( | ||
| 425 | std::span<const Anchor> anchors, | ||
| 426 | std::span<ResolvedAnchor> out, | ||
| 427 | const ScanProfile &profile, | ||
| 428 | Region scope = Region::host(), | ||
| 429 | std::size_t max_workers = 0 | ||
| 430 | ); | ||
| 431 | } // namespace anchor | ||
| 432 | } // namespace DetourModKit | ||
| 433 | |||
| 434 | #endif // DETOURMODKIT_ANCHOR_HPP | ||
| 435 |