#ifndef NOMINMAX #define NOMINMAX #endif #include "manifest.hpp" #include #include #include #include #include #include #include #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN #include #elif defined(__APPLE__) #include #include #elif defined(__linux__) #include #include #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 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> 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(*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& outId, uintptr_t& outBase) { #if defined(_WIN32) auto* base = reinterpret_cast(GetModuleHandleW(nullptr)); outBase = reinterpret_cast(base); const auto* dos = reinterpret_cast(base); const auto* nt = reinterpret_cast(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(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(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(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(_dyld_get_image_vmaddr_slide(0)); const auto* header64 = reinterpret_cast(header); const auto* cmd = reinterpret_cast(header64 + 1); for (uint32_t i = 0; i < header64->ncmds; ++i) { if (cmd->cmd == LC_UUID) { const auto* uuidCmd = reinterpret_cast(cmd); outId.assign(uuidCmd->uuid, uuidCmd->uuid + 16); return true; } cmd = reinterpret_cast( reinterpret_cast(cmd) + cmd->cmdsize); } return false; #elif defined(__linux__) struct Ctx { std::vector* id; uintptr_t probe; uintptr_t base = 0; bool found = false; } ctx{&outId, reinterpret_cast(&s_symdbDescriptor)}; dl_iterate_phdr( [](dl_phdr_info* info, size_t, void* data) -> int { auto* ctx = static_cast(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(info->dlpi_addr + phdr.p_vaddr); const auto* end = p + phdr.p_memsz; while (p + sizeof(ElfW(Nhdr)) <= end) { const auto* note = reinterpret_cast(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 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(imageBase + s_symdbDescriptor.rva); const auto blobLen = static_cast(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(header.compression); if ((compression != Compression::None && compression != Compression::Zstd) || header.buildIdLen > sizeof(header.buildId) || header.compressedLen > blobLen - sizeof(Header) || header.uncompressedLen > std::numeric_limits::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(header.compressedLen); const auto uncompressedLen = static_cast(header.uncompressedLen); std::vector 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(s_state.data.data()); s_state.entryCount = header.entryCount; s_state.strings = reinterpret_cast(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& image_build_id() { static const std::vector s_id = [] { std::vector 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(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(reinterpret_cast(addr) - s_state.imageBase); const auto it = std::lower_bound(s_state.inlineSites.begin(), s_state.inlineSites.end(), std::pair{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