VoltMod
C++23 framework for CS2 server plugins
Loading...
Searching...
No Matches
VtableLookup.linux.cpp
Go to the documentation of this file.
2
3#include <cstdint>
4#include <cstring>
5#include <dlfcn.h>
6#include <elf.h>
7#include <fcntl.h>
8#include <filesystem>
9#include <format>
10#include <string>
11#include <string_view>
12#include <sys/mman.h>
13#include <sys/stat.h>
14#include <unistd.h>
15#include <vector>
16
17namespace VoltMod
18{
19
21{
22public:
23 explicit MappedFile(const std::string& path)
24 {
25 const int fd = open(path.c_str(), O_RDONLY);
26 if (fd == -1)
27 {
28 return;
29 }
30
31 struct stat info{};
32 if (fstat(fd, &info) == 0 && info.st_size > 0)
33 {
34 void* map = mmap(nullptr, static_cast<size_t>(info.st_size), PROT_READ, MAP_PRIVATE, fd, 0);
35 if (map != MAP_FAILED)
36 {
37 _base = static_cast<const uint8_t*>(map);
38 _size = static_cast<size_t>(info.st_size);
39 }
40 }
41 close(fd);
42 }
43
45 {
46 if (_base)
47 {
48 munmap(const_cast<uint8_t*>(_base), _size);
49 }
50 }
51
52 MappedFile(const MappedFile&) = delete;
53 MappedFile& operator=(const MappedFile&) = delete;
54
55 explicit operator bool() const { return _base != nullptr; }
56
57 template <typename T>
58 const T* At(size_t offset, size_t count = 1) const
59 {
60 if (!_base || count == 0 || offset > _size || (_size - offset) / sizeof(T) < count)
61 {
62 return nullptr;
63 }
64 return reinterpret_cast<const T*>(_base + offset);
65 }
66
67private:
68 const uint8_t* _base = nullptr;
69 size_t _size = 0;
70};
71
72static uint64_t FindSymbolValue(const MappedFile& elf, const std::string& symbol)
73{
74 const auto* header = elf.At<Elf64_Ehdr>(0);
75 if (!header || std::memcmp(header->e_ident, ELFMAG, SELFMAG) != 0 || header->e_ident[EI_CLASS] != ELFCLASS64)
76 {
77 return 0;
78 }
79
80 const auto* sections = elf.At<Elf64_Shdr>(header->e_shoff, header->e_shnum);
81 if (!sections || header->e_shentsize != sizeof(Elf64_Shdr))
82 {
83 return 0;
84 }
85
86 // Use .symtab when present because .dynsym contains only exports.
88 {
89 for (uint16_t i = 0; i < header->e_shnum; ++i)
90 {
91 const Elf64_Shdr& section = sections[i];
92 if (section.sh_type != wanted || section.sh_entsize != sizeof(Elf64_Sym) ||
93 section.sh_link >= header->e_shnum)
94 {
95 continue;
96 }
97
98 const Elf64_Shdr& strings = sections[section.sh_link];
99 const auto* names = elf.At<char>(strings.sh_offset, strings.sh_size);
100 const auto* symbols = elf.At<Elf64_Sym>(section.sh_offset, section.sh_size / sizeof(Elf64_Sym));
101 if (!names || !symbols)
102 {
103 continue;
104 }
105
106 for (size_t s = 0; s < section.sh_size / sizeof(Elf64_Sym); ++s)
107 {
108 if (symbols[s].st_name >= strings.sh_size || symbols[s].st_value == 0)
109 {
110 continue;
111 }
112 if (symbol == names + symbols[s].st_name)
113 {
114 return symbols[s].st_value;
115 }
116 }
117 }
118 }
119 return 0;
120}
121
122static bool RangesOf(const Image& module, std::vector<ScanRange>& ranges)
123{
125 const std::string fileName = std::filesystem::path(module.Path).filename().string();
127}
128
129void* FindVirtualTableIn(const Image& module, std::string_view className)
130{
131 if (module.Path.empty())
132 {
133 return nullptr;
134 }
135
136 if (MappedFile elf(module.Path); elf)
137 {
138 // Itanium ABI vtable symbols use _ZTV<length><name>.
139 const std::string symbol = "_ZTV" + LengthPrefixedName(className);
140 // Object vptrs point past offset-to-top and typeinfo.
141 if (const uint64_t value = FindSymbolValue(elf, symbol))
142 {
143 return const_cast<uint8_t*>(module.Base + value + 2 * sizeof(void*));
144 }
145 }
146
147 // Game modules hide vtable symbols but retain RTTI, so search mapped segments.
148 std::vector<ScanRange> ranges;
149 if (!RangesOf(module, ranges))
150 {
151 return nullptr;
152 }
154}
155
156/** The cxxabi typeinfo vptrs exported by this process, cached for its lifetime. */
158{
159 static const TypeInfoKinds kinds = [] {
160 // A typeinfo's vptr points two words into its class vtable.
161 const auto vptrOf = [](const char* symbol) -> uintptr_t {
162 void* table = dlsym(RTLD_DEFAULT, symbol);
163 return table ? reinterpret_cast<uintptr_t>(table) + 2 * sizeof(void*) : 0;
164 };
165
166 const TypeInfoKinds found{.SingleBase = vptrOf("_ZTVN10__cxxabiv120__si_class_type_infoE"),
167 .MultipleBases = vptrOf("_ZTVN10__cxxabiv121__vmi_class_type_infoE")};
168 return found.SingleBase && found.MultipleBases ? found : TypeInfoKinds{};
169 }();
170 return kinds;
171}
172
173Result<BaseSubobject> FindBaseIn(const Image& module, std::string_view className, std::string_view baseName)
174{
176 if (!primary)
177 {
178 return std::unexpected(Error::NotFound(std::format("no vtable for '{}'", className)));
179 }
180
181 const void* typeInfo = static_cast<void**>(primary)[-1];
183 // Modules with a private C++ runtime have different vptrs, so validate record shapes instead.
184 if (!offset && offset.error().Code == ErrorCode::NotFound)
185 {
187 }
188 if (!offset)
189 {
190 return std::unexpected(offset.error());
191 }
192 if (*offset == 0)
193 {
194 return BaseSubobject{.Offset = 0, .Table = primary};
195 }
196
197 std::vector<ScanRange> ranges;
198 if (!RangesOf(module, ranges))
199 {
200 return BaseSubobject{.Offset = *offset};
201 }
202 return BaseSubobject{.Offset = *offset, .Table = FindVirtualTableByTypeInfo(ranges, typeInfo, -*offset)};
203}
204
205} // namespace VoltMod
MappedFile(const std::string &path)
MappedFile(const MappedFile &)=delete
const T * At(size_t offset, size_t count=1) const
MappedFile & operator=(const MappedFile &)=delete
void * FindVirtualTableByTypeInfo(std::span< const ScanRange > ranges, const void *typeInfo, intptr_t offsetToTop)
bool FindModuleAndRanges(std::string_view fileName, Image &module, std::vector< ScanRange > &ranges)
@ NotFound
The named thing does not exist (no such player, convar, row).
static std::string ReadFile(const std::filesystem::path &path)
Definition Loader.cpp:56
static uint64_t FindSymbolValue(const MappedFile &elf, const std::string &symbol)
Result< int > FindBaseOffsetByTypeInfo(const void *typeInfo, std::string_view baseName, const TypeInfoKinds &kinds)
Result< BaseSubobject > FindBaseIn(const Image &module, std::string_view className, std::string_view baseName)
static const TypeInfoKinds & ExportedTypeInfoKinds()
void * FindVirtualTableByTypeName(std::span< const ScanRange > ranges, std::string_view className)
std::string LengthPrefixedName(std::string_view className)
static bool RangesOf(const Image &module, std::vector< ScanRange > &ranges)
std::expected< T, Error > Result
Definition Result.hpp:64
void * FindVirtualTableIn(const Image &module, std::string_view className)
static Error NotFound(std::string detail)
Definition Result.hpp:49