GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 97.1% 402 / 0 / 414
Functions: 100.0% 32 / 0 / 32
Branches: 81.6% 337 / 0 / 413

src/manifest_overlay.cpp
Line Branch Exec Source
1 /**
2 * @file manifest_overlay.cpp
3 * @brief This TU implements Signature compile/adopt, the default-overlay merge, and the resolve-time trust gate.
4 * @details Record validation comes from internal/manifest_record_rules.hpp, so this TU and the parse-serialize TU
5 * (manifest.cpp) enforce one rule set. No INI machinery appears here.
6 */
7
8 #include "DetourModKit/manifest.hpp"
9
10 #include "DetourModKit/logger.hpp"
11
12 #include "internal/anchor_resolution.hpp"
13 #include "internal/manifest_record_rules.hpp"
14 #include "internal/memory_guarded.hpp"
15
16 #include <algorithm>
17 #include <array>
18 #include <cstddef>
19 #include <cstdint>
20 #include <span>
21 #include <string>
22 #include <string_view>
23 #include <type_traits>
24 #include <utility>
25 #include <vector>
26
27 namespace DetourModKit::manifest
28 {
29 namespace
30 {
31 // Compiles one CandidateSpec into the scan::Candidate for a Signature ladder. An unset or malformed rung fails
32 // closed. Only Signature::compile calls this helper. adopt copies an already-resolved ladder.
33 69 [[nodiscard]] Result<scan::Candidate> compile_rung(const CandidateSpec &spec)
34 {
35
3/5
✓ Branch 2 → 3 taken 61 times.
✓ Branch 2 → 19 taken 7 times.
✗ Branch 2 → 47 not taken.
✓ Branch 2 → 55 taken 1 time.
✗ Branch 2 → 62 not taken.
69 switch (spec.mode)
36 {
37 61 case scan::Mode::Direct:
38 {
39 61 const Result<scan::Pattern> pattern = scan::Pattern::compile(spec.pattern);
40
2/2
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 11 taken 59 times.
61 if (!pattern)
41 {
42 2 return std::unexpected(pattern.error());
43 }
44
2/4
✓ Branch 12 → 13 taken 59 times.
✗ Branch 12 → 68 not taken.
✓ Branch 13 → 14 taken 59 times.
✗ Branch 13 → 66 not taken.
59 return scan::Candidate::direct(spec.name, *pattern, spec.walk_back);
45 }
46 7 case scan::Mode::RipRelative:
47 {
48 7 const Result<scan::Pattern> pattern = scan::Pattern::compile(spec.pattern);
49
1/2
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 27 taken 7 times.
7 if (!pattern)
50 {
51 return std::unexpected(pattern.error());
52 }
53 // parse_rung guards the file path. Apply the same fail-closed RipRelative constraint here so the
54 // programmatic Signature::compile path cannot bypass it. This explicit check reports InvalidArg through
55 // Result instead of an exception from the factory.
56 21 if (spec.displacement_at < 0 ||
57
2/2
✓ Branch 29 → 30 taken 3 times.
✓ Branch 29 → 33 taken 4 times.
7 !scan::is_valid_rip_relative_layout(
58 7 static_cast<std::size_t>(spec.displacement_at),
59 7 spec.instruction_length
60
3/4
✓ Branch 27 → 28 taken 7 times.
✗ Branch 27 → 33 not taken.
✓ Branch 35 → 36 taken 5 times.
✓ Branch 35 → 39 taken 2 times.
14 ) ||
61
2/2
✓ Branch 32 → 33 taken 1 time.
✓ Branch 32 → 34 taken 2 times.
3 !rip_pattern_spans_displacement(*pattern, static_cast<std::size_t>(spec.displacement_at)))
62 {
63 5 return fail(ErrorCode::InvalidArg, "manifest::compile");
64 }
65
1/2
✓ Branch 41 → 42 taken 2 times.
✗ Branch 41 → 71 not taken.
4 return scan::Candidate::rip_relative(
66
1/2
✓ Branch 40 → 41 taken 2 times.
✗ Branch 40 → 73 not taken.
2 spec.name,
67 2 *pattern,
68 2 spec.displacement_at,
69 2 spec.instruction_length
70 2 );
71 }
72 case scan::Mode::RttiVtable:
73 return scan::Candidate::rtti_vtable(spec.name, spec.mangled);
74 1 case scan::Mode::StringXref:
75 {
76 const scan::StringRefQuery query{
77 1 .text = spec.string_text,
78 1 .encoding = spec.string_encoding,
79 1 .require_terminator = spec.string_require_terminator,
80 1 .return_mode = spec.string_return,
81 1 .broad_match = spec.string_broad_match,
82 1 };
83
2/4
✓ Branch 56 → 57 taken 1 time.
✗ Branch 56 → 85 not taken.
✓ Branch 57 → 58 taken 1 time.
✗ Branch 57 → 83 not taken.
1 return scan::Candidate::string_xref(spec.name, query);
84 }
85 }
86 return fail(ErrorCode::BadPattern, "manifest::compile");
87 }
88
89 // Reports whether a binding can authorize a write against a resolved typed domain. MidHook needs an executable
90 // code site. VmtMethod needs a vtable. Address and PointerChain accept only code or data addresses. Scalar,
91 // Unknown, and out-of-range kinds authorize nothing.
92 [[nodiscard]] constexpr bool
93 27 binding_authorizes_mutation(BindingKind binding_kind, anchor::ResultDomain domain) noexcept
94 {
95
2/4
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 3 times.
✓ Branch 2 → 5 taken 24 times.
✗ Branch 2 → 10 not taken.
27 switch (binding_kind)
96 {
97 case BindingKind::VmtMethod:
98 return domain == anchor::ResultDomain::VtableAddress;
99 3 case BindingKind::MidHookRegister:
100 3 return domain == anchor::ResultDomain::CodeSite;
101 24 case BindingKind::Address:
102 case BindingKind::PointerChain:
103
3/4
✓ Branch 5 → 6 taken 18 times.
✓ Branch 5 → 7 taken 6 times.
✓ Branch 6 → 7 taken 18 times.
✗ Branch 6 → 8 not taken.
24 return domain == anchor::ResultDomain::CodeSite || domain == anchor::ResultDomain::DataAddress;
104 }
105 return false;
106 }
107
108 // Extends anchor_fingerprint with the Binding contract through FNV-1a. It uses the endian-independent and
109 // length-prefixed discipline from anchor.cpp. The result stays stable across runs and builds. Keep these
110 // helpers local because the anchor.cpp FNV primitives are private.
111 inline constexpr std::uint64_t FNV1A64_PRIME = 1099511628211ULL;
112
113 3204 [[nodiscard]] std::uint64_t fnv1a_fold_byte(std::uint64_t hash, std::uint8_t value) noexcept
114 {
115 3204 return (hash ^ value) * FNV1A64_PRIME;
116 }
117
118 291 template <typename T> [[nodiscard]] std::uint64_t fnv1a_fold_int(std::uint64_t hash, T value) noexcept
119 {
120 291 auto bits = static_cast<std::uint64_t>(static_cast<std::make_unsigned_t<T>>(value));
121
4/4
unsigned long long DetourModKit::manifest::(anonymous namespace)::fnv1a_fold_int<long long>(unsigned long long, long long):
✓ Branch 5 → 3 taken 56 times.
✓ Branch 5 → 6 taken 7 times.
unsigned long long DetourModKit::manifest::(anonymous namespace)::fnv1a_fold_int<unsigned long long>(unsigned long long, unsigned long long):
✓ Branch 5 → 3 taken 2272 times.
✓ Branch 5 → 6 taken 284 times.
2619 for (std::size_t i = 0; i < sizeof(T); ++i)
122 {
123 2328 hash = fnv1a_fold_byte(hash, static_cast<std::uint8_t>(bits & 0xFFu));
124 2328 bits >>= 8;
125 }
126 291 return hash;
127 }
128
129 // A length prefix prevents collisions between "ab" and two folded fields "a" plus "b".
130 142 [[nodiscard]] std::uint64_t fnv1a_fold_string(std::uint64_t hash, std::string_view text) noexcept
131 {
132 142 hash = fnv1a_fold_int(hash, static_cast<std::uint64_t>(text.size()));
133
2/2
✓ Branch 8 → 6 taken 592 times.
✓ Branch 8 → 9 taken 142 times.
734 for (const char c : text)
134 {
135 592 hash = fnv1a_fold_byte(hash, static_cast<std::uint8_t>(c));
136 }
137 142 return hash;
138 }
139
140 // Fold every field, not only those read by the active BindingKind. Any binding edit then reports drift. An
141 // inert field edit also reports drift, which is the fail-closed direction.
142 71 [[nodiscard]] std::uint64_t fold_binding(std::uint64_t hash, const Binding &binding) noexcept
143 {
144 71 hash = fnv1a_fold_byte(hash, static_cast<std::uint8_t>(binding.kind));
145 71 hash = fnv1a_fold_int(hash, static_cast<std::uint64_t>(binding.offsets.size()));
146
2/2
✓ Branch 19 → 7 taken 7 times.
✓ Branch 19 → 20 taken 71 times.
149 for (const std::ptrdiff_t offset : binding.offsets)
147 {
148 7 hash = fnv1a_fold_int(hash, static_cast<std::int64_t>(offset));
149 }
150 71 hash = fnv1a_fold_byte(hash, binding.value_width);
151 71 hash = fnv1a_fold_byte(hash, static_cast<std::uint8_t>(binding.read_register));
152 71 hash = fnv1a_fold_byte(hash, binding.xmm_index);
153 71 return fnv1a_fold_int(hash, static_cast<std::uint64_t>(binding.vmt_index));
154 }
155 } // namespace
156
157 168 Signature::Signature(SignatureRecord record, std::vector<scan::Candidate> ladder) noexcept
158 504 : m_record(std::move(record)), m_ladder(std::move(ladder))
159 {
160 168 }
161
162 156 anchor::Anchor Signature::make_anchor() const noexcept
163 {
164 // Rebuild a borrowed anchor view over this object's owned storage. View and POD assignments require no
165 // allocation. The returned string_views alias m_record strings. Its site span aliases m_ladder. Those aliases
166 // remain valid for the resolve or fingerprint call that receives this view.
167 156 anchor::Anchor anchor{};
168 156 anchor.label = m_record.label;
169 156 anchor.kind = m_record.kind;
170 156 anchor.mangled = m_record.mangled;
171 156 anchor.site = m_ladder;
172 156 anchor.operand_kind = m_record.operand_kind;
173 156 anchor.operand_index = m_record.operand_index;
174 156 anchor.byte_width = m_record.byte_width;
175 156 anchor.pages = m_record.pages;
176 156 anchor.xref_text = m_record.xref_text;
177 156 anchor.xref_encoding = m_record.xref_encoding;
178 156 anchor.xref_return = m_record.xref_return;
179 156 anchor.xref_require_terminator = m_record.xref_require_terminator;
180 156 anchor.xref_broad_match = m_record.xref_broad_match;
181 // ExportName evidence consists of the export symbol and its module. resolve() also uses the shared module field
182 // as its scope. anchor_fingerprint folds export_module only for ExportName evidence. current_fingerprint folds
183 // record.module for every kind.
184 156 anchor.export_module = m_record.module;
185 156 anchor.export_name = m_record.export_name;
186 156 anchor.manual_value = m_record.manual_value;
187 // Add the post-resolve validator to the borrowed view. A compiled signature can then assert the same domain
188 // invariant as an in-code Anchor. Without these fields, the manifest path cannot reach a validator and silently
189 // trusts the raw backend address.
190 156 anchor.validator = m_record.validator;
191 156 anchor.validator_context = m_record.validator_context;
192 156 anchor.validate_manual = m_record.validate_manual;
193 156 anchor.require_validator = m_record.require_validator;
194 156 return anchor;
195 }
196
197 244 Result<Signature> Signature::compile(SignatureRecord record)
198 {
199 // Composite and unset kinds have no flat record form. A trusted zero from Unset is the fail-open case that this
200 // check rejects. Quorum and CallArgHome remain in-code anchors under evaluate_gate().
201
2/2
✓ Branch 3 → 4 taken 8 times.
✓ Branch 3 → 7 taken 236 times.
244 if (!record_policy_domains_are_valid(record))
202 {
203 8 return fail(ErrorCode::InvalidArg, "manifest::compile");
204 }
205
4/4
✓ Branch 8 → 9 taken 235 times.
✓ Branch 8 → 11 taken 1 time.
✓ Branch 13 → 14 taken 4 times.
✓ Branch 13 → 17 taken 232 times.
471 if (!image_identity_is_valid(record.expected_image_identity) ||
206
2/2
✓ Branch 10 → 11 taken 3 times.
✓ Branch 10 → 12 taken 232 times.
235 !winning_bytes_are_valid(record.expected_winning_bytes))
207 {
208 4 return fail(ErrorCode::InvalidArg, "manifest::compile");
209 }
210
211 // A label that cannot round-trip as a `[sig.<label>]` section (a structural INI character, or the reserved
212 // `.rung.<digits>` grammar) fails closed before Signature construction. The checked encoder cannot faithfully
213 // persist such a label.
214
3/4
✓ Branch 18 → 19 taken 232 times.
✗ Branch 18 → 175 not taken.
✓ Branch 19 → 20 taken 8 times.
✓ Branch 19 → 23 taken 224 times.
232 if (!label_is_serializable(record.label))
215 {
216 8 return fail(ErrorCode::InvalidArg, "manifest::compile");
217 }
218
219
4/4
✓ Branch 28 → 29 taken 218 times.
✓ Branch 28 → 38 taken 3 times.
✓ Branch 31 → 32 taken 206 times.
✓ Branch 31 → 38 taken 12 times.
663 if (value_is_unserializable(record.module) || value_is_unserializable(record.mangled) ||
220
8/8
✓ Branch 25 → 26 taken 221 times.
✓ Branch 25 → 38 taken 3 times.
✓ Branch 34 → 35 taken 203 times.
✓ Branch 34 → 38 taken 3 times.
✓ Branch 37 → 38 taken 11 times.
✓ Branch 37 → 39 taken 192 times.
✓ Branch 40 → 41 taken 32 times.
✓ Branch 40 → 44 taken 192 times.
866 value_is_unserializable(record.xref_text) || value_is_unserializable(record.export_name) ||
221 203 xref_evidence_is_malformed(record.xref_text, record.xref_encoding))
222 {
223 32 return fail(ErrorCode::InvalidArg, "manifest::compile");
224 }
225
2/2
✓ Branch 78 → 46 taken 89 times.
✓ Branch 78 → 79 taken 172 times.
453 for (const CandidateSpec &rung : record.ladder)
226 {
227
4/4
✓ Branch 53 → 54 taken 83 times.
✓ Branch 53 → 63 taken 3 times.
✓ Branch 56 → 57 taken 80 times.
✓ Branch 56 → 63 taken 3 times.
258 if (value_is_unserializable(rung.name) || value_is_unserializable(rung.pattern) ||
228
8/8
✓ Branch 50 → 51 taken 86 times.
✓ Branch 50 → 63 taken 3 times.
✓ Branch 59 → 60 taken 77 times.
✓ Branch 59 → 63 taken 3 times.
✓ Branch 62 → 63 taken 8 times.
✓ Branch 62 → 64 taken 69 times.
✓ Branch 65 → 66 taken 20 times.
✓ Branch 65 → 69 taken 69 times.
335 value_is_unserializable(rung.mangled) || value_is_unserializable(rung.string_text) ||
229 77 xref_evidence_is_malformed(rung.string_text, rung.string_encoding))
230 {
231 20 return fail(ErrorCode::InvalidArg, "manifest::compile");
232 }
233 }
234
235 // Each resolvable kind fails closed on empty mandatory evidence, so a hand-built record cannot compile into
236 // a Signature that overlays a trusted zero. Manual has no "empty" evidence (any int64 is a valid pin).
237
6/6
✓ Branch 79 → 80 taken 7 times.
✓ Branch 79 → 83 taken 165 times.
✓ Branch 81 → 82 taken 1 time.
✓ Branch 81 → 83 taken 6 times.
✓ Branch 84 → 85 taken 1 time.
✓ Branch 84 → 88 taken 171 times.
172 if (record.kind == anchor::AnchorKind::VtableIdentity && record.mangled.empty())
238 {
239 1 return fail(ErrorCode::InvalidArg, "manifest::compile");
240 }
241
6/6
✓ Branch 88 → 89 taken 19 times.
✓ Branch 88 → 92 taken 152 times.
✓ Branch 90 → 91 taken 1 time.
✓ Branch 90 → 92 taken 18 times.
✓ Branch 93 → 94 taken 1 time.
✓ Branch 93 → 97 taken 170 times.
171 if (record.kind == anchor::AnchorKind::StringXref && record.xref_text.empty())
242 {
243 1 return fail(ErrorCode::InvalidArg, "manifest::compile");
244 }
245 // An ExportName without an export symbol has no resolution evidence. An empty module name uses the fallback
246 // scope, such as a host executable export. Only export_name is mandatory.
247
6/6
✓ Branch 97 → 98 taken 13 times.
✓ Branch 97 → 101 taken 157 times.
✓ Branch 99 → 100 taken 2 times.
✓ Branch 99 → 101 taken 11 times.
✓ Branch 102 → 103 taken 2 times.
✓ Branch 102 → 106 taken 168 times.
170 if (record.kind == anchor::AnchorKind::ExportName && record.export_name.empty())
248 {
249 2 return fail(ErrorCode::InvalidArg, "manifest::compile");
250 }
251
252 // Reject a binding that the consumer primitive cannot interpret safely.
253
2/2
✓ Branch 107 → 108 taken 10 times.
✓ Branch 107 → 111 taken 158 times.
168 if (!binding_structure_is_valid(record.binding))
254 {
255 10 return fail(ErrorCode::InvalidArg, "manifest::compile");
256 }
257
258 158 std::vector<scan::Candidate> ladder;
259 158 const bool uses_ladder =
260
4/4
✓ Branch 111 → 112 taken 96 times.
✓ Branch 111 → 113 taken 62 times.
✓ Branch 112 → 113 taken 2 times.
✓ Branch 112 → 114 taken 94 times.
158 record.kind == anchor::AnchorKind::RipGlobal || record.kind == anchor::AnchorKind::CodeOperand;
261
2/2
✓ Branch 115 → 116 taken 64 times.
✓ Branch 115 → 154 taken 94 times.
158 if (uses_ladder)
262 {
263
2/2
✓ Branch 117 → 118 taken 2 times.
✓ Branch 117 → 121 taken 62 times.
64 if (record.ladder.empty())
264 {
265 2 return fail(ErrorCode::EmptyCandidates, "manifest::compile");
266 }
267
1/2
✓ Branch 122 → 123 taken 62 times.
✗ Branch 122 → 173 not taken.
62 ladder.reserve(record.ladder.size());
268
2/2
✓ Branch 152 → 125 taken 69 times.
✓ Branch 152 → 153 taken 55 times.
186 for (const CandidateSpec &spec : record.ladder)
269 {
270
1/2
✓ Branch 127 → 128 taken 69 times.
✗ Branch 127 → 171 not taken.
69 Result<scan::Candidate> candidate = compile_rung(spec);
271
2/2
✓ Branch 129 → 130 taken 7 times.
✓ Branch 129 → 134 taken 62 times.
69 if (!candidate)
272 {
273 7 return std::unexpected(candidate.error());
274 }
275
1/2
✓ Branch 137 → 138 taken 62 times.
✗ Branch 137 → 169 not taken.
124 ladder.push_back(std::move(*candidate));
276
2/2
✓ Branch 140 → 141 taken 62 times.
✓ Branch 140 → 145 taken 7 times.
69 }
277 }
278 298 return Signature(std::move(record), std::move(ladder));
279 158 }
280
281 62 Result<Signature> Signature::adopt(const anchor::Anchor &source)
282 {
283 62 SignatureRecord record;
284
1/2
✓ Branch 5 → 6 taken 62 times.
✗ Branch 5 → 145 not taken.
62 record.label = std::string(source.label);
285 62 record.kind = source.kind;
286 // ExportName stores its module in export_module. Every other kind leaves this field empty. record.module
287 // represents this shared field without a kind branch. An empty export_module remains empty.
288
1/2
✓ Branch 11 → 12 taken 62 times.
✗ Branch 11 → 149 not taken.
124 record.module = std::string(source.export_module);
289
1/2
✓ Branch 17 → 18 taken 62 times.
✗ Branch 17 → 153 not taken.
124 record.export_name = std::string(source.export_name);
290
1/2
✓ Branch 23 → 24 taken 62 times.
✗ Branch 23 → 157 not taken.
62 record.mangled = std::string(source.mangled);
291 62 record.operand_kind = source.operand_kind;
292 62 record.operand_index = source.operand_index;
293 62 record.byte_width = source.byte_width;
294 62 record.pages = source.pages;
295
1/2
✓ Branch 29 → 30 taken 62 times.
✗ Branch 29 → 161 not taken.
62 record.xref_text = std::string(source.xref_text);
296 62 record.xref_encoding = source.xref_encoding;
297 62 record.xref_return = source.xref_return;
298 62 record.xref_require_terminator = source.xref_require_terminator;
299 62 record.xref_broad_match = source.xref_broad_match;
300 62 record.manual_value = source.manual_value;
301 // Preserve the source anchor's post-resolve validator across adoption. Its loss silently downgrades a validated
302 // in-code anchor to an unchecked Signature. This causes a fail-open regression.
303 62 record.validator = source.validator;
304 62 record.validator_context = source.validator_context;
305 62 record.validate_manual = source.validate_manual;
306 62 record.require_validator = source.require_validator;
307
308 // Reject composite, unset, and out-of-range kinds through the same policy and checked serialization validation.
309 // An adopted Signature never carries a value that the emitter normalizes.
310
2/2
✓ Branch 34 → 35 taken 9 times.
✓ Branch 34 → 38 taken 53 times.
62 if (!record_policy_domains_are_valid(record))
311 {
312 9 return fail(ErrorCode::InvalidArg, "manifest::adopt");
313 }
314
5/6
✓ Branch 39 → 40 taken 53 times.
✗ Branch 39 → 168 not taken.
✓ Branch 43 → 44 taken 46 times.
✓ Branch 43 → 56 taken 3 times.
✓ Branch 46 → 47 taken 43 times.
✓ Branch 46 → 56 taken 3 times.
148 if (!label_is_serializable(record.label) || value_is_unserializable(record.module) ||
315
4/4
✓ Branch 49 → 50 taken 42 times.
✓ Branch 49 → 56 taken 1 time.
✓ Branch 52 → 53 taken 39 times.
✓ Branch 52 → 56 taken 3 times.
131 value_is_unserializable(record.mangled) || value_is_unserializable(record.xref_text) ||
316
6/6
✓ Branch 40 → 41 taken 49 times.
✓ Branch 40 → 56 taken 4 times.
✓ Branch 55 → 56 taken 8 times.
✓ Branch 55 → 57 taken 31 times.
✓ Branch 58 → 59 taken 22 times.
✓ Branch 58 → 62 taken 31 times.
144 value_is_unserializable(record.export_name) ||
317 39 xref_evidence_is_malformed(record.xref_text, record.xref_encoding))
318 {
319 22 return fail(ErrorCode::InvalidArg, "manifest::adopt");
320 }
321
2/2
✓ Branch 86 → 64 taken 16 times.
✓ Branch 86 → 87 taken 23 times.
70 for (const scan::Candidate &candidate : source.site)
322 {
323 16 const scan::StringXref *xref = candidate.as_string_xref();
324
5/6
✓ Branch 67 → 68 taken 8 times.
✓ Branch 67 → 72 taken 8 times.
✓ Branch 70 → 71 taken 8 times.
✗ Branch 70 → 72 not taken.
✓ Branch 73 → 74 taken 8 times.
✓ Branch 73 → 77 taken 8 times.
16 if (xref != nullptr && xref_evidence_is_malformed(xref->text, xref->encoding))
325 {
326 8 return fail(ErrorCode::InvalidArg, "manifest::adopt");
327 }
328 }
329
8/8
✓ Branch 87 → 88 taken 17 times.
✓ Branch 87 → 89 taken 6 times.
✓ Branch 88 → 89 taken 3 times.
✓ Branch 88 → 92 taken 14 times.
✓ Branch 90 → 91 taken 1 time.
✓ Branch 90 → 92 taken 8 times.
✓ Branch 93 → 94 taken 1 time.
✓ Branch 93 → 97 taken 22 times.
32 if ((record.kind == anchor::AnchorKind::RipGlobal || record.kind == anchor::AnchorKind::CodeOperand) &&
330 9 source.site.empty())
331 {
332 1 return fail(ErrorCode::InvalidArg, "manifest::adopt");
333 }
334
5/6
✓ Branch 97 → 98 taken 1 time.
✓ Branch 97 → 101 taken 21 times.
✓ Branch 99 → 100 taken 1 time.
✗ Branch 99 → 101 not taken.
✓ Branch 102 → 103 taken 1 time.
✓ Branch 102 → 106 taken 21 times.
22 if (record.kind == anchor::AnchorKind::VtableIdentity && record.mangled.empty())
335 {
336 1 return fail(ErrorCode::InvalidArg, "manifest::adopt");
337 }
338
6/6
✓ Branch 106 → 107 taken 3 times.
✓ Branch 106 → 110 taken 18 times.
✓ Branch 108 → 109 taken 1 time.
✓ Branch 108 → 110 taken 2 times.
✓ Branch 111 → 112 taken 1 time.
✓ Branch 111 → 115 taken 20 times.
21 if (record.kind == anchor::AnchorKind::StringXref && record.xref_text.empty())
339 {
340 1 return fail(ErrorCode::InvalidArg, "manifest::adopt");
341 }
342
6/6
✓ Branch 115 → 116 taken 2 times.
✓ Branch 115 → 119 taken 18 times.
✓ Branch 117 → 118 taken 1 time.
✓ Branch 117 → 119 taken 1 time.
✓ Branch 120 → 121 taken 1 time.
✓ Branch 120 → 124 taken 19 times.
20 if (record.kind == anchor::AnchorKind::ExportName && record.export_name.empty())
343 {
344 1 return fail(ErrorCode::InvalidArg, "manifest::adopt");
345 }
346
347 // An adopted Signature has no captured baseline. Its record.ladder source text stays empty because a compiled
348 // Pattern cannot recover source AOB text. The anchor view uses the copied candidates.
349
1/2
✓ Branch 128 → 129 taken 19 times.
✗ Branch 128 → 165 not taken.
38 std::vector<scan::Candidate> ladder(source.site.begin(), source.site.end());
350 38 return Signature(std::move(record), std::move(ladder));
351 62 }
352
353 38 anchor::ResolvedAnchor Signature::resolve(Region fallback_scope) const
354 {
355
2/2
✓ Branch 3 → 4 taken 35 times.
✓ Branch 3 → 5 taken 3 times.
38 const Region effective = m_record.module.empty() ? fallback_scope : Region::module_named(m_record.module);
356
1/2
✓ Branch 8 → 9 taken 38 times.
✗ Branch 8 → 12 not taken.
38 return anchor::resolve(make_anchor(), effective);
357 }
358
359 47 anchor::ResolvedAnchor Signature::resolve_for_gate(Region fallback_scope, Region &winning_span) const
360 {
361
2/2
✓ Branch 3 → 4 taken 41 times.
✓ Branch 3 → 5 taken 6 times.
47 const Region effective = m_record.module.empty() ? fallback_scope : Region::module_named(m_record.module);
362
1/2
✓ Branch 8 → 9 taken 47 times.
✗ Branch 8 → 12 not taken.
47 return anchor::internal::resolve_with_winning_span(make_anchor(), effective, winning_span);
363 }
364
365 1 Region Signature::scope() const noexcept
366 {
367
1/2
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 5 not taken.
1 return m_record.module.empty() ? Region::host() : Region::module_named(m_record.module);
368 }
369
370 71 std::uint64_t Signature::current_fingerprint() const noexcept
371 {
372 // anchor_fingerprint covers the locate evidence. Extend it with Binding, which defines the read-it-there
373 // contract. This also lets the drift gate detect a binding-only repair. Fold the record label and module last.
374 // This makes each baseline label-specific and scope-sensitive. A new module target or label copy registers as
375 // drift.
376 71 std::uint64_t hash = fold_binding(anchor::anchor_fingerprint(make_anchor()), m_record.binding);
377 71 hash = fnv1a_fold_string(hash, m_record.label);
378 71 return fnv1a_fold_string(hash, m_record.module);
379 }
380
381 58 FingerprintState Signature::fingerprint_state() const noexcept
382 {
383
2/2
✓ Branch 2 → 3 taken 29 times.
✓ Branch 2 → 4 taken 29 times.
58 if (m_record.expected_fingerprint == 0)
384 {
385 29 return FingerprintState::Unset;
386 }
387
2/2
✓ Branch 5 → 6 taken 22 times.
✓ Branch 5 → 7 taken 7 times.
29 return current_fingerprint() == m_record.expected_fingerprint ? FingerprintState::Match
388 29 : FingerprintState::Drifted;
389 }
390
391 4 void Signature::recapture_fingerprint() noexcept
392 {
393 4 m_record.expected_fingerprint = current_fingerprint();
394 4 }
395
396 10 Result<void> Signature::recapture(Region fallback_scope)
397 {
398 // Resolve through the same path resolve_and_gate will, so the baselines describe the scope the gate compares
399 // them in. A signature that names its own module ignores the fallback here exactly as it does there.
400
1/2
✓ Branch 2 → 3 taken 10 times.
✗ Branch 2 → 21 not taken.
10 const anchor::ResolvedAnchor resolved = resolve(fallback_scope);
401
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 7 taken 9 times.
10 if (resolved.status != anchor::AnchorStatus::Resolved)
402 {
403 1 return fail(ErrorCode::NoMatch, "manifest::recapture");
404 }
405 // A Scalar has no module. A rung without a literal span has no witness span. The other baselines create an
406 // unusable captured record that can never satisfy the gate.
407
6/6
✓ Branch 8 → 9 taken 8 times.
✓ Branch 8 → 11 taken 1 time.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 12 taken 6 times.
✓ Branch 13 → 14 taken 3 times.
✓ Branch 13 → 17 taken 6 times.
9 if (!resolved.witness.image.present() || !resolved.witness.evidence.present())
408 {
409 3 return fail(ErrorCode::UnexpectedShape, "manifest::recapture");
410 }
411
412 // Compute every baseline before any store. A partial update can pair one game version's content with another
413 // version's identity.
414 6 const std::uint64_t fingerprint = current_fingerprint();
415 6 m_record.expected_fingerprint = fingerprint;
416 6 m_record.expected_image_identity = resolved.witness.image;
417 6 m_record.expected_winning_bytes = resolved.witness.evidence;
418 6 return {};
419 }
420
421 50 std::string_view Signature::label() const noexcept
422 {
423 50 return m_record.label;
424 }
425
426 33 anchor::AnchorKind Signature::kind() const noexcept
427 {
428 33 return m_record.kind;
429 }
430
431 53 const Binding &Signature::binding() const noexcept
432 {
433 53 return m_record.binding;
434 }
435
436 58 const SignatureRecord &Signature::record() const noexcept
437 {
438 58 return m_record;
439 }
440
441 namespace detail
442 {
443 class GateAccess
444 {
445 public:
446 [[nodiscard]] static anchor::ResolvedAnchor
447 47 resolve(const Signature &signature, Region scope, Region &winning_span)
448 {
449 47 return signature.resolve_for_gate(scope, winning_span);
450 }
451 };
452 } // namespace detail
453
454 23 bool revision_compatible(const ManifestHeader &header, std::uint32_t build_revision) noexcept
455 {
456 // build_revision 0 opts out of the gate. Otherwise, the file must target this build's exact contract epoch.
457 // Any other value identifies a different in-code contract and requires safe disregard.
458
4/4
✓ Branch 2 → 3 taken 20 times.
✓ Branch 2 → 4 taken 3 times.
✓ Branch 3 → 4 taken 16 times.
✓ Branch 3 → 5 taken 4 times.
23 return build_revision == 0 || header.revision == build_revision;
459 }
460
461 namespace
462 {
463 12 [[nodiscard]] anchor::ResultDomain record_declared_domain(const SignatureRecord &record) noexcept
464 {
465 12 anchor::Anchor probe{};
466 12 probe.kind = record.kind;
467 12 probe.operand_kind = record.operand_kind;
468 12 probe.byte_width = record.byte_width;
469 12 probe.xref_encoding = record.xref_encoding;
470 12 probe.xref_return = record.xref_return;
471 12 probe.pages = record.pages;
472 12 return anchor::declared_domain(probe);
473 }
474
475 } // namespace
476
477 Result<std::vector<Signature>>
478 21 overlay(std::span<const anchor::Anchor> defaults, std::span<const SignatureRecord> overrides)
479 {
480 21 std::vector<Signature> merged;
481
1/2
✓ Branch 3 → 4 taken 21 times.
✗ Branch 3 → 125 not taken.
21 merged.reserve(defaults.size());
482
483
2/2
✓ Branch 100 → 6 taken 22 times.
✓ Branch 100 → 101 taken 21 times.
64 for (const anchor::Anchor &def : defaults)
484 {
485 // The file overrides only named entries. A default without a match keeps its in-code form.
486 22 const SignatureRecord *override_record = nullptr;
487
2/2
✓ Branch 25 → 10 taken 21 times.
✓ Branch 25 → 26 taken 3 times.
46 for (const SignatureRecord &candidate : overrides)
488 {
489
2/2
✓ Branch 14 → 15 taken 19 times.
✓ Branch 14 → 16 taken 2 times.
21 if (candidate.label == def.label)
490 {
491 19 override_record = &candidate;
492 19 break;
493 }
494 }
495
496
6/6
✓ Branch 26 → 27 taken 19 times.
✓ Branch 26 → 30 taken 3 times.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 30 taken 18 times.
✓ Branch 31 → 32 taken 1 time.
✓ Branch 31 → 35 taken 21 times.
22 if (override_record != nullptr && !is_serializable_anchor_kind(def.kind))
497 {
498 // A flat file rung cannot preserve Quorum corroboration.
499 1 log().warning(
500 "manifest overlay: override '{}' targets a non-serializable in-code default; ignored",
501
1/2
✓ Branch 33 → 34 taken 1 time.
✗ Branch 33 → 105 not taken.
1 def.label
502 );
503 1 override_record = nullptr;
504 }
505
506
2/2
✓ Branch 35 → 36 taken 18 times.
✓ Branch 35 → 40 taken 4 times.
22 if (override_record != nullptr &&
507
2/2
✓ Branch 36 → 37 taken 6 times.
✓ Branch 36 → 40 taken 12 times.
18 ((override_record->kind == anchor::AnchorKind::Manual) != (def.kind == anchor::AnchorKind::Manual)))
508 {
509 // Manual anchors bypass backend-only validator requirements.
510 6 log().warning(
511 "manifest overlay: override '{}' changes the Manual validation posture; keeping in-code "
512 "default",
513
1/2
✓ Branch 38 → 39 taken 6 times.
✗ Branch 38 → 106 not taken.
6 def.label
514 );
515 6 override_record = nullptr;
516 }
517
518
6/6
✓ Branch 40 → 41 taken 12 times.
✓ Branch 40 → 45 taken 10 times.
✓ Branch 43 → 44 taken 2 times.
✓ Branch 43 → 45 taken 10 times.
✓ Branch 46 → 47 taken 2 times.
✓ Branch 46 → 50 taken 20 times.
22 if (override_record != nullptr && record_declared_domain(*override_record) != anchor::declared_domain(def))
519 {
520 2 log().warning(
521 "manifest overlay: override '{}' changes the declared result domain; keeping in-code "
522 "default",
523
1/2
✓ Branch 48 → 49 taken 2 times.
✗ Branch 48 → 107 not taken.
2 def.label
524 );
525 2 override_record = nullptr;
526 }
527
528
2/2
✓ Branch 50 → 51 taken 10 times.
✓ Branch 50 → 79 taken 12 times.
22 if (override_record != nullptr)
529 {
530 // Validator policy cannot round-trip an INI.
531
1/2
✓ Branch 51 → 52 taken 10 times.
✗ Branch 51 → 119 not taken.
10 SignatureRecord effective = *override_record;
532 10 effective.validator = def.validator;
533 10 effective.validator_context = def.validator_context;
534 10 effective.validate_manual = def.validate_manual;
535 10 effective.require_validator = def.require_validator;
536
537
1/2
✓ Branch 55 → 56 taken 10 times.
✗ Branch 55 → 108 not taken.
10 Result<Signature> compiled = Signature::compile(std::move(effective));
538
2/2
✓ Branch 58 → 59 taken 8 times.
✓ Branch 58 → 64 taken 2 times.
10 if (compiled)
539 {
540
1/2
✓ Branch 62 → 63 taken 8 times.
✗ Branch 62 → 115 not taken.
16 merged.push_back(std::move(*compiled));
541 8 continue;
542 }
543 // A malformed override falls back to the in-code default. An override must never produce a worse
544 // result than file absence.
545 2 log().warning(
546 "manifest overlay: override '{}' failed to compile ({}); keeping in-code default",
547
1/2
✓ Branch 67 → 68 taken 2 times.
✗ Branch 67 → 111 not taken.
2 def.label,
548
1/2
✓ Branch 66 → 67 taken 2 times.
✗ Branch 66 → 114 not taken.
4 compiled.error().message()
549 );
550
4/4
✓ Branch 71 → 72 taken 2 times.
✓ Branch 71 → 73 taken 8 times.
✓ Branch 75 → 76 taken 2 times.
✓ Branch 75 → 78 taken 8 times.
18 }
551
552
1/2
✓ Branch 79 → 80 taken 14 times.
✗ Branch 79 → 123 not taken.
14 Result<Signature> adopted = Signature::adopt(def);
553
2/2
✓ Branch 81 → 82 taken 13 times.
✓ Branch 81 → 86 taken 1 time.
14 if (adopted)
554 {
555
1/2
✓ Branch 85 → 89 taken 13 times.
✗ Branch 85 → 121 not taken.
26 merged.push_back(std::move(*adopted));
556 }
557 else
558 {
559 // A Quorum or CallArgHome default has no flat Signature representation. Skip it rather than mis-adopt
560 // it.
561 2 log().warning(
562 "manifest overlay: default '{}' is not a serializable anchor kind; gate it in code",
563
1/2
✓ Branch 87 → 88 taken 1 time.
✗ Branch 87 → 120 not taken.
1 def.label
564 );
565 }
566 14 }
567 42 return merged;
568 21 }
569
570 31 const GatedSignature *GateResult::find(std::string_view label) const noexcept
571 {
572
2/2
✓ Branch 18 → 4 taken 19 times.
✓ Branch 18 → 19 taken 15 times.
65 for (const GatedSignature &entry : trusted)
573 {
574
2/2
✓ Branch 7 → 8 taken 16 times.
✓ Branch 7 → 9 taken 3 times.
19 if (entry.label == label)
575 {
576 16 return &entry;
577 }
578 }
579 15 return nullptr;
580 }
581
582 namespace
583 {
584 // revision_checked is deliberately separate from revision_ok: "compatible" and "never checked" are the same
585 // value there and must not be.
586 42 [[nodiscard]] GateResult gate_impl(
587 std::span<const Signature> signatures,
588 const GatePolicy &policy,
589 Region scope,
590 bool revision_checked,
591 bool revision_ok
592 )
593 {
594 42 GateResult result;
595
596 // Resolve every signature before summary. assess_quality needs the whole report. A signature's fingerprint
597 // verdict is independent of the resolve outcome.
598 42 std::vector<anchor::ResolvedAnchor> report;
599
1/2
✓ Branch 3 → 4 taken 42 times.
✗ Branch 3 → 167 not taken.
42 report.reserve(signatures.size());
600 42 std::vector<Region> winning_spans;
601
1/2
✓ Branch 5 → 6 taken 42 times.
✗ Branch 5 → 165 not taken.
42 winning_spans.reserve(signatures.size());
602
2/2
✓ Branch 22 → 8 taken 47 times.
✓ Branch 22 → 23 taken 42 times.
131 for (const Signature &signature : signatures)
603 {
604 47 Region winning_span{};
605
2/4
✓ Branch 10 → 11 taken 47 times.
✗ Branch 10 → 149 not taken.
✓ Branch 11 → 12 taken 47 times.
✗ Branch 11 → 149 not taken.
47 report.push_back(detail::GateAccess::resolve(signature, scope, winning_span));
606
1/2
✓ Branch 12 → 13 taken 47 times.
✗ Branch 12 → 150 not taken.
47 winning_spans.push_back(winning_span);
607 }
608 42 result.quality = anchor::assess_quality(report);
609
610 // Keep fingerprint states parallel to result.trusted. The whole-manifest floor demotion then reports the
611 // true drift state directly.
612 42 std::vector<FingerprintState> trusted_fingerprints;
613
1/2
✓ Branch 26 → 27 taken 42 times.
✗ Branch 26 → 163 not taken.
42 trusted_fingerprints.reserve(signatures.size());
614
615
2/2
✓ Branch 122 → 28 taken 47 times.
✓ Branch 122 → 123 taken 42 times.
89 for (std::size_t index = 0; index < signatures.size(); ++index)
616 {
617 47 const Signature &signature = signatures[index];
618 47 const anchor::ResolvedAnchor &resolved = report[index];
619 47 const FingerprintState fingerprint = signature.fingerprint_state();
620
621 // A non-unique or missed locate is never trusted.
622
2/2
✓ Branch 31 → 32 taken 1 time.
✓ Branch 31 → 35 taken 46 times.
47 if (resolved.status != anchor::AnchorStatus::Resolved)
623 {
624 2 result.rejected.push_back(
625 RejectedSignature{
626 1 .label = signature.label(),
627
1/2
✓ Branch 33 → 34 taken 1 time.
✗ Branch 33 → 152 not taken.
1 .status = resolved.status,
628 .fingerprint = fingerprint,
629 .reason = GateReason::Unresolved,
630 }
631 );
632 22 continue;
633 }
634
4/4
✓ Branch 35 → 36 taken 45 times.
✓ Branch 35 → 40 taken 1 time.
✓ Branch 36 → 37 taken 3 times.
✓ Branch 36 → 40 taken 42 times.
46 if (policy.reject_on_fingerprint_drift && fingerprint == FingerprintState::Drifted)
635 {
636 6 result.rejected.push_back(
637
1/2
✓ Branch 38 → 39 taken 3 times.
✗ Branch 38 → 153 not taken.
3 RejectedSignature{
638 3 .label = signature.label(),
639 .status = anchor::AnchorStatus::Resolved,
640 .fingerprint = FingerprintState::Drifted,
641 .reason = GateReason::FingerprintDrifted,
642 }
643 );
644 3 continue;
645 }
646
4/4
✓ Branch 40 → 41 taken 20 times.
✓ Branch 40 → 45 taken 23 times.
✓ Branch 41 → 42 taken 3 times.
✓ Branch 41 → 45 taken 17 times.
43 if (policy.reject_unset_fingerprint && fingerprint == FingerprintState::Unset)
647 {
648 6 result.rejected.push_back(
649
1/2
✓ Branch 43 → 44 taken 3 times.
✗ Branch 43 → 154 not taken.
3 RejectedSignature{
650 3 .label = signature.label(),
651 .status = anchor::AnchorStatus::Resolved,
652 .fingerprint = FingerprintState::Unset,
653 .reason = GateReason::FingerprintUnset,
654 }
655 );
656 3 continue;
657 }
658 // Evaluate binding_authorizes_mutation once here. Every later mutation gate uses this live, writable
659 // target fact.
660
4/4
✓ Branch 45 → 46 taken 27 times.
✓ Branch 45 → 50 taken 13 times.
✓ Branch 48 → 49 taken 25 times.
✓ Branch 48 → 50 taken 2 times.
67 const bool mutation_capable = resolved.kind != anchor::AnchorKind::Manual &&
661 27 binding_authorizes_mutation(signature.binding().kind, resolved.domain);
662 // A mutation-strict entry must bind a live address through a compatible consumer primitive. A Manual
663 // pin or binding-domain mismatch equals !mutation_capable and causes rejection.
664
4/4
✓ Branch 51 → 52 taken 20 times.
✓ Branch 51 → 56 taken 20 times.
✓ Branch 52 → 53 taken 2 times.
✓ Branch 52 → 56 taken 18 times.
40 if (policy.require_mutation_safe_binding && !mutation_capable)
665 {
666 4 result.rejected.push_back(
667
1/2
✓ Branch 54 → 55 taken 2 times.
✗ Branch 54 → 155 not taken.
2 RejectedSignature{
668 2 .label = signature.label(),
669 .status = anchor::AnchorStatus::Resolved,
670 .fingerprint = fingerprint,
671 .reason = GateReason::BindingCannotMutate,
672 }
673 );
674 2 continue;
675 }
676 // An unchecked revision and an incompatible revision both refuse authorization. The first skips
677 // comparison. The second comparison finds disagreement.
678
8/8
✓ Branch 56 → 57 taken 25 times.
✓ Branch 56 → 63 taken 13 times.
✓ Branch 57 → 58 taken 23 times.
✓ Branch 57 → 60 taken 2 times.
✓ Branch 58 → 59 taken 15 times.
✓ Branch 58 → 63 taken 8 times.
✓ Branch 59 → 60 taken 2 times.
✓ Branch 59 → 63 taken 13 times.
38 if (mutation_capable && (!revision_ok || (policy.require_contract_revision && !revision_checked)))
679 {
680 8 result.rejected.push_back(
681
1/2
✓ Branch 61 → 62 taken 4 times.
✗ Branch 61 → 156 not taken.
4 RejectedSignature{
682 4 .label = signature.label(),
683 .status = anchor::AnchorStatus::Resolved,
684 .fingerprint = fingerprint,
685 .reason = GateReason::ContractRevision,
686 }
687 );
688 4 continue;
689 }
690
5/6
✓ Branch 63 → 64 taken 21 times.
✓ Branch 63 → 86 taken 13 times.
✓ Branch 64 → 65 taken 5 times.
✓ Branch 64 → 66 taken 16 times.
✗ Branch 65 → 66 not taken.
✓ Branch 65 → 86 taken 5 times.
34 if (mutation_capable && (policy.require_live_image_identity || policy.require_captured_image_identity))
691 {
692 16 const scan::ImageIdentity &expected = signature.record().expected_image_identity;
693 16 const scan::ImageIdentity &live = resolved.witness.image;
694
4/4
✓ Branch 67 → 68 taken 13 times.
✓ Branch 67 → 71 taken 3 times.
✓ Branch 69 → 70 taken 1 time.
✓ Branch 69 → 71 taken 12 times.
16 const bool missing_baseline = policy.require_captured_image_identity && !expected.present();
695
3/4
✓ Branch 72 → 73 taken 16 times.
✗ Branch 72 → 80 not taken.
✓ Branch 74 → 75 taken 14 times.
✓ Branch 74 → 80 taken 2 times.
30 const bool mismatched = policy.require_live_image_identity && expected.present() &&
696
3/4
✓ Branch 76 → 77 taken 14 times.
✗ Branch 76 → 79 not taken.
✓ Branch 78 → 79 taken 2 times.
✓ Branch 78 → 80 taken 12 times.
14 (!live.present() || expected != live);
697
4/4
✓ Branch 81 → 82 taken 15 times.
✓ Branch 81 → 83 taken 1 time.
✓ Branch 82 → 83 taken 2 times.
✓ Branch 82 → 86 taken 13 times.
16 if (missing_baseline || mismatched)
698 {
699 6 result.rejected.push_back(
700
1/2
✓ Branch 84 → 85 taken 3 times.
✗ Branch 84 → 157 not taken.
3 RejectedSignature{
701 3 .label = signature.label(),
702 .status = anchor::AnchorStatus::Resolved,
703 .fingerprint = fingerprint,
704 .reason = GateReason::ImageIdentity,
705 }
706 );
707 3 continue;
708 }
709 }
710
4/4
✓ Branch 86 → 87 taken 18 times.
✓ Branch 86 → 112 taken 13 times.
✓ Branch 87 → 88 taken 11 times.
✓ Branch 87 → 112 taken 7 times.
31 if (mutation_capable && policy.require_winning_evidence_baseline)
711 {
712 // Both sides need complete captures. Empty captures never count as agreement. Re-read the selected
713 // match span directly before trust publication. The direct read detects any content change after
714 // the sweep.
715 11 const scan::WinningEvidence &expected = signature.record().expected_winning_bytes;
716 11 const scan::WinningEvidence &live = resolved.witness.evidence;
717 11 std::array<std::byte, scan::MAX_MUTATION_WITNESS_BYTES> current{};
718 11 const Region winning_span = winning_spans[index];
719 const bool span_matches =
720
3/4
✓ Branch 93 → 94 taken 9 times.
✗ Branch 93 → 106 not taken.
✓ Branch 95 → 96 taken 6 times.
✓ Branch 95 → 106 taken 3 times.
20 expected.present() && live.present() && expected == live &&
721
2/4
✓ Branch 96 → 97 taken 6 times.
✗ Branch 96 → 106 not taken.
✓ Branch 101 → 102 taken 6 times.
✗ Branch 101 → 106 not taken.
12 winning_span.size == expected.length &&
722 6 DetourModKit::detail::guarded_read_bytes(
723 winning_span.base.raw(),
724 6 current.data(),
725 6 expected.length
726
2/2
✓ Branch 91 → 92 taken 9 times.
✓ Branch 91 → 106 taken 2 times.
20 ) &&
727
3/4
✓ Branch 103 → 104 taken 6 times.
✗ Branch 103 → 159 not taken.
✓ Branch 104 → 105 taken 5 times.
✓ Branch 104 → 106 taken 1 time.
6 std::equal(current.begin(), current.begin() + expected.length, expected.bytes.begin());
728
2/2
✓ Branch 107 → 108 taken 6 times.
✓ Branch 107 → 111 taken 5 times.
11 if (!span_matches)
729 {
730 12 result.rejected.push_back(
731
1/2
✓ Branch 109 → 110 taken 6 times.
✗ Branch 109 → 158 not taken.
6 RejectedSignature{
732 6 .label = signature.label(),
733 .status = anchor::AnchorStatus::Resolved,
734 .fingerprint = fingerprint,
735 .reason = GateReason::WinningEvidence,
736 }
737 );
738 6 continue;
739 }
740 }
741
742 50 result.trusted.push_back(
743
1/2
✓ Branch 116 → 117 taken 25 times.
✗ Branch 116 → 160 not taken.
25 GatedSignature{
744 25 .label = signature.label(),
745 25 .kind = signature.kind(),
746 25 .address = Address{static_cast<std::uintptr_t>(resolved.value)},
747 25 .binding = &signature.binding(),
748 }
749 );
750
1/2
✓ Branch 117 → 118 taken 25 times.
✗ Branch 117 → 161 not taken.
25 trusted_fingerprints.push_back(fingerprint);
751 }
752
753 // If too small a fraction is trustworthy, demote the whole manifest. NaN and negative floors disable the
754 // floor, while values above one clamp to one.
755 42 double floor = policy.min_resolved_fraction;
756
1/2
✗ Branch 123 → 124 not taken.
✓ Branch 123 → 125 taken 42 times.
42 if (!(floor >= 0.0))
757 {
758 floor = 0.0;
759 }
760
1/2
✗ Branch 125 → 126 not taken.
✓ Branch 125 → 127 taken 42 times.
42 if (floor > 1.0)
761 {
762 floor = 1.0;
763 }
764
5/6
✓ Branch 128 → 129 taken 42 times.
✗ Branch 128 → 131 not taken.
✓ Branch 129 → 130 taken 20 times.
✓ Branch 129 → 131 taken 22 times.
✓ Branch 132 → 133 taken 20 times.
✓ Branch 132 → 144 taken 22 times.
42 if (!signatures.empty() && floor > 0.0)
765 {
766 const double fraction =
767 20 static_cast<double>(result.trusted.size()) / static_cast<double>(signatures.size());
768
2/2
✓ Branch 135 → 136 taken 15 times.
✓ Branch 135 → 144 taken 5 times.
20 if (fraction < floor)
769 {
770
2/2
✓ Branch 142 → 137 taken 2 times.
✓ Branch 142 → 143 taken 15 times.
17 for (std::size_t index = 0; index < result.trusted.size(); ++index)
771 {
772 4 result.rejected.push_back(
773
1/2
✓ Branch 139 → 140 taken 2 times.
✗ Branch 139 → 162 not taken.
2 RejectedSignature{
774 2 .label = result.trusted[index].label,
775 .status = anchor::AnchorStatus::Resolved,
776 2 .fingerprint = trusted_fingerprints[index],
777 .reason = GateReason::HealthFloor,
778 }
779 );
780 }
781 15 result.trusted.clear();
782 }
783 }
784
785 42 return result;
786 42 }
787 } // namespace
788
789 23 GateResult resolve_and_gate(std::span<const Signature> signatures, const GatePolicy &policy, Region scope)
790 {
791 // This overload receives no header, so it performs no contract revision comparison. Read-only gates retain
792 // their behavior. A policy that requires a checked revision refuses mutation authorization.
793 23 return gate_impl(signatures, policy, scope, /*revision_checked=*/false, /*revision_ok=*/true);
794 }
795
796 19 GateResult resolve_and_gate(
797 std::span<const Signature> signatures,
798 const ManifestHeader &header,
799 std::uint32_t build_revision,
800 const GatePolicy &policy,
801 Region scope
802 )
803 {
804 return gate_impl(
805 signatures,
806 policy,
807 scope,
808 /*revision_checked=*/build_revision != 0,
809 19 revision_compatible(header, build_revision)
810 19 );
811 }
812
813 3 std::string_view fingerprint_state_to_string(FingerprintState state) noexcept
814 {
815
3/4
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 1 time.
✓ Branch 2 → 5 taken 1 time.
✗ Branch 2 → 6 not taken.
3 switch (state)
816 {
817 1 case FingerprintState::Unset:
818 1 return "unset";
819 1 case FingerprintState::Match:
820 1 return "match";
821 1 case FingerprintState::Drifted:
822 1 return "drifted";
823 }
824 return "unset";
825 }
826
827 9 std::string_view gate_reason_to_string(GateReason reason) noexcept
828 {
829
9/10
✓ 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 1 time.
✓ Branch 2 → 8 taken 1 time.
✓ Branch 2 → 9 taken 1 time.
✓ Branch 2 → 10 taken 1 time.
✓ Branch 2 → 11 taken 1 time.
✗ Branch 2 → 12 not taken.
9 switch (reason)
830 {
831 1 case GateReason::None:
832 1 return "none";
833 1 case GateReason::Unresolved:
834 1 return "unresolved";
835 1 case GateReason::FingerprintDrifted:
836 1 return "fingerprint-drifted";
837 1 case GateReason::FingerprintUnset:
838 1 return "fingerprint-unset";
839 1 case GateReason::BindingCannotMutate:
840 1 return "binding-cannot-mutate";
841 1 case GateReason::ContractRevision:
842 1 return "contract-revision";
843 1 case GateReason::ImageIdentity:
844 1 return "image-identity";
845 1 case GateReason::WinningEvidence:
846 1 return "winning-evidence";
847 1 case GateReason::HealthFloor:
848 1 return "health-floor";
849 }
850 return "none";
851 }
852 } // namespace DetourModKit::manifest
853