mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-09-09 19:01:12 -04:00
Implement iOS hook prepatching & improve MSVC PTMF support (#2222)
This commit is contained in:
@@ -73,6 +73,7 @@ struct ModMetaParsed {
|
||||
std::vector<ModMetaExport*> exports;
|
||||
std::vector<ModMetaHookFn*> hookFns;
|
||||
std::vector<ModMetaHookMem*> hookMems;
|
||||
std::vector<ModMetaHookMemExt*> hookMemExts;
|
||||
std::vector<ModMetaHookName*> hookNames;
|
||||
};
|
||||
|
||||
@@ -85,6 +86,15 @@ inline const char* hook_mem_display_name(const ModMetaHookMem& rec) {
|
||||
return vtable + std::char_traits<char>::length(vtable) + 1;
|
||||
}
|
||||
|
||||
inline const char* hook_mem_vtable_symbol(const ModMetaHookMemExt& rec) {
|
||||
return reinterpret_cast<const char*>(&rec) + sizeof(ModMetaHookMemExt);
|
||||
}
|
||||
|
||||
inline const char* hook_mem_display_name(const ModMetaHookMemExt& rec) {
|
||||
const char* vtable = hook_mem_vtable_symbol(rec);
|
||||
return vtable + std::char_traits<char>::length(vtable) + 1;
|
||||
}
|
||||
|
||||
inline const char* hook_name_symbol(const ModMetaHookName& rec) {
|
||||
return reinterpret_cast<const char*>(&rec) + sizeof(ModMetaHookName);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
#include "miniz.h"
|
||||
#include "native_module.hpp"
|
||||
#include "nlohmann/json.hpp"
|
||||
#if DUSK_HAS_PREPATCH
|
||||
#include "prepatch.hpp"
|
||||
#endif
|
||||
|
||||
using namespace std::string_literals;
|
||||
using namespace std::string_view_literals;
|
||||
@@ -411,6 +414,28 @@ static bool parse_meta(NativeMod& native, LoadedMod& mod) {
|
||||
parsed.hookMems.push_back(record);
|
||||
break;
|
||||
}
|
||||
case MOD_META_HOOK_MEM_EXT: {
|
||||
if (size <= sizeof(ModMetaHookMemExt)) {
|
||||
return invalid("truncated extended hook record");
|
||||
}
|
||||
auto* record = reinterpret_cast<ModMetaHookMemExt*>(const_cast<uint8_t*>(cursor));
|
||||
if (record->pmf_size <= MOD_META_HOOK_MEM_CAPACITY ||
|
||||
record->pmf_size > MOD_META_HOOK_MEM_EXT_CAPACITY || record->materialize == nullptr)
|
||||
{
|
||||
return invalid("bad extended hook member-pointer size");
|
||||
}
|
||||
const char* strings = reinterpret_cast<const char*>(cursor) + sizeof(ModMetaHookMemExt);
|
||||
const size_t capacity = size - sizeof(ModMetaHookMemExt);
|
||||
if (!terminated_within(strings, capacity)) {
|
||||
return invalid("unterminated extended 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 extended hook display name");
|
||||
}
|
||||
parsed.hookMemExts.push_back(record);
|
||||
break;
|
||||
}
|
||||
case MOD_META_HOOK_NAME: {
|
||||
if (size <= sizeof(ModMetaHookName)) {
|
||||
return invalid("truncated hook record");
|
||||
@@ -906,6 +931,9 @@ void ModLoader::init() {
|
||||
m_initialized = true;
|
||||
|
||||
manifest::initialize();
|
||||
#if DUSK_HAS_PREPATCH
|
||||
prepatch::initialize();
|
||||
#endif
|
||||
|
||||
if (m_searchDirs.empty()) {
|
||||
Log.warn("no mod search directories configured; mod loading skipped");
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
#include "prepatch.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
|
||||
#if DUSK_HAS_PREPATCH
|
||||
#include <mach-o/dyld.h>
|
||||
#include <mach-o/loader.h>
|
||||
#endif
|
||||
|
||||
namespace dusk::mods::prepatch {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::prepatch");
|
||||
|
||||
constexpr std::string_view kSiteMagic = "PS01";
|
||||
constexpr size_t kSiteHeaderSize = 12;
|
||||
constexpr size_t kGatewaySize = 28;
|
||||
constexpr size_t kOriginalStubOffset = 20;
|
||||
|
||||
constexpr uint32_t kBranchMask = 0xfc000000u;
|
||||
constexpr uint32_t kBranch = 0x14000000u;
|
||||
constexpr uint32_t kLdarX16X16 = 0xc8dffe10u;
|
||||
constexpr uint32_t kCbzX16Stub = 0xb4000050u;
|
||||
constexpr uint32_t kBrX16 = 0xd61f0200u;
|
||||
constexpr uint32_t kBtiC = 0xd503245fu;
|
||||
|
||||
struct SiteHeader {
|
||||
char magic[4];
|
||||
int32_t targetDelta;
|
||||
int32_t slotDelta;
|
||||
};
|
||||
static_assert(sizeof(SiteHeader) == kSiteHeaderSize);
|
||||
|
||||
struct Range {
|
||||
uintptr_t begin = 0;
|
||||
uintptr_t end = 0;
|
||||
|
||||
bool contains(uintptr_t address, size_t size) const {
|
||||
return address >= begin && address <= end && size <= end - address;
|
||||
}
|
||||
};
|
||||
|
||||
struct State {
|
||||
std::vector<Range> executableRanges;
|
||||
std::vector<Range> writableRanges;
|
||||
std::string failureReason = "not initialized";
|
||||
bool loaded = false;
|
||||
bool initialized = false;
|
||||
};
|
||||
State s_state;
|
||||
|
||||
void fail(std::string reason) {
|
||||
s_state.failureReason = std::move(reason);
|
||||
Log.error("prepatch backend unavailable: {}", s_state.failureReason);
|
||||
}
|
||||
|
||||
void unavailable(std::string reason) {
|
||||
s_state.failureReason = std::move(reason);
|
||||
Log.info("prepatch backend unavailable: {}", s_state.failureReason);
|
||||
}
|
||||
|
||||
bool add_delta(uintptr_t address, int64_t delta, uintptr_t& out) {
|
||||
if (delta >= 0) {
|
||||
const auto offset = static_cast<uintptr_t>(delta);
|
||||
if (address > std::numeric_limits<uintptr_t>::max() - offset) {
|
||||
return false;
|
||||
}
|
||||
out = address + offset;
|
||||
} else {
|
||||
const auto offset = static_cast<uintptr_t>(-(delta + 1)) + 1;
|
||||
if (address < offset) {
|
||||
return false;
|
||||
}
|
||||
out = address - offset;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool branch_target(uintptr_t address, uint32_t instruction, uintptr_t& out) {
|
||||
if ((instruction & kBranchMask) != kBranch) {
|
||||
return false;
|
||||
}
|
||||
int64_t words = instruction & 0x03ffffffu;
|
||||
if ((words & 0x02000000) != 0) {
|
||||
words -= int64_t{1} << 26;
|
||||
}
|
||||
return add_delta(address, words * 4, out);
|
||||
}
|
||||
|
||||
const Range* containing(const std::vector<Range>& ranges, uintptr_t address, size_t size) {
|
||||
for (const auto& range : ranges) {
|
||||
if (range.contains(address, size)) {
|
||||
return ⦥
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
#if DUSK_HAS_PREPATCH
|
||||
bool slide_address(intptr_t slide, uint64_t vmaddr, uintptr_t& out) {
|
||||
if (vmaddr > std::numeric_limits<uintptr_t>::max()) {
|
||||
return false;
|
||||
}
|
||||
return add_delta(static_cast<uintptr_t>(vmaddr), slide, out);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
void initialize() {
|
||||
if (s_state.initialized) {
|
||||
return;
|
||||
}
|
||||
s_state.initialized = true;
|
||||
|
||||
#if DUSK_HAS_PREPATCH
|
||||
const auto* imageHeader = reinterpret_cast<const mach_header_64*>(_dyld_get_image_header(0));
|
||||
if (imageHeader == nullptr || imageHeader->magic != MH_MAGIC_64 ||
|
||||
imageHeader->cputype != CPU_TYPE_ARM64 ||
|
||||
(imageHeader->cpusubtype & ~CPU_SUBTYPE_MASK) == CPU_SUBTYPE_ARM64E ||
|
||||
(imageHeader->cpusubtype & CPU_SUBTYPE_ARM64_PTR_AUTH_MASK) != 0)
|
||||
{
|
||||
fail("main image is not a supported 64-bit arm64 Mach-O image");
|
||||
return;
|
||||
}
|
||||
|
||||
const intptr_t slide = _dyld_get_image_vmaddr_slide(0);
|
||||
const auto* commands = reinterpret_cast<const uint8_t*>(imageHeader + 1);
|
||||
size_t commandOffset = 0;
|
||||
for (uint32_t i = 0; i < imageHeader->ncmds; ++i) {
|
||||
if (commandOffset > imageHeader->sizeofcmds ||
|
||||
imageHeader->sizeofcmds - commandOffset < sizeof(load_command))
|
||||
{
|
||||
fail("main image has malformed load commands");
|
||||
return;
|
||||
}
|
||||
const auto* command = reinterpret_cast<const load_command*>(commands + commandOffset);
|
||||
if (command->cmdsize < sizeof(load_command) ||
|
||||
command->cmdsize > imageHeader->sizeofcmds - commandOffset)
|
||||
{
|
||||
fail("main image has malformed load commands");
|
||||
return;
|
||||
}
|
||||
if (command->cmd == LC_SEGMENT_64) {
|
||||
if (command->cmdsize < sizeof(segment_command_64)) {
|
||||
fail("main image has a truncated segment command");
|
||||
return;
|
||||
}
|
||||
const auto* segment = reinterpret_cast<const segment_command_64*>(command);
|
||||
const uint64_t vmEnd = segment->vmaddr + segment->vmsize;
|
||||
if (vmEnd < segment->vmaddr) {
|
||||
fail("main image segment range overflows");
|
||||
return;
|
||||
}
|
||||
uintptr_t begin = 0;
|
||||
uintptr_t end = 0;
|
||||
if (!slide_address(slide, segment->vmaddr, begin) ||
|
||||
!slide_address(slide, vmEnd, end) || end < begin)
|
||||
{
|
||||
fail("main image runtime segment range overflows");
|
||||
return;
|
||||
}
|
||||
constexpr vm_prot_t kRx = VM_PROT_READ | VM_PROT_EXECUTE;
|
||||
constexpr vm_prot_t kRw = VM_PROT_READ | VM_PROT_WRITE;
|
||||
if (segment->initprot == kRx && segment->maxprot == kRx) {
|
||||
s_state.executableRanges.push_back({begin, end});
|
||||
} else if (segment->initprot == kRw && segment->maxprot == kRw) {
|
||||
s_state.writableRanges.push_back({begin, end});
|
||||
}
|
||||
}
|
||||
commandOffset += command->cmdsize;
|
||||
}
|
||||
if (commandOffset != imageHeader->sizeofcmds || s_state.executableRanges.empty() ||
|
||||
s_state.writableRanges.empty())
|
||||
{
|
||||
fail("main image has no usable executable or writable segments");
|
||||
return;
|
||||
}
|
||||
|
||||
s_state.failureReason.clear();
|
||||
s_state.loaded = true;
|
||||
#else
|
||||
unavailable("prepatch support is not available in this build");
|
||||
#endif
|
||||
}
|
||||
|
||||
bool available() {
|
||||
return s_state.loaded;
|
||||
}
|
||||
|
||||
const char* unavailable_reason() {
|
||||
return s_state.failureReason.c_str();
|
||||
}
|
||||
|
||||
std::optional<Site> lookup(void* runtimeTarget) {
|
||||
if (!s_state.loaded || runtimeTarget == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto target = reinterpret_cast<uintptr_t>(runtimeTarget);
|
||||
if (containing(s_state.executableRanges, target, sizeof(uint32_t)) == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
uint32_t entry = 0;
|
||||
std::memcpy(&entry, reinterpret_cast<const void*>(target), sizeof(entry));
|
||||
uintptr_t gateway = 0;
|
||||
if (!branch_target(target, entry, gateway) || gateway < kSiteHeaderSize) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const uintptr_t headerAddress = gateway - kSiteHeaderSize;
|
||||
if (containing(s_state.executableRanges, headerAddress, kSiteHeaderSize + kGatewaySize) ==
|
||||
nullptr)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
SiteHeader header{};
|
||||
std::memcpy(&header, reinterpret_cast<const void*>(headerAddress), sizeof(header));
|
||||
if (std::memcmp(header.magic, kSiteMagic.data(), kSiteMagic.size()) != 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
uintptr_t recordedTarget = 0;
|
||||
uintptr_t slot = 0;
|
||||
if (!add_delta(gateway, header.targetDelta, recordedTarget) || recordedTarget != target ||
|
||||
!add_delta(gateway, header.slotDelta, slot) || (slot & (alignof(void*) - 1)) != 0 ||
|
||||
containing(s_state.writableRanges, slot, sizeof(void*)) == nullptr)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
uint32_t instructions[7]{};
|
||||
std::memcpy(instructions, reinterpret_cast<const void*>(gateway), sizeof(instructions));
|
||||
uintptr_t original = 0;
|
||||
if (instructions[2] != kLdarX16X16 || instructions[3] != kCbzX16Stub ||
|
||||
instructions[4] != kBrX16 || instructions[5] != kBtiC ||
|
||||
target > std::numeric_limits<uintptr_t>::max() - 4 ||
|
||||
!branch_target(gateway + 24, instructions[6], original) || original != target + 4)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return Site{
|
||||
reinterpret_cast<void**>(slot), reinterpret_cast<void*>(gateway + kOriginalStubOffset)};
|
||||
}
|
||||
|
||||
void publish(const Site& site, void* trampoline) {
|
||||
std::atomic_ref slot{*site.slot};
|
||||
slot.store(trampoline, std::memory_order_release);
|
||||
}
|
||||
|
||||
} // namespace dusk::mods::prepatch
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace dusk::mods::prepatch {
|
||||
|
||||
struct Site {
|
||||
void** slot = nullptr;
|
||||
void* original = nullptr;
|
||||
};
|
||||
|
||||
void initialize();
|
||||
bool available();
|
||||
const char* unavailable_reason();
|
||||
// Returns the target function's prepatched hook site, or nullopt if it is not prepatched.
|
||||
std::optional<Site> lookup(void* runtimeTarget);
|
||||
// Publishes a trampoline (or nullptr to deactivate).
|
||||
void publish(const Site& site, void* trampoline);
|
||||
|
||||
} // namespace dusk::mods::prepatch
|
||||
+213
-43
@@ -1,6 +1,9 @@
|
||||
#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"
|
||||
|
||||
@@ -9,11 +12,14 @@
|
||||
#include "dusk/mods/log_buffer.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <fmt/format.h>
|
||||
#if DUSK_HAS_FUNCHOOK
|
||||
#include <funchook.h>
|
||||
#endif
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -48,8 +54,7 @@ struct HookSlot {
|
||||
// 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 funchook detour is handed off to a surviving candidate and every candidate's
|
||||
// *orig_store is rewritten to the new original pointer.
|
||||
// unloads, the backend is handed off to a surviving candidate.
|
||||
struct HookCandidate {
|
||||
ModContext* context = nullptr;
|
||||
void* trampoline = nullptr;
|
||||
@@ -57,8 +62,28 @@ struct HookCandidate {
|
||||
uint64_t order = 0;
|
||||
};
|
||||
|
||||
struct InstalledHook {
|
||||
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<HookCandidate> candidates;
|
||||
@@ -205,7 +230,8 @@ void* resolve_target(void* addr) {
|
||||
return addr;
|
||||
}
|
||||
|
||||
funchook_t* install_trampoline(void* fnAddr, void* trampoline, void** outOriginal) {
|
||||
#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);
|
||||
@@ -214,18 +240,101 @@ funchook_t* install_trampoline(void* fnAddr, void* trampoline, void** outOrigina
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
*outOriginal = fn;
|
||||
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) {
|
||||
@@ -272,18 +381,16 @@ ModResult hook_install(ModContext* context, void* fnAddr, void* trampolineFn, vo
|
||||
name != nullptr ? name : "?", fnAddr, mod_id_from_context(context));
|
||||
}
|
||||
|
||||
void* original = nullptr;
|
||||
funchook_t* fh = install_trampoline(fnAddr, trampolineFn, &original);
|
||||
if (fh == nullptr) {
|
||||
InstalledBackend backend;
|
||||
if (!install_backend(fnAddr, trampolineFn, backend, outOriginal)) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
|
||||
auto& entry = s_installed[key];
|
||||
entry.handle = fh;
|
||||
entry.original = original;
|
||||
entry.backend = backend;
|
||||
entry.original = *outOriginal;
|
||||
entry.active = context;
|
||||
entry.candidates.push_back({context, trampolineFn, outOriginal, s_nextOrder++});
|
||||
*outOriginal = original;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
@@ -543,20 +650,67 @@ bool resolve_symbol_checked(const char* symbol, bool requireCode, void** out, st
|
||||
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));
|
||||
/*
|
||||
* 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<const void*>(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<size_t>(-1)) { // not a vcall thunk: direct address
|
||||
return const_cast<void*>(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;
|
||||
@@ -583,10 +737,18 @@ void* resolve_member_record(
|
||||
if (!isVirtual) { // non-virtual: the address itself
|
||||
return reinterpret_cast<void*>(words[0]);
|
||||
}
|
||||
|
||||
std::string displayWhy;
|
||||
void* displayTarget = nullptr;
|
||||
if (displayName[0] != '\0' &&
|
||||
resolve_symbol_checked(displayName, true, &displayTarget, displayWhy))
|
||||
{
|
||||
return displayTarget;
|
||||
}
|
||||
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";
|
||||
// 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') {
|
||||
@@ -638,35 +800,32 @@ void hook_remove_mod(LoadedMod& mod) {
|
||||
}
|
||||
|
||||
auto* target = reinterpret_cast<void*>(it->first);
|
||||
const int uninst = funchook_uninstall(entry.handle, 0);
|
||||
const int destr = funchook_destroy(entry.handle);
|
||||
if (uninst != 0 || destr != 0) {
|
||||
DuskLog.warn("HookSystem: funchook uninstall/destroy for {:p} returned {}/{}", target,
|
||||
uninst, destr);
|
||||
}
|
||||
entry.handle = nullptr;
|
||||
entry.active = nullptr;
|
||||
|
||||
if (entry.candidates.empty()) {
|
||||
deactivate_backend(target, entry.backend);
|
||||
it = s_installed.erase(it);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Hand the detour off to a surviving candidate (lowest registration order first; the
|
||||
// vector is append-ordered). A candidate whose install fails stays in the list: its
|
||||
// g_orig must still track the current original pointer.
|
||||
// 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;
|
||||
funchook_t* fh = install_trampoline(target, cand.trampoline, &original);
|
||||
if (fh == nullptr) {
|
||||
if (!handoff_backend(target, entry, cand, &original)) {
|
||||
continue;
|
||||
}
|
||||
entry.handle = fh;
|
||||
entry.original = original;
|
||||
entry.active = cand.context;
|
||||
DuskLog.info("HookSystem: reinstalled trampoline for {:p}: {} -> {} (tramp={:p})",
|
||||
target, mod_id_from_context(context), mod_id_from_context(cand.context),
|
||||
cand.trampoline);
|
||||
DuskLog.info("HookSystem: replaced trampoline for {:p}: {} -> {} (tramp={:p})", target,
|
||||
mod_id_from_context(context), mod_id_from_context(cand.context), cand.trampoline);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -677,6 +836,7 @@ void hook_remove_mod(LoadedMod& mod) {
|
||||
for (auto& cand : entry.candidates) {
|
||||
*cand.origStore = target;
|
||||
}
|
||||
deactivate_backend(target, entry.backend);
|
||||
it = s_installed.erase(it);
|
||||
continue;
|
||||
}
|
||||
@@ -772,14 +932,24 @@ void hook_resolve_mod_records(LoadedMod& mod) {
|
||||
unresolved("<fn>", "null link-time target", &record->resolved);
|
||||
}
|
||||
}
|
||||
for (auto* record : mod.native->parsed.hookMems) {
|
||||
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(*record, hook_mem_vtable_symbol(*record), why);
|
||||
void* target =
|
||||
resolve_member_record(pmf, pmfSize, hook_mem_vtable_symbol(*record), displayName, why);
|
||||
if (target != nullptr) {
|
||||
resolved(target, &record->resolved);
|
||||
} else {
|
||||
unresolved(hook_mem_display_name(*record), why, &record->resolved);
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user