src/internal/rtti_shared.hpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #ifndef DETOURMODKIT_INTERNAL_RTTI_SHARED_HPP | ||
| 2 | #define DETOURMODKIT_INTERNAL_RTTI_SHARED_HPP | ||
| 3 | |||
| 4 | /** | ||
| 5 | * @file internal/rtti_shared.hpp | ||
| 6 | * @brief Shared MSVC RTTI prelude for the rtti.cpp and rtti_dissect.cpp TUs. | ||
| 7 | * | ||
| 8 | * Houses the verified COL -> TypeDescriptor walk (@ref resolve_col_site) and the page-bounded name copy (@ref | ||
| 9 | * read_name_seh) that both the forward walker (type_name_of / vtable_is_type) and the reverse dissector | ||
| 10 | * (identify_pointee_type / heal_landmark) consume. The structures and helpers live in DetourModKit::rtti::detail and | ||
| 11 | * are NOT part of the installed public surface; ColHead in particular encodes a raw ABI layout that must never leak | ||
| 12 | * into a consumer-visible header. | ||
| 13 | */ | ||
| 14 | |||
| 15 | #include "DetourModKit/rtti.hpp" | ||
| 16 | |||
| 17 | #include <cstddef> | ||
| 18 | #include <cstdint> | ||
| 19 | #include <limits> | ||
| 20 | #include <type_traits> | ||
| 21 | |||
| 22 | namespace DetourModKit | ||
| 23 | { | ||
| 24 | // Forward declaration so the ModuleSpan overload of resolve_col_site can be declared here without pulling the | ||
| 25 | // guarded-memory engine header into this internal header. Both TUs that define or call the overload (rtti.cpp, | ||
| 26 | // rtti_dissect.cpp) already include the full definition via internal/memory_guarded.hpp. | ||
| 27 | namespace detail | ||
| 28 | { | ||
| 29 | struct ModuleSpan; | ||
| 30 | } | ||
| 31 | |||
| 32 | namespace rtti | ||
| 33 | { | ||
| 34 | namespace detail | ||
| 35 | { | ||
| 36 | // MSVC x64 RTTI ABI layout constants shared by the forward and reverse walkers. | ||
| 37 | inline constexpr std::ptrdiff_t COL_OFFSET_FROM_VTABLE = -8; | ||
| 38 | inline constexpr std::ptrdiff_t TD_NAME_OFFSET = 0x10; | ||
| 39 | inline constexpr std::uint32_t COL_SIGNATURE_X64 = 1; | ||
| 40 | inline constexpr std::uintptr_t MIN_VALID_PTR = 0x10000; | ||
| 41 | |||
| 42 | // Subtraction from PAGE_SIZE keeps the final page bound valid at UINTPTR_MAX. Proof: | ||
| 43 | // RttiReverseWrapTest.VtableForTypeAtMaxMinusFifteenTerminates and | ||
| 44 | // RttiReverseWrapTest.RegionRttiPresenceAtMaxMinusFifteenTerminates. | ||
| 45 | inline constexpr std::uintptr_t PAGE_MASK = 0xFFF; | ||
| 46 | inline constexpr std::uintptr_t PAGE_SIZE = PAGE_MASK + 1; | ||
| 47 | |||
| 48 | /** | ||
| 49 | * @brief Two's-complement magnitude of a signed offset, defined for every value including PTRDIFF_MIN. | ||
| 50 | * @details The naive `(v < 0) ? -v : v` is undefined behavior at PTRDIFF_MIN, whose magnitude is not | ||
| 51 | * representable in std::ptrdiff_t. Computing the negation in the unsigned domain (2^64 - v) yields | ||
| 52 | * the exact magnitude for every input, so a drift/delta comparison never trips signed-overflow UB. | ||
| 53 | */ | ||
| 54 | 4 | [[nodiscard]] constexpr std::uint64_t ptrdiff_magnitude(std::ptrdiff_t value) noexcept | |
| 55 | { | ||
| 56 | 4 | const std::uint64_t bits = static_cast<std::uint64_t>(value); | |
| 57 |
2/2✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 2 times.
|
4 | return (value < 0) ? (~bits + 1U) : bits; |
| 58 | } | ||
| 59 | |||
| 60 | /** | ||
| 61 | * @brief Subtracts two offsets and saturates when their mathematical difference is not representable. | ||
| 62 | * @details Saturation preserves the direction and severity of an extreme drift; wrapping could turn | ||
| 63 | * PTRDIFF_MAX - PTRDIFF_MIN into -1 and suppress a configured drift warning. | ||
| 64 | */ | ||
| 65 | 11 | [[nodiscard]] constexpr std::ptrdiff_t saturating_sub(std::ptrdiff_t a, std::ptrdiff_t b) noexcept | |
| 66 | { | ||
| 67 | 11 | constexpr std::ptrdiff_t MIN = std::numeric_limits<std::ptrdiff_t>::min(); | |
| 68 | 11 | constexpr std::ptrdiff_t MAX = std::numeric_limits<std::ptrdiff_t>::max(); | |
| 69 |
4/4✓ Branch 2 → 3 taken 10 times.
✓ Branch 2 → 5 taken 1 time.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 9 times.
|
11 | if (b > 0 && a < MIN + b) |
| 70 | 1 | return MIN; | |
| 71 |
3/4✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 8 taken 9 times.
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 8 not taken.
|
10 | if (b < 0 && a > MAX + b) |
| 72 | 1 | return MAX; | |
| 73 | 9 | return a - b; | |
| 74 | } | ||
| 75 | |||
| 76 | /** @brief Returns @p address - @p base as a signed offset, saturating when it is not representable. */ | ||
| 77 | 34 | [[nodiscard]] constexpr std::ptrdiff_t address_offset(std::uintptr_t address, std::uintptr_t base) noexcept | |
| 78 | { | ||
| 79 | 34 | constexpr auto MIN = std::numeric_limits<std::ptrdiff_t>::min(); | |
| 80 | 34 | constexpr auto MAX = std::numeric_limits<std::ptrdiff_t>::max(); | |
| 81 | 34 | constexpr std::uint64_t MIN_MAGNITUDE = static_cast<std::uint64_t>(MAX) + 1U; | |
| 82 |
1/2✓ Branch 2 → 3 taken 34 times.
✗ Branch 2 → 7 not taken.
|
34 | if (address >= base) |
| 83 | { | ||
| 84 | 34 | const std::uint64_t difference = address - base; | |
| 85 |
1/2✓ Branch 3 → 4 taken 34 times.
✗ Branch 3 → 5 not taken.
|
34 | return difference > static_cast<std::uint64_t>(MAX) ? MAX : static_cast<std::ptrdiff_t>(difference); |
| 86 | } | ||
| 87 | ✗ | const std::uint64_t difference = base - address; | |
| 88 | ✗ | if (difference >= MIN_MAGNITUDE) | |
| 89 | ✗ | return MIN; | |
| 90 | ✗ | return -static_cast<std::ptrdiff_t>(difference); | |
| 91 | } | ||
| 92 | |||
| 93 | /** | ||
| 94 | * @struct ColHead | ||
| 95 | * @brief First 24 bytes of an MSVC x64 RTTICompleteObjectLocator. | ||
| 96 | * @details Field order is fixed by the MSVC ABI; declaring it as a packed POD lets resolve_col_site() pull | ||
| 97 | * every field in a single SEH read rather than six separate ones. The struct is trivially copyable | ||
| 98 | * so guarded_read<ColHead> instantiates cleanly. | ||
| 99 | */ | ||
| 100 | struct ColHead | ||
| 101 | { | ||
| 102 | std::uint32_t signature; | ||
| 103 | std::uint32_t offset; | ||
| 104 | std::uint32_t cd_offset; | ||
| 105 | std::uint32_t p_type_descriptor; | ||
| 106 | std::uint32_t p_class_descriptor; | ||
| 107 | std::uint32_t p_self; | ||
| 108 | }; | ||
| 109 | static_assert(sizeof(ColHead) == 24, "ColHead must match MSVC x64 ABI layout"); | ||
| 110 | static_assert(std::is_trivially_copyable_v<ColHead>); | ||
| 111 | |||
| 112 | /** | ||
| 113 | * @struct ColSite | ||
| 114 | * @brief Fully resolved COL/TypeDescriptor coordinates for a vtable. | ||
| 115 | * @details Output of @ref resolve_col_site. Carries every address the verified prelude recovers so a caller | ||
| 116 | * can read the name (@ref name_addr) or recover the complete object from @ref col_offset without | ||
| 117 | * re-walking the COL. | ||
| 118 | */ | ||
| 119 | struct ColSite | ||
| 120 | { | ||
| 121 | /// Address of the COL the vtable points back to. | ||
| 122 | std::uintptr_t col_addr = 0; | ||
| 123 | /// TypeDescriptor base (image base + COL.pTypeDescriptor RVA). | ||
| 124 | std::uintptr_t td_addr = 0; | ||
| 125 | /// Mangled-name buffer (td_addr + TD_NAME_OFFSET). | ||
| 126 | std::uintptr_t name_addr = 0; | ||
| 127 | /// Exclusive end [base, end) of the owning module; bounds the name read (see @ref read_name_seh). | ||
| 128 | std::uintptr_t module_end = 0; | ||
| 129 | /// COL.offset (+0x04): this vtable's offset in the complete object. | ||
| 130 | std::uint32_t col_offset = 0; | ||
| 131 | }; | ||
| 132 | |||
| 133 | /** | ||
| 134 | * @brief Runs the verified COL prelude for @p vtable and reports every recovered coordinate in @p out. | ||
| 135 | * @details Every intermediate address (the COL pointer, the pSelf-recovered image base, and the final name | ||
| 136 | * buffer) is required to lie inside the vtable's owning module range. This forces a poisoned COL | ||
| 137 | * to fail closed: a forged pTypeDescriptor that would otherwise redirect a read to another loaded | ||
| 138 | * module or to unmapped memory is rejected here instead of read-through. @p out is left untouched | ||
| 139 | * on any failure. | ||
| 140 | * @param vtable Runtime vtable pointer (first qword of the object). | ||
| 141 | * @param out Receives the resolved coordinates on success only. | ||
| 142 | * @return true on a fully validated walk, false for every failure mode (vtable not in a loaded module, | ||
| 143 | * unreadable COL, COL pointer escaping the module range, pSelf disagreeing with the loader-reported | ||
| 144 | * base, zero TypeDescriptor RVA, name address escaping the module range). | ||
| 145 | */ | ||
| 146 | [[nodiscard]] bool resolve_col_site(std::uintptr_t vtable, ColSite &out) noexcept; | ||
| 147 | |||
| 148 | /** | ||
| 149 | * @brief resolve_col_site variant that reuses a caller-held owning-module span. | ||
| 150 | * @details Identical validation to the single-argument overload, but the caller supplies @p mod_range (the | ||
| 151 | * span of the module that owns @p vtable) instead of this function resolving it via | ||
| 152 | * memory::module_of. The reverse sweep already holds the module span for every candidate it tests, | ||
| 153 | * so routing through this overload removes a per-candidate GetModuleHandleExW loader lookup from | ||
| 154 | * the hot RTTI scan - the per-frame cliff a TypeIdentity polled for an absent type hits. The | ||
| 155 | * overload requires @p vtable to lie inside @p mod_range (the same invariant module_of grants | ||
| 156 | * implicitly: the loader only resolves a module that contains the address), so a candidate one | ||
| 157 | * past the module end still fails closed. | ||
| 158 | * @param vtable Runtime vtable pointer (first qword of the object). | ||
| 159 | * @param mod_range Pre-resolved span of the module that owns @p vtable. | ||
| 160 | * @param out Receives the resolved coordinates on success only. | ||
| 161 | * @return true on a fully validated walk; false for every failure mode the single-argument overload | ||
| 162 | * rejects, plus a @p vtable outside @p mod_range. | ||
| 163 | */ | ||
| 164 | [[nodiscard]] bool resolve_col_site( | ||
| 165 | std::uintptr_t vtable, | ||
| 166 | const DetourModKit::detail::ModuleSpan &mod_range, | ||
| 167 | ColSite &out | ||
| 168 | ) noexcept; | ||
| 169 | |||
| 170 | /** | ||
| 171 | * @brief Page-bounded, module-bounded NUL-terminated copy from @p addr into @p out. | ||
| 172 | * @details Reads in chunks that never cross a 4 KiB page boundary so a string that ends just before an | ||
| 173 | * unmapped page still terminates cleanly. To honour the all-or-nothing failure contract, bytes are | ||
| 174 | * first accumulated in a local temporary; the final commit into @p out only happens when either | ||
| 175 | * the terminating NUL is observed or the allowed length is filled in full. A read fault before | ||
| 176 | * either condition leaves @p out as the empty NUL-terminated string and returns 0. The read is | ||
| 177 | * additionally clamped to @p module_end so a mangled name that lacks a NUL before the end of its | ||
| 178 | * owning module (a forged or edge-of-module TypeDescriptor) is truncated at the module boundary | ||
| 179 | * rather than read-through into an adjacent mapped image, which would surface another module's | ||
| 180 | * bytes as a confident type name (an information leak). | ||
| 181 | * @param addr Address of the first name byte. Values below | ||
| 182 | * MIN_VALID_PTR are rejected without a read. | ||
| 183 | * @param out Destination buffer. nullptr or zero length returns 0. | ||
| 184 | * @param out_len Capacity of @p out including the NUL terminator. | ||
| 185 | * @param module_end Exclusive end of the owning module range; the read never advances to or past it. Zero | ||
| 186 | * means "no module bound known" and only the length caps apply (@ref resolve_col_site supplies | ||
| 187 | * a real end, so a caller threading a @ref ColSite gets the bound automatically). | ||
| 188 | * @return Number of bytes written excluding the NUL terminator, or | ||
| 189 | * 0 on any partial-read failure. | ||
| 190 | */ | ||
| 191 | [[nodiscard]] std::size_t | ||
| 192 | read_name_seh(std::uintptr_t addr, char *out, std::size_t out_len, std::uintptr_t module_end) noexcept; | ||
| 193 | } // namespace detail | ||
| 194 | } // namespace rtti | ||
| 195 | } // namespace DetourModKit | ||
| 196 | |||
| 197 | #endif // DETOURMODKIT_INTERNAL_RTTI_SHARED_HPP | ||
| 198 |