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 |
4/6✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 5 taken 106 times.
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 6 not taken.
✓ Branch 7 → 8 taken 108 times.
✗ Branch 7 → 9 not taken.
|
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 |
2/2✓ Branch 2 → 3 taken 99 times.
✓ Branch 2 → 4 taken 133 times.
|
232 | if (match == QuorumMatch::ExactValue) |
| 77 | { | ||
| 78 | 99 | return first == second; | |
| 79 | } | ||
| 80 |
2/2✓ Branch 4 → 5 taken 4 times.
✓ Branch 4 → 6 taken 129 times.
|
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 |
2/2✓ Branch 6 → 7 taken 38 times.
✓ Branch 6 → 8 taken 91 times.
|
129 | const std::int64_t lo = (first < second) ? first : second; |
| 87 |
2/2✓ Branch 9 → 10 taken 38 times.
✓ Branch 9 → 11 taken 91 times.
|
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 |
4/4✓ Branch 2 → 3 taken 64 times.
✓ Branch 2 → 4 taken 361 times.
✓ Branch 3 → 4 taken 62 times.
✓ Branch 3 → 5 taken 2 times.
|
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 |
4/4✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 4 taken 31 times.
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 2 times.
|
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 |
2/2✓ Branch 2 → 3 taken 31 times.
✓ Branch 2 → 4 taken 2 times.
|
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 |
4/4✓ Branch 2 → 3 taken 43 times.
✓ Branch 2 → 4 taken 100 times.
✓ Branch 3 → 4 taken 41 times.
✓ Branch 3 → 5 taken 2 times.
|
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 |
4/4✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 4 taken 106 times.
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 1 time.
|
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 |
3/6✓ Branch 3 → 4 taken 167 times.
✗ Branch 3 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 167 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 167 times.
|
167 | if (!span.base || span.size == 0) |
| 139 | { | ||
| 140 | ✗ | return; | |
| 141 | } | ||
| 142 |
1/2✓ Branch 11 → 12 taken 167 times.
✗ Branch 11 → 14 not taken.
|
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 |
1/2✓ Branch 3 → 4 taken 10 times.
✗ Branch 3 → 5 not taken.
|
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 |
2/2✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 84 times.
|
86 | if (DetourModKit::detail::same_export_site(m_export, other.m_export)) |
| 159 | { | ||
| 160 | 2 | return true; | |
| 161 | } | ||
| 162 |
2/2✓ Branch 15 → 6 taken 136 times.
✓ Branch 15 → 16 taken 72 times.
|
208 | for (std::size_t i = 0; i < m_size; ++i) |
| 163 | { | ||
| 164 |
2/2✓ Branch 13 → 7 taken 226 times.
✓ Branch 13 → 14 taken 124 times.
|
350 | for (std::size_t j = 0; j < other.m_size; ++j) |
| 165 | { | ||
| 166 |
2/2✓ Branch 10 → 11 taken 12 times.
✓ Branch 10 → 12 taken 214 times.
|
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 |
4/4✓ Branch 5 → 6 taken 80 times.
✓ Branch 5 → 11 taken 146 times.
✓ Branch 9 → 10 taken 12 times.
✓ Branch 9 → 11 taken 68 times.
|
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 |
2/2✓ Branch 18 → 4 taken 176 times.
✓ Branch 18 → 19 taken 70 times.
|
316 | for (const std::int64_t vote : votes) |
| 197 | { | ||
| 198 |
2/2✓ Branch 7 → 8 taken 135 times.
✓ Branch 7 → 9 taken 41 times.
|
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 | 411 | [[nodiscard]] PhysicalSource physical_source_of(AnchorKind kind) noexcept | |
| 208 | { | ||
| 209 |
9/9✓ Branch 2 → 3 taken 85 times.
✓ Branch 2 → 4 taken 12 times.
✓ Branch 2 → 5 taken 5 times.
✓ Branch 2 → 6 taken 39 times.
✓ Branch 2 → 7 taken 115 times.
✓ Branch 2 → 8 taken 68 times.
✓ Branch 2 → 9 taken 78 times.
✓ Branch 2 → 10 taken 8 times.
✓ Branch 2 → 11 taken 1 time.
|
411 | switch (kind) |
| 210 | { | ||
| 211 | 85 | case AnchorKind::RipGlobal: | |
| 212 | 85 | 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 | 73 | [[nodiscard]] PhysicalSource physical_source_of(scan::Mode mode) noexcept | |
| 233 | { | ||
| 234 |
2/4✓ Branch 2 → 3 taken 71 times.
✗ Branch 2 → 4 not taken.
✓ Branch 2 → 5 taken 2 times.
✗ Branch 2 → 6 not taken.
|
73 | switch (mode) |
| 235 | { | ||
| 236 | 71 | case scan::Mode::Direct: | |
| 237 | case scan::Mode::RipRelative: | ||
| 238 | 71 | 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 | 267 | 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 |
4/4✓ Branch 2 → 3 taken 8 times.
✓ Branch 2 → 7 taken 259 times.
✓ Branch 3 → 4 taken 7 times.
✓ Branch 3 → 7 taken 1 time.
|
267 | if (anchor.require_validator && anchor.kind != AnchorKind::Quorum && |
| 254 |
4/4✓ Branch 4 → 5 taken 6 times.
✓ Branch 4 → 7 taken 1 time.
✓ Branch 5 → 6 taken 3 times.
✓ Branch 5 → 7 taken 3 times.
|
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 |
6/6✓ Branch 7 → 8 taken 27 times.
✓ Branch 7 → 11 taken 237 times.
✓ Branch 9 → 10 taken 9 times.
✓ Branch 9 → 11 taken 18 times.
✓ Branch 12 → 13 taken 9 times.
✓ Branch 12 → 14 taken 255 times.
|
264 | 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 | 255 | result.value = value; | |
| 267 | 255 | 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 | 1389 | [[nodiscard]] constexpr bool tracked() const noexcept { return backing != OwnerBacking::None; } | |
| 299 | 856 | [[nodiscard]] constexpr bool has_module() const noexcept { return backing == OwnerBacking::Image; } | |
| 300 |
4/8✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 60 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 60 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 60 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 60 times.
|
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 | 462 | [[nodiscard]] bool scope_is_single_allocation(Region scope) noexcept | |
| 311 | { | ||
| 312 |
6/8✓ Branch 3 → 4 taken 461 times.
✓ Branch 3 → 7 taken 1 time.
✓ Branch 4 → 5 taken 461 times.
✗ Branch 4 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 461 times.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 461 times.
|
462 | if (scope.base.raw() == 0 || scope.size == 0 || scope.base.raw() > UINTPTR_MAX - scope.size) |
| 313 | { | ||
| 314 | 1 | return false; | |
| 315 | } | ||
| 316 | 461 | MEMORY_BASIC_INFORMATION memory_info{}; | |
| 317 |
2/4✓ Branch 13 → 14 taken 461 times.
✗ Branch 13 → 15 not taken.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 461 times.
|
922 | if (::VirtualQuery(scope.base.as<const void *>(), &memory_info, sizeof(memory_info)) == 0 || |
| 318 |
1/2✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 461 times.
|
461 | memory_info.AllocationBase == nullptr) |
| 319 | { | ||
| 320 | ✗ | return false; | |
| 321 | } | ||
| 322 | 461 | const void *const allocation_base = memory_info.AllocationBase; | |
| 323 | 461 | const std::uintptr_t scope_end = scope.base.raw() + scope.size; | |
| 324 | 461 | std::uintptr_t cursor = scope.base.raw(); | |
| 325 |
1/2✓ Branch 37 → 22 taken 17703 times.
✗ Branch 37 → 38 not taken.
|
17703 | while (cursor < scope_end) |
| 326 | { | ||
| 327 | 17703 | if (::VirtualQuery(reinterpret_cast<const void *>(cursor), &memory_info, sizeof(memory_info)) == | |
| 328 |
3/4✓ Branch 23 → 24 taken 17703 times.
✗ Branch 23 → 25 not taken.
✓ Branch 27 → 28 taken 3 times.
✓ Branch 27 → 29 taken 17700 times.
|
35406 | 0 || |
| 329 |
2/2✓ Branch 24 → 25 taken 3 times.
✓ Branch 24 → 26 taken 17700 times.
|
17703 | memory_info.AllocationBase != allocation_base) |
| 330 | { | ||
| 331 | 3 | return false; | |
| 332 | } | ||
| 333 | 17700 | const std::uintptr_t region_base = reinterpret_cast<std::uintptr_t>(memory_info.BaseAddress); | |
| 334 |
2/4✓ Branch 29 → 30 taken 17700 times.
✗ Branch 29 → 31 not taken.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 17700 times.
|
17700 | if (memory_info.RegionSize == 0 || region_base > UINTPTR_MAX - memory_info.RegionSize) |
| 335 | { | ||
| 336 | ✗ | return false; | |
| 337 | } | ||
| 338 | 17700 | const std::uintptr_t region_end = region_base + memory_info.RegionSize; | |
| 339 |
1/2✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 17700 times.
|
17700 | if (region_end <= cursor) |
| 340 | { | ||
| 341 | ✗ | return false; | |
| 342 | } | ||
| 343 |
2/2✓ Branch 34 → 35 taken 458 times.
✓ Branch 34 → 36 taken 17242 times.
|
17700 | if (region_end >= scope_end) |
| 344 | { | ||
| 345 | 458 | return true; | |
| 346 | } | ||
| 347 | 17242 | 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 | 250 | void add_evidence(const OwnerKey &key) noexcept | |
| 361 | { | ||
| 362 |
2/2✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 249 times.
|
250 | if (!key.tracked()) |
| 363 | { | ||
| 364 | 1 | return; | |
| 365 | } | ||
| 366 |
1/2✗ Branch 11 → 6 not taken.
✓ Branch 11 → 12 taken 249 times.
|
249 | for (std::size_t i = 0; i < evidence_count; ++i) |
| 367 | { | ||
| 368 | ✗ | if (evidence[i] == key) | |
| 369 | { | ||
| 370 | ✗ | return; | |
| 371 | } | ||
| 372 | } | ||
| 373 |
1/2✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 249 times.
|
498 | if (evidence_count == evidence.size()) |
| 374 | { | ||
| 375 | ✗ | overflowed = true; | |
| 376 | ✗ | return; | |
| 377 | } | ||
| 378 | 249 | 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 | 437 | [[nodiscard]] OwnerKey capture_owner_key(Region region) noexcept | |
| 388 | { | ||
| 389 |
5/6✓ Branch 3 → 4 taken 434 times.
✓ Branch 3 → 8 taken 3 times.
✓ Branch 4 → 5 taken 434 times.
✗ Branch 4 → 8 not taken.
✓ Branch 10 → 11 taken 4 times.
✓ Branch 10 → 12 taken 433 times.
|
871 | if (region.base.raw() == 0 || region.size == 0 || |
| 390 |
2/2✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 433 times.
|
434 | !DetourModKit::detail::is_plausible_ptr(region.base.raw())) |
| 391 | { | ||
| 392 | 4 | return OwnerKey{}; | |
| 393 | } | ||
| 394 | 433 | MEMORY_BASIC_INFORMATION memory_info{}; | |
| 395 |
2/4✓ Branch 14 → 15 taken 433 times.
✗ Branch 14 → 16 not taken.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 21 taken 433 times.
|
866 | if (::VirtualQuery(region.base.as<const void *>(), &memory_info, sizeof(memory_info)) == 0 || |
| 396 |
1/2✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 433 times.
|
433 | memory_info.State != MEM_COMMIT) |
| 397 | { | ||
| 398 | ✗ | return OwnerKey{region.base.raw(), 0, {}, OwnerBacking::Missing}; | |
| 399 | } | ||
| 400 | 433 | const std::uintptr_t allocation_base = reinterpret_cast<std::uintptr_t>(memory_info.AllocationBase); | |
| 401 |
1/2✗ Branch 21 → 22 not taken.
✓ Branch 21 → 24 taken 433 times.
|
433 | if (allocation_base == 0) |
| 402 | { | ||
| 403 | ✗ | return OwnerKey{region.base.raw(), 0, {}, OwnerBacking::Missing}; | |
| 404 | } | ||
| 405 |
2/2✓ Branch 24 → 25 taken 209 times.
✓ Branch 24 → 28 taken 224 times.
|
433 | if (memory_info.Type == MEM_IMAGE) |
| 406 | { | ||
| 407 | return OwnerKey{ | ||
| 408 | allocation_base, | ||
| 409 | allocation_base, | ||
| 410 | 209 | scan::image_identity(Region{Address{allocation_base}, 1}), | |
| 411 | OwnerBacking::Image | ||
| 412 | 418 | }; | |
| 413 | } | ||
| 414 |
1/2✓ Branch 28 → 29 taken 224 times.
✗ Branch 28 → 31 not taken.
|
224 | if (memory_info.Type == MEM_PRIVATE) |
| 415 | { | ||
| 416 | 224 | 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 | 212 | [[nodiscard]] OwnerKey capture_scope_owner_key(Region scope) noexcept | |
| 431 | { | ||
| 432 |
4/8✓ Branch 3 → 4 taken 212 times.
✗ Branch 3 → 7 not taken.
✓ Branch 4 → 5 taken 212 times.
✗ Branch 4 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 212 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 212 times.
|
212 | if (scope.base.raw() == 0 || scope.size == 0 || scope.base.raw() > UINTPTR_MAX - scope.size) |
| 433 | { | ||
| 434 | ✗ | return capture_owner_key(scope); | |
| 435 | } | ||
| 436 | 212 | const std::uintptr_t scope_end = scope.base.raw() + scope.size; | |
| 437 | 212 | std::uintptr_t cursor = scope.base.raw(); | |
| 438 |
1/2✓ Branch 30 → 14 taken 214 times.
✗ Branch 30 → 31 not taken.
|
214 | while (cursor < scope_end) |
| 439 | { | ||
| 440 | 214 | MEMORY_BASIC_INFORMATION memory_info{}; | |
| 441 |
1/2✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 214 times.
|
214 | if (::VirtualQuery(reinterpret_cast<const void *>(cursor), &memory_info, sizeof(memory_info)) == 0) |
| 442 | { | ||
| 443 | ✗ | break; | |
| 444 | } | ||
| 445 | 214 | const std::uintptr_t region_base = reinterpret_cast<std::uintptr_t>(memory_info.BaseAddress); | |
| 446 |
2/4✓ Branch 17 → 18 taken 214 times.
✗ Branch 17 → 29 not taken.
✓ Branch 18 → 19 taken 214 times.
✗ Branch 18 → 29 not taken.
|
214 | if (memory_info.RegionSize == 0 || region_base > UINTPTR_MAX - memory_info.RegionSize) |
| 447 | { | ||
| 448 | break; | ||
| 449 | } | ||
| 450 |
2/2✓ Branch 19 → 20 taken 212 times.
✓ Branch 19 → 26 taken 2 times.
|
214 | if (memory_info.State == MEM_COMMIT) |
| 451 | { | ||
| 452 |
1/2✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 212 times.
|
212 | const std::uintptr_t committed_address = (cursor > region_base) ? cursor : region_base; |
| 453 | 212 | 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/4✓ Branch 26 → 27 taken 2 times.
✗ Branch 26 → 29 not taken.
✓ Branch 27 → 28 taken 2 times.
✗ Branch 27 → 29 not taken.
|
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 | 425 | [[nodiscard]] bool named_export_owner_current( | |
| 467 | const Anchor &anchor, | ||
| 468 | Region expected_region, | ||
| 469 | const OwnerKey &expected_owner | ||
| 470 | ) noexcept | ||
| 471 | { | ||
| 472 |
6/6✓ Branch 2 → 3 taken 72 times.
✓ Branch 2 → 5 taken 353 times.
✓ Branch 4 → 5 taken 10 times.
✓ Branch 4 → 6 taken 62 times.
✓ Branch 7 → 8 taken 363 times.
✓ Branch 7 → 9 taken 62 times.
|
425 | if (anchor.kind != AnchorKind::ExportName || anchor.export_module.empty()) |
| 473 | { | ||
| 474 | 363 | return true; | |
| 475 | } | ||
| 476 | 62 | const Region live_region = Region::module_named(anchor.export_module); | |
| 477 |
4/6✓ Branch 11 → 12 taken 60 times.
✓ Branch 11 → 17 taken 2 times.
✓ Branch 12 → 13 taken 60 times.
✗ Branch 12 → 17 not taken.
✓ Branch 15 → 16 taken 60 times.
✗ Branch 15 → 17 not taken.
|
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 | 501 | [[nodiscard]] bool owner_key_current(const OwnerKey &key) noexcept | |
| 488 | { | ||
| 489 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 501 times.
|
501 | if (!key.tracked()) |
| 490 | { | ||
| 491 | ✗ | return true; | |
| 492 | } | ||
| 493 |
1/2✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 501 times.
|
501 | if (key.backing == OwnerBacking::Missing) |
| 494 | { | ||
| 495 | ✗ | return false; | |
| 496 | } | ||
| 497 | 501 | MEMORY_BASIC_INFORMATION memory_info{}; | |
| 498 | 501 | if (::VirtualQuery(reinterpret_cast<const void *>(key.address), &memory_info, sizeof(memory_info)) == | |
| 499 | 501 | 0 || | |
| 500 |
3/6✓ Branch 8 → 9 taken 501 times.
✗ Branch 8 → 11 not taken.
✓ Branch 9 → 10 taken 501 times.
✗ Branch 9 → 11 not taken.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 501 times.
|
1002 | memory_info.State != MEM_COMMIT || |
| 501 |
1/2✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 501 times.
|
501 | reinterpret_cast<std::uintptr_t>(memory_info.AllocationBase) != key.allocation_base) |
| 502 | { | ||
| 503 | ✗ | return false; | |
| 504 | } | ||
| 505 |
2/2✓ Branch 16 → 17 taken 343 times.
✓ Branch 16 → 21 taken 158 times.
|
501 | if (!key.has_module()) |
| 506 | { | ||
| 507 |
1/2✓ Branch 17 → 18 taken 343 times.
✗ Branch 17 → 19 not taken.
|
343 | const DWORD expected_type = key.backing == OwnerBacking::Private ? MEM_PRIVATE : MEM_MAPPED; |
| 508 | 343 | return memory_info.Type == expected_type; | |
| 509 | } | ||
| 510 |
6/6✓ Branch 21 → 22 taken 157 times.
✓ Branch 21 → 24 taken 1 time.
✓ Branch 23 → 24 taken 2 times.
✓ Branch 23 → 25 taken 155 times.
✓ Branch 26 → 27 taken 3 times.
✓ Branch 26 → 28 taken 155 times.
|
158 | if (memory_info.Type != MEM_IMAGE || !key.identity.present()) |
| 511 | { | ||
| 512 | 3 | return false; | |
| 513 | } | ||
| 514 | 155 | const scan::ImageIdentity live_identity = scan::image_identity(Region{Address{key.allocation_base}, 1}); | |
| 515 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 516 |
2/2✓ Branch 30 → 31 taken 2 times.
✓ Branch 30 → 32 taken 153 times.
|
155 | if (auto *const hook = DetourModKit::detail::g_anchor_after_owner_identity_test_hook) |
| 517 | { | ||
| 518 | 2 | hook(); | |
| 519 | } | ||
| 520 | #endif | ||
| 521 | 155 | MEMORY_BASIC_INFORMATION confirmed_info{}; | |
| 522 | 465 | if (::VirtualQuery( | |
| 523 | 155 | reinterpret_cast<const void *>(key.address), | |
| 524 | &confirmed_info, | ||
| 525 | sizeof(confirmed_info) | ||
| 526 | 155 | ) == 0 || | |
| 527 |
6/8✓ Branch 33 → 34 taken 155 times.
✗ Branch 33 → 37 not taken.
✓ Branch 34 → 35 taken 155 times.
✗ Branch 34 → 37 not taken.
✓ Branch 35 → 36 taken 154 times.
✓ Branch 35 → 37 taken 1 time.
✓ Branch 39 → 40 taken 1 time.
✓ Branch 39 → 41 taken 154 times.
|
309 | confirmed_info.State != MEM_COMMIT || confirmed_info.Type != MEM_IMAGE || |
| 528 |
1/2✗ Branch 36 → 37 not taken.
✓ Branch 36 → 38 taken 154 times.
|
154 | reinterpret_cast<std::uintptr_t>(confirmed_info.AllocationBase) != key.allocation_base) |
| 529 | { | ||
| 530 | 1 | return false; | |
| 531 | } | ||
| 532 | const scan::ImageIdentity confirmed_identity = | ||
| 533 | 154 | scan::image_identity(Region{Address{key.allocation_base}, 1}); | |
| 534 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 535 |
2/2✓ Branch 43 → 44 taken 1 time.
✓ Branch 43 → 45 taken 153 times.
|
154 | if (auto *const hook = DetourModKit::detail::g_anchor_after_confirmed_owner_identity_test_hook) |
| 536 | { | ||
| 537 | 1 | hook(); | |
| 538 | } | ||
| 539 | #endif | ||
| 540 | 154 | MEMORY_BASIC_INFORMATION final_info{}; | |
| 541 | 154 | if (::VirtualQuery(reinterpret_cast<const void *>(key.address), &final_info, sizeof(final_info)) == 0 || | |
| 542 |
6/8✓ Branch 46 → 47 taken 154 times.
✗ Branch 46 → 50 not taken.
✓ Branch 47 → 48 taken 154 times.
✗ Branch 47 → 50 not taken.
✓ Branch 48 → 49 taken 153 times.
✓ Branch 48 → 50 taken 1 time.
✓ Branch 52 → 53 taken 1 time.
✓ Branch 52 → 54 taken 153 times.
|
307 | final_info.State != MEM_COMMIT || final_info.Type != MEM_IMAGE || |
| 543 |
1/2✗ Branch 49 → 50 not taken.
✓ Branch 49 → 51 taken 153 times.
|
153 | reinterpret_cast<std::uintptr_t>(final_info.AllocationBase) != key.allocation_base) |
| 544 | { | ||
| 545 | 1 | return false; | |
| 546 | } | ||
| 547 |
4/4✓ Branch 55 → 56 taken 147 times.
✓ Branch 55 → 59 taken 6 times.
✓ Branch 57 → 58 taken 146 times.
✓ Branch 57 → 59 taken 1 time.
|
153 | return live_identity == key.identity && confirmed_identity == key.identity; |
| 548 | } | ||
| 549 | |||
| 550 | /// Returns whether two captured keys can describe one coherent generation. | ||
| 551 | 274 | [[nodiscard]] constexpr bool owner_keys_compatible(const OwnerKey &first, const OwnerKey &second) noexcept | |
| 552 | { | ||
| 553 |
7/8✓ Branch 3 → 4 taken 81 times.
✓ Branch 3 → 9 taken 193 times.
✓ Branch 5 → 6 taken 81 times.
✗ Branch 5 → 9 not taken.
✓ Branch 6 → 7 taken 78 times.
✓ Branch 6 → 9 taken 3 times.
✓ Branch 8 → 9 taken 76 times.
✓ Branch 8 → 10 taken 2 times.
|
352 | return !first.has_module() || !second.has_module() || first.allocation_base != second.allocation_base || |
| 554 | 352 | 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 | 405 | evidence_images_coherent(const ResolutionOwnerKeys &local, std::span<const OwnerKey> members) noexcept | |
| 565 | { | ||
| 566 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 405 times.
|
405 | if (local.overflowed) |
| 567 | { | ||
| 568 | ✗ | return false; | |
| 569 | } | ||
| 570 | |||
| 571 | 405 | std::array<OwnerKey, MAX_EVIDENCE_OWNER_KEYS + 1> local_keys{}; | |
| 572 | 405 | std::size_t local_count = local.evidence_count; | |
| 573 | 405 | std::copy_n(local.evidence.begin(), local_count, local_keys.begin()); | |
| 574 |
2/2✓ Branch 7 → 8 taken 126 times.
✓ Branch 7 → 10 taken 279 times.
|
405 | if (local.value.tracked()) |
| 575 | { | ||
| 576 | 126 | local_keys[local_count++] = local.value; | |
| 577 | } | ||
| 578 |
2/2✓ Branch 42 → 11 taken 367 times.
✓ Branch 42 → 43 taken 390 times.
|
757 | for (std::size_t i = 0; i < local_count; ++i) |
| 579 | { | ||
| 580 |
2/2✓ Branch 13 → 14 taken 13 times.
✓ Branch 13 → 15 taken 354 times.
|
367 | if (!owner_key_current(local_keys[i])) |
| 581 | { | ||
| 582 | 13 | return false; | |
| 583 | } | ||
| 584 |
2/2✓ Branch 22 → 16 taken 108 times.
✓ Branch 22 → 23 taken 354 times.
|
462 | for (std::size_t j = i + 1; j < local_count; ++j) |
| 585 | { | ||
| 586 |
1/2✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 108 times.
|
108 | if (!owner_keys_compatible(local_keys[i], local_keys[j])) |
| 587 | { | ||
| 588 | ✗ | return false; | |
| 589 | } | ||
| 590 | } | ||
| 591 |
2/2✓ Branch 40 → 25 taken 21 times.
✓ Branch 40 → 41 taken 352 times.
|
727 | for (const OwnerKey &member : members) |
| 592 | { | ||
| 593 |
2/2✓ Branch 29 → 30 taken 2 times.
✓ Branch 29 → 31 taken 19 times.
|
21 | if (!owner_keys_compatible(local_keys[i], member)) |
| 594 | { | ||
| 595 | 2 | return false; | |
| 596 | } | ||
| 597 | } | ||
| 598 | } | ||
| 599 |
2/2✓ Branch 59 → 44 taken 134 times.
✓ Branch 59 → 60 taken 389 times.
|
523 | for (std::size_t i = 0; i < members.size(); ++i) |
| 600 | { | ||
| 601 |
2/2✓ Branch 46 → 47 taken 1 time.
✓ Branch 46 → 48 taken 133 times.
|
134 | if (!owner_key_current(members[i])) |
| 602 | { | ||
| 603 | 1 | return false; | |
| 604 | } | ||
| 605 |
2/2✓ Branch 56 → 49 taken 145 times.
✓ Branch 56 → 57 taken 133 times.
|
278 | for (std::size_t j = i + 1; j < members.size(); ++j) |
| 606 | { | ||
| 607 |
1/2✗ Branch 52 → 53 not taken.
✓ Branch 52 → 54 taken 145 times.
|
145 | if (!owner_keys_compatible(members[i], members[j])) |
| 608 | { | ||
| 609 | ✗ | return false; | |
| 610 | } | ||
| 611 | } | ||
| 612 | } | ||
| 613 | 389 | return true; | |
| 614 | } | ||
| 615 | |||
| 616 | /// Returns whether @p domain carries an address rather than a scalar. | ||
| 617 | 576 | [[nodiscard]] constexpr bool is_address_domain(ResultDomain domain) noexcept | |
| 618 | { | ||
| 619 |
6/6✓ Branch 2 → 3 taken 456 times.
✓ Branch 2 → 5 taken 120 times.
✓ Branch 3 → 4 taken 334 times.
✓ Branch 3 → 5 taken 122 times.
✓ Branch 4 → 5 taken 10 times.
✓ Branch 4 → 6 taken 324 times.
|
576 | return domain == ResultDomain::CodeSite || domain == ResultDomain::DataAddress || |
| 620 | 576 | 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 | 267 | [[nodiscard]] OwnerKey capture_value_owner(const Anchor &anchor, std::int64_t value) noexcept | |
| 629 | { | ||
| 630 |
2/2✓ Branch 4 → 5 taken 127 times.
✓ Branch 4 → 8 taken 140 times.
|
267 | if (is_address_domain(declared_domain(anchor))) |
| 631 | { | ||
| 632 | 127 | 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 |
2/2✓ Branch 3 → 4 taken 145 times.
✓ Branch 3 → 5 taken 88 times.
|
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 |
2/2✓ Branch 6 → 3 taken 108 times.
✓ Branch 6 → 7 taken 125 times.
|
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 | 1217 | [[nodiscard]] constexpr bool evidence_module_is_scope(AnchorKind kind) noexcept | |
| 662 | { | ||
| 663 |
3/3✓ Branch 2 → 3 taken 638 times.
✓ Branch 2 → 4 taken 576 times.
✓ Branch 2 → 5 taken 3 times.
|
1217 | switch (kind) |
| 664 | { | ||
| 665 | 638 | case AnchorKind::VtableIdentity: | |
| 666 | case AnchorKind::RipGlobal: | ||
| 667 | case AnchorKind::CodeOperand: | ||
| 668 | case AnchorKind::StringXref: | ||
| 669 | 638 | 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 |
4/4✓ Branch 2 → 3 taken 10 times.
✓ Branch 2 → 4 taken 2 times.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 9 times.
|
12 | query.broad_match = query.broad_match || profile.default_broad_string_xref; |
| 697 | 12 | return query; | |
| 698 | } | ||
| 699 | |||
| 700 | namespace | ||
| 701 | { | ||
| 702 | 411 | ResolvedAnchor resolve_with_profile_impl( | |
| 703 | const Anchor &anchor, | ||
| 704 | const ScanProfile &profile, | ||
| 705 | Region scope, | ||
| 706 | PhysicalProvenance *provenance, | ||
| 707 | ResolutionOwnerKeys *owner_keys_out, | ||
| 708 | Region *winning_span_out | ||
| 709 | ) | ||
| 710 | { | ||
| 711 |
2/2✓ Branch 2 → 3 taken 132 times.
✓ Branch 2 → 5 taken 279 times.
|
411 | if (provenance != nullptr) |
| 712 | { | ||
| 713 | 132 | *provenance = PhysicalProvenance{}; | |
| 714 | } | ||
| 715 |
2/2✓ Branch 5 → 6 taken 132 times.
✓ Branch 5 → 7 taken 279 times.
|
411 | if (owner_keys_out != nullptr) |
| 716 | { | ||
| 717 | 132 | *owner_keys_out = ResolutionOwnerKeys{}; | |
| 718 | } | ||
| 719 |
2/2✓ Branch 7 → 8 taken 47 times.
✓ Branch 7 → 10 taken 364 times.
|
411 | if (winning_span_out != nullptr) |
| 720 | { | ||
| 721 | 47 | *winning_span_out = Region{}; | |
| 722 | } | ||
| 723 | 411 | ResolvedAnchor result{anchor.label, anchor.kind, AnchorStatus::Unresolved, 0}; | |
| 724 | 411 | PhysicalSource resolved_source = physical_source_of(anchor.kind); | |
| 725 | // Only a byte-signature rung witnesses a literal span, so this stays absent for every other backend. | ||
| 726 | 411 | scan::WinningEvidence resolved_evidence{}; | |
| 727 | 411 | Region resolved_winning_span{}; | |
| 728 | |||
| 729 | // A denied kind fails closed before any scan and is never silently replaced by another backend. | ||
| 730 |
2/2✓ Branch 12 → 13 taken 4 times.
✓ Branch 12 → 14 taken 407 times.
|
411 | if (profile.is_denied(anchor.kind)) |
| 731 | { | ||
| 732 | 4 | result.status = AnchorStatus::Failed; | |
| 733 | 4 | return result; | |
| 734 | } | ||
| 735 | |||
| 736 |
6/6✓ Branch 15 → 16 taken 214 times.
✓ Branch 15 → 19 taken 193 times.
✓ Branch 17 → 18 taken 2 times.
✓ Branch 17 → 19 taken 212 times.
✓ Branch 20 → 21 taken 2 times.
✓ Branch 20 → 22 taken 405 times.
|
407 | if (evidence_module_is_scope(anchor.kind) && !scope_is_single_allocation(scope)) |
| 737 | { | ||
| 738 | 2 | return failed_anchor_result(anchor); | |
| 739 | } | ||
| 740 | |||
| 741 | // Capture the owner before the walk. The witness then publishes the identity that produced the value, | ||
| 742 | // not the identity present at return. An ExportName captures its own effective module below. | ||
| 743 | 405 | ResolutionOwnerKeys owner_keys; | |
| 744 | 405 | owner_keys.requires_single_allocation = evidence_module_is_scope(anchor.kind); | |
| 745 |
2/2✓ Branch 24 → 25 taken 212 times.
✓ Branch 24 → 28 taken 193 times.
|
405 | if (evidence_module_is_scope(anchor.kind)) |
| 746 | { | ||
| 747 | 212 | owner_keys.add_evidence(capture_scope_owner_key(scope)); | |
| 748 | } | ||
| 749 | // A Quorum owns no direct evidence. It carries the key from each member that casts a vote. | ||
| 750 | 405 | std::vector<OwnerKey> member_keys; | |
| 751 | 405 | Region named_export_region{}; | |
| 752 | 405 | OwnerKey named_export_owner{}; | |
| 753 | |||
| 754 |
10/10✓ Branch 28 → 29 taken 5 times.
✓ Branch 28 → 41 taken 83 times.
✓ Branch 28 → 66 taken 112 times.
✓ Branch 28 → 93 taken 12 times.
✓ Branch 28 → 116 taken 38 times.
✓ Branch 28 → 141 taken 68 times.
✓ Branch 28 → 146 taken 6 times.
✓ Branch 28 → 147 taken 78 times.
✓ Branch 28 → 260 taken 2 times.
✓ Branch 28 → 261 taken 1 time.
|
405 | switch (anchor.kind) |
| 755 | { | ||
| 756 | 5 | case AnchorKind::VtableIdentity: | |
| 757 | { | ||
| 758 | const std::optional<Address> discovered = | ||
| 759 | 5 | DetourModKit::rtti::vtable_for_type(anchor.mangled, scope); | |
| 760 |
2/2✓ Branch 31 → 32 taken 4 times.
✓ Branch 31 → 38 taken 1 time.
|
5 | if (discovered) |
| 761 | { | ||
| 762 | 4 | owner_keys.value = capture_value_owner(anchor, static_cast<std::int64_t>(discovered->raw())); | |
| 763 | 4 | commit_resolved(anchor, result, static_cast<std::int64_t>(discovered->raw())); | |
| 764 | } | ||
| 765 | else | ||
| 766 | { | ||
| 767 | 1 | result.status = AnchorStatus::Failed; | |
| 768 | } | ||
| 769 | 5 | break; | |
| 770 | } | ||
| 771 | 83 | case AnchorKind::RipGlobal: | |
| 772 | { | ||
| 773 |
3/4✓ Branch 41 → 42 taken 20 times.
✓ Branch 41 → 44 taken 63 times.
✗ Branch 42 → 43 not taken.
✓ Branch 42 → 44 taken 20 times.
|
83 | if (anchor.pages != scan::Pages::Readable && anchor.pages != scan::Pages::Executable) |
| 774 | { | ||
| 775 | ✗ | return failed_anchor_result(anchor); | |
| 776 | } | ||
| 777 | // The cascade selects Direct or RIP-relative per candidate. It also applies the profile order. | ||
| 778 | // Pages defaults to Readable. If every rung anchors on an image instruction, select Executable. A | ||
| 779 | // data-page byte twin then cannot alias the site. | ||
| 780 | 83 | const scan::ScanRequest request{ | |
| 781 | .ladder = anchor.site, | ||
| 782 | .label = anchor.label, | ||
| 783 | .scope = scope, | ||
| 784 | 83 | .order = profile.candidate_order, | |
| 785 | 83 | .pages = anchor.pages, | |
| 786 | 83 | }; | |
| 787 |
1/2✓ Branch 45 → 46 taken 83 times.
✗ Branch 45 → 302 not taken.
|
83 | const Result<detail::ResolvedScanHit> discovered = detail::resolve_scan_with_provenance(request); |
| 788 |
2/2✓ Branch 47 → 48 taken 73 times.
✓ Branch 47 → 61 taken 10 times.
|
83 | if (discovered) |
| 789 | { | ||
| 790 | 73 | owner_keys.value = | |
| 791 | 73 | capture_value_owner(anchor, static_cast<std::int64_t>(discovered->hit.address.raw())); | |
| 792 | 73 | resolved_source = physical_source_of(discovered->hit.winning_mode); | |
| 793 | 73 | resolved_evidence = discovered->hit.evidence; | |
| 794 | 73 | resolved_winning_span = discovered->match_span; | |
| 795 |
2/2✓ Branch 55 → 56 taken 21 times.
✓ Branch 55 → 58 taken 52 times.
|
73 | if (provenance != nullptr) |
| 796 | { | ||
| 797 | 21 | provenance->add(discovered->physical_source); | |
| 798 | } | ||
| 799 | 73 | commit_resolved(anchor, result, static_cast<std::int64_t>(discovered->hit.address.raw())); | |
| 800 | } | ||
| 801 | else | ||
| 802 | { | ||
| 803 | 10 | result.status = AnchorStatus::Failed; | |
| 804 | } | ||
| 805 | 83 | break; | |
| 806 | 83 | } | |
| 807 | 112 | case AnchorKind::CodeOperand: | |
| 808 | { | ||
| 809 | 112 | if (!valid_operand_kind(anchor.operand_kind) || | |
| 810 |
6/6✓ Branch 67 → 68 taken 111 times.
✓ Branch 67 → 72 taken 1 time.
✓ Branch 69 → 70 taken 109 times.
✓ Branch 69 → 72 taken 2 times.
✓ Branch 74 → 75 taken 4 times.
✓ Branch 74 → 76 taken 108 times.
|
221 | !detail::valid_code_constant_byte_width(anchor.byte_width) || |
| 811 |
2/2✓ Branch 71 → 72 taken 1 time.
✓ Branch 71 → 73 taken 108 times.
|
109 | !valid_candidate_order(profile.candidate_order)) |
| 812 | { | ||
| 813 | 4 | result.status = AnchorStatus::Failed; | |
| 814 | 4 | break; | |
| 815 | } | ||
| 816 | // read_code_constant has no order parameter. Create a local ladder in profile order before the | ||
| 817 | // call. | ||
| 818 | 108 | std::vector<scan::Candidate> ordered_site; | |
| 819 | 108 | const scan::CodeConstant code_constant{ | |
| 820 |
1/2✓ Branch 76 → 77 taken 108 times.
✗ Branch 76 → 303 not taken.
|
108 | .site = profiled_candidates(profile, anchor.site, ordered_site), |
| 821 | 108 | .kind = anchor.operand_kind, | |
| 822 | 108 | .operand_index = anchor.operand_index, | |
| 823 | 108 | .byte_width = anchor.byte_width, | |
| 824 | 108 | }; | |
| 825 | const Result<detail::ResolvedCodeConstant> discovered = | ||
| 826 |
1/2✓ Branch 77 → 78 taken 108 times.
✗ Branch 77 → 303 not taken.
|
108 | detail::read_code_constant_with_provenance(code_constant, scope); |
| 827 |
2/2✓ Branch 79 → 80 taken 106 times.
✓ Branch 79 → 89 taken 2 times.
|
108 | if (discovered) |
| 828 | { | ||
| 829 | 106 | owner_keys.value = capture_value_owner(anchor, discovered->value); | |
| 830 |
2/2✓ Branch 82 → 83 taken 71 times.
✓ Branch 82 → 87 taken 35 times.
|
106 | if (provenance != nullptr) |
| 831 | { | ||
| 832 | 71 | provenance->add(discovered->instruction_span); | |
| 833 | 71 | provenance->add(discovered->physical_source); | |
| 834 | } | ||
| 835 | 106 | commit_resolved(anchor, result, discovered->value); | |
| 836 | } | ||
| 837 | else | ||
| 838 | { | ||
| 839 | 2 | result.status = AnchorStatus::Failed; | |
| 840 | } | ||
| 841 | 108 | break; | |
| 842 | 108 | } | |
| 843 | 12 | case AnchorKind::StringXref: | |
| 844 | { | ||
| 845 |
6/6✓ Branch 94 → 95 taken 11 times.
✓ Branch 94 → 97 taken 1 time.
✓ Branch 96 → 97 taken 1 time.
✓ Branch 96 → 98 taken 10 times.
✓ Branch 99 → 100 taken 2 times.
✓ Branch 99 → 101 taken 10 times.
|
12 | if (!valid_string_encoding(anchor.xref_encoding) || !valid_xref_return(anchor.xref_return)) |
| 846 | { | ||
| 847 | 2 | result.status = AnchorStatus::Failed; | |
| 848 | 2 | break; | |
| 849 | } | ||
| 850 | // Anchor on an immutable string literal, then resolve its reference site. An absent literal, | ||
| 851 | // duplicate literal, or literal without a reference fails closed. | ||
| 852 | 10 | scan::StringRefQuery query{}; | |
| 853 | 10 | query.text = anchor.xref_text; | |
| 854 | 10 | query.encoding = anchor.xref_encoding; | |
| 855 | 10 | query.require_terminator = anchor.xref_require_terminator; | |
| 856 | 10 | query.return_mode = anchor.xref_return; | |
| 857 | 10 | query.broad_match = anchor.xref_broad_match; | |
| 858 | 10 | query = apply_profile(profile, query); | |
| 859 | 10 | Region discovered_span{}; | |
| 860 | const Result<Address> discovered = | ||
| 861 |
1/2✓ Branch 102 → 103 taken 10 times.
✗ Branch 102 → 306 not taken.
|
10 | detail::find_string_xref_with_provenance(query, scope, discovered_span); |
| 862 |
2/2✓ Branch 104 → 105 taken 6 times.
✓ Branch 104 → 113 taken 4 times.
|
10 | if (discovered) |
| 863 | { | ||
| 864 | 6 | owner_keys.value = capture_value_owner(anchor, static_cast<std::int64_t>(discovered->raw())); | |
| 865 |
2/2✓ Branch 108 → 109 taken 4 times.
✓ Branch 108 → 110 taken 2 times.
|
6 | if (provenance != nullptr) |
| 866 | { | ||
| 867 | 4 | provenance->add(discovered_span); | |
| 868 | } | ||
| 869 | 6 | commit_resolved(anchor, result, static_cast<std::int64_t>(discovered->raw())); | |
| 870 | } | ||
| 871 | else | ||
| 872 | { | ||
| 873 | 4 | result.status = AnchorStatus::Failed; | |
| 874 | } | ||
| 875 | 10 | break; | |
| 876 | } | ||
| 877 | 38 | case AnchorKind::ExportName: | |
| 878 | { | ||
| 879 | // Resolve a named export through the module EAT. An explicit export_module uses module_named. An | ||
| 880 | // empty export_module uses the passed scope. An unloaded module, absent or forwarded export, or | ||
| 881 | // corrupt export directory fails closed. | ||
| 882 | const Region module = | ||
| 883 |
2/2✓ Branch 117 → 118 taken 6 times.
✓ Branch 117 → 119 taken 32 times.
|
38 | anchor.export_module.empty() ? scope : Region::module_named(anchor.export_module); |
| 884 | 38 | named_export_region = module; | |
| 885 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 886 |
2/2✓ Branch 120 → 121 taken 1 time.
✓ Branch 120 → 122 taken 37 times.
|
38 | if (auto *const hook = DetourModKit::detail::g_anchor_after_named_export_lookup_test_hook) |
| 887 | { | ||
| 888 | 1 | hook(); | |
| 889 | } | ||
| 890 | #endif | ||
| 891 | 38 | named_export_owner = capture_owner_key(module); | |
| 892 | 38 | owner_keys.add_evidence(named_export_owner); | |
| 893 |
2/2✓ Branch 125 → 126 taken 1 time.
✓ Branch 125 → 127 taken 37 times.
|
38 | if (!named_export_owner_current(anchor, named_export_region, named_export_owner)) |
| 894 | { | ||
| 895 | 1 | result.status = AnchorStatus::Failed; | |
| 896 | 1 | break; | |
| 897 | } | ||
| 898 | 37 | DetourModKit::detail::ExportResolution discovered_export; | |
| 899 | 37 | const Result<Address> discovered = DetourModKit::detail::resolve_export_with_provenance( | |
| 900 | anchor.export_name, | ||
| 901 | module, | ||
| 902 | discovered_export | ||
| 903 | ); | ||
| 904 |
2/2✓ Branch 129 → 130 taken 33 times.
✓ Branch 129 → 138 taken 4 times.
|
37 | if (discovered) |
| 905 | { | ||
| 906 | 33 | owner_keys.value = capture_value_owner(anchor, static_cast<std::int64_t>(discovered->raw())); | |
| 907 |
2/2✓ Branch 133 → 134 taken 10 times.
✓ Branch 133 → 135 taken 23 times.
|
33 | if (provenance != nullptr) |
| 908 | { | ||
| 909 | 10 | provenance->add(discovered_export); | |
| 910 | } | ||
| 911 | 33 | commit_resolved(anchor, result, static_cast<std::int64_t>(discovered->raw())); | |
| 912 | } | ||
| 913 | else | ||
| 914 | { | ||
| 915 | 4 | result.status = AnchorStatus::Failed; | |
| 916 | } | ||
| 917 | 37 | break; | |
| 918 | } | ||
| 919 | 68 | case AnchorKind::Manual: | |
| 920 | // A pinned literal always "resolves". A report flags its kind as at risk. The default path skips | ||
| 921 | // the validator. validate_manual selects the fail-closed validator path. | ||
| 922 |
2/2✓ Branch 141 → 142 taken 14 times.
✓ Branch 141 → 144 taken 54 times.
|
68 | if (anchor.validate_manual) |
| 923 | { | ||
| 924 | 14 | owner_keys.value = capture_value_owner(anchor, anchor.manual_value); | |
| 925 | 14 | commit_resolved(anchor, result, anchor.manual_value); | |
| 926 | } | ||
| 927 | else | ||
| 928 | { | ||
| 929 | 54 | result.value = anchor.manual_value; | |
| 930 | 54 | result.status = AnchorStatus::Resolved; | |
| 931 | } | ||
| 932 | 68 | break; | |
| 933 | 6 | case AnchorKind::CallArgHome: | |
| 934 | // This kind is reserved for a future prologue-dataflow backend. No resolver exists yet. | ||
| 935 | 6 | result.status = AnchorStatus::Unsupported; | |
| 936 | 6 | break; | |
| 937 | 78 | case AnchorKind::Quorum: | |
| 938 | { | ||
| 939 | // An N-of-M vote survives a patch that breaks some signals if N still agree. Fail closed on a | ||
| 940 | // malformed declaration. | ||
| 941 |
2/2✓ Branch 148 → 149 taken 1 time.
✓ Branch 148 → 150 taken 77 times.
|
78 | if (!valid_quorum_match(anchor.quorum_match)) |
| 942 | { | ||
| 943 | 1 | result.status = AnchorStatus::Failed; | |
| 944 | 1 | break; | |
| 945 | } | ||
| 946 | 77 | const std::span<const Anchor *const> members = anchor.quorum_members; | |
| 947 | |||
| 948 | // A quorum needs at least two members. A null or nested-Quorum member is malformed. This rule | ||
| 949 | // limits recursion to one level. | ||
| 950 |
2/2✓ Branch 151 → 152 taken 2 times.
✓ Branch 151 → 153 taken 75 times.
|
77 | if (members.size() < 2) |
| 951 | { | ||
| 952 | 2 | result.status = AnchorStatus::Failed; | |
| 953 | 2 | break; | |
| 954 | } | ||
| 955 |
1/2✓ Branch 155 → 156 taken 75 times.
✗ Branch 155 → 319 not taken.
|
75 | const bool malformed_member = std::any_of( |
| 956 | members.begin(), | ||
| 957 | members.end(), | ||
| 958 | 171 | [](const Anchor *member) noexcept | |
| 959 |
4/4✓ Branch 2 → 3 taken 170 times.
✓ Branch 2 → 4 taken 1 time.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 169 times.
|
171 | { return member == nullptr || member->kind == AnchorKind::Quorum; } |
| 960 | ); | ||
| 961 |
2/2✓ Branch 156 → 157 taken 2 times.
✓ Branch 156 → 158 taken 73 times.
|
75 | if (malformed_member) |
| 962 | { | ||
| 963 | 2 | result.status = AnchorStatus::Failed; | |
| 964 | 2 | break; | |
| 965 | } | ||
| 966 | |||
| 967 | // An effective N of zero means unanimous. An explicit N below two or above the member count fails | ||
| 968 | // closed rather than silently degrade to a single signal. | ||
| 969 | const std::size_t threshold = | ||
| 970 |
2/2✓ Branch 158 → 159 taken 57 times.
✓ Branch 158 → 160 taken 16 times.
|
73 | (anchor.quorum_threshold == 0) ? members.size() : anchor.quorum_threshold; |
| 971 |
6/6✓ Branch 161 → 162 taken 72 times.
✓ Branch 161 → 164 taken 1 time.
✓ Branch 163 → 164 taken 1 time.
✓ Branch 163 → 165 taken 71 times.
✓ Branch 166 → 167 taken 2 times.
✓ Branch 166 → 168 taken 71 times.
|
73 | if (threshold < 2 || threshold > members.size()) |
| 972 | { | ||
| 973 | 2 | result.status = AnchorStatus::Failed; | |
| 974 | 2 | break; | |
| 975 | } | ||
| 976 | |||
| 977 | // Independence has two sources. Check declaration evidence here before the recursive resolves. | ||
| 978 | // Check evidence from resolved sites afterward. | ||
| 979 |
3/4✓ Branch 168 → 169 taken 71 times.
✗ Branch 168 → 319 not taken.
✓ Branch 169 → 170 taken 15 times.
✓ Branch 169 → 171 taken 56 times.
|
71 | if (!internal::quorum_members_pairwise_independent(members)) |
| 980 | { | ||
| 981 | 15 | result.status = AnchorStatus::QuorumNotIndependent; | |
| 982 | 15 | break; | |
| 983 | } | ||
| 984 | |||
| 985 | // Resolve each member with the same profile so denied kinds and broad defaults propagate. A failed | ||
| 986 | // member contributes no vote. This behavior gives N-of-M its fault tolerance. | ||
| 987 | 56 | std::vector<std::int64_t> votes; | |
| 988 |
1/2✓ Branch 172 → 173 taken 56 times.
✗ Branch 172 → 317 not taken.
|
56 | votes.reserve(members.size()); |
| 989 | 56 | std::vector<PhysicalProvenance> vote_provenance; | |
| 990 |
1/2✓ Branch 174 → 175 taken 56 times.
✗ Branch 174 → 315 not taken.
|
56 | vote_provenance.reserve(members.size()); |
| 991 | 56 | bool physical_dependency = false; | |
| 992 |
1/2✓ Branch 176 → 177 taken 56 times.
✗ Branch 176 → 315 not taken.
|
56 | member_keys.reserve(members.size() * 2); |
| 993 |
2/2✓ Branch 207 → 179 taken 132 times.
✓ Branch 207 → 208 taken 56 times.
|
244 | for (const Anchor *member : members) |
| 994 | { | ||
| 995 | 132 | PhysicalProvenance member_provenance; | |
| 996 | 132 | ResolutionOwnerKeys member_owner_keys; | |
| 997 |
1/2✓ Branch 181 → 182 taken 132 times.
✗ Branch 181 → 307 not taken.
|
132 | const ResolvedAnchor resolved_member = resolve_with_profile_impl( |
| 998 | *member, | ||
| 999 | profile, | ||
| 1000 | scope, | ||
| 1001 | &member_provenance, | ||
| 1002 | &member_owner_keys, | ||
| 1003 | nullptr | ||
| 1004 | ); | ||
| 1005 |
2/2✓ Branch 182 → 183 taken 125 times.
✓ Branch 182 → 198 taken 7 times.
|
132 | if (resolved_member.status == AnchorStatus::Resolved) |
| 1006 | { | ||
| 1007 | 125 | physical_dependency = | |
| 1008 |
5/6✓ Branch 183 → 184 taken 123 times.
✓ Branch 183 → 188 taken 2 times.
✓ Branch 186 → 187 taken 123 times.
✗ Branch 186 → 307 not taken.
✓ Branch 187 → 188 taken 14 times.
✓ Branch 187 → 189 taken 109 times.
|
125 | physical_dependency || std::any_of( |
| 1009 | vote_provenance.begin(), | ||
| 1010 | vote_provenance.end(), | ||
| 1011 | 86 | [&](const PhysicalProvenance &existing) noexcept | |
| 1012 | 86 | { return member_provenance.intersects(existing); } | |
| 1013 | ); | ||
| 1014 |
1/2✓ Branch 190 → 191 taken 125 times.
✗ Branch 190 → 307 not taken.
|
125 | votes.push_back(resolved_member.value); |
| 1015 |
1/2✓ Branch 191 → 192 taken 125 times.
✗ Branch 191 → 307 not taken.
|
125 | vote_provenance.push_back(member_provenance); |
| 1016 | // Only a member that casts a vote supplies evidence for corroboration. | ||
| 1017 |
1/2✓ Branch 192 → 193 taken 125 times.
✗ Branch 192 → 307 not taken.
|
125 | append_owner_keys(member_keys, member_owner_keys); |
| 1018 | 125 | owner_keys.requires_single_allocation = | |
| 1019 |
4/4✓ Branch 193 → 194 taken 70 times.
✓ Branch 193 → 195 taken 55 times.
✓ Branch 194 → 195 taken 46 times.
✓ Branch 194 → 196 taken 24 times.
|
125 | owner_keys.requires_single_allocation || member_owner_keys.requires_single_allocation; |
| 1020 | } | ||
| 1021 | } | ||
| 1022 |
2/2✓ Branch 208 → 209 taken 14 times.
✓ Branch 208 → 210 taken 42 times.
|
56 | if (physical_dependency) |
| 1023 | { | ||
| 1024 | 14 | result.status = AnchorStatus::QuorumNotIndependent; | |
| 1025 | 14 | break; | |
| 1026 | } | ||
| 1027 | |||
| 1028 | // Collect every distinct vote value that anchors a cluster of at least N votes. Declaration order | ||
| 1029 | // never selects among these values. | ||
| 1030 | 42 | std::vector<std::int64_t> qualifying; | |
| 1031 |
2/2✓ Branch 239 → 212 taken 95 times.
✓ Branch 239 → 240 taken 42 times.
|
179 | for (const std::int64_t center : votes) |
| 1032 | { | ||
| 1033 |
3/4✓ Branch 217 → 218 taken 95 times.
✗ Branch 217 → 309 not taken.
✓ Branch 224 → 225 taken 25 times.
✓ Branch 224 → 226 taken 70 times.
|
190 | if (std::find(qualifying.begin(), qualifying.end(), center) != qualifying.end()) |
| 1034 | { | ||
| 1035 | 25 | continue; | |
| 1036 | } | ||
| 1037 |
2/2✓ Branch 228 → 229 taken 44 times.
✓ Branch 228 → 230 taken 26 times.
|
70 | if (votes_agreeing_with(center, votes, anchor.quorum_match, anchor.quorum_tolerance) >= |
| 1038 | threshold) | ||
| 1039 | { | ||
| 1040 |
1/2✓ Branch 229 → 230 taken 44 times.
✗ Branch 229 → 311 not taken.
|
44 | qualifying.push_back(center); |
| 1041 | } | ||
| 1042 | } | ||
| 1043 |
2/2✓ Branch 241 → 242 taken 8 times.
✓ Branch 241 → 243 taken 34 times.
|
42 | if (qualifying.empty()) |
| 1044 | { | ||
| 1045 | 8 | result.status = AnchorStatus::Failed; | |
| 1046 | 8 | break; | |
| 1047 | } | ||
| 1048 | // If two qualified centers disagree, separate clusters cleared N and no single value has | ||
| 1049 | // corroboration. This also catches the non-transitive WithinTolerance overlap at 0/4/8 with 4. | ||
| 1050 |
1/2✓ Branch 245 → 246 taken 34 times.
✗ Branch 245 → 313 not taken.
|
34 | const bool ambiguous = std::any_of( |
| 1051 | qualifying.begin(), | ||
| 1052 | qualifying.end(), | ||
| 1053 | 40 | [&](std::int64_t first) noexcept | |
| 1054 | { | ||
| 1055 | 40 | return std::any_of( | |
| 1056 | qualifying.begin(), | ||
| 1057 | qualifying.end(), | ||
| 1058 | 56 | [&](std::int64_t second) noexcept | |
| 1059 | { | ||
| 1060 | 56 | return !quorum_values_agree( | |
| 1061 | first, | ||
| 1062 | second, | ||
| 1063 | 56 | anchor.quorum_match, | |
| 1064 | 56 | anchor.quorum_tolerance | |
| 1065 | 56 | ); | |
| 1066 | } | ||
| 1067 | 40 | ); | |
| 1068 | } | ||
| 1069 | ); | ||
| 1070 |
2/2✓ Branch 246 → 247 taken 3 times.
✓ Branch 246 → 248 taken 31 times.
|
34 | if (ambiguous) |
| 1071 | { | ||
| 1072 | 3 | result.status = AnchorStatus::QuorumAmbiguous; | |
| 1073 | 3 | break; | |
| 1074 | } | ||
| 1075 | // For one coherent cluster, commit its canonical center through the shared path. The center is the | ||
| 1076 | // smallest qualified value. The shared path invokes the Quorum validator. | ||
| 1077 |
1/2✓ Branch 250 → 251 taken 31 times.
✗ Branch 250 → 312 not taken.
|
31 | const std::int64_t accepted = *std::min_element(qualifying.begin(), qualifying.end()); |
| 1078 | 31 | owner_keys.value = capture_value_owner(anchor, accepted); | |
| 1079 | 31 | commit_resolved(anchor, result, accepted); | |
| 1080 | 31 | break; | |
| 1081 | 56 | } | |
| 1082 | 2 | case AnchorKind::Unset: | |
| 1083 | // An Unset kind on a default-constructed anchor fails closed rather than invent a value. | ||
| 1084 | 2 | result.status = AnchorStatus::Failed; | |
| 1085 | 2 | break; | |
| 1086 | } | ||
| 1087 | |||
| 1088 | // An out-of-range AnchorKind reaches here with the initial non-terminal Unresolved. Normalize it to | ||
| 1089 | // Failed so a resolved report never leaves an entry Unresolved. | ||
| 1090 |
2/2✓ Branch 261 → 262 taken 1 time.
✓ Branch 261 → 263 taken 404 times.
|
405 | if (result.status == AnchorStatus::Unresolved) |
| 1091 | { | ||
| 1092 | 1 | result.status = AnchorStatus::Failed; | |
| 1093 | } | ||
| 1094 | // Stamp the typed domain only on a committed value: the single choke point every resolved path | ||
| 1095 | // reaches. A failed entry keeps the fail-closed ResultDomain::Unknown default. | ||
| 1096 |
2/2✓ Branch 263 → 264 taken 309 times.
✓ Branch 263 → 277 taken 96 times.
|
405 | if (result.status == AnchorStatus::Resolved) |
| 1097 | { | ||
| 1098 | 309 | result.domain = declared_domain(anchor); | |
| 1099 | // A CodeSite claim is trustworthy only on an executable page. A code-site kind at a non-executable | ||
| 1100 | // data address downgrades to DataAddress. The downgrade denies a mid-hook on a data export. | ||
| 1101 |
4/4✓ Branch 265 → 266 taken 63 times.
✓ Branch 265 → 269 taken 246 times.
✓ Branch 270 → 271 taken 6 times.
✓ Branch 270 → 272 taken 303 times.
|
372 | if (result.domain == ResultDomain::CodeSite && |
| 1102 |
2/2✓ Branch 267 → 268 taken 6 times.
✓ Branch 267 → 269 taken 57 times.
|
63 | !DetourModKit::detail::is_executable_address(static_cast<std::uintptr_t>(result.value))) |
| 1103 | { | ||
| 1104 | 6 | result.domain = ResultDomain::DataAddress; | |
| 1105 | } | ||
| 1106 | // A truncated or unauthoritative sweep cannot reach Resolved. Address-domain values copy the owner | ||
| 1107 | // identity captured before validation. Scalars have no owner image. | ||
| 1108 | 309 | result.witness.completeness = WitnessCompleteness::Complete; | |
| 1109 | 309 | result.witness.source = resolved_source; | |
| 1110 | 309 | result.witness.evidence = resolved_evidence; | |
| 1111 |
2/2✓ Branch 272 → 273 taken 103 times.
✓ Branch 272 → 274 taken 206 times.
|
309 | if (anchor.kind == AnchorKind::CodeOperand) |
| 1112 | { | ||
| 1113 | 103 | result.witness.operand_kind = anchor.operand_kind; | |
| 1114 | } | ||
| 1115 |
2/2✓ Branch 275 → 276 taken 125 times.
✓ Branch 275 → 277 taken 184 times.
|
309 | if (is_address_domain(result.domain)) |
| 1116 | { | ||
| 1117 | 125 | result.witness.image = owner_keys.value.identity; | |
| 1118 | } | ||
| 1119 | } | ||
| 1120 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 1121 |
2/2✓ Branch 277 → 278 taken 309 times.
✓ Branch 277 → 280 taken 96 times.
|
405 | if (result.status == AnchorStatus::Resolved) |
| 1122 | { | ||
| 1123 |
2/2✓ Branch 278 → 279 taken 2 times.
✓ Branch 278 → 280 taken 307 times.
|
309 | if (auto *const hook = DetourModKit::detail::g_anchor_after_witness_test_hook) |
| 1124 | { | ||
| 1125 | 2 | hook(); | |
| 1126 | } | ||
| 1127 | } | ||
| 1128 | #endif | ||
| 1129 | |||
| 1130 | // Re-check after every validator, domain probe, and witness write. Temporal drift overrides quorum | ||
| 1131 | // diagnostics: mixed generations are a failed trust transaction. | ||
| 1132 |
2/2✓ Branch 282 → 283 taken 16 times.
✓ Branch 282 → 284 taken 389 times.
|
405 | if (!evidence_images_coherent(owner_keys, member_keys)) |
| 1133 | { | ||
| 1134 | 16 | return failed_anchor_result(anchor); | |
| 1135 | } | ||
| 1136 |
6/6✓ Branch 284 → 285 taken 248 times.
✓ Branch 284 → 288 taken 141 times.
✓ Branch 286 → 287 taken 2 times.
✓ Branch 286 → 288 taken 246 times.
✓ Branch 289 → 290 taken 2 times.
✓ Branch 289 → 291 taken 387 times.
|
389 | if (owner_keys.requires_single_allocation && !scope_is_single_allocation(scope)) |
| 1137 | { | ||
| 1138 | 2 | return failed_anchor_result(anchor); | |
| 1139 | } | ||
| 1140 |
2/2✓ Branch 292 → 293 taken 1 time.
✓ Branch 292 → 294 taken 386 times.
|
387 | if (!named_export_owner_current(anchor, named_export_region, named_export_owner)) |
| 1141 | { | ||
| 1142 | 1 | return failed_anchor_result(anchor); | |
| 1143 | } | ||
| 1144 |
2/2✓ Branch 294 → 295 taken 131 times.
✓ Branch 294 → 296 taken 255 times.
|
386 | if (owner_keys_out != nullptr) |
| 1145 | { | ||
| 1146 | 131 | *owner_keys_out = owner_keys; | |
| 1147 | } | ||
| 1148 |
2/2✓ Branch 296 → 297 taken 47 times.
✓ Branch 296 → 298 taken 339 times.
|
386 | if (winning_span_out != nullptr) |
| 1149 | { | ||
| 1150 | 47 | *winning_span_out = resolved_winning_span; | |
| 1151 | } | ||
| 1152 | 386 | return result; | |
| 1153 | 405 | } | |
| 1154 | } // namespace | ||
| 1155 | |||
| 1156 | 232 | ResolvedAnchor resolve_with_profile(const Anchor &anchor, const ScanProfile &profile, Region scope) | |
| 1157 | { | ||
| 1158 | 232 | return resolve_with_profile_impl(anchor, profile, scope, nullptr, nullptr, nullptr); | |
| 1159 | } | ||
| 1160 | |||
| 1161 | namespace internal | ||
| 1162 | { | ||
| 1163 | 47 | ResolvedAnchor resolve_with_winning_span(const Anchor &anchor, Region scope, Region &winning_span) | |
| 1164 | { | ||
| 1165 |
1/2✓ Branch 2 → 3 taken 47 times.
✗ Branch 2 → 6 not taken.
|
47 | return resolve_with_profile_impl(anchor, ScanProfile{}, scope, nullptr, nullptr, &winning_span); |
| 1166 | } | ||
| 1167 | } // namespace internal | ||
| 1168 | |||
| 1169 | 216 | ResolvedAnchor resolve(const Anchor &anchor, Region scope) | |
| 1170 | { | ||
| 1171 | // An empty profile denies nothing and widens nothing, so this is exactly the un-profiled resolution. | ||
| 1172 |
1/2✓ Branch 2 → 3 taken 216 times.
✗ Branch 2 → 6 not taken.
|
216 | return resolve_with_profile(anchor, ScanProfile{}, scope); |
| 1173 | } | ||
| 1174 | |||
| 1175 | 5 | std::size_t resolve_all(std::span<const Anchor> anchors, std::span<ResolvedAnchor> out, Region scope) | |
| 1176 | { | ||
| 1177 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 5 times.
|
5 | const std::size_t count = (anchors.size() < out.size()) ? anchors.size() : out.size(); |
| 1178 |
2/2✓ Branch 12 → 8 taken 10 times.
✓ Branch 12 → 13 taken 5 times.
|
15 | for (std::size_t i = 0; i < count; ++i) |
| 1179 | { | ||
| 1180 | 10 | out[i] = resolve(anchors[i], scope); | |
| 1181 | } | ||
| 1182 | 5 | return count; | |
| 1183 | } | ||
| 1184 | |||
| 1185 | 1 | std::size_t resolve_all_parallel( | |
| 1186 | std::span<const Anchor> anchors, | ||
| 1187 | std::span<ResolvedAnchor> out, | ||
| 1188 | Region scope, | ||
| 1189 | std::size_t max_workers | ||
| 1190 | ) | ||
| 1191 | { | ||
| 1192 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 1 time.
|
1 | const std::size_t count = (anchors.size() < out.size()) ? anchors.size() : out.size(); |
| 1193 | const std::vector<ResolvedAnchor> results = DetourModKit::detail::run_fork_join<Anchor, ResolvedAnchor>( | ||
| 1194 | anchors.first(count), | ||
| 1195 | max_workers, | ||
| 1196 | 3 | [scope](const Anchor &anchor) -> ResolvedAnchor { return resolve(anchor, scope); }, | |
| 1197 | 3 | [](const Anchor &anchor) noexcept -> ResolvedAnchor { return failed_anchor_result(anchor); } | |
| 1198 |
1/2✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 17 not taken.
|
1 | ); |
| 1199 | |||
| 1200 |
2/2✓ Branch 13 → 10 taken 3 times.
✓ Branch 13 → 14 taken 1 time.
|
4 | for (std::size_t i = 0; i < count; ++i) |
| 1201 | { | ||
| 1202 | 3 | out[i] = results[i]; | |
| 1203 | } | ||
| 1204 | 1 | return count; | |
| 1205 | 1 | } | |
| 1206 | |||
| 1207 | 2 | std::size_t resolve_all_with_profile( | |
| 1208 | std::span<const Anchor> anchors, | ||
| 1209 | std::span<ResolvedAnchor> out, | ||
| 1210 | const ScanProfile &profile, | ||
| 1211 | Region scope | ||
| 1212 | ) | ||
| 1213 | { | ||
| 1214 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 2 times.
|
2 | const std::size_t count = (anchors.size() < out.size()) ? anchors.size() : out.size(); |
| 1215 |
2/2✓ Branch 12 → 8 taken 5 times.
✓ Branch 12 → 13 taken 2 times.
|
7 | for (std::size_t i = 0; i < count; ++i) |
| 1216 | { | ||
| 1217 | 5 | out[i] = resolve_with_profile(anchors[i], profile, scope); | |
| 1218 | } | ||
| 1219 | 2 | return count; | |
| 1220 | } | ||
| 1221 | |||
| 1222 | 1 | std::size_t resolve_all_with_profile_parallel( | |
| 1223 | std::span<const Anchor> anchors, | ||
| 1224 | std::span<ResolvedAnchor> out, | ||
| 1225 | const ScanProfile &profile, | ||
| 1226 | Region scope, | ||
| 1227 | std::size_t max_workers | ||
| 1228 | ) | ||
| 1229 | { | ||
| 1230 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 1 time.
|
1 | const std::size_t count = (anchors.size() < out.size()) ? anchors.size() : out.size(); |
| 1231 | const std::vector<ResolvedAnchor> results = DetourModKit::detail::run_fork_join<Anchor, ResolvedAnchor>( | ||
| 1232 | anchors.first(count), | ||
| 1233 | max_workers, | ||
| 1234 | 3 | [&profile, scope](const Anchor &anchor) -> ResolvedAnchor | |
| 1235 | 3 | { return resolve_with_profile(anchor, profile, scope); }, | |
| 1236 | 3 | [](const Anchor &anchor) noexcept -> ResolvedAnchor { return failed_anchor_result(anchor); } | |
| 1237 |
1/2✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 17 not taken.
|
1 | ); |
| 1238 | |||
| 1239 |
2/2✓ Branch 13 → 10 taken 3 times.
✓ Branch 13 → 14 taken 1 time.
|
4 | for (std::size_t i = 0; i < count; ++i) |
| 1240 | { | ||
| 1241 | 3 | out[i] = results[i]; | |
| 1242 | } | ||
| 1243 | 1 | return count; | |
| 1244 | 1 | } | |
| 1245 | |||
| 1246 | 781 | ResultDomain declared_domain(const Anchor &anchor) noexcept | |
| 1247 | { | ||
| 1248 |
9/9✓ Branch 2 → 3 taken 14 times.
✓ Branch 2 → 4 taken 313 times.
✓ Branch 2 → 13 taken 107 times.
✓ Branch 2 → 14 taken 23 times.
✓ Branch 2 → 25 taken 83 times.
✓ Branch 2 → 26 taken 173 times.
✓ Branch 2 → 32 taken 65 times.
✓ Branch 2 → 61 taken 2 times.
✓ Branch 2 → 62 taken 1 time.
|
781 | switch (anchor.kind) |
| 1249 | { | ||
| 1250 | 14 | case AnchorKind::VtableIdentity: | |
| 1251 | 14 | return ResultDomain::VtableAddress; | |
| 1252 | 313 | case AnchorKind::CodeOperand: | |
| 1253 |
4/4✓ Branch 5 → 6 taken 312 times.
✓ Branch 5 → 8 taken 1 time.
✓ Branch 10 → 11 taken 3 times.
✓ Branch 10 → 12 taken 310 times.
|
625 | if (!valid_operand_kind(anchor.operand_kind) || |
| 1254 |
2/2✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 310 times.
|
312 | !detail::valid_code_constant_byte_width(anchor.byte_width)) |
| 1255 | { | ||
| 1256 | 3 | return ResultDomain::Unknown; | |
| 1257 | } | ||
| 1258 | 310 | return ResultDomain::Scalar; | |
| 1259 | 107 | case AnchorKind::Manual: | |
| 1260 | // A decoded operand value and a pinned literal are constants, not addresses to write through. | ||
| 1261 | 107 | return ResultDomain::Scalar; | |
| 1262 | 23 | case AnchorKind::StringXref: | |
| 1263 |
6/6✓ Branch 15 → 16 taken 22 times.
✓ Branch 15 → 18 taken 1 time.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 17 → 19 taken 21 times.
✓ Branch 20 → 21 taken 2 times.
✓ Branch 20 → 22 taken 21 times.
|
23 | if (!valid_string_encoding(anchor.xref_encoding) || !valid_xref_return(anchor.xref_return)) |
| 1264 | { | ||
| 1265 | 2 | return ResultDomain::Unknown; | |
| 1266 | } | ||
| 1267 | // The instruction or its parent function is code. A pointer-slot return is a data slot. | ||
| 1268 |
2/2✓ Branch 22 → 23 taken 4 times.
✓ Branch 22 → 24 taken 17 times.
|
21 | if (anchor.xref_return == scan::XrefReturn::StringPointerSlot) |
| 1269 | { | ||
| 1270 | 4 | return ResultDomain::DataAddress; | |
| 1271 | } | ||
| 1272 | 17 | return ResultDomain::CodeSite; | |
| 1273 | 83 | case AnchorKind::ExportName: | |
| 1274 | 83 | return ResultDomain::CodeSite; | |
| 1275 | 173 | case AnchorKind::RipGlobal: | |
| 1276 |
4/4✓ Branch 26 → 27 taken 45 times.
✓ Branch 26 → 29 taken 128 times.
✓ Branch 27 → 28 taken 1 time.
✓ Branch 27 → 29 taken 44 times.
|
173 | if (anchor.pages != scan::Pages::Readable && anchor.pages != scan::Pages::Executable) |
| 1277 | { | ||
| 1278 | 1 | return ResultDomain::Unknown; | |
| 1279 | } | ||
| 1280 | // The pages field states the author contract. Executable narrows the cascade to image instruction | ||
| 1281 | // sites. The default Readable admits a data-page global. | ||
| 1282 |
2/2✓ Branch 29 → 30 taken 44 times.
✓ Branch 29 → 31 taken 128 times.
|
172 | if (anchor.pages == scan::Pages::Executable) |
| 1283 | { | ||
| 1284 | 44 | return ResultDomain::CodeSite; | |
| 1285 | } | ||
| 1286 | 128 | return ResultDomain::DataAddress; | |
| 1287 | 65 | case AnchorKind::Quorum: | |
| 1288 | { | ||
| 1289 |
2/2✓ Branch 33 → 34 taken 1 time.
✓ Branch 33 → 35 taken 64 times.
|
65 | if (!valid_quorum_match(anchor.quorum_match)) |
| 1290 | { | ||
| 1291 | 1 | return ResultDomain::Unknown; | |
| 1292 | } | ||
| 1293 | // The quorum domain is the one specific non-Scalar domain on which its members agree. A Manual or | ||
| 1294 | // Scalar member is a wildcard corroborator. Different specific domains are ambiguous. | ||
| 1295 | // A nested Quorum member is malformed (rejected at resolve), so it is skipped, not recursed into. | ||
| 1296 | 64 | ResultDomain domain = ResultDomain::Scalar; | |
| 1297 |
2/2✓ Branch 59 → 37 taken 150 times.
✓ Branch 59 → 60 taken 63 times.
|
277 | for (const Anchor *member : anchor.quorum_members) |
| 1298 | { | ||
| 1299 |
2/4✓ Branch 39 → 40 taken 150 times.
✗ Branch 39 → 41 not taken.
✗ Branch 40 → 41 not taken.
✓ Branch 40 → 42 taken 150 times.
|
150 | if (member == nullptr || member->kind == AnchorKind::Quorum) |
| 1300 | { | ||
| 1301 | ✗ | continue; | |
| 1302 | } | ||
| 1303 | 150 | const ResultDomain member_domain = declared_domain(*member); | |
| 1304 |
3/4✓ Branch 43 → 44 taken 33 times.
✓ Branch 43 → 45 taken 117 times.
✗ Branch 44 → 45 not taken.
✓ Branch 44 → 46 taken 33 times.
|
150 | if (member_domain == ResultDomain::Scalar || member_domain == ResultDomain::Unknown) |
| 1305 | { | ||
| 1306 | 117 | continue; | |
| 1307 | } | ||
| 1308 |
2/2✓ Branch 46 → 47 taken 25 times.
✓ Branch 46 → 48 taken 8 times.
|
33 | if (domain == ResultDomain::Scalar) |
| 1309 | { | ||
| 1310 | 25 | domain = member_domain; | |
| 1311 | } | ||
| 1312 |
2/2✓ Branch 48 → 49 taken 1 time.
✓ Branch 48 → 50 taken 7 times.
|
8 | else if (domain != member_domain) |
| 1313 | { | ||
| 1314 | 1 | return ResultDomain::Unknown; | |
| 1315 | } | ||
| 1316 | } | ||
| 1317 | 63 | return domain; | |
| 1318 | } | ||
| 1319 | 2 | case AnchorKind::CallArgHome: | |
| 1320 | case AnchorKind::Unset: | ||
| 1321 | 2 | return ResultDomain::Unknown; | |
| 1322 | } | ||
| 1323 | 1 | return ResultDomain::Unknown; | |
| 1324 | } | ||
| 1325 | } // namespace anchor | ||
| 1326 | } // namespace DetourModKit | ||
| 1327 |