GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 94.8% 217 / 0 / 229
Functions: 100.0% 15 / 0 / 15
Branches: 88.3% 144 / 0 / 163

src/rtti_dissect.cpp
Line Branch Exec Source
1 /**
2 * @file rtti_dissect.cpp
3 * @brief Reverse-direction RTTI dissection and self-healing offset resolvers.
4 *
5 * Builds four layers on top of the verified COL prelude shared with rtti.cpp:
6 * L1 identify_pointee_type -- reverse-identify the object behind one slot.
7 * L2 reverse_scan_block -- RTTI-label a block of slots (tooling).
8 * L3 heal_landmark/offset -- self-heal one field offset after a patch.
9 * L4 solve_fingerprint -- recover one uniform shift across many fields.
10 *
11 * Every entry point is noexcept and fails closed. The hot self-heal path allocates nothing (it reuses one stack
12 * PointeeType); only the explicitly tooling-only block scanner grows a vector. All reads go through the same
13 * SEH-guarded, module-bound-checked prelude the forward walker uses, so an unmapped page or forged COL is a clean
14 * non-match, never a fault. Matching is byte-exact on the MSVC most-derived mangled name (no UnDecorateSymbolName).
15 */
16
17 #include "DetourModKit/rtti_dissect.hpp"
18 #include "DetourModKit/memory.hpp"
19
20 #include "rtti_internal.hpp"
21
22 #include <cstdint>
23
24 namespace DetourModKit
25 {
26 namespace
27 {
28 /**
29 * @brief Soft policy filter: does a resolved slot's shape (and subobject position) satisfy the landmark's
30 * required indirection?
31 * @details Indirection::Any accepts either shape; PointerToObject and ObjectBase pin the slot to the
32 * pointer-to-object or direct-object form. This is a policy-layer decision deliberately kept out
33 * of L1 so a consumer can record Any when capture and heal may straddle a DLL boundary.
34 * CompleteObject adds a subobject constraint on top of the direct-object shape, which is why
35 * @p col_offset is consulted here.
36 * @param was_pointer The resolved slot's shape (PointeeType::was_pointer).
37 * @param col_offset The resolved object's COL.offset (PointeeType::col_offset): 0 for the primary subobject,
38 * nonzero for a multiple-inheritance secondary base.
39 * @param ind The landmark's required indirection.
40 */
41 66 [[nodiscard]] bool shape_ok(bool was_pointer, std::uint32_t col_offset, Rtti::Indirection ind) noexcept
42 {
43
4/5
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 54 times.
✓ Branch 2 → 5 taken 4 times.
✓ Branch 2 → 6 taken 6 times.
✗ Branch 2 → 11 not taken.
66 switch (ind)
44 {
45 2 case Rtti::Indirection::Any:
46 2 return true;
47 54 case Rtti::Indirection::PointerToObject:
48 54 return was_pointer;
49 4 case Rtti::Indirection::ObjectBase:
50 4 return !was_pointer;
51 6 case Rtti::Indirection::CompleteObject:
52 // A direct object base pinned to the most-derived (primary) subobject. Under multiple inheritance every
53 // base subobject has its own vtable, and each vtable's COL names the same complete type. COL.offset
54 // distinguishes those subobjects; the primary subobject has col_offset == 0, so its base is the
55 // complete object. Rejecting col_offset != 0 keeps a heal from latching a secondary base's adjacent
56 // vtable and reporting an offset shifted by that subobject delta.
57
4/4
✓ Branch 6 → 7 taken 5 times.
✓ Branch 6 → 9 taken 1 time.
✓ Branch 7 → 8 taken 3 times.
✓ Branch 7 → 9 taken 2 times.
6 return !was_pointer && col_offset == 0;
58 }
59 return false;
60 }
61
62 /**
63 * @brief Probe one slot: resolve, check shape, byte-exact name match.
64 * @details Fills @p pt whenever the slot resolves (so the caller can read the match details), and reports
65 * whether it also passed the shape filter and the exact mangled-name compare. The name compare reuses
66 * the same semantics as vtable_is_type: a superstring or a differing byte fails.
67 * @return true only on a full resolve + shape + exact-name match.
68 */
69 332 [[nodiscard]] bool slot_matches(std::uintptr_t addr, const Rtti::Landmark &lm, Rtti::PointeeType &pt) noexcept
70 {
71
2/2
✓ Branch 3 → 4 taken 266 times.
✓ Branch 3 → 5 taken 66 times.
332 if (!Rtti::identify_pointee_type(addr, pt))
72 266 return false;
73
2/2
✓ Branch 6 → 7 taken 5 times.
✓ Branch 6 → 8 taken 61 times.
66 if (!shape_ok(pt.was_pointer, pt.col_offset, lm.indirection))
74 5 return false;
75 61 return pt.name() == lm.expected_mangled;
76 }
77
78 /**
79 * @brief Builds a HealHit from a matched slot.
80 * @details healed_offset is the field's offset within the struct base (slot_addr - base), the value a consumer
81 * feeds straight into a pointer chain. It equals nominal_offset when the layout did not drift and
82 * nominal_offset +/- delta after a shift.
83 */
84 27 [[nodiscard]] Rtti::HealHit make_hit(std::uintptr_t slot_addr, std::uintptr_t base,
85 const Rtti::PointeeType &pt) noexcept
86 {
87 27 Rtti::HealHit h;
88 27 h.healed_offset = static_cast<std::ptrdiff_t>(slot_addr - base);
89 27 h.slot_addr = slot_addr;
90 27 h.object_addr = pt.object_base;
91 27 h.vtable = pt.vtable;
92 27 h.col_offset = pt.col_offset;
93 27 h.was_pointer = pt.was_pointer;
94 27 return h;
95 }
96
97 /**
98 * @brief Validates a landmark's type/shape descriptor fields.
99 * @details Shared by heal_landmark and solve_fingerprint. Does not touch @ref Rtti::Landmark::base or @ref
100 * Rtti::Landmark::window, which the two callers validate differently.
101 * @return true when expected_mangled is a sane length and indirection is a known enumerator.
102 */
103 70 [[nodiscard]] bool descriptor_ok(const Rtti::Landmark &lm) noexcept
104 {
105
6/6
✓ Branch 3 → 4 taken 69 times.
✓ Branch 3 → 6 taken 1 time.
✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 7 taken 67 times.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 10 taken 67 times.
70 if (lm.expected_mangled.empty() || lm.expected_mangled.size() >= Rtti::MAX_TYPE_NAME_LEN)
106 3 return false;
107
2/2
✓ Branch 10 → 11 taken 66 times.
✓ Branch 10 → 12 taken 1 time.
67 switch (lm.indirection)
108 {
109 66 case Rtti::Indirection::PointerToObject:
110 case Rtti::Indirection::ObjectBase:
111 case Rtti::Indirection::CompleteObject:
112 case Rtti::Indirection::Any:
113 66 return true;
114 }
115 1 return false;
116 }
117 } // anonymous namespace
118
119 373 std::expected<void, Rtti::IdentifyError> Rtti::identify_pointee_typed(std::uintptr_t slot_addr,
120 PointeeType &out) noexcept
121 {
122
2/2
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 7 taken 369 times.
373 if (slot_addr < detail::MIN_VALID_PTR)
123 4 return std::unexpected(IdentifyError::BadSlotAddress);
124
125 369 const auto slot_opt = Memory::seh_read<std::uintptr_t>(slot_addr);
126
6/6
✓ Branch 9 → 10 taken 366 times.
✓ Branch 9 → 12 taken 3 times.
✓ Branch 11 → 12 taken 268 times.
✓ Branch 11 → 13 taken 98 times.
✓ Branch 14 → 15 taken 271 times.
✓ Branch 14 → 19 taken 98 times.
369 if (!slot_opt || *slot_opt < detail::MIN_VALID_PTR)
127 271 return std::unexpected(IdentifyError::UnreadableSlot);
128 98 const std::uintptr_t slot_val = *slot_opt;
129
130 98 detail::ColSite site;
131 98 bool was_pointer = false;
132 98 std::uintptr_t object_base = 0;
133 98 std::uintptr_t vtable = 0;
134
135 // Pointer-to-object first: treat slot_val as a pointer to an object and try to resolve the pointee's vtable
136 // (*slot_val). A direct object would read its own first vtable entry here, which practically never satisfies
137 // the COL signature + pSelf cross-check, so this ordering does not misclassify real direct objects.
138 98 const auto vt2_opt = Memory::seh_read<std::uintptr_t>(slot_val);
139
7/8
✓ Branch 22 → 23 taken 89 times.
✓ Branch 22 → 29 taken 9 times.
✓ Branch 24 → 25 taken 59 times.
✓ Branch 24 → 29 taken 30 times.
✓ Branch 27 → 28 taken 59 times.
✗ Branch 27 → 29 not taken.
✓ Branch 30 → 31 taken 59 times.
✓ Branch 30 → 33 taken 39 times.
98 if (vt2_opt && *vt2_opt >= detail::MIN_VALID_PTR && detail::resolve_col_site(*vt2_opt, site))
140 {
141 59 was_pointer = true;
142 59 object_base = slot_val;
143 59 vtable = *vt2_opt;
144 }
145 // Else direct object base: the slot itself is the object, its value is the vtable. Pinned to ground truth: the
146 // object base is the slot
147 // ADDRESS, the vtable is the value READ at it (not a second deref).
148
2/2
✓ Branch 34 → 35 taken 28 times.
✓ Branch 34 → 36 taken 11 times.
39 else if (detail::resolve_col_site(slot_val, site))
149 {
150 28 was_pointer = false;
151 28 object_base = slot_addr;
152 28 vtable = slot_val;
153 }
154 else
155 {
156 11 return std::unexpected(IdentifyError::NoRtti);
157 }
158
159 // Read the name into the output buffer through the same page-bounded copy the forward walker uses. A faulted or
160 // empty name is a non-resolution.
161 87 const std::size_t name_len = detail::read_name_seh(site.name_addr, out.name_buf, sizeof(out.name_buf));
162
1/2
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 46 taken 87 times.
87 if (name_len == 0)
163 return std::unexpected(IdentifyError::NoRtti);
164
165 87 out.vtable = vtable;
166 87 out.col_addr = site.col_addr;
167 87 out.td_addr = site.td_addr;
168 87 out.name_addr = site.name_addr;
169 87 out.object_base = object_base;
170 87 out.col_offset = site.col_offset;
171 87 out.pointer_value = slot_val;
172 87 out.was_pointer = was_pointer;
173 87 out.name_len = static_cast<std::uint16_t>(name_len);
174
175 // Complete object with underflow clamp: a garbage or forged col_offset larger than object_base must not wrap
176 // the address; report object_base itself in that (non-physical) case.
177
1/2
✓ Branch 46 → 47 taken 87 times.
✗ Branch 46 → 48 not taken.
87 out.complete_obj = (object_base < site.col_offset) ? object_base : object_base - site.col_offset;
178 87 return {};
179 }
180
181 357 bool Rtti::identify_pointee_type(std::uintptr_t slot_addr, PointeeType &out) noexcept
182 {
183 // The bool primitive is exactly has_value() over the typed core: one probe, one prelude walk, one
184 // implementation. Callers that need the WHY of a miss use identify_pointee_typed / identify_pointee_type_or.
185 357 return identify_pointee_typed(slot_addr, out).has_value();
186 }
187
188 6 std::size_t Rtti::reverse_scan_block(std::uintptr_t start, std::size_t slot_count, std::vector<LabeledSlot> &out,
189 std::size_t stride) noexcept
190 {
191
2/4
✓ Branch 2 → 3 taken 6 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 6 times.
6 if (start < detail::MIN_VALID_PTR || slot_count == 0)
192 return 0;
193
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 6 times.
6 if (stride == 0)
194 stride = sizeof(std::uintptr_t);
195
196 // Overflow guard mirroring find_in_pointer_table: reject a span that overflows size_t or wraps the address
197 // space.
198
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 5 times.
6 if (slot_count > SIZE_MAX / stride)
199 1 return 0;
200 5 const std::uintptr_t span = static_cast<std::uintptr_t>(slot_count * stride);
201
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 5 times.
5 if (start + span < start)
202 return 0;
203
204 5 std::size_t added = 0;
205 5 PointeeType pt;
206
2/2
✓ Branch 18 → 12 taken 13 times.
✓ Branch 18 → 19 taken 5 times.
18 for (std::size_t i = 0; i < slot_count; ++i)
207 {
208 13 const std::uintptr_t slot_addr = start + i * stride;
209
2/2
✓ Branch 13 → 14 taken 3 times.
✓ Branch 13 → 15 taken 10 times.
13 if (!identify_pointee_type(slot_addr, pt))
210 3 continue;
211 try
212 {
213
1/2
✓ Branch 15 → 16 taken 10 times.
✗ Branch 15 → 22 not taken.
10 out.push_back(LabeledSlot{slot_addr, i, pt});
214 }
215 catch (...)
216 {
217 // A reallocation failure must not escape the noexcept boundary;
218 // stop and report the slots already appended.
219 return added;
220 }
221 10 ++added;
222 }
223 5 return added;
224 }
225
226 2 std::size_t Rtti::reverse_scan_block_bytes(std::uintptr_t start, std::size_t byte_len,
227 std::vector<LabeledSlot> &out, std::size_t stride) noexcept
228 {
229
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 1 time.
2 if (stride == 0)
230 1 stride = sizeof(std::uintptr_t);
231 2 return reverse_scan_block(start, byte_len / stride, out, stride);
232 }
233
234 42 std::expected<Rtti::HealHit, Rtti::HealError> Rtti::heal_landmark(const Landmark &lm) noexcept
235 {
236 // 1. Descriptor validation. Every check below touches no memory.
237
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 6 taken 41 times.
42 if (lm.base < detail::MIN_VALID_PTR)
238 1 return std::unexpected(HealError::BadDescriptor);
239
2/2
✓ Branch 7 → 8 taken 4 times.
✓ Branch 7 → 11 taken 37 times.
41 if (!descriptor_ok(lm))
240 4 return std::unexpected(HealError::BadDescriptor);
241
2/2
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 15 taken 36 times.
37 if (lm.window > MAX_HEAL_WINDOW)
242 1 return std::unexpected(HealError::BadDescriptor);
243
2/2
✓ Branch 15 → 16 taken 35 times.
✓ Branch 15 → 17 taken 1 time.
36 const std::size_t stride = (lm.stride == 0) ? sizeof(std::uintptr_t) : lm.stride;
244
245 // 2. Single bounds computation. Unsigned arithmetic is wrap-defined; a
246 // nominal_offset that wraps the address space or lands outside the
247 // canonical user-mode window is rejected here, before any read. With
248 // a validated nominal_slot (>= MIN_VALID_PTR) and window <= MAX_HEAL_WINDOW
249 // (4096) << MIN_VALID_PTR (0x10000), the lo/hi derivations below
250 // cannot themselves underflow or wrap.
251 36 const std::uintptr_t nominal_slot = lm.base + static_cast<std::uintptr_t>(lm.nominal_offset);
252
2/2
✓ Branch 19 → 20 taken 1 time.
✓ Branch 19 → 23 taken 35 times.
36 if (!Memory::plausible_userspace_ptr(nominal_slot))
253 1 return std::unexpected(HealError::BadDescriptor);
254 35 std::uintptr_t lo = nominal_slot - lm.window;
255
1/2
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 35 times.
35 if (lo < detail::MIN_VALID_PTR)
256 lo = detail::MIN_VALID_PTR;
257 35 const std::uintptr_t hi = nominal_slot + lm.window;
258
259 35 PointeeType pt;
260
261 // 3. Nominal slot first. An exact-offset match short-circuits before the
262 // window scan, so an unchanged offset -- or a same-typed neighbour in
263 // the window -- never reaches the ambiguity test.
264
2/2
✓ Branch 26 → 27 taken 12 times.
✓ Branch 26 → 30 taken 23 times.
35 if (slot_matches(nominal_slot, lm, pt))
265 12 return make_hit(nominal_slot, lm.base, pt);
266
267 // 4. Widened grid scan, nearest distance first. Candidate slots are
268 // congruent to nominal_slot modulo stride, so every probe stays pointer-aligned
269 // to the nominal slot. At each distance ring the -d and +d slots are
270 // both evaluated before deciding, so an equidistant tie is detected
271 // rather than silently resolved to one side.
272 23 for (std::size_t k = 1;; ++k)
273 {
274 112 const std::size_t step = k * stride;
275 112 const bool minus_in = step <= (nominal_slot - lo);
276 112 const bool plus_in = step <= (hi - nominal_slot);
277
3/4
✓ Branch 31 → 32 taken 9 times.
✓ Branch 31 → 35 taken 103 times.
✓ Branch 32 → 33 taken 9 times.
✗ Branch 32 → 35 not taken.
112 if (!minus_in && !plus_in)
278 9 break;
279
280 103 bool minus_match = false;
281 103 HealHit minus_hit{};
282
5/6
✓ Branch 35 → 36 taken 103 times.
✗ Branch 35 → 39 not taken.
✓ Branch 37 → 38 taken 3 times.
✓ Branch 37 → 39 taken 100 times.
✓ Branch 40 → 41 taken 3 times.
✓ Branch 40 → 43 taken 100 times.
103 if (minus_in && slot_matches(nominal_slot - step, lm, pt))
283 {
284 3 minus_match = true;
285 3 minus_hit = make_hit(nominal_slot - step, lm.base, pt);
286 }
287
288 103 bool plus_match = false;
289 103 HealHit plus_hit{};
290
5/6
✓ Branch 43 → 44 taken 103 times.
✗ Branch 43 → 47 not taken.
✓ Branch 45 → 46 taken 12 times.
✓ Branch 45 → 47 taken 91 times.
✓ Branch 48 → 49 taken 12 times.
✓ Branch 48 → 51 taken 91 times.
103 if (plus_in && slot_matches(nominal_slot + step, lm, pt))
291 {
292 12 plus_match = true;
293 12 plus_hit = make_hit(nominal_slot + step, lm.base, pt);
294 }
295
296 // A uniquely nearest match heals; an equidistant +d/-d pair is the irreducible ambiguity and fails closed.
297
4/4
✓ Branch 51 → 52 taken 3 times.
✓ Branch 51 → 56 taken 100 times.
✓ Branch 52 → 53 taken 1 time.
✓ Branch 52 → 56 taken 2 times.
103 if (minus_match && plus_match)
298 1 return std::unexpected(HealError::Ambiguous);
299
2/2
✓ Branch 56 → 57 taken 2 times.
✓ Branch 56 → 58 taken 100 times.
102 if (minus_match)
300 2 return minus_hit;
301
2/2
✓ Branch 58 → 59 taken 11 times.
✓ Branch 58 → 60 taken 89 times.
100 if (plus_match)
302 11 return plus_hit;
303 89 }
304
305 9 return std::unexpected(HealError::NoMatch);
306 }
307
308 6 std::optional<std::ptrdiff_t> Rtti::heal_offset(const Landmark &lm) noexcept
309 {
310 6 const auto result = heal_landmark(lm);
311
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 5 times.
6 if (!result)
312 1 return std::nullopt;
313 5 return result->healed_offset;
314 }
315
316 5 std::string_view Rtti::heal_error_to_string(HealError error) noexcept
317 {
318
3/4
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 2 times.
✓ Branch 2 → 5 taken 2 times.
✗ Branch 2 → 6 not taken.
5 switch (error)
319 {
320 1 case HealError::BadDescriptor:
321 1 return "Landmark descriptor is invalid";
322 2 case HealError::NoMatch:
323 2 return "No slot in the window resolved to the expected type";
324 2 case HealError::Ambiguous:
325 2 return "Equidistant slots both match; offset is ambiguous";
326 }
327 return "Unknown heal error";
328 }
329
330 9 std::string_view Rtti::identify_error_to_string(IdentifyError error) noexcept
331 {
332
3/4
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 4 taken 3 times.
✓ Branch 2 → 5 taken 3 times.
✗ Branch 2 → 6 not taken.
9 switch (error)
333 {
334 3 case IdentifyError::BadSlotAddress:
335 3 return "Slot address is null or below the user-mode floor";
336 3 case IdentifyError::UnreadableSlot:
337 3 return "Slot read faulted or held a null/low value";
338 3 case IdentifyError::NoRtti:
339 3 return "Slot resolved to no verifiable RTTI type";
340 }
341 return "Unknown identify error";
342 }
343
344 std::expected<Rtti::FingerprintHit, Rtti::HealError>
345 14 Rtti::solve_fingerprint(std::uintptr_t base, std::span<const Landmark> fp, std::size_t window_bytes) noexcept
346 {
347 // Validation. No memory is touched until a delta is probed below.
348
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 6 taken 13 times.
14 if (base < detail::MIN_VALID_PTR)
349 1 return std::unexpected(HealError::BadDescriptor);
350
6/6
✓ Branch 7 → 8 taken 12 times.
✓ Branch 7 → 10 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 11 times.
✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 16 taken 11 times.
13 if (fp.empty() || fp.size() > MAX_FINGERPRINT_LANDMARKS)
351 2 return std::unexpected(HealError::BadDescriptor);
352
2/2
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 20 taken 10 times.
11 if (window_bytes > MAX_HEAL_WINDOW)
353 1 return std::unexpected(HealError::BadDescriptor);
354
355 10 std::size_t required_count = 0;
356
2/2
✓ Branch 41 → 21 taken 29 times.
✓ Branch 41 → 42 taken 9 times.
38 for (std::size_t i = 0; i < fp.size(); ++i)
357 {
358
1/2
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 27 taken 29 times.
29 if (!descriptor_ok(fp[i]))
359 return std::unexpected(HealError::BadDescriptor);
360
2/2
✓ Branch 35 → 28 taken 29 times.
✓ Branch 35 → 36 taken 28 times.
57 for (std::size_t j = 0; j < i; ++j)
361 {
362
2/2
✓ Branch 30 → 31 taken 1 time.
✓ Branch 30 → 34 taken 28 times.
29 if (fp[j].nominal_offset == fp[i].nominal_offset)
363 1 return std::unexpected(HealError::BadDescriptor);
364 }
365
2/2
✓ Branch 37 → 38 taken 25 times.
✓ Branch 37 → 39 taken 3 times.
28 if (fp[i].required)
366 25 ++required_count;
367 }
368 // A template with no required landmark cannot fail closed against a dense region, so it is rejected rather than
369 // guessed.
370
2/2
✓ Branch 42 → 43 taken 1 time.
✓ Branch 42 → 46 taken 8 times.
9 if (required_count == 0)
371 1 return std::unexpected(HealError::BadDescriptor);
372
373 // Enumerate uniform deltas in [-window, +window] stepping by pointer size (real-world layout shifts are
374 // pointer-granular). A delta is a candidate only when it satisfies EVERY required landmark; among candidates
375 // the most optional hits wins, and a tie at the top latches
376 // Ambiguous (fail closed).
377 8 constexpr std::ptrdiff_t step = static_cast<std::ptrdiff_t>(sizeof(std::uintptr_t));
378 8 const std::ptrdiff_t w = static_cast<std::ptrdiff_t>(window_bytes);
379
380 8 PointeeType pt;
381 8 bool have_best = false;
382 8 bool tie = false;
383 8 std::ptrdiff_t best_delta = 0;
384 8 std::size_t best_optional = 0;
385
386 72 const auto eval_delta = [&](std::ptrdiff_t delta) noexcept
387 {
388 72 std::size_t opt_hits = 0;
389
2/2
✓ Branch 26 → 4 taken 91 times.
✓ Branch 26 → 27 taken 9 times.
172 for (const Landmark &lm : fp)
390 {
391 91 const std::uintptr_t addr =
392 91 base + static_cast<std::uintptr_t>(lm.nominal_offset) + static_cast<std::uintptr_t>(delta);
393
3/4
✓ Branch 7 → 8 taken 91 times.
✗ Branch 7 → 11 not taken.
✓ Branch 9 → 10 taken 27 times.
✓ Branch 9 → 11 taken 64 times.
91 const bool ok = Memory::plausible_userspace_ptr(addr) && slot_matches(addr, lm, pt);
394
2/2
✓ Branch 12 → 13 taken 89 times.
✓ Branch 12 → 15 taken 2 times.
91 if (lm.required)
395 {
396 // A missing required landmark disqualifies this delta outright; abandon it without scoring the
397 // rest.
398
2/2
✓ Branch 13 → 14 taken 63 times.
✓ Branch 13 → 17 taken 26 times.
89 if (!ok)
399 63 return;
400 }
401
2/2
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 17 taken 1 time.
2 else if (ok)
402 {
403 1 ++opt_hits;
404 }
405 }
406
407 // Every required landmark matched: this delta is a candidate.
408
3/4
✓ Branch 27 → 28 taken 2 times.
✓ Branch 27 → 29 taken 7 times.
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 2 times.
9 if (!have_best || opt_hits > best_optional)
409 {
410 7 have_best = true;
411 7 best_optional = opt_hits;
412 7 best_delta = delta;
413 7 tie = false;
414 }
415
2/2
✓ Branch 30 → 31 taken 1 time.
✓ Branch 30 → 32 taken 1 time.
2 else if (opt_hits == best_optional)
416 {
417 1 tie = true;
418 }
419 8 };
420
421 // Iterate magnitudes 0, +step, -step, +2*step, ... so the scan is nearest-first; the decision itself is
422 // score-based, not distance-based.
423
2/2
✓ Branch 51 → 47 taken 40 times.
✓ Branch 51 → 52 taken 8 times.
48 for (std::ptrdiff_t m = 0; m <= w; m += step)
424 {
425 40 eval_delta(m);
426
2/2
✓ Branch 48 → 49 taken 32 times.
✓ Branch 48 → 50 taken 8 times.
40 if (m != 0)
427 32 eval_delta(-m);
428 }
429
430
2/2
✓ Branch 52 → 53 taken 1 time.
✓ Branch 52 → 56 taken 7 times.
8 if (!have_best)
431 1 return std::unexpected(HealError::NoMatch);
432
2/2
✓ Branch 56 → 57 taken 1 time.
✓ Branch 56 → 60 taken 6 times.
7 if (tie)
433 1 return std::unexpected(HealError::Ambiguous);
434 6 return FingerprintHit{best_delta, required_count, best_optional};
435 }
436
437 3 std::size_t Rtti::heal_report(std::span<const Landmark> landmarks, std::span<DriftEntry> out) noexcept
438 {
439
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 3 times.
3 const std::size_t written = (landmarks.size() < out.size()) ? landmarks.size() : out.size();
440
2/2
✓ Branch 19 → 8 taken 5 times.
✓ Branch 19 → 20 taken 3 times.
8 for (std::size_t i = 0; i < written; ++i)
441 {
442 5 const Landmark &landmark = landmarks[i];
443 5 DriftEntry &entry = out[i];
444 // Start from a clean entry so a failed heal cannot expose stale healed_offset/delta from a reused
445 // (non-zeroed) output buffer.
446 5 entry = DriftEntry{};
447 5 entry.name = landmark.expected_mangled;
448 5 entry.nominal_offset = landmark.nominal_offset;
449
450 5 const auto heal = heal_landmark(landmark);
451
2/2
✓ Branch 12 → 13 taken 4 times.
✓ Branch 12 → 16 taken 1 time.
5 if (heal)
452 {
453 4 entry.ok = true;
454 4 entry.healed_offset = heal->healed_offset;
455 // delta is the realised layout shift: 0 when the field did not move, signed when it did. It is the
456 // headline number a changelog wants, derived purely from the existing heal result.
457 4 entry.delta = heal->healed_offset - landmark.nominal_offset;
458 }
459 else
460 {
461 // ok stays false; healed_offset and delta stay 0 (valid only when ok).
462 1 entry.error = heal.error();
463 }
464 }
465 3 return written;
466 }
467 } // namespace DetourModKit
468