./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
470 / 481
97.7%
Branches:
400 / 487
82.1%
Functions:
38 / 38
100.0%

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