From 9582319a678cd7f78afbff8e67b0e19c61c1c295 Mon Sep 17 00:00:00 2001 From: tashcan Date: Thu, 15 Oct 2026 16:40:40 -0700 Subject: [PATCH] Update SPUD Replace the vendored snapshot with the current upstream detour, relocation and signature-scanning code. Callers and the x64 fixture move to the new install API, which reports failures instead of returning a bare pointer. The upstream CMake manifests and license stay unchanged, so dependency provenance can still be checked directly. The chat title hooks go back to SetWidgetData and OnDidBindContext, on Windows too. The new SPUD patches fit the shared title wrapper, so neither the SetIdentifiers workaround nor the Windows skip is needed. --- example_community_patch_settings_en.toml | 12 +- macos-launcher/xmake.lua | 2 +- mods/src/config.cc | 2 +- mods/src/patches/native_hook_extent.cc | 9 +- mods/src/patches/parts/alliance_store.cc | 10 +- mods/src/patches/parts/chat_titles.cc | 168 +- mods/src/patches/parts/hotkeys.cc | 2 +- mods/src/patches/parts/object_tracker.cc | 2 +- mods/src/patches/parts/pinned_ship_sort.cc | 67 +- mods/src/prime/FleetPlayerData.h | 4 +- tests/shortcut_hint_cache.cc | 6 +- tests/spud_relocation.cc | 89 +- .../packages/s/spud/spud-src/CMakeLists.txt | 819 ++++-- .../packages/s/spud/spud-src/LICENSE | 21 + .../spud/spud-src/cmake/generated/deps.cmake | 74 + .../spud-src/cmake/generated/options.cmake | 8 + .../spud-src/cmake/generated/sources.cmake | 68 + .../s/spud/spud-src/cmake/spud.manifest.json | 148 ++ .../s/spud/spud-src/cmake/spudConfig.cmake.in | 40 + .../s/spud/spud-src/include/spud/arch.h | 18 +- .../include/spud/details/function_traits.h | 55 - .../s/spud/spud-src/include/spud/detour.h | 1078 ++++++-- .../spud/spud-src/include/spud/diagnostics.h | 74 + .../s/spud/spud-src/include/spud/error.h | 74 + .../s/spud/spud-src/include/spud/memory.h | 100 + .../spud-src/include/spud/memory/protection.h | 73 +- .../s/spud/spud-src/include/spud/pointer.h | 468 ++++ .../s/spud/spud-src/include/spud/signature.h | 443 +++- .../s/spud/spud-src/include/spud/utils.h | 110 +- .../s/spud/spud-src/src/capstone_utils.cc | 0 .../s/spud/spud-src/src/capstone_utils.h | 4 - .../spud-src/src/detour/aarch64/aarch64.cc | 1526 ++++++++--- .../spud-src/src/detour/aarch64/aarch64.h | 28 +- .../spud/spud-src/src/detour/aarch64/detour.S | 5 - .../spud-src/src/detour/aarch64/relocators.cc | 202 -- .../spud-src/src/detour/aarch64/relocators.h | 30 - .../s/spud/spud-src/src/detour/chain.cc | 215 ++ .../s/spud/spud-src/src/detour/chain.h | 159 ++ .../spud/spud-src/src/detour/control_flow.cc | 115 + .../s/spud/spud-src/src/detour/control_flow.h | 11 + .../s/spud/spud-src/src/detour/detour.cc | 1699 +++++++++++- .../s/spud/spud-src/src/detour/detour_impl.h | 248 +- .../s/spud/spud-src/src/detour/detour_stub.h | 516 ++++ .../s/spud/spud-src/src/detour/import.cc | 629 +++++ .../s/spud/spud-src/src/detour/pointer.cc | 593 +++++ .../s/spud/spud-src/src/detour/prologue.cc | 63 - .../s/spud/spud-src/src/detour/remapper.cc | 39 - .../s/spud/spud-src/src/detour/remapper.h | 20 - .../spud/spud-src/src/detour/x86_64/detour.S | 7 - .../detour/x86_64/detour_context_support.asm | 9 - .../detour/x86_64/detour_context_support.masm | 12 + .../spud-src/src/detour/x86_64/relocators.cc | 172 -- .../spud-src/src/detour/x86_64/relocators.h | 30 - .../spud/spud-src/src/detour/x86_64/x86_64.cc | 2293 ++++++++++++++--- .../spud/spud-src/src/detour/x86_64/x86_64.h | 34 +- .../spud-src/src/memory/executable_pool.cc | 660 +++++ .../spud-src/src/memory/executable_pool.h | 154 ++ .../s/spud/spud-src/src/memory/protection.cc | 1132 +++++++- .../s/spud/spud-src/src/memory/readable.h | 55 + .../s/spud/spud-src/src/memory/safe_patch.cc | 2172 ++++++++++++++++ .../s/spud/spud-src/src/memory/safe_patch.h | 276 ++ .../s/spud/spud-src/src/memory/unwind.cc | 139 + .../s/spud/spud-src/src/memory/unwind.h | 152 ++ .../spud/spud-src/src/memory/unwind/apple.cc | 457 ++++ .../spud-src/src/memory/unwind/compact.cc | 756 ++++++ .../spud/spud-src/src/memory/unwind/dwarf.cc | 1363 ++++++++++ .../s/spud/spud-src/src/memory/unwind/dwarf.h | 167 ++ .../spud/spud-src/src/memory/unwind/linux.cc | 207 ++ .../spud-src/src/memory/unwind/windows.cc | 919 +++++++ .../spud/spud-src/src/memory/unwind/windows.h | 52 + .../src/memory/unwind/windows_handler.cc | 776 ++++++ .../s/spud/spud-src/src/module_path.h | 138 + .../s/spud/spud-src/src/signature/scan.h | 184 ++ .../spud/spud-src/src/signature/signature.cc | 852 ++++-- .../spud-src/src/signature/signature_avx2.cc | 99 +- .../spud-src/src/signature/signature_neon.cc | 108 +- .../spud-src/src/signature/signature_sse.cc | 101 +- .../packages/s/spud/spud-src/src/utils.cc | 138 +- .../s/spud/spud-src/src/zydis_utils.cc | 379 --- .../s/spud/spud-src/src/zydis_utils.h | 10 - xmake-packages/packages/s/spud/xmake.lua | 2 +- xmake-requires.lock | 12 +- xmake/dependencies/common.lua | 2 +- 83 files changed, 21075 insertions(+), 3090 deletions(-) create mode 100644 xmake-packages/packages/s/spud/spud-src/LICENSE create mode 100644 xmake-packages/packages/s/spud/spud-src/cmake/generated/deps.cmake create mode 100644 xmake-packages/packages/s/spud/spud-src/cmake/generated/options.cmake create mode 100644 xmake-packages/packages/s/spud/spud-src/cmake/generated/sources.cmake create mode 100644 xmake-packages/packages/s/spud/spud-src/cmake/spud.manifest.json create mode 100644 xmake-packages/packages/s/spud/spud-src/cmake/spudConfig.cmake.in delete mode 100644 xmake-packages/packages/s/spud/spud-src/include/spud/details/function_traits.h create mode 100644 xmake-packages/packages/s/spud/spud-src/include/spud/diagnostics.h create mode 100644 xmake-packages/packages/s/spud/spud-src/include/spud/error.h create mode 100644 xmake-packages/packages/s/spud/spud-src/include/spud/memory.h create mode 100644 xmake-packages/packages/s/spud/spud-src/include/spud/pointer.h delete mode 100644 xmake-packages/packages/s/spud/spud-src/src/capstone_utils.cc delete mode 100644 xmake-packages/packages/s/spud/spud-src/src/capstone_utils.h delete mode 100644 xmake-packages/packages/s/spud/spud-src/src/detour/aarch64/detour.S delete mode 100644 xmake-packages/packages/s/spud/spud-src/src/detour/aarch64/relocators.cc delete mode 100644 xmake-packages/packages/s/spud/spud-src/src/detour/aarch64/relocators.h create mode 100644 xmake-packages/packages/s/spud/spud-src/src/detour/chain.cc create mode 100644 xmake-packages/packages/s/spud/spud-src/src/detour/chain.h create mode 100644 xmake-packages/packages/s/spud/spud-src/src/detour/control_flow.cc create mode 100644 xmake-packages/packages/s/spud/spud-src/src/detour/control_flow.h create mode 100644 xmake-packages/packages/s/spud/spud-src/src/detour/detour_stub.h create mode 100644 xmake-packages/packages/s/spud/spud-src/src/detour/import.cc create mode 100644 xmake-packages/packages/s/spud/spud-src/src/detour/pointer.cc delete mode 100644 xmake-packages/packages/s/spud/spud-src/src/detour/prologue.cc delete mode 100644 xmake-packages/packages/s/spud/spud-src/src/detour/remapper.cc delete mode 100644 xmake-packages/packages/s/spud/spud-src/src/detour/remapper.h delete mode 100644 xmake-packages/packages/s/spud/spud-src/src/detour/x86_64/detour.S delete mode 100644 xmake-packages/packages/s/spud/spud-src/src/detour/x86_64/detour_context_support.asm create mode 100644 xmake-packages/packages/s/spud/spud-src/src/detour/x86_64/detour_context_support.masm delete mode 100644 xmake-packages/packages/s/spud/spud-src/src/detour/x86_64/relocators.cc delete mode 100644 xmake-packages/packages/s/spud/spud-src/src/detour/x86_64/relocators.h create mode 100644 xmake-packages/packages/s/spud/spud-src/src/memory/executable_pool.cc create mode 100644 xmake-packages/packages/s/spud/spud-src/src/memory/executable_pool.h create mode 100644 xmake-packages/packages/s/spud/spud-src/src/memory/readable.h create mode 100644 xmake-packages/packages/s/spud/spud-src/src/memory/safe_patch.cc create mode 100644 xmake-packages/packages/s/spud/spud-src/src/memory/safe_patch.h create mode 100644 xmake-packages/packages/s/spud/spud-src/src/memory/unwind.cc create mode 100644 xmake-packages/packages/s/spud/spud-src/src/memory/unwind.h create mode 100644 xmake-packages/packages/s/spud/spud-src/src/memory/unwind/apple.cc create mode 100644 xmake-packages/packages/s/spud/spud-src/src/memory/unwind/compact.cc create mode 100644 xmake-packages/packages/s/spud/spud-src/src/memory/unwind/dwarf.cc create mode 100644 xmake-packages/packages/s/spud/spud-src/src/memory/unwind/dwarf.h create mode 100644 xmake-packages/packages/s/spud/spud-src/src/memory/unwind/linux.cc create mode 100644 xmake-packages/packages/s/spud/spud-src/src/memory/unwind/windows.cc create mode 100644 xmake-packages/packages/s/spud/spud-src/src/memory/unwind/windows.h create mode 100644 xmake-packages/packages/s/spud/spud-src/src/memory/unwind/windows_handler.cc create mode 100644 xmake-packages/packages/s/spud/spud-src/src/module_path.h create mode 100644 xmake-packages/packages/s/spud/spud-src/src/signature/scan.h delete mode 100644 xmake-packages/packages/s/spud/spud-src/src/zydis_utils.cc delete mode 100644 xmake-packages/packages/s/spud/spud-src/src/zydis_utils.h diff --git a/example_community_patch_settings_en.toml b/example_community_patch_settings_en.toml index dfca5dd06..93faa6b7a 100644 --- a/example_community_patch_settings_en.toml +++ b/example_community_patch_settings_en.toml @@ -784,19 +784,11 @@ notify_banner_types = "" auto_open_chat = false # Initial side-chat width in canvas units; 0 preserves the native width. Positive values are clamped to available space and the minimum width. Requires mod 1.1.10 or newer. -default_chat_width = 0 +default_chat_width = 0.0 # Change to true to remove Galaxy Chat from the game disable_galaxy_chat = false -# Initial side-chat width in canvas units. 0 keeps the game default. -# The game clamps positive values to the available screen space and minimum width. -default_chat_width = 0.0 - -# Open alliance side chat once per login, after station and chat initialization. -# Waits for section completion and three continuous seconds of readiness. -auto_open_chat = false - # Change to true to remove Veil Chat from the game disable_veil_chat = false @@ -812,7 +804,7 @@ extend_chest_purchase_max = 160 # Subgroup: Daily Goals # --------------------- -# Comma-separated factions (federation, klingon, romulan) for which the Daily Goals bulk-claim toggle applies; leave blank to preserve the game's default behaviour +# Comma-separated factions for Daily Goals bulk claim. Missing or blank uses the default below; "none" skips all three main-faction dailies. Other dailies remain eligible. daily_bulk_claim_factions = "federation,klingon,romulan" # Force the Daily Goals Claim All toggle back on the first time the screen is shown each session diff --git a/macos-launcher/xmake.lua b/macos-launcher/xmake.lua index 537d9e3ff..ef0ea8722 100644 --- a/macos-launcher/xmake.lua +++ b/macos-launcher/xmake.lua @@ -30,7 +30,7 @@ target("macOSLauncher") add_files("src/*.cc") add_includedirs("deps/PLzmaSDK") add_packages("librsync", "PLzmaSDK") - add_ldflags("-lc++") + add_ldflags("-lc++", {force = true}) add_values("xcode.bundle_identifier", "com.stfcmod.startrekpatch") add_values("xcode.codesign_identity", "-") diff --git a/mods/src/config.cc b/mods/src/config.cc index 51f535641..367441c34 100644 --- a/mods/src/config.cc +++ b/mods/src/config.cc @@ -1228,7 +1228,7 @@ void Config::Load() DCU::auto_confirm_reveal_ability, write_config); this->auto_confirm_alliance_contribution = get_config_or_default(config, parsed, "ui", "auto_confirm_alliance_contribution", - DCU::auto_confirm_alliance_contribution, write_config); + DCU::auto_confirm_alliance_contribution, write_config); this->extend_chest_purchase_max = get_config_or_default(config, parsed, "ui", "extend_chest_purchase_max", DCU::extend_chest_purchase_max, write_config); diff --git a/mods/src/patches/native_hook_extent.cc b/mods/src/patches/native_hook_extent.cc index 4d11461d1..f17e3fe77 100644 --- a/mods/src/patches/native_hook_extent.cc +++ b/mods/src/patches/native_hook_extent.cc @@ -1,4 +1,4 @@ -#include "native_hook_extent.h" +#include "patches/native_hook_extent.h" #if __APPLE__ #include @@ -91,7 +91,12 @@ bool native_hooks::MacHookFits(const void* method, std::size_t minimum_extent) { #if __APPLE__ const auto extent = FunctionExtent(method); - const bool fits = extent >= std::max(minimum_extent, std::size_t{32}) && spud::has_detour_prologue(method, extent); + bool fits = false; + if (extent >= std::max(minimum_extent, std::size_t{32})) { + const auto plan = spud::inspect_detour(method, spud::Arch::kHost, extent); + fits = plan.ok() && plan.resolved_target == reinterpret_cast(method) && plan.far_jump_size != 0 + && plan.far_required_patch_bytes >= plan.far_jump_size && plan.far_required_patch_bytes <= extent; + } spdlog::info("[MacHookExtent] entry={} bytes={} accepted={}", method, extent, fits); return fits; #else diff --git a/mods/src/patches/parts/alliance_store.cc b/mods/src/patches/parts/alliance_store.cc index 295b9c407..ba6393df2 100644 --- a/mods/src/patches/parts/alliance_store.cc +++ b/mods/src/patches/parts/alliance_store.cc @@ -470,9 +470,13 @@ bool StoreHookFits(const MethodInfo* method) #if _WIN32 && defined(_M_X64) DWORD64 base = 0; auto* function = RtlLookupFunctionEntry(reinterpret_cast(entry), &base, nullptr); - return function && base + function->BeginAddress == reinterpret_cast(entry) - && function->EndAddress > function->BeginAddress - && spud::has_detour_prologue(entry, function->EndAddress - function->BeginAddress); + if (!function || base + function->BeginAddress != reinterpret_cast(entry) + || function->EndAddress <= function->BeginAddress) + return false; + const size_t extent = function->EndAddress - function->BeginAddress; + const auto plan = spud::inspect_detour(entry, spud::Arch::kHost, extent); + return plan.ok() && plan.resolved_target == reinterpret_cast(entry) && plan.far_jump_size != 0 + && plan.far_required_patch_bytes >= plan.far_jump_size && plan.far_required_patch_bytes <= extent; #elif __APPLE__ return native_hooks::MacHookFits(entry); #else diff --git a/mods/src/patches/parts/chat_titles.cc b/mods/src/patches/parts/chat_titles.cc index 11ad63913..f648014dd 100644 --- a/mods/src/patches/parts/chat_titles.cc +++ b/mods/src/patches/parts/chat_titles.cc @@ -18,122 +18,42 @@ Il2CppObject* InvokeSelf(Il2CppObject* object, const char* method_name) { auto* method = il2cpp_class_get_method_from_name(object ? il2cpp_object_get_class(object) : nullptr, method_name, 0); if (!method) { - static int missing_reports = 0; - if (missing_reports++ < 8) { - spdlog::warn("[ChatTitleDisplay] Cannot resolve {} on {}", method_name, - object ? il2cpp_class_get_name(il2cpp_object_get_class(object)) : "null"); - } return nullptr; } Il2CppException* exception = nullptr; - auto* result = (Il2CppObject*)il2cpp_runtime_invoke(method, object, nullptr, &exception); - if (exception) { - static int exception_reports = 0; - if (exception_reports++ < 8) { - spdlog::warn("[ChatTitleDisplay] Managed invocation failed: {}", method_name); - } - return nullptr; - } - return result; + return (Il2CppObject*)il2cpp_runtime_invoke(method, object, nullptr, &exception); } -Il2CppObject* FindChatRowTransform(void* profile_widget) +bool IsChatRow(void* profile_widget) { auto* mb = (Il2CppObject*)profile_widget; if (!mb) { - return nullptr; + return false; } auto* transform = InvokeSelf(mb, "get_transform"); if (!transform) { - return nullptr; + return false; } for (int hops = 0; hops < 32 && transform; ++hops) { auto* gameObject = InvokeSelf(transform, "get_gameObject"); if (!gameObject) { - return nullptr; + return false; } if (reinterpret_cast(gameObject)->GetComponentFastPath2()) { - return transform; + return true; } transform = InvokeSelf(transform, "get_parent"); } - return nullptr; + return false; } -struct TitleLayoutElement { - static IL2CppClassHelper& get_class_helper() - { - static auto helper = il2cpp_get_class_helper("UnityEngine.UI", "UnityEngine.UI", "LayoutElement"); - return helper; - } -}; - -void HideTitleRow(void* title, Il2CppObject* row_transform) +void HideTitleRow(void* title) { auto* game_object = reinterpret_cast(InvokeSelf(static_cast(title), "get_gameObject")); if (!game_object) { return; } game_object->SetActive(false); - - // In the chat prefab, _container is the enclosing PlayerTitleContainer, whose - // LayoutElement reserves 24 units even when the title widget child is hidden. - // Resolve the serialized reference rather than assuming a particular parent. - static auto* container_field = - il2cpp_class_get_field_from_name(il2cpp_object_get_class(static_cast(title)), "_container"); - GameObject* container = nullptr; - if (container_field) { - il2cpp_field_get_value(static_cast(title), container_field, &container); - } - if (container) { - static int container_reports = 0; - if (container_reports++ < 8) { - spdlog::info("[ChatTitleDisplay] Hiding title '{}' and container '{}' (active={})", game_object->Name(), - container->Name(), container->activeInHierarchy); - } - // The native bind path may reactivate the container after a nested title - // callback. Excluding its LayoutElement keeps that empty row out of layout. - if (auto* element = container->GetComponentFastPath2()) { - static auto ignore_layout = TitleLayoutElement::get_class_helper().GetMethodInfo("set_ignoreLayout", 1); - if (ignore_layout) { - bool ignore = true; - void* args[] = {&ignore}; - Il2CppException* exception = nullptr; - il2cpp_runtime_invoke(ignore_layout, element, args, &exception); - if (exception) { - spdlog::warn("[ChatTitleDisplay] Cannot exclude title container from layout"); - } - } - } - container->SetActive(false); - } else { - static bool reported = false; - if (!reported) { - reported = true; - spdlog::warn("[ChatTitleDisplay] Title '{}' has no container (field found={})", game_object->Name(), - container_field != nullptr); - } - } - - // Rebuild the complete message row before the virtualized list measures it; - // marking only the inner text layout leaves the enclosing row size cached. - static auto layout = il2cpp_get_class_helper("UnityEngine.UI", "UnityEngine.UI", "LayoutRebuilder"); - static auto rebuild = layout.GetMethodInfo("ForceRebuildLayoutImmediate", 1); - static auto mark = layout.GetMethodInfo("MarkLayoutForRebuild", 1); - for (auto* method : {rebuild, mark}) { - if (method) { - void* args[] = {row_transform}; - Il2CppException* exception = nullptr; - il2cpp_runtime_invoke(method, nullptr, args, &exception); - if (exception) { - static bool reported = false; - if (!reported) { - reported = true; - spdlog::warn("[ChatTitleDisplay] Chat row layout rebuild failed"); - } - } - } - } } void ApplyTitleDisplay(void* profile_widget) @@ -145,26 +65,15 @@ void ApplyTitleDisplay(void* profile_widget) const auto show = Config::Get().show_player_title; auto* title = widget->_playerTitleWidget; - static bool reported_callback = false; - if (!reported_callback) { - reported_callback = true; - spdlog::info("[ChatTitleDisplay] Profile callback reached (show={}, title present={})", show, title != nullptr); - } if (show || title == nullptr) { return; } - auto* row_transform = FindChatRowTransform(widget); - if (!row_transform) { - static bool reported = false; - if (!reported) { - reported = true; - spdlog::info("[ChatTitleDisplay] Profile callback excluded: no ChatMessageWidget ancestor"); - } + if (!IsChatRow(widget)) { return; } - HideTitleRow(title, row_transform); + HideTitleRow(title); } } // namespace @@ -176,7 +85,7 @@ void UserProfileWidget_UpdatePlayerTitle_Hook(auto original, void* _this) } catch (...) { } } -void PlayerTitleWidget_SetIdentifiers_Hook(auto original, void* _this) +void PlayerTitleWidget_BindPath_Hook(auto original, void* _this) { original(_this); try { @@ -237,46 +146,65 @@ void InstallChatTitleDisplayHooks() ErrorMsg::MissingMethod("UserProfileWidget", "UpdatePlayerTitle"); } - void* set_identifiers = nullptr; + void* set_widget_data = nullptr; + void* on_did_bind_context = nullptr; if (auto title_widget = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.PlayerAvatars", "PlayerTitleWidget"); !title_widget.isValidHelper()) { ErrorMsg::MissingHelper("Digit.Prime.PlayerAvatars", "PlayerTitleWidget"); - } else if ((set_identifiers = title_widget.GetMethod("SetIdentifiers", 0)) == nullptr) { - ErrorMsg::MissingMethod("PlayerTitleWidget", "SetIdentifiers"); + } else { + set_widget_data = title_widget.GetMethod("SetWidgetData", 0); + on_did_bind_context = title_widget.GetMethod("OnDidBindContext", 0); + if (!set_widget_data || !on_did_bind_context) { + ErrorMsg::MissingMethod("PlayerTitleWidget", set_widget_data ? "OnDidBindContext" : "SetWidgetData"); + set_widget_data = nullptr; + on_did_bind_context = nullptr; + } } - if (!update_player_title && !set_identifiers) { + if (!update_player_title && !set_widget_data && !on_did_bind_context) { return; } if (update_player_title) { update_player_title = ValidateDetourTarget(update_player_title, "UpdatePlayerTitle"); } - if (set_identifiers) { - set_identifiers = ValidateDetourTarget(set_identifiers, "SetIdentifiers"); + if (set_widget_data) { + set_widget_data = ValidateDetourTarget(set_widget_data, "SetWidgetData"); + } + if (on_did_bind_context) { + on_did_bind_context = ValidateDetourTarget(on_did_bind_context, "OnDidBindContext"); } - if (!update_player_title && !set_identifiers) { + if (!update_player_title && !set_widget_data && !on_did_bind_context) { spdlog::warn("Skipping ChatTitleDisplayHooks: no detour target fits; no hooks installed."); return; } - // Both title-bind callbacks tail-call SetIdentifiers. On Windows client 272 - // their shared wrapper is only 16 bytes, less than SPUD's 24-byte overwrite, - // so detouring it also corrupts the adjacent title-clear callback. Hook the - // substantive implementation once instead. - if (set_identifiers && set_identifiers == update_player_title) { - spdlog::warn("[ChatTitleDisplay] SetIdentifiers shares its target with UpdatePlayerTitle; keeping one detour"); - set_identifiers = nullptr; + // The PlayerTitleWidget callbacks can resolve through thunks to one shared + // native implementation. A second detour on the same address would rebuild + // the first detour's jump into a broken trampoline, so keep one hook per + // target; the shared body funnels both method entries into the same hook. + if (set_widget_data && set_widget_data == update_player_title) { + spdlog::info("[ChatTitleDisplay] SetWidgetData shares its native implementation with UpdatePlayerTitle; " + "keeping one detour"); + set_widget_data = nullptr; + } + if (on_did_bind_context + && (on_did_bind_context == update_player_title || on_did_bind_context == set_widget_data)) { + spdlog::info("[ChatTitleDisplay] OnDidBindContext shares its native implementation with another target; " + "keeping one detour"); + on_did_bind_context = nullptr; } if (update_player_title) { SPUD_STATIC_DETOUR(update_player_title, UserProfileWidget_UpdatePlayerTitle_Hook); } - if (set_identifiers) { - SPUD_STATIC_DETOUR(set_identifiers, PlayerTitleWidget_SetIdentifiers_Hook); + if (set_widget_data) { + SPUD_STATIC_DETOUR(set_widget_data, PlayerTitleWidget_BindPath_Hook); + } + if (on_did_bind_context) { + SPUD_STATIC_DETOUR(on_did_bind_context, PlayerTitleWidget_BindPath_Hook); } - spdlog::info("Chat title display: chat player title rows {} (immediate row layout rebuild enabled)", - Config::Get().show_player_title ? "shown" : "hidden"); + spdlog::info("Chat title display: chat player title rows {}", Config::Get().show_player_title ? "shown" : "hidden"); } diff --git a/mods/src/patches/parts/hotkeys.cc b/mods/src/patches/parts/hotkeys.cc index 730ba5881..1d687f17d 100644 --- a/mods/src/patches/parts/hotkeys.cc +++ b/mods/src/patches/parts/hotkeys.cc @@ -274,7 +274,7 @@ void ShortcutKeybindHint_UpdateText_Hook(auto original, void* _this) auto* bar = ObjectFinder::Get(); auto* context = bar ? bar->CanvasContext() : nullptr; auto* fleets = context ? context->FleetData() : nullptr; - hint_action = ShipShortcutHintAction(hint_action, fleets ? fleets->max_length : 0, [fleets](std::size_t position) { + hint_action = ShipShortcutHintAction(hint_action, fleets ? fleets->max_length : 0, [fleets](std::size_t position) { auto* fleet = reinterpret_cast(fleets->vector)[position]; return fleet ? fleet->Index : -1; }); diff --git a/mods/src/patches/parts/object_tracker.cc b/mods/src/patches/parts/object_tracker.cc index e6a30fc60..a50138649 100644 --- a/mods/src/patches/parts/object_tracker.cc +++ b/mods/src/patches/parts/object_tracker.cc @@ -287,6 +287,6 @@ void InstallObjectTrackers() return; } - const auto GC_register_finalizer_inner_match = GC_register_finalizer_inner_matches.get(0); + const auto GC_register_finalizer_inner_match = GC_register_finalizer_inner_matches[0]; GC_register_finalizer_inner = (decltype(GC_register_finalizer_inner))GC_register_finalizer_inner_match.address(); } diff --git a/mods/src/patches/parts/pinned_ship_sort.cc b/mods/src/patches/parts/pinned_ship_sort.cc index 9230eb517..92f7f90aa 100644 --- a/mods/src/patches/parts/pinned_ship_sort.cc +++ b/mods/src/patches/parts/pinned_ship_sort.cc @@ -89,7 +89,8 @@ ShipTechQuery& TechQuery() void ResolveShipTechQuery() { auto& s = TechQuery(); - if (s.resolved) return; + if (s.resolved) + return; s.resolved = true; auto manager = il2cpp_get_class_helper("Assembly-CSharp", "Digit.Prime.ForbiddenTechs", "ForbiddenTechManager"); @@ -106,22 +107,19 @@ void ResolveShipTechQuery() s.slot_item = entity_slot.GetMethodInfo("get_SlotItemId", 0); s.ship_id = method_contract::Resolve(base_ship.get_cls(), "get_Id", false, "System.Int64", {}); - const bool manager_instance_valid = s.manager_instance && s.manager_instance->methodPointer - && (s.manager_instance->flags & METHOD_ATTRIBUTE_STATIC) - && s.manager_instance->parameters_count == 0 - && method_contract::Type(s.manager_instance->return_type, - "Digit.Prime.ForbiddenTechs.ForbiddenTechManager"); - const bool active_slots_valid = s.active_slots && s.active_slots->methodPointer - && !(s.active_slots->flags & METHOD_ATTRIBUTE_STATIC) - && method_contract::Type(s.active_slots->return_type, "System.Boolean") - && s.active_slots->parameters_count == 2 - && method_contract::Type(s.active_slots->parameters[0], "System.Int64") - && s.active_slots->parameters[1]->byref; + const bool manager_instance_valid = + s.manager_instance && s.manager_instance->methodPointer && (s.manager_instance->flags & METHOD_ATTRIBUTE_STATIC) + && s.manager_instance->parameters_count == 0 + && method_contract::Type(s.manager_instance->return_type, "Digit.Prime.ForbiddenTechs.ForbiddenTechManager"); + const bool active_slots_valid = + s.active_slots && s.active_slots->methodPointer && !(s.active_slots->flags & METHOD_ATTRIBUTE_STATIC) + && method_contract::Type(s.active_slots->return_type, "System.Boolean") && s.active_slots->parameters_count == 2 + && method_contract::Type(s.active_slots->parameters[0], "System.Int64") && s.active_slots->parameters[1]->byref; const bool slot_spec_valid = s.slot_spec != nullptr; const bool slot_item_valid = s.slot_item && s.slot_item->methodPointer && !(s.slot_item->flags & METHOD_ATTRIBUTE_STATIC) && s.slot_item->parameters_count == 0 && s.slot_item->return_type->type == IL2CPP_TYPE_GENERICINST; - const bool ship_id_valid = s.ship_id != nullptr; + const bool ship_id_valid = s.ship_id != nullptr; s.valid = manager_instance_valid && active_slots_valid && slot_spec_valid && slot_item_valid && ship_id_valid; if (!s.valid) { @@ -135,52 +133,64 @@ bool HasAppliedTech(FleetPlayerData* ship) { ResolveShipTechQuery(); auto& s = TechQuery(); - if (!ship || !s.valid) return false; + if (!ship || !s.valid) + return false; auto* ship_model = ship->__get_Ship(); - if (!ship_model) return false; + if (!ship_model) + return false; Il2CppException* exc = nullptr; auto* boxed_id = il2cpp_runtime_invoke(s.ship_id, ship_model, nullptr, &exc); - if (exc || !boxed_id) return false; + if (exc || !boxed_id) + return false; int64_t ship_id = *static_cast(il2cpp_object_unbox(boxed_id)); exc = nullptr; auto* manager = il2cpp_runtime_invoke(s.manager_instance, nullptr, nullptr, &exc); - if (exc || !manager) return false; + if (exc || !manager) + return false; Il2CppObject* slots = nullptr; void* slot_args[2] = {&ship_id, &slots}; exc = nullptr; auto* found = il2cpp_runtime_invoke(s.active_slots, manager, slot_args, &exc); - if (exc || !found || !*static_cast(il2cpp_object_unbox(found)) || !slots) return false; + if (exc || !found || !*static_cast(il2cpp_object_unbox(found)) || !slots) + return false; auto* slots_class = il2cpp_object_get_class(slots); - if (!slots_class) return false; + if (!slots_class) + return false; auto* get_count = il2cpp_class_get_method_from_name(slots_class, "get_Count", 0); auto* get_item = il2cpp_class_get_method_from_name(slots_class, "get_Item", 1); - if (!get_count || !get_item) return false; + if (!get_count || !get_item) + return false; - exc = nullptr; - auto* count_obj = il2cpp_runtime_invoke(get_count, slots, nullptr, &exc); - if (exc || !count_obj) return false; + exc = nullptr; + auto* count_obj = il2cpp_runtime_invoke(get_count, slots, nullptr, &exc); + if (exc || !count_obj) + return false; const auto count = *static_cast(il2cpp_object_unbox(count_obj)); for (int32_t i = 0; i < count; ++i) { exc = nullptr; void* idx[1] = {&i}; auto* slot = il2cpp_runtime_invoke(get_item, slots, idx, &exc); - if (exc || !slot) continue; + if (exc || !slot) + continue; exc = nullptr; auto* spec_obj = il2cpp_runtime_invoke(s.slot_spec, slot, nullptr, &exc); - if (exc || !spec_obj) continue; + if (exc || !spec_obj) + continue; const auto spec_id = *static_cast(il2cpp_object_unbox(spec_obj)); - if (spec_id != kForbiddenTechSlotSpecId && spec_id != kChaosTechSlotSpecId) continue; + if (spec_id != kForbiddenTechSlotSpecId && spec_id != kChaosTechSlotSpecId) + continue; exc = nullptr; auto* item = il2cpp_runtime_invoke(s.slot_item, slot, nullptr, &exc); - if (!exc && item) return true; // non-null Nullable: a tech is equipped + if (!exc && item) + return true; // non-null Nullable: a tech is equipped } return false; } @@ -205,7 +215,8 @@ struct ShipEntry { ShipEntry BuildShipEntry(void* item) { auto* ship = reinterpret_cast(item); - if (!ship) return ShipEntry{item, {}, 0, 0, -1}; + if (!ship) + return ShipEntry{item, {}, 0, 0, -1}; return ShipEntry{item, ShipNameMatch::DisplayWords(ship), ship->Level, ship->HullId, -1}; } diff --git a/mods/src/prime/FleetPlayerData.h b/mods/src/prime/FleetPlayerData.h index 05a029681..0a020be96 100644 --- a/mods/src/prime/FleetPlayerData.h +++ b/mods/src/prime/FleetPlayerData.h @@ -53,8 +53,8 @@ struct FleetPlayerData { __declspec(property(get = __get_Address)) void* Address; __declspec(property(get = __get_Level)) int64_t Level; __declspec(property(get = __get_HasShip)) bool HasShip; - __declspec(property(get = __get_HullId)) int64_t HullId; - __declspec(property(get = __get_Ship)) ::Ship* Ship; + __declspec(property(get = __get_HullId)) int64_t HullId; + __declspec(property(get = __get_Ship)) ::Ship* Ship; private: static IL2CppClassHelper& get_class_helper() diff --git a/tests/shortcut_hint_cache.cc b/tests/shortcut_hint_cache.cc index f91372de5..affb21e10 100644 --- a/tests/shortcut_hint_cache.cc +++ b/tests/shortcut_hint_cache.cc @@ -1,7 +1,7 @@ // Link against the built mods library. Only Unity-dependent Key operations are stubbed; // MapKey parsing, ModifierKey parsing, binding detection, and hint caching are production code. -#include "patches/mapkey.h" #include "defaultconfig.h" +#include "patches/mapkey.h" #include "patches/ship_shortcut_hint.h" #include @@ -44,7 +44,7 @@ int main() { // A locked dock F leaves native G at the sixth selectable position (the reported 1,2,3,4,5,7 layout). const std::array gapped_fleets{0, 1, 2, 3, 4, 6}; - auto ship_hint = [&](GameFunction action) { + auto ship_hint = [&](GameFunction action) { return ShipShortcutHintAction(action, gapped_fleets.size(), [&](std::size_t i) { return gapped_fleets[i]; }); }; Check(ship_hint(GameFunction::SelectShip7) == GameFunction::SelectShip6, @@ -60,7 +60,7 @@ int main() "Contiguous eight-dock hints must keep their original shortcuts"); } const std::array reordered_fleets{7, 0, 2, -1, 6, 4, 1, 3, 5}; - auto reordered_hint = [&](GameFunction action) { + auto reordered_hint = [&](GameFunction action) { return ShipShortcutHintAction(action, reordered_fleets.size(), [&](std::size_t i) { return reordered_fleets[i]; }); }; Check(reordered_hint(GameFunction::SelectShip8) == GameFunction::SelectShip1, "Ship hints must follow bar order"); diff --git a/tests/spud_relocation.cc b/tests/spud_relocation.cc index 4c0ba3f0a..8b8e41f9e 100644 --- a/tests/spud_relocation.cc +++ b/tests/spud_relocation.cc @@ -11,60 +11,87 @@ #include #endif -// Match the gallery constructor's sub/cmp/jne/lea/call prologue. Both the -// RIP-relative cmp and lea expand in the trampoline before the relative call. -// The helper increments a counter, so both branch paths and call-through are -// checked by executing the installed detour, without a game installation. +// Match the gallery constructor's sub/cmp/jne/lea/call prologue: a RIP-relative +// cmp and lea ahead of a relative call. When both expanded in the trampoline, +// older SPUD misplaced the branch targets after them. The helper increments a +// counter, so both branch paths and call-through are checked by executing the +// installed detour, without a game installation. int main() { - constexpr size_t size = 4096; #if _WIN32 - auto* code = static_cast(VirtualAlloc(nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)); + SYSTEM_INFO info{}; + GetSystemInfo(&info); + const size_t size = info.dwPageSize; + auto* code = static_cast(VirtualAlloc(nullptr, size * 2, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)); #else - auto* code = static_cast(mmap(nullptr, size, PROT_READ | PROT_WRITE | PROT_EXEC, - MAP_PRIVATE | MAP_ANON, -1, 0)); - if (code == MAP_FAILED) code = nullptr; + const auto page_size = sysconf(_SC_PAGESIZE); + if (page_size <= 0) + return 1; + const auto size = static_cast(page_size); + auto* code = static_cast(mmap(nullptr, size * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0)); + if (code == MAP_FAILED) + code = nullptr; #endif - if (!code) return 1; + if (!code) + return 1; - const uint8_t body[] = { - 0x48, 0x83, 0xec, 0x28, // sub rsp, 40 - 0x80, 0x3d, 0x75, 0x00, 0x00, 0x00, 0, // cmp byte [code+128], 0 - 0x75, 0x18, // jne code+37 - 0x48, 0x8d, 0x0d, 0x70, 0, 0, 0, // lea rcx, [code+132] - 0xe8, 0x27, 0, 0, 0, // call code+64 - 0x8b, 0x05, 0x65, 0, 0, 0, // mov eax, [code+132] - 0x48, 0x83, 0xc4, 0x28, // add rsp, 40 - 0xc3, 0x90, // ret; nop - 0x8b, 0x05, 0x59, 0, 0, 0, // mov eax, [code+132] - 0x48, 0x83, 0xc4, 0x28, 0xc3}; // add rsp, 40; ret - const uint8_t helper[] = {0xff, 0x05, 0x3e, 0, 0, 0, 0xc3}; // inc dword [code+132]; ret + const uint8_t body[] = {0x48, 0x83, 0xec, 0x28, // sub rsp, 40 + 0x80, 0x3d, 0x00, 0x00, 0x00, 0x00, 0, // cmp byte [data], 0 + 0x75, 0x18, // jne code+37 + 0x48, 0x8d, 0x0d, 0, 0, 0, 0, // lea rcx, [data+4] + 0xe8, 0x27, 0, 0, 0, // call code+64 + 0x8b, 0x05, 0, 0, 0, 0, // mov eax, [data+4] + 0x48, 0x83, 0xc4, 0x28, // add rsp, 40 + 0xc3, 0x90, // ret; nop + 0x8b, 0x05, 0, 0, 0, 0, // mov eax, [data+4] + 0x48, 0x83, 0xc4, 0x28, 0xc3}; // add rsp, 40; ret + const uint8_t helper[] = {0xff, 0x05, 0, 0, 0, 0, 0xc3}; // inc dword [data+4]; ret std::memcpy(code, body, sizeof(body)); std::memcpy(code + 64, helper, sizeof(helper)); + // Rosetta publishes patched code as RX. Keep the flag and counter on their own writable page. + auto* data = code + size; + const auto reference = [&](size_t offset, size_t next, size_t field) { + const auto displacement = static_cast(size + field - next); + std::memcpy(code + offset, &displacement, sizeof(displacement)); + }; + reference(6, 11, 0); + reference(16, 20, 4); + reference(27, 31, 4); + reference(39, 43, 4); + reference(66, 70, 4); #if _WIN32 + DWORD previous = 0; + if (!VirtualProtect(code, size, PAGE_EXECUTE_READ, &previous)) { + VirtualFree(code, 0, MEM_RELEASE); + return 1; + } FlushInstructionCache(GetCurrentProcess(), code, size); #else + if (mprotect(code, size, PROT_READ | PROT_EXEC) != 0) { + munmap(code, size * 2); + return 1; + } __builtin___clear_cache(reinterpret_cast(code), reinterpret_cast(code + size)); #endif using Function = int (*)(void*); - auto function = reinterpret_cast(code); - bool passed = function(nullptr) == 1; + auto function = reinterpret_cast(code); + bool passed = function(nullptr) == 1; { auto hook = spud::detour::create( function, +[](Function original, void* value) { return original(value) + 100; }); - hook.install(); - passed = passed && function(nullptr) == 102; - code[128] = 1; - passed = passed && function(nullptr) == 102; - code[128] = 0; - passed = passed && function(nullptr) == 103; + passed = passed && hook.try_install() == spud::detour_error::kOk; + passed = passed && function(nullptr) == 102; + data[0] = 1; + passed = passed && function(nullptr) == 102; + data[0] = 0; + passed = passed && function(nullptr) == 103; } passed = passed && function(nullptr) == 4; #if _WIN32 VirtualFree(code, 0, MEM_RELEASE); #else - munmap(code, size); + munmap(code, size * 2); #endif std::puts(passed ? "SPUD multi-relocation call/branch regression passed" : "SPUD relocation regression FAILED"); return passed ? 0 : 1; diff --git a/xmake-packages/packages/s/spud/spud-src/CMakeLists.txt b/xmake-packages/packages/s/spud/spud-src/CMakeLists.txt index 2c4aacecb..9f67fabb8 100644 --- a/xmake-packages/packages/s/spud/spud-src/CMakeLists.txt +++ b/xmake-packages/packages/s/spud/spud-src/CMakeLists.txt @@ -1,113 +1,34 @@ cmake_minimum_required(VERSION 3.23 FATAL_ERROR) + if(WIN32) - project(spud C CXX ASM_MASM) + project(spud VERSION 0.1.0 LANGUAGES C CXX ASM_MASM) else() - project(spud C CXX ASM) + project(spud VERSION 0.1.0 LANGUAGES C CXX ASM) endif(WIN32) -set(CMAKE_ASM_FLAGS "${CFLAGS} -x assembler-with-cpp") -include(CTest) -include(FetchContent) -set(CMAKE_CXX_STANDARD 20) -set(CXX_STANDARD_REQUIRED ON) -set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE INTERNAL "") -set(ASMJIT_STATIC TRUE) -option(SPUD_AARCH64_SUPPORT "" ON) -if(SPUD_AARCH64_SUPPORT) - set(CAPSTONE_BUILD_STATIC ON) - set(CAPSTONE_BUILD_STATIC_RUNTIME ON) - set(CAPSTONE_BUILD_MACOS_THIN OFF) - set(CAPSTONE_ARCHITECTURE_DEFAULT OFF) - set(CAPSTONE_AARCH64_SUPPORT ON) -endif() -option(SPUD_COMPARE_LIBS "" OFF) -option(SPUD_DETOUR_TRACING "" OFF) -if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) - set(__SPUD_IS_SELF ON) -else() - set(__SPUD_IS_SELF OFF) -endif() -option(SPUD_BUILD_TESTS "" ${__SPUD_IS_SELF}) -option(SPUD_NO_LTO "" OFF) -if(NOT TARGET Zydis) - option(ZYDIS_BUILD_TOOLS "" OFF) - option(ZYDIS_BUILD_EXAMPLES "" OFF) - option(ZYDIS_BUILD_TOOLS "" OFF) - FetchContent_Declare( - zydis - GIT_REPOSITORY https://github.com/zyantific/zydis.git - GIT_TAG v4.1.0 - ) - FetchContent_MakeAvailable(zydis) -endif() -if(SPUD_AARCH64_SUPPORT) - if(NOT TARGET capstone) - FetchContent_Declare( - capstone - GIT_REPOSITORY https://github.com/capstone-engine/capstone.git - GIT_TAG 6.0.0-Alpha5 - ) - FetchContent_MakeAvailable(capstone) - endif() -endif() -if(NOT TARGET asmjit) - FetchContent_Declare( - asmjit - GIT_REPOSITORY https://github.com/asmjit/asmjit.git - GIT_TAG 0b3aec39d18a98a87449f031a469b60aedae1a9b - ) - FetchContent_MakeAvailable(asmjit) -endif() -add_library("spud" STATIC) -target_link_libraries("spud" PRIVATE "Zydis") -target_link_libraries("spud" PRIVATE "asmjit") -if(SPUD_AARCH64_SUPPORT) - target_link_libraries("spud" PRIVATE "capstone") - add_dependencies("spud" "Zydis" "asmjit" "capstone") -else() - add_dependencies("spud" "Zydis" "asmjit") -endif() -if(MSVC) - target_compile_options( - spud - PRIVATE - # "/WX" - "/permissive-" - "/W4" - ) + +# Only the top-level project owns BUILD_TESTING and the CTest dashboard targets. +if(PROJECT_IS_TOP_LEVEL) + include(CTest) endif() -if(SPUD_DETOUR_TRACING) - target_compile_definitions(spud PUBLIC SPUD_DETOUR_TRACING=1) +include(FetchContent) +# Timestamps of extracted dependency tarballs are the extraction time, so a +# changed pin rebuilds. +if(POLICY CMP0135) + cmake_policy(SET CMP0135 NEW) endif() -if(SPUD_AARCH64_SUPPORT) - target_compile_definitions(spud PUBLIC SPUD_AARCH64_SUPPORT=1) +include(GNUInstallDirs) +include(CMakePackageConfigHelpers) + +if(PROJECT_IS_TOP_LEVEL) + set(CMAKE_CXX_STANDARD 20) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE INTERNAL "") endif() -target_compile_definitions(spud PRIVATE NOMINMAX=1) -if(NOT SPUD_NO_LTO) - include(CheckIPOSupported) - check_ipo_supported(RESULT lto_supported OUTPUT error) - if(lto_supported) - if(NOT "${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") - set_property(TARGET spud PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE) - endif() - endif() -endif() -set(SPUD_PUBLIC_HDRS - "include/spud/arch.h" - "include/spud/detour.h" - "include/spud/details/function_traits.h" - "include/spud/utils.h" - "include/spud/signature.h" - "include/spud/memory/protection.h" -) -target_sources( - "spud" - PUBLIC - FILE_SET public_headers - TYPE HEADERS - BASE_DIRS "include" - FILES ${SPUD_PUBLIC_HDRS} -) -target_include_directories("spud" PUBLIC "include" PRIVATE "src") + +option(SPUD_BUILD_TESTS "Build Catch2 unit tests" ${PROJECT_IS_TOP_LEVEL}) +option(SPUD_BUILD_BENCHMARKS "Build Catch2 benchmarks" ${PROJECT_IS_TOP_LEVEL}) +option(SPUD_BUILD_EXAMPLES "Build example programs" ${PROJECT_IS_TOP_LEVEL}) + if( CMAKE_SYSTEM_PROCESSOR MATCHES "arm" OR CMAKE_SYSTEM_PROCESSOR MATCHES "aarch" @@ -119,116 +40,195 @@ if( set(ARCH "arm") endif() elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^mips.*") - if(CMAKE_SIZEOF_VOID_P EQUAL 8) - set(ARCH "mips64el") - else() - set(ARCH "mipsel") - endif() + message(FATAL_ERROR "MIPS is not a supported Spud architecture") elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^ppc.*") - if(CMAKE_SIZEOF_VOID_P EQUAL 8) - set(ARCH "ppc64le") - else() - message(FATAL_ERROR "Architecture is not supported") - endif() + message(FATAL_ERROR "PowerPC is not a supported Spud architecture") else() if(CMAKE_SIZEOF_VOID_P EQUAL 8) set(ARCH "x86_64") else() - set(ARCH "x86") + message(FATAL_ERROR "32-bit x86 is not a supported Spud architecture") endif() endif() -# Cross compiling on macOS. The cross compiling architecture should override -# auto-detected system architecture settings. + if(CMAKE_OSX_ARCHITECTURES) - if(CMAKE_OSX_ARCHITECTURES MATCHES "arm64") + list(LENGTH CMAKE_OSX_ARCHITECTURES _spud_osx_arch_count) + if(_spud_osx_arch_count GREATER 1) + message( + FATAL_ERROR + "Spud does not support universal binaries. Specify one of arm64, x86_64." + ) + endif() + if(CMAKE_OSX_ARCHITECTURES STREQUAL "arm64") set(ARCH "arm64") - elseif(CMAKE_OSX_ARCHITECTURES MATCHES "x86_64") + elseif(CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64") set(ARCH "x86_64") else() message( FATAL_ERROR - "Architecture ${CMAKE_OSX_ARCHITECTURES} is not " - "supported. Only one architecture (arm64, x86_64)" - "could be specified at build time." + "Architecture ${CMAKE_OSX_ARCHITECTURES} is not supported. " + "Use arm64 or x86_64." ) endif() endif() -if(WIN32) - if(ARCH MATCHES "arm64") - file(GLOB_RECURSE ASM_SRCS src/detour/aarch64/*.asm) - else() - file(GLOB_RECURSE ASM_SRCS src/detour/x86_64/*.asm) - endif() + +if(NOT ARCH STREQUAL "x86_64" AND NOT ARCH STREQUAL "arm64") + message(FATAL_ERROR "Architecture ${ARCH} is not supported") +endif() +if(WIN32 AND ARCH STREQUAL "arm64") + message(FATAL_ERROR "Windows ARM64 is not a supported Spud target") +endif() + +# ARM64 hosts need the Capstone-backed backend. x86-64 builds only compile it +# when asked, or for the unit tests, which check ARM64 encodings on every host. +if(ARCH STREQUAL "arm64" OR SPUD_BUILD_TESTS) + set(SPUD_AARCH64_DEFAULT ON) else() - if(ARCH MATCHES "arm64") - file(GLOB_RECURSE ASM_SRCS src/detour/aarch64/*.S) + set(SPUD_AARCH64_DEFAULT OFF) +endif() + +# Generated from cmake/spud.manifest.json by `zig build gen-cmake`. +include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/generated/options.cmake") +include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/generated/deps.cmake") +include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/generated/sources.cmake") + +# Deprecated spelling, kept so existing -DSPUD_NO_INSTALL=ON builds still work. +if(SPUD_NO_INSTALL) + message(DEPRECATION "SPUD_NO_INSTALL is deprecated; use SPUD_INSTALL=OFF") + set(SPUD_INSTALL OFF) +endif() + + +spud_provide_zydis() +spud_provide_capstone() + +add_library("spud" STATIC) +add_library(spud::spud ALIAS spud) +target_compile_features("spud" PUBLIC cxx_std_20) +target_link_libraries("spud" PRIVATE "Zydis") +if(WIN32) + target_link_libraries("spud" PRIVATE "psapi") +endif() +if(SPUD_AARCH64_SUPPORT) + target_link_libraries("spud" PRIVATE "capstone") +endif() + +# Warnings for spud's own targets, never for vendored code. Errors are for +# CI only, so a newer compiler's new warning cannot break a consumer build. +function(spud_warnings target) + if(MSVC) + target_compile_options(${target} PRIVATE "/permissive-" "/W4") + if(SPUD_WARNINGS_AS_ERRORS) + target_compile_options(${target} PRIVATE "/WX") + endif() else() - file(GLOB_RECURSE ASM_SRCS src/detour/x86_64/*.S) + target_compile_options(${target} PRIVATE -Wall -Wextra) + if(SPUD_WARNINGS_AS_ERRORS) + target_compile_options(${target} PRIVATE -Werror) + endif() endif() -endif(WIN32) -if(ARCH MATCHES "arm64") - file(GLOB_RECURSE SPUD_NEON_SRCS src/*_neon.cc) -else() - file(GLOB_RECURSE SPUD_SSE_SRCS src/*_sse.cc) - file(GLOB_RECURSE SPUD_AVX2_SRCS src/*_avx2.cc) +endfunction() +spud_warnings(spud) +if(WIN32 AND CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND NOT MSVC) + target_compile_options(spud PUBLIC $<$:-fms-extensions>) endif() -if(MSVC) - set_source_files_properties( - ${SPUD_AVX2_SRCS} - PROPERTIES COMPILE_FLAGS "/arch:AVX2" - ) - if("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") - set_source_files_properties( - ${SPUD_SSE_SRCS} - PROPERTIES COMPILE_FLAGS "-msse4.2" - ) - endif() + +if(SPUD_DETOUR_TRACING) + target_compile_definitions(spud PUBLIC SPUD_DETOUR_TRACING=1) +endif() + +target_compile_definitions(spud PUBLIC + SPUD_UNWIND_SUPPORT=$) + +# No public header reads it; the tests that include src/ add it themselves. +if(SPUD_AARCH64_SUPPORT) + target_compile_definitions(spud PRIVATE SPUD_AARCH64_SUPPORT=1) +endif() + +target_compile_definitions(spud PRIVATE NOMINMAX=1) + +# No forced LTO. GCC's IPO objects are LTO-only (-fno-fat-lto-objects), so an +# installed libspud.a would only link with the same GCC and plugin. Set +# CMAKE_INTERPROCEDURAL_OPTIMIZATION in the consuming build to opt in. + +target_sources( + "spud" + PUBLIC + FILE_SET public_headers + TYPE HEADERS + BASE_DIRS "include" + FILES ${SPUD_PUBLIC_HDRS} +) +target_include_directories( + "spud" + PUBLIC + $ + $ + PRIVATE "src" +) + +if(ARCH STREQUAL "arm64" AND NOT SPUD_AARCH64_SUPPORT) + message(FATAL_ERROR "ARM64 hosts require SPUD_AARCH64_SUPPORT=ON") +endif() + +# GCC and Clang read the hook context register with inline assembly; MSVC +# has no x64 inline assembly and calls this helper instead. +# CMake's MASM language only claims .asm files, so the .masm helper needs an +# explicit language or it is silently left out of the library. clang-cl +# takes the inline path like Clang everywhere else. +set(ASM_SRCS "") +if(MSVC AND NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set(ASM_SRCS ${SPUD_ASM_X86_64_MASM}) + set_source_files_properties(${ASM_SRCS} PROPERTIES LANGUAGE ASM_MASM) +endif() + +if(ARCH STREQUAL "arm64") + set(SPUD_NEON_SRCS ${SPUD_SIMD_NEON_SRCS}) + set(SPUD_SSE_SRCS "") + set(SPUD_AVX2_SRCS "") else() - set_source_files_properties( - ${SPUD_AVX2_SRCS} - PROPERTIES COMPILE_FLAGS "-mavx2 -mfma -msse4.2" - ) - set_source_files_properties( - ${SPUD_SSE_SRCS} - PROPERTIES COMPILE_FLAGS "-msse4.2" - ) + set(SPUD_SSE_SRCS ${SPUD_SIMD_SSE_SRCS}) + set(SPUD_AVX2_SRCS ${SPUD_SIMD_AVX2_SRCS}) + set(SPUD_NEON_SRCS "") endif() + set(SPUD_AARCH64_FILES "") if(SPUD_AARCH64_SUPPORT) - list( - APPEND - SPUD_AARCH64_FILES - "src/detour/aarch64/aarch64.cc" - "src/detour/aarch64/aarch64.h" - "src/detour/aarch64/relocators.cc" - "src/detour/aarch64/relocators.h" - ) + set(SPUD_AARCH64_FILES ${SPUD_AARCH64_BACKEND_SRCS}) endif() + target_sources( "spud" PRIVATE - "src/utils.cc" - "src/zydis_utils.cc" - "src/zydis_utils.h" - "src/detour/x86_64/x86_64.cc" - "src/detour/x86_64/x86_64.h" - "src/detour/x86_64/relocators.cc" - "src/detour/x86_64/relocators.h" - "src/detour/detour.cc" - "src/detour/detour_impl.h" - "src/detour/prologue.cc" - "src/detour/fwd.h" - "src/detour/remapper.cc" - "src/detour/remapper.h" - "src/memory/protection.cc" - "src/signature/signature.cc" + ${SPUD_COMMON_SRCS} "${SPUD_AARCH64_FILES}" "${ASM_SRCS}" "${SPUD_SSE_SRCS}" "${SPUD_AVX2_SRCS}" "${SPUD_NEON_SRCS}" ) -if(NOT SPUD_NO_INSTALL) + +if(SPUD_INSTALL) + if(SPUD_UNWIND_SUPPORT) + set(SPUD_UNWIND_SUPPORT_VALUE 1) + else() + set(SPUD_UNWIND_SUPPORT_VALUE 0) + endif() + if(SPUD_DETOUR_TRACING) + set(SPUD_DETOUR_TRACING_VALUE 1) + else() + set(SPUD_DETOUR_TRACING_VALUE 0) + endif() + set(SPUD_STATIC_LIB_FILENAME + "${CMAKE_STATIC_LIBRARY_PREFIX}spud${CMAKE_STATIC_LIBRARY_SUFFIX}" + ) + set(SPUD_ZYDIS_FILENAME + "${CMAKE_STATIC_LIBRARY_PREFIX}Zydis${CMAKE_STATIC_LIBRARY_SUFFIX}" + ) + set(SPUD_ZYCORE_FILENAME + "${CMAKE_STATIC_LIBRARY_PREFIX}Zycore${CMAKE_STATIC_LIBRARY_SUFFIX}" + ) + install( TARGETS ${PROJECT_NAME} FILE_SET public_headers @@ -237,105 +237,434 @@ if(NOT SPUD_NO_INSTALL) LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" INCLUDES DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" ) + install( + FILES + "$" + "$" + DESTINATION "${CMAKE_INSTALL_LIBDIR}" + ) + configure_package_config_file( + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/spudConfig.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/spudConfig.cmake" + INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/spud" + PATH_VARS CMAKE_INSTALL_INCLUDEDIR CMAKE_INSTALL_LIBDIR + ) + write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/spudConfigVersion.cmake" + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion + ) + install( + FILES + "${CMAKE_CURRENT_BINARY_DIR}/spudConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/spudConfigVersion.cmake" + DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/spud" + ) +endif() + +if(SPUD_BUILD_TESTS OR SPUD_BUILD_BENCHMARKS) + spud_provide_catch2() + # The signature scan corpus, compiled once for both executables. + add_library(spud_scan_corpus OBJECT "tests/test_bin.cc") + target_link_libraries(spud_scan_corpus PRIVATE spud) +endif() + +if(SPUD_BUILD_BENCHMARKS) + add_executable("spud.benchmark" benchmark/test.cc) + target_link_libraries( + "spud.benchmark" + PRIVATE "Catch2" "Catch2::Catch2WithMain" "spud" spud_scan_corpus + ) + target_include_directories("spud.benchmark" PRIVATE "benchmark" "tests") + spud_warnings(spud.benchmark) endif() + if(SPUD_BUILD_TESTS) set(TEST_SRCS "tests/signature.cc") + file( - GLOB_RECURSE TEST_DETOUR_SHARED_SRCS + GLOB_RECURSE TEST_DETOUR_SHARED_SRCS CONFIGURE_DEPENDS tests/detour/shared/*.cc tests/detour/shared/*.h ) list(APPEND TEST_SRCS ${TEST_DETOUR_SHARED_SRCS}) + if(NOT MSVC) + set_source_files_properties( + "tests/detour/shared/instrumented.cc" + PROPERTIES COMPILE_OPTIONS "-finstrument-functions") + endif() + if(ARCH MATCHES "arm64") file( - GLOB_RECURSE TEST_DETOUR_SRCS + GLOB_RECURSE TEST_DETOUR_SRCS CONFIGURE_DEPENDS tests/detour/arm64/*.cc tests/detour/arm64/*.h ) else() file( - GLOB_RECURSE TEST_DETOUR_SRCS + GLOB_RECURSE TEST_DETOUR_SRCS CONFIGURE_DEPENDS tests/detour/x86_64/*.cc tests/detour/x86_64/*.h ) endif() - if(WIN32) - if(ARCH MATCHES "arm64") - file(GLOB_RECURSE TEST_ASM_SRCS tests/detour/arm64/*.asm) - else() - file(GLOB_RECURSE TEST_ASM_SRCS tests/detour/x86_64/*.asm) - endif() - else(WIN32) + + # clang-cl assembles the GAS form; MSVC needs the MASM twin. + if(WIN32 AND NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang") + file(GLOB_RECURSE TEST_ASM_SRCS CONFIGURE_DEPENDS tests/detour/x86_64/*.asm) + else() if(ARCH MATCHES "arm64") - file(GLOB_RECURSE TEST_ASM_SRCS tests/detour/arm64/*.S) + file(GLOB_RECURSE TEST_ASM_SRCS CONFIGURE_DEPENDS tests/detour/arm64/*.S) else() - file(GLOB_RECURSE TEST_ASM_SRCS tests/detour/x86_64/*.S) + file(GLOB_RECURSE TEST_ASM_SRCS CONFIGURE_DEPENDS tests/detour/x86_64/*.S) endif() - endif(WIN32) + endif() + list(APPEND TEST_SRCS ${TEST_DETOUR_SRCS}) list(APPEND TEST_SRCS ${TEST_ASM_SRCS}) - file(GLOB_RECURSE BENCHMARK_SRCS benchmark/*.cc) - if(NOT TARGET Catch2) - FetchContent_Declare( - Catch2 - GIT_REPOSITORY https://github.com/catchorg/Catch2.git - GIT_TAG v3.8.0 - ) - FetchContent_MakeAvailable(Catch2) - endif() - if(SPUD_COMPARE_LIBS) - add_compile_definitions("SPUD_COMPARE_LIBS=1") - FetchContent_Declare( - lime - GIT_REPOSITORY "https://github.com/Curve/lime" - ) - FetchContent_MakeAvailable(lime) - FetchContent_Declare( - minhook - GIT_REPOSITORY "https://github.com/TsudaKageyu/minhook" - ) - FetchContent_MakeAvailable(minhook) - set(ASMJIT_EXTERNAL ON) - set(POLYHOOK_USE_EXTERNAL_ZYDIS ON) - set(POLYHOOK_USE_EXTERNAL_ASMJIT ON) - FetchContent_Declare( - PolyHook2 - GIT_REPOSITORY "https://github.com/stevemk14ebr/PolyHook_2_0" - ) - FetchContent_MakeAvailable(PolyHook2) - endif() + add_executable( "spud.test" ${TEST_SRCS} "tests/test_util.h" - "tests/signature.cc" + "tests/test_util.cc" + ) + add_library(spud_import_probe SHARED "tests/detour/shared/import_probe.c") + set_target_properties( + spud_import_probe + PROPERTIES + WINDOWS_EXPORT_ALL_SYMBOLS ON + LIBRARY_OUTPUT_DIRECTORY "$" + RUNTIME_OUTPUT_DIRECTORY "$" ) - target_include_directories("spud.test" PRIVATE "tests") + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_library(spud_import_lazy SHARED "tests/detour/shared/import_lazy.c") + target_link_libraries(spud_import_lazy PRIVATE spud_import_probe) + target_link_options(spud_import_lazy PRIVATE "LINKER:-z,lazy") + set_target_properties( + spud_import_lazy + PROPERTIES LIBRARY_OUTPUT_DIRECTORY "$" + ) + target_link_libraries("spud.test" PRIVATE spud_import_lazy) + target_compile_definitions("spud.test" PRIVATE SPUD_TEST_LAZY_IMPORT=1) + endif() + target_include_directories("spud.test" PRIVATE "tests" "src") + spud_warnings(spud.test) target_link_libraries( "spud.test" - PRIVATE "Catch2" "Catch2::Catch2WithMain" "spud" + PRIVATE + "Catch2" + "Catch2::Catch2WithMain" + "spud" + "Zydis" + spud_import_probe + spud_scan_corpus ) - add_dependencies("spud.test" "Catch2" "spud") - add_executable("spud.benchmark" ${BENCHMARK_SRCS}) - target_include_directories("spud.benchmark" PRIVATE "benchmark") - if(SPUD_COMPARE_LIBS) - target_link_libraries( - "spud.benchmark" - PRIVATE - "Catch2" - "Catch2::Catch2WithMain" - "spud" - "lime" - "minhook" - "PolyHook2" - ) + if(SPUD_AARCH64_SUPPORT) + target_link_libraries("spud.test" PRIVATE "capstone") + endif() + + add_test(NAME spud.test COMMAND spud.test) + # A hung patch should fail the run, not stall CI until its global limit. + # The suite takes well under a minute on a desktop; 300 s leaves room + # for slow shared runners. + set_tests_properties(spud.test PROPERTIES TIMEOUT 300) + # Inline overflow checks that trap without a runtime; -O2 keeps clang's + # trap block after the return, the shape the prologue scan must end on. + if(NOT ARCH MATCHES "arm64" AND NOT WIN32 AND + CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + set_source_files_properties(tests/detour/x86_64/sanitized/sanitized.cc + PROPERTIES COMPILE_OPTIONS + "-O2;-fsanitize=signed-integer-overflow;-fsanitize-trap=signed-integer-overflow") + endif() + # A dense switch becomes a register-dispatched jump table at -O2. MSVC + # debug builds cannot mix /O2 with /RTC1, so they test the plain form. + if(NOT ARCH MATCHES "arm64" AND NOT MSVC) + set_source_files_properties(tests/detour/x86_64/optimized/switch.cc + PROPERTIES COMPILE_OPTIONS "-O2") + endif() + # GCC moves catch handlers into .cold parts at -O2. + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set_source_files_properties(tests/detour/shared/cold_landing.cc + PROPERTIES COMPILE_OPTIONS "-O2") + endif() + if(NOT WIN32) + add_executable(spud.window_allocations tests/detour/audit/window_allocations.cc) + target_include_directories(spud.window_allocations PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests ${CMAKE_CURRENT_SOURCE_DIR}/src) + target_link_libraries(spud.window_allocations PRIVATE spud) + add_test(NAME spud.window_allocations COMMAND spud.window_allocations) + endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_library(spud.source_module_library MODULE tests/detour/audit/source_module_library.c) + add_executable(spud.source_module tests/detour/audit/source_module.cc) + target_link_libraries(spud.source_module PRIVATE spud ${CMAKE_DL_LIBS}) + add_test(NAME spud.source_module COMMAND spud.source_module $) + add_test(NAME spud.source_module_disabled COMMAND spud.source_module $ disabled) + # A non-PIE executable takes a function's address as its own PLT + # entry, and -z lazy leaves the GOT slot behind it unbound. + add_executable(spud.lazy_import tests/detour/audit/lazy_import.cc) + target_include_directories(spud.lazy_import PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests) + target_compile_options(spud.lazy_import PRIVATE -fno-pie) + target_link_options(spud.lazy_import PRIVATE -no-pie "LINKER:-z,lazy") + target_link_libraries(spud.lazy_import PRIVATE spud spud_import_probe) + add_test(NAME spud.lazy_import COMMAND spud.lazy_import) + endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND ARCH STREQUAL "x86_64") + # Hand-assembled relocation hazards from the relocation audit, one + # process per case and mode because a bad trampoline faults. `near` + # emits a trampoline next to the code, `hook` installs a detour. + # Accepted cases must return the original result; refused ones must + # be refused (limits.md lists why), not run. + add_executable(spud.relocation_probe tests/detour/audit/probe.cc) + target_include_directories(spud.relocation_probe PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + target_link_libraries(spud.relocation_probe PRIVATE spud Zydis) + if(SPUD_AARCH64_SUPPORT) + target_link_libraries(spud.relocation_probe PRIVATE capstone) + endif() + # `near` writes trampoline dumps to the working directory. + set(_spud_probe_dir "${CMAKE_CURRENT_BINARY_DIR}/relocation-probe") + file(MAKE_DIRECTORY "${_spud_probe_dir}") + set(_spud_probe_accepted r11_indirect gs_push gs_call ah_load push16 + rsp_store eip_load late_self_read backedge_1100 backedge_padding + pop_slot gs_self) + # PC discovery (call_pc, x87_pc), a segmented indirect jump, branches + # into the middle of a copied instruction, an indirect jump back into + # the copy, and register-indexed RIP bit operations. + set(_spud_probe_refused call_pc gs_jmp overlap_branch overlap_rewrite + overlap_movabs backedge_indirect bt_dynamic_self bts_dynamic_self + x87_pc) + foreach(_case ${_spud_probe_accepted} ${_spud_probe_refused}) + foreach(_mode near hook) + # gs_self reads its own first byte through a nonzero GS base. + # Aliases into the patch through a segment base are outside + # the detour contract, so hooked it reads the jump stub. + if(_case STREQUAL "gs_self" AND _mode STREQUAL "hook") + continue() + endif() + set(_test spud.relocation_probe.${_case}.${_mode}) + add_test(NAME ${_test} + COMMAND spud.relocation_probe ${_case} ${_mode} + WORKING_DIRECTORY "${_spud_probe_dir}") + set_tests_properties(${_test} PROPERTIES TIMEOUT 30) + if(_case IN_LIST _spud_probe_refused) + # Exit code 3 with REFUSED; a run that gets as far as + # printing a result did not refuse. + set_tests_properties(${_test} PROPERTIES + PASS_REGULAR_EXPRESSION "REFUSED" + FAIL_REGULAR_EXPRESSION "actual=") + endif() + endforeach() + endforeach() + endif() + if(SPUD_UNWIND_SUPPORT AND + ((CMAKE_SYSTEM_NAME STREQUAL "Linux" AND (ARCH STREQUAL "x86_64" OR ARCH STREQUAL "arm64")) OR + (APPLE AND ARCH STREQUAL "arm64"))) + add_executable(spud.spill_unwind tests/detour/audit/spill_unwind.cc) + target_include_directories(spud.spill_unwind PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + target_link_libraries(spud.spill_unwind PRIVATE spud Zydis) + if(SPUD_AARCH64_SUPPORT) + target_link_libraries(spud.spill_unwind PRIVATE capstone) + endif() + add_test(NAME spud.spill_unwind COMMAND spud.spill_unwind) + set_tests_properties(spud.spill_unwind PROPERTIES SKIP_RETURN_CODE 77) + endif() + if(WIN32) + add_executable(spud.windows_unwind tests/detour/audit/windows_unwind.cc) + if(MSVC) + target_sources(spud.windows_unwind PRIVATE tests/detour/audit/windows_msvc.cc) + target_compile_definitions(spud.windows_unwind PRIVATE SPUD_MSVC_FIXTURE=1 SPUD_MSVC_GS_FIXTURE=1) + # The fixture deliberately throws through extern "C" entries. + set_source_files_properties(tests/detour/audit/windows_msvc.cc + PROPERTIES COMPILE_OPTIONS "/EHsc-") + endif() + target_include_directories(spud.windows_unwind PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + target_link_libraries(spud.windows_unwind PRIVATE spud Zydis) + if(SPUD_AARCH64_SUPPORT) + target_link_libraries(spud.windows_unwind PRIVATE capstone) + endif() + add_test(NAME spud.windows_unwind COMMAND spud.windows_unwind) + endif() + if(APPLE AND SPUD_UNWIND_SUPPORT) + add_executable(spud.macos_unwind tests/detour/audit/macos_unwind.cc) + target_include_directories(spud.macos_unwind PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + target_link_libraries(spud.macos_unwind PRIVATE spud) + if(SPUD_AARCH64_SUPPORT) + target_link_libraries(spud.macos_unwind PRIVATE capstone) + endif() + add_test(NAME spud.macos_unwind COMMAND spud.macos_unwind) + if(ARCH STREQUAL "x86_64") + add_executable(spud.macos_unwind_fde + tests/detour/audit/macos_unwind.cc + tests/detour/audit/macos_fde.cc) + target_include_directories(spud.macos_unwind_fde PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + target_link_libraries(spud.macos_unwind_fde PRIVATE spud) + if(SPUD_AARCH64_SUPPORT) + target_link_libraries(spud.macos_unwind_fde PRIVATE capstone) + endif() + add_test(NAME spud.macos_unwind_fde COMMAND spud.macos_unwind_fde) + endif() + add_executable(spud.macos_compact tests/detour/audit/macos_compact.cc) + target_include_directories(spud.macos_compact PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + target_link_libraries(spud.macos_compact PRIVATE spud Zydis) + if(SPUD_AARCH64_SUPPORT) + target_link_libraries(spud.macos_compact PRIVATE capstone) + endif() + add_test(NAME spud.macos_compact COMMAND spud.macos_compact) + if(ARCH STREQUAL "x86_64") + add_executable(spud.macos_handler_backedge + tests/detour/audit/macos_unwind.cc) + target_compile_definitions(spud.macos_handler_backedge PRIVATE + SPUD_UNWIND_HANDLER_BACKEDGE=1) + target_include_directories(spud.macos_handler_backedge PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + target_link_libraries(spud.macos_handler_backedge PRIVATE spud) + if(SPUD_AARCH64_SUPPORT) + target_link_libraries(spud.macos_handler_backedge PRIVATE capstone) + endif() + add_test(NAME spud.macos_handler_backedge + COMMAND spud.macos_handler_backedge) + endif() + endif() + + # SPUD_AARCH64_SUPPORT is private to the library, but these programs + # include src/ headers whose layouts depend on it. + if(SPUD_AARCH64_SUPPORT) + foreach(_target spud.test spud.window_allocations spud.relocation_probe + spud.spill_unwind spud.windows_unwind spud.macos_unwind + spud.macos_unwind_fde spud.macos_compact + spud.macos_handler_backedge) + if(TARGET ${_target}) + target_compile_definitions(${_target} PRIVATE SPUD_AARCH64_SUPPORT=1) + endif() + endforeach() + endif() + + if(MSVC) + set(_spud_syntax_only /nologo /std:c++20 /Zs) + set(_spud_header_warnings "") + set(_spud_include_flag /I) + set(_spud_define_flag /D) else() - target_link_libraries( - "spud.benchmark" - PRIVATE "Catch2" "Catch2::Catch2WithMain" "spud" + set(_spud_syntax_only -std=c++20 -fsyntax-only) + # Consumers may build with -Wundef; the headers test macros with + # defined() where arch.h only defines them when true. + set(_spud_header_warnings -Wall -Wextra -Wundef -Werror) + set(_spud_include_flag -I) + set(_spud_define_flag -D) + endif() + + add_test( + NAME spud.tracing_header + COMMAND + ${CMAKE_CXX_COMPILER} ${_spud_syntax_only} + "${_spud_define_flag}SPUD_DETOUR_TRACING=1" + "${_spud_include_flag}${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_CURRENT_SOURCE_DIR}/tests/install/tracing_consumer.cc" + ) + add_test( + NAME spud.tracing_header_order + COMMAND + ${CMAKE_CXX_COMPILER} ${_spud_syntax_only} + "${_spud_define_flag}SPUD_DETOUR_TRACING=1" + "${_spud_include_flag}${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_CURRENT_SOURCE_DIR}/tests/install/tracing_consumer_order.cc" + ) + + add_test( + NAME spud.signature_literal + COMMAND + ${CMAKE_CXX_COMPILER} ${_spud_syntax_only} + "${_spud_include_flag}${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_CURRENT_SOURCE_DIR}/tests/install/signature_literal.cc" + ) + add_test( + NAME spud.signature_literal_rejects + COMMAND + ${CMAKE_CXX_COMPILER} ${_spud_syntax_only} + "${_spud_define_flag}SPUD_BAD_PATTERN=1" + "${_spud_include_flag}${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_CURRENT_SOURCE_DIR}/tests/install/signature_literal.cc" + ) + # Rejected for the right reason: sig() names the bad token in its error. +set_tests_properties(spud.signature_literal_rejects PROPERTIES + PASS_REGULAR_EXPRESSION "signature_has_an_invalid_token") + + if(SPUD_INSTALL) + set(_spud_install_check "${CMAKE_CURRENT_BINARY_DIR}/install-check") + add_test( + NAME spud.install + COMMAND + ${CMAKE_COMMAND} --install ${CMAKE_CURRENT_BINARY_DIR} --prefix + ${_spud_install_check} + ) + add_test( + NAME spud.installed_header + COMMAND + ${CMAKE_CXX_COMPILER} ${_spud_syntax_only} ${_spud_header_warnings} + "${_spud_include_flag}${_spud_install_check}/include" + "${CMAKE_CURRENT_SOURCE_DIR}/tests/install/consumer.cc" + ) + add_test( + NAME spud.installed_tracing_header + COMMAND + ${CMAKE_CXX_COMPILER} ${_spud_syntax_only} + "${_spud_define_flag}SPUD_DETOUR_TRACING=1" + "${_spud_include_flag}${_spud_install_check}/include" + "${CMAKE_CURRENT_SOURCE_DIR}/tests/install/tracing_consumer.cc" + ) + set_tests_properties( + spud.installed_header spud.installed_tracing_header + PROPERTIES DEPENDS spud.install + ) + add_test( + NAME spud.installed_link + COMMAND + ${CMAKE_COMMAND} + -DCMAKE_COMMAND=${CMAKE_COMMAND} + -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} + -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} + -DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM} + "-DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES}" + -DSPUD_PREFIX=${_spud_install_check} + -DCONSUMER_SRC=${CMAKE_CURRENT_SOURCE_DIR}/tests/install/link_consumer + -DCONSUMER_BIN=${CMAKE_CURRENT_BINARY_DIR}/link-consumer-build + -DBUILD_TYPE=$ + -DGENERATOR=${CMAKE_GENERATOR} + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/install/run_link_consumer.cmake + ) + set_tests_properties( + spud.installed_link + PROPERTIES DEPENDS spud.install ) endif() - add_dependencies("spud.benchmark" "Catch2" "spud") - add_test(NAME spud.test COMMAND spud.test) - add_test(NAME spud.benchmark COMMAND spud.benchmark) + + # A consumer built with the other SPUD_DETOUR_TRACING setting must fail + # to link against this libspud, on the handle types the setting moves + # into an inline namespace, rather than run with the wrong layout. + if(NOT MSVC) + if(SPUD_DETOUR_TRACING) + set(_spud_other_tracing 0) + else() + set(_spud_other_tracing 1) + endif() + set(_spud_mismatch_libs $ $ $) + if(NOT APPLE) + list(APPEND _spud_mismatch_libs -pthread) + endif() + add_test( + NAME spud.tracing_mismatch + COMMAND + ${CMAKE_CXX_COMPILER} -std=c++20 + -DSPUD_DETOUR_TRACING=${_spud_other_tracing} + "-I${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_CURRENT_SOURCE_DIR}/tests/install/link_consumer/main.cc" + ${_spud_mismatch_libs} + -o "${CMAKE_CURRENT_BINARY_DIR}/tracing-mismatch" + ) + # GNU ld and lld print demangled names, ld64 may print mangled ones. + set_tests_properties(spud.tracing_mismatch PROPERTIES + PASS_REGULAR_EXPRESSION "detail::(tracing::)?detour::|detail(7tracing)?6detour") + endif() +endif() + +if(SPUD_BUILD_EXAMPLES) + add_subdirectory(examples) endif() diff --git a/xmake-packages/packages/s/spud/spud-src/LICENSE b/xmake-packages/packages/s/spud/spud-src/LICENSE new file mode 100644 index 000000000..abbe724c9 --- /dev/null +++ b/xmake-packages/packages/s/spud/spud-src/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 alex + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/xmake-packages/packages/s/spud/spud-src/cmake/generated/deps.cmake b/xmake-packages/packages/s/spud/spud-src/cmake/generated/deps.cmake new file mode 100644 index 000000000..a19573929 --- /dev/null +++ b/xmake-packages/packages/s/spud/spud-src/cmake/generated/deps.cmake @@ -0,0 +1,74 @@ +# GENERATED by `zig build gen-cmake` — do not edit. +# Source of truth: cmake/spud.manifest.json + +set(SPUD_VENDOR_DIR "${CMAKE_CURRENT_LIST_DIR}/../../vendor") +set(SPUD_DEP_ZYDIS_URL "https://github.com/zyantific/zydis/archive/refs/tags/v4.1.0.tar.gz") +set(SPUD_DEP_ZYDIS_SHA256 "31f23de8abb4cc2efa0fd0e827bbabcaa0f3d00fcaed8598e05295ba7b3806ad") +set(SPUD_DEP_ZYDIS_VENDOR_DIR "zydis") +set(SPUD_DEP_ZYCORE_URL "https://github.com/zyantific/zycore-c/archive/74620eefd233bec20daeb66e78e744ff06e273b7.tar.gz") +set(SPUD_DEP_ZYCORE_SHA256 "51717e66bc94966ecc8ae415b2d90aae5e624d8da9198332a1e81aa898f82a55") +set(SPUD_DEP_ZYCORE_VENDOR_DIR "zycore") +set(SPUD_DEP_CAPSTONE_URL "https://github.com/capstone-engine/capstone/archive/refs/tags/6.0.0-Alpha5.tar.gz") +set(SPUD_DEP_CAPSTONE_SHA256 "a909a0ea9cd9da5a55fb498b4bd54507469dafe80eadc2e1928c80c8773ed7c5") +set(SPUD_DEP_CAPSTONE_VENDOR_DIR "capstone") +set(SPUD_DEP_CATCH2_URL "https://github.com/catchorg/Catch2/archive/refs/tags/v3.8.0.tar.gz") +set(SPUD_DEP_CATCH2_SHA256 "1ab2de20460d4641553addfdfe6acd4109d871d5531f8f519a52ea4926303087") +set(SPUD_DEP_CATCH2_VENDOR_DIR "catch2") + +# Source tree for a pinned dep: vendor/ when scripts/fetch_vendor.sh has +# run, otherwise the same tarball, hash-checked, through FetchContent. +# SOURCE_SUBDIR points nowhere so FetchContent only unpacks; the caller +# adds the tree with its own options. +function(spud_dep_source_dir name out) + string(TOUPPER "${name}" upper) + set(vendor "${SPUD_VENDOR_DIR}/${SPUD_DEP_${upper}_VENDOR_DIR}") + if(EXISTS "${vendor}/CMakeLists.txt") + set(${out} "${vendor}" PARENT_SCOPE) + return() + endif() + FetchContent_Declare( + spud_${name} + URL "${SPUD_DEP_${upper}_URL}" + URL_HASH "SHA256=${SPUD_DEP_${upper}_SHA256}" + SOURCE_SUBDIR "spud-populate-only" + ) + FetchContent_MakeAvailable(spud_${name}) + set(${out} "${spud_${name}_SOURCE_DIR}" PARENT_SCOPE) +endfunction() + +macro(spud_provide_zydis) + if(NOT TARGET Zydis) + spud_dep_source_dir(zycore _spud_zycore_dir) + spud_dep_source_dir(zydis _spud_zydis_dir) + option(ZYDIS_BUILD_TOOLS "" OFF) + option(ZYDIS_BUILD_EXAMPLES "" OFF) + set(ZYAN_ZYCORE_PATH "${_spud_zycore_dir}" CACHE PATH "The path to look for Zycore" FORCE) + add_subdirectory("${_spud_zydis_dir}" zydis EXCLUDE_FROM_ALL) + endif() +endmacro() + +# Capstone links in as an OBJECT library, so its objects take the MSVC +# runtime of the parent build. CAPSTONE_BUILD_STATIC_MSVC_RUNTIME would +# force /MT and break /MD consumers. +macro(spud_provide_capstone) + if(SPUD_AARCH64_SUPPORT AND NOT TARGET capstone) + spud_dep_source_dir(capstone _spud_capstone_dir) + set(CAPSTONE_BUILD_STATIC_LIBS ON) + set(CAPSTONE_BUILD_SHARED_LIBS OFF) + set(CAPSTONE_BUILD_STATIC_MSVC_RUNTIME OFF) + set(CAPSTONE_BUILD_MACOS_THIN ON) + set(CAPSTONE_ARCHITECTURE_DEFAULT OFF) + set(CAPSTONE_AARCH64_SUPPORT ON) + add_subdirectory("${_spud_capstone_dir}" capstone EXCLUDE_FROM_ALL) + endif() +endmacro() + +# Catch2 is about a hundred small sources that each parse the same +# headers; as one unity source it builds roughly ten times cheaper. +macro(spud_provide_catch2) + if(NOT TARGET Catch2) + spud_dep_source_dir(catch2 _spud_catch2_dir) + add_subdirectory("${_spud_catch2_dir}" catch2 EXCLUDE_FROM_ALL) + set_target_properties(Catch2 PROPERTIES UNITY_BUILD ON UNITY_BUILD_BATCH_SIZE 0) + endif() +endmacro() diff --git a/xmake-packages/packages/s/spud/spud-src/cmake/generated/options.cmake b/xmake-packages/packages/s/spud/spud-src/cmake/generated/options.cmake new file mode 100644 index 000000000..a250c65d8 --- /dev/null +++ b/xmake-packages/packages/s/spud/spud-src/cmake/generated/options.cmake @@ -0,0 +1,8 @@ +# GENERATED by `zig build gen-cmake` — do not edit. +# Source of truth: cmake/spud.manifest.json + +option(SPUD_UNWIND_SUPPORT "Describe generated code for stack walks and exception handling" ON) +option(SPUD_AARCH64_SUPPORT "Build the ARM64 relocation backend (Capstone)" ${SPUD_AARCH64_DEFAULT}) +option(SPUD_DETOUR_TRACING "Embed source locations in installed hooks" OFF) +option(SPUD_INSTALL "Generate install rules" ${PROJECT_IS_TOP_LEVEL}) +option(SPUD_WARNINGS_AS_ERRORS "Treat warnings in spud's own targets as errors (CI)" OFF) diff --git a/xmake-packages/packages/s/spud/spud-src/cmake/generated/sources.cmake b/xmake-packages/packages/s/spud/spud-src/cmake/generated/sources.cmake new file mode 100644 index 000000000..1dd307421 --- /dev/null +++ b/xmake-packages/packages/s/spud/spud-src/cmake/generated/sources.cmake @@ -0,0 +1,68 @@ +# GENERATED by `zig build gen-cmake` — do not edit. +# Source of truth: cmake/spud.manifest.json + +set(SPUD_PUBLIC_HDRS + "include/spud/arch.h" + "include/spud/detour.h" + "include/spud/diagnostics.h" + "include/spud/error.h" + "include/spud/utils.h" + "include/spud/signature.h" + "include/spud/pointer.h" + "include/spud/memory.h" + "include/spud/memory/protection.h" +) + +set(SPUD_COMMON_SRCS + "src/utils.cc" + "src/detour/x86_64/x86_64.cc" + "src/detour/x86_64/x86_64.h" + "src/detour/chain.cc" + "src/detour/chain.h" + "src/detour/control_flow.cc" + "src/detour/control_flow.h" + "src/detour/import.cc" + "src/detour/pointer.cc" + "src/detour/detour.cc" + "src/detour/detour_impl.h" + "src/detour/detour_stub.h" + "src/detour/fwd.h" + "src/memory/protection.cc" + "src/memory/safe_patch.cc" + "src/memory/safe_patch.h" + "src/memory/executable_pool.cc" + "src/memory/executable_pool.h" + "src/memory/unwind.cc" + "src/memory/unwind.h" + "src/memory/unwind/apple.cc" + "src/memory/unwind/compact.cc" + "src/memory/unwind/dwarf.cc" + "src/memory/unwind/dwarf.h" + "src/memory/unwind/linux.cc" + "src/memory/unwind/windows.cc" + "src/memory/unwind/windows.h" + "src/memory/unwind/windows_handler.cc" + "src/signature/signature.cc" + "src/signature/scan.h" +) + +set(SPUD_AARCH64_BACKEND_SRCS + "src/detour/aarch64/aarch64.cc" + "src/detour/aarch64/aarch64.h" +) + +set(SPUD_ASM_X86_64_MASM + "src/detour/x86_64/detour_context_support.masm" +) + +set(SPUD_SIMD_SSE_SRCS + "src/signature/signature_sse.cc" +) + +set(SPUD_SIMD_AVX2_SRCS + "src/signature/signature_avx2.cc" +) + +set(SPUD_SIMD_NEON_SRCS + "src/signature/signature_neon.cc" +) diff --git a/xmake-packages/packages/s/spud/spud-src/cmake/spud.manifest.json b/xmake-packages/packages/s/spud/spud-src/cmake/spud.manifest.json new file mode 100644 index 000000000..1a2663c4f --- /dev/null +++ b/xmake-packages/packages/s/spud/spud-src/cmake/spud.manifest.json @@ -0,0 +1,148 @@ +{ + "name": "spud", + "version": "0.1.0", + "cxx_standard": 20, + "unsupported": [ + "32-bit x86", + "Windows ARM64", + "ARM32", + "MIPS", + "PowerPC", + "universal macOS binaries" + ], + "options": { + "SPUD_UNWIND_SUPPORT": { + "help": "Describe generated code for stack walks and exception handling", + "default": true, + "define": "SPUD_UNWIND_SUPPORT" + }, + "SPUD_AARCH64_SUPPORT": { + "help": "Build the ARM64 relocation backend (Capstone)", + "default": "SPUD_AARCH64_DEFAULT", + "define": "SPUD_AARCH64_SUPPORT" + }, + "SPUD_DETOUR_TRACING": { + "help": "Embed source locations in installed hooks", + "default": false, + "define": "SPUD_DETOUR_TRACING" + }, + "SPUD_INSTALL": { + "help": "Generate install rules", + "default": "PROJECT_IS_TOP_LEVEL" + }, + "SPUD_WARNINGS_AS_ERRORS": { + "help": "Treat warnings in spud's own targets as errors (CI)", + "default": false + } + }, + "public_headers": [ + "include/spud/arch.h", + "include/spud/detour.h", + "include/spud/diagnostics.h", + "include/spud/error.h", + "include/spud/utils.h", + "include/spud/signature.h", + "include/spud/pointer.h", + "include/spud/memory.h", + "include/spud/memory/protection.h" + ], + "sources": { + "common": [ + "src/utils.cc", + "src/detour/x86_64/x86_64.cc", + "src/detour/x86_64/x86_64.h", + "src/detour/chain.cc", + "src/detour/chain.h", + "src/detour/control_flow.cc", + "src/detour/control_flow.h", + "src/detour/import.cc", + "src/detour/pointer.cc", + "src/detour/detour.cc", + "src/detour/detour_impl.h", + "src/detour/detour_stub.h", + "src/detour/fwd.h", + "src/memory/protection.cc", + "src/memory/safe_patch.cc", + "src/memory/safe_patch.h", + "src/memory/executable_pool.cc", + "src/memory/executable_pool.h", + "src/memory/unwind.cc", + "src/memory/unwind.h", + "src/memory/unwind/apple.cc", + "src/memory/unwind/compact.cc", + "src/memory/unwind/dwarf.cc", + "src/memory/unwind/dwarf.h", + "src/memory/unwind/linux.cc", + "src/memory/unwind/windows.cc", + "src/memory/unwind/windows.h", + "src/memory/unwind/windows_handler.cc", + "src/signature/signature.cc", + "src/signature/scan.h" + ], + "aarch64_backend": [ + "src/detour/aarch64/aarch64.cc", + "src/detour/aarch64/aarch64.h" + ], + "asm": { + "x86_64_masm": [ + "src/detour/x86_64/detour_context_support.masm" + ] + }, + "simd": { + "sse": [ + "src/signature/signature_sse.cc" + ], + "avx2": [ + "src/signature/signature_avx2.cc" + ], + "neon": [ + "src/signature/signature_neon.cc" + ] + } + }, + "deps": { + "zydis": { + "url": "https://github.com/zyantific/zydis/archive/refs/tags/v4.1.0.tar.gz", + "sha256": "31f23de8abb4cc2efa0fd0e827bbabcaa0f3d00fcaed8598e05295ba7b3806ad", + "strip": 1, + "vendor_dir": "zydis", + "cmake_target": "Zydis", + "defines": [ + "ZYDIS_STATIC_BUILD", + "ZYCORE_STATIC_BUILD", + "ZYAN_STATIC_BUILD" + ] + }, + "zycore": { + "url": "https://github.com/zyantific/zycore-c/archive/74620eefd233bec20daeb66e78e744ff06e273b7.tar.gz", + "sha256": "51717e66bc94966ecc8ae415b2d90aae5e624d8da9198332a1e81aa898f82a55", + "strip": 1, + "vendor_dir": "zycore", + "cmake_target": "Zycore", + "defines": [ + "ZYCORE_STATIC_BUILD", + "ZYAN_STATIC_BUILD" + ] + }, + "capstone": { + "url": "https://github.com/capstone-engine/capstone/archive/refs/tags/6.0.0-Alpha5.tar.gz", + "sha256": "a909a0ea9cd9da5a55fb498b4bd54507469dafe80eadc2e1928c80c8773ed7c5", + "strip": 1, + "vendor_dir": "capstone", + "optional": "SPUD_AARCH64_SUPPORT", + "cmake_target": "capstone", + "defines": [ + "CAPSTONE_USE_SYS_DYN_MEM", + "CAPSTONE_HAS_AARCH64" + ] + }, + "catch2": { + "url": "https://github.com/catchorg/Catch2/archive/refs/tags/v3.8.0.tar.gz", + "sha256": "1ab2de20460d4641553addfdfe6acd4109d871d5531f8f519a52ea4926303087", + "strip": 1, + "vendor_dir": "catch2", + "tests_only": true, + "cmake_target": "Catch2" + } + } +} diff --git a/xmake-packages/packages/s/spud/spud-src/cmake/spudConfig.cmake.in b/xmake-packages/packages/s/spud/spud-src/cmake/spudConfig.cmake.in new file mode 100644 index 000000000..d79899e3b --- /dev/null +++ b/xmake-packages/packages/s/spud/spud-src/cmake/spudConfig.cmake.in @@ -0,0 +1,40 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +find_dependency(Threads) + +set_and_check(SPUD_INCLUDE_DIR "@PACKAGE_CMAKE_INSTALL_INCLUDEDIR@") +set_and_check(SPUD_LIB_DIR "@PACKAGE_CMAKE_INSTALL_LIBDIR@") + +if(NOT TARGET spud::spud) + add_library(spud::spud STATIC IMPORTED) + set(_spud_location + "${SPUD_LIB_DIR}/@SPUD_STATIC_LIB_FILENAME@") + set(_spud_link + "${SPUD_LIB_DIR}/@SPUD_ZYDIS_FILENAME@" + "${SPUD_LIB_DIR}/@SPUD_ZYCORE_FILENAME@" + Threads::Threads) + if(WIN32) + list(APPEND _spud_link psapi) + endif() + set_target_properties( + spud::spud + PROPERTIES + IMPORTED_LOCATION "${_spud_location}" + INTERFACE_INCLUDE_DIRECTORIES "${SPUD_INCLUDE_DIR}" + INTERFACE_COMPILE_FEATURES "cxx_std_20" + INTERFACE_COMPILE_DEFINITIONS + "SPUD_DETOUR_TRACING=@SPUD_DETOUR_TRACING_VALUE@;SPUD_UNWIND_SUPPORT=@SPUD_UNWIND_SUPPORT_VALUE@" + INTERFACE_LINK_LIBRARIES "${_spud_link}" + ) + # Mirrors the PUBLIC option in CMakeLists.txt: the Windows headers need MS + # extensions under GNU-style Clang. + if(WIN32 AND CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND NOT MSVC) + set_property(TARGET spud::spud PROPERTY INTERFACE_COMPILE_OPTIONS + "$<$:-fms-extensions>") + endif() + unset(_spud_location) + unset(_spud_link) +endif() + +check_required_components(spud) diff --git a/xmake-packages/packages/s/spud/spud-src/include/spud/arch.h b/xmake-packages/packages/s/spud/spud-src/include/spud/arch.h index 51e763eb8..d0ab72e42 100644 --- a/xmake-packages/packages/s/spud/spud-src/include/spud/arch.h +++ b/xmake-packages/packages/s/spud/spud-src/include/spud/arch.h @@ -1,6 +1,10 @@ #pragma once -namespace spud { +// Build systems set this for the library and its consumers. Direct builds +// retain unwind support unless explicitly disabled. +#ifndef SPUD_UNWIND_SUPPORT +#define SPUD_UNWIND_SUPPORT 1 +#endif #if defined(__APPLE__) #include @@ -30,12 +34,12 @@ namespace spud { #define SPUD_ARCH_X86_FAMILY 1 #define SPUD_ARCH_X86 1 #define SPUD_ARCH_32_BITS 1 -#define SPUD_ARCH_LE 1 +#define SPUD_ARCH_LITTLE_ENDIAN 1 #elif defined(_M_X64) || defined(__x86_64__) #define SPUD_ARCH_X86_FAMILY 1 #define SPUD_ARCH_X86_64 1 #define SPUD_ARCH_64_BITS 1 -#define SPUD_ARCH_LE 1 +#define SPUD_ARCH_LITTLE_ENDIAN 1 #elif defined(__ARMEL__) #define SPUD_ARCH_ARM_FAMILY 1 #define SPUD_ARCH_ARMEL 1 @@ -44,13 +48,19 @@ namespace spud { #elif defined(__aarch64__) || defined(_M_ARM64) #define SPUD_ARCH_ARM_FAMILY 1 #define SPUD_ARCH_ARM64 1 +#if defined(__arm64e__) +// arm64e signs code pointers; spud cannot emit a validly signed stub. +#define SPUD_ARCH_ARM64E 1 +#endif #define SPUD_ARCH_64_BITS 1 #define SPUD_ARCH_LITTLE_ENDIAN 1 #else #error "Add support for this architecture" #endif -enum Arch { +namespace spud { + +enum class Arch { kX86_64 = 0, kX86, kArm64, diff --git a/xmake-packages/packages/s/spud/spud-src/include/spud/details/function_traits.h b/xmake-packages/packages/s/spud/spud-src/include/spud/details/function_traits.h deleted file mode 100644 index c225911ee..000000000 --- a/xmake-packages/packages/s/spud/spud-src/include/spud/details/function_traits.h +++ /dev/null @@ -1,55 +0,0 @@ -#pragma once - -namespace spud { -namespace detail { -// -template struct function_traits; - -template -struct function_traits - : public function_traits< - decltype(&Function::template operator())> {}; - -template -struct function_traits - : function_traits {}; - -/* support the non-const operator () - * this will work with user defined functors */ -template -struct function_traits - : function_traits {}; - -template -struct function_traits { - using FuncType = ReturnType (*)(ReturnType (*)(Arguments...), Arguments...); -}; - -template struct function_traits_ptr; - -template -struct function_traits_ptr - : function_traits_ptr {}; - -/* support the non-const operator () - * this will work with user defined functors */ -template -struct function_traits_ptr - : function_traits_ptr {}; - -template -struct function_traits_ptr { - using FuncType = ReturnType (*)(ReturnType (*)(Arguments...), Arguments...); -}; - -} // namespace detail -} // namespace spud diff --git a/xmake-packages/packages/s/spud/spud-src/include/spud/detour.h b/xmake-packages/packages/s/spud/spud-src/include/spud/detour.h index 88e45ab69..bfda9ff56 100644 --- a/xmake-packages/packages/s/spud/spud-src/include/spud/detour.h +++ b/xmake-packages/packages/s/spud/spud-src/include/spud/detour.h @@ -1,113 +1,533 @@ #pragma once +#include +#include #include +#include #include -#include -#include -#include +#include +#include #include +#include #include -#if __cpp_lib_source_location && SPUD_DETOUR_TRACING +#ifndef SPUD_DETOUR_TRACING +#define SPUD_DETOUR_TRACING 0 +#endif + +#if SPUD_DETOUR_TRACING +#include #include #endif +// Tracing changes the layout of the hook context and handle. Those types +// sit in an inline namespace when it is on, so a consumer built with the +// other setting than libspud fails to link instead of misreading them. +// MSVC's linker also names the mismatch. +#if SPUD_DETOUR_TRACING +#define SPUD_TRACING_ABI_BEGIN inline namespace tracing { +#define SPUD_TRACING_ABI_END } +#if defined(_MSC_VER) +#pragma detect_mismatch("spud_detour_tracing", "1") +#endif +#else +#define SPUD_TRACING_ABI_BEGIN +#define SPUD_TRACING_ABI_END +#if defined(_MSC_VER) +#pragma detect_mismatch("spud_detour_tracing", "0") +#endif +#endif + #include "arch.h" -#include "details/function_traits.h" -#include "utils.h" +#include "diagnostics.h" +#include "error.h" namespace spud { -// Read-only, bounded check for enough complete host instructions before a -// return/tail jump. The caller must supply a readable, independently verified -// native extent. This does not install a detour or promise relocation success. -bool has_detour_prologue(const void *address, size_t extent); +// Linux freezes sibling threads from a cloned tracer that shares this +// address space but not this thread group. Direct ptrace of a sibling +// is EPERM. Set true only when the caller accepts a torn write. +void allow_unguarded_patches(bool allowed); + +// Frees removed hooks whose calls have all returned: hooks that removed +// themselves, request_uninstall(), and uninstalls that stopped waiting. +// Installs and uninstalls also do this on their way in. Returns how many +// removed hooks are still waiting. +[[nodiscard]] size_t collect(); + +// Decode and emit a trampoline for `address` without writing the target. +// The plan is for the short landing (5 or 9 bytes on x86-64, 4 or 8 on ARM64), +// which the first install on a target writes when the target's anchor block +// lands within branch reach. Later hooks on the target never write it. +// Otherwise install writes the far landing (14 or 18 bytes on x86-64, 16 or 20 +// on ARM64), which reaches a block anywhere. Also refuses PAC and an unreadable +// prologue. It does not freeze threads or translate unwind metadata, so +// install can still return `kSuspensionUnavailable` or `kUnwindFailed` after +// a clean plan. `trampoline_size` is an upper +// bound: install rebuilds the trampoline at its final address, where +// PC-relative instructions that still reach keep their short form. +struct detour_plan { + detour_error error = detour_error::kDecodeFailed; + // The address install patches. A pure forwarder (jmp, import thunk) is + // followed to its destination, which then carries the hook for every + // caller; this is that destination, or the input address. + uintptr_t resolved_target = 0; + // The short landing size, and the instruction-aligned bytes it displaces. + // Fewer required bytes than the landing means kInsufficientBytes. + size_t jump_size = 0; + size_t required_patch_bytes = 0; + size_t trampoline_size = 0; + size_t relocated_instruction_count = 0; + // The same pair for the far landing; set only when the short landing + // fits. Fewer required bytes than the landing means install fails with + // kInsufficientBytes if the pool cannot place the block within reach. + size_t far_jump_size = 0; + size_t far_required_patch_bytes = 0; + // The target already holds spud's landing. Another install links into + // its chain and writes nothing: the sizes describe the landing and the + // copy in place, and no far landing or instruction count is reported. + bool hooked = false; + + bool ok() const { + return error == detour_error::kOk; + } +}; + +// A nonzero function_size scans that complete contiguous instruction extent +// (at most 64KB), including code after early returns. Zero uses the bounded +// end: an unwind table entry starting at the target when one exists, or on +// Apple the image's LC_FUNCTION_STARTS, otherwise the scan heuristic. An +// explicit extent refuses an entry jump to another target. An extent shorter +// than the patch is accepted when it ends in a return, jump or trap followed by +// padding the patch can overwrite. +[[nodiscard]] detour_plan inspect_detour(uintptr_t address, + Arch arch = Arch::kHost, + size_t function_size = 0); + +template + requires std::is_pointer_v +[[nodiscard]] detour_plan inspect_detour(T target, Arch arch = Arch::kHost, + size_t function_size = 0) { + return inspect_detour(reinterpret_cast(target), arch, + function_size); +} + +// What this build can do for `arch`. `protected_patch` is the OS mechanism, +// not a live probe; install still checks again at commit time. +struct detour_capabilities { + bool decode = false; + bool emit = false; + bool execute = false; + bool protected_patch = false; +}; + +[[nodiscard]] detour_capabilities +query_detour_capabilities(Arch arch = Arch::kHost); + +// What this process is running with. Generated stubs retain IBT/BTI pads; +// x86 target entries preserve endbr64 only when already present. PAC cannot +// be satisfied from here, so install returns `kControlFlowEnforced`. +struct control_flow { + // Not detected; user space has no portable IBT status query. Stubs carry + // endbr64 landing pads either way; target patches keep existing pads only. + bool ibt = false; + bool shadow_stack = false; + bool cfg = false; + bool bti = false; + bool pac = false; +}; + +[[nodiscard]] control_flow query_control_flow(); namespace detail { +SPUD_TRACING_ABI_BEGIN + +#if SPUD_DETOUR_TRACING +using trace_location = std::source_location; +#else +struct trace_location { + static constexpr trace_location current() noexcept { + return {}; + } +}; +#endif + +// What a hook's wrapper reads. It sits right before the hook's entry +// counts (if it has any), and the entry stub hands the wrapper the counts' +// address, so both must stay alive at a stable address while the hook can +// be entered. The rest of a hook's state is internal (src/detour/chain.h). +struct alignas(64) detour_context { + uintptr_t func = 0; + // This hook's original(): a jump through its next slot, so the address + // stays valid while hooks below it come and go. + uintptr_t trampoline = 0; + // The hook object for wrappers that call a stateful callable. Its box is + // shared by the handle and by a removed hook that is still draining. + void *callable = nullptr; +#if SPUD_DETOUR_TRACING + std::source_location location; +#endif +}; +static_assert(sizeof(detour_context) == 64, + "the stub reaches the context one cache line before the counts"); + +SPUD_TRACING_ABI_END + +// Calls currently executing a removable hook, spread over cache-line shards +// so threads calling one hook at once do not all bounce a single line. The +// jump stub picks a shard from its stack pointer and increments it on entry +// (so every entry path is counted), then hands the shard's address to the +// wrapper, which decrements that same shard on exit. Once a hook is +// unlinked nothing new is counted, and its memory is freed after all shards +// read zero. A permanent hook's stub counts nothing and has no shards. +struct alignas(64) entry_counts { + struct alignas(64) shard { + std::atomic count{0}; + }; + static constexpr size_t kShards = 16; + shard shards[kShards]; + + // Whether any call is inside this hook. Exact only while every thread that + // could enter is stopped; otherwise a snapshot. Calls leave with a release + // decrement, so once this reads all zero their writes, including to the + // callable, happen before whatever frees the hook next. + bool active() const { + for (const shard &s : shards) { + if (s.count.load(std::memory_order_relaxed) != 0) + return true; + } + std::atomic_thread_fence(std::memory_order_acquire); + return false; + } +}; +static_assert(sizeof(entry_counts::shard) == 64, + "each entry shard must own one cache line"); +static_assert(std::atomic::is_always_lock_free && + sizeof(std::atomic) == sizeof(uint64_t), + "entry shards must be plain lock-free words"); + +// A hook's context, chain links and entry counts. Defined in +// src/detour/chain.h. +struct hook_node; + +// A stateful hook object and its reference count. The handle holds one +// reference. A removed hook that calls may still be running holds another +// until it is freed, and a detached or permanent hook holds one forever. +struct callable_box { + std::atomic refs{1}; + void *object = nullptr; + void (*destroy)(callable_box *) = nullptr; +}; + +template struct boxed_callable final : callable_box { + Fn fn; + template + explicit boxed_callable(F &&f) : fn(std::forward(f)) { + object = &fn; + destroy = [](callable_box *box) { + delete static_cast(box); + }; + } +}; + +inline void retain_callable(callable_box *box) { + if (box != nullptr) + box->refs.fetch_add(1, std::memory_order_relaxed); +} + +inline void release_callable(callable_box *box) { + if (box != nullptr && box->refs.fetch_sub(1, std::memory_order_acq_rel) == 1) + box->destroy(box); +} + +// Decrements the entry shard the stub counted this call in when the wrapper +// exits, including on exceptions. Release, so the hook's last writes are +// ordered before a remover that sees zero and frees its state; with +// unguarded patches on ARM64 the counts are all removal has to go on. +struct active_guard { + explicit active_guard(std::atomic *entry) : entry_(entry) {} + ~active_guard() { + entry_->fetch_sub(1, std::memory_order_release); + } + active_guard(const active_guard &) = delete; + active_guard &operator=(const active_guard &) = delete; + std::atomic *entry_; +}; + +struct staged_unlink; +struct prepared_install; +struct hook_step; + +// Applies transaction steps; a friend of detour. +detour_error commit(std::span steps); + +SPUD_TRACING_ABI_BEGIN + +class trampoline_pin { +public: + trampoline_pin() = default; + ~trampoline_pin() { + reset(); + } + trampoline_pin(const trampoline_pin &) = delete; + trampoline_pin &operator=(const trampoline_pin &) = delete; + trampoline_pin(trampoline_pin &&other) noexcept + : address_(std::exchange(other.address_, 0)), + pins_(std::exchange(other.pins_, nullptr)) {} + trampoline_pin &operator=(trampoline_pin &&other) noexcept { + if (this != &other) { + reset(); + address_ = std::exchange(other.address_, 0); + pins_ = std::exchange(other.pins_, nullptr); + } + return *this; + } + uintptr_t address() const { + return address_; + } + void reset() noexcept { + if (pins_ != nullptr) + pins_->fetch_sub(1, std::memory_order_release); + pins_ = nullptr; + address_ = 0; + } + +private: + friend struct detour; + uintptr_t address_ = 0; + // The hook's lease count, which keeps it from being freed. + std::atomic *pins_ = nullptr; +}; struct detour { public: ~detour() { remove(); + drop_callable(); } - uintptr_t trampoline() const { - return this->trampoline_; + // This hook's original(). It jumps through a slot that removing a hook + // below rewrites, so the address stays callable while this hook is in. + uintptr_t trampoline() const; + + detour_error last_error() const { + return last_error_; + } + // errno / GetLastError / kern_return_t behind last_error(), or 0 when the + // last operation succeeded or failed above the OS. Stable per handle; see + // spud::last_os_error() for the calling thread's most recent code. + int last_os_error() const { + return last_os_error_; + } + bool is_installed() const { + return state_ == handle_state::kInstalled; + } + bool is_detached() const { + return state_ == handle_state::kDetached; } - using Self = detail::detour; + // Set before install. The caller supplies the complete contiguous code + // extent, including code beyond early returns; zero restores the heuristic. + // Installed and detached handles cannot change their extent. + [[nodiscard]] bool set_function_size(size_t size) { + if (state_ != handle_state::kIdle || size > 0x10000 || + size > UINTPTR_MAX - address_) + return false; + function_size_ = size; + return true; + } -public: detour(detour &&other) noexcept { - this->trampoline_ = other.trampoline_; - this->address_ = other.address_; - this->func_ = other.func_; - this->wrapper_ = other.wrapper_; - this->context_container_ = std::move(other.context_container_); - this->original_func_data_ = other.original_func_data_; - other.original_func_data_.clear(); + take(other); } detour &operator=(detour &&other) noexcept { - this->trampoline_ = other.trampoline_; - this->address_ = other.address_; - this->func_ = other.func_; - this->wrapper_ = other.wrapper_; - this->context_container_ = std::move(other.context_container_); - this->original_func_data_ = other.original_func_data_; - other.original_func_data_.clear(); + if (this != &other) { + // Frees everything this object owns (unlinks when installed, drops the + // spare context otherwise); detached objects own nothing. A hook that + // cannot be removed keeps running and keeps its callable, unowned. + remove(); + drop_callable(); + take(other); + } return *this; } protected: - struct ContextContainer { - uintptr_t r15; - uintptr_t func; - uintptr_t trampoline; -#if __cpp_lib_source_location && SPUD_DETOUR_TRACING - std::source_location location; -#endif - }; - -#if __cpp_lib_source_location && SPUD_DETOUR_TRACING + detour() = default; + // `uncounted` is the same wrapper without the exit decrement, for + // permanent installs whose stubs do not count entries. detour(uintptr_t address, uintptr_t func, uintptr_t wrapper, - const std::source_location location = std::source_location::current()) - : address_(address), func_(func), wrapper_(wrapper), - context_container_(std::make_unique()), - location_(location) {} -#else - detour(uintptr_t address, uintptr_t func, uintptr_t wrapper) + uintptr_t uncounted, trace_location location) : address_(address), func_(func), wrapper_(wrapper), - context_container_(std::make_unique()) {} -#endif - Self &install(Arch arch = Arch::kHost); - void remove(); - Self &detach() { - original_func_data_.clear(); - // We are intentionally ignoring the return value here - // TODO(alex): Move this to a global cleanup thing - (void)context_container_.release(); + uncounted_wrapper_(uncounted), location_(location) {} - return *this; + // Takes ownership of the object a callable wrapper reads from its context. + void adopt_callable(callable_box *callable) { + callable_ = callable; } - - static uintptr_t get_context_value(); + detour_error install(Arch arch = Arch::kHost); + // Installs without entry counting and detaches the handle on success. + detour_error install_permanent(Arch arch); + // Unlinks the hook, then waits a bounded time for calls still inside it + // before freeing its memory. What has not drained by then is retired. + detour_error remove(); + // Unlinks the hook and retires its memory without waiting. + detour_error request_remove(); + trampoline_pin pin_trampoline(); + detour &detach(); private: + friend detour_error detail::commit(std::span steps); + detour(detour const &) = delete; detour &operator=(detour const &) = delete; - uintptr_t address_; - uintptr_t func_; - uintptr_t wrapper_; + // A handle without a node takes *spare when one is given, and uses what + // `prepared` holds, since a transaction's suspend window cannot allocate. + detour_error install_impl(Arch arch, bool permanent = false, + hook_node **spare = nullptr, + prepared_install *prepared = nullptr); + // With staged set, the node and its memory are handed to *staged instead + // of released. Otherwise they are freed once drained (only waited for + // when `wait`) or retired. + detour_error unlink(staged_unlink *staged, bool wait = true); + detour_error relink(staged_unlink &staged); + + // Drops the thread-local OS code, runs `op`, then snapshots that code onto + // this handle when the result is a failure. + template detour_error record_error(Op &&op) { + detail::clear_os_error(); + const detour_error err = op(); + // A layout retried after a refusal leaves its reason behind. + if (err == detour_error::kOk) + detail::clear_os_error(); + last_error_ = err; + last_os_error_ = err == detour_error::kOk ? 0 : spud::last_os_error(); + return last_error_; + } - std::unique_ptr context_container_ = nullptr; - uintptr_t trampoline_ = 0; - std::vector original_func_data_ = {}; -#if __cpp_lib_source_location && SPUD_DETOUR_TRACING - const std::source_location location_ = std::source_location::current(); -#endif + // Lets go of the callable. An installed hook the handle failed to remove + // can still call it, so it is leaked with that hook's node. Detached + // and draining hooks hold references of their own. + void drop_callable() noexcept { + if (state_ != handle_state::kInstalled) { + release_callable(callable_); + } + callable_ = nullptr; + } + + void take(detour &other) noexcept { + address_ = other.address_; + callable_ = std::exchange(other.callable_, nullptr); + function_size_ = other.function_size_; + func_ = other.func_; + wrapper_ = other.wrapper_; + uncounted_wrapper_ = other.uncounted_wrapper_; + wrappers_resolved_ = other.wrappers_resolved_; + arch_ = other.arch_; + node_ = other.node_; + state_ = other.state_; + location_ = other.location_; + last_error_ = other.last_error_; + last_os_error_ = other.last_os_error_; + other.node_ = nullptr; + other.state_ = handle_state::kIdle; + } + + uintptr_t address_ = 0; + size_t function_size_ = 0; + uintptr_t func_ = 0; + uintptr_t wrapper_ = 0; + uintptr_t uncounted_wrapper_ = 0; + bool wrappers_resolved_ = false; + Arch arch_ = Arch::kHost; + callable_box *callable_ = nullptr; + + // Allocated by the first install. idle: this handle owns node_, if any, + // and it is not in a chain. installed: node_ is live in a chain and this + // handle must uninstall it. detached: the chain owns node_; this handle + // owns nothing. + hook_node *node_ = nullptr; + enum class handle_state : uint8_t { kIdle, kInstalled, kDetached }; + handle_state state_ = handle_state::kIdle; + trace_location location_; + detour_error last_error_ = detour_error::kOk; + int last_os_error_ = 0; +}; + +SPUD_TRACING_ABI_END + +// The jump stub leaves the address of the hook's entry counts in r11 (x11 +// on arm64), with its context one cache line before it, and, for a counted +// hook, the entry shard it incremented in r10 (x10). Neither ABI +// uses those for arguments. The wrapper must read them before anything else +// can reuse those scratch registers, so the read is inline and the wrapper +// opts out of entry instrumentation (-finstrument-functions, -pg, XRay), +// which would run first and is free to clobber them. GCC instruments inlined +// callees too, so the attribute is on the read as well. +struct stub_registers { + detour_context *ctx; + // Garbage for a permanent hook, whose stub does not count. + std::atomic *entry; }; +#if defined(__GNUC__) || defined(__clang__) +#if defined(__clang__) +#define SPUD_WRAPPER_ATTRS \ + __attribute__((no_instrument_function, no_profile_instrument_function, \ + xray_never_instrument)) +#else +#define SPUD_WRAPPER_ATTRS __attribute__((no_instrument_function)) +#endif +[[gnu::always_inline]] SPUD_WRAPPER_ATTRS inline stub_registers +read_stub_registers() { + entry_counts *counts; + std::atomic *entry; + // One statement with both registers clobbered, so neither output can be + // allocated to a register the other move still has to read. +#if defined(SPUD_ARCH_X86_64) + __asm__ volatile("mov %%r11, %0\n\tmov %%r10, %1" + : "=r"(counts), "=r"(entry) + : + : "r10", "r11"); +#else + __asm__ volatile("mov %0, x11\n\tmov %1, x10" + : "=r"(counts), "=r"(entry) + : + : "x10", "x11"); +#endif + return {reinterpret_cast(counts) - 1, entry}; +} +#else +#define SPUD_WRAPPER_ATTRS +} // namespace detail +} // namespace spud +// MSVC has no x64 inline assembly; detour_context_support.masm stores r10 +// through `entry` and returns r11, in one call so nothing runs in between. +extern "C" uintptr_t spud_read_context_value(uintptr_t *entry); +namespace spud { +namespace detail { +inline stub_registers read_stub_registers() { + uintptr_t entry = 0; + const uintptr_t counts = spud_read_context_value(&entry); + return {reinterpret_cast(counts) - 1, + reinterpret_cast *>(entry)}; +} +#endif + +// Foreign Windows exceptions can bypass the compiler's C++ destructor +// handling, so there the wrapper balances the entry count in a native +// termination scope. +#if defined(SPUD_OS_WIN) && defined(_MSC_VER) +#define SPUD_WRAPPER_SEH 1 +#elif defined(SPUD_OS_WIN) && defined(__clang__) +#if !__is_identifier(__try) +#define SPUD_WRAPPER_SEH 1 +#endif +#endif +#ifndef SPUD_WRAPPER_SEH +#define SPUD_WRAPPER_SEH 0 +#endif template concept Address = requires(T a) { @@ -118,163 +538,483 @@ concept Address = requires(T a) { template struct detour; +template class trampoline_lease { +public: + trampoline_lease() = default; + trampoline_lease(trampoline_lease &&) noexcept = default; + trampoline_lease &operator=(trampoline_lease &&) noexcept = default; + Signature *get() const { + return reinterpret_cast(pin_.address()); + } + explicit operator bool() const { + return get() != nullptr; + } + void reset() noexcept { + pin_.reset(); + } + +private: + template friend struct detour; + explicit trampoline_lease(detail::trampoline_pin pin) + : pin_(std::move(pin)) {} + detail::trampoline_pin pin_; +}; + template struct detour : public detail::detour { public: using func_t = R(R (*)(Args...), Args...); using trampoline_t = R(Args...); using Self = detour; - using WrapperArgs = std::tuple; - static inline R wrapper(Args... args) { - const auto context = reinterpret_cast( - detail::detour::get_context_value()); -#if SPUD_DETOUR_TRACING - printf("%s\n", context->location.function_name()); -#endif - if constexpr (std::is_same_v, ContextContainer *>) - return (reinterpret_cast(context->func))( - reinterpret_cast(context->trampoline), args..., - context); - else - return (reinterpret_cast(context->func))( - reinterpret_cast(context->trampoline), args...); - }; -#if __cpp_lib_source_location && SPUD_DETOUR_TRACING - template - static auto create( - T target, F func, - const std::source_location location = std::source_location::current()) { - return Self{reinterpret_cast(target), - reinterpret_cast(func), - reinterpret_cast((R(*)(Args...))wrapper), location}; + // An empty handle. Assign a hook to it; until then try_install() returns + // kNotFound. + detour() = default; + + // Hooks `target` with a callable taking the next callable in the chain + // followed by the target's arguments. A captureless lambda is called + // directly from the wrapper, so it can be inlined there; anything with + // state is moved into the handle and lives until the handle is gone and + // no removed hook can still call it. + template + requires std::is_class_v> && + std::is_invocable_r_v &, trampoline_t *, + Args...> + detour(trampoline_t *target, F &&hook, + detail::trace_location location = detail::trace_location::current()) + : detail::detour( + reinterpret_cast(target), 0, + reinterpret_cast( + &wrapper>, true>), + reinterpret_cast( + &wrapper>, false>), + location) { + using Fn = std::remove_cvref_t; + if constexpr (!stateless) { + adopt_callable(new detail::boxed_callable(std::forward(hook))); + } } -#else + + // Function pointer hooks, including a function template whose first + // parameter is deduced from the target. + detour(trampoline_t *target, func_t *hook, + detail::trace_location location = detail::trace_location::current()) + : detail::detour( + reinterpret_cast(target), + reinterpret_cast(hook), + reinterpret_cast(&wrapper), + reinterpret_cast(&wrapper), + location) {} + template - static auto create(T target, F func) { - return Self{reinterpret_cast(target), - reinterpret_cast(func), - reinterpret_cast((R(*)(Args...))wrapper)}; + static auto + create(T target, F func, + detail::trace_location location = detail::trace_location::current()) { + return Self{ + reinterpret_cast(target), reinterpret_cast(func), + reinterpret_cast(&wrapper), + reinterpret_cast(&wrapper), location}; } -#endif - inline trampoline_t *trampoline() { + trampoline_t *trampoline() const { return reinterpret_cast(this->detail::detour::trampoline()); } - Self &install(Arch arch = Arch::kHost) { - this->detail::detour::install(arch); - return *this; + // Keep this lease alive while a callable or saved context can reach the + // trampoline outside a call. Removing the hook still unlinks it, but its + // original() stays callable, skipping hooks removed below it, until the + // lease is released; spud::collect() frees it after that. + [[nodiscard]] trampoline_lease pin_trampoline() { + return trampoline_lease{ + this->detail::detour::pin_trampoline()}; + } + [[nodiscard]] detour_error try_install(Arch arch = Arch::kHost) { + return this->detail::detour::install(arch); + } + + // Installs a hook that can never be removed, and detaches the handle once + // it is in (see detach()). Removable hooks count every call in and out so + // removal can wait for them; a permanent hook skips both atomic updates, so + // threads calling it at once never share a counter line. + [[nodiscard]] detour_error try_install_permanent(Arch arch = Arch::kHost) { + return this->detail::detour::install_permanent(arch); } + // Unlinks the hook so new calls skip it, then waits up to five seconds + // for calls already inside it and frees its memory. Calls still running + // after that, including the one making this call when a hook removes + // itself, keep the memory and callable alive until they return, and + // spud::collect() frees them afterwards. A trampoline lease does the same + // until it is released. Returns kOk once the hook is unlinked. + [[nodiscard]] detour_error uninstall() { + return this->detail::detour::remove(); + } + + // Unlinks like uninstall() but never waits. The hook's memory is freed by + // a later install, uninstall or spud::collect() once its calls are done. + [[nodiscard]] detour_error request_uninstall() { + return this->detail::detour::request_remove(); + } + + // Leaves the hook installed for good and lets go of it. The handle owns + // nothing from then on: try_install() and uninstall() return kNotFound. Self &detach() { this->detail::detour::detach(); return *this; } private: -#if __cpp_lib_source_location && SPUD_DETOUR_TRACING - detour(uintptr_t address, uintptr_t func, uintptr_t wrapper, - const std::source_location location = std::source_location::current()) - : detail::detour(address, func, wrapper, location) {} -#else - detour(uintptr_t address, uintptr_t func, uintptr_t wrapper) - : detail::detour(address, func, wrapper) {} - -#endif + template + static constexpr bool stateless = + std::is_empty_v && std::is_default_constructible_v; + + // The trampoline address is read inside call() so it never sits in the + // wrapper's own frame: removal scans stacks conservatively, and a stale + // copy there after the entry count drops reads as a pending return. + static trampoline_t *next_of(detail::detour_context *ctx) { + return reinterpret_cast(ctx->trampoline); + } - template struct tag { - using type = T; + struct call_pointer { + static R call(detail::detour_context *ctx, Args &&...args) { + return reinterpret_cast(ctx->func)(next_of(ctx), + std::forward(args)...); + } }; - template struct select_last { - // Use a fold-expression to fold the comma operator over the parameter pack. - using type = typename decltype((tag{}, ...))::type; + template struct call_object { + static R call(detail::detour_context *ctx, Args &&...args) { + if constexpr (stateless) { + return Fn{}(next_of(ctx), std::forward(args)...); + } else { + return (*static_cast(ctx->callable))(next_of(ctx), + std::forward(args)...); + } + } }; + + template + SPUD_WRAPPER_ATTRS static R wrapper(Args... args) { + const detail::stub_registers stub = detail::read_stub_registers(); +#if SPUD_DETOUR_TRACING + std::printf("%s\n", stub.ctx->location.function_name()); +#endif + if constexpr (!Counted) { + // A permanent hook's stub did not count this entry. + return Call::call(stub.ctx, std::forward(args)...); + } +#if SPUD_WRAPPER_SEH + else if constexpr ((std::is_void_v || + std::is_trivially_destructible_v) && + (std::is_trivially_destructible_v && ...)) { + __try { + return Call::call(stub.ctx, std::forward(args)...); + } __finally { + stub.entry->fetch_sub(1, std::memory_order_release); + } + } +#endif + else { + const detail::active_guard active_scope{stub.entry}; + return Call::call(stub.ctx, std::forward(args)...); + } + } + + detour(uintptr_t address, uintptr_t func, uintptr_t wrapper, + uintptr_t uncounted, detail::trace_location location) + : detail::detour(address, func, wrapper, uncounted, location) {} }; template struct detour - : public detour {}; + : public detour { + using detour::detour; +}; -template -concept invocable = requires(F &&f, Args &&...args) { - std::invoke(std::forward(f), std::forward(args)...); +// The handle type for hooks on a target of signature `Sig`: +// `spud::hook h{&target, [](auto original, int n) { ... }};` +template struct hook_handle; +template struct hook_handle { + using type = detour; }; +template using hook = typename hook_handle::type; + +// Hooks a function pointer target. The signature comes from the target, and +// the hook is checked against it. +template + requires std::is_class_v> && + std::is_invocable_r_v &, R (*)(Args...), + Args...> +[[nodiscard]] auto create_detour( + R (*target)(Args...), F &&hook, + detail::trace_location location = detail::trace_location::current()) { + return detour(target, std::forward(hook), + location); +} -namespace detail { -#if __cpp_lib_source_location && SPUD_DETOUR_TRACING -template -inline auto create_detour_impl( - auto target, typename detail::function_traits::FuncType func, - const std::source_location location = std::source_location::current()) { - return ::spud::detour::FuncType>::create( - target, func, location); +template +[[nodiscard]] auto create_detour( + R (*target)(Args...), + std::type_identity_t hook, + detail::trace_location location = detail::trace_location::current()) { + return detour(target, hook, location); } + +namespace detail { + +// A member function as the free function the ABI calls: `this` first. A +// const member takes `const C *`. +template struct member_function; +#define SPUD_MEMBER_FUNCTION(qualifiers, self) \ + template \ + struct member_function { \ + using pointer = R (*)(self, Args...); \ + using result = R; \ + }; +SPUD_MEMBER_FUNCTION(, C *) +SPUD_MEMBER_FUNCTION(const, const C *) +SPUD_MEMBER_FUNCTION(noexcept, C *) +SPUD_MEMBER_FUNCTION(const noexcept, const C *) +#undef SPUD_MEMBER_FUNCTION + +// The code address of a non-virtual member function that needs no `this` +// adjustment, or null. Itanium member pointers are {address or vtable +// offset, adjustment}; the virtual flag is the low bit of the first word on +// x86-64 and of the adjustment on ARM64. MSVC uses a plain code address for +// single inheritance, and a vcall thunk for a virtual function, which this +// cannot tell apart; hook virtual functions through spud/pointer.h. +template uintptr_t member_address(M method) { + uintptr_t words[sizeof(M) / sizeof(uintptr_t)] = {}; + std::memcpy(words, &method, sizeof(words)); +#if defined(_MSC_VER) + return sizeof(M) == sizeof(uintptr_t) ? words[0] : 0; +#elif defined(SPUD_ARCH_ARM64) + return words[1] == 0 ? words[0] : 0; #else -template inline auto create_detour_impl(auto target, F func) { - return ::spud::detour::create(target, func); + return (words[0] & 1) == 0 && words[1] == 0 ? words[0] : 0; +#endif } + +} // namespace detail + +// Hooks a non-virtual member function. The hook takes the original, then +// `this`, then the arguments: `[](auto original, Widget *self, int x)`. A +// virtual function, or one reached through a `this` adjustment, gives an +// empty handle whose install returns kNotFound. +template + requires std::is_member_function_pointer_v +[[nodiscard]] auto create_detour( + M method, F &&hook, + detail::trace_location location = detail::trace_location::current()) { + using pointer = typename detail::member_function::pointer; +#if defined(_MSC_VER) + // MSVC returns class types from member functions through a hidden pointer + // that free functions do not use, so the hook could not match the ABI. + static_assert( + !std::is_class_v::result>, + "MSVC member functions returning a class type cannot be detoured"); #endif + return create_detour( + reinterpret_cast(detail::member_address(method)), + std::forward(hook), location); +} + +namespace detail { +// FuncType is the hook signature R (*)(R (*)(Args...), Args...) for a hook +// whose first parameter is the original, given as a function pointer, a +// member function pointer, or a lambda (whose operator() is instantiated with +// a placeholder original when it is generic). +template struct function_traits; + +template +struct function_traits + : public function_traits< + decltype(&Function::template operator())> {}; + +// A lambda or functor with a single, non-template operator(). +template + requires requires { &Function::operator(); } +struct function_traits + : function_traits {}; + +template +struct function_traits + : function_traits {}; + +template +struct function_traits + : function_traits {}; + +template +struct function_traits { + using FuncType = ReturnType (*)(ReturnType (*)(Arguments...), Arguments...); +}; + } // namespace detail -#if __cpp_lib_source_location && SPUD_DETOUR_TRACING +// Hooks a raw address. The signature comes from the hook, which takes the +// original as its first parameter. template > * = nullptr> -auto create_detour( +[[nodiscard]] auto create_detour( auto target, F func, - const std::source_location location = std::source_location::current()) { - using FuncT = typename spud::detail::function_traits::FuncType; - return detail::create_detour_impl(target, func, location); + detail::trace_location location = detail::trace_location::current()) + requires(!std::is_function_v> && + !std::is_member_function_pointer_v) +{ + using FuncT = typename detail::function_traits::FuncType; + return detour::create(target, static_cast(func), location); } template >> * = nullptr> -auto create_detour( - auto target, typename detail::function_traits_ptr::FuncType func, - const std::source_location location = std::source_location::current()) { - return detail::create_detour_impl< - typename detail::function_traits_ptr::FuncType>(target, func, - location); +[[nodiscard]] auto create_detour( + auto target, typename detail::function_traits::FuncType func, + detail::trace_location location = detail::trace_location::current()) { + using FuncT = typename detail::function_traits::FuncType; + return detour::create(target, func, location); } -#else -template > * = nullptr> -auto create_detour(auto target, F func) { - using FuncT = typename spud::detail::function_traits::FuncType; - return detail::create_detour_impl(target, func); + +template +concept installable_hook = requires(H &h) { + { h.try_install() } -> std::same_as; + { h.uninstall() } -> std::same_as; + { h.is_installed() } -> std::same_as; +}; + +namespace detail { +struct hook_step { + void *hook; + detour_error (*try_install)(void *); + detour_error (*uninstall)(void *); + bool (*is_installed)(void *); + bool install; + // Set for spud detours, which a rollback relinks in place. Other hooks are + // undone through try_install and uninstall. + detour *handle = nullptr; +}; + +template hook_step make_hook_step(H &h, bool install) { + hook_step step{&h, [](void *p) { return static_cast(p)->try_install(); }, + [](void *p) { return static_cast(p)->uninstall(); }, + [](void *p) { return static_cast(p)->is_installed(); }, + install}; + if constexpr (std::derived_from) { + step.handle = &h; + } + return step; } -template >> * = nullptr> -auto create_detour(auto target, - typename detail::function_traits_ptr::FuncType func) { - return detail::create_detour_impl< - typename detail::function_traits_ptr::FuncType>(target, func); +// Uninstalls run newest first. Spelled out to keep out of this +// header. +inline void reverse_steps(std::span steps) { + for (size_t i = 0, j = steps.size(); i + 1 < j; ++i, --j) + std::swap(steps[i], steps[j - 1]); +} + +// What iterating an lvalue `R` yields, for install_all and uninstall_all +// over a container. +template +using range_element = + std::remove_cvref_t()))>; +} // namespace detail + +// Stages installs and uninstalls, then commit() applies them in order under +// one suspend window. If a step fails, the steps already applied are undone +// newest first and commit() returns the failing step's error, or +// kRollbackFailed when an undo fails too. An uninstalled detour goes back to +// its old place in its chain. Staged handles must outlive commit(). A +// transaction dropped without commit() applies nothing. +class transaction { +public: + transaction() = default; + transaction(const transaction &) = delete; + transaction &operator=(const transaction &) = delete; + transaction(transaction &&) noexcept = default; + transaction &operator=(transaction &&) noexcept = default; + + template transaction &install(H &hook) { + steps_.push_back(detail::make_hook_step(hook, true)); + return *this; + } + + template transaction &uninstall(H &hook) { + steps_.push_back(detail::make_hook_step(hook, false)); + return *this; + } + + // Clears the staged steps whatever the result. + [[nodiscard]] detour_error commit() { + const detour_error err = detail::commit(steps_); + steps_.clear(); + return err; + } + +private: + std::vector steps_; +}; + +// A transaction of installs in argument order. Handles that were already +// installed stay installed if a later one fails. +template +[[nodiscard]] detour_error install_all(Hs &...hs) { + std::array steps{ + detail::make_hook_step(hs, true)...}; + return detail::commit(steps); +} + +// A transaction of uninstalls, last to first so stacked hooks on one target +// come off the head. On failure the handles already removed are put back. +template +[[nodiscard]] detour_error uninstall_all(Hs &...hs) { + std::array steps{ + detail::make_hook_step(hs, false)...}; + detail::reverse_steps(steps); + return detail::commit(steps); +} + +template + requires installable_hook> +[[nodiscard]] detour_error install_all(R &hooks) { + std::vector steps; + for (auto &h : hooks) { + steps.push_back(detail::make_hook_step(h, true)); + } + return detail::commit(steps); +} + +template + requires installable_hook> +[[nodiscard]] detour_error uninstall_all(R &hooks) { + std::vector steps; + for (auto &h : hooks) { + steps.push_back(detail::make_hook_step(h, false)); + } + detail::reverse_steps(steps); + return detail::commit(steps); } -#endif } // namespace spud -#if __cpp_lib_source_location && SPUD_DETOUR_TRACING -#define SPUD_AUTO_HOOK(target, func, loc) \ - spud::create_detour)>(target, func, loc); -#else +// `func` is a hook template taking the original as its first parameter. The +// hook records the line that expands this macro when tracing is on. #define SPUD_AUTO_HOOK(target, func) \ - spud::create_detour)>(target, func); -#endif + spud::create_detour)>(target, func) -// Creates a detour that will live until the end of the program -#if __cpp_lib_source_location && SPUD_DETOUR_TRACING -#define SPUD_STATIC_DETOUR(addr, fn) \ - (([=]() -> auto { \ - static auto dh_static_hook = \ - SPUD_AUTO_HOOK(addr, fn, std::source_location::current()); \ - return dh_static_hook.install().trampoline(); \ - })()) -#else +// Installs a permanent hook (see try_install_permanent) the first time it +// runs and returns its trampoline, or null when that install failed. Nothing +// unpatches the target during static destruction. #define SPUD_STATIC_DETOUR(addr, fn) \ - (([=]() -> auto { \ - static auto dh_static_hook = SPUD_AUTO_HOOK(addr, fn); \ - return dh_static_hook.install().trampoline(); \ + (([=]() { \ + static const auto spud_static_trampoline = [&] { \ + auto spud_static_hook = SPUD_AUTO_HOOK(addr, fn); \ + return spud_static_hook.try_install_permanent() == \ + spud::detour_error::kOk \ + ? spud_static_hook.trampoline() \ + : nullptr; \ + }(); \ + return spud_static_trampoline; \ })()) -#endif diff --git a/xmake-packages/packages/s/spud/spud-src/include/spud/diagnostics.h b/xmake-packages/packages/s/spud/spud-src/include/spud/diagnostics.h new file mode 100644 index 000000000..9194a2c59 --- /dev/null +++ b/xmake-packages/packages/s/spud/spud-src/include/spud/diagnostics.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include + +namespace spud { + +// errno, GetLastError, or a kern_return_t from the most recent failed OS +// call on the calling thread during a spud operation. 0 when the operation +// succeeded or failed above the OS (kDecodeFailed, kInsufficientBytes, ...). +// Cleared at the start of each install/uninstall, inspect_detour, +// query_memory, read_memory, write_memory and protect_memory call, so a +// stale code cannot attach to a later result. A lookup that finds nothing +// mapped reports what the OS gives for an unmapped address (ENOMEM on Linux), +// and a read of a page that cannot be made readable what it gives for an access +// fault (EFAULT on Linux). A failed protection_scope records its code too +// but does not clear an earlier one. Prefer this over +// patch_stats::last_os_error when more than one thread patches at a time. +int last_os_error(); + +// What the calling thread's most recent failed install or inspect_detour +// refused, past its detour_error: a short stable token such as +// "function_too_short" or "unknown_language_handler", and the address it +// concerns (an instruction, the end of the function, a language handler or +// a table). Both are empty when spud has nothing more specific. The reason +// has static storage. Cleared with last_os_error(), and again when an +// install, removal or inspect_detour succeeds. +struct failure_detail { + const char *reason = nullptr; + uintptr_t address = 0; +}; + +failure_detail last_failure_detail(); + +// Process-wide counters accumulated over every patch since the process +// started. Relaxed atomics; read them after install/uninstall has returned. +// `last_os_error` is the most recent code any thread recorded, from any +// spud operation. +struct patch_stats { + uint64_t suspend_attempts = 0; + uint64_t suspend_engaged = 0; + uint64_t suspend_fallback = 0; + uint64_t ip_redirects = 0; + int last_os_error = 0; +}; + +patch_stats query_patch_stats(); + +struct pool_stats { + size_t live_bytes = 0; + size_t reserved_bytes = 0; + size_t free_bytes = 0; + size_t alloc_failures = 0; + // Freed blocks the pool could not take back and leaked: the free list + // was full, or the free came inside a patch while the pool was busy. + size_t dropped_bytes = 0; +}; + +pool_stats query_pool_stats(); + +namespace detail { + +// Drops the calling thread's code and failure detail at the start of a +// public operation; the header-only pointer hooks call it. +void clear_os_error(); + +// Records why an operation refused, unless a refusal is already recorded +// since the last clear: the first one is the innermost and most specific. +// Returns false so a refusal site can `return note_failure(...)`. +bool note_failure(const char *reason, uintptr_t address = 0); + +} // namespace detail + +} // namespace spud diff --git a/xmake-packages/packages/s/spud/spud-src/include/spud/error.h b/xmake-packages/packages/s/spud/spud-src/include/spud/error.h new file mode 100644 index 000000000..d07ee4eb9 --- /dev/null +++ b/xmake-packages/packages/s/spud/spud-src/include/spud/error.h @@ -0,0 +1,74 @@ +#pragma once + +// The result codes detours, pointer hooks and signature scans report. + +#include +#include + +namespace spud { + +// Outcome of a checked install or explicit uninstall. `kOk` is the only +// success. Detailed OS codes stay in diagnostics; this is the caller-facing +// contract. +enum class detour_error { + kOk = 0, + kUnsupportedArch, + kDecodeFailed, + kInsufficientBytes, + kProtectionFailed, + kSuspensionUnavailable, + kTargetChanged, + kBusy, + kChainFull, + kControlFlowEnforced, + kNotFound, + kRollbackFailed, + // The copied code's unwind or exception-handler records could not be + // built or registered. last_failure_detail() names the refusal. + kUnwindFailed, +}; + +constexpr std::string_view to_string(detour_error error) noexcept { + switch (error) { + case detour_error::kOk: + return "ok"; + case detour_error::kUnsupportedArch: + return "unsupported_arch"; + case detour_error::kDecodeFailed: + return "decode_failed"; + case detour_error::kInsufficientBytes: + return "insufficient_bytes"; + case detour_error::kProtectionFailed: + return "protection_failed"; + case detour_error::kSuspensionUnavailable: + return "suspension_unavailable"; + case detour_error::kTargetChanged: + return "target_changed"; + case detour_error::kBusy: + return "busy"; + case detour_error::kChainFull: + return "chain_full"; + case detour_error::kControlFlowEnforced: + return "control_flow_enforced"; + case detour_error::kNotFound: + return "not_found"; + case detour_error::kRollbackFailed: + return "rollback_failed"; + case detour_error::kUnwindFailed: + return "unwind_failed"; + } + return "unknown"; +} + +// detour_error converts to std::error_code, so `if (std::error_code ec = +// hook.try_install())` and `ec.message()` work. `kOk` is the empty code. +const std::error_category &detour_category() noexcept; + +inline std::error_code make_error_code(detour_error error) noexcept { + return {static_cast(error), detour_category()}; +} + +} // namespace spud + +template <> +struct std::is_error_code_enum : std::true_type {}; diff --git a/xmake-packages/packages/s/spud/spud-src/include/spud/memory.h b/xmake-packages/packages/s/spud/spud-src/include/spud/memory.h new file mode 100644 index 000000000..c9eac286f --- /dev/null +++ b/xmake-packages/packages/s/spud/spud-src/include/spud/memory.h @@ -0,0 +1,100 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace spud { + +struct memory_region { + uintptr_t start = 0; + size_t size = 0; + mem_protection protection{}; +}; + +// The mapping that contains `address`, or nullopt when nothing is mapped. +[[nodiscard]] std::optional query_memory(uintptr_t address); +// Changes protection until changed again. See protection_scope for a +// change that puts itself back. +[[nodiscard]] bool protect_memory(uintptr_t address, size_t size, + mem_protection protection); + +// Copy bytes out of or into this process. Reads go through the OS first, +// so an unreadable page fails instead of faulting; execute-only pages are +// made readable for the copy. Writes make the range writable for the copy, +// put the protection back, and flush the instruction cache when the +// mapping is executable. A range may cross adjacent mappings with +// different protections; each is handled on its own. A range with an +// unmapped hole fails before anything is written, but a protection change +// that fails part-way leaves the mappings before it written. +[[nodiscard]] bool read_memory(uintptr_t address, void *out, size_t size); +[[nodiscard]] bool write_memory(uintptr_t address, const void *in, size_t size); + +[[nodiscard]] inline bool read_memory(uintptr_t address, + std::span out) { + return read_memory(address, out.data(), out.size()); +} + +[[nodiscard]] inline bool read_memory(uintptr_t address, + std::span out) { + return read_memory(address, out.data(), out.size()); +} + +[[nodiscard]] inline bool write_memory(uintptr_t address, + std::span in) { + return write_memory(address, in.data(), in.size()); +} + +[[nodiscard]] inline bool write_memory(uintptr_t address, + std::span in) { + return write_memory(address, in.data(), in.size()); +} + +// write_memory(address, {0x90, 0x90}) +[[nodiscard]] inline bool write_memory(uintptr_t address, + std::initializer_list bytes) { + return write_memory(address, bytes.begin(), bytes.size()); +} + +template + requires std::is_trivially_copyable_v +[[nodiscard]] std::optional read_memory(uintptr_t address) { + T value{}; + if (!read_memory(address, &value, sizeof(T))) { + return std::nullopt; + } + return value; +} + +template + requires std::is_trivially_copyable_v +[[nodiscard]] std::optional read_memory(const T *address) { + return read_memory(reinterpret_cast(address)); +} + +namespace detail { +template inline constexpr bool is_span_v = false; +template +inline constexpr bool is_span_v> = true; +} // namespace detail + +// Writes the object representation of `value`. A span is trivially +// copyable too, but its bytes are a pointer; spans take the overloads above. +template + requires std::is_trivially_copyable_v && (!detail::is_span_v) +[[nodiscard]] bool write_memory(uintptr_t address, const T &value) { + return write_memory(address, &value, sizeof(T)); +} + +template + requires std::is_trivially_copyable_v && (!detail::is_span_v) +[[nodiscard]] bool write_memory(T *address, const T &value) { + return write_memory(reinterpret_cast(address), value); +} + +} // namespace spud diff --git a/xmake-packages/packages/s/spud/spud-src/include/spud/memory/protection.h b/xmake-packages/packages/s/spud/spud-src/include/spud/memory/protection.h index 938b25130..877012a0d 100644 --- a/xmake-packages/packages/s/spud/spud-src/include/spud/memory/protection.h +++ b/xmake-packages/packages/s/spud/spud-src/include/spud/memory/protection.h @@ -5,38 +5,77 @@ namespace spud { -enum class mem_protection { - READ = 1 << 1, - WRITE = 1 << 2, - EXECUTE = 1 << 3, +// Page access. Combine with | and test with has_protection(). +enum class mem_protection : uint8_t { + kNone = 0, + kRead = 1 << 0, + kWrite = 1 << 1, + kExecute = 1 << 2, - READ_WRITE = READ | WRITE, - READ_WRITE_EXECUTE = READ_WRITE | EXECUTE, + kReadWrite = kRead | kWrite, + kReadExecute = kRead | kExecute, + kReadWriteExecute = kRead | kWrite | kExecute, }; +constexpr mem_protection operator|(mem_protection a, mem_protection b) { + return static_cast(static_cast(a) | + static_cast(b)); +} + +constexpr mem_protection operator&(mem_protection a, mem_protection b) { + return static_cast(static_cast(a) & + static_cast(b)); +} + +constexpr mem_protection &operator|=(mem_protection &a, mem_protection b) { + return a = a | b; +} + +// True when `have` grants every access in `want`. +constexpr bool has_protection(mem_protection have, mem_protection want) { + return (have & want) == want; +} + +// Changes the protection of the pages under [address, address + size) and +// puts the previous protection back when it goes out of scope. The range +// may cross into a neighbouring mapping only when that mapping has the +// same protection. ok() is false when nothing was changed; last_os_error() +// then says why. +// +// kWrite alone grants read as well. On Apple platforms kReadWriteExecute +// on ordinary code becomes read-write with copy-on-write: the kernel +// refuses writable and executable together outside MAP_JIT, so the pages +// cannot run while the scope is open. Patch live code with the detour +// API, which goes through a separate writable alias instead. class protection_scope { public: protection_scope(uintptr_t address, size_t size, mem_protection protect); ~protection_scope(); + protection_scope(const protection_scope &) = delete; + protection_scope &operator=(const protection_scope &) = delete; + protection_scope(protection_scope &&other) noexcept; + protection_scope &operator=(protection_scope &&other) noexcept; + + // Keeps the new protection: nothing is restored. void detach() { address_ = 0; size_ = 0; + ok_ = false; + } + + bool ok() const { + return ok_; } private: + void restore(); + uintptr_t address_ = 0; size_t size_ = 0; - - uintptr_t extra_ = 0; - - uintptr_t original_protection_; + uintptr_t aligned_address_ = 0; + size_t aligned_size_ = 0; + uintptr_t original_protection_ = 0; + bool ok_ = false; }; } // namespace spud - -#define SPUD_SCOPED_PROTECTION_IMPL(n, addr, size, protection) \ - protection_scope n{addr, size, protection}; - -#define SPUD_SCOPED_PROTECTION(addr, size, protection) \ - SPUD_SCOPED_PROTECTION_IMPL(SPUD_PP_CAT(spud_protect, __COUNTER__), addr, \ - size, protection) diff --git a/xmake-packages/packages/s/spud/spud-src/include/spud/pointer.h b/xmake-packages/packages/s/spud/spud-src/include/spud/pointer.h new file mode 100644 index 000000000..4c235ed6d --- /dev/null +++ b/xmake-packages/packages/s/spud/spud-src/include/spud/pointer.h @@ -0,0 +1,468 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace spud { + +// A GOT, IAT or symbol pointer slot found by find_import_slots. A lazily +// bound ELF slot still holds its PLT resolver stub until the first call. +// Calling through the stub would resolve the symbol and rebind the slot over +// the hook, so the finder resolves it up front: `lazy_stub` is that stub and +// `bound` the function it leads to. Both are null for a bound slot. Hook +// these with hook_pointer(slots, ...), which records `bound` as the +// original, rather than through the bare `slot`. +struct import_slot { + void **slot = nullptr; + void *lazy_stub = nullptr; + void *bound = nullptr; +}; + +namespace detail { + +// The untyped body of pointer_hook, kept out of line so each function type +// only instantiates a thin wrapper. `original_out` is the caller's function +// pointer variable, or null. +class slot_patch { +public: + slot_patch(std::vector slots, void *replacement, void *original_out); + slot_patch(std::span imports, void *replacement, + void *original_out); + slot_patch(slot_patch &&other) noexcept; + slot_patch &operator=(slot_patch &&other) noexcept; + ~slot_patch(); + + detour_error try_install(); + detour_error uninstall(); + void detach() { + installed_ = false; + } + + bool is_installed() const { + return installed_; + } + detour_error last_error() const { + return last_error_; + } + int last_os_error() const { + return last_os_error_; + } + const std::vector &originals() const { + return previous_; + } + +private: + detour_error finish(detour_error error, int os_error = 0); + void take(slot_patch &other); + + std::vector slots_; + // Parallel to slots_ and only as long as the last lazy import slot: + // {resolver stub, bound function}. Install records the bound function as + // the original while the slot still holds the stub. + std::vector> lazy_; + void *replacement_ = nullptr; + std::vector previous_; + void *original_out_ = nullptr; + bool installed_ = false; + detour_error last_error_ = detour_error::kOk; + int last_os_error_ = 0; +}; + +// The untyped body of local_vtable_hook. +class local_vtable_patch { +public: + local_vtable_patch(void *object, size_t index, void *replacement, + void *original_out); + local_vtable_patch(local_vtable_patch &&other) noexcept; + local_vtable_patch &operator=(local_vtable_patch &&other) noexcept; + ~local_vtable_patch(); + + bool set_vtable_length(size_t length) { + if (installed_) { + return false; + } + length_ = length; + return true; + } + detour_error try_install(); + detour_error uninstall(); + void detach(); + + bool is_installed() const { + return installed_; + } + detour_error last_error() const { + return last_error_; + } + int last_os_error() const { + return last_os_error_; + } + void *original() const { + const auto &originals = slot_.originals(); + return originals.empty() ? nullptr : originals.front(); + } + +private: + detour_error finish(detour_error error, int os_error = 0); + void release(); + void abandon_copy(); + void take(local_vtable_patch &other); + + void *object_ = nullptr; + size_t index_ = 0; + // Entries to copy from the address point; 0 measures with vtable_length. + size_t length_ = 0; + void *replacement_ = nullptr; + void *original_out_ = nullptr; + void **vptr_slot_ = nullptr; + // Address point of copy_, where the object's vptr points while installed. + void **copied_ = nullptr; + void **original_vtable_ = nullptr; + std::vector copy_; + slot_patch slot_{std::vector{}, nullptr, nullptr}; + bool installed_ = false; + detour_error last_error_ = detour_error::kOk; + int last_os_error_ = 0; +}; + +template void *erase_fn(Fn fn) { + static_assert(sizeof(Fn) == sizeof(void *)); + return reinterpret_cast(fn); +} + +// Member pointers to virtual functions encode a vtable byte offset; this +// reads the entry index and the this-adjustment out of one. +bool vtable_index_from_member(const void *member, size_t size, size_t *out, + ptrdiff_t *adj_out); +// The MS ABI half of that, built on every host so it can be tested anywhere: +// follows the code address to the x64 vcall thunk the compiler emitted. +bool decode_msvc_member(const void *member, size_t size, size_t *index, + ptrdiff_t *adj); + +// &vtable[index] of the object's table, or null when the object has no +// table or `index` is not below vtable_length. +void **checked_vtable_slot(void *object, size_t index); + +// A virtual member function as the free function its vtable slot holds: +// `this` becomes the first parameter. noexcept members take the same entry. +template struct vtable_entry_of; +template +struct vtable_entry_of { + using self = C *; + using fn = R (*)(C *, A...); +}; +template +struct vtable_entry_of { + using self = const C *; + using fn = R (*)(const C *, A...); +}; +template +struct vtable_entry_of + : vtable_entry_of {}; +template +struct vtable_entry_of + : vtable_entry_of {}; + +template using vtable_self_t = typename vtable_entry_of::self; +template using vtable_fn_t = typename vtable_entry_of::fn; + +// `object` may be any class derived from the method's own; converting it +// finds the right base subobject. `replacement` may be anything that +// converts to the slot's function pointer type, captureless lambdas +// included. +template +concept vtable_hook_args = std::is_member_function_pointer_v && + std::is_convertible_v> && + std::is_convertible_v>; + +// A null object in the result means `method` is not a virtual function. +template +std::pair resolve_virtual(vtable_self_t object, M method) { + size_t index = 0; + ptrdiff_t adj = 0; + if (object == nullptr || + !vtable_index_from_member(&method, sizeof(method), &index, &adj)) { + return {nullptr, 0}; + } + auto *self = const_cast(reinterpret_cast(object)); + return {self + adj, index}; +} + +} // namespace detail + +// Replaces function pointers in place. No trampoline, no prologue rewrite. +// Call original() or the optional out-pointer after a successful install. +template class pointer_hook { +public: + pointer_hook(Fn *slot, Fn replacement, Fn *original_out = nullptr) + : patch_(slot != nullptr + ? std::vector{reinterpret_cast(slot)} + : std::vector{}, + detail::erase_fn(replacement), original_out) {} + + pointer_hook(const std::vector &slots, Fn replacement, + Fn *original_out = nullptr) + : patch_(erase_slots(slots), detail::erase_fn(replacement), + original_out) {} + + pointer_hook(std::span imports, Fn replacement, + Fn *original_out = nullptr) + : patch_(imports, detail::erase_fn(replacement), original_out) {} + + // Patches every slot, or none when a page cannot be made writable. + // A slot that already holds the replacement is refused with + // kTargetChanged, since the hook would become its own original. + [[nodiscard]] detour_error try_install() { + return patch_.try_install(); + } + + // Puts back only slots that still hold the replacement. One hooked again + // since is left alone and reported as kTargetChanged. + [[nodiscard]] detour_error uninstall() { + return patch_.uninstall(); + } + + // Leaves the slots patched for good and drops ownership, so destroying the + // handle no longer restores them. Installing again is refused with + // kTargetChanged while a slot still holds the replacement. + pointer_hook &detach() { + patch_.detach(); + return *this; + } + + bool is_installed() const { + return patch_.is_installed(); + } + detour_error last_error() const { + return patch_.last_error(); + } + // errno / GetLastError behind last_error(), or 0. See spud::last_os_error(). + int last_os_error() const { + return patch_.last_os_error(); + } + Fn original() const { + const auto &originals = patch_.originals(); + return originals.empty() ? Fn{} : reinterpret_cast(originals.front()); + } + // The value each slot held before install, in slot order. + std::vector originals() const { + std::vector out; + out.reserve(patch_.originals().size()); + for (void *original : patch_.originals()) { + out.push_back(reinterpret_cast(original)); + } + return out; + } + +private: + static std::vector erase_slots(const std::vector &slots) { + std::vector out; + out.reserve(slots.size()); + for (Fn *slot : slots) { + out.push_back(reinterpret_cast(slot)); + } + return out; + } + + detail::slot_patch patch_; +}; + +// The slot decides the type, so a captureless lambda converts to it. +template +[[nodiscard]] pointer_hook +hook_pointer(Fn *slot, std::type_identity_t replacement, + std::type_identity_t *original_out = nullptr) { + return pointer_hook(slot, replacement, original_out); +} + +// Patches every slot find_import_slots returned. The replacement decides +// the type. +template +[[nodiscard]] pointer_hook hook_pointer(std::span slots, + Fn replacement, + Fn *original_out = nullptr) { + return pointer_hook(slots, replacement, original_out); +} + +// `object` starts with a vptr. `index` is an entry in that table as it sits +// in memory, not a compiler "nth virtual". There is no length on the table, +// so a bad index writes off the end. Prefer the member-pointer overload. +template +[[nodiscard]] pointer_hook hook_vtable(T *object, size_t index, + Fn replacement, + Fn *original_out = nullptr) { + void **vtable = + object != nullptr ? *reinterpret_cast(object) : nullptr; + if (vtable == nullptr) { + return pointer_hook(static_cast(nullptr), replacement, + original_out); + } + return pointer_hook(reinterpret_cast(&vtable[index]), replacement, + original_out); +} + +template + requires std::is_member_function_pointer_v +[[nodiscard]] std::optional vtable_index(M method) { + size_t index = 0; + if (!detail::vtable_index_from_member(&method, sizeof(method), &index, + nullptr)) { + return std::nullopt; + } + return index; +} + +template + requires detail::vtable_hook_args +[[nodiscard]] pointer_hook> +hook_vtable(T *object, M method, F replacement, + detail::vtable_fn_t *original_out = nullptr) { + using Fn = detail::vtable_fn_t; + const auto [target, index] = detail::resolve_virtual(object, method); + return pointer_hook( + reinterpret_cast(detail::checked_vtable_slot(target, index)), + static_cast(replacement), original_out); +} + +// Counts the leading entries of `vtable` that point into executable memory, +// stopping at the first that does not or at an unreadable slot. A table +// that still has not ended after 8192 entries is reported as 0, since what +// is being walked is likely not a vtable. +[[nodiscard]] size_t vtable_length(void **vtable); + +// Copies the vtable, points this object at the copy, and patches the copy. +// Other instances of the class keep the shared table. +template class local_vtable_hook { +public: + local_vtable_hook(void *object, size_t index, Fn replacement, + Fn *original_out = nullptr) + : patch_(object, index, detail::erase_fn(replacement), original_out) {} + + // How many entries from the address point to copy, for a table whose end + // vtable_length cannot find. Returns false once installed; 0 goes back to + // measuring. The entries must be readable. + bool set_vtable_length(size_t length) { + return patch_.set_vtable_length(length); + } + + [[nodiscard]] detour_error try_install() { + return patch_.try_install(); + } + [[nodiscard]] detour_error uninstall() { + return patch_.uninstall(); + } + + // Forgets the object without touching it, for an object that is about to + // be or already was destroyed: uninstalling then would write to freed + // memory. The copied table is leaked, since the object may still use it. + local_vtable_hook &detach() { + patch_.detach(); + return *this; + } + + bool is_installed() const { + return patch_.is_installed(); + } + detour_error last_error() const { + return patch_.last_error(); + } + // errno / GetLastError behind last_error(), or 0. See spud::last_os_error(). + int last_os_error() const { + return patch_.last_os_error(); + } + Fn original() const { + return reinterpret_cast(patch_.original()); + } + +private: + detail::local_vtable_patch patch_; +}; + +template +[[nodiscard]] local_vtable_hook +hook_vtable_local(T *object, size_t index, Fn replacement, + Fn *original_out = nullptr) { + return local_vtable_hook(object, index, replacement, original_out); +} + +template + requires detail::vtable_hook_args +[[nodiscard]] local_vtable_hook> +hook_vtable_local(T *object, M method, F replacement, + detail::vtable_fn_t *original_out = nullptr) { + using Fn = detail::vtable_fn_t; + const auto [target, index] = detail::resolve_virtual(object, method); + return hook_vtable_local(target, index, static_cast(replacement), + original_out); +} + +// Import slots for `name` in the modules whose path `module` names (see +// guides/pointer), sorted by slot address. An empty module searches every +// loaded module, since an import hook usually has to catch every caller; +// find_in_module reads an empty module as the main executable instead. +[[nodiscard]] std::vector find_import_slots(std::string_view name); +[[nodiscard]] std::vector +find_import_slots(std::string_view module, std::string_view name); +// Slots in every module that currently hold `function`, plus on ELF and +// Mach-O those of the symbol it is defined as. +[[nodiscard]] std::vector +find_import_slots_for(const void *function); + +template + requires std::is_pointer_v && + std::is_function_v> +[[nodiscard]] std::vector find_import_slots(Fn function) { + return find_import_slots_for(reinterpret_cast(function)); +} + +// By name the replacement decides the type; name it to pass a lambda: +// hook_import("abs", [](int n) { ... }). +template +[[nodiscard]] pointer_hook +hook_import(std::string_view module, std::string_view name, Fn replacement, + Fn *original_out = nullptr) { + return pointer_hook(find_import_slots(module, name), replacement, + original_out); +} + +template +[[nodiscard]] pointer_hook +hook_import(std::string_view name, Fn replacement, Fn *original_out = nullptr) { + return hook_import(std::string_view{}, name, replacement, original_out); +} + +template + requires std::is_pointer_v && + std::is_function_v> && + std::is_pointer_v && + std::is_function_v> +[[nodiscard]] pointer_hook hook_import(Imported imported, Fn replacement, + Fn *original_out = nullptr) { + return pointer_hook( + find_import_slots_for(reinterpret_cast(imported)), + replacement, original_out); +} + +// The imported function decides the type, so a captureless lambda works. +template + requires std::is_pointer_v && + std::is_function_v> && + std::is_class_v && std::is_convertible_v +[[nodiscard]] pointer_hook +hook_import(Imported imported, F replacement, + Imported *original_out = nullptr) { + return hook_import(imported, static_cast(replacement), + original_out); +} + +} // namespace spud diff --git a/xmake-packages/packages/s/spud/spud-src/include/spud/signature.h b/xmake-packages/packages/s/spud/spud-src/include/spud/signature.h index 056860299..22918cfdb 100644 --- a/xmake-packages/packages/s/spud/spud-src/include/spud/signature.h +++ b/xmake-packages/packages/s/spud/spud-src/include/spud/signature.h @@ -2,109 +2,422 @@ #include +#include #include +#include +#include #include -#include #include +#include +#include +#include #include namespace spud { -enum cpu_feature : uint32_t { - FEATURE_NONE = 0, -#if SPUD_ARCH_X86_FAMILY - FEATURE_SSE42 = 1 << 1, - FEATURE_AVX2 = 1 << 2, - - FEATURE_ALL = FEATURE_SSE42 | FEATURE_AVX2 -#elif SPUD_ARCH_ARM_FAMILY - FEATURE_NEON = 1 << 1, - FEATURE_ALL = FEATURE_NEON -#endif +inline constexpr size_t kUnlimitedMatches = static_cast(-1); + +// Tokens are whitespace-separated (space, tab, CR, LF) or packed without +// separators. A token is two hex digits, `?`, or `??`; both wildcard forms +// are one byte. A pattern needs at least one fixed byte, so empty and +// all-wildcard input fail with kNoFixedBytes. +enum class signature_error { + kOk = 0, + kOddDigit, + kInvalidHex, + kUnexpectedCharacter, + kNoFixedBytes, }; -namespace detail { +constexpr std::string_view to_string(signature_error error) noexcept { + switch (error) { + case signature_error::kOk: + return "ok"; + case signature_error::kOddDigit: + return "odd_digit"; + case signature_error::kInvalidHex: + return "invalid_hex"; + case signature_error::kUnexpectedCharacter: + return "unexpected_character"; + case signature_error::kNoFixedBytes: + return "no_fixed_bytes"; + } + return "unknown"; +} + +// signature_error converts to std::error_code like detour_error; kOk is the +// empty code. +const std::error_category &signature_category() noexcept; -struct signature_result { - std::span buffer; - uintptr_t offset; +inline std::error_code make_error_code(signature_error error) noexcept { + return {static_cast(error), signature_category()}; +} + +// A pattern that does not own its bytes. Position i matches when the +// scanned byte agrees with bytes[i] in the bits set in mask[i]. The parser +// writes masks of 0xFF or 0 and byte 0 at wildcards; a hand-built view may +// mask part of a byte (0xF0 for 4?). Scans anchor on whole fixed bytes, so a +// view with none falls back to a compare at every offset. compiled_signature +// and the result of sig() both convert to it. +struct signature_view { + const uint8_t *bytes = nullptr; + const uint8_t *mask = nullptr; + size_t size = 0; }; -void generate_mask_and_data(std::string_view signature, std::string &mask, - std::string &data); +namespace detail { -inline bool signature_does_match(uintptr_t offset, std::string_view mask, - std::string_view data, - std::span search_buffer) { - for (size_t i = 0; i < mask.size(); ++i) { - if (mask[i] == '?') - continue; +constexpr bool is_pattern_space(char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; +} - // Make sure we don't walk past the end of the search buffer - if (search_buffer.size() < offset + i) - return false; +constexpr int hex_nibble(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + return -1; +} - if (search_buffer[offset + i] != static_cast(data[i])) - return false; +struct parse_status { + signature_error error = signature_error::kOk; + size_t offset = 0; +}; + +// The one grammar, shared by compile_signature and sig(). Calls +// emit(byte, fixed) per token; on an error some tokens may already have been +// emitted. +template +constexpr parse_status parse_pattern(std::string_view text, Emit &&emit) { + size_t i = 0; + bool any_fixed = false; + auto skip_space = [&] { + while (i < text.size() && is_pattern_space(text[i])) { + ++i; + } + }; + skip_space(); + while (i < text.size()) { + const size_t token = i; + if (text[i] == '?') { + ++i; + if (i < text.size() && text[i] == '?') { + ++i; + } + emit(uint8_t{0}, false); + } else { + const int hi = hex_nibble(text[i++]); + if (hi < 0) { + return {signature_error::kUnexpectedCharacter, token}; + } + if (i >= text.size() || is_pattern_space(text[i])) { + return {signature_error::kOddDigit, token}; + } + const int lo = hex_nibble(text[i++]); + if (lo < 0) { + return {signature_error::kInvalidHex, token}; + } + emit(static_cast((hi << 4) | lo), true); + any_fixed = true; + } + skip_space(); } - return true; + if (!any_fixed) { + return {signature_error::kNoFixedBytes, 0}; + } + return {}; } -std::vector -find_matches(std::string_view mask, std::string_view data, - std::span search_buffer, - uint32_t features = cpu_feature::FEATURE_ALL); +// Deliberately not constexpr: reaching one from sig() turns a bad pattern +// into a compile error that names the problem. +inline void signature_has_an_invalid_token() {} +inline void signature_needs_a_fixed_byte() {} + } // namespace detail -struct signature_matches { - struct match { - match(detail::signature_result result, size_t offset = 0) - : result(result), adjustment(offset) {} +// A pattern parsed at compile time by sig(). Capacity is the literal's +// length, an upper bound on its byte count. +template struct static_signature { + uint8_t bytes[Capacity] = {}; + uint8_t mask[Capacity] = {}; + size_t size = 0; + + constexpr operator signature_view() const { + return {bytes, mask, size}; + } +}; + +// Parses a literal pattern at compile time: a malformed pattern does not +// compile, and scanning it never parses or allocates. +// +// constexpr auto kPrologue = spud::sig("48 89 5C 24 ? 57"); +// auto hit = spud::find_first(kPrologue, bytes); +template consteval static_signature sig(const char (&text)[N]) { + static_signature out; + const auto status = detail::parse_pattern( + std::string_view(text, N - 1), [&](uint8_t byte, bool fixed) { + out.bytes[out.size] = byte; + out.mask[out.size] = fixed ? uint8_t{0xFF} : uint8_t{0}; + ++out.size; + }); + if (status.error == signature_error::kNoFixedBytes) { + detail::signature_needs_a_fixed_byte(); + } else if (status.error != signature_error::kOk) { + detail::signature_has_an_invalid_token(); + } + return out; +} + +// A pattern parsed at run time. When parsing fails it keeps the error and +// matches nothing. +class compiled_signature { +public: + compiled_signature() = default; + + bool ok() const { + return error_ == signature_error::kOk; + } + signature_error error() const { + return error_; + } + // Offset of the bad token in the pattern text. + size_t error_offset() const { + return error_offset_; + } + size_t size() const { + return bytes_.size(); + } + std::span bytes() const { + return bytes_; + } + std::span mask() const { + return mask_; + } + + operator signature_view() const { + return {bytes_.data(), mask_.data(), bytes_.size()}; + } + +private: + friend compiled_signature compile_signature(std::string_view pattern); + + std::vector bytes_; + std::vector mask_; + signature_error error_ = signature_error::kNoFixedBytes; + size_t error_offset_ = 0; +}; + +[[nodiscard]] compiled_signature compile_signature(std::string_view pattern); + +// One occurrence, bounded by the buffer (or merged module mapping) it was +// found in. Every accessor stays inside that buffer and returns an empty +// value instead of reading past it. A default match is empty. +class signature_match { +public: + signature_match() = default; + signature_match(std::span buffer, size_t offset) + : buffer_(buffer), offset_(offset) {} + + explicit operator bool() const { + return buffer_.data() != nullptr; + } + + uintptr_t address() const { + return buffer_.data() == nullptr + ? 0 + : reinterpret_cast(buffer_.data() + offset_); + } - uintptr_t address() const { - return reinterpret_cast(result.buffer.data()) + offset(); + // The address as a pointer. A pointer type is returned as is, so + // as() matches executable_memory::as; any other type T gives + // T*, so as() is the same function pointer and as() a + // void*. + template auto as() const { + if constexpr (std::is_pointer_v) { + return reinterpret_cast(address()); + } else { + return reinterpret_cast(address()); } + } - uintptr_t call_target() const { - const auto data = result.buffer.data() + offset(); - return reinterpret_cast(data) + - *reinterpret_cast(data + 1u) + 5ul; + // The match moved by delta bytes, either way. Empty when that leaves the + // buffer; the end of the buffer itself is still inside. + signature_match adjust(ptrdiff_t delta) const { + size_t moved = 0; + if (!shifted(delta, &moved)) { + return {}; } + return {buffer_, moved}; + } - match adjust(size_t offset) { - return {result, adjustment + offset}; + // The T stored `offset` bytes from the match, when all of it lies inside + // the buffer. + template std::optional read(ptrdiff_t offset = 0) const { + static_assert(std::is_trivially_copyable_v); + size_t at = 0; + if (!shifted(offset, &at) || buffer_.size() - at < sizeof(T)) { + return std::nullopt; } + T value; + std::memcpy(&value, buffer_.data() + at, sizeof(T)); + return value; + } - private: - uintptr_t offset() const { - return result.offset + adjustment; + // Target of a RIP-relative rel32 in an instruction that starts at the + // match: the end of the instruction plus the signed displacement at + // disp_offset. `mov rax, [rip+X]` (48 8B 05 X) is rip(3, 7), `call X` + // (E8 X) is rip(1, 5). 0 when the displacement is outside the buffer. + uintptr_t rip(ptrdiff_t disp_offset, size_t insn_length) const { + const auto disp = read(disp_offset); + if (!disp) { + return 0; } + return address() + insn_length + + static_cast(static_cast(*disp)); + } - detail::signature_result result; - uintptr_t adjustment = 0; - }; + // x86 relative CALL (E8 rel32) at the match. 0 when the match is not a + // CALL or the instruction would read past the buffer. + uintptr_t call_target() const { + const auto opcode = read(); + return opcode == uint8_t{0xE8} ? rip(1, 5) : 0; + } - match get(size_t index) { - return result[index]; +private: + // offset_ + delta, when it stays within [0, size]. Written so that no + // intermediate value can overflow, PTRDIFF_MIN included. + bool shifted(ptrdiff_t delta, size_t *out) const { + if (buffer_.data() == nullptr) { + return false; + } + if (delta < 0) { + const size_t back = static_cast(-(delta + 1)) + 1; + if (back > offset_) { + return false; + } + *out = offset_ - back; + return true; + } + const auto forward = static_cast(delta); + if (forward > buffer_.size() - offset_) { + return false; + } + *out = offset_ + forward; + return true; } + std::span buffer_; + size_t offset_ = 0; +}; + +// Matches in address order. Indexing past the end gives an empty match, so +// `find_in_module(...).front().address()` is 0 rather than undefined. +class signature_matches { +public: + signature_matches() = default; + explicit signature_matches(std::vector matches) + : matches_(std::move(matches)) {} + + auto begin() const { + return matches_.begin(); + } + auto end() const { + return matches_.end(); + } size_t size() const { - return result.size(); + return matches_.size(); + } + bool empty() const { + return matches_.empty(); + } + signature_match operator[](size_t index) const { + return index < matches_.size() ? matches_[index] : signature_match{}; + } + signature_match front() const { + return (*this)[0]; } - - signature_matches(std::vector result = {}) - : result(result) {} private: - std::vector result; + std::vector matches_; +}; + +// Which matcher runs. kAutomatic picks the widest SIMD the CPU and OS +// support, except on x86 for a pattern whose rarest byte is rare in code, +// where the memchr-based scalar matcher is faster. Asking for a backend this +// build or CPU lacks runs the scalar matcher; every backend returns the same +// matches. +enum class scan_backend : uint8_t { + kAutomatic, + kScalar, + kSse2, + kAvx2, + kNeon, +}; + +struct scan_options { + // Stops the scan after this many matches. A short pattern over a whole + // process can otherwise match millions of times. + size_t max_results = kUnlimitedMatches; + scan_backend backend = scan_backend::kAutomatic; + // Threads find_first_each may split its patterns across, the calling + // thread included. Other calls scan on the calling thread only. + unsigned threads = 1; }; -signature_matches find_matches(std::string_view signature, - std::span search_buffer, - uint32_t features = cpu_feature::FEATURE_ALL); +// Text patterns are parsed on every call and match nothing when they fail +// to parse; compile_signature reports why. Pass a compiled_signature or a +// sig() pattern to skip the parse. +[[nodiscard]] signature_matches find_matches(signature_view pattern, + std::span buffer, + scan_options options = {}); +[[nodiscard]] signature_matches find_matches(std::string_view pattern, + std::span buffer, + scan_options options = {}); -signature_matches find_in_module(std::string_view signature, - std::string_view module = {}, - uint32_t features = cpu_feature::FEATURE_ALL); +// The first match without building a result list. max_results is ignored. +[[nodiscard]] signature_match find_first(signature_view pattern, + std::span buffer, + scan_options options = {}); +[[nodiscard]] signature_match find_first(std::string_view pattern, + std::span buffer, + scan_options options = {}); + +// Readable, executable mappings of one module. An empty name is the main +// executable; otherwise a name matches the file name, the file name without +// its extensions, or a trailing part of the path. +[[nodiscard]] signature_matches find_in_module(signature_view pattern, + std::string_view module = {}, + scan_options options = {}); +[[nodiscard]] signature_matches find_in_module(std::string_view pattern, + std::string_view module = {}, + scan_options options = {}); + +// The first match of each pattern in one module, in pattern order, with an +// empty match for a pattern that does not occur. The same results as +// find_in_module with max_results = 1 per pattern, but the module is +// enumerated once and read in cache-sized chunks that every pattern scans +// before the next chunk, so memory is read once rather than once per +// pattern. max_results is ignored; with threads above 1 the patterns are +// split across that many threads, each walking the module the same way. +// +// constexpr spud::signature_view kPatterns[] = {kSpawn, kTick, kDraw}; +// const auto hits = spud::find_first_each(kPatterns, "Game.exe"); +[[nodiscard]] std::vector +find_first_each(std::span patterns, + std::string_view module = {}, scan_options options = {}); + +// Every readable, executable mapping in the process. +[[nodiscard]] signature_matches find_in_process(signature_view pattern, + scan_options options = {}); +[[nodiscard]] signature_matches find_in_process(std::string_view pattern, + scan_options options = {}); } // namespace spud + +template <> +struct std::is_error_code_enum : std::true_type {}; diff --git a/xmake-packages/packages/s/spud/spud-src/include/spud/utils.h b/xmake-packages/packages/s/spud/spud-src/include/spud/utils.h index a4c49415d..74854f4f4 100644 --- a/xmake-packages/packages/s/spud/spud-src/include/spud/utils.h +++ b/xmake-packages/packages/s/spud/spud-src/include/spud/utils.h @@ -1,6 +1,10 @@ #pragma once #include +#include +#include +#include +#include // From: // http://stackoverflow.com/questions/1082192/how-to-generate-random-variable-names-in-c-using-macros/17624752#17624752 @@ -9,13 +13,109 @@ #define SPUD_PP_CAT_I(a, b) SPUD_PP_CAT_II(~, a##b) #define SPUD_PP_CAT_II(p, res) res -#define SPUD_UNUSED(x) (void)x - namespace spud { +// Process-wide choice for new hook preparations. Enabled by default. +// Existing hooks keep their records until freed. A build with +// SPUD_UNWIND_SUPPORT=0 always reports disabled. +void set_unwind_support(bool enabled); +bool unwind_support_enabled(); + +// Raw pool allocation; free with the same size. executable_memory below +// does the pairing for you. A free with another size, a second free, or a +// pointer the pool never handed out asserts in debug builds and is ignored +// in release builds, leaving the block allocated. The pool cannot describe +// code written by the caller, so these allocations never carry unwind +// records, whatever set_unwind_support says. void *alloc_executable_memory(size_t size); +// Not named `near`: defines near (and far) to nothing. +void *alloc_executable_memory(size_t size, uintptr_t hint); void free_executable_memory(void *ptr, size_t size); -// TODO(tashcan): Ideally this should be a scoped thing -void disable_jit_write_protection(); -void enable_jit_write_protection(); +// Readable, writable and executable memory from spud's pool, returned to +// the pool when the owner goes away. Write to it inside +// SPUD_SCOPED_JIT_WRITE() and call flush_cache() before running it, or use +// write_memory(), which does both. +class executable_memory { +public: + executable_memory() = default; + explicit executable_memory(size_t size) + : data_(static_cast(alloc_executable_memory(size))), + size_(data_ != nullptr ? size : 0) {} + // Prefers an address a direct branch at `hint` can reach (2GB on x86-64, + // 128MB on arm64), and takes any address when nothing nearby is free. + executable_memory(size_t size, uintptr_t hint) + : data_(static_cast(alloc_executable_memory(size, hint))), + size_(data_ != nullptr ? size : 0) {} + ~executable_memory() { + reset(); + } + + executable_memory(executable_memory &&other) noexcept + : data_(std::exchange(other.data_, nullptr)), + size_(std::exchange(other.size_, 0)) {} + executable_memory &operator=(executable_memory &&other) noexcept { + if (this != &other) { + reset(); + data_ = std::exchange(other.data_, nullptr); + size_ = std::exchange(other.size_, 0); + } + return *this; + } + executable_memory(const executable_memory &) = delete; + executable_memory &operator=(const executable_memory &) = delete; + + explicit operator bool() const { + return data_ != nullptr; + } + std::byte *data() const { + return data_; + } + size_t size() const { + return size_; + } + uintptr_t address() const { + return reinterpret_cast(data_); + } + std::span bytes() const { + return {data_, size_}; + } + + // The code at `offset` as a function pointer, e.g. as(). + template + requires std::is_pointer_v && + std::is_function_v> + Fn as(size_t offset = 0) const { + return reinterpret_cast(data_ + offset); + } + + void reset() { + if (data_ != nullptr) + free_executable_memory(data_, size_); + data_ = nullptr; + size_ = 0; + } + +private: + std::byte *data_ = nullptr; + size_t size_ = 0; +}; + +// Apple MAP_JIT pages are write-protected per thread; this scope makes +// them writable on the calling thread until it ends. Nested scopes keep the +// thread writable until the outermost one ends. Elsewhere it does nothing. +class jit_write_scope { +public: + jit_write_scope(); + ~jit_write_scope(); + + jit_write_scope(const jit_write_scope &) = delete; + jit_write_scope &operator=(const jit_write_scope &) = delete; +}; + +// Publishes writes to executable memory for instruction fetch. Returns +// false only when the platform cache operation itself failed. +[[nodiscard]] bool flush_cache(void *ptr, size_t size); } // namespace spud + +#define SPUD_SCOPED_JIT_WRITE() \ + ::spud::jit_write_scope SPUD_PP_CAT(spud_jit_write, __COUNTER__) diff --git a/xmake-packages/packages/s/spud/spud-src/src/capstone_utils.cc b/xmake-packages/packages/s/spud/spud-src/src/capstone_utils.cc deleted file mode 100644 index e69de29bb..000000000 diff --git a/xmake-packages/packages/s/spud/spud-src/src/capstone_utils.h b/xmake-packages/packages/s/spud/spud-src/src/capstone_utils.h deleted file mode 100644 index 2f27a507c..000000000 --- a/xmake-packages/packages/s/spud/spud-src/src/capstone_utils.h +++ /dev/null @@ -1,4 +0,0 @@ -namespace spud { -// - -} // namespace spud diff --git a/xmake-packages/packages/s/spud/spud-src/src/detour/aarch64/aarch64.cc b/xmake-packages/packages/s/spud/spud-src/src/detour/aarch64/aarch64.cc index cf160b98c..c4ef2d95b 100644 --- a/xmake-packages/packages/s/spud/spud-src/src/detour/aarch64/aarch64.cc +++ b/xmake-packages/packages/s/spud/spud-src/src/detour/aarch64/aarch64.cc @@ -1,437 +1,1255 @@ -#include "capstone/arm64.h" +// Owns the arm64 jump stub and the relocation of an overwritten prologue +// into a trampoline. +#include "aarch64.h" + +#include "detour/chain.h" #include "detour/detour_impl.h" -#include "relocators.h" +#include "memory/readable.h" +#include "memory/safe_patch.h" #include -#include -#include -#include -#include #include -#if SPUD_OS_APPLE -#include -#include -#include -#endif - -#if SPUD_OS_WIN -#include -#endif - #include +#include +#include #include #include +#include +#include #include #include #include +#if defined(__linux__) && defined(__aarch64__) +#include +#endif + namespace spud { namespace detail { namespace arm64 { -struct RelocationResult { - std::vector data_labels; - size_t copy_offset; - std::vector> relocation_offsets; +namespace { +// Stub words. +constexpr uint32_t kMovzX11 = 0xD2800000u | 11u; +constexpr uint32_t kMovkX11 = 0xF2800000u | 11u; +// Picks the entry shard into x10: stack bits 12-15 xor bits 20-23, as on +// x86-64, so threads on separate stacks usually count on separate lines. +// x10 is a temporary that carries no argument; the wrapper decrements the +// shard it names. +constexpr uint32_t kPickShard[] = { + 0x910003EAu, // mov x10, sp + 0xCA4A214Au, // eor x10, x10, x10, lsr #8 + 0xD34C3D4Au, // ubfx x10, x10, #12, #4 + 0x8B0A196Au, // add x10, x11, x10, lsl #6 +}; +static_assert(sizeof(entry_counts::shard) == 64 && entry_counts::kShards == 16); +static_assert(offsetof(entry_counts, shards) == 0); +// Counts the entry before jumping, because uninstall waits for it to drain. +// x16 and x17 are intra-procedure-call scratch registers. Neither form takes +// an offset, so the shard address is computed into x10 first. The LSE form +// avoids an exclusive retry loop on contended hooks; it is padded to the +// same four words so the stub size and boundaries do not depend on the CPU. +constexpr uint32_t kCountEntry[] = { + 0xC85FFD50u, // ldaxr x16, [x10] + 0x91000610u, // add x16, x16, #1 + 0xC811FD50u, // stlxr w17, x16, [x10] + 0x35FFFFB1u, // cbnz w17, #-12 }; +constexpr uint32_t kCountEntryLse[] = { + 0xD2800030u, // mov x16, #1 + 0xF8F00150u, // ldaddal x16, x16, [x10] + 0xD503201Fu, // nop + 0xD503201Fu, // nop +}; +// A permanent hook is never drained, so its stub neither picks a shard nor +// counts entries. +constexpr uint32_t kCountNone[] = {0xD503201Fu, 0xD503201Fu, 0xD503201Fu, + 0xD503201Fu}; +static_assert(sizeof(kCountEntry) == sizeof(kCountEntryLse)); +static_assert(sizeof(kCountEntry) == sizeof(kCountNone)); +static_assert(sizeof(kPickShard) == sizeof(kCountNone)); +// Word indexes: bti, four context moves, shard pick, count, four wrapper +// moves, br. +constexpr size_t kPickWord = 5; +constexpr size_t kCountWord = kPickWord + std::size(kPickShard); +constexpr size_t kWrapperWord = kCountWord + std::size(kCountEntry); +constexpr size_t kStubWords = kStubCodeSize / sizeof(uint32_t); +static_assert(kStubWords == kWrapperWord + 5); + +constexpr uint32_t kNop = 0xD503201Fu; +constexpr uint32_t kB = 0x14000000u; +constexpr uint32_t kBtiJ = 0xD503249Fu; +// ldr x, , with a zero immediate. +constexpr uint32_t kLdrLiteral = 0x58000000u; + +constexpr uint32_t br_word(unsigned reg) { + return 0xD61F0000u | (reg << 5); +} +constexpr uint32_t kBrX16 = br_word(16); + +// ldr x, ; br x: the absolute jump used when a +// direct B cannot reach. Only x16 and x17 are used: they are the +// intra-procedure-call registers a linker veneer may clobber too, and BR +// through them is accepted by a BTI c landing pad. +constexpr std::array absolute_jump(unsigned reg, + unsigned literal_words) { + return {kLdrLiteral | (literal_words << 5) | reg, br_word(reg)}; +} -static void write_relocation_data( - std::span target, const relocation_info &relocation_info, - std::vector relocation_data, - std::vector> relocation_offsets, - asmjit::CodeHolder &code, asmjit::a64::Assembler &assembler); - -static inline constexpr uint32_t kFullMask = - std::numeric_limits::max(); -static void write_adjusted_target(size_t offset_bits, size_t size_bits, - uint8_t *target_code, intptr_t target) { - constexpr auto kBitsPerUnsigned = 32; // Total bits in an unsigned integer - const uint8_t irrelevant_bits = - kBitsPerUnsigned - size_bits; // Bits that don't fit - const uint32_t current_integer_mask = - (kFullMask >> irrelevant_bits) << offset_bits; // Mask for current integer - - uint32_t *current_integer = - reinterpret_cast(target_code); // Pointer to current integer - *current_integer = (*current_integer & ~current_integer_mask) | - ((target << offset_bits) & - current_integer_mask); // Update current integer +// movz for hw 0, movk for the others: x11 = bits [16 * hw, 16 * hw + 16). +constexpr uint32_t mov_x11_word(unsigned hw) { + return (hw == 0 ? kMovzX11 : kMovkX11) | (hw << 21); } -static void offset_target(auto size, auto target_code, auto target) { - assert(false && "immediate size too small for relocation"); +constexpr uint32_t mov_x16_word(unsigned hw) { + // Load the wrapper after the counter loop releases x16 and x17. BR x16 + // is compatible with BTI c on a guarded stub page. + return (mov_x11_word(hw) & ~31u) | 16u; } -std::vector create_absolute_jump(uintptr_t target_address, - uintptr_t data) { - using namespace asmjit; - using namespace asmjit::a64; +// Every A64 form whose meaning depends on where it executes. +enum class pc_form : uint8_t { + none, + branch26, // b, bl + branch19, // b.cond, bc.cond, cbz, cbnz + branch14, // tbz, tbnz + branch9, // FEAT_CMPBR register and immediate comparisons + adr, + adrp, + literal, // ldr/ldrsw/prfm (literal) and ldr s/d/q (literal) +}; - CodeHolder code; - code.init(Environment{asmjit::Arch::kAArch64}); - Assembler assembler(&code); +pc_form classify(uint32_t w) { + if ((w & 0x7E000000u) == 0x74000000u) { + const unsigned condition = (w >> 21) & 7u; + const unsigned size = (w >> 14) & 3u; + const bool immediate = (w & (1u << 24)) != 0; + if (condition == 4 || condition == 5 || + (immediate ? (size & 1u) != 0 + : size == 1 || (size != 0 && (w >> 31) != 0))) { + return pc_form::none; + } + return pc_form::branch9; + } + if ((w & 0x7C000000u) == 0x14000000u) { + return pc_form::branch26; + } + if ((w & 0xFF000000u) == 0x54000000u || (w & 0x7E000000u) == 0x34000000u) { + return pc_form::branch19; + } + if ((w & 0x7E000000u) == 0x36000000u) { + return pc_form::branch14; + } + if ((w & 0x9F000000u) == 0x10000000u) { + return pc_form::adr; + } + if ((w & 0x9F000000u) == 0x90000000u) { + return pc_form::adrp; + } + if ((w & 0x3B000000u) == 0x18000000u) { + return pc_form::literal; + } + return pc_form::none; +} - Label L1 = assembler.newLabel(); - assembler.ldr(x9, ptr(L1)); - assembler.mov(x11, data); - assembler.br(x9); - assembler.bind(L1); - assembler.embed(&target_address, sizeof(target_address)); +bool is_branch(pc_form form) { + return form == pc_form::branch26 || form == pc_form::branch19 || + form == pc_form::branch14 || form == pc_form::branch9; +} - auto &buffer = code.textSection()->buffer(); - return {buffer.begin(), buffer.end()}; +size_t literal_width(uint32_t w) { + const unsigned opc = w >> 30; + if (((w >> 26) & 1u) != 0) { + return size_t{4} << opc; + } + return opc == 3 ? 0 : (opc == 1 ? 8 : 4); } -static bool needs_relocate(uintptr_t decoder_offset, uintptr_t code_end, - uintptr_t jump_size, const cs_insn &instruction, - const cs_aarch64 &detail) { +int64_t sign_extend(uint64_t value, unsigned bits) { + const uint64_t sign = uint64_t{1} << (bits - 1); + return static_cast((value ^ sign) - sign); +} - auto has_group = [&](uint8_t group) { - for (size_t i = 0; i < instruction.detail->groups_count; i++) { - if (instruction.detail->groups[i] == group) { - return true; - } - } - return false; - }; - if (instruction.id != ARM64_INS_ADRP && instruction.id != ARM64_INS_ADR && - !has_group(ARM64_GRP_BRANCH_RELATIVE) && !has_group(ARM64_GRP_JUMP) && - !has_group(ARM64_GRP_CALL)) { - return false; +// adr and adrp split their 21-bit immediate into immlo [30:29] and immhi +// [23:5]. +int64_t adr_immediate(uint32_t w) { + return sign_extend((((w >> 5) & 0x7FFFFu) << 2) | ((w >> 29) & 3u), 21); +} + +// The address `w` references when it executes at `pc`. adrp yields a page. +uintptr_t pc_target(uint32_t w, pc_form form, uintptr_t pc) { + int64_t offset = 0; + switch (form) { + case pc_form::branch26: + offset = sign_extend(w & 0x3FFFFFFu, 26) * 4; + break; + case pc_form::branch19: + case pc_form::literal: + offset = sign_extend((w >> 5) & 0x7FFFFu, 19) * 4; + break; + case pc_form::branch14: + offset = sign_extend((w >> 5) & 0x3FFFu, 14) * 4; + break; + case pc_form::branch9: + offset = sign_extend((w >> 5) & 0x1FFu, 9) * 4; + break; + case pc_form::adr: + offset = adr_immediate(w); + break; + case pc_form::adrp: + return (pc & ~uintptr_t{0xFFF}) + + static_cast(adr_immediate(w) * 4096); + case pc_form::none: + break; } + return pc + static_cast(offset); +} - uintptr_t result = 0; - const auto kRuntimeAddress = instruction.address - decoder_offset; - if (instruction.id == AARCH64_INS_ADRP) { - const auto &op = detail.operands[1]; - if (op.type == AARCH64_OP_IMM) { - const auto offset = op.imm; - result = offset; - } - } else if (instruction.id == AARCH64_INS_ADR) { - const auto &op = detail.operands[1]; - if (op.type == AARCH64_OP_IMM) { - const auto offset = op.imm; - result = offset; - } - } else if (instruction.detail->groups_count > 0) { - if (has_group(ARM64_GRP_BRANCH_RELATIVE) || has_group(ARM64_GRP_JUMP) || - has_group(ARM64_GRP_CALL)) { - if (detail.op_count == 0) - return false; +// Stores `value` in the `bits`-wide field at `lsb` if it fits. +bool put_field(uint32_t &w, int64_t value, unsigned lsb, unsigned bits) { + const int64_t limit = int64_t{1} << (bits - 1); + if (value < -limit || value >= limit) { + return false; + } + const uint32_t mask = ((uint32_t{1} << bits) - 1) << lsb; + w = (w & ~mask) | ((static_cast(value) << lsb) & mask); + return true; +} - // Indirect branches such as BR/BLR keep their destination in a - // register. They are position-independent and must be copied verbatim. - // Reading the operand union as an immediate makes the register id look - // like an address, incorrectly sends the instruction through the - // relative-branch relocator, and corrupts its opcode. - const auto &target_operand = detail.operands[detail.op_count - 1]; - if (target_operand.type != AARCH64_OP_IMM) { - return false; - } - result = target_operand.imm; +// Re-encodes `w` so that, executed at `pc`, it references `target`. False +// when the immediate cannot reach. +bool retarget(uint32_t &w, pc_form form, uintptr_t pc, uintptr_t target) { + if (form == pc_form::adr || form == pc_form::adrp) { + int64_t imm = static_cast(target - pc); + if (form == pc_form::adrp) { + imm = static_cast((target & ~uintptr_t{0xFFF}) - + (pc & ~uintptr_t{0xFFF})) / + 4096; + } + uint32_t hi = w; + if (!put_field(hi, imm >> 2, 5, 19)) { + return false; } + w = (hi & ~(3u << 29)) | ((static_cast(imm) & 3u) << 29); + return true; } - - if (result == 0) { + const auto delta = static_cast(target - pc); + if (delta % 4 != 0) { + return false; + } + switch (form) { + case pc_form::branch26: + return put_field(w, delta / 4, 0, 26); + case pc_form::branch19: + case pc_form::literal: + return put_field(w, delta / 4, 5, 19); + case pc_form::branch14: + return put_field(w, delta / 4, 5, 14); + case pc_form::branch9: + return put_field(w, delta / 4, 5, 9); + default: return false; } +} - const auto instruction_start = decoder_offset; - const auto inside_trampoline = - instruction_start < code_end || instruction_start <= jump_size; +// retaasppc/retabsppc