mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-24 05:46:51 -04:00
Mod API: Static import/export/hook metadata & resolution
This commit is contained in:
+165
-53
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
@@ -16,6 +17,7 @@
|
||||
#include "dusk/io.hpp"
|
||||
#include "dusk/mods/log_buffer.hpp"
|
||||
#include "dusk/mods/svc/config.hpp"
|
||||
#include "dusk/mods/svc/hook.hpp"
|
||||
#include "dusk/mods/svc/registry.hpp"
|
||||
#include "dusk/ui/mods_window.hpp"
|
||||
#include "dusk/ui/ui.hpp"
|
||||
@@ -220,31 +222,149 @@ static ModMetadata load_metadata(const std::filesystem::path& modPath, ModBundle
|
||||
};
|
||||
}
|
||||
|
||||
static bool validate_manifest(const ModManifest* manifest, LoadedMod& mod) {
|
||||
if (manifest == nullptr) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "returned a null mod manifest");
|
||||
mod.nativeStatus = NativeModStatus::MissingExport;
|
||||
// True if the first `capacity` bytes of `str` contain a NUL.
|
||||
static bool terminated_within(const char* str, size_t capacity) {
|
||||
return std::memchr(str, '\0', capacity) != nullptr;
|
||||
}
|
||||
|
||||
static bool parse_meta(NativeMod& native, LoadedMod& mod) {
|
||||
const ModMeta* meta = native.meta;
|
||||
if (meta->struct_size < sizeof(ModMeta)) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "mod_meta descriptor has invalid size {}",
|
||||
meta->struct_size);
|
||||
mod.nativeStatus = NativeModStatus::InvalidMetadata;
|
||||
return false;
|
||||
}
|
||||
if (manifest->struct_size != sizeof(ModManifest)) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "manifest has invalid size {} (expected {})",
|
||||
manifest->struct_size, sizeof(ModManifest));
|
||||
mod.nativeStatus = NativeModStatus::ApiVersionMismatch;
|
||||
return false;
|
||||
}
|
||||
if (manifest->abi_version != MOD_ABI_VERSION) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expects ABI v{} but engine is v{}, skipping",
|
||||
manifest->abi_version, MOD_ABI_VERSION);
|
||||
mod.nativeStatus = NativeModStatus::ApiVersionMismatch;
|
||||
return false;
|
||||
}
|
||||
if ((manifest->import_count > 0 && manifest->imports == nullptr) ||
|
||||
(manifest->export_count > 0 && manifest->exports == nullptr))
|
||||
const auto* cursor = static_cast<const uint8_t*>(meta->records_begin);
|
||||
const auto* end = static_cast<const uint8_t*>(meta->records_end);
|
||||
if (cursor == nullptr || end == nullptr || cursor > end ||
|
||||
(reinterpret_cast<uintptr_t>(cursor) & 7) != 0)
|
||||
{
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "manifest has invalid import/export arrays");
|
||||
mod.nativeStatus = NativeModStatus::MissingExport;
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "mod_meta section bounds are invalid");
|
||||
mod.nativeStatus = NativeModStatus::InvalidMetadata;
|
||||
return false;
|
||||
}
|
||||
|
||||
ModMetaParsed parsed;
|
||||
size_t headerCount = 0;
|
||||
const auto invalid = [&](std::string_view why) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "invalid metadata record at offset {}: {}",
|
||||
cursor - static_cast<const uint8_t*>(meta->records_begin), why);
|
||||
mod.nativeStatus = NativeModStatus::InvalidMetadata;
|
||||
return false;
|
||||
};
|
||||
|
||||
while (cursor < end) {
|
||||
if (end - cursor < 8) {
|
||||
return invalid("trailing bytes");
|
||||
}
|
||||
uint64_t first = 0;
|
||||
std::memcpy(&first, cursor, sizeof(first));
|
||||
if (first == 0) { // linker padding / bounds sentinel
|
||||
cursor += 8;
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto* rec = reinterpret_cast<const ModMetaRecord*>(cursor);
|
||||
const size_t size = rec->size;
|
||||
if (size < 8 || size % 8 != 0 || size > static_cast<size_t>(end - cursor)) {
|
||||
return invalid("bad record size");
|
||||
}
|
||||
|
||||
switch (rec->kind) {
|
||||
case MOD_META_PAD:
|
||||
break;
|
||||
case MOD_META_HEADER: {
|
||||
if (size < sizeof(ModMetaHeader)) {
|
||||
return invalid("truncated header record");
|
||||
}
|
||||
const auto* header = reinterpret_cast<const ModMetaHeader*>(rec);
|
||||
++headerCount;
|
||||
parsed.abiVersion = header->abi_version;
|
||||
break;
|
||||
}
|
||||
case MOD_META_IMPORT: {
|
||||
if (size < sizeof(ModMetaImport)) {
|
||||
return invalid("truncated import record");
|
||||
}
|
||||
auto* record = reinterpret_cast<ModMetaImport*>(const_cast<uint8_t*>(cursor));
|
||||
if (!terminated_within(record->service_id, sizeof(record->service_id))) {
|
||||
return invalid("unterminated import service id");
|
||||
}
|
||||
parsed.imports.push_back(record);
|
||||
break;
|
||||
}
|
||||
case MOD_META_EXPORT: {
|
||||
if (size < sizeof(ModMetaExport)) {
|
||||
return invalid("truncated export record");
|
||||
}
|
||||
auto* record = reinterpret_cast<ModMetaExport*>(const_cast<uint8_t*>(cursor));
|
||||
if (!terminated_within(record->service_id, sizeof(record->service_id))) {
|
||||
return invalid("unterminated export service id");
|
||||
}
|
||||
parsed.exports.push_back(record);
|
||||
break;
|
||||
}
|
||||
case MOD_META_HOOK_FN: {
|
||||
if (size < sizeof(ModMetaHookFn)) {
|
||||
return invalid("truncated hook record");
|
||||
}
|
||||
parsed.hookFns.push_back(
|
||||
reinterpret_cast<ModMetaHookFn*>(const_cast<uint8_t*>(cursor)));
|
||||
break;
|
||||
}
|
||||
case MOD_META_HOOK_MEM: {
|
||||
if (size <= sizeof(ModMetaHookMem)) {
|
||||
return invalid("truncated hook record");
|
||||
}
|
||||
auto* record = reinterpret_cast<ModMetaHookMem*>(const_cast<uint8_t*>(cursor));
|
||||
const char* strings = reinterpret_cast<const char*>(cursor) + sizeof(ModMetaHookMem);
|
||||
const size_t capacity = size - sizeof(ModMetaHookMem);
|
||||
if (!terminated_within(strings, capacity)) {
|
||||
return invalid("unterminated hook vtable symbol");
|
||||
}
|
||||
const size_t vtableLen = std::char_traits<char>::length(strings);
|
||||
if (!terminated_within(strings + vtableLen + 1, capacity - vtableLen - 1)) {
|
||||
return invalid("unterminated hook display name");
|
||||
}
|
||||
parsed.hookMems.push_back(record);
|
||||
break;
|
||||
}
|
||||
case MOD_META_HOOK_NAME: {
|
||||
if (size <= sizeof(ModMetaHookName)) {
|
||||
return invalid("truncated hook record");
|
||||
}
|
||||
auto* record = reinterpret_cast<ModMetaHookName*>(const_cast<uint8_t*>(cursor));
|
||||
const char* name = reinterpret_cast<const char*>(cursor) + sizeof(ModMetaHookName);
|
||||
if (!terminated_within(name, size - sizeof(ModMetaHookName))) {
|
||||
return invalid("unterminated hook symbol name");
|
||||
}
|
||||
parsed.hookNames.push_back(record);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// Additive record kinds may appear within a format version; skip them.
|
||||
log::write(mod.metadata.id, LOG_LEVEL_DEBUG, "skipping unknown metadata record kind {}",
|
||||
rec->kind);
|
||||
break;
|
||||
}
|
||||
cursor += size;
|
||||
}
|
||||
|
||||
if (headerCount != 1) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expected 1 metadata header record, found {}",
|
||||
headerCount);
|
||||
mod.nativeStatus = NativeModStatus::InvalidMetadata;
|
||||
return false;
|
||||
}
|
||||
if (parsed.abiVersion != MOD_ABI_VERSION) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expects ABI v{} but engine is v{}, skipping",
|
||||
parsed.abiVersion, MOD_ABI_VERSION);
|
||||
mod.nativeStatus = NativeModStatus::ApiVersionMismatch;
|
||||
return false;
|
||||
}
|
||||
|
||||
native.parsed = std::move(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -276,6 +396,8 @@ static std::string native_status_message(const NativeModStatus status) {
|
||||
return "Mod ABI version mismatch";
|
||||
case NativeModStatus::MissingExport:
|
||||
return "Missing required mod API exports";
|
||||
case NativeModStatus::InvalidMetadata:
|
||||
return "Invalid mod metadata records";
|
||||
case NativeModStatus::Unknown:
|
||||
return "Unknown mod load failure";
|
||||
case NativeModStatus::None:
|
||||
@@ -375,13 +497,13 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto getManifest = nativeMod->handle->LookupSymbol<ModGetManifestFn>("mod_get_manifest");
|
||||
nativeMod->meta = nativeMod->handle->LookupSymbol<const ModMeta*>("mod_meta");
|
||||
nativeMod->contextSymbol = nativeMod->handle->LookupSymbol<ModContext**>("mod_ctx");
|
||||
nativeMod->fn_initialize = nativeMod->handle->LookupSymbol<ModInitializeFn>("mod_initialize");
|
||||
nativeMod->fn_update = nativeMod->handle->LookupSymbol<ModUpdateFn>("mod_update");
|
||||
nativeMod->fn_shutdown = nativeMod->handle->LookupSymbol<ModShutdownFn>("mod_shutdown");
|
||||
|
||||
if (!getManifest || !nativeMod->contextSymbol || !nativeMod->fn_initialize ||
|
||||
if (!nativeMod->meta || !nativeMod->contextSymbol || !nativeMod->fn_initialize ||
|
||||
!nativeMod->fn_update || !nativeMod->fn_shutdown)
|
||||
{
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR,
|
||||
@@ -391,8 +513,7 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) {
|
||||
return;
|
||||
}
|
||||
|
||||
nativeMod->manifest = getManifest();
|
||||
if (!validate_manifest(nativeMod->manifest, mod)) {
|
||||
if (!parse_meta(*nativeMod, mod)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -427,28 +548,22 @@ void ModLoader::drain_retired_natives() {
|
||||
m_retiredNatives.clear();
|
||||
}
|
||||
|
||||
static ModManifestInfo build_manifest_info(const ModManifest& manifest) {
|
||||
static ModManifestInfo build_manifest_info(const ModMetaParsed& parsed) {
|
||||
ModManifestInfo info;
|
||||
info.imports.reserve(manifest.import_count);
|
||||
for (size_t i = 0; i < manifest.import_count; ++i) {
|
||||
const auto& serviceImport = manifest.imports[i];
|
||||
if (serviceImport.struct_size != sizeof(ServiceImport) ||
|
||||
!svc::valid_service_id(serviceImport.service_id))
|
||||
{
|
||||
info.imports.reserve(parsed.imports.size());
|
||||
for (const auto* record : parsed.imports) {
|
||||
if (!svc::valid_service_id(record->service_id)) {
|
||||
continue;
|
||||
}
|
||||
info.imports.push_back({serviceImport.service_id, serviceImport.major_version,
|
||||
(serviceImport.flags & SERVICE_IMPORT_OPTIONAL) == 0});
|
||||
info.imports.push_back({record->service_id, record->major_version,
|
||||
(record->rec.flags & SERVICE_IMPORT_OPTIONAL) == 0});
|
||||
}
|
||||
info.exports.reserve(manifest.export_count);
|
||||
for (size_t i = 0; i < manifest.export_count; ++i) {
|
||||
const auto& serviceExport = manifest.exports[i];
|
||||
if (serviceExport.struct_size != sizeof(ServiceExport) ||
|
||||
!svc::valid_service_id(serviceExport.service_id))
|
||||
{
|
||||
info.exports.reserve(parsed.exports.size());
|
||||
for (const auto* record : parsed.exports) {
|
||||
if (!svc::valid_service_id(record->service_id)) {
|
||||
continue;
|
||||
}
|
||||
info.exports.push_back({serviceExport.service_id, serviceExport.major_version});
|
||||
info.exports.push_back({record->service_id, record->major_version});
|
||||
}
|
||||
return info;
|
||||
}
|
||||
@@ -484,24 +599,20 @@ static bool required_deps_active(const LoadedMod& mod) {
|
||||
// A deferred export that was not published by the end of the provider's initialization can
|
||||
// never resolve, which is almost certainly a bug in the provider.
|
||||
static void warn_unpublished_deferred_exports(const LoadedMod& mod) {
|
||||
if (!mod.active || !mod.native || mod.native->manifest == nullptr) {
|
||||
if (!mod.active || !mod.native) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& manifest = *mod.native->manifest;
|
||||
for (size_t i = 0; i < manifest.export_count; ++i) {
|
||||
const auto& serviceExport = manifest.exports[i];
|
||||
if (serviceExport.struct_size != sizeof(ServiceExport) ||
|
||||
(serviceExport.flags & SERVICE_EXPORT_DEFERRED) == 0)
|
||||
{
|
||||
for (const auto* serviceExport : mod.native->parsed.exports) {
|
||||
if ((serviceExport->rec.flags & SERVICE_EXPORT_DEFERRED) == 0) {
|
||||
continue;
|
||||
}
|
||||
const auto* record =
|
||||
svc::find_service_record(serviceExport.service_id, serviceExport.major_version);
|
||||
svc::find_service_record(serviceExport->service_id, serviceExport->major_version);
|
||||
if (record != nullptr && record->service == nullptr) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_WARN,
|
||||
"declared deferred service '{}@{}' but never published it during initialization",
|
||||
serviceExport.service_id, serviceExport.major_version);
|
||||
serviceExport->service_id, serviceExport->major_version);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -560,7 +671,7 @@ void ModLoader::try_load_mod(
|
||||
Log.error("Native mod '{}' failed to load, disabling", mod.metadata.id);
|
||||
fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus));
|
||||
} else {
|
||||
mod.manifestInfo = build_manifest_info(*mod.native->manifest);
|
||||
mod.manifestInfo = build_manifest_info(mod.native->parsed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,6 +704,8 @@ bool ModLoader::activate_mod(LoadedMod& mod) {
|
||||
return false;
|
||||
}
|
||||
|
||||
svc::hook_resolve_mod_records(mod);
|
||||
|
||||
*mod.native->contextSymbol = mod.context.get();
|
||||
|
||||
log::write(mod.metadata.id, LOG_LEVEL_TRACE, "calling mod_initialize");
|
||||
@@ -929,7 +1042,7 @@ bool ModLoader::reload_bundle(LoadedMod& mod) {
|
||||
fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus));
|
||||
return false;
|
||||
}
|
||||
newInfo = build_manifest_info(*mod.native->manifest);
|
||||
newInfo = build_manifest_info(mod.native->parsed);
|
||||
} else {
|
||||
mod.nativeStatus = NativeModStatus::None;
|
||||
++mod.cacheGeneration;
|
||||
@@ -998,8 +1111,7 @@ void ModLoader::apply_lifecycle_change(LoadedMod& target, const bool reload) {
|
||||
if (register_static_service_exports(*mod)) {
|
||||
mod->servicesRegistered = true;
|
||||
} else {
|
||||
log::write(
|
||||
mod->metadata.id, LOG_LEVEL_ERROR, "failed to register service exports");
|
||||
log::write(mod->metadata.id, LOG_LEVEL_ERROR, "failed to register service exports");
|
||||
deactivate_mod(*mod);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#if DUSK_CODE_MODS
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/mods/log_buffer.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -13,6 +14,7 @@
|
||||
#include <exception>
|
||||
#include <fmt/format.h>
|
||||
#include <funchook.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#endif
|
||||
@@ -64,8 +66,29 @@ struct InstalledHook {
|
||||
|
||||
std::unordered_map<uintptr_t, HookSlot> s_registry;
|
||||
std::unordered_map<uintptr_t, InstalledHook> s_installed;
|
||||
std::unordered_map<const ModContext*, std::vector<uintptr_t>> s_declaredTargets;
|
||||
uint64_t s_nextOrder = 0;
|
||||
|
||||
bool declared_target(const ModContext* context, void* fnAddr) {
|
||||
if (context == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const auto it = s_declaredTargets.find(context);
|
||||
if (it == s_declaredTargets.end()) {
|
||||
return false;
|
||||
}
|
||||
const auto addr = reinterpret_cast<uintptr_t>(fnAddr);
|
||||
return std::ranges::find(it->second, addr) != it->second.end();
|
||||
}
|
||||
|
||||
ModResult reject_undeclared(ModContext* context, void* fnAddr) {
|
||||
log::write(mod_id_from_context(context), LOG_LEVEL_ERROR,
|
||||
"tried to hook undeclared target {:p}; hook targets must be declared with "
|
||||
"DEFINE_HOOK/DEFINE_HOOK_SYMBOL",
|
||||
fnAddr);
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
HookOptions normalize_options(const HookOptions* options) {
|
||||
if (options == nullptr || options->struct_size < sizeof(HookOptions)) {
|
||||
return HOOK_OPTIONS_INIT;
|
||||
@@ -200,6 +223,9 @@ ModResult hook_install(ModContext* context, void* fnAddr, void* trampolineFn, vo
|
||||
}
|
||||
|
||||
fnAddr = resolve_import_thunk(fnAddr);
|
||||
if (!declared_target(context, fnAddr)) {
|
||||
return reject_undeclared(context, fnAddr);
|
||||
}
|
||||
const auto key = reinterpret_cast<uintptr_t>(fnAddr);
|
||||
if (const auto it = s_installed.find(key); it != s_installed.end()) {
|
||||
auto& entry = it->second;
|
||||
@@ -243,6 +269,9 @@ ModResult hook_add_pre(
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
fnAddr = resolve_import_thunk(fnAddr);
|
||||
if (!declared_target(context, fnAddr)) {
|
||||
return reject_undeclared(context, fnAddr);
|
||||
}
|
||||
auto& hooks = s_registry[reinterpret_cast<uintptr_t>(fnAddr)].pre;
|
||||
hooks.push_back({context, callback, normalize_options(options), s_nextOrder++});
|
||||
sort_hooks(hooks);
|
||||
@@ -255,6 +284,9 @@ ModResult hook_add_post(
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
fnAddr = resolve_import_thunk(fnAddr);
|
||||
if (!declared_target(context, fnAddr)) {
|
||||
return reject_undeclared(context, fnAddr);
|
||||
}
|
||||
auto& hooks = s_registry[reinterpret_cast<uintptr_t>(fnAddr)].post;
|
||||
hooks.push_back({context, nullptr, callback, normalize_options(options), s_nextOrder++});
|
||||
sort_hooks(hooks);
|
||||
@@ -269,6 +301,9 @@ ModResult hook_replace(
|
||||
|
||||
const HookOptions normalized = normalize_options(options);
|
||||
fnAddr = resolve_import_thunk(fnAddr);
|
||||
if (!declared_target(context, fnAddr)) {
|
||||
return reject_undeclared(context, fnAddr);
|
||||
}
|
||||
auto& slot = s_registry[reinterpret_cast<uintptr_t>(fnAddr)];
|
||||
if (slot.replace.replaceCallback == nullptr) {
|
||||
slot.replace = {context, callback, nullptr, normalized, s_nextOrder++};
|
||||
@@ -371,8 +406,187 @@ ModResult hook_dispatch_post(ModContext*, void* fnAddr, void* args, void* retval
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
/* Follow jump stubs, then match the MSVC vcall thunk a virtual mfp points at.
|
||||
* Returns the vtable slot's byte offset, or npos when fn is not a vcall thunk. */
|
||||
size_t vcall_slot_offset(const void*& fn) noexcept {
|
||||
constexpr size_t npos = static_cast<size_t>(-1);
|
||||
#if defined(_M_X64) || defined(__x86_64__)
|
||||
const auto* p = static_cast<const uint8_t*>(fn);
|
||||
for (int i = 0; i < 8 && p[0] == 0xE9; ++i) { // incremental-link stubs
|
||||
int32_t rel;
|
||||
std::memcpy(&rel, p + 1, 4);
|
||||
p += 5 + rel;
|
||||
}
|
||||
fn = p;
|
||||
// The vptr load. Unoptimized clang-cl thunks spill/reload rcx first
|
||||
// (push rax; mov [rsp], rcx; mov rcx, [rsp]), so scan a short window.
|
||||
const uint8_t* q = nullptr;
|
||||
for (int i = 0; i <= 12; ++i) {
|
||||
if (p[i] == 0x48 && p[i + 1] == 0x8B && p[i + 2] == 0x01) { // mov rax, [rcx]
|
||||
q = p + i + 3;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (q == nullptr) {
|
||||
return npos;
|
||||
}
|
||||
if (q[0] == 0xFF && q[1] == 0x20) { // jmp [rax] (MSVC)
|
||||
return 0;
|
||||
}
|
||||
if (q[0] == 0xFF && q[1] == 0x60) { // jmp [rax + imm8]
|
||||
return static_cast<int8_t>(q[2]);
|
||||
}
|
||||
if (q[0] == 0xFF && q[1] == 0xA0) { // jmp [rax + imm32]
|
||||
int32_t off;
|
||||
std::memcpy(&off, q + 2, 4);
|
||||
return off;
|
||||
}
|
||||
// clang-cl: mov rax, [rax + off]; (pop r10;) jmp rax. Requiring the jmp rax
|
||||
// distinguishes the thunk from an ordinary getter that begins the same way.
|
||||
if (q[0] == 0x48 && q[1] == 0x8B && (q[2] == 0x00 || q[2] == 0x40 || q[2] == 0x80)) {
|
||||
size_t off = 0;
|
||||
const uint8_t* r = q + 3;
|
||||
if (q[2] == 0x40) {
|
||||
off = static_cast<int8_t>(q[3]);
|
||||
r = q + 4;
|
||||
} else if (q[2] == 0x80) {
|
||||
int32_t off32;
|
||||
std::memcpy(&off32, q + 3, 4);
|
||||
off = off32;
|
||||
r = q + 7;
|
||||
}
|
||||
for (int i = 0; i <= 8; ++i) {
|
||||
if (r[i] == 0xFF && r[i + 1] == 0xE0) { // jmp rax (48 REX optional)
|
||||
return off;
|
||||
}
|
||||
}
|
||||
}
|
||||
return npos;
|
||||
#elif defined(_M_ARM64) || defined(__aarch64__)
|
||||
const auto* p = static_cast<const uint8_t*>(fn);
|
||||
uint32_t insn[3];
|
||||
for (int i = 0; i < 8; ++i) { // incremental-link `b` stubs
|
||||
std::memcpy(insn, p, 4);
|
||||
if ((insn[0] & 0xFC000000u) != 0x14000000u) {
|
||||
break;
|
||||
}
|
||||
const auto imm26 = static_cast<int32_t>(insn[0] << 6) >> 6;
|
||||
p += static_cast<intptr_t>(imm26) * 4;
|
||||
}
|
||||
fn = p;
|
||||
std::memcpy(insn, p, 12);
|
||||
// ldr Xt, [x0]; ldr Xs, [Xt, #imm12*8]; br Xs
|
||||
if ((insn[0] & 0xFFFFFFE0u) != 0xF9400000u) {
|
||||
return npos;
|
||||
}
|
||||
const uint32_t t = insn[0] & 0x1Fu;
|
||||
if ((insn[1] & 0xFFC003E0u) != (0xF9400000u | (t << 5))) {
|
||||
return npos;
|
||||
}
|
||||
const uint32_t s = insn[1] & 0x1Fu;
|
||||
if (insn[2] != (0xD61F0000u | (s << 5))) {
|
||||
return npos;
|
||||
}
|
||||
return ((insn[1] >> 10) & 0xFFFu) * 8;
|
||||
#else
|
||||
(void)fn;
|
||||
return npos;
|
||||
#endif
|
||||
}
|
||||
#endif // _WIN32
|
||||
|
||||
bool resolve_symbol_checked(const char* symbol, bool requireCode, void** out, std::string& why) {
|
||||
HookSymbolFlags flags{};
|
||||
switch (manifest::resolve(symbol, out, &flags)) {
|
||||
case manifest::ResolveStatus::Ok:
|
||||
if (requireCode && (flags & HOOK_SYMBOL_CODE) == 0) {
|
||||
why = fmt::format("'{}' is not a code symbol", symbol);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
case manifest::ResolveStatus::Unavailable:
|
||||
why = "no symbol manifest for this build";
|
||||
return false;
|
||||
case manifest::ResolveStatus::NotFound:
|
||||
why = fmt::format("symbol '{}' not found", symbol);
|
||||
return false;
|
||||
case manifest::ResolveStatus::Ambiguous:
|
||||
why = fmt::format("'{}' maps to more than one address; use the mangled name", symbol);
|
||||
return false;
|
||||
}
|
||||
why = "unexpected resolve failure";
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Decode a HOOK_MEM record's pointer-to-member representation into the target code address,
|
||||
* mirroring what calling through the mfp would invoke. Virtual members hook the class's own
|
||||
* overrider, read from its vtable (resolved from the symbol manifest). */
|
||||
void* resolve_member_record(
|
||||
const ModMetaHookMem& record, const char* vtableSymbol, std::string& why) {
|
||||
uintptr_t words[2];
|
||||
std::memcpy(words, record.pmf, sizeof(words));
|
||||
|
||||
#if defined(_WIN32)
|
||||
const void* fn = reinterpret_cast<const void*>(words[0]);
|
||||
const size_t slot = vcall_slot_offset(fn);
|
||||
if (slot == static_cast<size_t>(-1)) { // not a vcall thunk: direct address
|
||||
return const_cast<void*>(fn);
|
||||
}
|
||||
if (vtableSymbol[0] == '\0') {
|
||||
why = "class name is not representable as a vtable symbol";
|
||||
return nullptr;
|
||||
}
|
||||
void* vtable = nullptr;
|
||||
if (!resolve_symbol_checked(vtableSymbol, false, &vtable, why)) {
|
||||
return nullptr;
|
||||
}
|
||||
// ??_7 points at the first slot.
|
||||
return *reinterpret_cast<void**>(static_cast<char*>(vtable) + slot);
|
||||
#else
|
||||
#if defined(__aarch64__) || defined(__arm__)
|
||||
// AAPCS C++ ABI: the virtual flag is bit 0 of the adjustment word (function
|
||||
// addresses can't spare their low bit), and ptr holds the slot offset directly.
|
||||
const bool isVirtual = (words[1] & 1) != 0;
|
||||
const uintptr_t thisAdjust = words[1] >> 1;
|
||||
const uintptr_t slotOffset = words[0];
|
||||
#else
|
||||
// Itanium C++ ABI: virtual mfps set bit 0 of ptr; the slot offset is ptr - 1.
|
||||
const bool isVirtual = (words[0] & 1) != 0;
|
||||
const uintptr_t thisAdjust = words[1];
|
||||
const uintptr_t slotOffset = words[0] - 1;
|
||||
#endif
|
||||
if (!isVirtual) { // non-virtual: the address itself
|
||||
return reinterpret_cast<void*>(words[0]);
|
||||
}
|
||||
if (thisAdjust != 0) {
|
||||
// this-adjusting mfp (member of a secondary base): the slot offset is relative to a
|
||||
// vtable we can't locate. Hook the overrider by name instead.
|
||||
why = "virtual member of a secondary base; hook the overrider by name";
|
||||
return nullptr;
|
||||
}
|
||||
if (vtableSymbol[0] == '\0') {
|
||||
why = "class name is not representable as a vtable symbol";
|
||||
return nullptr;
|
||||
}
|
||||
void* vtable = nullptr;
|
||||
if (!resolve_symbol_checked(vtableSymbol, false, &vtable, why)) {
|
||||
return nullptr;
|
||||
}
|
||||
// _ZTV points at the offset-to-top slot; the address point mfps index from is
|
||||
// two pointers in (past offset-to-top and the typeinfo pointer).
|
||||
void* target =
|
||||
*reinterpret_cast<void**>(static_cast<char*>(vtable) + 2 * sizeof(void*) + slotOffset);
|
||||
if (target == nullptr) {
|
||||
why = "vtable slot is empty";
|
||||
}
|
||||
return target;
|
||||
#endif
|
||||
}
|
||||
|
||||
void hook_remove_mod(LoadedMod& mod) {
|
||||
ModContext* context = mod.context.get();
|
||||
s_declaredTargets.erase(context);
|
||||
|
||||
for (auto it = s_registry.begin(); it != s_registry.end();) {
|
||||
auto& slot = it->second;
|
||||
@@ -508,6 +722,56 @@ constexpr HookService s_hookService{
|
||||
|
||||
} // namespace
|
||||
|
||||
#if DUSK_CODE_MODS
|
||||
void hook_resolve_mod_records(LoadedMod& mod) {
|
||||
auto& declared = s_declaredTargets[mod.context.get()];
|
||||
declared.clear();
|
||||
if (!mod.native) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto resolved = [&](void* target, void** slot) {
|
||||
target = resolve_import_thunk(target);
|
||||
*slot = target;
|
||||
declared.push_back(reinterpret_cast<uintptr_t>(target));
|
||||
};
|
||||
const auto unresolved = [&](const char* what, std::string_view why, void** slot) {
|
||||
*slot = nullptr;
|
||||
log::write(mod.metadata.id, LOG_LEVEL_WARN,
|
||||
"hook target '{}' did not resolve ({}); installing this hook will fail", what, why);
|
||||
};
|
||||
|
||||
for (auto* record : mod.native->parsed.hookFns) {
|
||||
if (record->target != nullptr) {
|
||||
resolved(record->target, &record->resolved);
|
||||
} else {
|
||||
unresolved("<fn>", "null link-time target", &record->resolved);
|
||||
}
|
||||
}
|
||||
for (auto* record : mod.native->parsed.hookMems) {
|
||||
std::string why;
|
||||
void* target = resolve_member_record(*record, hook_mem_vtable_symbol(*record), why);
|
||||
if (target != nullptr) {
|
||||
resolved(target, &record->resolved);
|
||||
} else {
|
||||
unresolved(hook_mem_display_name(*record), why, &record->resolved);
|
||||
}
|
||||
}
|
||||
for (auto* record : mod.native->parsed.hookNames) {
|
||||
const char* name = hook_name_symbol(*record);
|
||||
std::string why;
|
||||
void* target = nullptr;
|
||||
if (resolve_symbol_checked(name, true, &target, why)) {
|
||||
resolved(target, &record->resolved);
|
||||
} else {
|
||||
unresolved(name, why, &record->resolved);
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
void hook_resolve_mod_records(LoadedMod&) {}
|
||||
#endif // DUSK_CODE_MODS
|
||||
|
||||
constinit const ServiceModule g_hookModule{
|
||||
.id = HOOK_SERVICE_ID,
|
||||
.majorVersion = HOOK_SERVICE_MAJOR,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk::mods {
|
||||
struct LoadedMod;
|
||||
}
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
|
||||
void hook_resolve_mod_records(LoadedMod& mod);
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -217,29 +217,25 @@ void ModLoader::init_services() {
|
||||
}
|
||||
|
||||
bool ModLoader::register_static_service_exports(LoadedMod& mod) {
|
||||
if (!mod.native || mod.native->manifest == nullptr) {
|
||||
if (!mod.native) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto& manifest = *mod.native->manifest;
|
||||
for (size_t i = 0; i < manifest.export_count; ++i) {
|
||||
const auto& serviceExport = manifest.exports[i];
|
||||
if (serviceExport.struct_size != sizeof(ServiceExport) ||
|
||||
!svc::valid_service_id(serviceExport.service_id))
|
||||
{
|
||||
for (const auto* serviceExport : mod.native->parsed.exports) {
|
||||
if (!svc::valid_service_id(serviceExport->service_id)) {
|
||||
fail_mod(mod, MOD_INVALID_ARGUMENT, "Invalid service export descriptor");
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool deferred = (serviceExport.flags & SERVICE_EXPORT_DEFERRED) != 0;
|
||||
if (!deferred && serviceExport.service == nullptr) {
|
||||
const bool deferred = (serviceExport->rec.flags & SERVICE_EXPORT_DEFERRED) != 0;
|
||||
if (!deferred && serviceExport->service == nullptr) {
|
||||
fail_mod(mod, MOD_INVALID_ARGUMENT, "Static service export has null service pointer");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto result =
|
||||
svc::register_service(serviceExport.service_id, serviceExport.major_version,
|
||||
serviceExport.minor_version, serviceExport.service, &mod, deferred);
|
||||
svc::register_service(serviceExport->service_id, serviceExport->major_version,
|
||||
serviceExport->minor_version, serviceExport->service, &mod, deferred);
|
||||
if (result != MOD_OK) {
|
||||
fail_mod(mod, result, "Service export registration failed");
|
||||
return false;
|
||||
@@ -262,18 +258,13 @@ std::string ModLoader::describe_missing_import(
|
||||
|
||||
// No record can also mean the provider failed or is disabled and its services were removed.
|
||||
for (const auto& other : mods()) {
|
||||
if ((other.active && !other.loadFailed) || !other.native ||
|
||||
other.native->manifest == nullptr)
|
||||
{
|
||||
if ((other.active && !other.loadFailed) || !other.native) {
|
||||
continue;
|
||||
}
|
||||
const auto& manifest = *other.native->manifest;
|
||||
for (size_t i = 0; i < manifest.export_count; ++i) {
|
||||
const auto& serviceExport = manifest.exports[i];
|
||||
if (serviceExport.struct_size == sizeof(ServiceExport) &&
|
||||
svc::valid_service_id(serviceExport.service_id) &&
|
||||
std::string_view{serviceExport.service_id} == serviceId &&
|
||||
serviceExport.major_version == majorVersion)
|
||||
for (const auto* serviceExport : other.native->parsed.exports) {
|
||||
if (svc::valid_service_id(serviceExport->service_id) &&
|
||||
std::string_view{serviceExport->service_id} == serviceId &&
|
||||
serviceExport->major_version == majorVersion)
|
||||
{
|
||||
return fmt::format("Required service {}@{} unavailable: provider '{}' {}",
|
||||
serviceId, majorVersion, other.metadata.id,
|
||||
@@ -286,35 +277,31 @@ std::string ModLoader::describe_missing_import(
|
||||
}
|
||||
|
||||
bool ModLoader::resolve_service_imports(LoadedMod& mod) {
|
||||
if (!mod.native || mod.native->manifest == nullptr) {
|
||||
if (!mod.native) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto& manifest = *mod.native->manifest;
|
||||
for (size_t i = 0; i < manifest.import_count; ++i) {
|
||||
const auto& serviceImport = manifest.imports[i];
|
||||
if (serviceImport.struct_size != sizeof(ServiceImport) ||
|
||||
!svc::valid_service_id(serviceImport.service_id) || serviceImport.slot == nullptr)
|
||||
{
|
||||
for (const auto* serviceImport : mod.native->parsed.imports) {
|
||||
if (!svc::valid_service_id(serviceImport->service_id) || serviceImport->slot == nullptr) {
|
||||
fail_mod(mod, MOD_INVALID_ARGUMENT, "Invalid service import descriptor");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto* service = svc::find_service(
|
||||
serviceImport.service_id, serviceImport.major_version, serviceImport.min_minor_version);
|
||||
const auto* service = svc::find_service(serviceImport->service_id,
|
||||
serviceImport->major_version, serviceImport->min_minor_version);
|
||||
if (service == nullptr) {
|
||||
*static_cast<const void**>(serviceImport.slot) = nullptr;
|
||||
if ((serviceImport.flags & SERVICE_IMPORT_OPTIONAL) != 0) {
|
||||
*static_cast<const void**>(serviceImport->slot) = nullptr;
|
||||
if ((serviceImport->rec.flags & SERVICE_IMPORT_OPTIONAL) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fail_mod(mod, MOD_UNAVAILABLE,
|
||||
describe_missing_import(serviceImport.service_id, serviceImport.major_version,
|
||||
serviceImport.min_minor_version));
|
||||
describe_missing_import(serviceImport->service_id, serviceImport->major_version,
|
||||
serviceImport->min_minor_version));
|
||||
return false;
|
||||
}
|
||||
|
||||
*static_cast<const void**>(serviceImport.slot) = service->service;
|
||||
*static_cast<const void**>(serviceImport->slot) = service->service;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user