VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
VtableLookup.cpp
Go to the documentation of this file.
2
4
6#include <cctype>
7#include <cstdint>
8#include <cstring>
9#include <format>
10#include <span>
11#include <string>
12#include <string_view>
13#include <vector>
14
15namespace VoltMod
16{
17
18void* FindVirtualTable(std::string_view moduleName, std::string_view className)
19{
20 if (moduleName.empty() || className.empty())
21 {
22 return nullptr;
23 }
24
25 Image module;
27 {
28 Log::Warn("VtableLookup: Module '{}' not found.", PlatformModuleName(moduleName));
29 return nullptr;
30 }
31
33 if (!vtable)
34 {
35 Log::Warn("VtableLookup: '{}' vtable not found in '{}'.", className, PlatformModuleName(moduleName));
36 }
37 return vtable;
38}
39
41{
42 uintptr_t word = 0;
43 std::memcpy(&word, reinterpret_cast<const void*>(address), sizeof(word));
44 return word;
45}
46
48{
49 uint32_t value = 0;
50 std::memcpy(&value, reinterpret_cast<const void*>(address), sizeof(value));
51 return value;
52}
53
54static std::vector<uintptr_t> FindWords(std::span<const ScanRange> ranges, uintptr_t value)
55{
56 std::vector<uintptr_t> found;
57 for (const ScanRange& range : ranges)
58 {
59 const auto begin = reinterpret_cast<uintptr_t>(range.Base);
60 const uintptr_t first = ((begin + sizeof(void*) - 1) & ~uintptr_t{sizeof(void*) - 1}) + sizeof(void*);
61 for (uintptr_t at = first; at + 2 * sizeof(void*) <= begin + range.Size; at += sizeof(void*))
62 {
63 if (ReadWord(at) == value)
64 {
65 found.push_back(at);
66 }
67 }
68 }
69 return found;
70}
71
72std::string LengthPrefixedName(std::string_view className)
73{
74 return std::to_string(className.size()) + std::string(className);
75}
76
77void* FindVirtualTableByTypeName(std::span<const ScanRange> ranges, std::string_view className)
78{
79 // Itanium names are length-prefixed; typeinfos hold a name and vtables hold offset-to-top and typeinfo.
80 const std::string typeName = LengthPrefixedName(className);
81 const std::string_view needle(typeName.c_str(), typeName.size() + 1);
82 for (const ScanRange& range : ranges)
83 {
84 const std::string_view memory(reinterpret_cast<const char*>(range.Base), range.Size);
85 for (size_t at = memory.find(needle); at != std::string_view::npos; at = memory.find(needle, at + 1))
86 {
87 // A preceding name character means this is part of a longer mangled name.
88 if (at > 0 && (std::isalnum(static_cast<unsigned char>(memory[at - 1])) || memory[at - 1] == '_'))
89 {
90 continue;
91 }
92
93 for (uintptr_t name : FindWords(ranges, reinterpret_cast<uintptr_t>(range.Base) + at))
94 {
95 for (uintptr_t typeInfo : FindWords(ranges, name - sizeof(void*)))
96 {
97 // The primary table has offset-to-top zero and executable code in its first slot.
98 if (ReadWord(typeInfo - sizeof(void*)) == 0 &&
99 IsExecutableAddress(reinterpret_cast<const void*>(ReadWord(typeInfo + sizeof(void*)))))
100 {
101 return reinterpret_cast<void*>(typeInfo + sizeof(void*));
102 }
103 }
104 }
105 }
106 }
107 return nullptr;
108}
109
110bool IsInstanceOf(const void* object, const void* table)
111{
112 return table && IsReadableAddress(object, sizeof(void*)) &&
113 ReadWord(reinterpret_cast<uintptr_t>(object)) == reinterpret_cast<uintptr_t>(table);
114}
115
116// Itanium typeinfo layouts: {vptr, name}, optionally followed by base or base-list records.
117static constexpr size_t TypeInfoBase = 2 * sizeof(void*);
118static constexpr size_t TypeInfoFlags = 2 * sizeof(void*);
119static constexpr size_t TypeInfoCount = TypeInfoFlags + sizeof(uint32_t);
120static constexpr size_t TypeInfoBaseList = 3 * sizeof(void*);
121static constexpr size_t BaseEntrySize = 2 * sizeof(void*);
122static constexpr uintptr_t VirtualBaseFlag = 1;
123
124static constexpr uint32_t MaxBases = 64;
125static constexpr int MaxDepth = 32;
126
128{
129 if (!address || address % alignof(void*) != 0 ||
130 !IsReadableAddress(reinterpret_cast<const void*>(address), 2 * sizeof(void*)))
131 {
132 return false;
133 }
134
135 const auto* name = reinterpret_cast<const char*>(ReadWord(address + sizeof(void*)));
136 if (!IsReadableAddress(name, 1))
137 {
138 return false;
139 }
140 return std::isdigit(static_cast<unsigned char>(*name)) || *name == 'N' || *name == '*';
141}
142
143static bool NameIs(uintptr_t address, std::string_view wanted)
144{
145 const auto* name = reinterpret_cast<const char*>(ReadWord(address + sizeof(void*)));
146 return IsReadableAddress(name, wanted.size() + 1) && std::string_view(name, wanted.size()) == wanted &&
147 name[wanted.size()] == '\0';
148}
149
151{
152 if (kinds.SingleBase)
153 {
154 return ReadWord(typeInfo) == kinds.SingleBase;
155 }
156 return IsReadableAddress(reinterpret_cast<const void*>(typeInfo), TypeInfoBase + sizeof(void*)) &&
158}
159
161{
162 if (kinds.MultipleBases && ReadWord(typeInfo) != kinds.MultipleBases)
163 {
164 return 0;
165 }
166 if (!IsReadableAddress(reinterpret_cast<const void*>(typeInfo), TypeInfoBaseList))
167 {
168 return 0;
169 }
170
172 if (count == 0 || count > MaxBases ||
173 !IsReadableAddress(reinterpret_cast<const void*>(typeInfo + TypeInfoBaseList), count * BaseEntrySize))
174 {
175 return 0;
176 }
177
178 // Without the real vptr, validate the flags and first-base shape.
179 if (!kinds.MultipleBases &&
181 {
182 return 0;
183 }
184 return count;
185}
186
188{
190 bool Virtual = false;
191};
192
193static void CollectBases(uintptr_t typeInfo, std::string_view wanted, FoundBase at, const TypeInfoKinds& kinds,
194 int depth, std::vector<FoundBase>& found)
195{
196 if (depth > MaxDepth)
197 {
198 return;
199 }
200
201 const auto visit = [&](uintptr_t base, intptr_t offset, bool isVirtual) {
202 if (!LooksLikeTypeInfo(base))
203 {
204 return;
205 }
206
207 const FoundBase here{.Offset = at.Offset + offset, .Virtual = at.Virtual || isVirtual};
208 if (NameIs(base, wanted))
209 {
210 found.push_back(here);
211 }
212 else
213 {
214 CollectBases(base, wanted, here, kinds, depth + 1, found);
215 }
216 };
217
219 {
220 visit(ReadWord(typeInfo + TypeInfoBase), 0, false);
221 return;
222 }
223
225 for (uint32_t i = 0; i < count; ++i)
226 {
228 const auto offsetFlags = static_cast<intptr_t>(ReadWord(entry + sizeof(void*)));
229 visit(ReadWord(entry), offsetFlags >> 8, (static_cast<uintptr_t>(offsetFlags) & VirtualBaseFlag) != 0);
230 }
231}
232
234{
235 const auto address = reinterpret_cast<uintptr_t>(typeInfo);
237 {
238 return std::unexpected(Error::Invalid("the class has no readable typeinfo"));
239 }
240
241 const std::string wanted = LengthPrefixedName(baseName);
242 std::vector<FoundBase> found;
244
245 if (found.empty())
246 {
247 return std::unexpected(Error::NotFound(std::format("'{}' is not a base", baseName)));
248 }
249 if (found.size() > 1)
250 {
251 return std::unexpected(Error::Invalid(std::format("'{}' is a base {} times", baseName, found.size())));
252 }
253 if (found.front().Virtual)
254 {
255 return std::unexpected(Error::Unsupported(std::format("'{}' is a virtual base", baseName)));
256 }
257 return static_cast<int>(found.front().Offset);
258}
259
260void* FindVirtualTableByTypeInfo(std::span<const ScanRange> ranges, const void* typeInfo, intptr_t offsetToTop)
261{
262 void* found = nullptr;
263 for (uintptr_t at : FindWords(ranges, reinterpret_cast<uintptr_t>(typeInfo)))
264 {
265 const uintptr_t table = at + sizeof(void*);
266 if (static_cast<intptr_t>(ReadWord(at - sizeof(void*))) != offsetToTop ||
267 !IsExecutableAddress(reinterpret_cast<const void*>(ReadWord(table))))
268 {
269 continue;
270 }
271
272 if (found)
273 {
274 return nullptr;
275 }
276 found = reinterpret_cast<void*>(table);
277 }
278 return found;
279}
280
281} // namespace VoltMod
void Warn(std::format_string< Args... > fmt, Args &&... args)
Definition Log.hpp:88
void * FindVirtualTableByTypeInfo(std::span< const ScanRange > ranges, const void *typeInfo, intptr_t offsetToTop)
bool FindImage(std::string_view moduleName, Image &module)
static constexpr int MaxDepth
static std::vector< uintptr_t > FindWords(std::span< const ScanRange > ranges, uintptr_t value)
static uint32_t ReadU32(uintptr_t address)
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
bool IsExecutableAddress(const void *address)
bool IsReadableAddress(const void *address, size_t bytes)
static uintptr_t ReadWord(uintptr_t address)
static void CollectBases(uintptr_t typeInfo, std::string_view wanted, FoundBase at, const TypeInfoKinds &kinds, int depth, std::vector< FoundBase > &found)
static bool HasSingleBase(uintptr_t typeInfo, const TypeInfoKinds &kinds)
static bool NameIs(uintptr_t address, std::string_view wanted)
static bool LooksLikeTypeInfo(uintptr_t address)
Result< int > FindBaseOffsetByTypeInfo(const void *typeInfo, std::string_view baseName, const TypeInfoKinds &kinds)
static uint32_t BaseCount(uintptr_t typeInfo, const TypeInfoKinds &kinds)
static constexpr size_t TypeInfoFlags
static constexpr uintptr_t VirtualBaseFlag
void * FindVirtualTableByTypeName(std::span< const ScanRange > ranges, std::string_view className)
static constexpr size_t TypeInfoCount
std::string LengthPrefixedName(std::string_view className)
static constexpr size_t BaseEntrySize
static constexpr uint32_t MaxBases
static constexpr size_t TypeInfoBaseList
void * FindVirtualTable(std::string_view moduleName, std::string_view className)
static constexpr size_t TypeInfoBase
std::expected< T, Error > Result
Definition Result.hpp:64
std::string PlatformModuleName(std::string_view moduleName)
bool IsInstanceOf(const void *object, const void *table)
void * FindVirtualTableIn(const Image &module, std::string_view className)
static Error Invalid(std::string detail)
Definition Result.hpp:51
static Error Unsupported(std::string detail)
Definition Result.hpp:57
static Error NotFound(std::string detail)
Definition Result.hpp:49