mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-09-04 17:21:31 -04:00
414 lines
14 KiB
C++
414 lines
14 KiB
C++
#ifndef NOMINMAX
|
|
#define NOMINMAX
|
|
#endif
|
|
|
|
#include "manifest.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <limits>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <zstd.h>
|
|
|
|
#include <borealis/log.hpp>
|
|
|
|
#if defined(_WIN32)
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <windows.h>
|
|
#elif defined(__APPLE__)
|
|
#include <mach-o/dyld.h>
|
|
#include <mach-o/loader.h>
|
|
#elif defined(__linux__)
|
|
#include <elf.h>
|
|
#include <link.h>
|
|
#endif
|
|
|
|
namespace dusk::mods::manifest {
|
|
namespace {
|
|
|
|
constexpr borealis::Log Log{"dusk::mods::manifest"};
|
|
|
|
constexpr char kMagic[8] = {'S', 'Y', 'M', 'G', 'E', 'N', '\0', '\0'};
|
|
constexpr uint32_t kVersion = 2;
|
|
|
|
enum class Compression : uint32_t {
|
|
None = 0,
|
|
Zstd = 1,
|
|
};
|
|
|
|
// Mirrors the symgen manifest writer.
|
|
struct Header {
|
|
char magic[8];
|
|
uint32_t version;
|
|
uint32_t compression;
|
|
uint64_t uncompressedLen;
|
|
uint64_t compressedLen;
|
|
uint32_t buildIdLen;
|
|
uint8_t buildId[32];
|
|
uint32_t entryCount;
|
|
};
|
|
static_assert(sizeof(Header) == 72);
|
|
|
|
struct Entry {
|
|
uint64_t hash;
|
|
uint64_t rva;
|
|
uint32_t nameOff;
|
|
HookSymbolFlags flags;
|
|
};
|
|
static_assert(sizeof(Entry) == 24);
|
|
|
|
/*
|
|
* `symgen manifest --embed` appends the symbol manifest to the linked executable as an added
|
|
* section and patches this descriptor with its location.
|
|
*/
|
|
struct SymdbDescriptor {
|
|
volatile uint64_t magic;
|
|
volatile uint64_t rva;
|
|
volatile uint64_t size;
|
|
};
|
|
constexpr uint64_t kDescriptorMagic = 0x52444842444d5953ull; // "SYMDBHDR"
|
|
#if defined(_WIN32)
|
|
#pragma section(".symdbh", read)
|
|
__declspec(allocate(".symdbh"))
|
|
#if defined(__clang__)
|
|
__attribute__((used))
|
|
#endif
|
|
constinit SymdbDescriptor s_symdbDescriptor{kDescriptorMagic, 0, 0};
|
|
#elif defined(__APPLE__)
|
|
__attribute__((section("__DATA,__symdbh"), used)) constinit SymdbDescriptor s_symdbDescriptor{
|
|
kDescriptorMagic, 0, 0};
|
|
#else
|
|
__attribute__((section("symdbh"), used)) constinit SymdbDescriptor s_symdbDescriptor{
|
|
kDescriptorMagic, 0, 0};
|
|
#endif
|
|
|
|
struct State {
|
|
std::vector<uint8_t> data;
|
|
const Entry* entries = nullptr;
|
|
uint32_t entryCount = 0;
|
|
const char* strings = nullptr;
|
|
uint64_t stringsLen = 0;
|
|
uintptr_t imageBase = 0;
|
|
// (rva, nameOff) of entries flagged kFlagInlineSites, sorted by rva
|
|
std::vector<std::pair<uint64_t, uint32_t>> inlineSites;
|
|
bool loaded = false;
|
|
bool initialized = false;
|
|
};
|
|
State s_state;
|
|
|
|
uint64_t fnv1a64(const char* str) {
|
|
uint64_t hash = 0xcbf29ce484222325ull;
|
|
for (const char* p = str; *p != '\0'; ++p) {
|
|
hash ^= static_cast<uint8_t>(*p);
|
|
hash *= 0x100000001b3ull;
|
|
}
|
|
return hash;
|
|
}
|
|
|
|
// Build id of the running executable image, matching what symgen recorded:
|
|
// PDB GUID (RFC 4122 byte order) + age on Windows, LC_UUID on Mach-O, GNU
|
|
// build-id on ELF. Also reports the address RVAs are relative to.
|
|
bool running_image_identity(std::vector<uint8_t>& outId, uintptr_t& outBase) {
|
|
#if defined(_WIN32)
|
|
auto* base = reinterpret_cast<uint8_t*>(GetModuleHandleW(nullptr));
|
|
outBase = reinterpret_cast<uintptr_t>(base);
|
|
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
|
|
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(base + dos->e_lfanew);
|
|
const auto& dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG];
|
|
if (dir.VirtualAddress == 0) {
|
|
return false;
|
|
}
|
|
const auto* entries = reinterpret_cast<const IMAGE_DEBUG_DIRECTORY*>(base + dir.VirtualAddress);
|
|
for (size_t i = 0; i < dir.Size / sizeof(IMAGE_DEBUG_DIRECTORY); ++i) {
|
|
if (entries[i].Type != IMAGE_DEBUG_TYPE_CODEVIEW) {
|
|
continue;
|
|
}
|
|
struct CvInfo {
|
|
uint32_t signature; // 'RSDS'
|
|
uint8_t guid[16];
|
|
uint32_t age;
|
|
};
|
|
if (entries[i].SizeOfData < sizeof(CvInfo)) {
|
|
continue;
|
|
}
|
|
const auto* cv = reinterpret_cast<const CvInfo*>(base + entries[i].AddressOfRawData);
|
|
if (cv->signature != 0x53445352) { // "RSDS"
|
|
continue;
|
|
}
|
|
// The GUID struct stores Data1..Data3 little-endian in memory; the manifest
|
|
// stores RFC 4122 (big-endian) order, so swap them here.
|
|
outId.assign(cv->guid, cv->guid + 16);
|
|
std::swap(outId[0], outId[3]);
|
|
std::swap(outId[1], outId[2]);
|
|
std::swap(outId[4], outId[5]);
|
|
std::swap(outId[6], outId[7]);
|
|
for (int b = 0; b < 4; ++b) {
|
|
outId.push_back(static_cast<uint8_t>(cv->age >> (8 * b)));
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
#elif defined(__APPLE__)
|
|
// Image 0 is the main executable. The manifest stores link-time vmaddrs
|
|
// (nm convention, __TEXT vmaddr included).
|
|
const auto* header = _dyld_get_image_header(0);
|
|
outBase = static_cast<uintptr_t>(_dyld_get_image_vmaddr_slide(0));
|
|
const auto* header64 = reinterpret_cast<const mach_header_64*>(header);
|
|
const auto* cmd = reinterpret_cast<const load_command*>(header64 + 1);
|
|
for (uint32_t i = 0; i < header64->ncmds; ++i) {
|
|
if (cmd->cmd == LC_UUID) {
|
|
const auto* uuidCmd = reinterpret_cast<const uuid_command*>(cmd);
|
|
outId.assign(uuidCmd->uuid, uuidCmd->uuid + 16);
|
|
return true;
|
|
}
|
|
cmd = reinterpret_cast<const load_command*>(
|
|
reinterpret_cast<const uint8_t*>(cmd) + cmd->cmdsize);
|
|
}
|
|
return false;
|
|
#elif defined(__linux__)
|
|
struct Ctx {
|
|
std::vector<uint8_t>* id;
|
|
uintptr_t probe;
|
|
uintptr_t base = 0;
|
|
bool found = false;
|
|
} ctx{&outId, reinterpret_cast<uintptr_t>(&s_symdbDescriptor)};
|
|
dl_iterate_phdr(
|
|
[](dl_phdr_info* info, size_t, void* data) -> int {
|
|
auto* ctx = static_cast<Ctx*>(data);
|
|
// Select the image containing our descriptor: the game is not always the
|
|
// first entry (on Android it is libmain.so, behind the app process).
|
|
bool contains = false;
|
|
for (int i = 0; i < info->dlpi_phnum; ++i) {
|
|
const auto& phdr = info->dlpi_phdr[i];
|
|
if (phdr.p_type == PT_LOAD && ctx->probe >= info->dlpi_addr + phdr.p_vaddr &&
|
|
ctx->probe - (info->dlpi_addr + phdr.p_vaddr) < phdr.p_memsz)
|
|
{
|
|
contains = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!contains) {
|
|
return 0;
|
|
}
|
|
ctx->base = info->dlpi_addr;
|
|
for (int i = 0; i < info->dlpi_phnum; ++i) {
|
|
const auto& phdr = info->dlpi_phdr[i];
|
|
if (phdr.p_type != PT_NOTE) {
|
|
continue;
|
|
}
|
|
const auto* p = reinterpret_cast<const uint8_t*>(info->dlpi_addr + phdr.p_vaddr);
|
|
const auto* end = p + phdr.p_memsz;
|
|
while (p + sizeof(ElfW(Nhdr)) <= end) {
|
|
const auto* note = reinterpret_cast<const ElfW(Nhdr)*>(p);
|
|
const auto* name = p + sizeof(ElfW(Nhdr));
|
|
const auto* desc = name + ((note->n_namesz + 3) & ~3u);
|
|
if (note->n_type == NT_GNU_BUILD_ID && note->n_namesz == 4 &&
|
|
std::memcmp(name, "GNU", 4) == 0)
|
|
{
|
|
ctx->id->assign(desc, desc + note->n_descsz);
|
|
ctx->found = true;
|
|
return 1;
|
|
}
|
|
p = desc + ((note->n_descsz + 3) & ~3u);
|
|
}
|
|
}
|
|
return 1; // matched our image; stop either way
|
|
},
|
|
&ctx);
|
|
outBase = ctx.base;
|
|
return ctx.found;
|
|
#else
|
|
(void)outId;
|
|
(void)outBase;
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
std::string hex_string(const uint8_t* data, size_t len) {
|
|
std::string out;
|
|
out.reserve(len * 2);
|
|
for (size_t i = 0; i < len; ++i) {
|
|
constexpr char kHex[] = "0123456789abcdef";
|
|
out.push_back(kHex[data[i] >> 4]);
|
|
out.push_back(kHex[data[i] & 0xF]);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void initialize() {
|
|
if (s_state.initialized) {
|
|
return;
|
|
}
|
|
s_state.initialized = true;
|
|
|
|
if (s_symdbDescriptor.magic != kDescriptorMagic) {
|
|
Log.error("symbol manifest descriptor is corrupt");
|
|
return;
|
|
}
|
|
if (s_symdbDescriptor.rva == 0) {
|
|
Log.info("no symbol manifest embedded; by-name resolution unavailable");
|
|
return;
|
|
}
|
|
|
|
std::vector<uint8_t> imageId;
|
|
uintptr_t imageBase = 0;
|
|
if (!running_image_identity(imageId, imageBase)) {
|
|
Log.error("cannot determine the running image's build id; ignoring symbol manifest");
|
|
return;
|
|
}
|
|
|
|
const auto* blob = reinterpret_cast<const uint8_t*>(imageBase + s_symdbDescriptor.rva);
|
|
const auto blobLen = static_cast<size_t>(s_symdbDescriptor.size);
|
|
if (blobLen < sizeof(Header)) {
|
|
Log.error("embedded symbol manifest is truncated ({} bytes)", blobLen);
|
|
return;
|
|
}
|
|
|
|
Header header{};
|
|
std::memcpy(&header, blob, sizeof(header));
|
|
if (std::memcmp(header.magic, kMagic, sizeof(kMagic)) != 0 || header.version != kVersion) {
|
|
Log.error("embedded symbol manifest has wrong magic/version");
|
|
return;
|
|
}
|
|
const auto compression = static_cast<Compression>(header.compression);
|
|
if ((compression != Compression::None && compression != Compression::Zstd) ||
|
|
header.buildIdLen > sizeof(header.buildId) ||
|
|
header.compressedLen > blobLen - sizeof(Header) ||
|
|
header.uncompressedLen > std::numeric_limits<size_t>::max() ||
|
|
(compression == Compression::None && header.compressedLen != header.uncompressedLen))
|
|
{
|
|
Log.error("embedded symbol manifest is malformed");
|
|
return;
|
|
}
|
|
|
|
// The manifest travels inside the image it describes, so a mismatch here means broken
|
|
// build tooling rather than a stale file — but it still guards resolved addresses.
|
|
if (imageId.size() != header.buildIdLen ||
|
|
std::memcmp(imageId.data(), header.buildId, imageId.size()) != 0)
|
|
{
|
|
Log.error("embedded symbol manifest is stale: built for {}, running image is {}",
|
|
hex_string(header.buildId, header.buildIdLen),
|
|
hex_string(imageId.data(), imageId.size()));
|
|
return;
|
|
}
|
|
|
|
const auto compressedLen = static_cast<size_t>(header.compressedLen);
|
|
const auto uncompressedLen = static_cast<size_t>(header.uncompressedLen);
|
|
std::vector<uint8_t> data;
|
|
const auto* storedPayload = blob + sizeof(Header);
|
|
if (compression == Compression::None) {
|
|
data.assign(storedPayload, storedPayload + compressedLen);
|
|
} else {
|
|
data.resize(uncompressedLen);
|
|
const size_t decompressedLen =
|
|
ZSTD_decompress(data.data(), data.size(), storedPayload, compressedLen);
|
|
if (ZSTD_isError(decompressedLen)) {
|
|
Log.error("failed to decompress embedded symbol manifest: {}",
|
|
ZSTD_getErrorName(decompressedLen));
|
|
return;
|
|
}
|
|
if (decompressedLen != data.size()) {
|
|
Log.error("embedded symbol manifest decompressed to {} bytes, expected {}",
|
|
decompressedLen, data.size());
|
|
return;
|
|
}
|
|
}
|
|
|
|
const uint64_t entriesEnd = uint64_t{header.entryCount} * sizeof(Entry);
|
|
if (entriesEnd > data.size()) {
|
|
Log.error("decompressed embedded symbol manifest is malformed");
|
|
return;
|
|
}
|
|
|
|
s_state.data = std::move(data);
|
|
s_state.entries = reinterpret_cast<const Entry*>(s_state.data.data());
|
|
s_state.entryCount = header.entryCount;
|
|
s_state.strings = reinterpret_cast<const char*>(s_state.data.data() + entriesEnd);
|
|
s_state.stringsLen = s_state.data.size() - entriesEnd;
|
|
s_state.imageBase = imageBase;
|
|
for (uint32_t i = 0; i < s_state.entryCount; ++i) {
|
|
const Entry& entry = s_state.entries[i];
|
|
if ((entry.flags & kFlagInlineSites) != 0 && entry.nameOff < s_state.stringsLen) {
|
|
s_state.inlineSites.emplace_back(entry.rva, entry.nameOff);
|
|
}
|
|
}
|
|
std::sort(s_state.inlineSites.begin(), s_state.inlineSites.end());
|
|
s_state.inlineSites.erase(std::unique(s_state.inlineSites.begin(), s_state.inlineSites.end(),
|
|
[](const auto& a, const auto& b) { return a.first == b.first; }),
|
|
s_state.inlineSites.end());
|
|
s_state.loaded = true;
|
|
Log.info("symbol manifest loaded: {} symbols, build id {}", s_state.entryCount,
|
|
hex_string(header.buildId, header.buildIdLen));
|
|
}
|
|
|
|
bool available() {
|
|
return s_state.loaded;
|
|
}
|
|
|
|
const std::vector<uint8_t>& image_build_id() {
|
|
static const std::vector<uint8_t> s_id = [] {
|
|
std::vector<uint8_t> id;
|
|
uintptr_t base = 0;
|
|
running_image_identity(id, base);
|
|
return id;
|
|
}();
|
|
return s_id;
|
|
}
|
|
|
|
ResolveStatus resolve(const char* name, void** outAddr, HookSymbolFlags* outFlags) {
|
|
if (!s_state.loaded) {
|
|
return ResolveStatus::Unavailable;
|
|
}
|
|
const uint64_t hash = fnv1a64(name);
|
|
const Entry* begin = s_state.entries;
|
|
const Entry* end = begin + s_state.entryCount;
|
|
size_t lo = 0;
|
|
size_t hi = s_state.entryCount;
|
|
while (lo < hi) {
|
|
const size_t mid = lo + (hi - lo) / 2;
|
|
if (begin[mid].hash < hash) {
|
|
lo = mid + 1;
|
|
} else {
|
|
hi = mid;
|
|
}
|
|
}
|
|
for (const Entry* entry = begin + lo; entry != end && entry->hash == hash; ++entry) {
|
|
if (entry->nameOff >= s_state.stringsLen ||
|
|
std::strcmp(s_state.strings + entry->nameOff, name) != 0)
|
|
{
|
|
continue;
|
|
}
|
|
if ((entry->flags & kFlagDupName) != 0) {
|
|
return ResolveStatus::Ambiguous;
|
|
}
|
|
*outAddr = reinterpret_cast<void*>(s_state.imageBase + entry->rva);
|
|
if (outFlags != nullptr) {
|
|
*outFlags = entry->flags;
|
|
}
|
|
return ResolveStatus::Ok;
|
|
}
|
|
return ResolveStatus::NotFound;
|
|
}
|
|
|
|
bool has_inline_sites(const void* addr, const char** outName) {
|
|
if (!s_state.loaded || s_state.inlineSites.empty()) {
|
|
return false;
|
|
}
|
|
const auto rva = static_cast<uint64_t>(reinterpret_cast<uintptr_t>(addr) - s_state.imageBase);
|
|
const auto it = std::lower_bound(s_state.inlineSites.begin(), s_state.inlineSites.end(),
|
|
std::pair<uint64_t, uint32_t>{rva, 0});
|
|
if (it == s_state.inlineSites.end() || it->first != rva) {
|
|
return false;
|
|
}
|
|
if (outName != nullptr) {
|
|
*outName = s_state.strings + it->second;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace dusk::mods::manifest
|