GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 97.5% 115 / 0 / 118
Functions: 100.0% 9 / 0 / 9
Branches: 90.1% 64 / 0 / 71

src/anchor_gate.cpp
Line Branch Exec Source
1 /**
2 * @file anchor_gate.cpp
3 * @brief This TU owns the anchor quality summary, the launch gate verdict, and the report string tables.
4 *
5 * It consumes only the public ResolvedAnchor vocabulary. The resolution engine stays in anchor.cpp and the drift
6 * fingerprints in anchor_evidence.cpp.
7 */
8
9 #include "DetourModKit/anchor.hpp"
10
11 #include <cmath>
12 #include <cstddef>
13 #include <span>
14 #include <string_view>
15
16 namespace DetourModKit
17 {
18 namespace anchor
19 {
20 namespace
21 {
22 15 [[nodiscard]] double clamped_gate_ratio(double ratio) noexcept
23 {
24
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 14 times.
15 if (std::isnan(ratio))
25 {
26 1 return 1.0;
27 }
28
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 13 times.
14 if (ratio < 0.0)
29 {
30 1 return 0.0;
31 }
32
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 12 times.
13 if (ratio > 1.0)
33 {
34 1 return 1.0;
35 }
36 12 return ratio;
37 }
38 } // anonymous namespace
39
40 219 AnchorQuality assess_quality(std::span<const ResolvedAnchor> report) noexcept
41 {
42 219 AnchorQuality quality{};
43 219 quality.total = report.size();
44
2/2
✓ Branch 28 → 5 taken 118 times.
✓ Branch 28 → 29 taken 219 times.
556 for (const ResolvedAnchor &entry : report)
45 {
46
6/7
✓ Branch 7 → 8 taken 88 times.
✓ Branch 7 → 9 taken 16 times.
✓ Branch 7 → 10 taken 4 times.
✓ Branch 7 → 11 taken 2 times.
✓ Branch 7 → 12 taken 1 time.
✓ Branch 7 → 13 taken 7 times.
✗ Branch 7 → 14 not taken.
118 switch (entry.status)
47 {
48 88 case AnchorStatus::Resolved:
49 88 ++quality.resolved;
50 88 break;
51 16 case AnchorStatus::Failed:
52 16 ++quality.failed;
53 16 break;
54 4 case AnchorStatus::Unsupported:
55 4 ++quality.unsupported;
56 4 break;
57 2 case AnchorStatus::QuorumNotIndependent:
58 2 ++quality.not_independent;
59 2 break;
60 1 case AnchorStatus::QuorumAmbiguous:
61 // This status commits no trusted value, so it is a failure alongside a backend miss.
62 1 ++quality.failed;
63 1 break;
64 7 case AnchorStatus::Unresolved:
65 7 break;
66 }
67 // A pinned literal is at-risk regardless of status: it "resolves" but cannot self-heal across a patch.
68
2/2
✓ Branch 14 → 15 taken 28 times.
✓ Branch 14 → 16 taken 90 times.
118 if (entry.kind == AnchorKind::Manual)
69 {
70 28 ++quality.manual_at_risk;
71 }
72 // A corroborated quorum is the strongest evidence: N independent signals had to agree.
73
4/4
✓ Branch 16 → 17 taken 5 times.
✓ Branch 16 → 19 taken 113 times.
✓ Branch 17 → 18 taken 2 times.
✓ Branch 17 → 19 taken 3 times.
118 if (entry.kind == AnchorKind::Quorum && entry.status == AnchorStatus::Resolved)
74 {
75 2 ++quality.corroborated;
76 }
77 }
78 219 return quality;
79 }
80
81 24 GateVerdict evaluate_gate(const AnchorQuality &quality, const GatePolicy &policy) noexcept
82 {
83 // The span overload always feeds a self-consistent summary, but the direct AnchorQuality overload is
84 // public. If a caller supplies impossible counts, fail closed so an inflated resolved count cannot create
85 // a healthy verdict.
86 24 std::size_t accounted = 0;
87 117 const auto count_fits = [&accounted, total = quality.total](std::size_t count) noexcept -> bool
88 {
89
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 92 times.
93 if (count > total - accounted)
90 {
91 1 return false;
92 }
93 92 accounted += count;
94 92 return true;
95 24 };
96
6/8
✓ Branch 3 → 4 taken 23 times.
✓ Branch 3 → 10 taken 1 time.
✓ Branch 5 → 6 taken 23 times.
✗ Branch 5 → 10 not taken.
✓ Branch 7 → 8 taken 23 times.
✗ Branch 7 → 10 not taken.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 14 taken 23 times.
47 if (!count_fits(quality.resolved) || !count_fits(quality.failed) || !count_fits(quality.unsupported) ||
97
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 23 times.
23 !count_fits(quality.not_independent))
98 {
99 1 return GateVerdict::Fail;
100 }
101
102 // QuorumNotIndependent counts as a failure alongside Failed for the cap: both mean a declared anchor
103 // yielded no verified value. Check the hard cap before the ratio. A failure-heavy manifest then fails even
104 // when its few resolved entries clear the ratio.
105
2/2
✓ Branch 14 → 15 taken 5 times.
✓ Branch 14 → 16 taken 18 times.
23 if (quality.failed > policy.max_failed)
106 {
107 5 return GateVerdict::Fail;
108 }
109 18 const std::size_t remaining_failure_budget = policy.max_failed - quality.failed;
110
2/2
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 18 taken 17 times.
18 if (quality.not_independent > remaining_failure_budget)
111 {
112 1 return GateVerdict::Fail;
113 }
114
115 // Resolvable excludes the Unsupported kind, which has no backend and can never heal. Every other unresolved
116 // kind stays in the denominator. A partial result therefore reduces the ratio and fails closed.
117 17 const std::size_t resolvable = quality.total - quality.unsupported;
118
2/2
✓ Branch 18 → 19 taken 2 times.
✓ Branch 18 → 20 taken 15 times.
17 if (resolvable == 0)
119 {
120 // No assessable entry proves runtime health. An empty report or all-unsupported table must
121 // not become a healthy Pass merely because no resolvable anchor contradicts it.
122 2 return GateVerdict::Degraded;
123 }
124
125 // Clamp the ratio to [0, 1]. NaN becomes the strict default. This prevents an out-of-range value from
126 // inversion of the comparison. Avoid division so the full-ratio case retains exact equality.
127 15 const double ratio = clamped_gate_ratio(policy.min_resolved_ratio);
128
2/2
✓ Branch 21 → 22 taken 3 times.
✓ Branch 21 → 23 taken 12 times.
15 if (static_cast<double>(quality.resolved) < ratio * static_cast<double>(resolvable))
129 {
130 3 return GateVerdict::Fail;
131 }
132
133 // A Manual literal cannot self-heal in any status (AnchorQuality::manual_at_risk counts every Manual
134 // entry). If policy requests it, surface this soft risk so the caller can report silent manual offset
135 // drift after a patch.
136
4/4
✓ Branch 23 → 24 taken 10 times.
✓ Branch 23 → 26 taken 2 times.
✓ Branch 24 → 25 taken 4 times.
✓ Branch 24 → 26 taken 6 times.
12 if (policy.manual_at_risk_degrades && quality.manual_at_risk > 0)
137 {
138 4 return GateVerdict::Degraded;
139 }
140 8 return GateVerdict::Pass;
141 }
142
143 19 GateVerdict evaluate_gate(std::span<const ResolvedAnchor> report, const GatePolicy &policy) noexcept
144 {
145 19 return evaluate_gate(assess_quality(report), policy);
146 }
147
148 6 std::string_view anchor_status_to_string(AnchorStatus status) noexcept
149 {
150
6/7
✓ 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 not taken.
6 switch (status)
151 {
152 1 case AnchorStatus::Unresolved:
153 1 return "Unresolved";
154 1 case AnchorStatus::Resolved:
155 1 return "Resolved";
156 1 case AnchorStatus::Failed:
157 1 return "Failed";
158 1 case AnchorStatus::Unsupported:
159 1 return "Unsupported";
160 1 case AnchorStatus::QuorumNotIndependent:
161 1 return "QuorumNotIndependent";
162 1 case AnchorStatus::QuorumAmbiguous:
163 1 return "QuorumAmbiguous";
164 }
165 return "Unknown";
166 }
167
168 5 std::string_view result_domain_to_string(ResultDomain domain) noexcept
169 {
170
5/6
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 1 time.
✓ Branch 2 → 5 taken 1 time.
✓ Branch 2 → 6 taken 1 time.
✓ Branch 2 → 7 taken 1 time.
✗ Branch 2 → 8 not taken.
5 switch (domain)
171 {
172 1 case ResultDomain::Unknown:
173 1 return "Unknown";
174 1 case ResultDomain::CodeSite:
175 1 return "CodeSite";
176 1 case ResultDomain::DataAddress:
177 1 return "DataAddress";
178 1 case ResultDomain::VtableAddress:
179 1 return "VtableAddress";
180 1 case ResultDomain::Scalar:
181 1 return "Scalar";
182 }
183 return "Unknown";
184 }
185
186 9 std::string_view physical_source_to_string(PhysicalSource source) noexcept
187 {
188
9/9
✓ 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.
9 switch (source)
189 {
190 1 case PhysicalSource::None:
191 1 return "None";
192 1 case PhysicalSource::ByteSignature:
193 1 return "ByteSignature";
194 1 case PhysicalSource::StringLiteral:
195 1 return "StringLiteral";
196 1 case PhysicalSource::TypeIdentity:
197 1 return "TypeIdentity";
198 1 case PhysicalSource::ExportTable:
199 1 return "ExportTable";
200 1 case PhysicalSource::CodeOperand:
201 1 return "CodeOperand";
202 1 case PhysicalSource::ManualPin:
203 1 return "ManualPin";
204 1 case PhysicalSource::Corroborated:
205 1 return "Corroborated";
206 }
207 1 return "Unknown";
208 }
209
210 3 std::string_view gate_verdict_to_string(GateVerdict verdict) noexcept
211 {
212
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 (verdict)
213 {
214 1 case GateVerdict::Pass:
215 1 return "Pass";
216 1 case GateVerdict::Degraded:
217 1 return "Degraded";
218 1 case GateVerdict::Fail:
219 1 return "Fail";
220 }
221 return "Unknown";
222 }
223 } // namespace anchor
224 } // namespace DetourModKit
225