#include "registry.hpp" #include "dusk/mods/loader/loader.hpp" #if DUSK_HAS_PREPATCH #include "dusk/mods/loader/prepatch.hpp" #endif #include "dusk/mods/manifest.hpp" #include "mods/svc/hook.h" #if DUSK_CODE_MODS #include "dusk/logging.h" #include "dusk/mods/log_buffer.hpp" #include #include #include #include #include #include #if DUSK_HAS_FUNCHOOK #include #endif #include #include #include #endif namespace dusk::mods::svc { namespace { #if DUSK_CODE_MODS struct PreHookFn { ModContext* context = nullptr; HookPreFn callback = nullptr; HookOptions options = HOOK_OPTIONS_INIT; uint64_t order = 0; }; struct VoidHookFn { ModContext* context = nullptr; HookReplaceFn replaceCallback = nullptr; HookPostFn postCallback = nullptr; HookOptions options = HOOK_OPTIONS_INIT; uint64_t order = 0; }; struct HookSlot { std::vector pre; VoidHookFn replace{}; std::vector post; }; // One per mod that requested a hook on a target: its template-generated trampoline and the // address of its Hook::g_orig, both living in the mod's dylib. Any candidate's trampoline // is interchangeable (dispatch walks the shared HookSlot), so when the active installer's mod // unloads, the backend is handed off to a surviving candidate. struct HookCandidate { ModContext* context = nullptr; void* trampoline = nullptr; void** origStore = nullptr; uint64_t order = 0; }; enum class BackendKind { None, #if DUSK_HAS_FUNCHOOK Funchook, #endif #if DUSK_HAS_PREPATCH Prepatch, #endif }; struct InstalledBackend { BackendKind kind = BackendKind::None; #if DUSK_HAS_FUNCHOOK funchook_t* handle = nullptr; #endif #if DUSK_HAS_PREPATCH prepatch::Site prepatchSite{}; #endif }; struct InstalledHook { InstalledBackend backend{}; void* original = nullptr; ModContext* active = nullptr; std::vector candidates; }; std::unordered_map s_registry; std::unordered_map s_installed; std::unordered_map> 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(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; } return *options; } void fail_hook_callback(ModContext* context, const char* kind, const std::exception& e) { if (auto* mod = mod_from_context(context)) { fail_mod(*mod, MOD_ERROR, fmt::format("Exception in {} hook callback: {}", kind, e.what())); } } void fail_unknown_hook_callback(ModContext* context, const char* kind) { if (auto* mod = mod_from_context(context)) { fail_mod(*mod, MOD_ERROR, fmt::format("Unknown exception in {} hook callback", kind)); } } template void sort_hooks(std::vector& hooks) { std::ranges::stable_sort(hooks, [](const T& a, const T& b) { if (a.options.priority != b.options.priority) { return a.options.priority > b.options.priority; } return a.order < b.order; }); } // Once a hook is installed, funchook has patched the target's entry: its bytes lead to the // detour, not the function, so canonicalization must stop there. [[maybe_unused]] bool installed_target(void* addr) { return s_installed.contains(reinterpret_cast(addr)); } // Follow E9/FF25 chains to skip MSVC incremental-link and import stubs. void* resolve_import_thunk(void* addr) { #if defined(_WIN32) && (defined(_M_X64) || defined(__x86_64__)) for (int i = 0; i < 8; ++i) { if (installed_target(addr)) { break; } const auto* p = static_cast(addr); if (p[0] == 0x48 && p[1] == 0xFF && p[2] == 0x25) { // lld emits a REX.W prefix ++p; } if (p[0] == 0xFF && p[1] == 0x25) { int32_t offset; std::memcpy(&offset, p + 2, 4); addr = const_cast(*reinterpret_cast(p + 6 + offset)); break; } if (p[0] == 0xE9) { int32_t offset; std::memcpy(&offset, p + 1, 4); addr = const_cast(p) + 5 + offset; } else { break; } } #elif defined(_WIN32) && (defined(_M_ARM64) || defined(__aarch64__)) // Import thunks are `adrp x16; ldr x16, [x16, #off]; br x16` (deref the IAT slot); // incremental-link stubs are a plain `b`, or `adrp x16; add x16, x16, #off; br x16` // range-extension thunks when the target is out of B range. for (int i = 0; i < 8; ++i) { if (installed_target(addr)) { break; } const auto* p = static_cast(addr); uint32_t insn0, insn1, insn2; std::memcpy(&insn0, p, 4); if ((insn0 & 0xFC000000u) == 0x14000000u) { // b imm26 auto imm26 = static_cast(insn0 << 6) >> 6; addr = const_cast(p) + static_cast(imm26) * 4; continue; } if ((insn0 & 0x9F00001Fu) != 0x90000010u) { // adrp x16, page break; } std::memcpy(&insn1, p + 4, 4); std::memcpy(&insn2, p + 8, 4); if (insn2 != 0xD61F0200u) { // br x16 break; } auto immhi = static_cast(static_cast(insn0 << 8) >> 13); // bits 23:5 auto immlo = static_cast((insn0 >> 29) & 3); auto page = (reinterpret_cast(p) & ~uintptr_t{0xFFF}) + (static_cast((immhi << 2) | immlo) << 12); if ((insn1 & 0xFFC003FFu) == 0xF9400210u) { // ldr x16, [x16, #imm12*8] auto slot = page + ((insn1 >> 10) & 0xFFF) * 8; addr = *reinterpret_cast(slot); break; } if ((insn1 & 0xFF8003FFu) == 0x91000210u) { // add x16, x16, #imm12{, lsl #12} auto imm = static_cast((insn1 >> 10) & 0xFFF); addr = reinterpret_cast(page + (((insn1 >> 22) & 1) != 0 ? imm << 12 : imm)); continue; } break; } #endif return addr; } // Resolve thunks recursively (max of 8 steps) until we find our target. void* resolve_target(void* addr) { for (int i = 0; i < 8; ++i) { void* next = resolve_import_thunk(addr); if (next == addr) { break; } addr = next; } return addr; } #if DUSK_HAS_FUNCHOOK funchook_t* install_funchook(void* fnAddr, void* trampoline, void** outOriginal) { funchook_t* fh = funchook_create(); if (fh == nullptr) { DuskLog.warn("HookSystem: funchook_create failed for {:p}", fnAddr); return nullptr; } void* fn = fnAddr; const int prep = funchook_prepare(fh, &fn, trampoline); if (prep == 0) { *outOriginal = fn; } const int inst = prep == 0 ? funchook_install(fh, 0) : -1; if (prep != 0 || inst != 0) { const char* message = funchook_error_message(fh); DuskLog.warn("HookSystem: funchook failed for {:p} (prepare={} install={}): {}", fnAddr, prep, inst, message != nullptr && message[0] != '\0' ? message : "no details"); funchook_destroy(fh); *outOriginal = nullptr; return nullptr; } return fh; } #endif bool install_backend( void* fnAddr, void* trampoline, InstalledBackend& outBackend, void** originalStore) { #if DUSK_HAS_PREPATCH if (const auto site = prepatch::lookup(fnAddr)) { *originalStore = site->original; prepatch::publish(*site, trampoline); outBackend.kind = BackendKind::Prepatch; outBackend.prepatchSite = *site; return true; } #endif #if DUSK_HAS_FUNCHOOK funchook_t* handle = install_funchook(fnAddr, trampoline, originalStore); if (handle == nullptr) { return false; } outBackend.kind = BackendKind::Funchook; outBackend.handle = handle; return true; #else #if DUSK_HAS_PREPATCH DuskLog.warn("HookSystem: prepatch backend cannot install target {:p}: {}", fnAddr, prepatch::available() ? "target has no valid gateway" : prepatch::unavailable_reason()); #else DuskLog.warn("HookSystem: no hook backend can install target {:p}", fnAddr); #endif return false; #endif } void deactivate_backend(void* target, InstalledBackend& backend) { #if DUSK_HAS_PREPATCH if (backend.kind == BackendKind::Prepatch) { prepatch::publish(backend.prepatchSite, nullptr); backend = {}; return; } #endif #if DUSK_HAS_FUNCHOOK if (backend.kind == BackendKind::Funchook) { const int uninst = funchook_uninstall(backend.handle, 0); const int destr = funchook_destroy(backend.handle); if (uninst != 0 || destr != 0) { DuskLog.warn("HookSystem: funchook uninstall/destroy for {:p} returned {}/{}", target, uninst, destr); } } #else (void)target; #endif backend = {}; } bool handoff_backend( void* target, InstalledHook& entry, const HookCandidate& candidate, void** outOriginal) { #if DUSK_HAS_PREPATCH if (entry.backend.kind == BackendKind::Prepatch) { *candidate.origStore = entry.original; prepatch::publish(entry.backend.prepatchSite, candidate.trampoline); *outOriginal = entry.original; return true; } #endif #if DUSK_HAS_FUNCHOOK InstalledBackend backend; if (!install_backend(target, candidate.trampoline, backend, candidate.origStore)) { return false; } entry.backend = backend; *outOriginal = *candidate.origStore; return true; #else (void)target; (void)candidate; (void)outOriginal; return false; #endif } ModResult hook_install(ModContext* context, void* fnAddr, void* trampolineFn, void** outOriginal) { if (fnAddr == nullptr || trampolineFn == nullptr || outOriginal == nullptr) { return MOD_INVALID_ARGUMENT; } // Try to detect an invalid function pointer (possibly a vtable slot offset or Itanium mfp // value) and provide a helpful warning instead of faulting const auto raw = reinterpret_cast(fnAddr); if (raw < 0x10000 #if defined(__aarch64__) || defined(_M_ARM64) || (raw & 3) != 0 // code is 4-aligned #endif ) { DuskLog.warn("HookSystem: {:p} from {} is not a code address (virtual member function " "pointer? hook via dusk::mods::Hook or resolve())", fnAddr, mod_id_from_context(context)); return MOD_INVALID_ARGUMENT; } fnAddr = resolve_target(fnAddr); if (!declared_target(context, fnAddr)) { return reject_undeclared(context, fnAddr); } const auto key = reinterpret_cast(fnAddr); if (const auto it = s_installed.find(key); it != s_installed.end()) { auto& entry = it->second; // hook_add_pre + hook_add_post on the same target share one g_orig per mod. const bool known = std::ranges::any_of(entry.candidates, [&](const HookCandidate& cand) { return cand.context == context && cand.origStore == outOriginal; }); if (!known) { entry.candidates.push_back({context, trampolineFn, outOriginal, s_nextOrder++}); } *outOriginal = entry.original; return MOD_OK; } // Inlining can't be intercepted by an entry patch: warn once per target when this // build inlined the function into callers. if (const char* name = nullptr; manifest::has_inline_sites(fnAddr, &name)) { DuskLog.warn("HookSystem: '{}' ({:p}) for {} was inlined into callers in this build; " "the hook only covers the calls that were not inlined", name != nullptr ? name : "?", fnAddr, mod_id_from_context(context)); } InstalledBackend backend; if (!install_backend(fnAddr, trampolineFn, backend, outOriginal)) { return MOD_ERROR; } auto& entry = s_installed[key]; entry.backend = backend; entry.original = *outOriginal; entry.active = context; entry.candidates.push_back({context, trampolineFn, outOriginal, s_nextOrder++}); return MOD_OK; } ModResult hook_add_pre( ModContext* context, void* fnAddr, HookPreFn callback, const HookOptions* options) { if (fnAddr == nullptr || context == nullptr || callback == nullptr) { return MOD_INVALID_ARGUMENT; } fnAddr = resolve_target(fnAddr); if (!declared_target(context, fnAddr)) { return reject_undeclared(context, fnAddr); } auto& hooks = s_registry[reinterpret_cast(fnAddr)].pre; hooks.push_back({context, callback, normalize_options(options), s_nextOrder++}); sort_hooks(hooks); return MOD_OK; } ModResult hook_add_post( ModContext* context, void* fnAddr, HookPostFn callback, const HookOptions* options) { if (fnAddr == nullptr || context == nullptr || callback == nullptr) { return MOD_INVALID_ARGUMENT; } fnAddr = resolve_target(fnAddr); if (!declared_target(context, fnAddr)) { return reject_undeclared(context, fnAddr); } auto& hooks = s_registry[reinterpret_cast(fnAddr)].post; hooks.push_back({context, nullptr, callback, normalize_options(options), s_nextOrder++}); sort_hooks(hooks); return MOD_OK; } ModResult hook_replace( ModContext* context, void* fnAddr, HookReplaceFn callback, const HookOptions* options) { if (fnAddr == nullptr || context == nullptr || callback == nullptr) { return MOD_INVALID_ARGUMENT; } const HookOptions normalized = normalize_options(options); fnAddr = resolve_target(fnAddr); if (!declared_target(context, fnAddr)) { return reject_undeclared(context, fnAddr); } auto& slot = s_registry[reinterpret_cast(fnAddr)]; if (slot.replace.replaceCallback == nullptr) { slot.replace = {context, callback, nullptr, normalized, s_nextOrder++}; return MOD_OK; } switch (normalized.replace_policy) { case HOOK_REPLACE_CONFLICT: DuskLog.error("HookSystem: '{}' conflicts with '{}', both replace the same function", mod_id_from_context(context), mod_id_from_context(slot.replace.context)); return MOD_CONFLICT; case HOOK_REPLACE_PRIORITY: if (normalized.priority <= slot.replace.options.priority) { return MOD_CONFLICT; } slot.replace = {context, callback, nullptr, normalized, s_nextOrder++}; return MOD_OK; case HOOK_REPLACE_OVERRIDE: slot.replace = {context, callback, nullptr, normalized, s_nextOrder++}; return MOD_OK; } return MOD_INVALID_ARGUMENT; } ModResult hook_dispatch_pre( ModContext*, void* fnAddr, void* args, void* retval, int* outSkipOriginal) { if (outSkipOriginal != nullptr) { *outSkipOriginal = 0; } if (fnAddr == nullptr) { return MOD_INVALID_ARGUMENT; } fnAddr = resolve_target(fnAddr); const auto it = s_registry.find(reinterpret_cast(fnAddr)); if (it == s_registry.end()) { return MOD_OK; } auto& slot = it->second; for (auto& hook : slot.pre) { if (hook.callback == nullptr) { continue; } HookAction action = HOOK_CONTINUE; try { action = hook.callback(hook.context, args, retval, hook.options.userdata); } catch (const std::exception& e) { fail_hook_callback(hook.context, "pre", e); continue; } catch (...) { fail_unknown_hook_callback(hook.context, "pre"); continue; } if (action == HOOK_SKIP_ORIGINAL) { if (outSkipOriginal != nullptr) { *outSkipOriginal = 1; } return MOD_OK; } } if (slot.replace.replaceCallback != nullptr) { try { slot.replace.replaceCallback( slot.replace.context, args, retval, slot.replace.options.userdata); } catch (const std::exception& e) { fail_hook_callback(slot.replace.context, "replace", e); return MOD_ERROR; } catch (...) { fail_unknown_hook_callback(slot.replace.context, "replace"); return MOD_ERROR; } if (outSkipOriginal != nullptr) { *outSkipOriginal = 1; } } return MOD_OK; } ModResult hook_dispatch_post(ModContext*, void* fnAddr, void* args, void* retval) { if (fnAddr == nullptr) { return MOD_INVALID_ARGUMENT; } fnAddr = resolve_target(fnAddr); const auto it = s_registry.find(reinterpret_cast(fnAddr)); if (it == s_registry.end()) { return MOD_OK; } for (auto& hook : it->second.post) { if (hook.postCallback != nullptr) { try { hook.postCallback(hook.context, args, retval, hook.options.userdata); } catch (const std::exception& e) { fail_hook_callback(hook.context, "post", e); } catch (...) { fail_unknown_hook_callback(hook.context, "post"); } } } 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(-1); #if defined(_M_X64) || defined(__x86_64__) const auto* p = static_cast(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(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(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(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(insn[0] << 6) >> 6; p += static_cast(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; } /* * Decodes a member hook record's pointer-to-member representation into the target code address. * Virtual members prefer their named overrider, then fall back to reading the primary vtable slot. */ void* resolve_member_record(const unsigned char* pmf, size_t pmfSize, const char* vtableSymbol, const char* displayName, std::string& why) { if (pmfSize < sizeof(uintptr_t)) { why = "truncated pointer-to-member representation"; return nullptr; } uintptr_t words[2]{}; std::memcpy(words, pmf, std::min(sizeof(words), pmfSize)); #if defined(_WIN32) const void* fn = reinterpret_cast(words[0]); if (fn == nullptr) { why = "null pointer-to-member target"; return nullptr; } const size_t slot = vcall_slot_offset(fn); if (slot == static_cast(-1)) { // not a vcall thunk: direct address return const_cast(fn); } // A display name resolves the actual overrider rather than an ABI vcall thunk. Besides being // more direct, this covers secondary vtables: the MSVC representation has `this` adjustment // but does not have the base-path suffix used by the decorated vtable symbol. std::string displayWhy; void* displayTarget = nullptr; if (displayName[0] != '\0' && resolve_symbol_checked(displayName, true, &displayTarget, displayWhy)) { return displayTarget; } int32_t thisAdjustment = 0; if (pmfSize >= sizeof(uintptr_t) + sizeof(thisAdjustment)) { std::memcpy(&thisAdjustment, pmf + sizeof(uintptr_t), sizeof(thisAdjustment)); } if (thisAdjustment != 0) { why = fmt::format( "virtual member requires a {}-byte this adjustment and its overrider did not " "resolve by name ({})", thisAdjustment, displayWhy); return nullptr; } int32_t firstVirtualField = 0; if (pmfSize > MOD_META_HOOK_MEM_CAPACITY && pmfSize >= sizeof(uintptr_t) + 2 * sizeof(int32_t)) { std::memcpy(&firstVirtualField, pmf + sizeof(uintptr_t) + sizeof(int32_t), sizeof(int32_t)); } int32_t secondVirtualField = 0; if (pmfSize > MOD_META_HOOK_MEM_CAPACITY && pmfSize >= sizeof(uintptr_t) + 3 * sizeof(int32_t)) { std::memcpy( &secondVirtualField, pmf + sizeof(uintptr_t) + 2 * sizeof(int32_t), sizeof(int32_t)); } if (firstVirtualField != 0 || secondVirtualField != 0) { why = fmt::format("virtual-base member did not resolve by name ({})", displayWhy); 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; } // ??_7 points at the first slot. return *reinterpret_cast(static_cast(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(words[0]); } std::string displayWhy; void* displayTarget = nullptr; if (displayName[0] != '\0' && resolve_symbol_checked(displayName, true, &displayTarget, displayWhy)) { return displayTarget; } if (thisAdjust != 0) { // The slot is relative to a secondary vtable whose base path is not encoded in the mfp. why = fmt::format( "virtual member of a secondary base did not resolve by name ({})", displayWhy); 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(static_cast(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; std::erase_if(slot.pre, [&](const PreHookFn& hook) { return hook.context == context; }); std::erase_if(slot.post, [&](const VoidHookFn& hook) { return hook.context == context; }); if (slot.replace.context == context) { slot.replace = {}; } if (slot.pre.empty() && slot.post.empty() && slot.replace.replaceCallback == nullptr) { it = s_registry.erase(it); } else { ++it; } } for (auto it = s_installed.begin(); it != s_installed.end();) { auto& entry = it->second; // The departing mod's g_orig slots are about to be unmapped; drop its candidates before // any orig_store rewrites below. std::erase_if(entry.candidates, [context](const HookCandidate& cand) { return cand.context == context; }); if (entry.active != context) { ++it; continue; } auto* target = reinterpret_cast(it->first); if (entry.candidates.empty()) { deactivate_backend(target, entry.backend); it = s_installed.erase(it); continue; } // A prepatch may be atomically updated directly. // Funchook must first restore the original instructions before reinstalling. #if DUSK_HAS_PREPATCH const bool prepatched = entry.backend.kind == BackendKind::Prepatch; #else constexpr bool prepatched = false; #endif if (!prepatched) { deactivate_backend(target, entry.backend); } entry.active = nullptr; for (auto& cand : entry.candidates) { void* original = nullptr; if (!handoff_backend(target, entry, cand, &original)) { continue; } entry.original = original; entry.active = cand.context; DuskLog.info("HookSystem: replaced trampoline for {:p}: {} -> {} (tramp={:p})", target, mod_id_from_context(context), mod_id_from_context(cand.context), cand.trampoline); break; } if (entry.active == nullptr) { DuskLog.warn("HookSystem: no reinstallable trampoline for {:p}; hooks there are " "disabled until a mod reinstalls one", target); for (auto& cand : entry.candidates) { *cand.origStore = target; } deactivate_backend(target, entry.backend); it = s_installed.erase(it); continue; } for (auto& cand : entry.candidates) { *cand.origStore = entry.original; } ++it; } } #else // DUSK_CODE_MODS ModResult hook_install(ModContext*, void*, void*, void**) { return MOD_UNSUPPORTED; } ModResult hook_add_pre(ModContext*, void*, HookPreFn, const HookOptions*) { return MOD_UNSUPPORTED; } ModResult hook_add_post(ModContext*, void*, HookPostFn, const HookOptions*) { return MOD_UNSUPPORTED; } ModResult hook_replace(ModContext*, void*, HookReplaceFn, const HookOptions*) { return MOD_UNSUPPORTED; } ModResult hook_dispatch_pre(ModContext*, void*, void*, void*, int* outSkipOriginal) { if (outSkipOriginal != nullptr) { *outSkipOriginal = 0; } return MOD_UNSUPPORTED; } ModResult hook_dispatch_post(ModContext*, void*, void*, void*) { return MOD_UNSUPPORTED; } void hook_remove_mod(LoadedMod&) {} #endif // DUSK_CODE_MODS // By-name resolution reads the symbol manifest, which is independent of the hook engine. ModResult hook_resolve(ModContext*, const char* symbol, void** outAddr, HookSymbolFlags* outFlags) { if (symbol == nullptr || outAddr == nullptr) { return MOD_INVALID_ARGUMENT; } switch (manifest::resolve(symbol, outAddr, outFlags)) { case manifest::ResolveStatus::Ok: return MOD_OK; case manifest::ResolveStatus::Unavailable: return MOD_UNSUPPORTED; case manifest::ResolveStatus::NotFound: return MOD_UNAVAILABLE; case manifest::ResolveStatus::Ambiguous: return MOD_CONFLICT; } return MOD_ERROR; } constexpr HookService s_hookService{ .header = SERVICE_HEADER(HookService, HOOK_SERVICE_MAJOR, HOOK_SERVICE_MINOR), .install = hook_install, .add_pre = hook_add_pre, .add_post = hook_add_post, .replace = hook_replace, .dispatch_pre = hook_dispatch_pre, .dispatch_post = hook_dispatch_post, .resolve = hook_resolve, }; } // 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_target(target); *slot = target; declared.push_back(reinterpret_cast(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("", "null link-time target", &record->resolved); } } const auto resolveMember = [&](auto* record, const unsigned char* pmf, size_t pmfSize) { const char* displayName = hook_mem_display_name(*record); std::string why; void* target = resolve_member_record(pmf, pmfSize, hook_mem_vtable_symbol(*record), displayName, why); if (target != nullptr) { resolved(target, &record->resolved); } else { unresolved(displayName, why, &record->resolved); } }; for (auto* record : mod.native->parsed.hookMems) { resolveMember(record, record->pmf, sizeof(record->pmf)); } for (auto* record : mod.native->parsed.hookMemExts) { alignas(std::max_align_t) unsigned char pmf[MOD_META_HOOK_MEM_EXT_CAPACITY]{}; record->materialize(pmf); resolveMember(record, pmf, record->pmf_size); } 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, .minorVersion = HOOK_SERVICE_MINOR, .service = &s_hookService, .modDetached = hook_remove_mod, }; } // namespace dusk::mods::svc