Implement iOS hook prepatching & improve MSVC PTMF support (#2222)

This commit is contained in:
Luke Street
2026-07-17 22:35:37 -06:00
committed by GitHub
parent 1bae8a5e6a
commit 8caab1a6ba
12 changed files with 707 additions and 65 deletions
+52 -10
View File
@@ -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)
+16
View File
@@ -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
+33
View File
@@ -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 ()
+9 -5
View File
@@ -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"
+2
View File
@@ -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
View File
@@ -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
+40 -6
View File
@@ -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);
+10
View File
@@ -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);
}
+28
View File
@@ -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");
+260
View File
@@ -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 &range;
}
}
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
+20
View File
@@ -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
View File
@@ -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);