GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 93.9% 540 / 0 / 575
Functions: 100.0% 50 / 0 / 50
Branches: 81.2% 446 / 0 / 549

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