src/rtti.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /** | ||
| 2 | * @file rtti.cpp | ||
| 3 | * @brief Implementation of MSVC RTTI introspection primitives. | ||
| 4 | * | ||
| 5 | * Walks RTTICompleteObjectLocator -> TypeDescriptor -> mangled name through SEH-guarded reads. The COL layout is read | ||
| 6 | * in a single batch to minimise the number of guarded-read transitions; on MSVC each __try frame is essentially free, | ||
| 7 | * while MinGW enters the VEH fault-containment path for each transition. | ||
| 8 | * | ||
| 9 | * Every address derived from a COL field is bound-checked against the vtable's owning module range before being | ||
| 10 | * dereferenced. This guarantees that a forged or corrupted COL cannot redirect the walker to read from another loaded | ||
| 11 | * module or from an unrelated mapped region. | ||
| 12 | * | ||
| 13 | * The verified prelude (resolve_col_site) and the page-bounded name copy (read_name_seh) live in the internal | ||
| 14 | * rtti_shared.hpp header so the reverse dissector in rtti_dissect.cpp reuses them byte-for-byte rather than | ||
| 15 | * duplicating the walk. | ||
| 16 | * | ||
| 17 | * The public surface uses Address; the raw ABI sweepers below stay on std::uintptr_t because they do qword-granular | ||
| 18 | * pointer arithmetic over image memory, and the Address <-> integer punning is confined to the boundary conversions at | ||
| 19 | * each public entry point. | ||
| 20 | */ | ||
| 21 | |||
| 22 | #include "DetourModKit/rtti.hpp" | ||
| 23 | #include "DetourModKit/memory.hpp" | ||
| 24 | |||
| 25 | #include "internal/image_identity.hpp" | ||
| 26 | #include "internal/memory_guarded.hpp" | ||
| 27 | #include "internal/memory_representation_win32.hpp" | ||
| 28 | #include "internal/rtti_shared.hpp" | ||
| 29 | |||
| 30 | #include <windows.h> | ||
| 31 | |||
| 32 | #include <cstdint> | ||
| 33 | #include <cstring> | ||
| 34 | |||
| 35 | namespace DetourModKit::detail | ||
| 36 | { | ||
| 37 | template <> struct enable_representation_safe_aggregate<rtti::detail::ColHead> : std::true_type | ||
| 38 | { | ||
| 39 | }; | ||
| 40 | } // namespace DetourModKit::detail | ||
| 41 | |||
| 42 | namespace DetourModKit::detail | ||
| 43 | { | ||
| 44 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 45 | // Test-only override for the monotonic millisecond clock TypeIdentity's unresolved re-sweep throttle reads. Null in | ||
| 46 | // test builds unless a fixture installs a controllable source. | ||
| 47 | std::uint64_t (*g_rtti_resolve_clock_override)() noexcept = nullptr; | ||
| 48 | |||
| 49 | // Test-only controls for mapping-generation and module-extent transitions. | ||
| 50 | std::uint64_t (*g_rtti_image_generation_override)(std::uintptr_t address) noexcept = nullptr; | ||
| 51 | Region (*g_rtti_module_region_override)(Address address) noexcept = nullptr; | ||
| 52 | #endif | ||
| 53 | } // namespace DetourModKit::detail | ||
| 54 | |||
| 55 | namespace DetourModKit | ||
| 56 | { | ||
| 57 | namespace | ||
| 58 | { | ||
| 59 | // Monotonic millisecond clock for the TypeIdentity re-sweep throttle. GetTickCount64 reads KUSER_SHARED_DATA | ||
| 60 | // (no syscall), so gating the per-frame miss path on it is essentially free. Routed through the test override | ||
| 61 | // so the suite can advance time deterministically without a real sleep. | ||
| 62 | 26 | [[nodiscard]] std::uint64_t rtti_now_ms() noexcept | |
| 63 | { | ||
| 64 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 65 |
2/2✓ Branch 2 → 3 taken 11 times.
✓ Branch 2 → 4 taken 15 times.
|
26 | if (auto *override_fn = DetourModKit::detail::g_rtti_resolve_clock_override) |
| 66 | { | ||
| 67 | 11 | return override_fn(); | |
| 68 | } | ||
| 69 | #endif | ||
| 70 | 15 | return ::GetTickCount64(); | |
| 71 | } | ||
| 72 | |||
| 73 | // After an unresolved TypeIdentity miss, skip re-sweeping the whole module until this many milliseconds have | ||
| 74 | // elapsed. A miss is never latched permanently (the owning module may map the type later, when a DLL loads or | ||
| 75 | // a patch finishes relocating the vtable), so without a throttle a per-frame identity check for an absent type | ||
| 76 | // would re-sweep the entire module every frame, the one genuine per-frame cliff. 250 ms bounds that to at most | ||
| 77 | // ~4 module sweeps per second while keeping the eventual resolve latency sub-second once the type appears. | ||
| 78 | constexpr std::uint64_t RESOLVE_RETRY_COOLDOWN_MS = 250; | ||
| 79 | |||
| 80 | // Complete < Incomplete < Saturated, so the maximum retains the strongest reason a verdict is not | ||
| 81 | // authoritative. | ||
| 82 | 12 | [[nodiscard]] rtti::Traversal merge_traversal(rtti::Traversal a, rtti::Traversal b) noexcept | |
| 83 | { | ||
| 84 |
1/2✓ Branch 2 → 3 taken 12 times.
✗ Branch 2 → 4 not taken.
|
12 | return (static_cast<std::uint8_t>(a) < static_cast<std::uint8_t>(b)) ? b : a; |
| 85 | } | ||
| 86 | |||
| 87 | [[nodiscard]] Region current_module_region(Address address) noexcept; | ||
| 88 | [[nodiscard]] std::uint64_t current_image_stamp(std::uintptr_t module_base) noexcept; | ||
| 89 | } // namespace | ||
| 90 | |||
| 91 | 1249142 | bool rtti::detail::resolve_col_site( | |
| 92 | std::uintptr_t vtable, | ||
| 93 | const DetourModKit::detail::ModuleSpan &mod_range, | ||
| 94 | ColSite &out | ||
| 95 | ) noexcept | ||
| 96 | { | ||
| 97 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1249142 times.
|
1249142 | if (vtable < MIN_VALID_PTR) |
| 98 | ✗ | return false; | |
| 99 | |||
| 100 | // The caller supplies the vtable's owning-module span, so this overload skips the per-candidate | ||
| 101 | // memory::module_of loader lookup. Require the vtable to lie inside the supplied span so every bound check | ||
| 102 | // below anchors on the module that actually owns it - the invariant the module_of-resolving overload gets | ||
| 103 | // implicitly (the loader only resolves a module that contains the address). A candidate one past the module | ||
| 104 | // end fails closed here rather than validating against a foreign module. | ||
| 105 |
5/6✓ Branch 5 → 6 taken 1249131 times.
✓ Branch 5 → 8 taken 11 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1249131 times.
✓ Branch 10 → 11 taken 11 times.
✓ Branch 10 → 12 taken 1249131 times.
|
1249142 | if (!mod_range.valid() || !mod_range.contains(vtable)) |
| 106 | 11 | return false; | |
| 107 | |||
| 108 | // vtable[-1] holds a pointer to the RTTICompleteObjectLocator. The COL must live in the same module as the | ||
| 109 | // vtable; a COL pointer escaping the module range is the signature of a forged or relocated structure and | ||
| 110 | // aborts the walk. | ||
| 111 | 1249131 | const auto col_ptr_opt = DetourModKit::detail::guarded_read<std::uintptr_t>( | |
| 112 | 1249131 | static_cast<std::uintptr_t>(vtable + COL_OFFSET_FROM_VTABLE) | |
| 113 | ); | ||
| 114 |
3/6✓ Branch 14 → 15 taken 1249131 times.
✗ Branch 14 → 18 not taken.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 1249131 times.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 1249131 times.
|
1249131 | if (!col_ptr_opt || !mod_range.contains(*col_ptr_opt)) |
| 115 | ✗ | return false; | |
| 116 | 1249131 | const std::uintptr_t col_addr = *col_ptr_opt; | |
| 117 | |||
| 118 | // contains() above proved col_addr is in [base, end), but the guarded_read<ColHead> below pulls sizeof(ColHead) | ||
| 119 | // bytes, which a COL sitting within that many bytes of the module end would straddle past. The SEH guard | ||
| 120 | // faults cleanly on an unmapped straddle, but reject the whole-span overrun up front so the walk never reads | ||
| 121 | // COL fields out of an adjacent mapped image. (mod_range.end - col_addr cannot underflow: col_addr < end.) | ||
| 122 |
1/2✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 1249131 times.
|
1249131 | if (mod_range.end - col_addr < sizeof(ColHead)) |
| 123 | ✗ | return false; | |
| 124 | |||
| 125 | // Batched read: pulling all six COL fields through one guard matters on MinGW, where each transition enters | ||
| 126 | // the VEH and TLS fault-containment path. | ||
| 127 | 1249131 | const auto head_opt = DetourModKit::detail::guarded_read<ColHead>(col_addr); | |
| 128 |
5/6✓ Branch 27 → 28 taken 1249131 times.
✗ Branch 27 → 30 not taken.
✓ Branch 29 → 30 taken 360969 times.
✓ Branch 29 → 31 taken 888162 times.
✓ Branch 32 → 33 taken 360969 times.
✓ Branch 32 → 34 taken 888162 times.
|
1249131 | if (!head_opt || head_opt->p_type_descriptor == 0) |
| 129 | 360969 | return false; | |
| 130 | |||
| 131 | // Scope is x64 MSVC, where the COL signature is always 1 and carries pSelf, an RVA back to the COL itself. | ||
| 132 | // Reject any other signature outright: a non-x64 or corrupt signature has no pSelf to cross-check, so accepting | ||
| 133 | // it would skip the forgery guard below and fall through to the loader base unverified. The canonical | ||
| 134 | // IDA/Ghidra technique computes the image base as col_addr - p_self; cross-check that recovered base against | ||
| 135 | // the loader-reported module base so a forged p_self cannot bend the walk to another module. | ||
| 136 |
2/2✓ Branch 35 → 36 taken 887820 times.
✓ Branch 35 → 37 taken 342 times.
|
888162 | if (head_opt->signature != COL_SIGNATURE_X64) |
| 137 | 887820 | return false; | |
| 138 |
3/6✓ Branch 38 → 39 taken 342 times.
✗ Branch 38 → 41 not taken.
✗ Branch 40 → 41 not taken.
✓ Branch 40 → 42 taken 342 times.
✗ Branch 43 → 44 not taken.
✓ Branch 43 → 45 taken 342 times.
|
342 | if (head_opt->p_self == 0 || col_addr < head_opt->p_self) |
| 139 | ✗ | return false; | |
| 140 |
2/2✓ Branch 46 → 47 taken 3 times.
✓ Branch 46 → 48 taken 339 times.
|
342 | if (col_addr - head_opt->p_self != mod_range.base) |
| 141 | 3 | return false; | |
| 142 | |||
| 143 | // Compute the TypeDescriptor and name-buffer addresses from the module base plus the type-descriptor RVA. A | ||
| 144 | // bogus RVA that places the name buffer outside the module range (or wraps the address space) is rejected by | ||
| 145 | // the in-module bound check, which also requires the range to be valid. The name address is the strictly-higher | ||
| 146 | // of the two (td + 0x10), so bound-checking it implies td_addr is in range as well. | ||
| 147 | 339 | const std::uintptr_t td_addr = mod_range.base + head_opt->p_type_descriptor; | |
| 148 | 339 | const std::uintptr_t name_addr = td_addr + TD_NAME_OFFSET; | |
| 149 |
2/2✓ Branch 50 → 51 taken 4 times.
✓ Branch 50 → 52 taken 335 times.
|
339 | if (!mod_range.contains(name_addr)) |
| 150 | 4 | return false; | |
| 151 | |||
| 152 | 335 | out.col_addr = col_addr; | |
| 153 | 335 | out.td_addr = td_addr; | |
| 154 | 335 | out.name_addr = name_addr; | |
| 155 | // Carry the owning module's end so read_name_seh can clamp the name copy to this module: name_addr is proven | ||
| 156 | // below module_end (contains() above), but the string past it is not, so the read must stop at the boundary. | ||
| 157 | 335 | out.module_end = mod_range.end; | |
| 158 | 335 | out.col_offset = head_opt->offset; | |
| 159 | 335 | return true; | |
| 160 | } | ||
| 161 | |||
| 162 | 101 | bool rtti::detail::resolve_col_site(std::uintptr_t vtable, ColSite &out) noexcept | |
| 163 | { | ||
| 164 | // Reject obviously-invalid pointers before the loader lookup so a heap or tiny address never pays for a | ||
| 165 | // GetModuleHandleExW that would only fail. A valid vtable resolves its owning module, then the shared walk runs | ||
| 166 | // through the span overload; an unmapped or unowned address yields an invalid span the overload rejects. | ||
| 167 |
2/2✓ Branch 2 → 3 taken 6 times.
✓ Branch 2 → 4 taken 95 times.
|
101 | if (vtable < MIN_VALID_PTR) |
| 168 | 6 | return false; | |
| 169 | const DetourModKit::detail::ModuleSpan mod_range = | ||
| 170 | 95 | DetourModKit::detail::module_span(DetourModKit::detail::live_module_region(Address{vtable})); | |
| 171 | 95 | return resolve_col_site(vtable, mod_range, out); | |
| 172 | } | ||
| 173 | |||
| 174 | std::size_t | ||
| 175 | 125 | rtti::detail::read_name_seh(std::uintptr_t addr, char *out, std::size_t out_len, std::uintptr_t module_end) noexcept | |
| 176 | { | ||
| 177 |
2/4✓ Branch 2 → 3 taken 125 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 125 times.
|
125 | if (!out || out_len == 0) |
| 178 | ✗ | return 0; | |
| 179 | 125 | out[0] = '\0'; | |
| 180 |
1/2✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 125 times.
|
125 | if (addr < MIN_VALID_PTR) |
| 181 | ✗ | return 0; | |
| 182 | |||
| 183 | 125 | const std::size_t max_chars = out_len - 1; | |
| 184 | |||
| 185 | // The temporary is capped at rtti::MAX_TYPE_NAME_LEN, which is the documented hard upper bound for any single | ||
| 186 | // name read. Names produced by MSVC RTTI in practice never approach this bound, so the cap costs nothing. | ||
| 187 | char tmp[MAX_TYPE_NAME_LEN]; | ||
| 188 | 125 | std::size_t accum_cap = (max_chars < sizeof(tmp)) ? max_chars : sizeof(tmp); | |
| 189 | |||
| 190 | // Clamp the accumulation to the owning module's end. resolve_col_site proves name_addr < module_end, so the | ||
| 191 | // in-module span is at least one byte; capping accum_cap at (module_end - addr) stops a name with no NUL before | ||
| 192 | // the module boundary from reading forward into an adjacent mapped image and returning another module's bytes | ||
| 193 | // as a confident name. A zero module_end means no bound was supplied (only the length caps apply). | ||
| 194 |
2/2✓ Branch 7 → 8 taken 123 times.
✓ Branch 7 → 12 taken 2 times.
|
125 | if (module_end != 0) |
| 195 | { | ||
| 196 |
2/2✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 122 times.
|
123 | if (module_end <= addr) |
| 197 | 1 | return 0; | |
| 198 | 122 | const std::size_t to_module_end = static_cast<std::size_t>(module_end - addr); | |
| 199 |
2/2✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 14 taken 121 times.
|
122 | if (to_module_end < accum_cap) |
| 200 | 1 | accum_cap = to_module_end; | |
| 201 | } | ||
| 202 | else | ||
| 203 | { | ||
| 204 | // The address space is the one bound the unbounded mode still has. Capping at the bytes at or above | ||
| 205 | // addr keeps every cur inside [addr, UINTPTR_MAX], so no iteration reads from a wrapped address. | ||
| 206 | // addr >= MIN_VALID_PTR above, so the increment cannot overflow. Proof: | ||
| 207 | // RttiTest.ReadNameSehClampsToModuleEnd. | ||
| 208 | 2 | const std::size_t to_space_end = static_cast<std::size_t>(UINTPTR_MAX - addr) + 1; | |
| 209 |
2/2✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 14 taken 1 time.
|
2 | if (to_space_end < accum_cap) |
| 210 | 1 | accum_cap = to_space_end; | |
| 211 | } | ||
| 212 | |||
| 213 | 124 | std::size_t written = 0; | |
| 214 | 124 | bool found_nul = false; | |
| 215 | 124 | bool read_failed = false; | |
| 216 | |||
| 217 |
2/2✓ Branch 29 → 15 taken 124 times.
✓ Branch 29 → 30 taken 9 times.
|
133 | while (written < accum_cap) |
| 218 | { | ||
| 219 | 124 | const std::uintptr_t cur = addr + written; | |
| 220 | 124 | const std::size_t to_page_end = static_cast<std::size_t>(PAGE_SIZE - (cur & PAGE_MASK)); | |
| 221 | 124 | const std::size_t remaining = accum_cap - written; | |
| 222 |
2/2✓ Branch 15 → 16 taken 88 times.
✓ Branch 15 → 17 taken 36 times.
|
124 | const std::size_t chunk = (to_page_end < remaining) ? to_page_end : remaining; |
| 223 | |||
| 224 | char buf[256]; | ||
| 225 | 124 | const std::size_t this_read = (chunk > sizeof(buf)) ? sizeof(buf) : chunk; | |
| 226 |
2/2✓ Branch 19 → 20 taken 3 times.
✓ Branch 19 → 21 taken 121 times.
|
124 | if (!DetourModKit::detail::guarded_read_bytes(cur, buf, this_read)) |
| 227 | { | ||
| 228 | 3 | read_failed = true; | |
| 229 | 115 | break; | |
| 230 | } | ||
| 231 | |||
| 232 | 121 | const auto *nul = static_cast<const char *>(std::memchr(buf, 0, this_read)); | |
| 233 |
2/2✓ Branch 22 → 23 taken 112 times.
✓ Branch 22 → 24 taken 9 times.
|
121 | const std::size_t copy_len = nul ? static_cast<std::size_t>(nul - buf) : this_read; |
| 234 | 121 | std::memcpy(tmp + written, buf, copy_len); | |
| 235 | 121 | written += copy_len; | |
| 236 |
2/2✓ Branch 25 → 26 taken 112 times.
✓ Branch 25 → 27 taken 9 times.
|
121 | if (nul) |
| 237 | { | ||
| 238 | 112 | found_nul = true; | |
| 239 | 112 | break; | |
| 240 | } | ||
| 241 | } | ||
| 242 | |||
| 243 | // Commit only when either a NUL was found or the buffer was filled to accum_cap without faulting. A read fault | ||
| 244 | // before either condition is reported as a clean failure: the caller-visible buffer stays empty and the return | ||
| 245 | // is zero. | ||
| 246 |
3/4✓ Branch 30 → 31 taken 3 times.
✓ Branch 30 → 33 taken 121 times.
✓ Branch 31 → 32 taken 3 times.
✗ Branch 31 → 33 not taken.
|
124 | if (read_failed && !found_nul) |
| 247 | { | ||
| 248 | 3 | out[0] = '\0'; | |
| 249 | 3 | return 0; | |
| 250 | } | ||
| 251 | |||
| 252 | 121 | std::memcpy(out, tmp, written); | |
| 253 | 121 | out[written] = '\0'; | |
| 254 | 121 | return written; | |
| 255 | } | ||
| 256 | |||
| 257 | namespace | ||
| 258 | { | ||
| 259 | /** | ||
| 260 | * @brief Resolves the address of the mangled-name buffer for @p vtable, plus its owning-module end. | ||
| 261 | * @details Thin wrapper over rtti::detail::resolve_col_site that keeps the forward walker's "name address or | ||
| 262 | * zero" contract. Every failure mode of the prelude collapses to a 0 return. @p module_end_out | ||
| 263 | * receives the owning module's exclusive end so the caller's name read can clamp to the module (the | ||
| 264 | * RTTI-name over-read guard); it is left untouched on a 0 return. | ||
| 265 | * @return Address of the first byte of the NUL-terminated name, or 0 on any failure. | ||
| 266 | */ | ||
| 267 | 62 | std::uintptr_t resolve_name_site(std::uintptr_t vtable, std::uintptr_t &module_end_out) noexcept | |
| 268 | { | ||
| 269 | 62 | rtti::detail::ColSite site; | |
| 270 |
2/2✓ Branch 3 → 4 taken 13 times.
✓ Branch 3 → 5 taken 49 times.
|
62 | if (!rtti::detail::resolve_col_site(vtable, site)) |
| 271 | 13 | return 0; | |
| 272 | 49 | module_end_out = site.module_end; | |
| 273 | 49 | return site.name_addr; | |
| 274 | } | ||
| 275 | } // anonymous namespace | ||
| 276 | |||
| 277 | 11 | std::optional<std::string> rtti::type_name_of(Address vtable, std::size_t max_len) noexcept | |
| 278 | { | ||
| 279 | 11 | std::uintptr_t module_end = 0; | |
| 280 | 11 | const std::uintptr_t name_addr = resolve_name_site(vtable.raw(), module_end); | |
| 281 |
2/2✓ Branch 4 → 5 taken 5 times.
✓ Branch 4 → 6 taken 6 times.
|
11 | if (name_addr == 0) |
| 282 | 5 | return std::nullopt; | |
| 283 | |||
| 284 |
2/2✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 5 times.
|
6 | if (max_len == 0) |
| 285 | 1 | max_len = DEFAULT_TYPE_NAME_MAX; | |
| 286 |
1/2✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 6 times.
|
6 | if (max_len > MAX_TYPE_NAME_LEN) |
| 287 | ✗ | max_len = MAX_TYPE_NAME_LEN; | |
| 288 | |||
| 289 | 6 | std::string out; | |
| 290 | try | ||
| 291 | { | ||
| 292 |
1/2✓ Branch 11 → 12 taken 6 times.
✗ Branch 11 → 22 not taken.
|
6 | out.resize(max_len + 1); |
| 293 | } | ||
| 294 | ✗ | catch (...) | |
| 295 | { | ||
| 296 | ✗ | return std::nullopt; | |
| 297 | ✗ | } | |
| 298 | 6 | const std::size_t len = detail::read_name_seh(name_addr, out.data(), out.size(), module_end); | |
| 299 |
2/2✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 17 taken 5 times.
|
6 | if (len == 0) |
| 300 | 1 | return std::nullopt; | |
| 301 | 5 | out.resize(len); | |
| 302 | 5 | return out; | |
| 303 | 6 | } | |
| 304 | |||
| 305 | 6 | std::size_t rtti::type_name_into(Address vtable, char *out, std::size_t out_len) noexcept | |
| 306 | { | ||
| 307 |
4/4✓ Branch 2 → 3 taken 5 times.
✓ Branch 2 → 4 taken 1 time.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 4 times.
|
6 | if (!out || out_len == 0) |
| 308 | 2 | return 0; | |
| 309 | 4 | out[0] = '\0'; | |
| 310 | 4 | std::uintptr_t module_end = 0; | |
| 311 | 4 | const std::uintptr_t name_addr = resolve_name_site(vtable.raw(), module_end); | |
| 312 |
2/2✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 2 times.
|
4 | if (name_addr == 0) |
| 313 | 2 | return 0; | |
| 314 | 2 | return detail::read_name_seh(name_addr, out, out_len, module_end); | |
| 315 | } | ||
| 316 | |||
| 317 | 12 | rtti::NameRead rtti::type_name_checked(Address vtable, char *out, std::size_t out_len) noexcept | |
| 318 | { | ||
| 319 | 12 | NameRead result; | |
| 320 |
2/4✓ Branch 2 → 3 taken 12 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 12 times.
|
12 | if (!out || out_len == 0) |
| 321 | ✗ | return result; | |
| 322 | 12 | out[0] = '\0'; | |
| 323 | 12 | std::uintptr_t module_end = 0; | |
| 324 | 12 | const std::uintptr_t name_addr = resolve_name_site(vtable.raw(), module_end); | |
| 325 |
2/2✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 11 times.
|
12 | if (name_addr == 0) |
| 326 | 1 | return result; | |
| 327 | |||
| 328 | 11 | const std::size_t written = detail::read_name_seh(name_addr, out, out_len, module_end); | |
| 329 |
2/2✓ Branch 10 → 11 taken 7 times.
✓ Branch 10 → 16 taken 4 times.
|
11 | if (written == 0) |
| 330 | { | ||
| 331 | // Zero bytes copied means a complete empty name or a read failure. Probe the first byte at every capacity. | ||
| 332 | // A failed probe keeps the default Failed status. An empty RTTI name is still a complete read. A readable | ||
| 333 | // non-terminator means that the name exists but no byte was copied. This case reports Truncated. | ||
| 334 | // RttiTest.TypeNameChecked_EmptyName* pins the matrix. | ||
| 335 | 7 | char first = 1; | |
| 336 |
2/2✓ Branch 12 → 13 taken 4 times.
✓ Branch 12 → 14 taken 3 times.
|
7 | if (DetourModKit::detail::guarded_read_bytes(name_addr, &first, 1)) |
| 337 | 4 | result.status = (first == '\0') ? NameStatus::Ok : NameStatus::Truncated; | |
| 338 | 7 | return result; | |
| 339 | } | ||
| 340 | 4 | result.written = written; | |
| 341 | |||
| 342 | // read_name_seh stopped at the first NUL or at the length cap. The byte immediately after the copied prefix | ||
| 343 | // decides which: the terminator (complete) or another name byte (truncated). A byte at or past the module | ||
| 344 | // boundary, or one that faults, means the name has no in-module terminator within the cap - not a trustworthy | ||
| 345 | // whole name, so report Truncated so an identity comparison rejects it rather than matching a prefix. | ||
| 346 | 4 | const std::uintptr_t next = name_addr + written; | |
| 347 |
2/4✓ Branch 16 → 17 taken 4 times.
✗ Branch 16 → 19 not taken.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 4 times.
|
4 | if (module_end != 0 && next >= module_end) |
| 348 | { | ||
| 349 | ✗ | result.status = NameStatus::Truncated; | |
| 350 | ✗ | return result; | |
| 351 | } | ||
| 352 | 4 | char probe = 1; | |
| 353 |
1/2✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 4 times.
|
4 | if (!DetourModKit::detail::guarded_read_bytes(next, &probe, 1)) |
| 354 | { | ||
| 355 | ✗ | result.status = NameStatus::Truncated; | |
| 356 | ✗ | return result; | |
| 357 | } | ||
| 358 | 4 | result.status = (probe == '\0') ? NameStatus::Ok : NameStatus::Truncated; | |
| 359 | 4 | return result; | |
| 360 | } | ||
| 361 | |||
| 362 | 37 | bool rtti::vtable_is_type(Address vtable, std::string_view expected) noexcept | |
| 363 | { | ||
| 364 |
6/6✓ Branch 3 → 4 taken 36 times.
✓ Branch 3 → 6 taken 1 time.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 35 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 35 times.
|
37 | if (expected.empty() || expected.size() >= MAX_TYPE_NAME_LEN) |
| 365 | 2 | return false; | |
| 366 | |||
| 367 | 35 | std::uintptr_t module_end = 0; | |
| 368 | 35 | const std::uintptr_t name_addr = resolve_name_site(vtable.raw(), module_end); | |
| 369 |
2/2✓ Branch 12 → 13 taken 5 times.
✓ Branch 12 → 14 taken 30 times.
|
35 | if (name_addr == 0) |
| 370 | 5 | return false; | |
| 371 | |||
| 372 | // Read expected.size() + 1 bytes to capture the terminating NUL. A name that lacks the NUL at expected.size() | ||
| 373 | // is either longer (a superstring) or unreadable past that point; both are rejected. | ||
| 374 | char buf[MAX_TYPE_NAME_LEN + 1]; | ||
| 375 | 30 | const std::size_t need = expected.size() + 1; | |
| 376 | // Fail closed if the compare would read to or past the owning module's end. A matching in-module name has its | ||
| 377 | // NUL before module_end, so a name buffer within `need` bytes of the boundary cannot equal `expected`, and the | ||
| 378 | // read must not spill into an adjacent mapped image. (module_end > name_addr is guaranteed by resolve_col_site; | ||
| 379 | // the <= guard is defensive.) | ||
| 380 |
2/4✓ Branch 15 → 16 taken 30 times.
✗ Branch 15 → 17 not taken.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 30 times.
|
30 | if (module_end <= name_addr || module_end - name_addr < need) |
| 381 | ✗ | return false; | |
| 382 |
1/2✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 30 times.
|
30 | if (!DetourModKit::detail::guarded_read_bytes(name_addr, buf, need)) |
| 383 | ✗ | return false; | |
| 384 |
2/2✓ Branch 22 → 23 taken 4 times.
✓ Branch 22 → 24 taken 26 times.
|
30 | if (buf[expected.size()] != '\0') |
| 385 | 4 | return false; | |
| 386 | 26 | return std::memcmp(buf, expected.data(), expected.size()) == 0; | |
| 387 | } | ||
| 388 | |||
| 389 | 1 | void rtti::PointerTableCache::reset() noexcept | |
| 390 | { | ||
| 391 |
1/2✗ Branch 6 → 3 not taken.
✓ Branch 6 → 7 taken 1 time.
|
2 | while (m_writer.test_and_set(std::memory_order_acquire)) |
| 392 | ✗ | (void)::SwitchToThread(); | |
| 393 | |||
| 394 | 1 | const std::uint32_t sequence = m_seq.load(std::memory_order_relaxed); | |
| 395 | 1 | m_seq.store(sequence + 1, std::memory_order_relaxed); | |
| 396 | std::atomic_thread_fence(std::memory_order_release); | ||
| 397 | 1 | m_vtable.store(Address{}, std::memory_order_relaxed); | |
| 398 | 1 | m_image_base.store(Address{}, std::memory_order_relaxed); | |
| 399 | 1 | m_generation.store(0, std::memory_order_relaxed); | |
| 400 | 1 | m_epoch.fetch_add(1, std::memory_order_relaxed); | |
| 401 | std::atomic_thread_fence(std::memory_order_release); | ||
| 402 | 1 | m_seq.store(sequence + 2, std::memory_order_relaxed); | |
| 403 | 1 | m_writer.clear(std::memory_order_release); | |
| 404 | 1 | } | |
| 405 | |||
| 406 | 25 | std::optional<Address> rtti::find_in_pointer_table( | |
| 407 | Address table, | ||
| 408 | std::size_t slot_count, | ||
| 409 | std::string_view expected, | ||
| 410 | std::atomic<Address> *vtable_cache, | ||
| 411 | std::size_t stride | ||
| 412 | ) noexcept | ||
| 413 | { | ||
| 414 |
8/8✓ Branch 3 → 4 taken 24 times.
✓ Branch 3 → 7 taken 1 time.
✓ Branch 4 → 5 taken 23 times.
✓ Branch 4 → 7 taken 1 time.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 22 times.
✓ Branch 9 → 10 taken 3 times.
✓ Branch 9 → 11 taken 22 times.
|
25 | if (table.raw() < detail::MIN_VALID_PTR || slot_count == 0 || expected.empty()) |
| 415 | 3 | return std::nullopt; | |
| 416 |
2/2✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 21 times.
|
22 | if (stride == 0) |
| 417 | 1 | stride = sizeof(std::uintptr_t); | |
| 418 | |||
| 419 | // Overflow guard on the addressable span. Two failure modes are possible: | ||
| 420 | // 1. (slot_count * stride) overflows std::size_t. | ||
| 421 | // 2. (table + span) overflows std::uintptr_t. | ||
| 422 | // The first check rejects (1) directly; the second catches (2) by comparing the wrapped sum against the base. A | ||
| 423 | // malformed (table, stride, slot_count) tuple is treated as an empty table. | ||
| 424 |
1/2✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 22 times.
|
22 | if (slot_count > SIZE_MAX / stride) |
| 425 | ✗ | return std::nullopt; | |
| 426 | 22 | const std::uintptr_t table_raw = table.raw(); | |
| 427 | 22 | const std::uintptr_t span = static_cast<std::uintptr_t>(slot_count * stride); | |
| 428 |
2/2✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 18 taken 21 times.
|
22 | if (table_raw + span < table_raw) |
| 429 | 1 | return std::nullopt; | |
| 430 | |||
| 431 | 21 | Address cached_vt{}; | |
| 432 |
2/2✓ Branch 18 → 19 taken 15 times.
✓ Branch 18 → 20 taken 6 times.
|
21 | if (vtable_cache) |
| 433 | 15 | cached_vt = vtable_cache->load(std::memory_order_relaxed); | |
| 434 | |||
| 435 | 22 | const auto scan = [&](Address warm_vtable) noexcept -> std::optional<Address> | |
| 436 | { | ||
| 437 |
2/2✓ Branch 43 → 3 taken 38 times.
✓ Branch 43 → 44 taken 5 times.
|
43 | for (std::size_t i = 0; i < slot_count; ++i) |
| 438 | { | ||
| 439 | 38 | const std::uintptr_t slot_addr = table_raw + i * stride; | |
| 440 | 38 | const auto obj_opt = DetourModKit::detail::guarded_read<std::uintptr_t>(slot_addr); | |
| 441 |
5/6✓ Branch 5 → 6 taken 38 times.
✗ Branch 5 → 8 not taken.
✓ Branch 7 → 8 taken 13 times.
✓ Branch 7 → 9 taken 25 times.
✓ Branch 10 → 11 taken 13 times.
✓ Branch 10 → 12 taken 25 times.
|
38 | if (!obj_opt || *obj_opt < detail::MIN_VALID_PTR) |
| 442 | 15 | continue; | |
| 443 | 25 | const std::uintptr_t obj = *obj_opt; | |
| 444 | |||
| 445 | 25 | const auto vt_opt = DetourModKit::detail::guarded_read<std::uintptr_t>(obj); | |
| 446 |
3/6✓ Branch 15 → 16 taken 25 times.
✗ Branch 15 → 18 not taken.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 25 times.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 25 times.
|
25 | if (!vt_opt || *vt_opt < detail::MIN_VALID_PTR) |
| 447 | ✗ | continue; | |
| 448 | 25 | const Address vt{*vt_opt}; | |
| 449 | |||
| 450 |
2/2✓ Branch 25 → 26 taken 7 times.
✓ Branch 25 → 32 taken 18 times.
|
25 | if (warm_vtable) |
| 451 | { | ||
| 452 |
2/2✓ Branch 27 → 28 taken 5 times.
✓ Branch 27 → 31 taken 2 times.
|
7 | if (vt == warm_vtable) |
| 453 | 5 | return Address{obj}; | |
| 454 | 2 | continue; | |
| 455 | } | ||
| 456 | |||
| 457 |
2/2✓ Branch 33 → 34 taken 12 times.
✓ Branch 33 → 39 taken 6 times.
|
18 | if (vtable_is_type(vt, expected)) |
| 458 | { | ||
| 459 |
2/2✓ Branch 34 → 35 taken 7 times.
✓ Branch 34 → 36 taken 5 times.
|
12 | if (vtable_cache) |
| 460 | 7 | vtable_cache->store(vt, std::memory_order_relaxed); | |
| 461 | 12 | return Address{obj}; | |
| 462 | } | ||
| 463 | } | ||
| 464 | 5 | return std::nullopt; | |
| 465 | 21 | }; | |
| 466 | |||
| 467 |
2/2✓ Branch 21 → 22 taken 6 times.
✓ Branch 21 → 30 taken 15 times.
|
21 | if (cached_vt) |
| 468 | { | ||
| 469 |
2/2✓ Branch 24 → 25 taken 5 times.
✓ Branch 24 → 26 taken 1 time.
|
6 | if (const auto warm = scan(cached_vt)) |
| 470 | 5 | return warm; | |
| 471 | |||
| 472 | // No slot carried the cached vtable. Clear only the value this call observed, then run one cold pass so a | ||
| 473 | // remapped table refreshes immediately instead of remaining wedged on a stale address. | ||
| 474 |
1/2✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 30 not taken.
|
1 | if (vtable_cache) |
| 475 | { | ||
| 476 | 1 | Address expected_cache = cached_vt; | |
| 477 | 1 | (void)vtable_cache->compare_exchange_strong(expected_cache, Address{}, std::memory_order_relaxed); | |
| 478 | } | ||
| 479 | } | ||
| 480 | 16 | return scan(Address{}); | |
| 481 | } | ||
| 482 | |||
| 483 | 12 | std::optional<Address> rtti::find_in_pointer_table( | |
| 484 | Address table, | ||
| 485 | std::size_t slot_count, | ||
| 486 | std::string_view expected, | ||
| 487 | PointerTableCache &cache, | ||
| 488 | std::size_t stride | ||
| 489 | ) noexcept | ||
| 490 | { | ||
| 491 | struct CacheSnapshot | ||
| 492 | { | ||
| 493 | Address vtable{}; | ||
| 494 | Address image_base{}; | ||
| 495 | std::uint64_t generation{0}; | ||
| 496 | std::uint64_t epoch{0}; | ||
| 497 | }; | ||
| 498 | |||
| 499 | 12 | const auto load_cache = [&cache]() noexcept -> CacheSnapshot | |
| 500 | { | ||
| 501 | 12 | constexpr std::size_t MAX_ATTEMPTS = 16; | |
| 502 |
1/2✓ Branch 39 → 3 taken 12 times.
✗ Branch 39 → 40 not taken.
|
12 | for (std::size_t attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) |
| 503 | { | ||
| 504 | 12 | const std::uint32_t sequence = cache.m_seq.load(std::memory_order_acquire); | |
| 505 |
1/2✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 12 times.
|
12 | if ((sequence & 1U) != 0U) |
| 506 | ✗ | continue; | |
| 507 | const CacheSnapshot snapshot{ | ||
| 508 | 12 | cache.m_vtable.load(std::memory_order_relaxed), | |
| 509 | 12 | cache.m_image_base.load(std::memory_order_relaxed), | |
| 510 | 12 | cache.m_generation.load(std::memory_order_relaxed), | |
| 511 | 24 | cache.m_epoch.load(std::memory_order_relaxed) | |
| 512 | 24 | }; | |
| 513 | std::atomic_thread_fence(std::memory_order_acquire); | ||
| 514 |
1/2✓ Branch 36 → 37 taken 12 times.
✗ Branch 36 → 38 not taken.
|
24 | if (cache.m_seq.load(std::memory_order_relaxed) == sequence) |
| 515 | 12 | return snapshot; | |
| 516 | } | ||
| 517 | ✗ | return {}; | |
| 518 | 12 | }; | |
| 519 | |||
| 520 | 7 | const auto publish_cache = [&cache](CacheSnapshot desired, CacheSnapshot expected_snapshot) noexcept | |
| 521 | { | ||
| 522 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 7 times.
|
14 | if (cache.m_writer.test_and_set(std::memory_order_acquire)) |
| 523 | ✗ | return; | |
| 524 | 7 | if (cache.m_vtable.load(std::memory_order_relaxed) != expected_snapshot.vtable || | |
| 525 |
1/2✓ Branch 11 → 12 taken 7 times.
✗ Branch 11 → 28 not taken.
|
7 | cache.m_image_base.load(std::memory_order_relaxed) != expected_snapshot.image_base || |
| 526 |
3/6✓ Branch 8 → 9 taken 7 times.
✗ Branch 8 → 28 not taken.
✓ Branch 19 → 20 taken 7 times.
✗ Branch 19 → 28 not taken.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 40 taken 7 times.
|
28 | cache.m_generation.load(std::memory_order_relaxed) != expected_snapshot.generation || |
| 527 |
1/2✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 7 times.
|
14 | cache.m_epoch.load(std::memory_order_relaxed) != expected_snapshot.epoch) |
| 528 | { | ||
| 529 | ✗ | cache.m_writer.clear(std::memory_order_release); | |
| 530 | ✗ | return; | |
| 531 | } | ||
| 532 | 7 | const std::uint32_t sequence = cache.m_seq.load(std::memory_order_relaxed); | |
| 533 | 7 | cache.m_seq.store(sequence + 1, std::memory_order_relaxed); | |
| 534 | std::atomic_thread_fence(std::memory_order_release); | ||
| 535 | 7 | cache.m_vtable.store(desired.vtable, std::memory_order_relaxed); | |
| 536 | 7 | cache.m_image_base.store(desired.image_base, std::memory_order_relaxed); | |
| 537 | 7 | cache.m_generation.store(desired.generation, std::memory_order_relaxed); | |
| 538 | std::atomic_thread_fence(std::memory_order_release); | ||
| 539 | 7 | cache.m_seq.store(sequence + 2, std::memory_order_relaxed); | |
| 540 | 7 | cache.m_writer.clear(std::memory_order_release); | |
| 541 | 12 | }; | |
| 542 | |||
| 543 |
1/2✓ Branch 89 → 3 taken 12 times.
✗ Branch 89 → 90 not taken.
|
12 | for (std::size_t attempt = 0; attempt < 2; ++attempt) |
| 544 | { | ||
| 545 | 12 | const CacheSnapshot snapshot = load_cache(); | |
| 546 |
4/6✓ Branch 5 → 6 taken 6 times.
✓ Branch 5 → 13 taken 6 times.
✓ Branch 7 → 8 taken 6 times.
✗ Branch 7 → 13 not taken.
✓ Branch 8 → 9 taken 6 times.
✗ Branch 8 → 13 not taken.
|
18 | const bool warm = snapshot.vtable && snapshot.image_base && snapshot.generation != 0 && |
| 547 |
2/2✓ Branch 11 → 12 taken 4 times.
✓ Branch 11 → 13 taken 2 times.
|
6 | current_image_stamp(snapshot.image_base.raw()) == snapshot.generation; |
| 548 |
2/2✓ Branch 14 → 15 taken 4 times.
✓ Branch 14 → 16 taken 8 times.
|
12 | std::atomic<Address> candidate_cache{warm ? snapshot.vtable : Address{}}; |
| 549 | 12 | const auto hit = find_in_pointer_table(table, slot_count, expected, &candidate_cache, stride); | |
| 550 |
2/2✓ Branch 20 → 21 taken 3 times.
✓ Branch 20 → 28 taken 9 times.
|
12 | if (!hit) |
| 551 | { | ||
| 552 |
2/2✓ Branch 22 → 23 taken 2 times.
✓ Branch 22 → 27 taken 1 time.
|
3 | if (snapshot.vtable) |
| 553 | 2 | publish_cache({}, snapshot); | |
| 554 | 12 | return std::nullopt; | |
| 555 | } | ||
| 556 | |||
| 557 | 9 | const Address candidate_vtable = candidate_cache.load(std::memory_order_relaxed); | |
| 558 |
5/6✓ Branch 29 → 30 taken 4 times.
✓ Branch 29 → 33 taken 5 times.
✓ Branch 31 → 32 taken 4 times.
✗ Branch 31 → 33 not taken.
✓ Branch 34 → 35 taken 4 times.
✓ Branch 34 → 43 taken 5 times.
|
9 | if (warm && candidate_vtable == snapshot.vtable) |
| 559 | { | ||
| 560 |
1/2✓ Branch 37 → 38 taken 4 times.
✗ Branch 37 → 39 not taken.
|
4 | if (current_image_stamp(snapshot.image_base.raw()) == snapshot.generation) |
| 561 | 4 | return hit; | |
| 562 | ✗ | publish_cache({}, snapshot); | |
| 563 | ✗ | continue; | |
| 564 | } | ||
| 565 | |||
| 566 | 5 | const Region image_before = current_module_region(candidate_vtable); | |
| 567 | const std::uint64_t generation_before = | ||
| 568 |
1/2✓ Branch 45 → 46 taken 5 times.
✗ Branch 45 → 48 not taken.
|
5 | image_before.base ? current_image_stamp(image_before.base.raw()) : 0; |
| 569 |
4/8✓ Branch 50 → 51 taken 5 times.
✗ Branch 50 → 54 not taken.
✓ Branch 51 → 52 taken 5 times.
✗ Branch 51 → 54 not taken.
✗ Branch 53 → 54 not taken.
✓ Branch 53 → 55 taken 5 times.
✗ Branch 56 → 57 not taken.
✓ Branch 56 → 61 taken 5 times.
|
5 | if (!image_before.base || generation_before == 0 || !vtable_is_type(candidate_vtable, expected)) |
| 570 | { | ||
| 571 | ✗ | publish_cache({}, snapshot); | |
| 572 | ✗ | return std::nullopt; | |
| 573 | } | ||
| 574 | |||
| 575 | 5 | const auto object_vtable = DetourModKit::detail::guarded_read<std::uintptr_t>(hit->raw()); | |
| 576 | 5 | const Region image_after = current_module_region(candidate_vtable); | |
| 577 |
1/2✓ Branch 66 → 67 taken 5 times.
✗ Branch 66 → 69 not taken.
|
5 | const std::uint64_t generation_after = image_after.base ? current_image_stamp(image_after.base.raw()) : 0; |
| 578 |
5/10✓ Branch 71 → 72 taken 5 times.
✗ Branch 71 → 78 not taken.
✓ Branch 74 → 75 taken 5 times.
✗ Branch 74 → 78 not taken.
✓ Branch 76 → 77 taken 5 times.
✗ Branch 76 → 78 not taken.
✗ Branch 77 → 78 not taken.
✓ Branch 77 → 79 taken 5 times.
✗ Branch 80 → 81 not taken.
✓ Branch 80 → 85 taken 5 times.
|
5 | if (!object_vtable || *object_vtable != candidate_vtable.raw() || image_after.base != image_before.base || |
| 579 | generation_after != generation_before) | ||
| 580 | { | ||
| 581 | ✗ | publish_cache({}, snapshot); | |
| 582 | ✗ | continue; | |
| 583 | } | ||
| 584 | |||
| 585 | 5 | publish_cache({candidate_vtable, image_after.base, generation_after}, snapshot); | |
| 586 | 5 | return hit; | |
| 587 | } | ||
| 588 | ✗ | return std::nullopt; | |
| 589 | } | ||
| 590 | |||
| 591 | namespace | ||
| 592 | { | ||
| 593 | // Upper bound on distinct sub-object vtables collected for one mangled name. Multiple/virtual inheritance | ||
| 594 | // produces a handful at most; the cap keeps the reverse scan allocation-free (matches live in a stack array) | ||
| 595 | // and bounds a pathological duplicate-type image. | ||
| 596 | inline constexpr std::size_t MAX_REVERSE_MATCHES = 64; | ||
| 597 | |||
| 598 | // Cap on the readable non-executable sections collected into the stack range buffer for one reverse sweep. A | ||
| 599 | // normal PE keeps vtables and their RTTI meta-pointers in one or two sections (.rdata, .data), so this is | ||
| 600 | // generous; a scope with more qualifying sections than fit reports Traversal::Saturated rather than silently | ||
| 601 | // dropping the overflow. | ||
| 602 | inline constexpr std::size_t MAX_RTTI_SCAN_RANGES = 32; | ||
| 603 | |||
| 604 | // One readable, non-executable image-resident scan window. | ||
| 605 | struct ScanRange | ||
| 606 | { | ||
| 607 | std::uintptr_t begin = 0; | ||
| 608 | std::uintptr_t end = 0; | ||
| 609 | }; | ||
| 610 | |||
| 611 | // A validated reverse-RTTI hit: the vtable and the COL.offset of the sub-object it belongs to (0 == | ||
| 612 | // primary/most-derived). | ||
| 613 | struct VtMatch | ||
| 614 | { | ||
| 615 | std::uintptr_t vtable = 0; | ||
| 616 | std::uint32_t col_offset = 0; | ||
| 617 | }; | ||
| 618 | |||
| 619 | 81 | [[nodiscard]] Region current_module_region(Address address) noexcept | |
| 620 | { | ||
| 621 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 622 |
2/2✓ Branch 2 → 3 taken 7 times.
✓ Branch 2 → 4 taken 74 times.
|
81 | if (auto *override_fn = DetourModKit::detail::g_rtti_module_region_override) |
| 623 | 7 | return override_fn(address); | |
| 624 | #endif | ||
| 625 | 74 | return DetourModKit::detail::live_module_region(address); | |
| 626 | } | ||
| 627 | |||
| 628 | 90 | [[nodiscard]] std::uint64_t current_image_stamp(std::uintptr_t module_base) noexcept | |
| 629 | { | ||
| 630 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 631 |
2/2✓ Branch 2 → 3 taken 52 times.
✓ Branch 2 → 4 taken 38 times.
|
90 | if (auto *override_fn = DetourModKit::detail::g_rtti_image_generation_override) |
| 632 | 52 | return override_fn(module_base); | |
| 633 | #endif | ||
| 634 | 38 | return DetourModKit::detail::image_generation_token(module_base); | |
| 635 | } | ||
| 636 | |||
| 637 | /** | ||
| 638 | * @brief Enumerates the module's readable, non-executable, non-discardable sections, where MSVC keeps vtables | ||
| 639 | * and their RTTI meta-pointers (.rdata for a normal /GR image, .data for a packed or section-merged | ||
| 640 | * one). | ||
| 641 | * @details .text is skipped on purpose: a vtable's [-1] COL meta-slot never lives in executable code, and | ||
| 642 | * sweeping code pages qword-by-qword would multiply the one-time cost for nothing. Every header field | ||
| 643 | * is bound- and signature-checked so a malformed or hostile image fails closed instead of being read | ||
| 644 | * through. | ||
| 645 | * @return The number of ranges written into @p out; 0 means the PE headers could not be parsed and the caller | ||
| 646 | * falls back to the whole module image. @p completeness is set to @ref rtti::Traversal::Incomplete when | ||
| 647 | * a section header could not be read after a valid prefix, or @ref rtti::Traversal::Saturated when a | ||
| 648 | * qualifying section did not fit @p out; it is left @ref rtti::Traversal::Complete otherwise (including | ||
| 649 | * the 0-return, where the caller's whole-image fallback determines its own completeness). | ||
| 650 | */ | ||
| 651 | 70 | std::size_t collect_rtti_scan_ranges( | |
| 652 | DetourModKit::detail::ModuleSpan mod, | ||
| 653 | ScanRange *out, | ||
| 654 | std::size_t cap, | ||
| 655 | rtti::Traversal &completeness | ||
| 656 | ) noexcept | ||
| 657 | { | ||
| 658 | 70 | const auto dos = DetourModKit::detail::guarded_read<IMAGE_DOS_HEADER>(mod.base); | |
| 659 |
7/8✓ Branch 4 → 5 taken 66 times.
✓ Branch 4 → 9 taken 4 times.
✓ Branch 6 → 7 taken 20 times.
✓ Branch 6 → 9 taken 46 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 20 times.
✓ Branch 11 → 12 taken 50 times.
✓ Branch 11 → 13 taken 20 times.
|
70 | if (!dos || dos->e_magic != IMAGE_DOS_SIGNATURE || dos->e_lfanew <= 0) |
| 660 | 50 | return 0; | |
| 661 | |||
| 662 | 20 | const std::uintptr_t nt_offset = static_cast<std::uint32_t>(dos->e_lfanew); | |
| 663 |
1/2✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 20 times.
|
20 | if (nt_offset >= mod.end - mod.base) |
| 664 | ✗ | return 0; | |
| 665 | 20 | const std::uintptr_t nt_addr = mod.base + nt_offset; | |
| 666 | // The NT headers must lie inside the image; a wild e_lfanew is the signature of a forged or truncated | ||
| 667 | // header. | ||
| 668 |
3/6✓ Branch 17 → 18 taken 20 times.
✗ Branch 17 → 19 not taken.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 20 times.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 20 times.
|
20 | if (!mod.contains(nt_addr) || mod.end - nt_addr < sizeof(IMAGE_NT_HEADERS64)) |
| 669 | ✗ | return 0; | |
| 670 | |||
| 671 | 20 | const auto nt = DetourModKit::detail::guarded_read<IMAGE_NT_HEADERS64>(nt_addr); | |
| 672 |
4/8✓ Branch 25 → 26 taken 20 times.
✗ Branch 25 → 30 not taken.
✓ Branch 27 → 28 taken 20 times.
✗ Branch 27 → 30 not taken.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 20 times.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 20 times.
|
20 | if (!nt || nt->Signature != IMAGE_NT_SIGNATURE || nt->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) |
| 673 | ✗ | return 0; | |
| 674 | |||
| 675 | // The Windows loader caps a PE at 96 sections; a larger count is corrupt and would otherwise let the loop | ||
| 676 | // below run away. | ||
| 677 | 20 | const std::uint32_t num_sections = nt->FileHeader.NumberOfSections; | |
| 678 |
2/4✓ Branch 35 → 36 taken 20 times.
✗ Branch 35 → 37 not taken.
✗ Branch 36 → 37 not taken.
✓ Branch 36 → 38 taken 20 times.
|
20 | if (num_sections == 0 || num_sections > 96) |
| 679 | ✗ | return 0; | |
| 680 | |||
| 681 | // IMAGE_FIRST_SECTION: the section table starts immediately after the optional header, whose length is | ||
| 682 | // SizeOfOptionalHeader. Using sizeof(IMAGE_NT_HEADERS64) instead would misplace the table whenever the | ||
| 683 | // optional-header size differs from the compile-time struct size. | ||
| 684 | const std::uintptr_t sec_table = | ||
| 685 | 20 | nt_addr + offsetof(IMAGE_NT_HEADERS64, OptionalHeader) + nt->FileHeader.SizeOfOptionalHeader; | |
| 686 | |||
| 687 | 20 | std::size_t count = 0; | |
| 688 |
2/2✓ Branch 75 → 40 taken 313 times.
✓ Branch 75 → 76 taken 16 times.
|
329 | for (std::uint32_t i = 0; i < num_sections; ++i) |
| 689 | { | ||
| 690 | 313 | const std::uintptr_t hdr_addr = | |
| 691 | 313 | sec_table + static_cast<std::uintptr_t>(i) * sizeof(IMAGE_SECTION_HEADER); | |
| 692 | // A section header that escapes the image, or one whose guarded read faults, truncates the enumeration | ||
| 693 | // after a valid prefix: the sections past it are never examined, so a unique/absent verdict built on | ||
| 694 | // this range set is not authoritative. Surface Incomplete rather than silently returning the prefix. | ||
| 695 |
3/6✓ Branch 41 → 42 taken 313 times.
✗ Branch 41 → 43 not taken.
✗ Branch 42 → 43 not taken.
✓ Branch 42 → 44 taken 313 times.
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 47 taken 313 times.
|
313 | if (!mod.contains(hdr_addr) || mod.end - hdr_addr < sizeof(IMAGE_SECTION_HEADER)) |
| 696 | { | ||
| 697 | ✗ | completeness = rtti::Traversal::Incomplete; | |
| 698 | 4 | break; | |
| 699 | } | ||
| 700 | |||
| 701 | 313 | const auto sec = DetourModKit::detail::guarded_read<IMAGE_SECTION_HEADER>(hdr_addr); | |
| 702 |
2/2✓ Branch 49 → 50 taken 2 times.
✓ Branch 49 → 51 taken 311 times.
|
313 | if (!sec) |
| 703 | { | ||
| 704 | 2 | completeness = rtti::Traversal::Incomplete; | |
| 705 | 2 | break; | |
| 706 | } | ||
| 707 | |||
| 708 | 311 | const std::uint32_t ch = sec->Characteristics; | |
| 709 | 311 | const bool readable = (ch & IMAGE_SCN_MEM_READ) != 0; | |
| 710 | 311 | const bool executable = (ch & IMAGE_SCN_MEM_EXECUTE) != 0; | |
| 711 | 311 | const bool discardable = (ch & IMAGE_SCN_MEM_DISCARDABLE) != 0; | |
| 712 |
5/6✓ Branch 52 → 53 taken 311 times.
✗ Branch 52 → 55 not taken.
✓ Branch 53 → 54 taken 297 times.
✓ Branch 53 → 55 taken 14 times.
✓ Branch 54 → 55 taken 118 times.
✓ Branch 54 → 56 taken 179 times.
|
311 | if (!readable || executable || discardable) |
| 713 | 134 | continue; | |
| 714 | |||
| 715 | // Use the in-memory extent (VirtualAddress + VirtualSize), never the on-disk | ||
| 716 | // PointerToRawData/SizeOfRawData: those are file offsets and do not survive section alignment once | ||
| 717 | // mapped. | ||
| 718 |
1/2✗ Branch 57 → 58 not taken.
✓ Branch 57 → 59 taken 179 times.
|
179 | if (sec->Misc.VirtualSize == 0) |
| 719 | ✗ | continue; | |
| 720 |
2/2✓ Branch 60 → 61 taken 2 times.
✓ Branch 60 → 63 taken 177 times.
|
179 | if (sec->VirtualAddress >= mod.end - mod.base) |
| 721 | { | ||
| 722 | 2 | completeness = merge_traversal(completeness, rtti::Traversal::Incomplete); | |
| 723 | 2 | continue; | |
| 724 | } | ||
| 725 | 177 | const std::uintptr_t begin = mod.base + sec->VirtualAddress; | |
| 726 |
1/2✗ Branch 65 → 66 not taken.
✓ Branch 65 → 68 taken 177 times.
|
177 | if (sec->Misc.VirtualSize > mod.end - begin) |
| 727 | { | ||
| 728 | ✗ | completeness = merge_traversal(completeness, rtti::Traversal::Incomplete); | |
| 729 | ✗ | continue; | |
| 730 | } | ||
| 731 | 177 | const std::uintptr_t end = begin + sec->Misc.VirtualSize; | |
| 732 | |||
| 733 | // A qualifying section that will not fit the caller's fixed range buffer means the sweep would omit a | ||
| 734 | // region that could hold the type: report Saturated so the verdict is not treated as authoritative. | ||
| 735 |
2/2✓ Branch 69 → 70 taken 2 times.
✓ Branch 69 → 71 taken 175 times.
|
177 | if (count == cap) |
| 736 | { | ||
| 737 | 2 | completeness = rtti::Traversal::Saturated; | |
| 738 | 2 | break; | |
| 739 | } | ||
| 740 | 175 | out[count].begin = begin; | |
| 741 | 175 | out[count].end = end; | |
| 742 | 175 | ++count; | |
| 743 | } | ||
| 744 | 20 | return count; | |
| 745 | } | ||
| 746 | |||
| 747 | /** | ||
| 748 | * @brief Byte-exact comparison of the NUL-terminated name at @p name_addr with @p mangled, including the | ||
| 749 | * terminator so a superstring does not match. | ||
| 750 | * @details Mirrors vtable_is_type's name check; the caller guarantees mangled.size() < MAX_TYPE_NAME_LEN. | ||
| 751 | * @param module_end Exclusive end of the name's owning module; the compare fails closed rather than reading to | ||
| 752 | * or past it, so an edge-of-module name buffer cannot over-read into an adjacent mapped image. | ||
| 753 | */ | ||
| 754 | [[nodiscard]] bool | ||
| 755 | 183 | name_equals(std::uintptr_t name_addr, std::string_view mangled, std::uintptr_t module_end) noexcept | |
| 756 | { | ||
| 757 | char buf[rtti::MAX_TYPE_NAME_LEN + 1]; | ||
| 758 | 183 | const std::size_t need = mangled.size() + 1; | |
| 759 |
2/4✓ Branch 3 → 4 taken 183 times.
✗ Branch 3 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 183 times.
|
183 | if (module_end <= name_addr || module_end - name_addr < need) |
| 760 | ✗ | return false; | |
| 761 |
1/2✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 183 times.
|
183 | if (!DetourModKit::detail::guarded_read_bytes(name_addr, buf, need)) |
| 762 | ✗ | return false; | |
| 763 |
2/2✓ Branch 10 → 11 taken 3 times.
✓ Branch 10 → 12 taken 180 times.
|
183 | if (buf[mangled.size()] != '\0') |
| 764 | 3 | return false; | |
| 765 | 180 | return std::memcmp(buf, mangled.data(), mangled.size()) == 0; | |
| 766 | } | ||
| 767 | |||
| 768 | /** | ||
| 769 | * @brief Sweeps one [begin,end) window for vtables whose RTTI name equals @p mangled, appending validated, | ||
| 770 | * deduped matches to @p out (capped at @p cap). | ||
| 771 | * @details The window is read in page-bounded chunks (one guarded read per page) and scanned in-process, so the | ||
| 772 | * guarded-read count is per-page rather than per-qword - the difference between a few hundred and a | ||
| 773 | * few hundred thousand guarded transitions over a multi-megabyte section. A meta-slot is a qword that | ||
| 774 | * points to an in-scope COL; the vtable that owns it is slot + 8, validated by the same COL prelude | ||
| 775 | * the forward walker uses, so the in-range pre-filter only spares a deeper read and never decides | ||
| 776 | * correctness. | ||
| 777 | * @param mod The scan SCOPE, used for the meta-slot pre-filter (a candidate's COL pointer must land in it). | ||
| 778 | * @param owning The vtables' OWNING-MODULE span, used to validate each candidate (see the resolve_col_site | ||
| 779 | * call below). May be an invalid span, in which case each candidate resolves its own module. | ||
| 780 | */ | ||
| 781 | 180 | void sweep_range_for_name( | |
| 782 | DetourModKit::detail::ModuleSpan mod, | ||
| 783 | DetourModKit::detail::ModuleSpan owning, | ||
| 784 | std::uintptr_t begin, | ||
| 785 | std::uintptr_t end, | ||
| 786 | std::string_view mangled, | ||
| 787 | VtMatch *out, | ||
| 788 | std::size_t cap, | ||
| 789 | std::size_t &count, | ||
| 790 | bool &page_unreadable | ||
| 791 | ) noexcept | ||
| 792 | { | ||
| 793 | // A range above UINTPTR_MAX - 7 contains no complete aligned qword. Reject it before alignment. | ||
| 794 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 179 times.
|
180 | if (begin > UINTPTR_MAX - 7) |
| 795 | 1 | return; | |
| 796 | 179 | std::uintptr_t addr = (begin + 7) & ~static_cast<std::uintptr_t>(7); | |
| 797 | |||
| 798 | // Subtraction keeps each range bound valid at UINTPTR_MAX. PAGE_SIZE defines the page span. | ||
| 799 |
5/6✓ Branch 39 → 40 taken 11713 times.
✓ Branch 39 → 42 taken 143 times.
✓ Branch 40 → 41 taken 11677 times.
✓ Branch 40 → 42 taken 36 times.
✓ Branch 41 → 5 taken 11677 times.
✗ Branch 41 → 42 not taken.
|
11856 | while (addr < end && end - addr >= sizeof(std::uintptr_t) && count < cap) |
| 800 | { | ||
| 801 | // Never let one guarded read cross a page boundary: an unmapped page then fails only its own chunk, not | ||
| 802 | // the whole window. | ||
| 803 | 11677 | const std::uintptr_t to_page_end = rtti::detail::PAGE_SIZE - (addr & rtti::detail::PAGE_MASK); | |
| 804 | 11677 | const std::uintptr_t remaining = end - addr; | |
| 805 |
2/2✓ Branch 5 → 6 taken 11498 times.
✓ Branch 5 → 7 taken 179 times.
|
11677 | const std::uintptr_t chunk = (to_page_end < remaining) ? to_page_end : remaining; |
| 806 | |||
| 807 | // a 4 KiB page holds at most 512 qwords | ||
| 808 | std::uintptr_t buf[512]; | ||
| 809 | 11677 | const std::size_t qwords = static_cast<std::size_t>(chunk) / sizeof(std::uintptr_t); | |
| 810 | 11677 | const std::size_t want = (qwords < 512) ? qwords : 512; | |
| 811 |
2/2✓ Branch 9 → 10 taken 3 times.
✓ Branch 9 → 11 taken 11674 times.
|
11677 | if (!DetourModKit::detail::guarded_read_bytes(addr, buf, want * sizeof(std::uintptr_t))) |
| 812 | { | ||
| 813 | // A page inside a qualifying section could not be read, so a meta-slot on it (and the vtable it | ||
| 814 | // anchors) is invisible to this sweep. Record the gap: a unique/absent verdict built on a sweep | ||
| 815 | // that skipped a page is not authoritative. The page is skipped (advance past it) rather than | ||
| 816 | // aborting, so the readable remainder of the range is still scanned. | ||
| 817 | 3 | page_unreadable = true; | |
| 818 | } | ||
| 819 | else | ||
| 820 | { | ||
| 821 |
4/4✓ Branch 36 → 37 taken 5893733 times.
✓ Branch 36 → 38 taken 11672 times.
✓ Branch 37 → 12 taken 5893731 times.
✓ Branch 37 → 38 taken 2 times.
|
5905405 | for (std::size_t j = 0; j < want && count < cap; ++j) |
| 822 | { | ||
| 823 | // Pre-filter: a meta-slot holds a pointer to a COL inside | ||
| 824 | // this module. Most qwords fail here without a second read. | ||
| 825 |
2/2✓ Branch 13 → 14 taken 5060921 times.
✓ Branch 13 → 15 taken 832810 times.
|
5893731 | if (!mod.contains(buf[j])) |
| 826 | 5893554 | continue; | |
| 827 | |||
| 828 | 832810 | const std::uintptr_t slot_addr = addr + j * sizeof(std::uintptr_t); | |
| 829 | 832810 | const std::uintptr_t candidate_vtable = slot_addr + sizeof(std::uintptr_t); | |
| 830 | |||
| 831 | // Validate against the owning-module span the caller resolved once, not the scan scope `mod`: | ||
| 832 | // resolve_col_site cross-checks the recovered image base (col_addr - pSelf) against the module | ||
| 833 | // base and computes the TypeDescriptor / name addresses from module-base + RVA, so it needs the | ||
| 834 | // true module extent, which a sub-range scope (a tight fixture window whose base is not the | ||
| 835 | // image base) is not. Passing the pre-resolved owning span hoists the per-candidate | ||
| 836 | // memory::module_of loader lookup out of the hot sweep; if that one-time resolve failed (owning | ||
| 837 | // invalid), fall back to the self-resolving overload so behaviour is unchanged. | ||
| 838 | 832810 | rtti::detail::ColSite site; | |
| 839 | 832810 | const bool resolved_ok = owning.valid() | |
| 840 |
1/2✓ Branch 16 → 17 taken 832810 times.
✗ Branch 16 → 19 not taken.
|
832810 | ? rtti::detail::resolve_col_site(candidate_vtable, owning, site) |
| 841 | 832810 | : rtti::detail::resolve_col_site(candidate_vtable, site); | |
| 842 |
2/2✓ Branch 21 → 22 taken 832627 times.
✓ Branch 21 → 23 taken 183 times.
|
832810 | if (!resolved_ok) |
| 843 | 832627 | continue; | |
| 844 |
2/2✓ Branch 24 → 25 taken 6 times.
✓ Branch 24 → 26 taken 177 times.
|
183 | if (!name_equals(site.name_addr, mangled, site.module_end)) |
| 845 | 6 | continue; | |
| 846 | |||
| 847 | 177 | bool seen = false; | |
| 848 |
2/2✓ Branch 30 → 27 taken 4043 times.
✓ Branch 30 → 31 taken 177 times.
|
4220 | for (std::size_t k = 0; k < count; ++k) |
| 849 | { | ||
| 850 |
1/2✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 4043 times.
|
4043 | if (out[k].vtable == candidate_vtable) |
| 851 | { | ||
| 852 | ✗ | seen = true; | |
| 853 | ✗ | break; | |
| 854 | } | ||
| 855 | } | ||
| 856 |
1/2✓ Branch 31 → 32 taken 177 times.
✗ Branch 31 → 33 not taken.
|
177 | if (!seen) |
| 857 | { | ||
| 858 | 177 | out[count].vtable = candidate_vtable; | |
| 859 | 177 | out[count].col_offset = site.col_offset; | |
| 860 | 177 | ++count; | |
| 861 | } | ||
| 862 | } | ||
| 863 | } | ||
| 864 | |||
| 865 | 11677 | addr += want * sizeof(std::uintptr_t); | |
| 866 | } | ||
| 867 | } | ||
| 868 | |||
| 869 | /** | ||
| 870 | * @brief Finds vtables for @p mangled across the module's RTTI-bearing sections, returning the count written | ||
| 871 | * into @p out (deduped, capped at @p cap). | ||
| 872 | * @details Sweeps the readable non-executable sections; if the PE headers cannot be parsed, falls back to the | ||
| 873 | * whole module image rather than reporting a confident-but-wrong "not found" for a packed or merged | ||
| 874 | * binary. | ||
| 875 | */ | ||
| 876 | 64 | std::size_t scan_vtables_for_name( | |
| 877 | DetourModKit::detail::ModuleSpan mod, | ||
| 878 | std::string_view mangled, | ||
| 879 | VtMatch *out, | ||
| 880 | std::size_t cap, | ||
| 881 | rtti::Traversal &completeness | ||
| 882 | ) noexcept | ||
| 883 | { | ||
| 884 |
6/8✓ Branch 3 → 4 taken 60 times.
✓ Branch 3 → 8 taken 4 times.
✓ Branch 5 → 6 taken 60 times.
✗ Branch 5 → 8 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 60 times.
✓ Branch 10 → 11 taken 4 times.
✓ Branch 10 → 13 taken 60 times.
|
64 | if (!mod.valid() || mangled.empty() || mangled.size() >= rtti::MAX_TYPE_NAME_LEN) |
| 885 | { | ||
| 886 | 4 | completeness = merge_traversal(completeness, rtti::Traversal::Incomplete); | |
| 887 | 4 | return 0; | |
| 888 | } | ||
| 889 | |||
| 890 | // Resolve the vtables' owning module once for the whole sweep. resolve_col_site needs the true module base | ||
| 891 | // and extent (for the pSelf/base cross-check and RVA->VA), which is not the scan scope `mod`: a caller may | ||
| 892 | // scope the sweep to a sub-range of the module (a tight fixture window), whose base is not the image base. | ||
| 893 | // module_of at the scope base recovers the real owning module, shared by every candidate in the scope, so | ||
| 894 | // the per-candidate loader lookup is hoisted here. An invalid result (the scope base is not in a loaded | ||
| 895 | // module) leaves each candidate to resolve its own module in sweep_range_for_name, preserving behaviour. | ||
| 896 | const DetourModKit::detail::ModuleSpan owning = | ||
| 897 | 60 | DetourModKit::detail::module_span(DetourModKit::detail::live_module_region(Address{mod.base})); | |
| 898 | |||
| 899 | 60 | ScanRange ranges[MAX_RTTI_SCAN_RANGES]; | |
| 900 | 60 | const std::size_t range_count = collect_rtti_scan_ranges(mod, ranges, MAX_RTTI_SCAN_RANGES, completeness); | |
| 901 | |||
| 902 | 60 | std::size_t count = 0; | |
| 903 | 60 | bool page_unreadable = false; | |
| 904 |
2/2✓ Branch 17 → 18 taken 45 times.
✓ Branch 17 → 19 taken 15 times.
|
60 | if (range_count == 0) |
| 905 | { | ||
| 906 | // No usable scan window (the PE headers could not be parsed, or no readable non-executable section | ||
| 907 | // qualified): sweep the whole image rather than report a confident-but-wrong "not found" for a packed | ||
| 908 | // or section-merged binary. The whole-image sweep is a strict superset, and resolve_col_site still | ||
| 909 | // validates every candidate. | ||
| 910 | 45 | sweep_range_for_name(mod, owning, mod.base, mod.end, mangled, out, cap, count, page_unreadable); | |
| 911 | } | ||
| 912 | else | ||
| 913 | { | ||
| 914 |
3/4✓ Branch 22 → 23 taken 135 times.
✓ Branch 22 → 24 taken 15 times.
✓ Branch 23 → 20 taken 135 times.
✗ Branch 23 → 24 not taken.
|
150 | for (std::size_t i = 0; i < range_count && count < cap; ++i) |
| 915 | { | ||
| 916 | 135 | sweep_range_for_name( | |
| 917 | mod, | ||
| 918 | owning, | ||
| 919 | ranges[i].begin, | ||
| 920 | ranges[i].end, | ||
| 921 | mangled, | ||
| 922 | out, | ||
| 923 | cap, | ||
| 924 | count, | ||
| 925 | page_unreadable | ||
| 926 | ); | ||
| 927 | } | ||
| 928 | } | ||
| 929 | |||
| 930 | // An unreadable page anywhere in the sweep means a match could have hidden on it; a match buffer that | ||
| 931 | // filled to capacity means the same for the matches past the cap. Either makes a uniqueness or absence | ||
| 932 | // verdict unsafe, so fold both into the completeness the caller gates on. | ||
| 933 |
2/2✓ Branch 24 → 25 taken 3 times.
✓ Branch 24 → 27 taken 57 times.
|
60 | if (page_unreadable) |
| 934 | 3 | completeness = merge_traversal(completeness, rtti::Traversal::Incomplete); | |
| 935 |
2/2✓ Branch 27 → 28 taken 2 times.
✓ Branch 27 → 30 taken 58 times.
|
60 | if (count == cap) |
| 936 | 2 | completeness = merge_traversal(completeness, rtti::Traversal::Saturated); | |
| 937 | 60 | return count; | |
| 938 | } | ||
| 939 | |||
| 940 | /** | ||
| 941 | * @brief Sweeps one [begin,end) window and returns true when it finds any validated COL. | ||
| 942 | * @details Mirrors the name sweep's page-bounded read, meta-slot pre-filter, and resolve_col_site validation, | ||
| 943 | * but returns on the first hit instead of collecting and deduping matches. The page-walk is duplicated | ||
| 944 | * from @ref sweep_range_for_name rather than factored into a shared callback-driven core: sharing one | ||
| 945 | * would complicate the security-critical name-resolve sweep purely to | ||
| 946 | * save the mechanical loop, and the two sweeps terminate differently (collect-to-cap versus first-hit | ||
| 947 | * exit). The validation authority, @ref resolve_col_site, IS shared, so the two paths cannot diverge | ||
| 948 | * on what counts as a resolvable COL; only the copied loop could drift, and a drift there is | ||
| 949 | * fail-safe (a missed COL yields a false negative that routes the caller to the raw-byte fallback). | ||
| 950 | * @param mod The scan SCOPE, used for the in-scope meta-slot pre-filter. | ||
| 951 | * @param owning The vtables' OWNING-MODULE span used for validation; may be invalid, in which case each | ||
| 952 | * candidate resolves its own module (mirrors @ref sweep_range_for_name). | ||
| 953 | */ | ||
| 954 | 47 | bool sweep_range_for_first_col( | |
| 955 | DetourModKit::detail::ModuleSpan mod, | ||
| 956 | DetourModKit::detail::ModuleSpan owning, | ||
| 957 | std::uintptr_t begin, | ||
| 958 | std::uintptr_t end, | ||
| 959 | bool &page_unreadable | ||
| 960 | ) noexcept | ||
| 961 | { | ||
| 962 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 46 times.
|
47 | if (begin > UINTPTR_MAX - 7) |
| 963 | 1 | return false; | |
| 964 | 46 | std::uintptr_t addr = (begin + 7) & ~static_cast<std::uintptr_t>(7); | |
| 965 | |||
| 966 |
4/4✓ Branch 27 → 28 taken 5580 times.
✓ Branch 27 → 29 taken 42 times.
✓ Branch 28 → 5 taken 5578 times.
✓ Branch 28 → 29 taken 2 times.
|
5622 | while (addr < end && end - addr >= sizeof(std::uintptr_t)) |
| 967 | { | ||
| 968 | // One guarded read per page: an unmapped page then fails only its own chunk, not the whole window. | ||
| 969 | 5578 | const std::uintptr_t to_page_end = rtti::detail::PAGE_SIZE - (addr & rtti::detail::PAGE_MASK); | |
| 970 | 5578 | const std::uintptr_t remaining = end - addr; | |
| 971 |
2/2✓ Branch 5 → 6 taken 5532 times.
✓ Branch 5 → 7 taken 46 times.
|
5578 | const std::uintptr_t chunk = (to_page_end < remaining) ? to_page_end : remaining; |
| 972 | |||
| 973 | // A 4 KiB page holds at most 512 qwords. | ||
| 974 | std::uintptr_t buf[512]; | ||
| 975 | 5578 | const std::size_t qwords = static_cast<std::size_t>(chunk) / sizeof(std::uintptr_t); | |
| 976 | 5578 | const std::size_t want = (qwords < 512) ? qwords : 512; | |
| 977 |
2/2✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 5577 times.
|
5578 | if (!DetourModKit::detail::guarded_read_bytes(addr, buf, want * sizeof(std::uintptr_t))) |
| 978 | { | ||
| 979 | // An unreadable page hides any COL on it; record the gap so a "no records" answer built on this | ||
| 980 | // sweep is reported as Incomplete rather than an authoritative absence. | ||
| 981 | 1 | page_unreadable = true; | |
| 982 | } | ||
| 983 | else | ||
| 984 | { | ||
| 985 |
2/2✓ Branch 25 → 12 taken 2833739 times.
✓ Branch 25 → 26 taken 5575 times.
|
2839314 | for (std::size_t j = 0; j < want; ++j) |
| 986 | { | ||
| 987 | // A meta-slot holds a pointer to a COL inside this module; most qwords fail this cheap | ||
| 988 | // in-scope pre-filter without a second read. | ||
| 989 |
2/2✓ Branch 13 → 14 taken 2417575 times.
✓ Branch 13 → 15 taken 416164 times.
|
2833739 | if (!mod.contains(buf[j])) |
| 990 | 2417575 | continue; | |
| 991 | |||
| 992 | 416164 | const std::uintptr_t slot_addr = addr + j * sizeof(std::uintptr_t); | |
| 993 | 416164 | const std::uintptr_t candidate_vtable = slot_addr + sizeof(std::uintptr_t); | |
| 994 | |||
| 995 | // resolve_col_site is the shared definition of "resolvable", so this predicate and the name | ||
| 996 | // resolver accept the same COL shapes. | ||
| 997 | 416164 | rtti::detail::ColSite site; | |
| 998 | 416164 | const bool resolved_ok = owning.valid() | |
| 999 |
1/2✓ Branch 16 → 17 taken 416164 times.
✗ Branch 16 → 19 not taken.
|
416164 | ? rtti::detail::resolve_col_site(candidate_vtable, owning, site) |
| 1000 | 416164 | : rtti::detail::resolve_col_site(candidate_vtable, site); | |
| 1001 |
2/2✓ Branch 21 → 22 taken 2 times.
✓ Branch 21 → 23 taken 416162 times.
|
416164 | if (resolved_ok) |
| 1002 | 2 | return true; | |
| 1003 | } | ||
| 1004 | } | ||
| 1005 | |||
| 1006 | 5576 | addr += want * sizeof(std::uintptr_t); | |
| 1007 | } | ||
| 1008 | 44 | return false; | |
| 1009 | } | ||
| 1010 | |||
| 1011 | /** | ||
| 1012 | * @brief Reports whether @p mod holds any resolvable RTTI record, mirroring @ref scan_vtables_for_name's range | ||
| 1013 | * selection but short-circuiting on the first validated COL. | ||
| 1014 | * @details Resolves the vtables' owning-module span once (exactly as @ref scan_vtables_for_name does at its | ||
| 1015 | * scope base) so every candidate is validated against the true image base, then sweeps the same | ||
| 1016 | * readable non-executable sections, with the identical whole-image fallback when the PE headers do not | ||
| 1017 | * parse - a packed or section-merged image, or a tight non-PE scope window. The caller | ||
| 1018 | * (@ref rtti::region_has_rtti) has already proven @p mod valid. | ||
| 1019 | */ | ||
| 1020 | 10 | rtti::RttiPresence scope_has_rtti(DetourModKit::detail::ModuleSpan mod) noexcept | |
| 1021 | { | ||
| 1022 | const DetourModKit::detail::ModuleSpan owning = | ||
| 1023 | 10 | DetourModKit::detail::module_span(DetourModKit::detail::live_module_region(Address{mod.base})); | |
| 1024 | |||
| 1025 | 10 | rtti::Traversal completeness = rtti::Traversal::Complete; | |
| 1026 | 10 | ScanRange ranges[MAX_RTTI_SCAN_RANGES]; | |
| 1027 | 10 | const std::size_t range_count = collect_rtti_scan_ranges(mod, ranges, MAX_RTTI_SCAN_RANGES, completeness); | |
| 1028 | |||
| 1029 | 10 | bool page_unreadable = false; | |
| 1030 |
2/2✓ Branch 6 → 7 taken 7 times.
✓ Branch 6 → 10 taken 3 times.
|
10 | if (range_count == 0) |
| 1031 | { | ||
| 1032 | // No parseable section table (packed / section-merged / a non-PE scope window): sweep the whole image | ||
| 1033 | // as a strict superset, exactly as scan_vtables_for_name does on the same condition. | ||
| 1034 |
2/2✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 16 taken 5 times.
|
7 | if (sweep_range_for_first_col(mod, owning, mod.base, mod.end, page_unreadable)) |
| 1035 | 2 | return rtti::RttiPresence::Present; | |
| 1036 | } | ||
| 1037 | else | ||
| 1038 | { | ||
| 1039 |
2/2✓ Branch 15 → 11 taken 40 times.
✓ Branch 15 → 16 taken 3 times.
|
43 | for (std::size_t i = 0; i < range_count; ++i) |
| 1040 | { | ||
| 1041 |
1/2✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 40 times.
|
40 | if (sweep_range_for_first_col(mod, owning, ranges[i].begin, ranges[i].end, page_unreadable)) |
| 1042 | ✗ | return rtti::RttiPresence::Present; | |
| 1043 | } | ||
| 1044 | } | ||
| 1045 | |||
| 1046 | // No record was found. Only call that an authoritative absence when the sweep was complete: a faulted | ||
| 1047 | // section header (in `completeness`) or an unreadable page (`page_unreadable`) could have hidden the only | ||
| 1048 | // record, so report Incomplete instead of a false Absent. | ||
| 1049 |
2/2✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 19 taken 7 times.
|
8 | if (page_unreadable) |
| 1050 | 1 | completeness = merge_traversal(completeness, rtti::Traversal::Incomplete); | |
| 1051 |
2/2✓ Branch 19 → 20 taken 4 times.
✓ Branch 19 → 21 taken 4 times.
|
8 | return (completeness == rtti::Traversal::Complete) ? rtti::RttiPresence::Absent |
| 1052 | 8 | : rtti::RttiPresence::Incomplete; | |
| 1053 | } | ||
| 1054 | } // anonymous namespace | ||
| 1055 | |||
| 1056 | 48 | std::optional<Address> rtti::vtable_for_type(std::string_view mangled, Region range) noexcept | |
| 1057 | { | ||
| 1058 | 48 | VtMatch matches[MAX_REVERSE_MATCHES]; | |
| 1059 | 48 | rtti::Traversal completeness = rtti::Traversal::Complete; | |
| 1060 | 48 | const std::size_t match_count = scan_vtables_for_name( | |
| 1061 | DetourModKit::detail::module_span(range), | ||
| 1062 | mangled, | ||
| 1063 | matches, | ||
| 1064 | MAX_REVERSE_MATCHES, | ||
| 1065 | completeness | ||
| 1066 | ); | ||
| 1067 | |||
| 1068 | // A unique or absent verdict is the inverse of vtable_is_type and is trustworthy only across a COMPLETE sweep. | ||
| 1069 | // An Incomplete sweep (a faulted section header or an unreadable page) or a Saturated one (the section-range or | ||
| 1070 | // match buffer filled) can hide a second distinct primary, which makes a "unique" answer wrong, or the only | ||
| 1071 | // primary, which makes an "absent" answer wrong. Fail closed rather than authorize a verdict from a partial | ||
| 1072 | // sweep. Match-buffer exhaustion is reported as Traversal::Saturated by the same completeness gate. | ||
| 1073 |
2/2✓ Branch 4 → 5 taken 5 times.
✓ Branch 4 → 6 taken 43 times.
|
48 | if (completeness != rtti::Traversal::Complete) |
| 1074 | 5 | return std::nullopt; | |
| 1075 | |||
| 1076 | // The primary vtable is the COL.offset == 0 sub-object: the value an object pointer's first qword holds for a | ||
| 1077 | // most-derived instance. More than one distinct primary for the same name (a type linked into the image twice) | ||
| 1078 | // is ambiguous; fail closed rather than return an arbitrary one. | ||
| 1079 | 43 | std::optional<std::uintptr_t> primary; | |
| 1080 |
2/2✓ Branch 20 → 7 taken 32 times.
✓ Branch 20 → 21 taken 41 times.
|
73 | for (std::size_t i = 0; i < match_count; ++i) |
| 1081 | { | ||
| 1082 |
2/2✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 31 times.
|
32 | if (matches[i].col_offset != 0) |
| 1083 | 1 | continue; | |
| 1084 |
5/6✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 14 taken 29 times.
✓ Branch 12 → 13 taken 2 times.
✗ Branch 12 → 14 not taken.
✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 17 taken 29 times.
|
31 | if (primary && *primary != matches[i].vtable) |
| 1085 | 2 | return std::nullopt; | |
| 1086 | 29 | primary = matches[i].vtable; | |
| 1087 | } | ||
| 1088 | |||
| 1089 |
2/2✓ Branch 22 → 23 taken 14 times.
✓ Branch 22 → 24 taken 27 times.
|
41 | if (!primary) |
| 1090 | 14 | return std::nullopt; | |
| 1091 | 27 | return Address{*primary}; | |
| 1092 | } | ||
| 1093 | |||
| 1094 | rtti::VtablesResult | ||
| 1095 | 16 | rtti::vtables_for_type_checked(std::string_view mangled, Address *out, std::size_t out_cap, Region range) noexcept | |
| 1096 | { | ||
| 1097 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 15 times.
|
16 | if (!out) |
| 1098 | 1 | out_cap = 0; | |
| 1099 | |||
| 1100 | 16 | VtMatch matches[MAX_REVERSE_MATCHES]; | |
| 1101 | 16 | rtti::Traversal completeness = rtti::Traversal::Complete; | |
| 1102 | 16 | const std::size_t match_count = scan_vtables_for_name( | |
| 1103 | DetourModKit::detail::module_span(range), | ||
| 1104 | mangled, | ||
| 1105 | matches, | ||
| 1106 | MAX_REVERSE_MATCHES, | ||
| 1107 | completeness | ||
| 1108 | ); | ||
| 1109 | |||
| 1110 | // Ascending COL.offset so the primary (offset 0) sorts first. The match count is tiny (one per base | ||
| 1111 | // sub-object), so an in-place insertion sort is both adequate and allocation-free. | ||
| 1112 |
2/2✓ Branch 12 → 7 taken 70 times.
✓ Branch 12 → 13 taken 16 times.
|
86 | for (std::size_t i = 1; i < match_count; ++i) |
| 1113 | { | ||
| 1114 | 70 | const VtMatch key = matches[i]; | |
| 1115 | 70 | std::size_t sorted = i; | |
| 1116 |
4/4✓ Branch 9 → 10 taken 71 times.
✓ Branch 9 → 11 taken 1 time.
✓ Branch 10 → 8 taken 2 times.
✓ Branch 10 → 11 taken 69 times.
|
72 | while (sorted > 0 && matches[sorted - 1].col_offset > key.col_offset) |
| 1117 | { | ||
| 1118 | 2 | matches[sorted] = matches[sorted - 1]; | |
| 1119 | 2 | --sorted; | |
| 1120 | } | ||
| 1121 | 70 | matches[sorted] = key; | |
| 1122 | } | ||
| 1123 | |||
| 1124 |
2/2✓ Branch 13 → 14 taken 3 times.
✓ Branch 13 → 15 taken 13 times.
|
16 | const std::size_t to_write = (match_count < out_cap) ? match_count : out_cap; |
| 1125 |
2/2✓ Branch 19 → 17 taken 77 times.
✓ Branch 19 → 20 taken 16 times.
|
93 | for (std::size_t i = 0; i < to_write; ++i) |
| 1126 | 77 | out[i] = Address{matches[i].vtable}; | |
| 1127 | 16 | return VtablesResult{match_count, completeness}; | |
| 1128 | } | ||
| 1129 | |||
| 1130 | std::size_t | ||
| 1131 | 9 | rtti::vtables_for_type(std::string_view mangled, Address *out, std::size_t out_cap, Region range) noexcept | |
| 1132 | { | ||
| 1133 | // Best-effort count-only face over the checked form: it discards the completeness, so a caller that must | ||
| 1134 | // distinguish an authoritative absence from a truncated sweep uses vtables_for_type_checked instead. | ||
| 1135 | 9 | return vtables_for_type_checked(mangled, out, out_cap, range).count; | |
| 1136 | } | ||
| 1137 | |||
| 1138 | 3 | bool rtti::region_has_rtti(Region range) noexcept | |
| 1139 | { | ||
| 1140 | // true only on a found record (sound regardless of completeness); Absent and Incomplete both collapse to the | ||
| 1141 | // fail-closed "no record found, use the raw-byte fallback" answer, the same one an invalid range receives. | ||
| 1142 | 3 | return region_rtti_presence(range) == RttiPresence::Present; | |
| 1143 | } | ||
| 1144 | |||
| 1145 | 12 | rtti::RttiPresence rtti::region_rtti_presence(Region range) noexcept | |
| 1146 | { | ||
| 1147 | 12 | const auto mod = DetourModKit::detail::module_span(range); | |
| 1148 | // No sweep can establish absence from an invalid range. | ||
| 1149 |
2/2✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 10 times.
|
12 | if (!mod.valid()) |
| 1150 | 2 | return RttiPresence::Incomplete; | |
| 1151 | 10 | return scope_has_rtti(mod); | |
| 1152 | } | ||
| 1153 | |||
| 1154 | 17 | std::uint64_t rtti::image_generation(Address addr) noexcept | |
| 1155 | { | ||
| 1156 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 1157 |
2/2✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 5 taken 13 times.
|
17 | if (auto *override_fn = DetourModKit::detail::g_rtti_image_generation_override) |
| 1158 | 4 | return override_fn(addr.raw()); | |
| 1159 | #endif | ||
| 1160 | 13 | const Region module = current_module_region(addr); | |
| 1161 |
2/2✓ Branch 7 → 8 taken 12 times.
✓ Branch 7 → 10 taken 1 time.
|
13 | return module.base ? DetourModKit::detail::image_generation_token(module.base.raw()) : 0; |
| 1162 | } | ||
| 1163 | |||
| 1164 |
1/2✓ Branch 4 → 5 taken 14 times.
✗ Branch 4 → 22 not taken.
|
42 | rtti::TypeIdentity::TypeIdentity(std::string_view mangled, Region range) : m_mangled(mangled), m_range(range) |
| 1165 | { | ||
| 1166 | 14 | const Region module = current_module_region(range.base); | |
| 1167 |
3/4✓ Branch 17 → 18 taken 3 times.
✓ Branch 17 → 20 taken 11 times.
✓ Branch 18 → 19 taken 3 times.
✗ Branch 18 → 20 not taken.
|
14 | m_tracks_module_range = module.base == range.base && module.size == range.size; |
| 1168 | 14 | } | |
| 1169 | |||
| 1170 | 1 | void rtti::TypeIdentity::invalidate() noexcept | |
| 1171 | { | ||
| 1172 |
1/2✗ Branch 6 → 3 not taken.
✓ Branch 6 → 7 taken 1 time.
|
2 | while (m_cache_writer.test_and_set(std::memory_order_acquire)) |
| 1173 | ✗ | (void)::SwitchToThread(); | |
| 1174 | 1 | m_cache_epoch.fetch_add(1, std::memory_order_acq_rel); | |
| 1175 | 1 | m_resolved.store(false, std::memory_order_release); | |
| 1176 | 1 | m_cached.store(Address{}, std::memory_order_relaxed); | |
| 1177 | 1 | m_image_stamp.store(0, std::memory_order_relaxed); | |
| 1178 | 1 | m_image_base.store(Address{}, std::memory_order_relaxed); | |
| 1179 | 1 | m_last_attempt_ms.store(0, std::memory_order_relaxed); | |
| 1180 | 1 | m_cache_writer.clear(std::memory_order_release); | |
| 1181 | 1 | } | |
| 1182 | |||
| 1183 | 53 | std::optional<Address> rtti::TypeIdentity::vtable() const noexcept | |
| 1184 | { | ||
| 1185 | 53 | const std::uint64_t cache_epoch = m_cache_epoch.load(std::memory_order_acquire); | |
| 1186 | 23 | const auto resolve_and_cache = [this](std::uint64_t expected_epoch) -> std::optional<Address> | |
| 1187 | { | ||
| 1188 | 23 | Region resolve_range = m_range; | |
| 1189 |
2/2✓ Branch 2 → 3 taken 5 times.
✓ Branch 2 → 11 taken 18 times.
|
23 | if (m_tracks_module_range) |
| 1190 | { | ||
| 1191 | 5 | resolve_range = current_module_region(m_range.base); | |
| 1192 |
3/6✓ Branch 5 → 6 taken 5 times.
✗ Branch 5 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 5 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 5 times.
|
5 | if (!resolve_range.base || resolve_range.size == 0) |
| 1193 | ✗ | return std::nullopt; | |
| 1194 | } | ||
| 1195 | |||
| 1196 | 23 | const Region image_before = current_module_region(resolve_range.base); | |
| 1197 |
1/2✓ Branch 13 → 14 taken 23 times.
✗ Branch 13 → 16 not taken.
|
23 | const std::uint64_t stamp_before = image_before.base ? current_image_stamp(image_before.base.raw()) : 0; |
| 1198 | 23 | const auto resolved = vtable_for_type(m_mangled, resolve_range); | |
| 1199 |
2/2✓ Branch 20 → 21 taken 7 times.
✓ Branch 20 → 22 taken 16 times.
|
23 | if (!resolved) |
| 1200 | 7 | return std::nullopt; | |
| 1201 | |||
| 1202 | 16 | const Region image_after = current_module_region(*resolved); | |
| 1203 |
1/2✓ Branch 25 → 26 taken 16 times.
✗ Branch 25 → 28 not taken.
|
16 | const std::uint64_t stamp_after = image_after.base ? current_image_stamp(image_after.base.raw()) : 0; |
| 1204 |
2/4✓ Branch 30 → 31 taken 16 times.
✗ Branch 30 → 33 not taken.
✓ Branch 31 → 32 taken 16 times.
✗ Branch 31 → 33 not taken.
|
16 | const bool module_backed_before = image_before.base && image_before.size != 0; |
| 1205 |
2/4✓ Branch 35 → 36 taken 16 times.
✗ Branch 35 → 38 not taken.
✓ Branch 36 → 37 taken 16 times.
✗ Branch 36 → 38 not taken.
|
16 | const bool module_backed_after = image_after.base && image_after.size != 0; |
| 1206 |
3/4✓ Branch 41 → 42 taken 16 times.
✗ Branch 41 → 45 not taken.
✓ Branch 42 → 43 taken 15 times.
✓ Branch 42 → 45 taken 1 time.
|
16 | if (module_backed_before != module_backed_after || image_before.base != image_after.base || |
| 1207 |
5/8✓ Branch 39 → 40 taken 16 times.
✗ Branch 39 → 45 not taken.
✓ Branch 43 → 44 taken 15 times.
✗ Branch 43 → 46 not taken.
✗ Branch 44 → 45 not taken.
✓ Branch 44 → 46 taken 15 times.
✓ Branch 47 → 48 taken 1 time.
✓ Branch 47 → 49 taken 15 times.
|
32 | stamp_before != stamp_after || (module_backed_after && stamp_after == 0)) |
| 1208 | 1 | return std::nullopt; | |
| 1209 | |||
| 1210 |
1/2✗ Branch 51 → 52 not taken.
✓ Branch 51 → 53 taken 15 times.
|
30 | if (m_cache_writer.test_and_set(std::memory_order_acquire)) |
| 1211 | ✗ | return std::nullopt; | |
| 1212 |
1/2✗ Branch 60 → 61 not taken.
✓ Branch 60 → 70 taken 15 times.
|
30 | if (m_cache_epoch.load(std::memory_order_acquire) != expected_epoch) |
| 1213 | { | ||
| 1214 | ✗ | m_cache_writer.clear(std::memory_order_release); | |
| 1215 | ✗ | return std::nullopt; | |
| 1216 | } | ||
| 1217 | 15 | m_image_base.store(image_after.base, std::memory_order_relaxed); | |
| 1218 | 15 | m_image_stamp.store(stamp_after, std::memory_order_relaxed); | |
| 1219 | 15 | m_cached.store(*resolved, std::memory_order_relaxed); | |
| 1220 | 15 | m_resolved.store(true, std::memory_order_release); | |
| 1221 | 15 | m_cache_writer.clear(std::memory_order_release); | |
| 1222 | 15 | return resolved; | |
| 1223 | 53 | }; | |
| 1224 | |||
| 1225 |
2/2✓ Branch 10 → 11 taken 31 times.
✓ Branch 10 → 104 taken 22 times.
|
53 | if (m_resolved.load(std::memory_order_acquire)) |
| 1226 | { | ||
| 1227 | 31 | const Address image_base = m_image_base.load(std::memory_order_relaxed); | |
| 1228 | 31 | const std::uint64_t image_stamp = m_image_stamp.load(std::memory_order_relaxed); | |
| 1229 |
5/6✓ Branch 19 → 20 taken 31 times.
✗ Branch 19 → 24 not taken.
✓ Branch 22 → 23 taken 4 times.
✓ Branch 22 → 24 taken 27 times.
✓ Branch 25 → 26 taken 4 times.
✓ Branch 25 → 83 taken 27 times.
|
31 | if (image_stamp != 0 && current_image_stamp(image_base.raw()) != image_stamp) |
| 1230 | { | ||
| 1231 |
1/2✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 4 times.
|
8 | if (m_cache_writer.test_and_set(std::memory_order_acquire)) |
| 1232 | ✗ | return std::nullopt; | |
| 1233 |
1/2✗ Branch 37 → 38 not taken.
✓ Branch 37 → 47 taken 4 times.
|
8 | if (m_cache_epoch.load(std::memory_order_acquire) != cache_epoch) |
| 1234 | { | ||
| 1235 | ✗ | m_cache_writer.clear(std::memory_order_release); | |
| 1236 | ✗ | return std::nullopt; | |
| 1237 | } | ||
| 1238 | 4 | const std::uint64_t next_epoch = m_cache_epoch.fetch_add(1, std::memory_order_acq_rel) + 1; | |
| 1239 | 4 | m_resolved.store(false, std::memory_order_release); | |
| 1240 | 4 | m_cached.store(Address{}, std::memory_order_relaxed); | |
| 1241 | 4 | m_image_stamp.store(0, std::memory_order_relaxed); | |
| 1242 | 4 | m_image_base.store(Address{}, std::memory_order_relaxed); | |
| 1243 | // Stamp the attempt before the gate drops: the refresh below is itself a whole-image sweep, so a miss | ||
| 1244 | // must start the `[B-67]` cooldown. Without the stamp the next miss-path call reads the pre-refresh | ||
| 1245 | // timestamp and runs a second adjacent sweep. | ||
| 1246 | 4 | const std::uint64_t refresh_now = rtti_now_ms(); | |
| 1247 |
1/2✓ Branch 63 → 64 taken 4 times.
✗ Branch 63 → 65 not taken.
|
4 | m_last_attempt_ms.store(refresh_now == 0 ? 1 : refresh_now, std::memory_order_release); |
| 1248 | 4 | m_cache_writer.clear(std::memory_order_release); | |
| 1249 | 4 | return resolve_and_cache(next_epoch); | |
| 1250 | } | ||
| 1251 | 27 | const Address cached = m_cached.load(std::memory_order_relaxed); | |
| 1252 |
2/4✓ Branch 91 → 92 taken 27 times.
✗ Branch 91 → 94 not taken.
✗ Branch 96 → 97 not taken.
✓ Branch 96 → 98 taken 27 times.
|
81 | if (m_cache_epoch.load(std::memory_order_acquire) != cache_epoch || |
| 1253 |
1/2✗ Branch 93 → 94 not taken.
✓ Branch 93 → 95 taken 27 times.
|
27 | !m_resolved.load(std::memory_order_acquire)) |
| 1254 | ✗ | return std::nullopt; | |
| 1255 |
1/2✓ Branch 99 → 100 taken 27 times.
✗ Branch 99 → 101 not taken.
|
27 | return cached ? std::optional<Address>(cached) : std::nullopt; |
| 1256 | } | ||
| 1257 | |||
| 1258 | // Miss path. vtable_for_type sweeps every RTTI-bearing section (a heavy walk), so throttle the re-sweep: | ||
| 1259 | // after a miss, skip until RESOLVE_RETRY_COOLDOWN_MS has elapsed, turning a per-frame full-module scan for an | ||
| 1260 | // absent type into at most one scan per cooldown while still eventually retrying. last == 0 is the | ||
| 1261 | // never-attempted sentinel; the now >= last guard keeps a non-monotonic clock from underflowing into a skip. | ||
| 1262 | 22 | const std::uint64_t now = rtti_now_ms(); | |
| 1263 |
1/2✗ Branch 107 → 108 not taken.
✓ Branch 107 → 109 taken 22 times.
|
44 | if (m_cache_writer.test_and_set(std::memory_order_acquire)) |
| 1264 | ✗ | return std::nullopt; | |
| 1265 |
1/2✗ Branch 116 → 117 not taken.
✓ Branch 116 → 126 taken 22 times.
|
44 | if (m_cache_epoch.load(std::memory_order_acquire) != cache_epoch) |
| 1266 | { | ||
| 1267 | ✗ | m_cache_writer.clear(std::memory_order_release); | |
| 1268 | ✗ | return std::nullopt; | |
| 1269 | } | ||
| 1270 | 22 | const std::uint64_t last = m_last_attempt_ms.load(std::memory_order_acquire); | |
| 1271 |
5/6✓ Branch 133 → 134 taken 7 times.
✓ Branch 133 → 145 taken 15 times.
✓ Branch 134 → 135 taken 7 times.
✗ Branch 134 → 145 not taken.
✓ Branch 135 → 136 taken 3 times.
✓ Branch 135 → 145 taken 4 times.
|
22 | if (last != 0 && now >= last && (now - last) < RESOLVE_RETRY_COOLDOWN_MS) |
| 1272 | { | ||
| 1273 | 3 | m_cache_writer.clear(std::memory_order_release); | |
| 1274 | 3 | return std::nullopt; | |
| 1275 | } | ||
| 1276 |
1/2✓ Branch 145 → 146 taken 19 times.
✗ Branch 145 → 147 not taken.
|
19 | m_last_attempt_ms.store(now == 0 ? 1 : now, std::memory_order_release); |
| 1277 | 19 | m_cache_writer.clear(std::memory_order_release); | |
| 1278 | 19 | return resolve_and_cache(cache_epoch); | |
| 1279 | } | ||
| 1280 | |||
| 1281 | 19 | bool rtti::TypeIdentity::matches(Address vtable) const noexcept | |
| 1282 | { | ||
| 1283 | 19 | const auto resolved = TypeIdentity::vtable(); | |
| 1284 |
4/4✓ Branch 4 → 5 taken 18 times.
✓ Branch 4 → 9 taken 1 time.
✓ Branch 7 → 8 taken 17 times.
✓ Branch 7 → 9 taken 1 time.
|
19 | return resolved.has_value() && *resolved == vtable; |
| 1285 | } | ||
| 1286 | } // namespace DetourModKit | ||
| 1287 |