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