GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 92.4% 255 / 0 / 276
Functions: 100.0% 25 / 0 / 25
Branches: 69.2% 139 / 0 / 201

src/anchors.cpp
Line Branch Exec Source
1 /**
2 * @file anchors.cpp
3 * @brief Declarative self-healing anchor registry.
4 *
5 * Dispatches each anchor to the backend its kind names (reverse-RTTI vtable resolve, AOB/RIP cascade, in-code constant
6 * decode, a string-literal xref resolve, or a pinned literal) and reports a uniform value + status, so a consumer
7 * declares every magic constant once and resolves the whole table in a single pass. A quorum kind layers corroboration
8 * on top, accepting a target only when two independent sub-anchors resolve and agree, and any resolved value may be
9 * screened by an optional caller-supplied validator. Every backend already fails closed; this layer only maps their
10 * typed failures onto a common status.
11 */
12
13 #include "DetourModKit/anchors.hpp"
14
15 #include "DetourModKit/memory.hpp"
16 #include "DetourModKit/profile.hpp"
17 #include "DetourModKit/rtti.hpp"
18 #include "DetourModKit/scanner.hpp"
19
20 #include "fork_join.hpp"
21
22 #include <cstddef>
23 #include <cstdint>
24 #include <span>
25 #include <type_traits>
26 #include <vector>
27
28 namespace DetourModKit
29 {
30 namespace
31 {
32 // Applies the profile's candidate-order preference by building a local reordered span. The caller's candidate
33 // table is typically static storage and must not be mutated; the vector owns the temporary copy for the
34 // duration of the backend call. AsDeclared returns the original span without allocation.
35 [[nodiscard]] std::span<const Scanner::AddrCandidate>
36 37 profiled_candidates(const ScanProfile &profile, std::span<const Scanner::AddrCandidate> site,
37 std::vector<Scanner::AddrCandidate> &ordered)
38 {
39
5/6
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 5 taken 34 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 3 times.
✓ Branch 7 → 8 taken 34 times.
✓ Branch 7 → 9 taken 3 times.
37 if (profile.candidate_order == CandidateOrder::AsDeclared || site.size() < 2)
40 {
41 34 return site;
42 }
43
44
1/2
✓ Branch 12 → 13 taken 3 times.
✗ Branch 12 → 27 not taken.
3 std::vector<std::size_t> indices(site.size());
45 3 const std::size_t count = order_candidates(profile, site, indices);
46
1/2
✓ Branch 16 → 17 taken 3 times.
✗ Branch 16 → 30 not taken.
3 ordered.reserve(count);
47
2/2
✓ Branch 22 → 18 taken 6 times.
✓ Branch 22 → 23 taken 3 times.
9 for (std::size_t i = 0; i < count; ++i)
48 {
49
1/2
✓ Branch 20 → 21 taken 6 times.
✗ Branch 20 → 30 not taken.
6 ordered.push_back(site[indices[i]]);
50 }
51 3 return ordered;
52 3 }
53
54 // Fail-closed agreement test for the two quorum signals. A negative tolerance is rejected outright: widening it
55 // through unsigned subtraction would turn -1 into a huge bound that accepts almost any gap and defeat the
56 // corroboration the quorum exists to provide.
57 11 [[nodiscard]] bool quorum_values_agree(std::int64_t first, std::int64_t second, Anchors::QuorumMatch match,
58 std::int64_t tolerance) noexcept
59 {
60
2/2
✓ Branch 2 → 3 taken 8 times.
✓ Branch 2 → 4 taken 3 times.
11 if (match == Anchors::QuorumMatch::ExactValue)
61 {
62 8 return first == second;
63 }
64
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 2 times.
3 if (tolerance < 0)
65 {
66 1 return false;
67 }
68 // Order the pair so the gap is hi - lo, then widen through unsigned subtraction to avoid signed overflow
69 // across a large address span.
70
1/2
✓ Branch 6 → 7 taken 2 times.
✗ Branch 6 → 8 not taken.
2 const std::int64_t lo = (first < second) ? first : second;
71
1/2
✓ Branch 9 → 10 taken 2 times.
✗ Branch 9 → 11 not taken.
2 const std::int64_t hi = (first < second) ? second : first;
72 2 const auto gap = static_cast<std::uint64_t>(hi) - static_cast<std::uint64_t>(lo);
73 2 return gap <= static_cast<std::uint64_t>(tolerance);
74 }
75
76 // True when two resolvable sub-anchors share the SAME backend AND the SAME backend inputs, so they would decode
77 // the identical site/name/literal and therefore cannot corroborate each other. Compared per kind so only the
78 // fields that kind actually consumes participate. View/span comparison is identity over the underlying storage
79 // (same data pointer and length), not a deep byte compare: two distinct candidate arrays expressing the same
80 // pattern are still two independent scan sites, so only literally-shared storage counts as the same evidence.
81 14 [[nodiscard]] bool same_backend_config(const Anchors::Anchor &a, const Anchors::Anchor &b) noexcept
82 {
83
2/2
✓ Branch 2 → 3 taken 12 times.
✓ Branch 2 → 4 taken 2 times.
14 if (a.kind != b.kind)
84 {
85 12 return false;
86 }
87
1/6
✗ Branch 4 → 5 not taken.
✗ Branch 4 → 14 not taken.
✓ Branch 4 → 23 taken 2 times.
✗ Branch 4 → 35 not taken.
✗ Branch 4 → 48 not taken.
✗ Branch 4 → 49 not taken.
2 switch (a.kind)
88 {
89 case Anchors::AnchorKind::VtableIdentity:
90 return a.mangled.data() == b.mangled.data() && a.mangled.size() == b.mangled.size();
91 case Anchors::AnchorKind::RipGlobal:
92 return a.site.data() == b.site.data() && a.site.size() == b.site.size();
93 2 case Anchors::AnchorKind::CodeOperand:
94
1/2
✓ Branch 28 → 29 taken 1 time.
✗ Branch 28 → 33 not taken.
3 return a.site.data() == b.site.data() && a.site.size() == b.site.size() &&
95
4/6
✓ Branch 25 → 26 taken 1 time.
✓ Branch 25 → 33 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
✗ Branch 29 → 33 not taken.
✓ Branch 30 → 31 taken 1 time.
✗ Branch 30 → 33 not taken.
4 a.operand_kind == b.operand_kind && a.operand_index == b.operand_index &&
96
1/2
✓ Branch 31 → 32 taken 1 time.
✗ Branch 31 → 33 not taken.
3 a.byte_width == b.byte_width;
97 case Anchors::AnchorKind::StringXref:
98 return a.xref_text.data() == b.xref_text.data() && a.xref_text.size() == b.xref_text.size() &&
99 a.xref_encoding == b.xref_encoding && a.xref_return == b.xref_return &&
100 a.xref_require_terminator == b.xref_require_terminator &&
101 a.xref_broad_match == b.xref_broad_match;
102 case Anchors::AnchorKind::Manual:
103 case Anchors::AnchorKind::CallArgHome:
104 case Anchors::AnchorKind::Quorum:
105 // Handled by dedicated rules: a dual-Manual pair is rejected wholesale, CallArgHome cannot resolve, and
106 // a nested Quorum is already rejected upstream. No config-identity verdict is needed here.
107 return false;
108 }
109 return false;
110 }
111
112 // Fail-closed independence gate run BEFORE agreement is considered. Two signals are not independent evidence
113 // when: (a) they are the exact same Anchor object (pointer-equal); (b) both are Manual literals -- two
114 // hand-pinned constants agreeing proves only that the author typed the same number twice, not that the live
115 // image corroborates it; or (c) they share backend and inputs (same_backend_config), so they decode one site
116 // twice. Any of these would let a dependent pair masquerade as corroboration, defeating the quorum's purpose.
117 16 [[nodiscard]] bool quorum_sub_anchors_independent(const Anchors::Anchor &a, const Anchors::Anchor &b) noexcept
118 {
119
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 15 times.
16 if (&a == &b)
120 {
121 1 return false;
122 }
123
4/4
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 7 taken 13 times.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 1 time.
15 if (a.kind == Anchors::AnchorKind::Manual && b.kind == Anchors::AnchorKind::Manual)
124 {
125 1 return false;
126 }
127 14 return !same_backend_config(a, b);
128 }
129
130 // Commits a backend-resolved value, applying the anchor's optional fail-closed validator. On a validator miss
131 // the anchor is reported Failed with no value, identical to a backend miss, so the caller re-heals by
132 // re-running resolve.
133 44 void commit_resolved(const Anchors::Anchor &anchor, Anchors::ResolvedAnchor &result,
134 std::int64_t value) noexcept
135 {
136 // Opt-in required-validator policy: a backend-resolved (function/global) target with no domain check is
137 // treated as unverified and fails closed. A Quorum is exempt -- its two-signal corroboration already is the
138 // verification -- so a caller is not forced to also attach a validator to a corroborated anchor.
139
6/6
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 6 taken 41 times.
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 6 taken 1 time.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 1 time.
44 if (anchor.require_validator && anchor.kind != Anchors::AnchorKind::Quorum && anchor.validator == nullptr)
140 {
141 1 result.status = Anchors::AnchorStatus::Failed;
142 1 result.value = 0;
143 1 return;
144 }
145
6/6
✓ Branch 6 → 7 taken 8 times.
✓ Branch 6 → 10 taken 35 times.
✓ Branch 8 → 9 taken 4 times.
✓ Branch 8 → 10 taken 4 times.
✓ Branch 11 → 12 taken 4 times.
✓ Branch 11 → 13 taken 39 times.
43 if (anchor.validator != nullptr && !anchor.validator(value, anchor.validator_context))
146 {
147 4 result.status = Anchors::AnchorStatus::Failed;
148 4 result.value = 0;
149 4 return;
150 }
151 39 result.value = value;
152 39 result.status = Anchors::AnchorStatus::Resolved;
153 }
154
155 8 [[nodiscard]] Anchors::ResolvedAnchor failed_anchor_result(const Anchors::Anchor &anchor) noexcept
156 {
157 8 Anchors::ResolvedAnchor result{};
158 8 result.label = anchor.label;
159 8 result.kind = anchor.kind;
160 8 result.status = Anchors::AnchorStatus::Failed;
161 8 result.value = 0;
162 8 return result;
163 }
164
165 // FNV-1a 64 evidence hashing for anchor_fingerprint. The fingerprint must be stable across runs and builds so a
166 // persisted manifest can be diffed, so integers are folded least-significant-byte first (a fixed order
167 // independent of host endianness) and every variable-length field is length-prefixed to keep adjacent fields
168 // unambiguous (the literal "ab" then "" cannot collide with "a" then "b").
169 inline constexpr std::uint64_t FNV1A64_OFFSET = 14695981039346656037ULL;
170 inline constexpr std::uint64_t FNV1A64_PRIME = 1099511628211ULL;
171
172 1612 [[nodiscard]] constexpr std::uint64_t fnv1a_byte(std::uint64_t hash, std::uint8_t value) noexcept
173 {
174 1612 return (hash ^ value) * FNV1A64_PRIME;
175 }
176
177 134 template <typename T> [[nodiscard]] constexpr std::uint64_t fnv1a_int(std::uint64_t hash, T value) noexcept
178 {
179 134 auto bits = static_cast<std::make_unsigned_t<T>>(value);
180
4/4
unsigned long long DetourModKit::(anonymous namespace)::fnv1a_int<long long>(unsigned long long, long long):
✓ Branch 5 → 3 taken 496 times.
✓ Branch 5 → 6 taken 62 times.
unsigned long long DetourModKit::(anonymous namespace)::fnv1a_int<unsigned long long>(unsigned long long, unsigned long long):
✓ Branch 5 → 3 taken 576 times.
✓ Branch 5 → 6 taken 72 times.
1206 for (std::size_t i = 0; i < sizeof(T); ++i)
181 {
182 1072 hash = fnv1a_byte(hash, static_cast<std::uint8_t>(bits & 0xFFu));
183 1072 bits >>= 8;
184 }
185 134 return hash;
186 }
187
188 // Length-prefixed variable-length byte field, so two adjacent variable fields cannot alias one another.
189 38 [[nodiscard]] std::uint64_t fnv1a_field(std::uint64_t hash, std::string_view bytes) noexcept
190 {
191 38 hash = fnv1a_int(hash, bytes.size());
192
2/2
✓ Branch 8 → 6 taken 379 times.
✓ Branch 8 → 9 taken 38 times.
417 for (const char ch : bytes)
193 {
194 379 hash = fnv1a_byte(hash, static_cast<std::uint8_t>(ch));
195 }
196 38 return hash;
197 }
198
199 // Folds a cascade's address-independent evidence: each candidate's pattern text as authored plus its resolve
200 // mode, displacement / instruction-length offsets, and uniqueness flag. The pattern is hashed as written rather
201 // than re-parsed to canonical bytes -- that keeps the fingerprint allocation-free and total (a malformed
202 // pattern still hashes), and a cross-version diff reuses the same static anchor table verbatim, so two textual
203 // spellings of one signature never arise in practice. The candidate's cosmetic name is excluded because it
204 // never affects which address the cascade resolves.
205 18 [[nodiscard]] std::uint64_t fnv1a_cascade(std::uint64_t hash,
206 std::span<const Scanner::AddrCandidate> site) noexcept
207 {
208 18 hash = fnv1a_int(hash, site.size());
209
2/2
✓ Branch 22 → 6 taken 20 times.
✓ Branch 22 → 23 taken 18 times.
56 for (const Scanner::AddrCandidate &candidate : site)
210 {
211 20 hash = fnv1a_field(hash, candidate.pattern);
212 20 hash = fnv1a_byte(hash, static_cast<std::uint8_t>(candidate.mode));
213 20 hash = fnv1a_int(hash, static_cast<std::int64_t>(candidate.disp_offset));
214 20 hash = fnv1a_int(hash, static_cast<std::int64_t>(candidate.instr_end_offset));
215 20 hash = fnv1a_byte(hash, static_cast<std::uint8_t>(candidate.require_unique));
216 }
217 18 return hash;
218 }
219
220 // Hashes one anchor's own evidence with no quorum recursion. A Quorum reaching here -- which the public entry
221 // point only allows for a malformed sub-anchor, since nesting is rejected at resolve time -- contributes only
222 // its kind, which bounds recursion to a single level.
223 52 [[nodiscard]] std::uint64_t fingerprint_evidence(const Anchors::Anchor &anchor) noexcept
224 {
225 using Anchors::AnchorKind;
226 52 std::uint64_t hash = fnv1a_byte(FNV1A64_OFFSET, static_cast<std::uint8_t>(anchor.kind));
227
6/7
✓ Branch 3 → 4 taken 8 times.
✓ Branch 3 → 6 taken 11 times.
✓ Branch 3 → 8 taken 7 times.
✓ Branch 3 → 13 taken 10 times.
✓ Branch 3 → 19 taken 14 times.
✓ Branch 3 → 21 taken 2 times.
✗ Branch 3 → 22 not taken.
52 switch (anchor.kind)
228 {
229 8 case AnchorKind::VtableIdentity:
230 8 hash = fnv1a_field(hash, anchor.mangled);
231 8 break;
232 11 case AnchorKind::RipGlobal:
233 11 hash = fnv1a_cascade(hash, anchor.site);
234 11 break;
235 7 case AnchorKind::CodeOperand:
236 7 hash = fnv1a_cascade(hash, anchor.site);
237 7 hash = fnv1a_byte(hash, static_cast<std::uint8_t>(anchor.operand_kind));
238 7 hash = fnv1a_byte(hash, anchor.operand_index);
239 7 hash = fnv1a_byte(hash, anchor.byte_width);
240 7 break;
241 10 case AnchorKind::StringXref:
242 10 hash = fnv1a_field(hash, anchor.xref_text);
243 10 hash = fnv1a_byte(hash, static_cast<std::uint8_t>(anchor.xref_encoding));
244 10 hash = fnv1a_byte(hash, static_cast<std::uint8_t>(anchor.xref_return));
245 10 hash = fnv1a_byte(hash, static_cast<std::uint8_t>(anchor.xref_require_terminator));
246 10 hash = fnv1a_byte(hash, static_cast<std::uint8_t>(anchor.xref_broad_match));
247 10 break;
248 14 case AnchorKind::Manual:
249 14 hash = fnv1a_int(hash, anchor.manual_value);
250 14 break;
251 2 case AnchorKind::CallArgHome:
252 case AnchorKind::Quorum:
253 // No address-independent evidence beyond the kind byte already folded above.
254 2 break;
255 }
256 52 return hash;
257 }
258 } // anonymous namespace
259
260 95 Anchors::ResolvedAnchor Anchors::resolve_with_profile(const Anchor &anchor, const ScanProfile &profile,
261 Memory::ModuleRange range)
262 {
263 95 ResolvedAnchor result{};
264 95 result.label = anchor.label;
265 95 result.kind = anchor.kind;
266 95 result.status = AnchorStatus::Unresolved;
267 95 result.value = 0;
268
269 // Backend deny-list: a denied kind fails closed before any scan. It is never silently replaced by another
270 // backend, which would risk returning a different, wrong target. An empty profile (the default resolve() path)
271 // denies nothing, so this is a no-op there.
272
2/2
✓ Branch 3 → 4 taken 6 times.
✓ Branch 3 → 5 taken 89 times.
95 if (profile.is_denied(anchor.kind))
273 {
274 6 result.status = AnchorStatus::Failed;
275 6 return result;
276 }
277
278
7/8
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 14 taken 13 times.
✓ Branch 5 → 24 taken 24 times.
✓ Branch 5 → 34 taken 2 times.
✓ Branch 5 → 46 taken 26 times.
✓ Branch 5 → 50 taken 5 times.
✓ Branch 5 → 51 taken 18 times.
✗ Branch 5 → 72 not taken.
89 switch (anchor.kind)
279 {
280 1 case AnchorKind::VtableIdentity:
281 {
282 1 const auto vtable = Rtti::vtable_for_type(anchor.mangled, range);
283
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 11 taken 1 time.
1 if (vtable)
284 {
285 commit_resolved(anchor, result, static_cast<std::int64_t>(*vtable));
286 }
287 else
288 {
289 1 result.status = AnchorStatus::Failed;
290 }
291 1 break;
292 }
293 13 case AnchorKind::RipGlobal:
294 {
295 // The cascade itself selects Direct vs RIP-relative per candidate, so a plain global address and a
296 // RIP-relative one share this backend.
297 13 std::vector<Scanner::AddrCandidate> ordered_site;
298
1/2
✓ Branch 14 → 15 taken 13 times.
✗ Branch 14 → 74 not taken.
13 const auto site = profiled_candidates(profile, anchor.site, ordered_site);
299
1/2
✓ Branch 15 → 16 taken 13 times.
✗ Branch 15 → 74 not taken.
13 const auto hit = Scanner::resolve_cascade_in_module(site, anchor.label, range);
300
2/2
✓ Branch 17 → 18 taken 9 times.
✓ Branch 17 → 20 taken 4 times.
13 if (hit)
301 {
302 9 commit_resolved(anchor, result, static_cast<std::int64_t>(hit->address));
303 }
304 else
305 {
306 4 result.status = AnchorStatus::Failed;
307 }
308 13 break;
309 13 }
310 24 case AnchorKind::CodeOperand:
311 {
312 24 std::vector<Scanner::AddrCandidate> ordered_site;
313 24 Scanner::CodeConstant code_constant{};
314
1/2
✓ Branch 24 → 25 taken 24 times.
✗ Branch 24 → 77 not taken.
24 code_constant.site = profiled_candidates(profile, anchor.site, ordered_site);
315 24 code_constant.kind = anchor.operand_kind;
316 24 code_constant.operand_index = anchor.operand_index;
317 24 code_constant.byte_width = anchor.byte_width;
318
1/2
✓ Branch 25 → 26 taken 24 times.
✗ Branch 25 → 77 not taken.
24 const auto constant = Scanner::read_code_constant(code_constant, range);
319
1/2
✓ Branch 27 → 28 taken 24 times.
✗ Branch 27 → 30 not taken.
24 if (constant)
320 {
321 24 commit_resolved(anchor, result, *constant);
322 }
323 else
324 {
325 result.status = AnchorStatus::Failed;
326 }
327 24 break;
328 24 }
329 2 case AnchorKind::StringXref:
330 {
331 // Anchor on an immutable string literal, then resolve the instruction (or enclosing function) that
332 // references it. The string survives game updates far better than the surrounding code, so this is the most
333 // update-resilient backend; it fails closed on a missing, duplicated, or unreferenced string.
334 2 Scanner::StringRefQuery query{};
335 2 query.text = anchor.xref_text;
336 2 query.encoding = anchor.xref_encoding;
337 2 query.require_terminator = anchor.xref_require_terminator;
338 2 query.return_mode = anchor.xref_return;
339 // The profile can only widen the broad sweep on (never off); a per-anchor xref_broad_match still wins.
340
2/4
✓ Branch 34 → 35 taken 2 times.
✗ Branch 34 → 36 not taken.
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 37 taken 2 times.
2 query.broad_match = anchor.xref_broad_match || profile.default_broad_string_xref;
341
1/2
✓ Branch 38 → 39 taken 2 times.
✗ Branch 38 → 80 not taken.
2 const auto site = Scanner::find_string_xref(query, range);
342
2/2
✓ Branch 40 → 41 taken 1 time.
✓ Branch 40 → 43 taken 1 time.
2 if (site)
343 {
344 1 commit_resolved(anchor, result, static_cast<std::int64_t>(*site));
345 }
346 else
347 {
348 1 result.status = AnchorStatus::Failed;
349 }
350 2 break;
351 }
352 26 case AnchorKind::Manual:
353 // A pinned literal always "resolves"; a report should still flag it as at-risk (it cannot self-heal) by
354 // inspecting the kind. By default the validator is skipped (the pinned-literal exemption); a caller that
355 // opts in via validate_manual routes the literal through the same fail-closed validator path as a backend.
356
2/2
✓ Branch 46 → 47 taken 2 times.
✓ Branch 46 → 48 taken 24 times.
26 if (anchor.validate_manual)
357 {
358 2 commit_resolved(anchor, result, anchor.manual_value);
359 }
360 else
361 {
362 24 result.value = anchor.manual_value;
363 24 result.status = AnchorStatus::Resolved;
364 }
365 26 break;
366 5 case AnchorKind::CallArgHome:
367 // Reserved for a future prologue-dataflow backend; no resolver yet.
368 5 result.status = AnchorStatus::Unsupported;
369 5 break;
370 18 case AnchorKind::Quorum:
371 {
372 // A critical target accepts only when two independent signals corroborate. Fail closed on a malformed
373 // declaration (a missing sub-anchor, or a sub-anchor that is itself a Quorum) exactly as the single-signal
374 // backends fail closed on ambiguity; rejecting nested Quorum bounds the recursion to one level.
375 18 const Anchor *first = anchor.quorum_a;
376 18 const Anchor *second = anchor.quorum_b;
377
6/8
✓ Branch 51 → 52 taken 18 times.
✗ Branch 51 → 55 not taken.
✓ Branch 52 → 53 taken 17 times.
✓ Branch 52 → 55 taken 1 time.
✓ Branch 53 → 54 taken 16 times.
✓ Branch 53 → 55 taken 1 time.
✗ Branch 54 → 55 not taken.
✓ Branch 54 → 56 taken 16 times.
18 if (!first || !second || first->kind == AnchorKind::Quorum || second->kind == AnchorKind::Quorum)
378 {
379 2 result.status = AnchorStatus::Failed;
380 2 break;
381 }
382 // Independence is a static property of the declaration, so check it before the (potentially expensive)
383 // recursive resolves. A dependent pair (same object, dual Manual, or same backend + inputs) is not
384 // corroboration; report it precisely instead of letting two scans of one site agree and look corroborated.
385
2/2
✓ Branch 57 → 58 taken 3 times.
✓ Branch 57 → 59 taken 13 times.
16 if (!quorum_sub_anchors_independent(*first, *second))
386 {
387 3 result.status = AnchorStatus::QuorumNotIndependent;
388 3 break;
389 }
390
391 // Recurse with the same profile so a denied sub-anchor kind (or a profile broad-default) threads down and a
392 // denied signal fails the quorum closed.
393
1/2
✓ Branch 59 → 60 taken 13 times.
✗ Branch 59 → 81 not taken.
13 const ResolvedAnchor resolved_first = resolve_with_profile(*first, profile, range);
394
1/2
✓ Branch 60 → 61 taken 13 times.
✗ Branch 60 → 81 not taken.
13 const ResolvedAnchor resolved_second = resolve_with_profile(*second, profile, range);
395
8/8
✓ Branch 61 → 62 taken 12 times.
✓ Branch 61 → 66 taken 1 time.
✓ Branch 62 → 63 taken 11 times.
✓ Branch 62 → 66 taken 1 time.
✓ Branch 64 → 65 taken 8 times.
✓ Branch 64 → 66 taken 3 times.
✓ Branch 67 → 68 taken 8 times.
✓ Branch 67 → 69 taken 5 times.
24 if (resolved_first.status == AnchorStatus::Resolved && resolved_second.status == AnchorStatus::Resolved &&
396 11 quorum_values_agree(resolved_first.value, resolved_second.value, anchor.quorum_match,
397 11 anchor.quorum_tolerance))
398 {
399 // Both agree under the policy. Commit through the same path as the single-signal backends so the Quorum
400 // anchor's own validator runs on the corroborated value (each sub-anchor's validator already ran in its
401 // recursive resolve).
402 8 commit_resolved(anchor, result, resolved_first.value);
403 }
404 else
405 {
406 5 result.status = AnchorStatus::Failed;
407 }
408 13 break;
409 }
410 }
411
412 89 return result;
413 }
414
415 54 Anchors::ResolvedAnchor Anchors::resolve(const Anchor &anchor, Memory::ModuleRange range)
416 {
417 // An empty profile denies nothing and widens nothing, so this is exactly the un-profiled resolution.
418
1/2
✓ Branch 2 → 3 taken 54 times.
✗ Branch 2 → 6 not taken.
54 return resolve_with_profile(anchor, ScanProfile{}, range);
419 }
420
421 4 std::size_t Anchors::resolve_all(std::span<const Anchor> anchors, std::span<ResolvedAnchor> out,
422 Memory::ModuleRange range)
423 {
424
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 4 times.
4 const std::size_t count = (anchors.size() < out.size()) ? anchors.size() : out.size();
425
2/2
✓ Branch 12 → 8 taken 11 times.
✓ Branch 12 → 13 taken 4 times.
15 for (std::size_t i = 0; i < count; ++i)
426 {
427 11 out[i] = resolve(anchors[i], range);
428 }
429 4 return count;
430 }
431
432 1 std::size_t Anchors::resolve_all_parallel(std::span<const Anchor> anchors, std::span<ResolvedAnchor> out,
433 Memory::ModuleRange range, std::size_t max_workers)
434 {
435
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();
436 const auto results = DetourModKit::detail::run_fork_join<Anchor, ResolvedAnchor>(
437 anchors.first(count), max_workers,
438 5 [range](const Anchor &anchor) -> ResolvedAnchor { return resolve(anchor, range); },
439
1/2
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 17 not taken.
6 [](const Anchor &anchor) noexcept -> ResolvedAnchor { return failed_anchor_result(anchor); });
440
441
2/2
✓ Branch 13 → 10 taken 5 times.
✓ Branch 13 → 14 taken 1 time.
6 for (std::size_t i = 0; i < count; ++i)
442 {
443 5 out[i] = results[i];
444 }
445 1 return count;
446 1 }
447
448 2 std::size_t Anchors::resolve_all_with_profile(std::span<const Anchor> anchors, std::span<ResolvedAnchor> out,
449 const ScanProfile &profile, Memory::ModuleRange range)
450 {
451
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();
452
2/2
✓ Branch 12 → 8 taken 5 times.
✓ Branch 12 → 13 taken 2 times.
7 for (std::size_t i = 0; i < count; ++i)
453 {
454 5 out[i] = resolve_with_profile(anchors[i], profile, range);
455 }
456 2 return count;
457 }
458
459 1 std::size_t Anchors::resolve_all_with_profile_parallel(std::span<const Anchor> anchors,
460 std::span<ResolvedAnchor> out, const ScanProfile &profile,
461 Memory::ModuleRange range, std::size_t max_workers)
462 {
463
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();
464 const auto results = DetourModKit::detail::run_fork_join<Anchor, ResolvedAnchor>(
465 3 anchors.first(count), max_workers, [&profile, range](const Anchor &anchor) -> ResolvedAnchor
466 3 { return resolve_with_profile(anchor, profile, range); },
467
1/2
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 17 not taken.
4 [](const Anchor &anchor) noexcept -> ResolvedAnchor { return failed_anchor_result(anchor); });
468
469
2/2
✓ Branch 13 → 10 taken 3 times.
✓ Branch 13 → 14 taken 1 time.
4 for (std::size_t i = 0; i < count; ++i)
470 {
471 3 out[i] = results[i];
472 }
473 1 return count;
474 1 }
475
476 7 Anchors::AnchorQuality Anchors::assess_quality(std::span<const ResolvedAnchor> report) noexcept
477 {
478 7 AnchorQuality quality{};
479 7 quality.total = report.size();
480
2/2
✓ Branch 27 → 5 taken 10 times.
✓ Branch 27 → 28 taken 7 times.
24 for (const ResolvedAnchor &entry : report)
481 {
482
3/6
✓ Branch 7 → 8 taken 6 times.
✓ Branch 7 → 9 taken 3 times.
✗ Branch 7 → 10 not taken.
✓ Branch 7 → 11 taken 1 time.
✗ Branch 7 → 12 not taken.
✗ Branch 7 → 13 not taken.
10 switch (entry.status)
483 {
484 6 case AnchorStatus::Resolved:
485 6 ++quality.resolved;
486 6 break;
487 3 case AnchorStatus::Failed:
488 3 ++quality.failed;
489 3 break;
490 case AnchorStatus::Unsupported:
491 ++quality.unsupported;
492 break;
493 1 case AnchorStatus::QuorumNotIndependent:
494 1 ++quality.not_independent;
495 1 break;
496 case AnchorStatus::Unresolved:
497 break;
498 }
499 // A pinned literal is at-risk regardless of status: it "resolves" but cannot self-heal across a patch.
500
2/2
✓ Branch 13 → 14 taken 3 times.
✓ Branch 13 → 15 taken 7 times.
10 if (entry.kind == AnchorKind::Manual)
501 {
502 3 ++quality.manual_at_risk;
503 }
504 // A corroborated quorum is the strongest evidence: two independent signals had to agree.
505
3/4
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 18 taken 9 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 1 time.
10 if (entry.kind == AnchorKind::Quorum && entry.status == AnchorStatus::Resolved)
506 {
507 ++quality.corroborated;
508 }
509 }
510 7 return quality;
511 }
512
513 48 std::uint64_t Anchors::anchor_fingerprint(const Anchor &anchor) noexcept
514 {
515
2/2
✓ Branch 2 → 3 taken 40 times.
✓ Branch 2 → 4 taken 8 times.
48 if (anchor.kind != AnchorKind::Quorum)
516 {
517 40 return fingerprint_evidence(anchor);
518 }
519
520 // A quorum's evidence is its two sub-anchors, combined order-independently (the resolver treats the pair
521 // symmetrically when checking agreement, so swapping quorum_a / quorum_b must not change the fingerprint). A
522 // null sub-anchor -- which fails closed at resolve time -- contributes a fixed sentinel so the result stays
523 // defined rather than dereferencing through nullptr.
524 8 constexpr std::uint64_t NULL_SUB_ANCHOR = 0;
525
2/2
✓ Branch 4 → 5 taken 6 times.
✓ Branch 4 → 6 taken 2 times.
8 const std::uint64_t fp_a = anchor.quorum_a ? fingerprint_evidence(*anchor.quorum_a) : NULL_SUB_ANCHOR;
526
2/2
✓ Branch 7 → 8 taken 6 times.
✓ Branch 7 → 9 taken 2 times.
8 const std::uint64_t fp_b = anchor.quorum_b ? fingerprint_evidence(*anchor.quorum_b) : NULL_SUB_ANCHOR;
527
2/2
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 7 times.
8 const std::uint64_t lo = fp_a < fp_b ? fp_a : fp_b;
528
2/2
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 15 taken 7 times.
8 const std::uint64_t hi = fp_a < fp_b ? fp_b : fp_a;
529
530 8 std::uint64_t hash = fnv1a_byte(FNV1A64_OFFSET, static_cast<std::uint8_t>(AnchorKind::Quorum));
531 8 hash = fnv1a_int(hash, lo);
532 8 hash = fnv1a_int(hash, hi);
533 8 hash = fnv1a_byte(hash, static_cast<std::uint8_t>(anchor.quorum_match));
534 8 hash = fnv1a_int(hash, anchor.quorum_tolerance);
535 8 return hash;
536 }
537
538 6 std::string_view Anchors::anchor_status_to_string(AnchorStatus status) noexcept
539 {
540
5/6
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 1 time.
✓ Branch 2 → 5 taken 1 time.
✓ Branch 2 → 6 taken 1 time.
✓ Branch 2 → 7 taken 2 times.
✗ Branch 2 → 8 not taken.
6 switch (status)
541 {
542 1 case AnchorStatus::Unresolved:
543 1 return "Unresolved";
544 1 case AnchorStatus::Resolved:
545 1 return "Resolved";
546 1 case AnchorStatus::Failed:
547 1 return "Failed";
548 1 case AnchorStatus::Unsupported:
549 1 return "Unsupported";
550 2 case AnchorStatus::QuorumNotIndependent:
551 2 return "QuorumNotIndependent";
552 }
553 return "Unknown";
554 }
555 } // namespace DetourModKit
556