src/anchor.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /** | ||
| 2 | * @file anchor.cpp | ||
| 3 | * @brief The declarative anchor registry dispatches each anchor kind to its v4 backend and reports drift uniformly. | ||
| 4 | * @details Five kinds use one self-heal backend and fail closed. Manual has no backend, and CallArgHome has no | ||
| 5 | * resolver. Unset fails closed, while Quorum combines independent member results. This layer maps each typed | ||
| 6 | * backend error to AnchorStatus. It also applies the optional validator and per-game ScanProfile defaults. | ||
| 7 | * The drift fingerprints live in anchor_evidence.cpp and the quality gate in anchor_gate.cpp. | ||
| 8 | */ | ||
| 9 | |||
| 10 | #include "DetourModKit/anchor.hpp" | ||
| 11 | #include "DetourModKit/rtti.hpp" | ||
| 12 | |||
| 13 | #include "fork_join.hpp" | ||
| 14 | #include "internal/anchor_evidence.hpp" | ||
| 15 | #include "internal/anchor_resolution.hpp" | ||
| 16 | #include "internal/export_resolution.hpp" | ||
| 17 | #include "internal/scan_pages.hpp" | ||
| 18 | #include "internal/scan_shared.hpp" | ||
| 19 | |||
| 20 | #include <windows.h> | ||
| 21 | |||
| 22 | #include <algorithm> | ||
| 23 | #include <array> | ||
| 24 | #include <cstdint> | ||
| 25 | #include <span> | ||
| 26 | #include <vector> | ||
| 27 | |||
| 28 | namespace DetourModKit | ||
| 29 | { | ||
| 30 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 31 | namespace detail | ||
| 32 | { | ||
| 33 | void (*g_anchor_after_named_export_lookup_test_hook)() noexcept = nullptr; | ||
| 34 | void (*g_anchor_after_owner_identity_test_hook)() noexcept = nullptr; | ||
| 35 | void (*g_anchor_after_confirmed_owner_identity_test_hook)() noexcept = nullptr; | ||
| 36 | void (*g_anchor_after_witness_test_hook)() noexcept = nullptr; | ||
| 37 | } // namespace detail | ||
| 38 | #endif | ||
| 39 | |||
| 40 | namespace anchor | ||
| 41 | { | ||
| 42 | namespace | ||
| 43 | { | ||
| 44 | // Applies the profile's candidate order through a local span because read_code_constant has no order | ||
| 45 | // parameter. The local copy preserves the caller's static table. | ||
| 46 | 108 | [[nodiscard]] std::span<const scan::Candidate> profiled_candidates( | |
| 47 | const ScanProfile &profile, | ||
| 48 | std::span<const scan::Candidate> site, | ||
| 49 | std::vector<scan::Candidate> &ordered | ||
| 50 | ) | ||
| 51 | { | ||
| 52 | 108 | if (profile.candidate_order == scan::CandidateOrder::AsDeclared || site.size() < 2) | |
| 53 | { | ||
| 54 | 108 | return site; | |
| 55 | } | ||
| 56 | |||
| 57 | ✗ | std::vector<std::size_t> indices(site.size()); | |
| 58 | ✗ | const std::size_t count = scan::order_candidates(profile.candidate_order, site, indices); | |
| 59 | ✗ | ordered.reserve(count); | |
| 60 | ✗ | for (std::size_t i = 0; i < count; ++i) | |
| 61 | { | ||
| 62 | ✗ | ordered.push_back(site[indices[i]]); | |
| 63 | } | ||
| 64 | ✗ | return ordered; | |
| 65 | ✗ | } | |
| 66 | |||
| 67 | // This agreement test fails closed. Unsigned subtraction converts a negative tolerance to a huge bound | ||
| 68 | // that accepts almost any gap, so reject it first. | ||
| 69 | 232 | [[nodiscard]] bool quorum_values_agree( | |
| 70 | std::int64_t first, | ||
| 71 | std::int64_t second, | ||
| 72 | QuorumMatch match, | ||
| 73 | std::int64_t tolerance | ||
| 74 | ) noexcept | ||
| 75 | { | ||
| 76 | 232 | if (match == QuorumMatch::ExactValue) | |
| 77 | { | ||
| 78 | 99 | return first == second; | |
| 79 | } | ||
| 80 | 133 | if (tolerance < 0) | |
| 81 | { | ||
| 82 | 4 | return false; | |
| 83 | } | ||
| 84 | // Pair order makes the gap hi - lo. Unsigned subtraction avoids signed overflow across a large address | ||
| 85 | // span. | ||
| 86 | 129 | const std::int64_t lo = (first < second) ? first : second; | |
| 87 | 129 | const std::int64_t hi = (first < second) ? second : first; | |
| 88 | 129 | const auto gap = static_cast<std::uint64_t>(hi) - static_cast<std::uint64_t>(lo); | |
| 89 | 129 | return gap <= static_cast<std::uint64_t>(tolerance); | |
| 90 | } | ||
| 91 | |||
| 92 | // These per-kind enum range checks fail closed. A hand-built out-of-range enum reports AnchorStatus::Failed | ||
| 93 | // at this boundary and cannot silently select a resolution mode. | ||
| 94 | 425 | [[nodiscard]] constexpr bool valid_operand_kind(scan::OperandKind kind) noexcept | |
| 95 | { | ||
| 96 | 425 | return kind == scan::OperandKind::Immediate || kind == scan::OperandKind::MemoryDisplacement; | |
| 97 | } | ||
| 98 | |||
| 99 | 35 | [[nodiscard]] constexpr bool valid_string_encoding(scan::StringEncoding encoding) noexcept | |
| 100 | { | ||
| 101 | 35 | return encoding == scan::StringEncoding::Utf8 || encoding == scan::StringEncoding::Utf16le; | |
| 102 | } | ||
| 103 | |||
| 104 | 33 | [[nodiscard]] constexpr bool valid_xref_return(scan::XrefReturn mode) noexcept | |
| 105 | { | ||
| 106 | 33 | switch (mode) | |
| 107 | { | ||
| 108 | 31 | case scan::XrefReturn::ReferencingInstruction: | |
| 109 | case scan::XrefReturn::EnclosingFunction: | ||
| 110 | case scan::XrefReturn::StringPointerSlot: | ||
| 111 | 31 | return true; | |
| 112 | } | ||
| 113 | 2 | return false; | |
| 114 | } | ||
| 115 | |||
| 116 | 143 | [[nodiscard]] constexpr bool valid_quorum_match(QuorumMatch match) noexcept | |
| 117 | { | ||
| 118 | 143 | return match == QuorumMatch::ExactValue || match == QuorumMatch::WithinTolerance; | |
| 119 | } | ||
| 120 | |||
| 121 | // CodeOperand consumes the order locally. RipGlobal delegates validation to scan::resolve. | ||
| 122 | 109 | [[nodiscard]] constexpr bool valid_candidate_order(scan::CandidateOrder order) noexcept | |
| 123 | { | ||
| 124 | 109 | return order == scan::CandidateOrder::AsDeclared || order == scan::CandidateOrder::UniqueFirst; | |
| 125 | } | ||
| 126 | |||
| 127 | /** | ||
| 128 | * @brief The physical source one resolved member's value depends on. | ||
| 129 | * @details Two members that resolve from one physical source share one failure domain. A quorum must count | ||
| 130 | * them once. Two spans suffice for every current backend. If a backend adds a third span, raise | ||
| 131 | * this bound. add() discards excess spans, which lose dependency evidence. | ||
| 132 | */ | ||
| 133 | class PhysicalProvenance | ||
| 134 | { | ||
| 135 | public: | ||
| 136 | 167 | void add(Region span) noexcept | |
| 137 | { | ||
| 138 | 167 | if (!span.base || span.size == 0) | |
| 139 | { | ||
| 140 | ✗ | return; | |
| 141 | } | ||
| 142 | 334 | if (m_size < m_spans.size()) | |
| 143 | { | ||
| 144 | 167 | m_spans[m_size++] = span; | |
| 145 | } | ||
| 146 | } | ||
| 147 | |||
| 148 | 10 | void add(const DetourModKit::detail::ExportResolution &resolution) noexcept | |
| 149 | { | ||
| 150 | 10 | if (resolution.present()) | |
| 151 | { | ||
| 152 | 10 | m_export = resolution; | |
| 153 | } | ||
| 154 | 10 | } | |
| 155 | |||
| 156 | 86 | [[nodiscard]] bool intersects(const PhysicalProvenance &other) const noexcept | |
| 157 | { | ||
| 158 | 86 | if (DetourModKit::detail::same_export_site(m_export, other.m_export)) | |
| 159 | { | ||
| 160 | 2 | return true; | |
| 161 | } | ||
| 162 | 208 | for (std::size_t i = 0; i < m_size; ++i) | |
| 163 | { | ||
| 164 | 350 | for (std::size_t j = 0; j < other.m_size; ++j) | |
| 165 | { | ||
| 166 | 226 | if (overlaps(m_spans[i], other.m_spans[j])) | |
| 167 | { | ||
| 168 | 12 | return true; | |
| 169 | } | ||
| 170 | } | ||
| 171 | } | ||
| 172 | 72 | return false; | |
| 173 | } | ||
| 174 | |||
| 175 | private: | ||
| 176 | 226 | [[nodiscard]] static bool overlaps(Region a, Region b) noexcept | |
| 177 | { | ||
| 178 | 226 | return a.base.raw() < b.end().raw() && b.base.raw() < a.end().raw(); | |
| 179 | } | ||
| 180 | |||
| 181 | std::array<Region, 2> m_spans{}; | ||
| 182 | std::size_t m_size = 0; | ||
| 183 | DetourModKit::detail::ExportResolution m_export{}; | ||
| 184 | }; | ||
| 185 | |||
| 186 | // Counts votes that agree with a candidate cluster center. A negative WithinTolerance rejects even the | ||
| 187 | // center against itself, so the quorum fails closed. | ||
| 188 | 70 | [[nodiscard]] std::size_t votes_agreeing_with( | |
| 189 | std::int64_t center, | ||
| 190 | std::span<const std::int64_t> votes, | ||
| 191 | QuorumMatch match, | ||
| 192 | std::int64_t tolerance | ||
| 193 | ) noexcept | ||
| 194 | { | ||
| 195 | 70 | std::size_t agree = 0; | |
| 196 | 316 | for (const std::int64_t vote : votes) | |
| 197 | { | ||
| 198 | 176 | if (quorum_values_agree(center, vote, match, tolerance)) | |
| 199 | { | ||
| 200 | 135 | ++agree; | |
| 201 | } | ||
| 202 | } | ||
| 203 | 70 | return agree; | |
| 204 | } | ||
| 205 | |||
| 206 | // Maps a flat anchor backend to its physical source. RipGlobal uses its selected ladder mode. | ||
| 207 | 414 | [[nodiscard]] PhysicalSource physical_source_of(AnchorKind kind) noexcept | |
| 208 | { | ||
| 209 | 414 | switch (kind) | |
| 210 | { | ||
| 211 | 88 | case AnchorKind::RipGlobal: | |
| 212 | 88 | return PhysicalSource::None; | |
| 213 | 12 | case AnchorKind::StringXref: | |
| 214 | 12 | return PhysicalSource::StringLiteral; | |
| 215 | 5 | case AnchorKind::VtableIdentity: | |
| 216 | 5 | return PhysicalSource::TypeIdentity; | |
| 217 | 39 | case AnchorKind::ExportName: | |
| 218 | 39 | return PhysicalSource::ExportTable; | |
| 219 | 115 | case AnchorKind::CodeOperand: | |
| 220 | 115 | return PhysicalSource::CodeOperand; | |
| 221 | 68 | case AnchorKind::Manual: | |
| 222 | 68 | return PhysicalSource::ManualPin; | |
| 223 | 78 | case AnchorKind::Quorum: | |
| 224 | 78 | return PhysicalSource::Corroborated; | |
| 225 | 8 | case AnchorKind::CallArgHome: | |
| 226 | case AnchorKind::Unset: | ||
| 227 | 8 | return PhysicalSource::None; | |
| 228 | } | ||
| 229 | 1 | return PhysicalSource::None; | |
| 230 | } | ||
| 231 | |||
| 232 | 76 | [[nodiscard]] PhysicalSource physical_source_of(scan::Mode mode) noexcept | |
| 233 | { | ||
| 234 | 76 | switch (mode) | |
| 235 | { | ||
| 236 | 74 | case scan::Mode::Direct: | |
| 237 | case scan::Mode::RipRelative: | ||
| 238 | 74 | return PhysicalSource::ByteSignature; | |
| 239 | ✗ | case scan::Mode::RttiVtable: | |
| 240 | ✗ | return PhysicalSource::TypeIdentity; | |
| 241 | 2 | case scan::Mode::StringXref: | |
| 242 | 2 | return PhysicalSource::StringLiteral; | |
| 243 | } | ||
| 244 | ✗ | return PhysicalSource::None; | |
| 245 | } | ||
| 246 | |||
| 247 | // Commits a backend-resolved value through the optional fail-closed validator. A validator miss reports | ||
| 248 | // Failed with no value, identical to a backend miss. | ||
| 249 | 270 | void commit_resolved(const Anchor &anchor, ResolvedAnchor &result, std::int64_t value) noexcept | |
| 250 | { | ||
| 251 | // Backend-resolved targets use an opt-in required-validator policy. Manual and Quorum are exempt. A | ||
| 252 | // pinned literal is not a resolved target, and Quorum corroboration already supplies verification. | ||
| 253 | 270 | if (anchor.require_validator && anchor.kind != AnchorKind::Quorum && | |
| 254 | 7 | anchor.kind != AnchorKind::Manual && anchor.validator == nullptr) | |
| 255 | { | ||
| 256 | 3 | result.status = AnchorStatus::Failed; | |
| 257 | 3 | result.value = 0; | |
| 258 | 3 | return; | |
| 259 | } | ||
| 260 | 267 | if (anchor.validator != nullptr && !anchor.validator(value, anchor.validator_context)) | |
| 261 | { | ||
| 262 | 9 | result.status = AnchorStatus::Failed; | |
| 263 | 9 | result.value = 0; | |
| 264 | 9 | return; | |
| 265 | } | ||
| 266 | 258 | result.value = value; | |
| 267 | 258 | result.status = AnchorStatus::Resolved; | |
| 268 | } | ||
| 269 | |||
| 270 | 27 | [[nodiscard]] ResolvedAnchor failed_anchor_result(const Anchor &anchor) noexcept | |
| 271 | { | ||
| 272 | 27 | return ResolvedAnchor{anchor.label, anchor.kind, AnchorStatus::Failed, 0}; | |
| 273 | } | ||
| 274 | |||
| 275 | enum class OwnerBacking : std::uint8_t | ||
| 276 | { | ||
| 277 | None, | ||
| 278 | Missing, | ||
| 279 | Image, | ||
| 280 | Private, | ||
| 281 | Mapped | ||
| 282 | }; | ||
| 283 | |||
| 284 | /** | ||
| 285 | * @struct OwnerKey | ||
| 286 | * @brief The memory source an anchor read and, for an image, that image's PE identity. | ||
| 287 | * @details Synthetic ranges carry their OS region class and allocation base but no PE identity. This key | ||
| 288 | * remains valid while the range stays stable. It detects a synthetic-to-image transition before | ||
| 289 | * publication. | ||
| 290 | */ | ||
| 291 | struct OwnerKey | ||
| 292 | { | ||
| 293 | 60 | std::uintptr_t address{0}; | |
| 294 | 60 | std::uintptr_t allocation_base{0}; | |
| 295 | 60 | scan::ImageIdentity identity{}; | |
| 296 | 60 | OwnerBacking backing{OwnerBacking::None}; | |
| 297 | |||
| 298 | 1401 | [[nodiscard]] constexpr bool tracked() const noexcept { return backing != OwnerBacking::None; } | |
| 299 | 867 | [[nodiscard]] constexpr bool has_module() const noexcept { return backing == OwnerBacking::Image; } | |
| 300 | 60 | [[nodiscard]] constexpr bool operator==(const OwnerKey &other) const noexcept = default; | |
| 301 | }; | ||
| 302 | |||
| 303 | constexpr std::size_t MAX_EVIDENCE_OWNER_KEYS = 3; | ||
| 304 | |||
| 305 | /** | ||
| 306 | * @brief Returns whether the complete scan scope stays inside one reserved allocation. | ||
| 307 | * @details A single allocation lets one captured owner key cover the complete evidence domain. A wider | ||
| 308 | * sweep leaves unrelated memory sources outside that key. | ||
| 309 | */ | ||
| 310 | 468 | [[nodiscard]] bool scope_is_single_allocation(Region scope) noexcept | |
| 311 | { | ||
| 312 | 468 | if (scope.base.raw() == 0 || scope.size == 0 || scope.base.raw() > UINTPTR_MAX - scope.size) | |
| 313 | { | ||
| 314 | 1 | return false; | |
| 315 | } | ||
| 316 | 467 | MEMORY_BASIC_INFORMATION memory_info{}; | |
| 317 | 934 | if (::VirtualQuery(scope.base.as<const void *>(), &memory_info, sizeof(memory_info)) == 0 || | |
| 318 | 467 | memory_info.AllocationBase == nullptr) | |
| 319 | { | ||
| 320 | ✗ | return false; | |
| 321 | } | ||
| 322 | 467 | const void *const allocation_base = memory_info.AllocationBase; | |
| 323 | 467 | const std::uintptr_t scope_end = scope.base.raw() + scope.size; | |
| 324 | 467 | std::uintptr_t cursor = scope.base.raw(); | |
| 325 | 23071 | while (cursor < scope_end) | |
| 326 | { | ||
| 327 | 23071 | if (::VirtualQuery(reinterpret_cast<const void *>(cursor), &memory_info, sizeof(memory_info)) == | |
| 328 | 46142 | 0 || | |
| 329 | 23071 | memory_info.AllocationBase != allocation_base) | |
| 330 | { | ||
| 331 | 3 | return false; | |
| 332 | } | ||
| 333 | 23068 | const std::uintptr_t region_base = reinterpret_cast<std::uintptr_t>(memory_info.BaseAddress); | |
| 334 | 23068 | if (memory_info.RegionSize == 0 || region_base > UINTPTR_MAX - memory_info.RegionSize) | |
| 335 | { | ||
| 336 | ✗ | return false; | |
| 337 | } | ||
| 338 | 23068 | const std::uintptr_t region_end = region_base + memory_info.RegionSize; | |
| 339 | 23068 | if (region_end <= cursor) | |
| 340 | { | ||
| 341 | ✗ | return false; | |
| 342 | } | ||
| 343 | 23068 | if (region_end >= scope_end) | |
| 344 | { | ||
| 345 | 464 | return true; | |
| 346 | } | ||
| 347 | 22604 | cursor = region_end; | |
| 348 | } | ||
| 349 | ✗ | return false; | |
| 350 | } | ||
| 351 | |||
| 352 | struct ResolutionOwnerKeys | ||
| 353 | { | ||
| 354 | std::array<OwnerKey, MAX_EVIDENCE_OWNER_KEYS> evidence{}; | ||
| 355 | std::size_t evidence_count{0}; | ||
| 356 | OwnerKey value{}; | ||
| 357 | bool overflowed{false}; | ||
| 358 | bool requires_single_allocation{false}; | ||
| 359 | |||
| 360 | 253 | void add_evidence(const OwnerKey &key) noexcept | |
| 361 | { | ||
| 362 | 253 | if (!key.tracked()) | |
| 363 | { | ||
| 364 | 1 | return; | |
| 365 | } | ||
| 366 | 252 | for (std::size_t i = 0; i < evidence_count; ++i) | |
| 367 | { | ||
| 368 | ✗ | if (evidence[i] == key) | |
| 369 | { | ||
| 370 | ✗ | return; | |
| 371 | } | ||
| 372 | } | ||
| 373 | 504 | if (evidence_count == evidence.size()) | |
| 374 | { | ||
| 375 | ✗ | overflowed = true; | |
| 376 | ✗ | return; | |
| 377 | } | ||
| 378 | 252 | evidence[evidence_count++] = key; | |
| 379 | } | ||
| 380 | }; | ||
| 381 | |||
| 382 | /** | ||
| 383 | * @brief Captures owner identity for an evidence or value address. | ||
| 384 | * @details A committed non-image range carries a synthetic owner key without a PE identity. A lost | ||
| 385 | * address carries a fail-closed key. A MEM_IMAGE range with an unreadable PE identity does too. | ||
| 386 | */ | ||
| 387 | 443 | [[nodiscard]] OwnerKey capture_owner_key(Region region) noexcept | |
| 388 | { | ||
| 389 | 883 | if (region.base.raw() == 0 || region.size == 0 || | |
| 390 | 440 | !DetourModKit::detail::is_plausible_ptr(region.base.raw())) | |
| 391 | { | ||
| 392 | 4 | return OwnerKey{}; | |
| 393 | } | ||
| 394 | 439 | MEMORY_BASIC_INFORMATION memory_info{}; | |
| 395 | 878 | if (::VirtualQuery(region.base.as<const void *>(), &memory_info, sizeof(memory_info)) == 0 || | |
| 396 | 439 | memory_info.State != MEM_COMMIT) | |
| 397 | { | ||
| 398 | ✗ | return OwnerKey{region.base.raw(), 0, {}, OwnerBacking::Missing}; | |
| 399 | } | ||
| 400 | 439 | const std::uintptr_t allocation_base = reinterpret_cast<std::uintptr_t>(memory_info.AllocationBase); | |
| 401 | 439 | if (allocation_base == 0) | |
| 402 | { | ||
| 403 | ✗ | return OwnerKey{region.base.raw(), 0, {}, OwnerBacking::Missing}; | |
| 404 | } | ||
| 405 | 439 | if (memory_info.Type == MEM_IMAGE) | |
| 406 | { | ||
| 407 | return OwnerKey{ | ||
| 408 | allocation_base, | ||
| 409 | allocation_base, | ||
| 410 | 213 | scan::image_identity(Region{Address{allocation_base}, 1}), | |
| 411 | OwnerBacking::Image | ||
| 412 | 426 | }; | |
| 413 | } | ||
| 414 | 226 | if (memory_info.Type == MEM_PRIVATE) | |
| 415 | { | ||
| 416 | 226 | return OwnerKey{region.base.raw(), allocation_base, {}, OwnerBacking::Private}; | |
| 417 | } | ||
| 418 | ✗ | if (memory_info.Type == MEM_MAPPED) | |
| 419 | { | ||
| 420 | ✗ | return OwnerKey{region.base.raw(), allocation_base, {}, OwnerBacking::Mapped}; | |
| 421 | } | ||
| 422 | ✗ | return OwnerKey{region.base.raw(), allocation_base, {}, OwnerBacking::Missing}; | |
| 423 | } | ||
| 424 | |||
| 425 | /** | ||
| 426 | * @brief Captures the first committed region inside a scope whose allocation was already validated. | ||
| 427 | * @details A reserved prefix has no region class. The first committed page's key places scalar and | ||
| 428 | * address-domain backends in the same region-class transaction. | ||
| 429 | */ | ||
| 430 | 215 | [[nodiscard]] OwnerKey capture_scope_owner_key(Region scope) noexcept | |
| 431 | { | ||
| 432 | 215 | if (scope.base.raw() == 0 || scope.size == 0 || scope.base.raw() > UINTPTR_MAX - scope.size) | |
| 433 | { | ||
| 434 | ✗ | return capture_owner_key(scope); | |
| 435 | } | ||
| 436 | 215 | const std::uintptr_t scope_end = scope.base.raw() + scope.size; | |
| 437 | 215 | std::uintptr_t cursor = scope.base.raw(); | |
| 438 | 217 | while (cursor < scope_end) | |
| 439 | { | ||
| 440 | 217 | MEMORY_BASIC_INFORMATION memory_info{}; | |
| 441 | 217 | if (::VirtualQuery(reinterpret_cast<const void *>(cursor), &memory_info, sizeof(memory_info)) == 0) | |
| 442 | { | ||
| 443 | ✗ | break; | |
| 444 | } | ||
| 445 | 217 | const std::uintptr_t region_base = reinterpret_cast<std::uintptr_t>(memory_info.BaseAddress); | |
| 446 | 217 | if (memory_info.RegionSize == 0 || region_base > UINTPTR_MAX - memory_info.RegionSize) | |
| 447 | { | ||
| 448 | break; | ||
| 449 | } | ||
| 450 | 217 | if (memory_info.State == MEM_COMMIT) | |
| 451 | { | ||
| 452 | 215 | const std::uintptr_t committed_address = (cursor > region_base) ? cursor : region_base; | |
| 453 | 215 | return capture_owner_key(Region{Address{committed_address}, 1}); | |
| 454 | } | ||
| 455 | 2 | const std::uintptr_t region_end = region_base + memory_info.RegionSize; | |
| 456 | 2 | if (region_end <= cursor || region_end >= scope_end) | |
| 457 | { | ||
| 458 | break; | ||
| 459 | } | ||
| 460 | 2 | cursor = region_end; | |
| 461 | } | ||
| 462 | ✗ | return capture_owner_key(scope); | |
| 463 | } | ||
| 464 | |||
| 465 | /// Returns whether an explicit export module still resolves to its captured region and owner key. | ||
| 466 | 428 | [[nodiscard]] bool named_export_owner_current( | |
| 467 | const Anchor &anchor, | ||
| 468 | Region expected_region, | ||
| 469 | const OwnerKey &expected_owner | ||
| 470 | ) noexcept | ||
| 471 | { | ||
| 472 | 428 | if (anchor.kind != AnchorKind::ExportName || anchor.export_module.empty()) | |
| 473 | { | ||
| 474 | 366 | return true; | |
| 475 | } | ||
| 476 | 62 | const Region live_region = Region::module_named(anchor.export_module); | |
| 477 | 122 | return live_region.base == expected_region.base && live_region.size == expected_region.size && | |
| 478 | 122 | capture_owner_key(live_region) == expected_owner; | |
| 479 | } | ||
| 480 | |||
| 481 | /** | ||
| 482 | * @brief Returns true while the captured memory source remains current at its address. | ||
| 483 | * @details Synthetic ranges retain region class and allocation base. Image checks surround two PE identity | ||
| 484 | * reads with MEM_IMAGE and allocation-base checks. A same-base image or private-header replacement | ||
| 485 | * therefore fails closed. | ||
| 486 | */ | ||
| 487 | 507 | [[nodiscard]] bool owner_key_current(const OwnerKey &key) noexcept | |
| 488 | { | ||
| 489 | 507 | if (!key.tracked()) | |
| 490 | { | ||
| 491 | ✗ | return true; | |
| 492 | } | ||
| 493 | 507 | if (key.backing == OwnerBacking::Missing) | |
| 494 | { | ||
| 495 | ✗ | return false; | |
| 496 | } | ||
| 497 | 507 | MEMORY_BASIC_INFORMATION memory_info{}; | |
| 498 | 507 | if (::VirtualQuery(reinterpret_cast<const void *>(key.address), &memory_info, sizeof(memory_info)) == | |
| 499 | 507 | 0 || | |
| 500 | 1014 | memory_info.State != MEM_COMMIT || | |
| 501 | 507 | reinterpret_cast<std::uintptr_t>(memory_info.AllocationBase) != key.allocation_base) | |
| 502 | { | ||
| 503 | ✗ | return false; | |
| 504 | } | ||
| 505 | 507 | if (!key.has_module()) | |
| 506 | { | ||
| 507 | 345 | const DWORD expected_type = key.backing == OwnerBacking::Private ? MEM_PRIVATE : MEM_MAPPED; | |
| 508 | 345 | return memory_info.Type == expected_type; | |
| 509 | } | ||
| 510 | 162 | if (memory_info.Type != MEM_IMAGE || !key.identity.present()) | |
| 511 | { | ||
| 512 | 3 | return false; | |
| 513 | } | ||
| 514 | 159 | const scan::ImageIdentity live_identity = scan::image_identity(Region{Address{key.allocation_base}, 1}); | |
| 515 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 516 | 159 | if (auto *const hook = DetourModKit::detail::g_anchor_after_owner_identity_test_hook) | |
| 517 | { | ||
| 518 | 2 | hook(); | |
| 519 | } | ||
| 520 | #endif | ||
| 521 | 159 | MEMORY_BASIC_INFORMATION confirmed_info{}; | |
| 522 | 477 | if (::VirtualQuery( | |
| 523 | 159 | reinterpret_cast<const void *>(key.address), | |
| 524 | &confirmed_info, | ||
| 525 | sizeof(confirmed_info) | ||
| 526 | 159 | ) == 0 || | |
| 527 | 317 | confirmed_info.State != MEM_COMMIT || confirmed_info.Type != MEM_IMAGE || | |
| 528 | 158 | reinterpret_cast<std::uintptr_t>(confirmed_info.AllocationBase) != key.allocation_base) | |
| 529 | { | ||
| 530 | 1 | return false; | |
| 531 | } | ||
| 532 | const scan::ImageIdentity confirmed_identity = | ||
| 533 | 158 | scan::image_identity(Region{Address{key.allocation_base}, 1}); | |
| 534 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 535 | 158 | if (auto *const hook = DetourModKit::detail::g_anchor_after_confirmed_owner_identity_test_hook) | |
| 536 | { | ||
| 537 | 1 | hook(); | |
| 538 | } | ||
| 539 | #endif | ||
| 540 | 158 | MEMORY_BASIC_INFORMATION final_info{}; | |
| 541 | 158 | if (::VirtualQuery(reinterpret_cast<const void *>(key.address), &final_info, sizeof(final_info)) == 0 || | |
| 542 | 315 | final_info.State != MEM_COMMIT || final_info.Type != MEM_IMAGE || | |
| 543 | 157 | reinterpret_cast<std::uintptr_t>(final_info.AllocationBase) != key.allocation_base) | |
| 544 | { | ||
| 545 | 1 | return false; | |
| 546 | } | ||
| 547 | 157 | return live_identity == key.identity && confirmed_identity == key.identity; | |
| 548 | } | ||
| 549 | |||
| 550 | /// Returns whether two captured keys can describe one coherent generation. | ||
| 551 | 277 | [[nodiscard]] constexpr bool owner_keys_compatible(const OwnerKey &first, const OwnerKey &second) noexcept | |
| 552 | { | ||
| 553 | 357 | return !first.has_module() || !second.has_module() || first.allocation_base != second.allocation_base || | |
| 554 | 357 | first.identity == second.identity; | |
| 555 | } | ||
| 556 | |||
| 557 | /** | ||
| 558 | * @brief Decides whether every image that supplied evidence remains the same image. | ||
| 559 | * @details Run this check after the value commits because the validator can replace a module. A replacement | ||
| 560 | * between two member resolves mixes image generations. Restoration before this check does not | ||
| 561 | * repair that vote. | ||
| 562 | */ | ||
| 563 | [[nodiscard]] bool | ||
| 564 | 408 | evidence_images_coherent(const ResolutionOwnerKeys &local, std::span<const OwnerKey> members) noexcept | |
| 565 | { | ||
| 566 | 408 | if (local.overflowed) | |
| 567 | { | ||
| 568 | ✗ | return false; | |
| 569 | } | ||
| 570 | |||
| 571 | 408 | std::array<OwnerKey, MAX_EVIDENCE_OWNER_KEYS + 1> local_keys{}; | |
| 572 | 408 | std::size_t local_count = local.evidence_count; | |
| 573 | 408 | std::copy_n(local.evidence.begin(), local_count, local_keys.begin()); | |
| 574 | 408 | if (local.value.tracked()) | |
| 575 | { | ||
| 576 | 129 | local_keys[local_count++] = local.value; | |
| 577 | } | ||
| 578 | 766 | for (std::size_t i = 0; i < local_count; ++i) | |
| 579 | { | ||
| 580 | 373 | if (!owner_key_current(local_keys[i])) | |
| 581 | { | ||
| 582 | 13 | return false; | |
| 583 | } | ||
| 584 | 471 | for (std::size_t j = i + 1; j < local_count; ++j) | |
| 585 | { | ||
| 586 | 111 | if (!owner_keys_compatible(local_keys[i], local_keys[j])) | |
| 587 | { | ||
| 588 | ✗ | return false; | |
| 589 | } | ||
| 590 | } | ||
| 591 | 739 | for (const OwnerKey &member : members) | |
| 592 | { | ||
| 593 | 21 | if (!owner_keys_compatible(local_keys[i], member)) | |
| 594 | { | ||
| 595 | 2 | return false; | |
| 596 | } | ||
| 597 | } | ||
| 598 | } | ||
| 599 | 526 | for (std::size_t i = 0; i < members.size(); ++i) | |
| 600 | { | ||
| 601 | 134 | if (!owner_key_current(members[i])) | |
| 602 | { | ||
| 603 | 1 | return false; | |
| 604 | } | ||
| 605 | 278 | for (std::size_t j = i + 1; j < members.size(); ++j) | |
| 606 | { | ||
| 607 | 145 | if (!owner_keys_compatible(members[i], members[j])) | |
| 608 | { | ||
| 609 | ✗ | return false; | |
| 610 | } | ||
| 611 | } | ||
| 612 | } | ||
| 613 | 392 | return true; | |
| 614 | } | ||
| 615 | |||
| 616 | /// Returns whether @p domain carries an address rather than a scalar. | ||
| 617 | 582 | [[nodiscard]] constexpr bool is_address_domain(ResultDomain domain) noexcept | |
| 618 | { | ||
| 619 | 582 | return domain == ResultDomain::CodeSite || domain == ResultDomain::DataAddress || | |
| 620 | 582 | domain == ResultDomain::VtableAddress; | |
| 621 | } | ||
| 622 | |||
| 623 | /** | ||
| 624 | * @brief Captures an address-domain value's owner before its validator. | ||
| 625 | * @details Synthetic addresses keep an owner key without an image identity. An unreadable loader-backed | ||
| 626 | * address keeps a fail-closed image key. | ||
| 627 | */ | ||
| 628 | 270 | [[nodiscard]] OwnerKey capture_value_owner(const Anchor &anchor, std::int64_t value) noexcept | |
| 629 | { | ||
| 630 | 270 | if (is_address_domain(declared_domain(anchor))) | |
| 631 | { | ||
| 632 | 130 | return capture_owner_key(Region{Address{static_cast<std::uintptr_t>(value)}, 1}); | |
| 633 | } | ||
| 634 | 140 | return OwnerKey{}; | |
| 635 | } | ||
| 636 | |||
| 637 | /// Appends @p key when it identifies a tracked memory source. | ||
| 638 | 233 | void append_owner_key(std::vector<OwnerKey> &keys, const OwnerKey &key) | |
| 639 | { | ||
| 640 | 233 | if (key.tracked()) | |
| 641 | { | ||
| 642 | 145 | keys.push_back(key); | |
| 643 | } | ||
| 644 | 233 | } | |
| 645 | |||
| 646 | /// Appends every tracked owner key from @p owner_keys. | ||
| 647 | 125 | void append_owner_keys(std::vector<OwnerKey> &keys, const ResolutionOwnerKeys &owner_keys) | |
| 648 | { | ||
| 649 | 233 | for (std::size_t i = 0; i < owner_keys.evidence_count; ++i) | |
| 650 | { | ||
| 651 | 108 | append_owner_key(keys, owner_keys.evidence[i]); | |
| 652 | } | ||
| 653 | 125 | append_owner_key(keys, owner_keys.value); | |
| 654 | 125 | } | |
| 655 | |||
| 656 | /** | ||
| 657 | * @brief Returns whether a backend reads evidence from the scan scope's own memory source. | ||
| 658 | * @details ExportName captures its effective export module instead. Manual pins a literal. | ||
| 659 | * Quorum inherits its members' keys. The two kinds without resolvers read nothing. | ||
| 660 | */ | ||
| 661 | 1226 | [[nodiscard]] constexpr bool evidence_module_is_scope(AnchorKind kind) noexcept | |
| 662 | { | ||
| 663 | 1226 | switch (kind) | |
| 664 | { | ||
| 665 | 647 | case AnchorKind::VtableIdentity: | |
| 666 | case AnchorKind::RipGlobal: | ||
| 667 | case AnchorKind::CodeOperand: | ||
| 668 | case AnchorKind::StringXref: | ||
| 669 | 647 | return true; | |
| 670 | 576 | case AnchorKind::ExportName: | |
| 671 | case AnchorKind::Manual: | ||
| 672 | case AnchorKind::Quorum: | ||
| 673 | case AnchorKind::CallArgHome: | ||
| 674 | case AnchorKind::Unset: | ||
| 675 | 576 | return false; | |
| 676 | } | ||
| 677 | 3 | return false; | |
| 678 | } | ||
| 679 | |||
| 680 | static_assert(evidence_module_is_scope(AnchorKind::VtableIdentity)); | ||
| 681 | static_assert(evidence_module_is_scope(AnchorKind::RipGlobal)); | ||
| 682 | static_assert(evidence_module_is_scope(AnchorKind::CodeOperand)); | ||
| 683 | static_assert(evidence_module_is_scope(AnchorKind::StringXref)); | ||
| 684 | static_assert(!evidence_module_is_scope(AnchorKind::ExportName)); | ||
| 685 | static_assert(!evidence_module_is_scope(AnchorKind::Manual)); | ||
| 686 | static_assert(!evidence_module_is_scope(AnchorKind::Quorum)); | ||
| 687 | static_assert(!evidence_module_is_scope(AnchorKind::CallArgHome)); | ||
| 688 | static_assert(!evidence_module_is_scope(AnchorKind::Unset)); | ||
| 689 | |||
| 690 | } // anonymous namespace | ||
| 691 | |||
| 692 | 12 | scan::StringRefQuery apply_profile(const ScanProfile &profile, scan::StringRefQuery query) noexcept | |
| 693 | { | ||
| 694 | // This policy only widens the scan. A per-anchor broad_match value stays set. The profile can enable broad | ||
| 695 | // mode but cannot disable it. | ||
| 696 | 12 | query.broad_match = query.broad_match || profile.default_broad_string_xref; | |
| 697 | 12 | return query; | |
| 698 | } | ||
| 699 | |||
| 700 | namespace | ||
| 701 | { | ||
| 702 | /// Resets each non-null output so that it holds only data from this resolution. | ||
| 703 | 414 | void clear_resolution_outputs( | |
| 704 | PhysicalProvenance *provenance, | ||
| 705 | ResolutionOwnerKeys *owner_keys_out, | ||
| 706 | Region *winning_span_out | ||
| 707 | ) noexcept | ||
| 708 | { | ||
| 709 | 414 | if (provenance != nullptr) | |
| 710 | { | ||
| 711 | 132 | *provenance = PhysicalProvenance{}; | |
| 712 | } | ||
| 713 | 414 | if (owner_keys_out != nullptr) | |
| 714 | { | ||
| 715 | 132 | *owner_keys_out = ResolutionOwnerKeys{}; | |
| 716 | } | ||
| 717 | 414 | if (winning_span_out != nullptr) | |
| 718 | { | ||
| 719 | 48 | *winning_span_out = Region{}; | |
| 720 | } | ||
| 721 | 414 | } | |
| 722 | |||
| 723 | 5 | void resolve_vtable_identity( | |
| 724 | const Anchor &anchor, | ||
| 725 | Region scope, | ||
| 726 | ResolutionOwnerKeys &owner_keys, | ||
| 727 | ResolvedAnchor &result | ||
| 728 | ) noexcept | ||
| 729 | { | ||
| 730 | 5 | const std::optional<Address> discovered = DetourModKit::rtti::vtable_for_type(anchor.mangled, scope); | |
| 731 | 5 | if (discovered) | |
| 732 | { | ||
| 733 | 4 | owner_keys.value = capture_value_owner(anchor, static_cast<std::int64_t>(discovered->raw())); | |
| 734 | 4 | commit_resolved(anchor, result, static_cast<std::int64_t>(discovered->raw())); | |
| 735 | } | ||
| 736 | else | ||
| 737 | { | ||
| 738 | 1 | result.status = AnchorStatus::Failed; | |
| 739 | } | ||
| 740 | 5 | } | |
| 741 | |||
| 742 | /// Writes the winning mode's source, byte evidence, and match span only on a hit. | ||
| 743 | 86 | void resolve_rip_global( | |
| 744 | const Anchor &anchor, | ||
| 745 | const ScanProfile &profile, | ||
| 746 | Region scope, | ||
| 747 | PhysicalProvenance *provenance, | ||
| 748 | ResolutionOwnerKeys &owner_keys, | ||
| 749 | ResolvedAnchor &result, | ||
| 750 | PhysicalSource &resolved_source, | ||
| 751 | scan::WinningEvidence &resolved_evidence, | ||
| 752 | Region &resolved_winning_span | ||
| 753 | ) | ||
| 754 | { | ||
| 755 | // The cascade selects Direct or RIP-relative per candidate. It also applies the profile order. | ||
| 756 | 86 | const scan::ScanRequest request{ | |
| 757 | .ladder = anchor.site, | ||
| 758 | .label = anchor.label, | ||
| 759 | .scope = scope, | ||
| 760 | 86 | .order = profile.candidate_order, | |
| 761 | 86 | .pages = anchor.pages, | |
| 762 | 86 | }; | |
| 763 | 86 | const Result<detail::ResolvedScanHit> discovered = detail::resolve_scan_with_provenance(request); | |
| 764 | 86 | if (discovered) | |
| 765 | { | ||
| 766 | 76 | owner_keys.value = | |
| 767 | 76 | capture_value_owner(anchor, static_cast<std::int64_t>(discovered->hit.address.raw())); | |
| 768 | 76 | resolved_source = physical_source_of(discovered->hit.winning_mode); | |
| 769 | 76 | resolved_evidence = discovered->hit.evidence; | |
| 770 | 76 | resolved_winning_span = discovered->match_span; | |
| 771 | 76 | if (provenance != nullptr) | |
| 772 | { | ||
| 773 | 21 | provenance->add(discovered->physical_source); | |
| 774 | } | ||
| 775 | 76 | commit_resolved(anchor, result, static_cast<std::int64_t>(discovered->hit.address.raw())); | |
| 776 | } | ||
| 777 | else | ||
| 778 | { | ||
| 779 | 10 | result.status = AnchorStatus::Failed; | |
| 780 | } | ||
| 781 | 86 | } | |
| 782 | |||
| 783 | 112 | void resolve_code_operand( | |
| 784 | const Anchor &anchor, | ||
| 785 | const ScanProfile &profile, | ||
| 786 | Region scope, | ||
| 787 | PhysicalProvenance *provenance, | ||
| 788 | ResolutionOwnerKeys &owner_keys, | ||
| 789 | ResolvedAnchor &result | ||
| 790 | ) | ||
| 791 | { | ||
| 792 | 112 | if (!valid_operand_kind(anchor.operand_kind) || | |
| 793 | 221 | !detail::valid_code_constant_byte_width(anchor.byte_width) || | |
| 794 | 109 | !valid_candidate_order(profile.candidate_order)) | |
| 795 | { | ||
| 796 | 4 | result.status = AnchorStatus::Failed; | |
| 797 | 4 | return; | |
| 798 | } | ||
| 799 | // read_code_constant has no order parameter. Create a local ladder in profile order before the call. | ||
| 800 | 108 | std::vector<scan::Candidate> ordered_site; | |
| 801 | 108 | const scan::CodeConstant code_constant{ | |
| 802 | 108 | .site = profiled_candidates(profile, anchor.site, ordered_site), | |
| 803 | 108 | .kind = anchor.operand_kind, | |
| 804 | 108 | .operand_index = anchor.operand_index, | |
| 805 | 108 | .byte_width = anchor.byte_width, | |
| 806 | 108 | }; | |
| 807 | const Result<detail::ResolvedCodeConstant> discovered = | ||
| 808 | 108 | detail::read_code_constant_with_provenance(code_constant, scope); | |
| 809 | 108 | if (discovered) | |
| 810 | { | ||
| 811 | 106 | owner_keys.value = capture_value_owner(anchor, discovered->value); | |
| 812 | 106 | if (provenance != nullptr) | |
| 813 | { | ||
| 814 | 71 | provenance->add(discovered->instruction_span); | |
| 815 | 71 | provenance->add(discovered->physical_source); | |
| 816 | } | ||
| 817 | 106 | commit_resolved(anchor, result, discovered->value); | |
| 818 | } | ||
| 819 | else | ||
| 820 | { | ||
| 821 | 2 | result.status = AnchorStatus::Failed; | |
| 822 | } | ||
| 823 | 108 | } | |
| 824 | |||
| 825 | 12 | void resolve_string_xref( | |
| 826 | const Anchor &anchor, | ||
| 827 | const ScanProfile &profile, | ||
| 828 | Region scope, | ||
| 829 | PhysicalProvenance *provenance, | ||
| 830 | ResolutionOwnerKeys &owner_keys, | ||
| 831 | ResolvedAnchor &result | ||
| 832 | ) | ||
| 833 | { | ||
| 834 | 12 | if (!valid_string_encoding(anchor.xref_encoding) || !valid_xref_return(anchor.xref_return)) | |
| 835 | { | ||
| 836 | 2 | result.status = AnchorStatus::Failed; | |
| 837 | 2 | return; | |
| 838 | } | ||
| 839 | // A StringXref anchors on an immutable string literal and resolves its reference site. An absent | ||
| 840 | // literal, duplicate literal, or literal without a reference fails closed. | ||
| 841 | 10 | scan::StringRefQuery query{}; | |
| 842 | 10 | query.text = anchor.xref_text; | |
| 843 | 10 | query.encoding = anchor.xref_encoding; | |
| 844 | 10 | query.require_terminator = anchor.xref_require_terminator; | |
| 845 | 10 | query.return_mode = anchor.xref_return; | |
| 846 | 10 | query.broad_match = anchor.xref_broad_match; | |
| 847 | 10 | query = apply_profile(profile, query); | |
| 848 | 10 | Region discovered_span{}; | |
| 849 | const Result<Address> discovered = | ||
| 850 | 10 | detail::find_string_xref_with_provenance(query, scope, discovered_span); | |
| 851 | 10 | if (discovered) | |
| 852 | { | ||
| 853 | 6 | owner_keys.value = capture_value_owner(anchor, static_cast<std::int64_t>(discovered->raw())); | |
| 854 | 6 | if (provenance != nullptr) | |
| 855 | { | ||
| 856 | 4 | provenance->add(discovered_span); | |
| 857 | } | ||
| 858 | 6 | commit_resolved(anchor, result, static_cast<std::int64_t>(discovered->raw())); | |
| 859 | } | ||
| 860 | else | ||
| 861 | { | ||
| 862 | 4 | result.status = AnchorStatus::Failed; | |
| 863 | } | ||
| 864 | } | ||
| 865 | |||
| 866 | /// Writes the effective export module and its owner key for the final resolution_sources_current check. | ||
| 867 | 38 | void resolve_export_name( | |
| 868 | const Anchor &anchor, | ||
| 869 | Region scope, | ||
| 870 | PhysicalProvenance *provenance, | ||
| 871 | ResolutionOwnerKeys &owner_keys, | ||
| 872 | ResolvedAnchor &result, | ||
| 873 | Region &named_export_region, | ||
| 874 | OwnerKey &named_export_owner | ||
| 875 | ) noexcept | ||
| 876 | { | ||
| 877 | // Resolve a named export through the module EAT. An unloaded module, absent or forwarded export, or | ||
| 878 | // corrupt export directory fails closed. | ||
| 879 | 38 | const Region module = anchor.export_module.empty() ? scope : Region::module_named(anchor.export_module); | |
| 880 | 38 | named_export_region = module; | |
| 881 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 882 | 38 | if (auto *const hook = DetourModKit::detail::g_anchor_after_named_export_lookup_test_hook) | |
| 883 | { | ||
| 884 | 1 | hook(); | |
| 885 | } | ||
| 886 | #endif | ||
| 887 | 38 | named_export_owner = capture_owner_key(module); | |
| 888 | 38 | owner_keys.add_evidence(named_export_owner); | |
| 889 | 38 | if (!named_export_owner_current(anchor, named_export_region, named_export_owner)) | |
| 890 | { | ||
| 891 | 1 | result.status = AnchorStatus::Failed; | |
| 892 | 1 | return; | |
| 893 | } | ||
| 894 | 37 | DetourModKit::detail::ExportResolution discovered_export; | |
| 895 | const Result<Address> discovered = | ||
| 896 | 37 | DetourModKit::detail::resolve_export_with_provenance(anchor.export_name, module, discovered_export); | |
| 897 | 37 | if (discovered) | |
| 898 | { | ||
| 899 | 33 | owner_keys.value = capture_value_owner(anchor, static_cast<std::int64_t>(discovered->raw())); | |
| 900 | 33 | if (provenance != nullptr) | |
| 901 | { | ||
| 902 | 10 | provenance->add(discovered_export); | |
| 903 | } | ||
| 904 | 33 | commit_resolved(anchor, result, static_cast<std::int64_t>(discovered->raw())); | |
| 905 | } | ||
| 906 | else | ||
| 907 | { | ||
| 908 | 4 | result.status = AnchorStatus::Failed; | |
| 909 | } | ||
| 910 | } | ||
| 911 | |||
| 912 | 68 | void resolve_manual(const Anchor &anchor, ResolutionOwnerKeys &owner_keys, ResolvedAnchor &result) noexcept | |
| 913 | { | ||
| 914 | // A pinned literal always "resolves". A report flags its kind as at risk. The default path skips | ||
| 915 | // the validator. validate_manual selects the fail-closed validator path. | ||
| 916 | 68 | if (anchor.validate_manual) | |
| 917 | { | ||
| 918 | 14 | owner_keys.value = capture_value_owner(anchor, anchor.manual_value); | |
| 919 | 14 | commit_resolved(anchor, result, anchor.manual_value); | |
| 920 | } | ||
| 921 | else | ||
| 922 | { | ||
| 923 | 54 | result.value = anchor.manual_value; | |
| 924 | 54 | result.status = AnchorStatus::Resolved; | |
| 925 | } | ||
| 926 | 68 | } | |
| 927 | |||
| 928 | /// Commits the canonical center only when exactly one coherent vote cluster reaches @p threshold. | ||
| 929 | 42 | void commit_quorum_consensus( | |
| 930 | const Anchor &anchor, | ||
| 931 | const std::vector<std::int64_t> &votes, | ||
| 932 | std::size_t threshold, | ||
| 933 | ResolutionOwnerKeys &owner_keys, | ||
| 934 | ResolvedAnchor &result | ||
| 935 | ) | ||
| 936 | { | ||
| 937 | // Collect every distinct vote value that anchors a cluster of at least N votes. Declaration order | ||
| 938 | // never selects among these values. | ||
| 939 | 42 | std::vector<std::int64_t> qualifying; | |
| 940 | 179 | for (const std::int64_t center : votes) | |
| 941 | { | ||
| 942 | 190 | if (std::find(qualifying.begin(), qualifying.end(), center) != qualifying.end()) | |
| 943 | { | ||
| 944 | 25 | continue; | |
| 945 | } | ||
| 946 | 70 | if (votes_agreeing_with(center, votes, anchor.quorum_match, anchor.quorum_tolerance) >= threshold) | |
| 947 | { | ||
| 948 | 44 | qualifying.push_back(center); | |
| 949 | } | ||
| 950 | } | ||
| 951 | 42 | if (qualifying.empty()) | |
| 952 | { | ||
| 953 | 8 | result.status = AnchorStatus::Failed; | |
| 954 | 8 | return; | |
| 955 | } | ||
| 956 | // If two qualified centers disagree, separate clusters cleared N and no single value has | ||
| 957 | // corroboration. This also catches the non-transitive WithinTolerance overlap at 0/4/8 with 4. | ||
| 958 | 34 | const bool ambiguous = std::any_of( | |
| 959 | qualifying.begin(), | ||
| 960 | qualifying.end(), | ||
| 961 | 40 | [&](std::int64_t first) noexcept | |
| 962 | { | ||
| 963 | 40 | return std::any_of( | |
| 964 | qualifying.begin(), | ||
| 965 | qualifying.end(), | ||
| 966 | 56 | [&](std::int64_t second) noexcept | |
| 967 | { | ||
| 968 | 56 | return !quorum_values_agree( | |
| 969 | first, | ||
| 970 | second, | ||
| 971 | 56 | anchor.quorum_match, | |
| 972 | 56 | anchor.quorum_tolerance | |
| 973 | 56 | ); | |
| 974 | } | ||
| 975 | 40 | ); | |
| 976 | } | ||
| 977 | ); | ||
| 978 | 34 | if (ambiguous) | |
| 979 | { | ||
| 980 | 3 | result.status = AnchorStatus::QuorumAmbiguous; | |
| 981 | 3 | return; | |
| 982 | } | ||
| 983 | // The canonical center is the smallest qualified value. commit_resolved invokes the Quorum validator. | ||
| 984 | 31 | const std::int64_t accepted = *std::min_element(qualifying.begin(), qualifying.end()); | |
| 985 | 31 | owner_keys.value = capture_value_owner(anchor, accepted); | |
| 986 | 31 | commit_resolved(anchor, result, accepted); | |
| 987 | 42 | } | |
| 988 | |||
| 989 | ResolvedAnchor resolve_with_profile_impl( | ||
| 990 | const Anchor &anchor, | ||
| 991 | const ScanProfile &profile, | ||
| 992 | Region scope, | ||
| 993 | PhysicalProvenance *provenance, | ||
| 994 | ResolutionOwnerKeys *owner_keys_out, | ||
| 995 | Region *winning_span_out | ||
| 996 | ); | ||
| 997 | |||
| 998 | 78 | void resolve_quorum( | |
| 999 | const Anchor &anchor, | ||
| 1000 | const ScanProfile &profile, | ||
| 1001 | Region scope, | ||
| 1002 | ResolutionOwnerKeys &owner_keys, | ||
| 1003 | std::vector<OwnerKey> &member_keys, | ||
| 1004 | ResolvedAnchor &result | ||
| 1005 | ) | ||
| 1006 | { | ||
| 1007 | // An N-of-M vote survives a patch that breaks some signals if N still agree. Fail closed on a | ||
| 1008 | // malformed declaration. | ||
| 1009 | 78 | if (!valid_quorum_match(anchor.quorum_match)) | |
| 1010 | { | ||
| 1011 | 1 | result.status = AnchorStatus::Failed; | |
| 1012 | 36 | return; | |
| 1013 | } | ||
| 1014 | 77 | const std::span<const Anchor *const> members = anchor.quorum_members; | |
| 1015 | |||
| 1016 | // A quorum needs at least two members. A null or nested-Quorum member is malformed. This rule | ||
| 1017 | // limits recursion to one level. | ||
| 1018 | 77 | if (members.size() < 2) | |
| 1019 | { | ||
| 1020 | 2 | result.status = AnchorStatus::Failed; | |
| 1021 | 2 | return; | |
| 1022 | } | ||
| 1023 | 75 | const bool malformed_member = std::any_of( | |
| 1024 | members.begin(), | ||
| 1025 | members.end(), | ||
| 1026 | 171 | [](const Anchor *member) noexcept | |
| 1027 | 171 | { return member == nullptr || member->kind == AnchorKind::Quorum; } | |
| 1028 | ); | ||
| 1029 | 75 | if (malformed_member) | |
| 1030 | { | ||
| 1031 | 2 | result.status = AnchorStatus::Failed; | |
| 1032 | 2 | return; | |
| 1033 | } | ||
| 1034 | |||
| 1035 | // An N of zero means unanimous. An explicit N of one fails closed rather than silently degrade to a | ||
| 1036 | // single signal. An N above the member count also fails closed. | ||
| 1037 | 73 | const std::size_t threshold = (anchor.quorum_threshold == 0) ? members.size() : anchor.quorum_threshold; | |
| 1038 | 73 | if (threshold < 2 || threshold > members.size()) | |
| 1039 | { | ||
| 1040 | 2 | result.status = AnchorStatus::Failed; | |
| 1041 | 2 | return; | |
| 1042 | } | ||
| 1043 | |||
| 1044 | // Independence has two sources. Check declaration evidence here before the recursive resolves. | ||
| 1045 | // Check evidence from resolved sites afterward. | ||
| 1046 | 71 | if (!internal::quorum_members_pairwise_independent(members)) | |
| 1047 | { | ||
| 1048 | 15 | result.status = AnchorStatus::QuorumNotIndependent; | |
| 1049 | 15 | return; | |
| 1050 | } | ||
| 1051 | |||
| 1052 | // Resolve each member with the same profile so denied kinds and broad defaults propagate. A failed | ||
| 1053 | // member contributes no vote. This behavior gives N-of-M its fault tolerance. | ||
| 1054 | 56 | std::vector<std::int64_t> votes; | |
| 1055 | 56 | votes.reserve(members.size()); | |
| 1056 | 56 | std::vector<PhysicalProvenance> vote_provenance; | |
| 1057 | 56 | vote_provenance.reserve(members.size()); | |
| 1058 | 56 | bool physical_dependency = false; | |
| 1059 | 56 | member_keys.reserve(members.size() * 2); | |
| 1060 | 244 | for (const Anchor *member : members) | |
| 1061 | { | ||
| 1062 | 132 | PhysicalProvenance member_provenance; | |
| 1063 | 132 | ResolutionOwnerKeys member_owner_keys; | |
| 1064 | 132 | const ResolvedAnchor resolved_member = resolve_with_profile_impl( | |
| 1065 | *member, | ||
| 1066 | profile, | ||
| 1067 | scope, | ||
| 1068 | &member_provenance, | ||
| 1069 | &member_owner_keys, | ||
| 1070 | nullptr | ||
| 1071 | ); | ||
| 1072 | 132 | if (resolved_member.status == AnchorStatus::Resolved) | |
| 1073 | { | ||
| 1074 | 125 | physical_dependency = | |
| 1075 | 125 | physical_dependency || std::any_of( | |
| 1076 | vote_provenance.begin(), | ||
| 1077 | vote_provenance.end(), | ||
| 1078 | 86 | [&](const PhysicalProvenance &existing) noexcept | |
| 1079 | 86 | { return member_provenance.intersects(existing); } | |
| 1080 | ); | ||
| 1081 | 125 | votes.push_back(resolved_member.value); | |
| 1082 | 125 | vote_provenance.push_back(member_provenance); | |
| 1083 | // Only a member that casts a vote supplies evidence for corroboration. | ||
| 1084 | 125 | append_owner_keys(member_keys, member_owner_keys); | |
| 1085 | 125 | owner_keys.requires_single_allocation = | |
| 1086 | 125 | owner_keys.requires_single_allocation || member_owner_keys.requires_single_allocation; | |
| 1087 | } | ||
| 1088 | } | ||
| 1089 | 56 | if (physical_dependency) | |
| 1090 | { | ||
| 1091 | 14 | result.status = AnchorStatus::QuorumNotIndependent; | |
| 1092 | 14 | return; | |
| 1093 | } | ||
| 1094 | 42 | commit_quorum_consensus(anchor, votes, threshold, owner_keys, result); | |
| 1095 | 70 | } | |
| 1096 | |||
| 1097 | 408 | void stamp_resolved_domain_and_witness( | |
| 1098 | const Anchor &anchor, | ||
| 1099 | PhysicalSource resolved_source, | ||
| 1100 | const scan::WinningEvidence &resolved_evidence, | ||
| 1101 | const ResolutionOwnerKeys &owner_keys, | ||
| 1102 | ResolvedAnchor &result | ||
| 1103 | ) noexcept | ||
| 1104 | { | ||
| 1105 | // Stamp the typed domain only on a committed value: the single choke point every resolved path | ||
| 1106 | // reaches. A failed entry keeps the fail-closed ResultDomain::Unknown default. | ||
| 1107 | 408 | if (result.status == AnchorStatus::Resolved) | |
| 1108 | { | ||
| 1109 | 312 | result.domain = declared_domain(anchor); | |
| 1110 | // A CodeSite claim is trustworthy only on an executable page. A code-site kind at a non-executable | ||
| 1111 | // data address downgrades to DataAddress. The downgrade denies a mid-hook on a data export. | ||
| 1112 | 375 | if (result.domain == ResultDomain::CodeSite && | |
| 1113 | 63 | !DetourModKit::detail::is_executable_address(static_cast<std::uintptr_t>(result.value))) | |
| 1114 | { | ||
| 1115 | 6 | result.domain = ResultDomain::DataAddress; | |
| 1116 | } | ||
| 1117 | // A truncated or unauthoritative sweep cannot reach Resolved. Address-domain values copy the owner | ||
| 1118 | // identity captured before validation. Scalars have no owner image. | ||
| 1119 | 312 | result.witness.completeness = WitnessCompleteness::Complete; | |
| 1120 | 312 | result.witness.source = resolved_source; | |
| 1121 | 312 | result.witness.evidence = resolved_evidence; | |
| 1122 | 312 | if (anchor.kind == AnchorKind::CodeOperand) | |
| 1123 | { | ||
| 1124 | 103 | result.witness.operand_kind = anchor.operand_kind; | |
| 1125 | } | ||
| 1126 | 312 | if (is_address_domain(result.domain)) | |
| 1127 | { | ||
| 1128 | 128 | result.witness.image = owner_keys.value.identity; | |
| 1129 | } | ||
| 1130 | } | ||
| 1131 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 1132 | 408 | if (result.status == AnchorStatus::Resolved) | |
| 1133 | { | ||
| 1134 | 312 | if (auto *const hook = DetourModKit::detail::g_anchor_after_witness_test_hook) | |
| 1135 | { | ||
| 1136 | 2 | hook(); | |
| 1137 | } | ||
| 1138 | } | ||
| 1139 | #endif | ||
| 1140 | 408 | } | |
| 1141 | |||
| 1142 | 408 | [[nodiscard]] bool resolution_sources_current( | |
| 1143 | const Anchor &anchor, | ||
| 1144 | Region scope, | ||
| 1145 | const ResolutionOwnerKeys &owner_keys, | ||
| 1146 | const std::vector<OwnerKey> &member_keys, | ||
| 1147 | Region named_export_region, | ||
| 1148 | const OwnerKey &named_export_owner | ||
| 1149 | ) noexcept | ||
| 1150 | { | ||
| 1151 | 408 | if (!evidence_images_coherent(owner_keys, member_keys)) | |
| 1152 | { | ||
| 1153 | 16 | return false; | |
| 1154 | } | ||
| 1155 | 392 | if (owner_keys.requires_single_allocation && !scope_is_single_allocation(scope)) | |
| 1156 | { | ||
| 1157 | 2 | return false; | |
| 1158 | } | ||
| 1159 | 390 | if (!named_export_owner_current(anchor, named_export_region, named_export_owner)) | |
| 1160 | { | ||
| 1161 | 1 | return false; | |
| 1162 | } | ||
| 1163 | 389 | return true; | |
| 1164 | } | ||
| 1165 | |||
| 1166 | 414 | ResolvedAnchor resolve_with_profile_impl( | |
| 1167 | const Anchor &anchor, | ||
| 1168 | const ScanProfile &profile, | ||
| 1169 | Region scope, | ||
| 1170 | PhysicalProvenance *provenance, | ||
| 1171 | ResolutionOwnerKeys *owner_keys_out, | ||
| 1172 | Region *winning_span_out | ||
| 1173 | ) | ||
| 1174 | { | ||
| 1175 | 414 | clear_resolution_outputs(provenance, owner_keys_out, winning_span_out); | |
| 1176 | 414 | ResolvedAnchor result{anchor.label, anchor.kind, AnchorStatus::Unresolved, 0}; | |
| 1177 | 414 | PhysicalSource resolved_source = physical_source_of(anchor.kind); | |
| 1178 | // Only a byte-signature rung witnesses a literal span, so this stays absent for every other backend. | ||
| 1179 | 414 | scan::WinningEvidence resolved_evidence{}; | |
| 1180 | 414 | Region resolved_winning_span{}; | |
| 1181 | |||
| 1182 | // A denied kind fails closed before any scan. No other backend silently replaces it. | ||
| 1183 | 414 | if (profile.is_denied(anchor.kind)) | |
| 1184 | { | ||
| 1185 | 4 | result.status = AnchorStatus::Failed; | |
| 1186 | 4 | return result; | |
| 1187 | } | ||
| 1188 | |||
| 1189 | 410 | if (evidence_module_is_scope(anchor.kind) && !scope_is_single_allocation(scope)) | |
| 1190 | { | ||
| 1191 | 2 | return failed_anchor_result(anchor); | |
| 1192 | } | ||
| 1193 | |||
| 1194 | // Capture the owner before the walk. The witness then publishes the identity that produced the value, | ||
| 1195 | // not the identity present at return. resolve_export_name captures an ExportName's effective module. | ||
| 1196 | 408 | ResolutionOwnerKeys owner_keys; | |
| 1197 | 408 | owner_keys.requires_single_allocation = evidence_module_is_scope(anchor.kind); | |
| 1198 | 408 | if (evidence_module_is_scope(anchor.kind)) | |
| 1199 | { | ||
| 1200 | 215 | owner_keys.add_evidence(capture_scope_owner_key(scope)); | |
| 1201 | } | ||
| 1202 | // A Quorum owns no direct evidence. It carries the key from each member that casts a vote. | ||
| 1203 | 408 | std::vector<OwnerKey> member_keys; | |
| 1204 | 408 | Region named_export_region{}; | |
| 1205 | 408 | OwnerKey named_export_owner{}; | |
| 1206 | |||
| 1207 | 408 | switch (anchor.kind) | |
| 1208 | { | ||
| 1209 | 5 | case AnchorKind::VtableIdentity: | |
| 1210 | 5 | resolve_vtable_identity(anchor, scope, owner_keys, result); | |
| 1211 | 5 | break; | |
| 1212 | 86 | case AnchorKind::RipGlobal: | |
| 1213 | 86 | if (anchor.pages != scan::Pages::Readable && anchor.pages != scan::Pages::Executable) | |
| 1214 | { | ||
| 1215 | ✗ | return failed_anchor_result(anchor); | |
| 1216 | } | ||
| 1217 | 86 | resolve_rip_global( | |
| 1218 | anchor, | ||
| 1219 | profile, | ||
| 1220 | scope, | ||
| 1221 | provenance, | ||
| 1222 | owner_keys, | ||
| 1223 | result, | ||
| 1224 | resolved_source, | ||
| 1225 | resolved_evidence, | ||
| 1226 | resolved_winning_span | ||
| 1227 | ); | ||
| 1228 | 86 | break; | |
| 1229 | 112 | case AnchorKind::CodeOperand: | |
| 1230 | 112 | resolve_code_operand(anchor, profile, scope, provenance, owner_keys, result); | |
| 1231 | 112 | break; | |
| 1232 | 12 | case AnchorKind::StringXref: | |
| 1233 | 12 | resolve_string_xref(anchor, profile, scope, provenance, owner_keys, result); | |
| 1234 | 12 | break; | |
| 1235 | 38 | case AnchorKind::ExportName: | |
| 1236 | 38 | resolve_export_name( | |
| 1237 | anchor, | ||
| 1238 | scope, | ||
| 1239 | provenance, | ||
| 1240 | owner_keys, | ||
| 1241 | result, | ||
| 1242 | named_export_region, | ||
| 1243 | named_export_owner | ||
| 1244 | ); | ||
| 1245 | 38 | break; | |
| 1246 | 68 | case AnchorKind::Manual: | |
| 1247 | 68 | resolve_manual(anchor, owner_keys, result); | |
| 1248 | 68 | break; | |
| 1249 | 6 | case AnchorKind::CallArgHome: | |
| 1250 | // This kind is reserved for a future prologue-dataflow backend. No resolver exists yet. | ||
| 1251 | 6 | result.status = AnchorStatus::Unsupported; | |
| 1252 | 6 | break; | |
| 1253 | 78 | case AnchorKind::Quorum: | |
| 1254 | 78 | resolve_quorum(anchor, profile, scope, owner_keys, member_keys, result); | |
| 1255 | 78 | break; | |
| 1256 | 2 | case AnchorKind::Unset: | |
| 1257 | // An Unset kind on a default-constructed anchor fails closed rather than invent a value. | ||
| 1258 | 2 | result.status = AnchorStatus::Failed; | |
| 1259 | 2 | break; | |
| 1260 | } | ||
| 1261 | |||
| 1262 | // An out-of-range AnchorKind reaches here with the initial non-terminal Unresolved. Normalize it to | ||
| 1263 | // Failed so a resolved report never leaves an entry Unresolved. | ||
| 1264 | 408 | if (result.status == AnchorStatus::Unresolved) | |
| 1265 | { | ||
| 1266 | 1 | result.status = AnchorStatus::Failed; | |
| 1267 | } | ||
| 1268 | 408 | stamp_resolved_domain_and_witness(anchor, resolved_source, resolved_evidence, owner_keys, result); | |
| 1269 | |||
| 1270 | // Re-check after every validator, domain probe, and witness write. Temporal drift overrides quorum | ||
| 1271 | // diagnostics: mixed generations are a failed trust transaction. | ||
| 1272 | 408 | if (!resolution_sources_current( | |
| 1273 | anchor, | ||
| 1274 | scope, | ||
| 1275 | owner_keys, | ||
| 1276 | member_keys, | ||
| 1277 | named_export_region, | ||
| 1278 | named_export_owner | ||
| 1279 | )) | ||
| 1280 | { | ||
| 1281 | 19 | return failed_anchor_result(anchor); | |
| 1282 | } | ||
| 1283 | 389 | if (owner_keys_out != nullptr) | |
| 1284 | { | ||
| 1285 | 131 | *owner_keys_out = owner_keys; | |
| 1286 | } | ||
| 1287 | 389 | if (winning_span_out != nullptr) | |
| 1288 | { | ||
| 1289 | 48 | *winning_span_out = resolved_winning_span; | |
| 1290 | } | ||
| 1291 | 389 | return result; | |
| 1292 | 408 | } | |
| 1293 | } // namespace | ||
| 1294 | |||
| 1295 | 234 | ResolvedAnchor resolve_with_profile(const Anchor &anchor, const ScanProfile &profile, Region scope) | |
| 1296 | { | ||
| 1297 | 234 | return resolve_with_profile_impl(anchor, profile, scope, nullptr, nullptr, nullptr); | |
| 1298 | } | ||
| 1299 | |||
| 1300 | namespace internal | ||
| 1301 | { | ||
| 1302 | 48 | ResolvedAnchor resolve_with_winning_span(const Anchor &anchor, Region scope, Region &winning_span) | |
| 1303 | { | ||
| 1304 | 48 | return resolve_with_profile_impl(anchor, ScanProfile{}, scope, nullptr, nullptr, &winning_span); | |
| 1305 | } | ||
| 1306 | } // namespace internal | ||
| 1307 | |||
| 1308 | 218 | ResolvedAnchor resolve(const Anchor &anchor, Region scope) | |
| 1309 | { | ||
| 1310 | // An empty profile denies nothing and widens nothing, so this is exactly the un-profiled resolution. | ||
| 1311 | 218 | return resolve_with_profile(anchor, ScanProfile{}, scope); | |
| 1312 | } | ||
| 1313 | |||
| 1314 | 5 | std::size_t resolve_all(std::span<const Anchor> anchors, std::span<ResolvedAnchor> out, Region scope) | |
| 1315 | { | ||
| 1316 | 5 | const std::size_t count = (anchors.size() < out.size()) ? anchors.size() : out.size(); | |
| 1317 | 15 | for (std::size_t i = 0; i < count; ++i) | |
| 1318 | { | ||
| 1319 | 10 | out[i] = resolve(anchors[i], scope); | |
| 1320 | } | ||
| 1321 | 5 | return count; | |
| 1322 | } | ||
| 1323 | |||
| 1324 | 1 | std::size_t resolve_all_parallel( | |
| 1325 | std::span<const Anchor> anchors, | ||
| 1326 | std::span<ResolvedAnchor> out, | ||
| 1327 | Region scope, | ||
| 1328 | std::size_t max_workers | ||
| 1329 | ) | ||
| 1330 | { | ||
| 1331 | 1 | const std::size_t count = (anchors.size() < out.size()) ? anchors.size() : out.size(); | |
| 1332 | const std::vector<ResolvedAnchor> results = DetourModKit::detail::run_fork_join<Anchor, ResolvedAnchor>( | ||
| 1333 | anchors.first(count), | ||
| 1334 | max_workers, | ||
| 1335 | 3 | [scope](const Anchor &anchor) -> ResolvedAnchor { return resolve(anchor, scope); }, | |
| 1336 | 3 | [](const Anchor &anchor) noexcept -> ResolvedAnchor { return failed_anchor_result(anchor); } | |
| 1337 | 1 | ); | |
| 1338 | |||
| 1339 | 4 | for (std::size_t i = 0; i < count; ++i) | |
| 1340 | { | ||
| 1341 | 3 | out[i] = results[i]; | |
| 1342 | } | ||
| 1343 | 1 | return count; | |
| 1344 | 1 | } | |
| 1345 | |||
| 1346 | 2 | std::size_t resolve_all_with_profile( | |
| 1347 | std::span<const Anchor> anchors, | ||
| 1348 | std::span<ResolvedAnchor> out, | ||
| 1349 | const ScanProfile &profile, | ||
| 1350 | Region scope | ||
| 1351 | ) | ||
| 1352 | { | ||
| 1353 | 2 | const std::size_t count = (anchors.size() < out.size()) ? anchors.size() : out.size(); | |
| 1354 | 7 | for (std::size_t i = 0; i < count; ++i) | |
| 1355 | { | ||
| 1356 | 5 | out[i] = resolve_with_profile(anchors[i], profile, scope); | |
| 1357 | } | ||
| 1358 | 2 | return count; | |
| 1359 | } | ||
| 1360 | |||
| 1361 | 1 | std::size_t resolve_all_with_profile_parallel( | |
| 1362 | std::span<const Anchor> anchors, | ||
| 1363 | std::span<ResolvedAnchor> out, | ||
| 1364 | const ScanProfile &profile, | ||
| 1365 | Region scope, | ||
| 1366 | std::size_t max_workers | ||
| 1367 | ) | ||
| 1368 | { | ||
| 1369 | 1 | const std::size_t count = (anchors.size() < out.size()) ? anchors.size() : out.size(); | |
| 1370 | const std::vector<ResolvedAnchor> results = DetourModKit::detail::run_fork_join<Anchor, ResolvedAnchor>( | ||
| 1371 | anchors.first(count), | ||
| 1372 | max_workers, | ||
| 1373 | 3 | [&profile, scope](const Anchor &anchor) -> ResolvedAnchor | |
| 1374 | 3 | { return resolve_with_profile(anchor, profile, scope); }, | |
| 1375 | 3 | [](const Anchor &anchor) noexcept -> ResolvedAnchor { return failed_anchor_result(anchor); } | |
| 1376 | 1 | ); | |
| 1377 | |||
| 1378 | 4 | for (std::size_t i = 0; i < count; ++i) | |
| 1379 | { | ||
| 1380 | 3 | out[i] = results[i]; | |
| 1381 | } | ||
| 1382 | 1 | return count; | |
| 1383 | 1 | } | |
| 1384 | |||
| 1385 | 793 | ResultDomain declared_domain(const Anchor &anchor) noexcept | |
| 1386 | { | ||
| 1387 | 793 | switch (anchor.kind) | |
| 1388 | { | ||
| 1389 | 14 | case AnchorKind::VtableIdentity: | |
| 1390 | 14 | return ResultDomain::VtableAddress; | |
| 1391 | 313 | case AnchorKind::CodeOperand: | |
| 1392 | 625 | if (!valid_operand_kind(anchor.operand_kind) || | |
| 1393 | 312 | !detail::valid_code_constant_byte_width(anchor.byte_width)) | |
| 1394 | { | ||
| 1395 | 3 | return ResultDomain::Unknown; | |
| 1396 | } | ||
| 1397 | 310 | return ResultDomain::Scalar; | |
| 1398 | 107 | case AnchorKind::Manual: | |
| 1399 | // A decoded operand value and a pinned literal are constants, not addresses to write through. | ||
| 1400 | 107 | return ResultDomain::Scalar; | |
| 1401 | 23 | case AnchorKind::StringXref: | |
| 1402 | 23 | if (!valid_string_encoding(anchor.xref_encoding) || !valid_xref_return(anchor.xref_return)) | |
| 1403 | { | ||
| 1404 | 2 | return ResultDomain::Unknown; | |
| 1405 | } | ||
| 1406 | // The instruction or its parent function is code. A pointer-slot return is a data slot. | ||
| 1407 | 21 | if (anchor.xref_return == scan::XrefReturn::StringPointerSlot) | |
| 1408 | { | ||
| 1409 | 4 | return ResultDomain::DataAddress; | |
| 1410 | } | ||
| 1411 | 17 | return ResultDomain::CodeSite; | |
| 1412 | 83 | case AnchorKind::ExportName: | |
| 1413 | 83 | return ResultDomain::CodeSite; | |
| 1414 | 185 | case AnchorKind::RipGlobal: | |
| 1415 | 185 | if (anchor.pages != scan::Pages::Readable && anchor.pages != scan::Pages::Executable) | |
| 1416 | { | ||
| 1417 | 1 | return ResultDomain::Unknown; | |
| 1418 | } | ||
| 1419 | // The pages field states the author contract. Executable narrows the cascade to image instruction | ||
| 1420 | // sites. The default Readable admits a data-page global. | ||
| 1421 | 184 | if (anchor.pages == scan::Pages::Executable) | |
| 1422 | { | ||
| 1423 | 44 | return ResultDomain::CodeSite; | |
| 1424 | } | ||
| 1425 | 140 | return ResultDomain::DataAddress; | |
| 1426 | 65 | case AnchorKind::Quorum: | |
| 1427 | { | ||
| 1428 | 65 | if (!valid_quorum_match(anchor.quorum_match)) | |
| 1429 | { | ||
| 1430 | 1 | return ResultDomain::Unknown; | |
| 1431 | } | ||
| 1432 | // The quorum domain is the one specific non-Scalar domain on which its members agree. A Manual or | ||
| 1433 | // Scalar member is a wildcard corroborator. Different specific domains are ambiguous. | ||
| 1434 | // A nested Quorum member is malformed (rejected at resolve), so it is skipped, not recursed into. | ||
| 1435 | 64 | ResultDomain domain = ResultDomain::Scalar; | |
| 1436 | 277 | for (const Anchor *member : anchor.quorum_members) | |
| 1437 | { | ||
| 1438 | 150 | if (member == nullptr || member->kind == AnchorKind::Quorum) | |
| 1439 | { | ||
| 1440 | ✗ | continue; | |
| 1441 | } | ||
| 1442 | 150 | const ResultDomain member_domain = declared_domain(*member); | |
| 1443 | 150 | if (member_domain == ResultDomain::Scalar || member_domain == ResultDomain::Unknown) | |
| 1444 | { | ||
| 1445 | 117 | continue; | |
| 1446 | } | ||
| 1447 | 33 | if (domain == ResultDomain::Scalar) | |
| 1448 | { | ||
| 1449 | 25 | domain = member_domain; | |
| 1450 | } | ||
| 1451 | 8 | else if (domain != member_domain) | |
| 1452 | { | ||
| 1453 | 1 | return ResultDomain::Unknown; | |
| 1454 | } | ||
| 1455 | } | ||
| 1456 | 63 | return domain; | |
| 1457 | } | ||
| 1458 | 2 | case AnchorKind::CallArgHome: | |
| 1459 | case AnchorKind::Unset: | ||
| 1460 | 2 | return ResultDomain::Unknown; | |
| 1461 | } | ||
| 1462 | 1 | return ResultDomain::Unknown; | |
| 1463 | } | ||
| 1464 | } // namespace anchor | ||
| 1465 | } // namespace DetourModKit | ||
| 1466 |