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 |