./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
569 / 604
94.2%
Branches:
459 / 567
81.0%
Functions:
61 / 61
100.0%

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
0/2
✗ Branch 12 → 13 not taken.
✗ Branch 12 → 27 not taken.
✗ std::vector<std::size_t> indices(site.size());
58 ✗ const std::size_t count = scan::order_candidates(profile.candidate_order, site, indices);
59
0/2
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 30 not taken.
✗ ordered.reserve(count);
60
0/2
✗ Branch 22 → 18 not taken.
✗ Branch 22 → 23 not taken.
✗ for (std::size_t i = 0; i < count; ++i)
61 {
62
0/2
✗ Branch 20 → 21 not taken.
✗ Branch 20 → 30 not taken.
✗ 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 414 [[nodiscard]] PhysicalSource physical_source_of(AnchorKind kind) noexcept
208 {
209
9/9
✓ Branch 2 → 3 taken 88 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.
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
2/4
✓ Branch 2 → 3 taken 74 times.
✗ Branch 2 → 4 not taken.
✓ Branch 2 → 5 taken 2 times.
✗ Branch 2 → 6 not taken.
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
4/4
✓ Branch 2 → 3 taken 8 times.
✓ Branch 2 → 7 taken 262 times.
✓ Branch 3 → 4 taken 7 times.
✓ Branch 3 → 7 taken 1 time.
270 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 240 times.
✓ Branch 9 → 10 taken 9 times.
✓ Branch 9 → 11 taken 18 times.
✓ Branch 12 → 13 taken 9 times.
✓ Branch 12 → 14 taken 258 times.
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
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 468 [[nodiscard]] bool scope_is_single_allocation(Region scope) noexcept
311 {
312
6/8
✓ Branch 3 → 4 taken 467 times.
✓ Branch 3 → 7 taken 1 time.
✓ Branch 4 → 5 taken 467 times.
✗ Branch 4 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 467 times.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 467 times.
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
2/4
✓ Branch 13 → 14 taken 467 times.
✗ Branch 13 → 15 not taken.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 467 times.
934 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 467 times.
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
1/2
✓ Branch 37 → 22 taken 23071 times.
✗ Branch 37 → 38 not taken.
23071 while (cursor < scope_end)
326 {
327 23071 if (::VirtualQuery(reinterpret_cast<const void *>(cursor), &memory_info, sizeof(memory_info)) ==
328
3/4
✓ Branch 23 → 24 taken 23071 times.
✗ Branch 23 → 25 not taken.
✓ Branch 27 → 28 taken 3 times.
✓ Branch 27 → 29 taken 23068 times.
46142 0 ||
329
2/2
✓ Branch 24 → 25 taken 3 times.
✓ Branch 24 → 26 taken 23068 times.
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
2/4
✓ Branch 29 → 30 taken 23068 times.
✗ Branch 29 → 31 not taken.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 23068 times.
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
1/2
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 23068 times.
23068 if (region_end <= cursor)
340 {
341 ✗ return false;
342 }
343
2/2
✓ Branch 34 → 35 taken 464 times.
✓ Branch 34 → 36 taken 22604 times.
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
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 252 times.
253 if (!key.tracked())
363 {
364 1 return;
365 }
366
1/2
✗ Branch 11 → 6 not taken.
✓ Branch 11 → 12 taken 252 times.
252 for (std::size_t i = 0; i < evidence_count; ++i)
367 {
368
0/2
✗ Branch 8 → 9 not taken.
✗ Branch 8 → 10 not taken.
✗ if (evidence[i] == key)
369 {
370 ✗ return;
371 }
372 }
373
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 252 times.
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
5/6
✓ Branch 3 → 4 taken 440 times.
✓ Branch 3 → 8 taken 3 times.
✓ Branch 4 → 5 taken 440 times.
✗ Branch 4 → 8 not taken.
✓ Branch 10 → 11 taken 4 times.
✓ Branch 10 → 12 taken 439 times.
883 if (region.base.raw() == 0 || region.size == 0 ||
390
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 439 times.
440 !DetourModKit::detail::is_plausible_ptr(region.base.raw()))
391 {
392 4 return OwnerKey{};
393 }
394 439 MEMORY_BASIC_INFORMATION memory_info{};
395
2/4
✓ Branch 14 → 15 taken 439 times.
✗ Branch 14 → 16 not taken.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 21 taken 439 times.
878 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 439 times.
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
1/2
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 24 taken 439 times.
439 if (allocation_base == 0)
402 {
403 ✗ return OwnerKey{region.base.raw(), 0, {}, OwnerBacking::Missing};
404 }
405
2/2
✓ Branch 24 → 25 taken 213 times.
✓ Branch 24 → 28 taken 226 times.
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
1/2
✓ Branch 28 → 29 taken 226 times.
✗ Branch 28 → 31 not taken.
226 if (memory_info.Type == MEM_PRIVATE)
415 {
416 226 return OwnerKey{region.base.raw(), allocation_base, {}, OwnerBacking::Private};
417 }
418
0/2
✗ Branch 31 → 32 not taken.
✗ Branch 31 → 34 not taken.
✗ 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
4/8
✓ Branch 3 → 4 taken 215 times.
✗ Branch 3 → 7 not taken.
✓ Branch 4 → 5 taken 215 times.
✗ Branch 4 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 215 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 215 times.
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
1/2
✓ Branch 30 → 14 taken 217 times.
✗ Branch 30 → 31 not taken.
217 while (cursor < scope_end)
439 {
440 217 MEMORY_BASIC_INFORMATION memory_info{};
441
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 217 times.
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
2/4
✓ Branch 17 → 18 taken 217 times.
✗ Branch 17 → 29 not taken.
✓ Branch 18 → 19 taken 217 times.
✗ Branch 18 → 29 not taken.
217 if (memory_info.RegionSize == 0 || region_base > UINTPTR_MAX - memory_info.RegionSize)
447 {
448 break;
449 }
450
2/2
✓ Branch 19 → 20 taken 215 times.
✓ Branch 19 → 26 taken 2 times.
217 if (memory_info.State == MEM_COMMIT)
451 {
452
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 215 times.
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/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 428 [[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 356 times.
✓ Branch 4 → 5 taken 10 times.
✓ Branch 4 → 6 taken 62 times.
✓ Branch 7 → 8 taken 366 times.
✓ Branch 7 → 9 taken 62 times.
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
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 507 [[nodiscard]] bool owner_key_current(const OwnerKey &key) noexcept
488 {
489
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 507 times.
507 if (!key.tracked())
490 {
491 ✗ return true;
492 }
493
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 507 times.
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
3/6
✓ Branch 8 → 9 taken 507 times.
✗ Branch 8 → 11 not taken.
✓ Branch 9 → 10 taken 507 times.
✗ Branch 9 → 11 not taken.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 507 times.
1014 memory_info.State != MEM_COMMIT ||
501
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 507 times.
507 reinterpret_cast<std::uintptr_t>(memory_info.AllocationBase) != key.allocation_base)
502 {
503 ✗ return false;
504 }
505
2/2
✓ Branch 16 → 17 taken 345 times.
✓ Branch 16 → 21 taken 162 times.
507 if (!key.has_module())
506 {
507
1/2
✓ Branch 17 → 18 taken 345 times.
✗ Branch 17 → 19 not taken.
345 const DWORD expected_type = key.backing == OwnerBacking::Private ? MEM_PRIVATE : MEM_MAPPED;
508 345 return memory_info.Type == expected_type;
509 }
510
6/6
✓ Branch 21 → 22 taken 161 times.
✓ Branch 21 → 24 taken 1 time.
✓ Branch 23 → 24 taken 2 times.
✓ Branch 23 → 25 taken 159 times.
✓ Branch 26 → 27 taken 3 times.
✓ Branch 26 → 28 taken 159 times.
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
2/2
✓ Branch 30 → 31 taken 2 times.
✓ Branch 30 → 32 taken 157 times.
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
6/8
✓ Branch 33 → 34 taken 159 times.
✗ Branch 33 → 37 not taken.
✓ Branch 34 → 35 taken 159 times.
✗ Branch 34 → 37 not taken.
✓ Branch 35 → 36 taken 158 times.
✓ Branch 35 → 37 taken 1 time.
✓ Branch 39 → 40 taken 1 time.
✓ Branch 39 → 41 taken 158 times.
317 confirmed_info.State != MEM_COMMIT || confirmed_info.Type != MEM_IMAGE ||
528
1/2
✗ Branch 36 → 37 not taken.
✓ Branch 36 → 38 taken 158 times.
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
2/2
✓ Branch 43 → 44 taken 1 time.
✓ Branch 43 → 45 taken 157 times.
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
6/8
✓ Branch 46 → 47 taken 158 times.
✗ Branch 46 → 50 not taken.
✓ Branch 47 → 48 taken 158 times.
✗ Branch 47 → 50 not taken.
✓ Branch 48 → 49 taken 157 times.
✓ Branch 48 → 50 taken 1 time.
✓ Branch 52 → 53 taken 1 time.
✓ Branch 52 → 54 taken 157 times.
315 final_info.State != MEM_COMMIT || final_info.Type != MEM_IMAGE ||
543
1/2
✗ Branch 49 → 50 not taken.
✓ Branch 49 → 51 taken 157 times.
157 reinterpret_cast<std::uintptr_t>(final_info.AllocationBase) != key.allocation_base)
544 {
545 1 return false;
546 }
547
4/4
✓ Branch 55 → 56 taken 151 times.
✓ Branch 55 → 59 taken 6 times.
✓ Branch 57 → 58 taken 150 times.
✓ Branch 57 → 59 taken 1 time.
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
7/8
✓ Branch 3 → 4 taken 83 times.
✓ Branch 3 → 9 taken 194 times.
✓ Branch 5 → 6 taken 83 times.
✗ Branch 5 → 9 not taken.
✓ Branch 6 → 7 taken 80 times.
✓ Branch 6 → 9 taken 3 times.
✓ Branch 8 → 9 taken 78 times.
✓ Branch 8 → 10 taken 2 times.
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
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 408 times.
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
2/2
✓ Branch 7 → 8 taken 129 times.
✓ Branch 7 → 10 taken 279 times.
408 if (local.value.tracked())
575 {
576 129 local_keys[local_count++] = local.value;
577 }
578
2/2
✓ Branch 42 → 11 taken 373 times.
✓ Branch 42 → 43 taken 393 times.
766 for (std::size_t i = 0; i < local_count; ++i)
579 {
580
2/2
✓ Branch 13 → 14 taken 13 times.
✓ Branch 13 → 15 taken 360 times.
373 if (!owner_key_current(local_keys[i]))
581 {
582 13 return false;
583 }
584
2/2
✓ Branch 22 → 16 taken 111 times.
✓ Branch 22 → 23 taken 360 times.
471 for (std::size_t j = i + 1; j < local_count; ++j)
585 {
586
1/2
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 111 times.
111 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 358 times.
739 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 392 times.
526 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 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
6/6
✓ Branch 2 → 3 taken 462 times.
✓ Branch 2 → 5 taken 120 times.
✓ Branch 3 → 4 taken 334 times.
✓ Branch 3 → 5 taken 128 times.
✓ Branch 4 → 5 taken 10 times.
✓ Branch 4 → 6 taken 324 times.
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
2/2
✓ Branch 4 → 5 taken 130 times.
✓ Branch 4 → 8 taken 140 times.
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
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 1226 [[nodiscard]] constexpr bool evidence_module_is_scope(AnchorKind kind) noexcept
662 {
663
3/3
✓ Branch 2 → 3 taken 647 times.
✓ Branch 2 → 4 taken 576 times.
✓ Branch 2 → 5 taken 3 times.
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
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 /// 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
2/2
✓ Branch 2 → 3 taken 132 times.
✓ Branch 2 → 5 taken 282 times.
414 if (provenance != nullptr)
710 {
711 132 *provenance = PhysicalProvenance{};
712 }
713
2/2
✓ Branch 5 → 6 taken 132 times.
✓ Branch 5 → 7 taken 282 times.
414 if (owner_keys_out != nullptr)
714 {
715 132 *owner_keys_out = ResolutionOwnerKeys{};
716 }
717
2/2
✓ Branch 7 → 8 taken 48 times.
✓ Branch 7 → 10 taken 366 times.
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
2/2
✓ Branch 4 → 5 taken 4 times.
✓ Branch 4 → 11 taken 1 time.
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
1/2
✓ Branch 3 → 4 taken 86 times.
✗ Branch 3 → 22 not taken.
86 const Result<detail::ResolvedScanHit> discovered = detail::resolve_scan_with_provenance(request);
764
2/2
✓ Branch 5 → 6 taken 76 times.
✓ Branch 5 → 19 taken 10 times.
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
2/2
✓ Branch 13 → 14 taken 21 times.
✓ Branch 13 → 16 taken 55 times.
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
6/6
✓ Branch 3 → 4 taken 111 times.
✓ Branch 3 → 8 taken 1 time.
✓ Branch 5 → 6 taken 109 times.
✓ Branch 5 → 8 taken 2 times.
✓ Branch 10 → 11 taken 4 times.
✓ Branch 10 → 12 taken 108 times.
221 !detail::valid_code_constant_byte_width(anchor.byte_width) ||
794
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 108 times.
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
1/2
✓ Branch 12 → 13 taken 108 times.
✗ Branch 12 → 29 not taken.
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
1/2
✓ Branch 13 → 14 taken 108 times.
✗ Branch 13 → 29 not taken.
108 detail::read_code_constant_with_provenance(code_constant, scope);
809
2/2
✓ Branch 15 → 16 taken 106 times.
✓ Branch 15 → 25 taken 2 times.
108 if (discovered)
810 {
811 106 owner_keys.value = capture_value_owner(anchor, discovered->value);
812
2/2
✓ Branch 18 → 19 taken 71 times.
✓ Branch 18 → 23 taken 35 times.
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
6/6
✓ Branch 3 → 4 taken 11 times.
✓ Branch 3 → 6 taken 1 time.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 10 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 10 times.
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
1/2
✓ Branch 11 → 12 taken 10 times.
✗ Branch 11 → 25 not taken.
10 detail::find_string_xref_with_provenance(query, scope, discovered_span);
851
2/2
✓ Branch 13 → 14 taken 6 times.
✓ Branch 13 → 22 taken 4 times.
10 if (discovered)
852 {
853 6 owner_keys.value = capture_value_owner(anchor, static_cast<std::int64_t>(discovered->raw()));
854
2/2
✓ Branch 17 → 18 taken 4 times.
✓ Branch 17 → 19 taken 2 times.
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
2/2
✓ Branch 3 → 4 taken 6 times.
✓ Branch 3 → 5 taken 32 times.
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
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 37 times.
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
2/2
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 37 times.
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
2/2
✓ Branch 15 → 16 taken 33 times.
✓ Branch 15 → 24 taken 4 times.
37 if (discovered)
898 {
899 33 owner_keys.value = capture_value_owner(anchor, static_cast<std::int64_t>(discovered->raw()));
900
2/2
✓ Branch 19 → 20 taken 10 times.
✓ Branch 19 → 21 taken 23 times.
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
2/2
✓ Branch 2 → 3 taken 14 times.
✓ Branch 2 → 5 taken 54 times.
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
2/2
✓ Branch 31 → 4 taken 95 times.
✓ Branch 31 → 32 taken 42 times.
179 for (const std::int64_t center : votes)
941 {
942
3/4
✓ Branch 9 → 10 taken 95 times.
✗ Branch 9 → 54 not taken.
✓ Branch 16 → 17 taken 25 times.
✓ Branch 16 → 18 taken 70 times.
190 if (std::find(qualifying.begin(), qualifying.end(), center) != qualifying.end())
943 {
944 25 continue;
945 }
946
2/2
✓ Branch 20 → 21 taken 44 times.
✓ Branch 20 → 22 taken 26 times.
70 if (votes_agreeing_with(center, votes, anchor.quorum_match, anchor.quorum_tolerance) >= threshold)
947 {
948
1/2
✓ Branch 21 → 22 taken 44 times.
✗ Branch 21 → 56 not taken.
44 qualifying.push_back(center);
949 }
950 }
951
2/2
✓ Branch 33 → 34 taken 8 times.
✓ Branch 33 → 35 taken 34 times.
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
1/2
✓ Branch 37 → 38 taken 34 times.
✗ Branch 37 → 58 not taken.
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
2/2
✓ Branch 38 → 39 taken 3 times.
✓ Branch 38 → 40 taken 31 times.
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
1/2
✓ Branch 42 → 43 taken 31 times.
✗ Branch 42 → 57 not taken.
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
2/2
✓ Branch 49 → 50 taken 31 times.
✓ Branch 49 → 52 taken 11 times.
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
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 77 times.
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
2/2
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 8 taken 75 times.
77 if (members.size() < 2)
1019 {
1020 2 result.status = AnchorStatus::Failed;
1021 2 return;
1022 }
1023
1/2
✓ Branch 10 → 11 taken 75 times.
✗ Branch 10 → 83 not taken.
75 const bool malformed_member = std::any_of(
1024 members.begin(),
1025 members.end(),
1026 171 [](const Anchor *member) noexcept
1027
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; }
1028 );
1029
2/2
✓ Branch 11 → 12 taken 2 times.
✓ Branch 11 → 13 taken 73 times.
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
2/2
✓ Branch 13 → 14 taken 57 times.
✓ Branch 13 → 15 taken 16 times.
73 const std::size_t threshold = (anchor.quorum_threshold == 0) ? members.size() : anchor.quorum_threshold;
1038
6/6
✓ Branch 16 → 17 taken 72 times.
✓ Branch 16 → 19 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 20 taken 71 times.
✓ Branch 21 → 22 taken 2 times.
✓ Branch 21 → 23 taken 71 times.
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
3/4
✓ Branch 23 → 24 taken 71 times.
✗ Branch 23 → 83 not taken.
✓ Branch 24 → 25 taken 15 times.
✓ Branch 24 → 26 taken 56 times.
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
1/2
✓ Branch 27 → 28 taken 56 times.
✗ Branch 27 → 81 not taken.
56 votes.reserve(members.size());
1056 56 std::vector<PhysicalProvenance> vote_provenance;
1057
1/2
✓ Branch 29 → 30 taken 56 times.
✗ Branch 29 → 79 not taken.
56 vote_provenance.reserve(members.size());
1058 56 bool physical_dependency = false;
1059
1/2
✓ Branch 31 → 32 taken 56 times.
✗ Branch 31 → 79 not taken.
56 member_keys.reserve(members.size() * 2);
1060
2/2
✓ Branch 62 → 34 taken 132 times.
✓ Branch 62 → 63 taken 56 times.
244 for (const Anchor *member : members)
1061 {
1062 132 PhysicalProvenance member_provenance;
1063 132 ResolutionOwnerKeys member_owner_keys;
1064
1/2
✓ Branch 36 → 37 taken 132 times.
✗ Branch 36 → 77 not taken.
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
2/2
✓ Branch 37 → 38 taken 125 times.
✓ Branch 37 → 53 taken 7 times.
132 if (resolved_member.status == AnchorStatus::Resolved)
1073 {
1074 125 physical_dependency =
1075
5/6
✓ Branch 38 → 39 taken 123 times.
✓ Branch 38 → 43 taken 2 times.
✓ Branch 41 → 42 taken 123 times.
✗ Branch 41 → 77 not taken.
✓ Branch 42 → 43 taken 14 times.
✓ Branch 42 → 44 taken 109 times.
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
1/2
✓ Branch 45 → 46 taken 125 times.
✗ Branch 45 → 77 not taken.
125 votes.push_back(resolved_member.value);
1082
1/2
✓ Branch 46 → 47 taken 125 times.
✗ Branch 46 → 77 not taken.
125 vote_provenance.push_back(member_provenance);
1083 // Only a member that casts a vote supplies evidence for corroboration.
1084
1/2
✓ Branch 47 → 48 taken 125 times.
✗ Branch 47 → 77 not taken.
125 append_owner_keys(member_keys, member_owner_keys);
1085 125 owner_keys.requires_single_allocation =
1086
4/4
✓ Branch 48 → 49 taken 70 times.
✓ Branch 48 → 50 taken 55 times.
✓ Branch 49 → 50 taken 46 times.
✓ Branch 49 → 51 taken 24 times.
125 owner_keys.requires_single_allocation || member_owner_keys.requires_single_allocation;
1087 }
1088 }
1089
2/2
✓ Branch 63 → 64 taken 14 times.
✓ Branch 63 → 65 taken 42 times.
56 if (physical_dependency)
1090 {
1091 14 result.status = AnchorStatus::QuorumNotIndependent;
1092 14 return;
1093 }
1094
1/2
✓ Branch 65 → 66 taken 42 times.
✗ Branch 65 → 79 not taken.
42 commit_quorum_consensus(anchor, votes, threshold, owner_keys, result);
1095
4/4
✓ Branch 68 → 69 taken 42 times.
✓ Branch 68 → 70 taken 14 times.
✓ Branch 72 → 73 taken 42 times.
✓ Branch 72 → 75 taken 14 times.
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
2/2
✓ Branch 2 → 3 taken 312 times.
✓ Branch 2 → 16 taken 96 times.
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
4/4
✓ Branch 4 → 5 taken 63 times.
✓ Branch 4 → 8 taken 249 times.
✓ Branch 9 → 10 taken 6 times.
✓ Branch 9 → 11 taken 306 times.
375 if (result.domain == ResultDomain::CodeSite &&
1113
2/2
✓ Branch 6 → 7 taken 6 times.
✓ Branch 6 → 8 taken 57 times.
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
2/2
✓ Branch 11 → 12 taken 103 times.
✓ Branch 11 → 13 taken 209 times.
312 if (anchor.kind == AnchorKind::CodeOperand)
1123 {
1124 103 result.witness.operand_kind = anchor.operand_kind;
1125 }
1126
2/2
✓ Branch 14 → 15 taken 128 times.
✓ Branch 14 → 16 taken 184 times.
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
2/2
✓ Branch 16 → 17 taken 312 times.
✓ Branch 16 → 19 taken 96 times.
408 if (result.status == AnchorStatus::Resolved)
1133 {
1134
2/2
✓ Branch 17 → 18 taken 2 times.
✓ Branch 17 → 19 taken 310 times.
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
2/2
✓ Branch 4 → 5 taken 16 times.
✓ Branch 4 → 6 taken 392 times.
408 if (!evidence_images_coherent(owner_keys, member_keys))
1152 {
1153 16 return false;
1154 }
1155
6/6
✓ Branch 6 → 7 taken 251 times.
✓ Branch 6 → 10 taken 141 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 249 times.
✓ Branch 11 → 12 taken 2 times.
✓ Branch 11 → 13 taken 390 times.
392 if (owner_keys.requires_single_allocation && !scope_is_single_allocation(scope))
1156 {
1157 2 return false;
1158 }
1159
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 16 taken 389 times.
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
2/2
✓ Branch 5 → 6 taken 4 times.
✓ Branch 5 → 7 taken 410 times.
414 if (profile.is_denied(anchor.kind))
1184 {
1185 4 result.status = AnchorStatus::Failed;
1186 4 return result;
1187 }
1188
1189
6/6
✓ Branch 8 → 9 taken 217 times.
✓ Branch 8 → 12 taken 193 times.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 12 taken 215 times.
✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 15 taken 408 times.
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
2/2
✓ Branch 17 → 18 taken 215 times.
✓ Branch 17 → 21 taken 193 times.
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
10/10
✓ Branch 21 → 22 taken 5 times.
✓ Branch 21 → 24 taken 86 times.
✓ Branch 21 → 29 taken 112 times.
✓ Branch 21 → 31 taken 12 times.
✓ Branch 21 → 33 taken 38 times.
✓ Branch 21 → 35 taken 68 times.
✓ Branch 21 → 37 taken 6 times.
✓ Branch 21 → 38 taken 78 times.
✓ Branch 21 → 40 taken 2 times.
✓ Branch 21 → 41 taken 1 time.
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
3/4
✓ Branch 24 → 25 taken 20 times.
✓ Branch 24 → 27 taken 66 times.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 20 times.
86 if (anchor.pages != scan::Pages::Readable && anchor.pages != scan::Pages::Executable)
1214 {
1215 ✗ return failed_anchor_result(anchor);
1216 }
1217
1/2
✓ Branch 27 → 28 taken 86 times.
✗ Branch 27 → 55 not taken.
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
1/2
✓ Branch 29 → 30 taken 112 times.
✗ Branch 29 → 55 not taken.
112 resolve_code_operand(anchor, profile, scope, provenance, owner_keys, result);
1231 112 break;
1232 12 case AnchorKind::StringXref:
1233
1/2
✓ Branch 31 → 32 taken 12 times.
✗ Branch 31 → 55 not taken.
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
1/2
✓ Branch 38 → 39 taken 78 times.
✗ Branch 38 → 55 not taken.
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
2/2
✓ Branch 41 → 42 taken 1 time.
✓ Branch 41 → 43 taken 407 times.
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
2/2
✓ Branch 45 → 46 taken 19 times.
✓ Branch 45 → 47 taken 389 times.
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
2/2
✓ Branch 47 → 48 taken 131 times.
✓ Branch 47 → 49 taken 258 times.
389 if (owner_keys_out != nullptr)
1284 {
1285 131 *owner_keys_out = owner_keys;
1286 }
1287
2/2
✓ Branch 49 → 50 taken 48 times.
✓ Branch 49 → 51 taken 341 times.
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
1/2
✓ Branch 2 → 3 taken 48 times.
✗ Branch 2 → 6 not taken.
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
1/2
✓ Branch 2 → 3 taken 218 times.
✗ Branch 2 → 6 not taken.
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
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();
1317
2/2
✓ Branch 12 → 8 taken 10 times.
✓ Branch 12 → 13 taken 5 times.
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/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();
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/2
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 17 not taken.
1 );
1338
1339
2/2
✓ Branch 13 → 10 taken 3 times.
✓ Branch 13 → 14 taken 1 time.
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
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();
1354
2/2
✓ Branch 12 → 8 taken 5 times.
✓ Branch 12 → 13 taken 2 times.
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/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();
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/2
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 17 not taken.
1 );
1377
1378
2/2
✓ Branch 13 → 10 taken 3 times.
✓ Branch 13 → 14 taken 1 time.
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
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 185 times.
✓ Branch 2 → 32 taken 65 times.
✓ Branch 2 → 61 taken 2 times.
✓ Branch 2 → 62 taken 1 time.
793 switch (anchor.kind)
1388 {
1389 14 case AnchorKind::VtableIdentity:
1390 14 return ResultDomain::VtableAddress;
1391 313 case AnchorKind::CodeOperand:
1392
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) ||
1393
2/2
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 310 times.
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
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))
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
2/2
✓ Branch 22 → 23 taken 4 times.
✓ Branch 22 → 24 taken 17 times.
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
4/4
✓ Branch 26 → 27 taken 45 times.
✓ Branch 26 → 29 taken 140 times.
✓ Branch 27 → 28 taken 1 time.
✓ Branch 27 → 29 taken 44 times.
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
2/2
✓ Branch 29 → 30 taken 44 times.
✓ Branch 29 → 31 taken 140 times.
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
2/2
✓ Branch 33 → 34 taken 1 time.
✓ Branch 33 → 35 taken 64 times.
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
2/2
✓ Branch 59 → 37 taken 150 times.
✓ Branch 59 → 60 taken 63 times.
277 for (const Anchor *member : anchor.quorum_members)
1437 {
1438
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)
1439 {
1440 ✗ continue;
1441 }
1442 150 const ResultDomain member_domain = declared_domain(*member);
1443
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)
1444 {
1445 117 continue;
1446 }
1447
2/2
✓ Branch 46 → 47 taken 25 times.
✓ Branch 46 → 48 taken 8 times.
33 if (domain == ResultDomain::Scalar)
1448 {
1449 25 domain = member_domain;
1450 }
1451
2/2
✓ Branch 48 → 49 taken 1 time.
✓ Branch 48 → 50 taken 7 times.
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