mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-23 05:32:42 -04:00
Implement iOS hook prepatching & improve MSVC PTMF support (#2222)
This commit is contained in:
+52
-10
@@ -91,6 +91,35 @@ option(DUSK_PACKAGE_INSTALL "Install Dusklight with a Linux-native file structur
|
||||
option(DUSK_GFX_DEBUG_GROUPS "Report debug groups to the native graphics API" ${DUSK_GFX_DEBUG_GROUPS_DEFAULT})
|
||||
option(DUSK_ENABLE_CODE_MODS "Enable code mods" OFF)
|
||||
|
||||
set(DUSK_HAS_FUNCHOOK OFF)
|
||||
if (DUSK_ENABLE_CODE_MODS AND (NOT APPLE OR CMAKE_SYSTEM_NAME STREQUAL "Darwin"))
|
||||
set(DUSK_HAS_FUNCHOOK ON)
|
||||
endif ()
|
||||
set(DUSK_HAS_FUNCHOOK ${DUSK_HAS_FUNCHOOK} CACHE INTERNAL "Enable funchook" FORCE)
|
||||
|
||||
set(_target_architectures ${CMAKE_OSX_ARCHITECTURES})
|
||||
if (NOT _target_architectures)
|
||||
set(_target_architectures ${CMAKE_SYSTEM_PROCESSOR})
|
||||
endif ()
|
||||
list(LENGTH _target_architectures _target_arch_count)
|
||||
set(DUSK_HAS_PREPATCH OFF)
|
||||
if (DUSK_ENABLE_CODE_MODS AND APPLE AND _target_arch_count EQUAL 1)
|
||||
list(GET _target_architectures 0 _target_arch)
|
||||
if (_target_arch STREQUAL "arm64")
|
||||
set(DUSK_HAS_PREPATCH ON)
|
||||
endif ()
|
||||
endif ()
|
||||
set(DUSK_HAS_PREPATCH ${DUSK_HAS_PREPATCH} CACHE INTERNAL "Enable symgen prepatch support" FORCE)
|
||||
|
||||
if (DUSK_ENABLE_CODE_MODS AND CMAKE_SYSTEM_NAME MATCHES "^(iOS|tvOS)$")
|
||||
list(LENGTH CMAKE_OSX_ARCHITECTURES _mobile_arch_count)
|
||||
if (NOT _mobile_arch_count EQUAL 1 OR NOT CMAKE_OSX_ARCHITECTURES STREQUAL "arm64")
|
||||
message(FATAL_ERROR
|
||||
"iOS/tvOS code mods require a single arm64 architecture; got "
|
||||
"CMAKE_OSX_ARCHITECTURES='${CMAKE_OSX_ARCHITECTURES}'")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
# Edit & Continue
|
||||
if (MSVC)
|
||||
if ("${CMAKE_MSVC_DEBUG_INFORMATION_FORMAT}" STREQUAL "" AND CMAKE_BUILD_TYPE STREQUAL "Debug"
|
||||
@@ -175,8 +204,8 @@ if (CMAKE_SYSTEM_NAME STREQUAL Linux)
|
||||
set(CMAKE_BUILD_RPATH "$ORIGIN")
|
||||
elseif (APPLE)
|
||||
add_compile_options(-Wno-declaration-after-statement -Wno-non-pod-varargs)
|
||||
set(CMAKE_INSTALL_RPATH "$ORIGIN")
|
||||
set(CMAKE_BUILD_RPATH "$ORIGIN")
|
||||
set(CMAKE_INSTALL_RPATH "@loader_path")
|
||||
set(CMAKE_BUILD_RPATH "@loader_path")
|
||||
elseif (MSVC)
|
||||
add_compile_options(
|
||||
$<$<COMPILE_LANGUAGE:C,CXX>:/bigobj>
|
||||
@@ -219,7 +248,7 @@ FetchContent_Declare(miniz
|
||||
)
|
||||
|
||||
set(_fetch_content_deps cxxopts json miniz)
|
||||
if (DUSK_ENABLE_CODE_MODS)
|
||||
if (DUSK_HAS_FUNCHOOK)
|
||||
message(STATUS "dusklight: Fetching funchook")
|
||||
# cmake/PatchFunchook.cmake patches funchook's cmake/capstone.cmake.in to inject a
|
||||
# PATCH_COMMAND into capstone's inner ExternalProject. That PATCH_COMMAND runs
|
||||
@@ -272,10 +301,10 @@ if (DUSK_ENABLE_SENTRY_NATIVE)
|
||||
)
|
||||
if (NOT sentry_native_POPULATED)
|
||||
FetchContent_Populate(sentry_native)
|
||||
set(_dusk_skip_install_rules ${CMAKE_SKIP_INSTALL_RULES})
|
||||
set(_skip_install_rules ${CMAKE_SKIP_INSTALL_RULES})
|
||||
set(CMAKE_SKIP_INSTALL_RULES ON)
|
||||
add_subdirectory(${sentry_native_SOURCE_DIR} ${sentry_native_BINARY_DIR} EXCLUDE_FROM_ALL)
|
||||
set(CMAKE_SKIP_INSTALL_RULES ${_dusk_skip_install_rules})
|
||||
set(CMAKE_SKIP_INSTALL_RULES ${_skip_install_rules})
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
@@ -308,7 +337,7 @@ set(GAME_COMPILE_DEFS DUSK_BUILDING_GAME=1)
|
||||
set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd
|
||||
aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt
|
||||
Threads::Threads zstd::libzstd dusklight_game_headers)
|
||||
if (DUSK_ENABLE_CODE_MODS)
|
||||
if (DUSK_HAS_FUNCHOOK)
|
||||
list(APPEND GAME_LIBS funchook-static)
|
||||
endif ()
|
||||
|
||||
@@ -381,6 +410,9 @@ endif ()
|
||||
if (DUSK_ENABLE_CODE_MODS)
|
||||
list(APPEND GAME_COMPILE_DEFS DUSK_CODE_MODS=1)
|
||||
endif ()
|
||||
list(APPEND GAME_COMPILE_DEFS
|
||||
DUSK_HAS_FUNCHOOK=$<BOOL:${DUSK_HAS_FUNCHOOK}>
|
||||
DUSK_HAS_PREPATCH=$<BOOL:${DUSK_HAS_PREPATCH}>)
|
||||
|
||||
if (DUSK_PACKAGE_INSTALL)
|
||||
include(GNUInstallDirs)
|
||||
@@ -473,18 +505,28 @@ if (MSVC)
|
||||
else ()
|
||||
target_link_options(dusklight PRIVATE /FUNCTIONPADMIN /OPT:NOICF)
|
||||
endif ()
|
||||
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
elseif (CMAKE_CXX_COMPILER_ID MATCHES "^(AppleClang|Clang)$")
|
||||
if (CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64" OR CMAKE_OSX_ARCHITECTURES MATCHES "arm64")
|
||||
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:-fpatchable-function-entry=2,1>)
|
||||
else ()
|
||||
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:-fpatchable-function-entry=10,5>)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
if (DEFINED DUSK_PATCHABLE_ENTRY_FLAG)
|
||||
target_compile_options(dusklight PRIVATE ${DUSK_PATCHABLE_ENTRY_FLAG})
|
||||
foreach(jsystem_lib IN LISTS JSYSTEM_LIBRARIES)
|
||||
target_compile_options(${jsystem_lib} PRIVATE ${DUSK_PATCHABLE_ENTRY_FLAG})
|
||||
endforeach()
|
||||
foreach(_sdk_lib aurora_card aurora_core aurora_dvd aurora_gd aurora_gx aurora_mtx
|
||||
aurora_os aurora_pad aurora_si aurora_vi)
|
||||
if (TARGET ${_sdk_lib})
|
||||
get_target_property(_sdk_lib_imported ${_sdk_lib} IMPORTED)
|
||||
if (NOT _sdk_lib_imported)
|
||||
target_compile_options(${_sdk_lib} PRIVATE ${DUSK_PATCHABLE_ENTRY_FLAG})
|
||||
endif ()
|
||||
endif ()
|
||||
endforeach ()
|
||||
endif ()
|
||||
|
||||
if (WIN32)
|
||||
@@ -612,13 +654,13 @@ if (APPLE)
|
||||
list(APPEND _apple_bundle_properties
|
||||
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${DUSK_ENTITLEMENTS})
|
||||
endif ()
|
||||
if (NOT IOS AND NOT TVOS)
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||
list(APPEND _apple_bundle_properties
|
||||
XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME "YES")
|
||||
endif ()
|
||||
set_target_properties(dusklight PROPERTIES ${_apple_bundle_properties})
|
||||
|
||||
if (NOT IOS AND NOT TVOS AND NOT "${CMAKE_GENERATOR}" STREQUAL "Xcode")
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND NOT "${CMAKE_GENERATOR}" STREQUAL "Xcode")
|
||||
set(_sign_nested_commands)
|
||||
if (TARGET crashpad_handler)
|
||||
list(APPEND _sign_nested_commands
|
||||
@@ -635,7 +677,7 @@ if (APPLE)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
if (APPLE AND NOT IOS AND NOT TVOS)
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||
find_library(APPKIT_FRAMEWORK AppKit REQUIRED)
|
||||
target_sources(dusklight PRIVATE src/dusk/file_select_macos.mm)
|
||||
set_source_files_properties(src/dusk/file_select_macos.mm PROPERTIES COMPILE_FLAGS -fobjc-arc)
|
||||
|
||||
@@ -314,11 +314,19 @@
|
||||
"type": "BOOL",
|
||||
"value": false
|
||||
},
|
||||
"ENABLE_VISIBILITY": {
|
||||
"type": "BOOL",
|
||||
"value": true
|
||||
},
|
||||
"Rust_CARGO_TARGET": "aarch64-apple-ios",
|
||||
"BUILD_SHARED_LIBS": {
|
||||
"type": "BOOL",
|
||||
"value": false
|
||||
},
|
||||
"DUSK_ENABLE_CODE_MODS": {
|
||||
"type": "BOOL",
|
||||
"value": true
|
||||
},
|
||||
"CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": {
|
||||
"type": "BOOL",
|
||||
"value": true
|
||||
@@ -354,12 +362,20 @@
|
||||
"type": "BOOL",
|
||||
"value": false
|
||||
},
|
||||
"ENABLE_VISIBILITY": {
|
||||
"type": "BOOL",
|
||||
"value": true
|
||||
},
|
||||
"Rust_CARGO_TARGET": "aarch64-apple-tvos",
|
||||
"Rust_TOOLCHAIN": "nightly",
|
||||
"BUILD_SHARED_LIBS": {
|
||||
"type": "BOOL",
|
||||
"value": false
|
||||
},
|
||||
"DUSK_ENABLE_CODE_MODS": {
|
||||
"type": "BOOL",
|
||||
"value": true
|
||||
},
|
||||
"CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": {
|
||||
"type": "BOOL",
|
||||
"value": true
|
||||
|
||||
@@ -417,6 +417,21 @@ function(install_bundled_mods)
|
||||
DESTINATION "${_bundle_dir}/Frameworks"
|
||||
PATTERN "${_lib_name}" EXCLUDE)
|
||||
endforeach ()
|
||||
if (DUSK_HAS_PREPATCH)
|
||||
set(_prepatch_executable "${_bundle_dir}/Dusklight")
|
||||
set(_prepatch_mod_args "")
|
||||
foreach (_id IN LISTS _ids)
|
||||
string(APPEND _prepatch_mod_args
|
||||
" \"${_bundle_dir}/Frameworks/${_id}.so\"")
|
||||
endforeach ()
|
||||
install(CODE "
|
||||
execute_process(
|
||||
COMMAND \"${SYMGEN_EXE}\" prepatch
|
||||
--binary \"${_prepatch_executable}\"
|
||||
--report \"${CMAKE_BINARY_DIR}/prepatch-report-$<CONFIG>.json\"
|
||||
${_prepatch_mod_args}
|
||||
COMMAND_ERROR_IS_FATAL ANY)")
|
||||
endif ()
|
||||
else ()
|
||||
foreach (_i RANGE ${_last})
|
||||
list(GET _ids ${_i} _id)
|
||||
@@ -432,6 +447,24 @@ function(install_bundled_mods)
|
||||
endif ()
|
||||
endforeach ()")
|
||||
endforeach ()
|
||||
if (DUSK_HAS_PREPATCH)
|
||||
set(_prepatch_executable "${_bundle_dir}/Contents/MacOS/Dusklight")
|
||||
set(_prepatch_mod_args "")
|
||||
foreach (_i RANGE ${_last})
|
||||
list(GET _ids ${_i} _id)
|
||||
list(GET _lib_platforms ${_i} _lib_platform)
|
||||
list(GET _lib_names ${_i} _lib_name)
|
||||
string(APPEND _prepatch_mod_args
|
||||
" \"${_bundle_dir}/Contents/Resources/mods/${_id}/lib/${_lib_platform}/${_lib_name}\"")
|
||||
endforeach ()
|
||||
install(CODE "
|
||||
execute_process(
|
||||
COMMAND \"${SYMGEN_EXE}\" prepatch
|
||||
--binary \"${_prepatch_executable}\"
|
||||
--report \"${CMAKE_BINARY_DIR}/prepatch-report-$<CONFIG>.json\"
|
||||
${_prepatch_mod_args}
|
||||
COMMAND_ERROR_IS_FATAL ANY)")
|
||||
endif ()
|
||||
if (TARGET crashpad_handler)
|
||||
install(CODE "execute_process(COMMAND /usr/bin/codesign --force --sign - \"${_bundle_dir}/Contents/MacOS/$<TARGET_FILE_NAME:crashpad_handler>\" COMMAND_ERROR_IS_FATAL ANY)")
|
||||
endif ()
|
||||
|
||||
@@ -2,7 +2,7 @@ include_guard(GLOBAL)
|
||||
|
||||
get_filename_component(_SYMBOL_MANIFEST_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
|
||||
|
||||
set(_SYMGEN_VERSION "1.2.3")
|
||||
set(_SYMGEN_VERSION "1.3.1")
|
||||
set(_SYMGEN_RELEASE_BASE_URL "https://github.com/encounter/symgen/releases/download/v${_SYMGEN_VERSION}")
|
||||
set(SYMGEN_PATH "" CACHE FILEPATH "Path to a symgen executable; empty downloads the pinned release")
|
||||
mark_as_advanced(SYMGEN_PATH)
|
||||
@@ -113,14 +113,18 @@ function(setup_symbol_manifest target)
|
||||
endif ()
|
||||
|
||||
if (APPLE)
|
||||
# Room for the load command `symgen manifest --embed` inserts post-link.
|
||||
target_link_options(${target} PRIVATE "LINKER:-headerpad,0x200")
|
||||
# Room for the symbol manifest and several prepatch arenas.
|
||||
target_link_options(${target} PRIVATE "LINKER:-headerpad,0x1000")
|
||||
# ld64 may update an existing output, which breaks our symdb insertion. Remove it first.
|
||||
add_custom_command(TARGET ${target} PRE_LINK
|
||||
COMMAND "${CMAKE_COMMAND}" -E rm -f "$<TARGET_FILE:${target}>"
|
||||
VERBATIM)
|
||||
endif ()
|
||||
|
||||
# The manifest is embedded into the image as a new section, located at runtime through
|
||||
# the descriptor manifest.cpp reserves. On Apple platforms this command must stay
|
||||
# attached before the ad-hoc codesign POST_BUILD command: the patch invalidates any
|
||||
# existing signature.
|
||||
# attached before the ad-hoc codesign POST_BUILD command: the patch removes any existing
|
||||
# signature.
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND "${SYMGEN_EXE}" manifest ${_input} --embed "$<TARGET_FILE:${target}>"
|
||||
COMMENT "Embedding symbol manifest"
|
||||
|
||||
@@ -1479,6 +1479,8 @@ set(DUSK_FILES
|
||||
src/dusk/mods/loader/loader.hpp
|
||||
src/dusk/mods/loader/native_module.cpp
|
||||
src/dusk/mods/loader/native_module.hpp
|
||||
src/dusk/mods/loader/prepatch.cpp
|
||||
src/dusk/mods/loader/prepatch.hpp
|
||||
src/dusk/mods/log_buffer.cpp
|
||||
src/dusk/mods/log_buffer.hpp
|
||||
src/dusk/mods/manifest.cpp
|
||||
|
||||
+24
-1
@@ -148,6 +148,7 @@ typedef enum ModMetaKind {
|
||||
MOD_META_HOOK_FN = 4,
|
||||
MOD_META_HOOK_MEM = 5,
|
||||
MOD_META_HOOK_NAME = 6,
|
||||
MOD_META_HOOK_MEM_EXT = 7,
|
||||
} ModMetaKind;
|
||||
|
||||
typedef struct ModMetaRecord {
|
||||
@@ -203,15 +204,37 @@ static_assert(sizeof(ModMetaHookFn) == 24);
|
||||
* NUL-terminated strings follow `resolved`: the class vtable symbol (empty if the class name is not
|
||||
* representable), then the stringified target for tooling display.
|
||||
*/
|
||||
#define MOD_META_HOOK_MEM_CAPACITY 16u
|
||||
typedef struct MOD_META_ALIGN ModMetaHookMem {
|
||||
ModMetaRecord rec;
|
||||
uint32_t reserved;
|
||||
unsigned char pmf[16];
|
||||
unsigned char pmf[MOD_META_HOOK_MEM_CAPACITY];
|
||||
void* resolved; /* runtime only */
|
||||
} ModMetaHookMem;
|
||||
|
||||
static_assert(sizeof(ModMetaHookMem) == 32);
|
||||
|
||||
/*
|
||||
* Extended member-function hook record. The Microsoft x64 ABI uses a 24-byte member pointer for
|
||||
* classes whose inheritance model was fixed while the class was incomplete. Keep this a distinct
|
||||
* record kind so loaders can continue to consume MOD_META_HOOK_MEM's original 16-byte layout.
|
||||
*
|
||||
* MSVC cannot constant-initialize that member pointer, so materialize points to a compiler-
|
||||
* generated helper that writes its native representation to a caller-provided buffer. The record
|
||||
* itself, including its trailing names and the helper relocation, remains constant-initialized and
|
||||
* available to static tooling. pmf_size is the number of bytes the helper writes.
|
||||
*/
|
||||
#define MOD_META_HOOK_MEM_EXT_CAPACITY 24u
|
||||
typedef void (*ModMetaHookMemMaterializeFn)(unsigned char* out_pmf);
|
||||
typedef struct MOD_META_ALIGN ModMetaHookMemExt {
|
||||
ModMetaRecord rec;
|
||||
uint32_t pmf_size;
|
||||
ModMetaHookMemMaterializeFn materialize;
|
||||
void* resolved; /* runtime only */
|
||||
} ModMetaHookMemExt;
|
||||
|
||||
static_assert(sizeof(ModMetaHookMemExt) == 24);
|
||||
|
||||
/*
|
||||
* Hook on a function by symbol name, for targets that cannot be named in C++ (file-local statics,
|
||||
* private members). One NUL-terminated string follows `resolved`; it may be either the platform
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
@@ -185,12 +186,21 @@ struct HookMemRecord {
|
||||
uint32_t reserved;
|
||||
union {
|
||||
F fn;
|
||||
unsigned char raw[16];
|
||||
unsigned char raw[MOD_META_HOOK_MEM_CAPACITY];
|
||||
} pmf;
|
||||
void* resolved;
|
||||
char names[N];
|
||||
};
|
||||
|
||||
template <size_t N>
|
||||
struct HookMemExtRecord {
|
||||
ModMetaRecord rec;
|
||||
uint32_t pmfSize;
|
||||
ModMetaHookMemMaterializeFn materialize;
|
||||
void* resolved;
|
||||
char names[N];
|
||||
};
|
||||
|
||||
template <size_t N>
|
||||
struct HookNameRecord {
|
||||
ModMetaRecord rec;
|
||||
@@ -232,19 +242,43 @@ consteval auto make_hook_mem_names() {
|
||||
}
|
||||
|
||||
/*
|
||||
* MSVC constant-evaluates a pointer-to-member only when every other operand in the
|
||||
* initializer is a literal: no consteval calls, constexpr-object copies, or default
|
||||
* member initializers.
|
||||
* MSVC constant-evaluates a compact pointer-to-member only when every other operand in the
|
||||
* initializer is a literal: no consteval calls, constexpr-object copies, or default member
|
||||
* initializers. It cannot constant-initialize the 24-byte general representation at all, so the
|
||||
* extended record points at a compiler-generated materializer while keeping the metadata itself
|
||||
* constant-initialized for static tooling.
|
||||
*/
|
||||
template <auto Target, bool Extended, char... Cs>
|
||||
struct HookMemHolderImpl;
|
||||
|
||||
template <auto Target, char... Cs>
|
||||
struct HookMemHolder {
|
||||
struct HookMemHolderImpl<Target, false, Cs...> {
|
||||
using F = decltype(Target);
|
||||
static_assert(sizeof(F) <= 16, "unsupported pointer-to-member representation");
|
||||
MOD_META_RECORD static constinit inline HookMemRecord<F, sizeof...(Cs)> record = {
|
||||
{sizeof(HookMemRecord<F, sizeof...(Cs)>), MOD_META_HOOK_MEM, 0}, 0, {Target}, nullptr,
|
||||
{Cs...}};
|
||||
};
|
||||
|
||||
template <auto Target, char... Cs>
|
||||
struct HookMemHolderImpl<Target, true, Cs...> {
|
||||
using F = decltype(Target);
|
||||
static void materialize(unsigned char* outPmf) {
|
||||
const F target = Target;
|
||||
std::memcpy(outPmf, &target, sizeof(target));
|
||||
}
|
||||
MOD_META_RECORD static constinit inline HookMemExtRecord<sizeof...(Cs)> record = {
|
||||
{sizeof(HookMemExtRecord<sizeof...(Cs)>), MOD_META_HOOK_MEM_EXT, 0}, sizeof(F), materialize,
|
||||
nullptr, {Cs...}};
|
||||
};
|
||||
|
||||
template <auto Target, char... Cs>
|
||||
struct HookMemHolder
|
||||
: HookMemHolderImpl<Target, (sizeof(decltype(Target)) > MOD_META_HOOK_MEM_CAPACITY), Cs...> {
|
||||
using F = decltype(Target);
|
||||
static_assert(sizeof(F) <= MOD_META_HOOK_MEM_EXT_CAPACITY,
|
||||
"unsupported pointer-to-member representation");
|
||||
};
|
||||
|
||||
template <auto Target>
|
||||
struct HookFnHolder {
|
||||
using F = decltype(Target);
|
||||
|
||||
@@ -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