GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 81.8% 18 / 0 / 22
Functions: 100.0% 3 / 0 / 3
Branches: 68.8% 11 / 0 / 16

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