diff --git a/3rdparty/CMakeLists.txt b/3rdparty/CMakeLists.txt index c0d59cdce..636687c8a 100644 --- a/3rdparty/CMakeLists.txt +++ b/3rdparty/CMakeLists.txt @@ -2,6 +2,32 @@ set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) add_subdirectory(cpuinfo) +# The Windows guest red-zone patcher needs a decoder/encoder and a small +# trampoline assembler. Pin the same revisions used by shadPS4's implementation. +include(FetchContent) + +set(XBYAK_BUILD_TESTS OFF CACHE BOOL "" FORCE) +FetchContent_Declare( + xbyak + GIT_REPOSITORY https://github.com/herumi/xbyak.git + GIT_TAG 44a72f369268f7d552650891b296693e91db86bb + GIT_SHALLOW FALSE +) + +set(ZYDIS_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(ZYDIS_BUILD_TOOLS OFF CACHE BOOL "" FORCE) +set(ZYDIS_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(ZYDIS_BUILD_DOXYGEN OFF CACHE BOOL "" FORCE) +FetchContent_Declare( + zydis + GIT_REPOSITORY https://github.com/zyantific/zydis.git + GIT_TAG 120e0e705f8e3b507dc49377ac2879979f0d545c + GIT_SUBMODULES_RECURSE TRUE + GIT_SHALLOW FALSE +) + +FetchContent_MakeAvailable(xbyak zydis) + set(SDL_SHARED OFF CACHE BOOL "" FORCE) add_subdirectory(SDL2) diff --git a/CMakeLists.txt b/CMakeLists.txt index 74fa3268c..fdf11de89 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -292,7 +292,7 @@ if (KYTY_CLANG_CL) set_source_files_properties(${kyty_emulator_src} PROPERTIES COMPILE_FLAGS "-Wno-pragma-pack -Wno-deprecated-declarations -D_TIMESPEC_DEFINED") endif() -set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL2-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version) +set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL2-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version Zydis::Zydis xbyak::xbyak) # Linux system libraries required by the static FFmpeg archive. if(LINUX) @@ -514,6 +514,8 @@ if(BUILD_TESTING) add_test(NAME shader_recompiler_compute COMMAND $) add_test(NAME virtual_memory_allocation COMMAND $) + add_test(NAME guest_red_zone_patcher + COMMAND $ --red-zone-patcher-only) add_test(NAME command_scheduler_timeline COMMAND $ --scheduler-only) add_test(NAME stream_buffer_ring diff --git a/src/common/emulatorConfig.cpp b/src/common/emulatorConfig.cpp index 9c196f679..7f115da0c 100644 --- a/src/common/emulatorConfig.cpp +++ b/src/common/emulatorConfig.cpp @@ -101,6 +101,12 @@ bool ReadbackLinearImagesEnabled() { return g_config->readback_linear_images; } +#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS +bool RedZoneProtectionEnabled() { + return g_config->red_zone_protection_enabled; +} +#endif + const Keymap& GetKeymap() { return g_config->keymap; } diff --git a/src/common/emulatorConfig.h b/src/common/emulatorConfig.h index d8bf2709e..241c2eeb0 100644 --- a/src/common/emulatorConfig.h +++ b/src/common/emulatorConfig.h @@ -45,7 +45,10 @@ struct ConfigOptions { bool spirv_debug_printf_enabled = false; bool renderdoc_enabled = false; bool readback_linear_images = false; - Keymap keymap; +#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS + bool red_zone_protection_enabled = false; +#endif + Keymap keymap; }; void Load(const ConfigOptions& cfg); @@ -76,6 +79,9 @@ bool SpirvDebugPrintfEnabled(); bool RenderDocEnabled(); bool ReadbackLinearImagesEnabled(); +#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS +bool RedZoneProtectionEnabled(); +#endif const Keymap& GetKeymap(); diff --git a/src/graphics/guest_gpu/command_processor/pm4Handlers.cpp b/src/graphics/guest_gpu/command_processor/pm4Handlers.cpp index f47527d90..034a72222 100644 --- a/src/graphics/guest_gpu/command_processor/pm4Handlers.cpp +++ b/src/graphics/guest_gpu/command_processor/pm4Handlers.cpp @@ -2135,7 +2135,7 @@ KYTY_CP_OP_PARSER(CpOpClearState) { EXIT_NOT_IMPLEMENTED(cmd_id != 0xc0001200); EXIT_NOT_IMPLEMENTED((buffer[0] & ~0xfu) != 0); - cp.Reset(); + cp.ApplyContextStateOperation(ContextStateOperation::Clear); return 1; } diff --git a/src/launcher/forms/configuration_edit_dialog.ui b/src/launcher/forms/configuration_edit_dialog.ui index e94c74241..67deb7b11 100644 --- a/src/launcher/forms/configuration_edit_dialog.ui +++ b/src/launcher/forms/configuration_edit_dialog.ui @@ -88,6 +88,16 @@ + + + + Helps prevent crashes in some games on Windows + + + Windows SysV red zone crash protection (Experimental) + + + diff --git a/src/launcher/include/configuration.h b/src/launcher/include/configuration.h index 4a0d7e6b0..36c55217c 100644 --- a/src/launcher/include/configuration.h +++ b/src/launcher/include/configuration.h @@ -99,7 +99,10 @@ public: QString printf_output_file = "_kyty.txt"; ProfilerDirection profiler_direction = ProfilerDirection::None; bool renderdoc_enabled = false; - QStringList host_input_mapping; +#if defined(_WIN32) + bool red_zone_protection_enabled = false; +#endif + QStringList host_input_mapping; QString elf = QStringLiteral("eboot.bin"); @@ -119,7 +122,10 @@ public: printf_output_file = other.printf_output_file; profiler_direction = other.profiler_direction; renderdoc_enabled = other.renderdoc_enabled; - host_input_mapping = other.host_input_mapping; +#if defined(_WIN32) + red_zone_protection_enabled = other.red_zone_protection_enabled; +#endif + host_input_mapping = other.host_input_mapping; } void CopyFrom(const Configuration& other) { @@ -156,6 +162,9 @@ public: KYTY_CFG_SET(printf_output_file); KYTY_CFG_SET(profiler_direction); KYTY_CFG_SET(renderdoc_enabled); +#if defined(_WIN32) + KYTY_CFG_SET(red_zone_protection_enabled); +#endif s->setValue("host_input_mapping", host_input_mapping); KYTY_CFG_SET(elf); } @@ -183,6 +192,10 @@ public: KYTY_CFG_GET(printf_output_file); KYTY_CFG_GET(profiler_direction); KYTY_CFG_GET(renderdoc_enabled); +#if defined(_WIN32) + red_zone_protection_enabled = + s->value("red_zone_protection_enabled", red_zone_protection_enabled).toBool(); +#endif host_input_mapping = s->value("host_input_mapping", host_input_mapping).toStringList(); elf = s->value("elf", elf).toString(); } diff --git a/src/launcher/src/configurationEditDialog.cpp b/src/launcher/src/configurationEditDialog.cpp index 4975b6525..470712ecf 100644 --- a/src/launcher/src/configurationEditDialog.cpp +++ b/src/launcher/src/configurationEditDialog.cpp @@ -164,6 +164,11 @@ void ConfigurationEditDialog::Init(const Configuration& info) { m_ui->checkBox_shader_validation->setChecked(info.shader_validation_enabled); m_ui->checkBox_vulkan_validation->setChecked(info.vulkan_validation_enabled); m_ui->checkBox_renderdoc_capture->setChecked(info.renderdoc_enabled); +#if defined(_WIN32) + m_ui->checkBox_red_zone_protection->setChecked(info.red_zone_protection_enabled); +#else + m_ui->checkBox_red_zone_protection->setVisible(false); +#endif ListInit(m_ui->comboBox_shader_optimization_type, info.shader_optimization_type); ListInit(m_ui->comboBox_shader_log_direction, info.shader_log_direction); m_ui->lineEdit_shader_log_folder->setText(info.shader_log_folder); @@ -286,7 +291,10 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui) { info.vulkan_validation_enabled = ui.checkBox_vulkan_validation->isChecked(); info.shader_validation_enabled = ui.checkBox_shader_validation->isChecked(); info.renderdoc_enabled = ui.checkBox_renderdoc_capture->isChecked(); - info.shader_optimization_type = TextToEnum( +#if defined(_WIN32) + info.red_zone_protection_enabled = ui.checkBox_red_zone_protection->isChecked(); +#endif + info.shader_optimization_type = TextToEnum( ui.comboBox_shader_optimization_type->currentText()); info.shader_log_direction = TextToEnum( ui.comboBox_shader_log_direction->currentText()); diff --git a/src/launcher/src/mainDialog.cpp b/src/launcher/src/mainDialog.cpp index 4edb30aa0..38f12f103 100644 --- a/src/launcher/src/mainDialog.cpp +++ b/src/launcher/src/mainDialog.cpp @@ -220,6 +220,11 @@ static QStringList CreateEmulatorArgs(const Configuration& info) { args << "--printf-output-file" << info.printf_output_file; args << "--profiler-direction" << EnumToText(info.profiler_direction); args << "--spirv-debug-printf" << "false"; +#if defined(_WIN32) + if (info.red_zone_protection_enabled) { + args << "--redzone"; + } +#endif for (const auto& binding: info.host_input_mapping) { args << "--keymap" << binding; } diff --git a/src/libs/libKernel.cpp b/src/libs/libKernel.cpp index 750eb9dfd..e72df0cf2 100644 --- a/src/libs/libKernel.cpp +++ b/src/libs/libKernel.cpp @@ -516,6 +516,7 @@ static void QueuePendingSignal(Pthread thread, int signum) { PthreadQueuePendingSignal(thread, signum); } +#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS static void WaitForSignalDispatch(Pthread thread, int signum) { constexpr auto DISPATCH_WAIT_STEP = std::chrono::microseconds(1000); constexpr auto DISPATCH_WAIT_MAX = std::chrono::milliseconds(2); @@ -526,6 +527,7 @@ static void WaitForSignalDispatch(Pthread thread, int signum) { waited += DISPATCH_WAIT_STEP; } } +#endif static bool TakePendingSignal(Pthread thread, int signum) { return PthreadTakePendingSignal(thread, signum); @@ -1070,60 +1072,6 @@ static void SignalApcHandler(void* arg1, void* arg2, void* /*arg3*/, PCONTEXT co } } -static bool DispatchSignalWithSuspendedThreadContext(HANDLE target_thread, Pthread thread, - int signum, exception_handler_func_t handler) { - if (target_thread == nullptr || thread == nullptr || handler == nullptr) { - return false; - } - - if (!PthreadHasPendingSignal(thread, signum)) { - return true; - } - - if (SuspendThread(target_thread) == static_cast(-1)) { - return false; - } - - bool dispatched = false; - CONTEXT host_ctx {}; - host_ctx.ContextFlags = CONTEXT_FULL | CONTEXT_SEGMENTS; - if (GetThreadContext(target_thread, &host_ctx) != FALSE) { - if (TakePendingSignal(thread, signum)) { - auto ctx = CreateSignalUcontext(&host_ctx); - auto guest_context = IsGuestCodeAddress(ctx.uc_mcontext.mc_rip); - HANDLE helper_thread_handle = nullptr; - if (guest_context) { - if (DuplicateHandle(GetCurrentProcess(), target_thread, GetCurrentProcess(), - &helper_thread_handle, THREAD_SUSPEND_RESUME, FALSE, - 0) == FALSE) { - ResumeThread(target_thread); - return false; - } - } else { - SanitizeNonGuestSignalUcontext(&ctx, thread); - ResumeThread(target_thread); - } - - std::thread([helper_thread_handle, thread, signum, handler, ctx]() mutable { - SignalDispatchScope scope; - auto* previous_self = PthreadSwapSelfForSignal(thread); - handler(signum, &ctx); - PthreadSwapSelfForSignal(previous_self); - if (helper_thread_handle != nullptr) { - ResumeThread(helper_thread_handle); - CloseHandle(helper_thread_handle); - } - }).detach(); - dispatched = true; - return true; - } - dispatched = true; - } - - ResumeThread(target_thread); - - return dispatched; -} #endif void KernelDispatchPendingSignalForCurrentThread() { @@ -1206,8 +1154,7 @@ static int KYTY_SYSV_ABI KernelRaiseException(Pthread thread, int signum) { } HANDLE target_thread = - OpenThread(THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT, FALSE, - static_cast(target_thread_id)); + OpenThread(THREAD_SET_CONTEXT, FALSE, static_cast(target_thread_id)); if (target_thread == nullptr) { return KERNEL_ERROR_EINVAL; } @@ -1231,10 +1178,6 @@ static int KYTY_SYSV_ABI KernelRaiseException(Pthread thread, int signum) { Common::CondVar::SignalThread(PthreadGetUniqueId(thread)); PthreadWakeForSignal(thread); - WaitForSignalDispatch(thread, signum); - if (PthreadHasPendingSignal(thread, signum)) { - DispatchSignalWithSuspendedThreadContext(target_thread, thread, signum, handler); - } CloseHandle(target_thread); return OK; #elif defined(__x86_64__) diff --git a/src/loader/elf.h b/src/loader/elf.h index 3680620ff..6515f94ab 100644 --- a/src/loader/elf.h +++ b/src/loader/elf.h @@ -53,6 +53,7 @@ constexpr Elf64_Half ET_DYNAMIC = 0xfe18; // Shared constexpr Elf64_Word PT_LOAD = 1; constexpr Elf64_Word PT_DYNAMIC = 2; constexpr Elf64_Word PT_TLS = 7; +constexpr Elf64_Word PT_GNU_EH_FRAME = 0x6474e550; constexpr Elf64_Word PT_OS_DYNLIBDATA = 0x61000000; constexpr Elf64_Word PT_OS_PROCPARAM = 0x61000001; constexpr Elf64_Word PT_OS_RELRO = 0x61000010; diff --git a/src/loader/redZonePatcher.cpp b/src/loader/redZonePatcher.cpp new file mode 100644 index 000000000..5c41bcb36 --- /dev/null +++ b/src/loader/redZonePatcher.cpp @@ -0,0 +1,1443 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "loader/redZonePatcher.h" + +#include "common/assert.h" +#include "common/logging/log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef min +#undef min +#endif +#ifdef max +#undef max +#endif + +using namespace Xbyak::util; + +namespace Loader { + +using u8 = uint8_t; +using s8 = int8_t; +using u16 = uint16_t; +using s16 = int16_t; +using u32 = uint32_t; +using s32 = int32_t; +using u64 = uint64_t; +using s64 = int64_t; + +#define ASSERT(condition) EXIT_IF(!(condition)) + +constexpr size_t NearJumpSize = 5; + +#if defined(_WIN32) + +struct PatchModule { + std::mutex mutex {}; + u8* start = nullptr; + u8* end = nullptr; + std::set patched; + Xbyak::CodeGenerator patch_gen; + Xbyak::CodeGenerator trampoline_gen; + bool trampoline_exhausted = false; + + PatchModule(u8* module_ptr, u64 module_size, u8* trampoline_ptr, u64 trampoline_size) + : start(module_ptr), end(module_ptr + module_size), patch_gen(module_size, module_ptr), + trampoline_gen(trampoline_size, trampoline_ptr) {} +}; + +static std::map g_patch_modules; + +static PatchModule* GetContainingModule(const void* ptr) { + auto upper = g_patch_modules.upper_bound(reinterpret_cast(ptr)); + if (upper == g_patch_modules.begin()) { + return nullptr; + } + auto* module = &std::prev(upper)->second; + auto* address = static_cast(ptr); + return address >= module->start && address < module->end ? module : nullptr; +} + +static bool HandleTrampolineError(PatchModule* module, const Xbyak::Error& error) { + if (static_cast(error) != Xbyak::ERR_CODE_IS_TOO_BIG) { + return false; + } + if (!module->trampoline_exhausted) { + LOGF("Windows guest red-zone trampoline space exhausted for module %p\n", + static_cast(module->start)); + module->trampoline_exhausted = true; + } + return true; +} + +static ZydisDecoder& GetDecoder() { + static ZydisDecoder decoder = [] { + ZydisDecoder value {}; + EXIT_IF(!ZYAN_SUCCESS( + ZydisDecoderInit(&value, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64))); + return value; + }(); + return decoder; +} + +static ZyanStatus DecodeInstruction(ZydisDecodedInstruction& instruction, + ZydisDecodedOperand* operands, void* data, u64 size) { + return ZydisDecoderDecodeFull(&GetDecoder(), data, size, &instruction, operands); +} + +#endif +#if defined(_WIN32) + +namespace { + +constexpr size_t GuestRedZoneSize = 128; +constexpr size_t ShortJumpSize = 2; +using RedZoneMask = std::bitset; + +struct DecodedCodeInstruction { + uintptr_t address {}; + ZydisDecodedInstruction instruction {}; + std::array operands {}; + RedZoneMask red_zone_use {}; + RedZoneMask red_zone_def {}; + RedZoneMask red_zone_live {}; + bool accesses_memory {}; + bool uses_stack_pointer {}; + bool has_red_zone_operand {}; + bool has_unmodeled_red_zone_operand {}; + bool changes_stack_pointer {}; + bool replaces_stack_pointer {}; + std::optional stack_pointer_delta; +}; + +struct DecodedFunction { + std::map instructions; + std::set branch_targets; + bool uses_red_zone {}; + bool has_indirect_branch {}; + bool requires_conservative_red_zone_tracking {}; +}; + +struct InstructionRewrite { + bool protect_red_zone {}; + bool protected_indirect_call {}; +}; + +bool IsStackPointerRegister(ZydisRegister reg) { + return reg != ZYDIS_REGISTER_NONE && + ZydisRegisterGetLargestEnclosing(ZYDIS_MACHINE_MODE_LONG_64, reg) == ZYDIS_REGISTER_RSP; +} + +bool IsControlFlowTerminator(const ZydisDecodedInstruction& instruction) { + return instruction.meta.category == ZYDIS_CATEGORY_UNCOND_BR || + instruction.meta.category == ZYDIS_CATEGORY_RET || + instruction.meta.category == ZYDIS_CATEGORY_INTERRUPT || + instruction.meta.category == ZYDIS_CATEGORY_SYSRET || + instruction.mnemonic == ZYDIS_MNEMONIC_UD2; +} + +uintptr_t GetRelativeTarget(const DecodedCodeInstruction& decoded) { + for (u8 index = 0; index < decoded.instruction.operand_count_visible; ++index) { + const auto& operand = decoded.operands[index]; + if (operand.type != ZYDIS_OPERAND_TYPE_IMMEDIATE || !operand.imm.is_relative) { + continue; + } + + ZyanU64 target {}; + if (ZYAN_SUCCESS(ZydisCalcAbsoluteAddress(&decoded.instruction, &operand, decoded.address, + &target))) { + return target; + } + } + return 0; +} + +DecodedCodeInstruction DecodeCodeInstruction(uintptr_t address, uintptr_t end) { + DecodedCodeInstruction decoded {.address = address}; + const auto status = DecodeInstruction(decoded.instruction, decoded.operands.data(), + reinterpret_cast(address), end - address); + if (!ZYAN_SUCCESS(status)) { + decoded.instruction.length = 0; + return decoded; + } + + for (u8 index = 0; index < decoded.instruction.operand_count; ++index) { + const auto& operand = decoded.operands[index]; + if (operand.type == ZYDIS_OPERAND_TYPE_REGISTER) { + decoded.uses_stack_pointer |= IsStackPointerRegister(operand.reg.value); + if (IsStackPointerRegister(operand.reg.value) && + (operand.actions & ZYDIS_OPERAND_ACTION_MASK_WRITE) != 0 && + decoded.instruction.meta.category != ZYDIS_CATEGORY_CALL && + decoded.instruction.meta.category != ZYDIS_CATEGORY_RET) { + decoded.changes_stack_pointer = true; + } + continue; + } + if (operand.type != ZYDIS_OPERAND_TYPE_MEMORY) { + continue; + } + + const bool stack_relative = + IsStackPointerRegister(operand.mem.base) || IsStackPointerRegister(operand.mem.index); + decoded.uses_stack_pointer |= stack_relative; + constexpr ZydisOperandActions MemoryAccessMask = + ZYDIS_OPERAND_ACTION_MASK_READ | ZYDIS_OPERAND_ACTION_MASK_WRITE; + if (decoded.instruction.mnemonic != ZYDIS_MNEMONIC_LEA && + decoded.instruction.mnemonic != ZYDIS_MNEMONIC_NOP && !stack_relative && + (operand.actions & MemoryAccessMask) != 0) { + decoded.accesses_memory = true; + } + } + + if (decoded.changes_stack_pointer) { + const auto& operands = decoded.operands; + if (decoded.instruction.mnemonic == ZYDIS_MNEMONIC_MOV && + operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER && + IsStackPointerRegister(operands[0].reg.value) && + operands[1].type == ZYDIS_OPERAND_TYPE_MEMORY) { + decoded.replaces_stack_pointer = true; + } else if ((decoded.instruction.mnemonic == ZYDIS_MNEMONIC_ADD || + decoded.instruction.mnemonic == ZYDIS_MNEMONIC_SUB) && + operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER && + IsStackPointerRegister(operands[0].reg.value) && + operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE) { + const s64 immediate = operands[1].imm.is_signed + ? operands[1].imm.value.s + : static_cast(operands[1].imm.value.u); + decoded.stack_pointer_delta = + decoded.instruction.mnemonic == ZYDIS_MNEMONIC_ADD ? immediate : -immediate; + } else if (decoded.instruction.mnemonic == ZYDIS_MNEMONIC_LEA && + operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER && + IsStackPointerRegister(operands[0].reg.value) && + operands[1].type == ZYDIS_OPERAND_TYPE_MEMORY && + IsStackPointerRegister(operands[1].mem.base) && + operands[1].mem.index == ZYDIS_REGISTER_NONE) { + decoded.stack_pointer_delta = operands[1].mem.disp.value; + } else if (decoded.instruction.mnemonic == ZYDIS_MNEMONIC_PUSH || + decoded.instruction.mnemonic == ZYDIS_MNEMONIC_PUSHF || + decoded.instruction.mnemonic == ZYDIS_MNEMONIC_PUSHFD || + decoded.instruction.mnemonic == ZYDIS_MNEMONIC_PUSHFQ) { + decoded.stack_pointer_delta = -static_cast(sizeof(u64)); + } else if (decoded.instruction.mnemonic == ZYDIS_MNEMONIC_POP || + decoded.instruction.mnemonic == ZYDIS_MNEMONIC_POPF || + decoded.instruction.mnemonic == ZYDIS_MNEMONIC_POPFD || + decoded.instruction.mnemonic == ZYDIS_MNEMONIC_POPFQ) { + decoded.stack_pointer_delta = sizeof(u64); + } + } + + for (u8 index = 0; index < decoded.instruction.operand_count_visible; ++index) { + const auto& operand = decoded.operands[index]; + if (operand.type != ZYDIS_OPERAND_TYPE_MEMORY || + !IsStackPointerRegister(operand.mem.base) || operand.mem.disp.size == 0 || + operand.mem.disp.value >= 0) { + continue; + } + + decoded.has_red_zone_operand = true; + if (decoded.instruction.mnemonic == ZYDIS_MNEMONIC_LEA) { + if (decoded.stack_pointer_delta.has_value()) { + decoded.has_red_zone_operand = false; + continue; + } + decoded.has_unmodeled_red_zone_operand = true; + continue; + } + + const s64 access_start = operand.mem.disp.value; + const s64 access_size = std::max(operand.size / 8, 1); + const s64 range_start = std::max(access_start, -static_cast(GuestRedZoneSize)); + const s64 range_end = std::min(access_start + access_size, 0LL); + for (s64 offset = range_start; offset < range_end; ++offset) { + const size_t bit = static_cast(offset + static_cast(GuestRedZoneSize)); + if ((operand.actions & ZYDIS_OPERAND_ACTION_MASK_READ) != 0) { + decoded.red_zone_use.set(bit); + } + if ((operand.actions & ZYDIS_OPERAND_ACTION_WRITE) != 0) { + decoded.red_zone_def.set(bit); + } + } + } + return decoded; +} + +bool IsSameRegister(ZydisRegister lhs, ZydisRegister rhs) { + return lhs != ZYDIS_REGISTER_NONE && rhs != ZYDIS_REGISTER_NONE && + ZydisRegisterGetLargestEnclosing(ZYDIS_MACHINE_MODE_LONG_64, lhs) == + ZydisRegisterGetLargestEnclosing(ZYDIS_MACHINE_MODE_LONG_64, rhs); +} + +bool WritesRegister(const DecodedCodeInstruction& decoded, ZydisRegister reg) { + return std::ranges::any_of( + std::span {decoded.operands}.first(decoded.instruction.operand_count), + [reg](const ZydisDecodedOperand& operand) { + return operand.type == ZYDIS_OPERAND_TYPE_REGISTER && + (operand.actions & ZYDIS_OPERAND_ACTION_MASK_WRITE) != 0 && + IsSameRegister(operand.reg.value, reg); + }); +} + +std::optional> +ResolveBoundedJumpTable(const DecodedFunction& function, uintptr_t branch_address, + uintptr_t function_start, uintptr_t function_end, uintptr_t segment_start, + uintptr_t segment_end) { + const auto branch = function.instructions.find(branch_address); + if (branch == function.instructions.end() || + branch->second.instruction.mnemonic != ZYDIS_MNEMONIC_JMP || + branch->second.operands[0].type != ZYDIS_OPERAND_TYPE_REGISTER) { + return std::nullopt; + } + const ZydisRegister target_reg = branch->second.operands[0].reg.value; + + const auto previous_contiguous = [&function](auto instruction) { + if (instruction == function.instructions.begin()) { + return function.instructions.end(); + } + const auto previous = std::prev(instruction); + return previous->first + previous->second.instruction.length == instruction->first + ? previous + : function.instructions.end(); + }; + + constexpr size_t MaxInterveningInstructions = 4; + auto add = function.instructions.end(); + auto pattern_cursor = branch; + for (size_t count = 0; count <= MaxInterveningInstructions; ++count) { + const auto candidate = previous_contiguous(pattern_cursor); + if (candidate == function.instructions.end()) { + break; + } + const auto& decoded = candidate->second; + if (decoded.instruction.mnemonic == ZYDIS_MNEMONIC_ADD && + decoded.operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER && + IsSameRegister(decoded.operands[0].reg.value, target_reg) && + decoded.operands[1].type == ZYDIS_OPERAND_TYPE_REGISTER) { + add = candidate; + break; + } + if (WritesRegister(decoded, target_reg) || IsControlFlowTerminator(decoded.instruction)) { + return std::nullopt; + } + pattern_cursor = candidate; + } + if (add == function.instructions.end()) { + return std::nullopt; + } + const ZydisRegister table_reg = add->second.operands[1].reg.value; + + const auto load = previous_contiguous(add); + if (load == function.instructions.end() || + load->second.instruction.mnemonic != ZYDIS_MNEMONIC_MOVSXD || + load->second.operands[0].type != ZYDIS_OPERAND_TYPE_REGISTER || + !IsSameRegister(load->second.operands[0].reg.value, target_reg) || + load->second.operands[1].type != ZYDIS_OPERAND_TYPE_MEMORY || + !IsSameRegister(load->second.operands[1].mem.base, table_reg) || + load->second.operands[1].mem.index == ZYDIS_REGISTER_NONE || + load->second.operands[1].mem.scale != sizeof(s32)) { + return std::nullopt; + } + const ZydisRegister index_reg = load->second.operands[1].mem.index; + + constexpr size_t MaxPatternInstructions = 64; + std::optional table_size; + std::optional guarded_path_start; + auto cursor = load; + for (size_t count = 0; count < MaxPatternInstructions; ++count) { + cursor = previous_contiguous(cursor); + if (cursor == function.instructions.end()) { + break; + } + const auto& decoded = cursor->second; + if (decoded.instruction.mnemonic == ZYDIS_MNEMONIC_CMP && + decoded.operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER && + IsSameRegister(decoded.operands[0].reg.value, index_reg) && + decoded.operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE) { + const uintptr_t next_address = decoded.address + decoded.instruction.length; + const auto bounds_branch = function.instructions.find(next_address); + if (bounds_branch == function.instructions.end()) { + return std::nullopt; + } + const s64 bound = decoded.operands[1].imm.is_signed + ? decoded.operands[1].imm.value.s + : static_cast(decoded.operands[1].imm.value.u); + if (bound < 0) { + return std::nullopt; + } + if (bounds_branch->second.instruction.mnemonic == ZYDIS_MNEMONIC_JNBE) { + table_size = static_cast(bound) + 1; + } else if (bounds_branch->second.instruction.mnemonic == ZYDIS_MNEMONIC_JNB) { + table_size = static_cast(bound); + } else { + return std::nullopt; + } + guarded_path_start = next_address + bounds_branch->second.instruction.length; + break; + } + if (WritesRegister(decoded, index_reg)) { + return std::nullopt; + } + } + if (!table_size) { + return std::nullopt; + } + if (std::ranges::any_of(function.branch_targets, [&](uintptr_t target) { + return target >= *guarded_path_start && target <= branch_address; + })) { + return std::nullopt; + } + + const auto decode_table_address = + [table_reg](const DecodedCodeInstruction& decoded) -> std::optional { + if (decoded.instruction.mnemonic != ZYDIS_MNEMONIC_LEA || + decoded.operands[0].type != ZYDIS_OPERAND_TYPE_REGISTER || + !IsSameRegister(decoded.operands[0].reg.value, table_reg) || + decoded.operands[1].type != ZYDIS_OPERAND_TYPE_MEMORY) { + return std::nullopt; + } + ZyanU64 absolute_address {}; + if (!ZYAN_SUCCESS(ZydisCalcAbsoluteAddress(&decoded.instruction, &decoded.operands[1], + decoded.address, &absolute_address))) { + return std::nullopt; + } + return absolute_address; + }; + + std::set table_candidates; + cursor = load; + for (size_t count = 0; count < MaxPatternInstructions; ++count) { + cursor = previous_contiguous(cursor); + if (cursor == function.instructions.end()) { + break; + } + if (!WritesRegister(cursor->second, table_reg)) { + continue; + } + if (const auto address = decode_table_address(cursor->second)) { + table_candidates.insert(*address); + } + break; + } + if (table_candidates.empty()) { + for (const auto& [address, decoded]: function.instructions) { + if (address >= branch_address) { + break; + } + if (const auto table_address = decode_table_address(decoded)) { + table_candidates.insert(*table_address); + } + } + } + constexpr size_t MaxJumpTableEntries = 4096; + if (*table_size == 0 || *table_size > MaxJumpTableEntries) { + return std::nullopt; + } + + std::optional> resolved_targets; + for (const uintptr_t table_address: table_candidates) { + if (table_address < segment_start || table_address > segment_end || + *table_size > (segment_end - table_address) / sizeof(s32)) { + continue; + } + + std::vector targets; + targets.reserve(*table_size); + bool valid = true; + for (size_t index = 0; index < *table_size; ++index) { + s32 offset; + std::memcpy(&offset, + reinterpret_cast(table_address + index * sizeof(offset)), + sizeof(offset)); + const s64 target = static_cast(table_address) + offset; + if (target < static_cast(function_start) || + target >= static_cast(function_end)) { + valid = false; + break; + } + targets.push_back(static_cast(target)); + } + if (!valid) { + continue; + } + std::ranges::sort(targets); + targets.erase(std::ranges::unique(targets).begin(), targets.end()); + if (resolved_targets) { + return std::nullopt; + } + resolved_targets = std::move(targets); + } + return resolved_targets; +} + +DecodedFunction DecodeFunction(uintptr_t function_start, uintptr_t function_end, + uintptr_t segment_start, uintptr_t segment_end) { + DecodedFunction function; + std::vector blocks {function_start}; + std::unordered_set visited; + std::set indirect_branches; + std::set resolved_indirect_branches; + + while (true) { + while (!blocks.empty()) { + uintptr_t address = blocks.back(); + blocks.pop_back(); + + while (address >= function_start && address < function_end && + !visited.contains(address)) { + visited.insert(address); + auto decoded = DecodeCodeInstruction(address, function_end); + if (decoded.instruction.length == 0) { + break; + } + + function.uses_red_zone |= decoded.has_red_zone_operand; + function.requires_conservative_red_zone_tracking |= + decoded.has_unmodeled_red_zone_operand || + (decoded.changes_stack_pointer && !decoded.stack_pointer_delta.has_value()); + + const uintptr_t next_address = address + decoded.instruction.length; + const uintptr_t branch_target = GetRelativeTarget(decoded); + if (branch_target >= function_start && branch_target < function_end) { + function.branch_targets.insert(branch_target); + } + function.instructions.emplace(address, decoded); + + if (decoded.instruction.meta.category == ZYDIS_CATEGORY_COND_BR) { + if (branch_target >= function_start && branch_target < function_end) { + blocks.push_back(branch_target); + } + } else if (IsControlFlowTerminator(decoded.instruction)) { + if (decoded.instruction.meta.category == ZYDIS_CATEGORY_UNCOND_BR && + branch_target >= function_start && branch_target < function_end) { + blocks.push_back(branch_target); + } else if (decoded.instruction.meta.category == ZYDIS_CATEGORY_UNCOND_BR && + branch_target == 0) { + indirect_branches.insert(address); + } + break; + } + address = next_address; + } + } + + bool discovered_block = false; + for (const uintptr_t branch_address: indirect_branches) { + if (resolved_indirect_branches.contains(branch_address)) { + continue; + } + const auto targets = ResolveBoundedJumpTable(function, branch_address, function_start, + function_end, segment_start, segment_end); + if (!targets) { + continue; + } + resolved_indirect_branches.insert(branch_address); + for (const uintptr_t target: *targets) { + function.branch_targets.insert(target); + if (!visited.contains(target)) { + blocks.push_back(target); + discovered_block = true; + } + } + } + if (!discovered_block) { + break; + } + } + function.has_indirect_branch = indirect_branches.size() != resolved_indirect_branches.size(); + return function; +} + +RedZoneMask TranslateRedZoneMask(const RedZoneMask& mask, s64 stack_pointer_delta) { + RedZoneMask translated; + for (size_t bit = 0; bit < GuestRedZoneSize; ++bit) { + if (!mask.test(bit)) { + continue; + } + const s64 after_offset = static_cast(bit) - static_cast(GuestRedZoneSize); + const s64 before_offset = after_offset + stack_pointer_delta; + if (before_offset >= -static_cast(GuestRedZoneSize) && before_offset < 0) { + translated.set(static_cast(before_offset + static_cast(GuestRedZoneSize))); + } + } + return translated; +} + +void AnalyzeRedZoneLiveness(DecodedFunction& function) { + if (!function.uses_red_zone) { + return; + } + + if (function.has_indirect_branch || function.requires_conservative_red_zone_tracking || + std::ranges::any_of(function.branch_targets, [&function](uintptr_t target) { + return !function.instructions.contains(target); + })) { + for (auto& [_, decoded]: function.instructions) { + decoded.red_zone_live.set(); + } + return; + } + + std::vector instructions; + instructions.reserve(function.instructions.size()); + std::map indices; + for (auto& [address, decoded]: function.instructions) { + indices.emplace(address, instructions.size()); + instructions.push_back(&decoded); + } + + std::vector live_in(instructions.size()); + bool changed; + do { + changed = false; + for (size_t reverse_index = instructions.size(); reverse_index-- > 0;) { + const auto& decoded = *instructions[reverse_index]; + RedZoneMask live_out; + + const auto add_successor = [&](uintptr_t address) { + if (const auto successor = indices.find(address); successor != indices.end()) { + live_out |= live_in[successor->second]; + } + }; + + const uintptr_t next_address = decoded.address + decoded.instruction.length; + const uintptr_t branch_target = GetRelativeTarget(decoded); + if (decoded.instruction.meta.category == ZYDIS_CATEGORY_COND_BR) { + add_successor(next_address); + add_successor(branch_target); + } else if (decoded.instruction.meta.category == ZYDIS_CATEGORY_UNCOND_BR) { + add_successor(branch_target); + } else if (!IsControlFlowTerminator(decoded.instruction)) { + add_successor(next_address); + } + + const RedZoneMask translated_live_out = + decoded.stack_pointer_delta.has_value() + ? TranslateRedZoneMask(live_out, *decoded.stack_pointer_delta) + : live_out; + const RedZoneMask new_live_in = + decoded.red_zone_use | (translated_live_out & ~decoded.red_zone_def); + if (new_live_in != live_in[reverse_index]) { + live_in[reverse_index] = new_live_in; + changed = true; + } + } + } while (changed); + + for (size_t index = 0; index < instructions.size(); ++index) { + instructions[index]->red_zone_live = live_in[index]; + } +} + +bool EncodeRelocatedInstruction(const DecodedCodeInstruction& decoded, + Xbyak::CodeGenerator& generator) { + ZydisEncoderRequest request; + if (!ZYAN_SUCCESS(ZydisEncoderDecodedInstructionToEncoderRequest( + &decoded.instruction, decoded.operands.data(), + decoded.instruction.operand_count_visible, &request))) { + return false; + } + + for (u8 index = 0; index < decoded.instruction.operand_count_visible; ++index) { + const auto& operand = decoded.operands[index]; + if (operand.type == ZYDIS_OPERAND_TYPE_IMMEDIATE && operand.imm.is_relative) { + ZyanU64 absolute_address {}; + if (!ZYAN_SUCCESS(ZydisCalcAbsoluteAddress(&decoded.instruction, &operand, + decoded.address, &absolute_address))) { + return false; + } + request.operands[index].imm.u = absolute_address; + } else if (operand.type == ZYDIS_OPERAND_TYPE_MEMORY && + (operand.mem.base == ZYDIS_REGISTER_RIP || + operand.mem.base == ZYDIS_REGISTER_EIP)) { + ZyanU64 absolute_address {}; + if (!ZYAN_SUCCESS(ZydisCalcAbsoluteAddress(&decoded.instruction, &operand, + decoded.address, &absolute_address))) { + return false; + } + request.operands[index].mem.displacement = static_cast(absolute_address); + } + } + + std::array encoded {}; + ZyanUSize encoded_size = encoded.size(); + if (!ZYAN_SUCCESS(ZydisEncoderEncodeInstructionAbsolute( + &request, encoded.data(), &encoded_size, + reinterpret_cast(generator.getCurr())))) { + return false; + } + generator.db(encoded.data(), encoded_size); + return true; +} + +bool GenerateProtectedIndirectCall(const DecodedCodeInstruction& decoded, + Xbyak::CodeGenerator& generator) { + ASSERT(decoded.instruction.meta.category == ZYDIS_CATEGORY_CALL); + const auto& target = decoded.operands[0]; + ASSERT(target.type == ZYDIS_OPERAND_TYPE_MEMORY && target.size == sizeof(uintptr_t) * CHAR_BIT); + + ZydisEncoderRequest request {}; + request.machine_mode = ZYDIS_MACHINE_MODE_LONG_64; + request.mnemonic = ZYDIS_MNEMONIC_MOV; + request.operand_count = 2; + request.operands[0].type = ZYDIS_OPERAND_TYPE_REGISTER; + request.operands[0].reg.value = ZYDIS_REGISTER_R11; + + ZydisEncoderRequest original_request {}; + if (!ZYAN_SUCCESS(ZydisEncoderDecodedInstructionToEncoderRequest( + &decoded.instruction, decoded.operands.data(), + decoded.instruction.operand_count_visible, &original_request))) { + return false; + } + request.prefixes = original_request.prefixes & ZYDIS_ATTRIB_HAS_SEGMENT; + request.address_size_hint = original_request.address_size_hint; + request.operands[1] = original_request.operands[0]; + request.operands[1].mem.size = sizeof(uintptr_t); + + if (target.mem.base == ZYDIS_REGISTER_RIP || target.mem.base == ZYDIS_REGISTER_EIP) { + ZyanU64 absolute_address {}; + if (!ZYAN_SUCCESS(ZydisCalcAbsoluteAddress(&decoded.instruction, &target, decoded.address, + &absolute_address))) { + return false; + } + request.operands[1].mem.displacement = static_cast(absolute_address); + } + + generator.lea(rsp, ptr[rsp - GuestRedZoneSize]); + generator.push(r11); + + std::array encoded {}; + ZyanUSize encoded_size = encoded.size(); + if (!ZYAN_SUCCESS(ZydisEncoderEncodeInstructionAbsolute( + &request, encoded.data(), &encoded_size, + reinterpret_cast(generator.getCurr())))) { + return false; + } + generator.db(encoded.data(), encoded_size); + + generator.xchg(r11, ptr[rsp]); + generator.lea(rsp, ptr[rsp + GuestRedZoneSize + sizeof(uintptr_t)]); + generator.call(ptr[rsp - GuestRedZoneSize - sizeof(uintptr_t)]); + return true; +} +} // namespace + +RedZonePatchResult PatchRedZoneMemoryInstructions(u64 segment_addr, u64 segment_size, + std::span function_starts) { + RedZonePatchResult result {}; + auto* module = GetContainingModule(reinterpret_cast(segment_addr)); + if (module == nullptr || function_starts.empty()) { + return result; + } + + const uintptr_t segment_end = segment_addr + segment_size; + std::vector starts; + starts.reserve(function_starts.size()); + for (const uintptr_t start: function_starts) { + if (start >= segment_addr && start < segment_end) { + starts.push_back(start); + } + } + std::ranges::sort(starts); + const auto unique_end = std::ranges::unique(starts).begin(); + starts.erase(unique_end, starts.end()); + + std::unique_lock lock {module->mutex}; + for (size_t function_index = 0; function_index < starts.size(); ++function_index) { + const uintptr_t function_start = starts[function_index]; + const uintptr_t function_end = + function_index + 1 < starts.size() ? starts[function_index + 1] : segment_end; + if (function_end <= function_start) { + continue; + } + + ++result.function_count; + auto function = DecodeFunction(function_start, function_end, segment_addr, segment_end); + AnalyzeRedZoneLiveness(function); + result.instruction_count += function.instructions.size(); + + std::map rewrite_sites; + + if (function.uses_red_zone) { + ++result.red_zone_function_count; + result.indirect_red_zone_function_count += function.has_indirect_branch; + for (const auto& [address, decoded]: function.instructions) { + if (!decoded.accesses_memory || !decoded.red_zone_live.any() || + rewrite_sites.contains(address)) { + continue; + } + ++result.memory_instruction_count; + if (decoded.instruction.length < NearJumpSize) { + ++result.short_memory_instruction_count; + } + if (decoded.instruction.meta.category == ZYDIS_CATEGORY_CALL && + decoded.operands[0].type == ZYDIS_OPERAND_TYPE_MEMORY && + decoded.operands[0].size == sizeof(uintptr_t) * CHAR_BIT && + !IsStackPointerRegister(decoded.operands[0].mem.base) && + !IsStackPointerRegister(decoded.operands[0].mem.index)) { + rewrite_sites[address] = { + .protect_red_zone = true, + .protected_indirect_call = true, + }; + continue; + } + if (decoded.uses_stack_pointer && !decoded.replaces_stack_pointer) { + ++result.stack_dependent_memory_instruction_count; + continue; + } + if (decoded.instruction.meta.category == ZYDIS_CATEGORY_CALL || + IsControlFlowTerminator(decoded.instruction)) { + ++result.control_flow_memory_instruction_count; + continue; + } + rewrite_sites[address].protect_red_zone = true; + } + } + if (rewrite_sites.empty()) { + continue; + } + + struct RelocationSpan { + std::vector instructions; + uintptr_t patch_start {}; + uintptr_t continuation {}; + size_t patch_size {}; + }; + + const auto emit_span = [&](const RelocationSpan& span) -> std::optional { + const size_t trampoline_offset = module->trampoline_gen.getSize(); + if (module->trampoline_exhausted) { + return std::nullopt; + } + try { + for (const auto* decoded: span.instructions) { + const auto rewrite = rewrite_sites.find(decoded->address); + const bool protected_indirect_call = + rewrite != rewrite_sites.end() && rewrite->second.protected_indirect_call; + const bool protect_red_zone = rewrite != rewrite_sites.end() && + rewrite->second.protect_red_zone && + !protected_indirect_call; + if (protect_red_zone) { + module->trampoline_gen.lea(rsp, ptr[rsp - GuestRedZoneSize]); + } + if (protected_indirect_call) { + if (!GenerateProtectedIndirectCall(*decoded, module->trampoline_gen)) { + module->trampoline_gen.setSize(trampoline_offset); + return std::nullopt; + } + } else if (!EncodeRelocatedInstruction(*decoded, module->trampoline_gen)) { + module->trampoline_gen.setSize(trampoline_offset); + return std::nullopt; + } + if (protect_red_zone && !decoded->replaces_stack_pointer) { + module->trampoline_gen.lea(rsp, ptr[rsp + GuestRedZoneSize]); + } + } + module->trampoline_gen.jmp(reinterpret_cast(span.continuation)); + } catch (const Xbyak::Error& error) { + module->trampoline_gen.setSize(trampoline_offset); + if (HandleTrampolineError(module, error)) { + return std::nullopt; + } + throw; + } + return trampoline_offset; + }; + + const auto record_rewrites = [&](const RelocationSpan& span) { + for (const auto* decoded: span.instructions) { + const auto rewrite = rewrite_sites.find(decoded->address); + if (rewrite == rewrite_sites.end()) { + continue; + } + if (rewrite->second.protect_red_zone) { + ++result.patched_memory_instruction_count; + } + module->patched.insert(reinterpret_cast(decoded->address)); + } + }; + + std::vector> patched_spans; + std::vector relay_slots; + std::vector short_relay_slots; + std::vector unresolved_sites; + uintptr_t covered_until {}; + for (auto site_it = rewrite_sites.begin(); site_it != rewrite_sites.end(); ++site_it) { + const uintptr_t site = site_it->first; + if (site < covered_until) { + continue; + } + + const auto collect_forward_span = [&]() -> std::optional { + RelocationSpan span {.patch_start = site, .continuation = site}; + while (span.patch_size < NearJumpSize) { + const auto decoded_it = function.instructions.find(span.continuation); + if (decoded_it == function.instructions.end() || + (span.continuation != site && + function.branch_targets.contains(span.continuation))) { + return std::nullopt; + } + + const auto& decoded = decoded_it->second; + span.instructions.push_back(&decoded); + span.patch_size += decoded.instruction.length; + span.continuation += decoded.instruction.length; + if (span.patch_size < NearJumpSize && + IsControlFlowTerminator(decoded.instruction)) { + return std::nullopt; + } + } + return span; + }; + + const auto collect_backward_span = [&]() -> std::optional { + const auto site_instruction = function.instructions.find(site); + ASSERT(site_instruction != function.instructions.end()); + RelocationSpan span { + .instructions = {&site_instruction->second}, + .patch_start = site, + .continuation = site + site_instruction->second.instruction.length, + .patch_size = site_instruction->second.instruction.length, + }; + + while (span.patch_size < NearJumpSize) { + const auto previous_end = function.instructions.lower_bound(span.patch_start); + if (previous_end == function.instructions.begin() || + function.branch_targets.contains(span.patch_start)) { + return std::nullopt; + } + + const auto previous = std::prev(previous_end); + const auto& decoded = previous->second; + if (previous->first + decoded.instruction.length != span.patch_start || + previous->first < covered_until || + IsControlFlowTerminator(decoded.instruction)) { + return std::nullopt; + } + + span.instructions.insert(span.instructions.begin(), &decoded); + span.patch_start = previous->first; + span.patch_size += decoded.instruction.length; + } + return span; + }; + + std::optional selected_span; + std::optional trampoline_offset; + const auto& site_instruction = function.instructions.at(site); + const bool can_relocate_neighbors = !function.has_indirect_branch; + if (can_relocate_neighbors || site_instruction.instruction.length >= NearJumpSize) { + auto forward_span = collect_forward_span(); + if (forward_span) { + trampoline_offset = emit_span(*forward_span); + if (trampoline_offset) { + selected_span = std::move(forward_span); + } + } + } + if (!selected_span && can_relocate_neighbors) { + if (auto backward_span = collect_backward_span()) { + trampoline_offset = emit_span(*backward_span); + if (trampoline_offset) { + selected_span = std::move(backward_span); + } + } + } + if (!selected_span) { + unresolved_sites.push_back(site); + continue; + } + + const auto& span = *selected_span; + const auto* trampoline = module->trampoline_gen.getCode() + *trampoline_offset; + + auto& patch_gen = module->patch_gen; + patch_gen.reset(); + patch_gen.setSize(span.patch_start - reinterpret_cast(patch_gen.getCode())); + patch_gen.jmp(trampoline, Xbyak::CodeGenerator::LabelType::T_NEAR); + patch_gen.nop(span.patch_size - NearJumpSize); + + record_rewrites(span); + patched_spans.emplace_back(span.patch_start, span.continuation); + if (span.patch_size >= NearJumpSize * 2) { + relay_slots.push_back(span.patch_start + NearJumpSize); + } + if (span.patch_size >= NearJumpSize + ShortJumpSize) { + short_relay_slots.push_back(span.patch_start + NearJumpSize); + } + covered_until = span.continuation; + } + + const auto record_unsupported = [&](uintptr_t site) { + const auto& rewrite = rewrite_sites.at(site); + if (rewrite.protect_red_zone) { + ++result.unrelocatable_memory_instruction_count; + } + }; + const auto overlaps_patched_span = [&patched_spans](uintptr_t start, uintptr_t end) { + return std::ranges::any_of(patched_spans, [start, end](const auto& patched) { + return start < patched.second && patched.first < end; + }); + }; + + for (const uintptr_t site: unresolved_sites) { + if (module->patched.contains(reinterpret_cast(site))) { + continue; + } + + const auto site_instruction = function.instructions.find(site); + ASSERT(site_instruction != function.instructions.end()); + if (function.has_indirect_branch || + site_instruction->second.instruction.length < ShortJumpSize) { + record_unsupported(site); + continue; + } + + const RelocationSpan site_span { + .instructions = {&site_instruction->second}, + .patch_start = site, + .continuation = site + site_instruction->second.instruction.length, + .patch_size = site_instruction->second.instruction.length, + }; + const size_t trampoline_start = module->trampoline_gen.getSize(); + const auto site_trampoline_offset = emit_span(site_span); + if (!site_trampoline_offset) { + record_unsupported(site); + continue; + } + + constexpr s64 ShortJumpMin = std::numeric_limits::min(); + constexpr s64 ShortJumpMax = std::numeric_limits::max(); + const auto relay_slot = std::ranges::find_if(relay_slots, [site](uintptr_t address) { + const s64 displacement = + static_cast(address) - static_cast(site + ShortJumpSize); + return displacement >= ShortJumpMin && displacement <= ShortJumpMax; + }); + if (relay_slot != relay_slots.end()) { + const auto* site_trampoline = + module->trampoline_gen.getCode() + *site_trampoline_offset; + + auto& patch_gen = module->patch_gen; + patch_gen.reset(); + patch_gen.setSize(*relay_slot - reinterpret_cast(patch_gen.getCode())); + patch_gen.jmp(site_trampoline, Xbyak::CodeGenerator::LabelType::T_NEAR); + + patch_gen.reset(); + patch_gen.setSize(site - reinterpret_cast(patch_gen.getCode())); + patch_gen.jmp(reinterpret_cast(*relay_slot), + Xbyak::CodeGenerator::LabelType::T_SHORT); + patch_gen.nop(site_span.patch_size - ShortJumpSize); + + record_rewrites(site_span); + patched_spans.emplace_back(site_span.patch_start, site_span.continuation); + std::erase(short_relay_slots, *relay_slot); + relay_slots.erase(relay_slot); + continue; + } + + const auto find_host = [&](uintptr_t short_jump_address) { + std::pair, std::optional> result; + const s64 minimum_host = static_cast(short_jump_address) + + static_cast(ShortJumpSize) - + static_cast(NearJumpSize) + ShortJumpMin; + const s64 maximum_host = static_cast(short_jump_address) + + static_cast(ShortJumpSize) - + static_cast(NearJumpSize) + ShortJumpMax; + + auto candidate = function.instructions.lower_bound( + static_cast(std::max(minimum_host, 0))); + for (; candidate != function.instructions.end() && + static_cast(candidate->first) <= maximum_host; + ++candidate) { + RelocationSpan span {.patch_start = candidate->first, + .continuation = candidate->first}; + while (span.patch_size < NearJumpSize * 2) { + const auto instruction = function.instructions.find(span.continuation); + if (instruction == function.instructions.end() || + (span.continuation != span.patch_start && + function.branch_targets.contains(span.continuation))) { + span.instructions.clear(); + break; + } + span.instructions.push_back(&instruction->second); + span.patch_size += instruction->second.instruction.length; + span.continuation += instruction->second.instruction.length; + if (span.patch_size < NearJumpSize * 2 && + IsControlFlowTerminator(instruction->second.instruction)) { + span.instructions.clear(); + break; + } + } + if (span.instructions.empty() || + !(span.continuation <= site || + span.patch_start >= site_span.continuation) || + overlaps_patched_span(span.patch_start, span.continuation)) { + continue; + } + + const uintptr_t relay_address = span.patch_start + NearJumpSize; + const s64 displacement = static_cast(relay_address) - + static_cast(short_jump_address + ShortJumpSize); + if (displacement < ShortJumpMin || displacement > ShortJumpMax) { + continue; + } + + const auto offset = emit_span(span); + if (!offset) { + continue; + } + result.first = std::move(span); + result.second = offset; + break; + } + return result; + }; + + auto [host_span, host_trampoline_offset] = find_host(site); + std::optional final_relay_slot; + uintptr_t final_short_jump = site; + std::map relay_parent {{site, site}}; + std::vector relay_queue {site}; + for (size_t queue_index = 0; + !host_span && !final_relay_slot && queue_index < relay_queue.size(); + ++queue_index) { + const uintptr_t current = relay_queue[queue_index]; + if (current != site) { + if (std::ranges::find(relay_slots, current) != relay_slots.end()) { + final_relay_slot = current; + final_short_jump = relay_parent.at(current); + break; + } + const auto near_slot = + std::ranges::find_if(relay_slots, [current](uintptr_t address) { + const s64 displacement = static_cast(address) - + static_cast(current + ShortJumpSize); + return address != current && displacement >= ShortJumpMin && + displacement <= ShortJumpMax; + }); + if (near_slot != relay_slots.end()) { + final_relay_slot = *near_slot; + final_short_jump = current; + break; + } + + auto bridge_host = find_host(current); + if (bridge_host.first) { + host_span = std::move(bridge_host.first); + host_trampoline_offset = bridge_host.second; + final_short_jump = current; + break; + } + } + + for (const uintptr_t slot: short_relay_slots) { + if (relay_parent.contains(slot)) { + continue; + } + const s64 displacement = + static_cast(slot) - static_cast(current + ShortJumpSize); + if (displacement < ShortJumpMin || displacement > ShortJumpMax) { + continue; + } + relay_parent.emplace(slot, current); + relay_queue.push_back(slot); + } + } + + if (!host_span && !final_relay_slot) { + module->trampoline_gen.setSize(trampoline_start); + record_unsupported(site); + continue; + } + + const auto* site_trampoline = + module->trampoline_gen.getCode() + *site_trampoline_offset; + auto& patch_gen = module->patch_gen; + uintptr_t relay_address {}; + if (final_relay_slot) { + relay_address = *final_relay_slot; + patch_gen.reset(); + patch_gen.setSize(relay_address - reinterpret_cast(patch_gen.getCode())); + patch_gen.jmp(site_trampoline, Xbyak::CodeGenerator::LabelType::T_NEAR); + std::erase(relay_slots, relay_address); + std::erase(short_relay_slots, relay_address); + } else { + ASSERT(host_span && host_trampoline_offset); + const auto* host_trampoline = + module->trampoline_gen.getCode() + *host_trampoline_offset; + relay_address = host_span->patch_start + NearJumpSize; + + patch_gen.reset(); + patch_gen.setSize(host_span->patch_start - + reinterpret_cast(patch_gen.getCode())); + patch_gen.jmp(host_trampoline, Xbyak::CodeGenerator::LabelType::T_NEAR); + patch_gen.jmp(site_trampoline, Xbyak::CodeGenerator::LabelType::T_NEAR); + patch_gen.nop(host_span->patch_size - NearJumpSize * 2); + } + + uintptr_t jump_target = relay_address; + while (final_short_jump != site) { + patch_gen.reset(); + patch_gen.setSize(final_short_jump - + reinterpret_cast(patch_gen.getCode())); + patch_gen.jmp(reinterpret_cast(jump_target), + Xbyak::CodeGenerator::LabelType::T_SHORT); + jump_target = final_short_jump; + const auto parent = relay_parent.find(final_short_jump); + ASSERT(parent != relay_parent.end()); + final_short_jump = parent->second; + std::erase(relay_slots, jump_target); + std::erase(short_relay_slots, jump_target); + } + + patch_gen.reset(); + patch_gen.setSize(site - reinterpret_cast(patch_gen.getCode())); + patch_gen.jmp(reinterpret_cast(jump_target), + Xbyak::CodeGenerator::LabelType::T_SHORT); + patch_gen.nop(site_span.patch_size - ShortJumpSize); + + if (host_span) { + record_rewrites(*host_span); + patched_spans.emplace_back(host_span->patch_start, host_span->continuation); + if (host_span->patch_size >= NearJumpSize * 3) { + relay_slots.push_back(host_span->patch_start + NearJumpSize * 2); + } + if (host_span->patch_size >= NearJumpSize * 2 + ShortJumpSize) { + short_relay_slots.push_back(host_span->patch_start + NearJumpSize * 2); + } + } + record_rewrites(site_span); + patched_spans.emplace_back(site_span.patch_start, site_span.continuation); + } + } + return result; +} + +#else + +RedZonePatchResult PatchRedZoneMemoryInstructions(u64, u64, std::span) { + return {}; +} + +#endif + +namespace { + +constexpr uint8_t DW_EH_PE_FORMAT_MASK = 0x0f; +constexpr uint8_t DW_EH_PE_APPLICATION_MASK = 0x70; +constexpr uint8_t DW_EH_PE_PCREL = 0x10; +constexpr uint8_t DW_EH_PE_DATAREL = 0x30; +constexpr uint8_t DW_EH_PE_INDIRECT = 0x80; +constexpr uint8_t DW_EH_PE_OMIT = 0xff; + +template +bool ReadEhValue(const uint8_t** cursor, const uint8_t* end, T* value) { + if (cursor == nullptr || *cursor == nullptr || value == nullptr || + static_cast(end - *cursor) < sizeof(T)) { + return false; + } + std::memcpy(value, *cursor, sizeof(T)); + *cursor += sizeof(T); + return true; +} + +bool ReadEncodedEhPointer(const uint8_t** cursor, const uint8_t* end, uint8_t encoding, + uintptr_t datarel_base, uintptr_t* value) { + if (cursor == nullptr || *cursor == nullptr || value == nullptr || encoding == DW_EH_PE_OMIT) { + return false; + } + + const auto field_address = reinterpret_cast(*cursor); + uint64_t raw = 0; + bool is_signed = false; + switch (encoding & DW_EH_PE_FORMAT_MASK) { + case 0x00: { + uintptr_t decoded = 0; + if (!ReadEhValue(cursor, end, &decoded)) { + return false; + } + raw = decoded; + break; + } + case 0x02: { + uint16_t decoded = 0; + if (!ReadEhValue(cursor, end, &decoded)) { + return false; + } + raw = decoded; + break; + } + case 0x03: { + uint32_t decoded = 0; + if (!ReadEhValue(cursor, end, &decoded)) { + return false; + } + raw = decoded; + break; + } + case 0x04: + if (!ReadEhValue(cursor, end, &raw)) { + return false; + } + break; + case 0x0a: { + int16_t decoded = 0; + if (!ReadEhValue(cursor, end, &decoded)) { + return false; + } + raw = static_cast(static_cast(decoded)); + is_signed = true; + break; + } + case 0x0b: { + int32_t decoded = 0; + if (!ReadEhValue(cursor, end, &decoded)) { + return false; + } + raw = static_cast(static_cast(decoded)); + is_signed = true; + break; + } + case 0x0c: { + int64_t decoded = 0; + if (!ReadEhValue(cursor, end, &decoded)) { + return false; + } + raw = static_cast(decoded); + is_signed = true; + break; + } + default: return false; + } + + uint64_t base = 0; + switch (encoding & DW_EH_PE_APPLICATION_MASK) { + case 0x00: break; + case DW_EH_PE_PCREL: base = field_address; break; + case DW_EH_PE_DATAREL: base = datarel_base; break; + default: return false; + } + + uint64_t decoded = 0; + if (is_signed) { + decoded = static_cast(static_cast(base) + static_cast(raw)); + } else { + if (raw > UINT64_MAX - base) { + return false; + } + decoded = base + raw; + } + if ((encoding & DW_EH_PE_INDIRECT) != 0) { + const auto* indirect = reinterpret_cast(decoded); + uintptr_t target = 0; + std::memcpy(&target, indirect, sizeof(target)); + decoded = target; + } + *value = static_cast(decoded); + return true; +} + +} // namespace + +bool DecodeEhFrameFunctionStarts(uint64_t eh_frame_header_addr, uint64_t eh_frame_header_size, + std::vector* function_starts) { + if (function_starts == nullptr) { + return false; + } + function_starts->clear(); + if (eh_frame_header_addr == 0 || eh_frame_header_size < 4 || + eh_frame_header_size > UINTPTR_MAX - eh_frame_header_addr) { + return false; + } + + const auto* start = reinterpret_cast(eh_frame_header_addr); + const auto* end = start + eh_frame_header_size; + const auto* cursor = start; + uint8_t version = 0; + uint8_t eh_frame_encoding = 0; + uint8_t count_encoding = 0; + uint8_t table_encoding = 0; + if (!ReadEhValue(&cursor, end, &version) || !ReadEhValue(&cursor, end, &eh_frame_encoding) || + !ReadEhValue(&cursor, end, &count_encoding) || + !ReadEhValue(&cursor, end, &table_encoding) || version != 1) { + return false; + } + + uintptr_t ignored_eh_frame = 0; + if (!ReadEncodedEhPointer(&cursor, end, eh_frame_encoding, eh_frame_header_addr, + &ignored_eh_frame)) { + return false; + } + if (count_encoding == DW_EH_PE_OMIT || table_encoding == DW_EH_PE_OMIT) { + return true; + } + + uintptr_t fde_count = 0; + if (!ReadEncodedEhPointer(&cursor, end, count_encoding, eh_frame_header_addr, &fde_count)) { + return false; + } + if (fde_count > eh_frame_header_size / 2u) { + return false; + } + + function_starts->reserve(static_cast(fde_count)); + for (uintptr_t index = 0; index < fde_count; ++index) { + uintptr_t function_start = 0; + uintptr_t ignored_fde = 0; + if (!ReadEncodedEhPointer(&cursor, end, table_encoding, eh_frame_header_addr, + &function_start) || + !ReadEncodedEhPointer(&cursor, end, table_encoding, eh_frame_header_addr, + &ignored_fde)) { + function_starts->clear(); + return false; + } + function_starts->push_back(function_start); + } + return true; +} + +void RegisterRedZonePatchModule(void* module_ptr, uint64_t module_size, void* trampoline_area_ptr, + uint64_t trampoline_area_size) { +#if defined(_WIN32) + EXIT_IF(module_ptr == nullptr || module_size == 0 || trampoline_area_ptr == nullptr || + trampoline_area_size == 0); + const auto module_addr = reinterpret_cast(module_ptr); + g_patch_modules.erase(module_addr); + g_patch_modules.emplace(std::piecewise_construct, std::forward_as_tuple(module_addr), + std::forward_as_tuple(static_cast(module_ptr), module_size, + static_cast(trampoline_area_ptr), + trampoline_area_size)); +#else + (void)module_ptr; + (void)module_size; + (void)trampoline_area_ptr; + (void)trampoline_area_size; +#endif +} + +void UnregisterRedZonePatchModule(void* module_ptr) { +#if defined(_WIN32) + g_patch_modules.erase(reinterpret_cast(module_ptr)); +#else + (void)module_ptr; +#endif +} + +#undef ASSERT + +} // namespace Loader diff --git a/src/loader/redZonePatcher.h b/src/loader/redZonePatcher.h new file mode 100644 index 000000000..a9d2a24f6 --- /dev/null +++ b/src/loader/redZonePatcher.h @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifndef KYTY_LOADER_RED_ZONE_PATCHER_H_ +#define KYTY_LOADER_RED_ZONE_PATCHER_H_ + +#include "common/common.h" + +#include +#include + +namespace Loader { + +struct RedZonePatchResult { + uint64_t function_count = 0; + uint64_t instruction_count = 0; + uint64_t red_zone_function_count = 0; + uint64_t memory_instruction_count = 0; + uint64_t short_memory_instruction_count = 0; + uint64_t patched_memory_instruction_count = 0; + uint64_t stack_dependent_memory_instruction_count = 0; + uint64_t control_flow_memory_instruction_count = 0; + uint64_t unrelocatable_memory_instruction_count = 0; + uint64_t indirect_red_zone_function_count = 0; +}; + +void RegisterRedZonePatchModule(void* module_ptr, uint64_t module_size, void* trampoline_area_ptr, + uint64_t trampoline_area_size); +void UnregisterRedZonePatchModule(void* module_ptr); + +RedZonePatchResult PatchRedZoneMemoryInstructions(uint64_t segment_addr, uint64_t segment_size, + std::span function_starts); + +bool DecodeEhFrameFunctionStarts(uint64_t eh_frame_header_addr, uint64_t eh_frame_header_size, + std::vector* function_starts); + +} // namespace Loader + +#endif /* KYTY_LOADER_RED_ZONE_PATCHER_H_ */ diff --git a/src/loader/runtimeLinker.cpp b/src/loader/runtimeLinker.cpp index 114a55af7..6567ffba6 100644 --- a/src/loader/runtimeLinker.cpp +++ b/src/loader/runtimeLinker.cpp @@ -19,6 +19,7 @@ #include "loader/elf.h" #include "loader/gamePatch.h" #include "loader/jit.h" +#include "loader/redZonePatcher.h" #include "loader/symbolDatabase.h" #include "loader/x64InstructionEmulator.h" @@ -2202,17 +2203,39 @@ void RuntimeLinker::LoadProgramToMemory(Program* program) { EXIT_IF(tls_handler_size > UINT64_MAX - program->base_size_aligned); program->mapped_size = program->base_size_aligned + tls_handler_size; +#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS + const bool use_red_zone_protection = Config::RedZoneProtectionEnabled(); + constexpr uint64_t RED_ZONE_TRAMPOLINE_SIZE = 8u * 1024u * 1024u; + if (use_red_zone_protection) { + EXIT_IF(RED_ZONE_TRAMPOLINE_SIZE > UINT64_MAX - program->mapped_size); + program->mapped_size += RED_ZONE_TRAMPOLINE_SIZE; + } +#endif + program->base_vaddr = Libs::LibKernel::Memory::AllocateProgramMemory( g_desired_base_addr, program->mapped_size, Common::VirtualMemory::Mode::ExecuteReadWrite, Common::PathToString(program->file_name.filename()).c_str()); + EXIT_IF(program->base_vaddr == 0); +#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS + if (use_red_zone_protection) { + program->red_zone_trampoline_vaddr = program->base_vaddr + program->base_size_aligned; + program->red_zone_trampoline_size = RED_ZONE_TRAMPOLINE_SIZE; + RegisterRedZonePatchModule(reinterpret_cast(program->base_vaddr), + program->base_size_aligned, + reinterpret_cast(program->red_zone_trampoline_vaddr), + program->red_zone_trampoline_size); + } +#endif if (!is_shared) { program->tls.handler_vaddr = program->base_vaddr + program->base_size_aligned; +#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS + program->tls.handler_vaddr += program->red_zone_trampoline_size; +#endif } g_desired_base_addr += CODE_BASE_INCR * (1 + program->mapped_size / CODE_BASE_INCR); - EXIT_IF(program->base_vaddr == 0); EXIT_IF(program->base_size_aligned < program->base_size); LOGF("base_vaddr = 0x%016" PRIx64 "\n" "base_size = 0x%016" PRIx64 "\n" @@ -2228,6 +2251,11 @@ void RuntimeLinker::LoadProgramToMemory(Program* program) { } // program->elf->SetBaseVAddr(program->base_vaddr); +#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS + std::vector> executable_segments; + uint64_t eh_frame_header_addr = 0; + uint64_t eh_frame_header_size = 0; +#endif for (Elf64_Half i = 0; i < ehdr->e_phnum; i++) { if (phdr[i].p_memsz != 0 && (phdr[i].p_type == PT_LOAD || phdr[i].p_type == PT_OS_RELRO)) { @@ -2250,6 +2278,11 @@ void RuntimeLinker::LoadProgramToMemory(Program* program) { if (Common::VirtualMemory::IsExecute(mode)) { PatchProgram(program, segment_addr, segment_memory_size); +#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS + if (use_red_zone_protection) { + executable_segments.emplace_back(segment_addr, segment_file_size); + } +#endif } if (!skip_protect) { @@ -2284,8 +2317,42 @@ void RuntimeLinker::LoadProgramToMemory(Program* program) { program->proc_param_vaddr = phdr[i].p_vaddr + program->base_vaddr; } + +#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS + if (use_red_zone_protection && phdr[i].p_type == PT_GNU_EH_FRAME) { + eh_frame_header_addr = phdr[i].p_vaddr + program->base_vaddr; + eh_frame_header_size = phdr[i].p_memsz; + } +#endif } +#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS + if (use_red_zone_protection) { + std::vector function_starts; + if (!DecodeEhFrameFunctionStarts(eh_frame_header_addr, eh_frame_header_size, + &function_starts)) { + LOGF("Windows guest red-zone patching could not decode function boundaries for %s\n", + Common::PathToString(program->file_name).c_str()); + } + for (const auto& [segment_addr, segment_size]: executable_segments) { + const auto result = + PatchRedZoneMemoryInstructions(segment_addr, segment_size, function_starts); + LOGF("Windows guest red-zone patching: %s, functions=%" PRIu64 ", red_zone=%" PRIu64 + ", memory=%" PRIu64 ", patched=%" PRIu64 ", short=%" PRIu64 ", stack=%" PRIu64 + ", control=%" PRIu64 ", unrelocatable=%" PRIu64 "\n", + Common::PathToString(program->file_name.filename()).c_str(), result.function_count, + result.red_zone_function_count, result.memory_instruction_count, + result.patched_memory_instruction_count, result.short_memory_instruction_count, + result.stack_dependent_memory_instruction_count, + result.control_flow_memory_instruction_count, + result.unrelocatable_memory_instruction_count); + Common::VirtualMemory::FlushInstructionCache(segment_addr, segment_size); + } + Common::VirtualMemory::FlushInstructionCache(program->red_zone_trampoline_vaddr, + program->red_zone_trampoline_size); + } +#endif + if (!is_shared) { SetupTlsHandler(program); } @@ -2311,6 +2378,11 @@ void RuntimeLinker::DeleteProgram(Program* p) { if (program->base_vaddr != 0 || program->mapped_size != 0) { EXIT_IF(program->base_vaddr == 0 || program->mapped_size == 0); +#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS + if (program->red_zone_trampoline_size != 0) { + UnregisterRedZonePatchModule(reinterpret_cast(program->base_vaddr)); + } +#endif EXIT_IF( !Libs::LibKernel::Memory::FreeGuestMemory(program->base_vaddr, program->mapped_size)); } diff --git a/src/loader/runtimeLinker.h b/src/loader/runtimeLinker.h index 86a83f154..a09f7d036 100644 --- a/src/loader/runtimeLinker.h +++ b/src/loader/runtimeLinker.h @@ -114,15 +114,19 @@ struct Program { Program(); ~Program(); - int32_t unique_id = -1; - RuntimeLinker* rt = nullptr; - std::filesystem::path file_name; - std::unique_ptr elf; - std::unique_ptr dynamic_info; - uint64_t base_vaddr = 0; - uint64_t base_size = 0; - uint64_t base_size_aligned = 0; - uint64_t mapped_size = 0; + int32_t unique_id = -1; + RuntimeLinker* rt = nullptr; + std::filesystem::path file_name; + std::unique_ptr elf; + std::unique_ptr dynamic_info; + uint64_t base_vaddr = 0; + uint64_t base_size = 0; + uint64_t base_size_aligned = 0; + uint64_t mapped_size = 0; +#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS + uint64_t red_zone_trampoline_vaddr = 0; + uint64_t red_zone_trampoline_size = 0; +#endif std::unique_ptr export_symbols; std::unique_ptr import_symbols; ThreadLocalStorage tls; diff --git a/src/main.cpp b/src/main.cpp index 88e9f02d7..57cddad2e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -64,6 +64,9 @@ static void PrintUsage() { ::printf(" --spirv-debug-printf Enable SPIR-V debug printf.\n"); ::printf( " --readback-linear-images Read back writable linear images on submit.\n"); +#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS + ::printf(" --redzone Protect the guest SysV red zone.\n"); +#endif ::printf(" --keymap DualSense mapping; may be repeated.\n"); ::printf(" --rd Enable RenderDoc capture.\n"); } @@ -138,6 +141,13 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he continue; } +#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS + if (arg == "--redzone") { + options.config.red_zone_protection_enabled = true; + continue; + } +#endif + if (!Common::StartsWith(arg, "--")) { ::printf("game input must be provided with --game\n"); return false; diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 8b24a4ce5..f1321f20d 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -18265,39 +18265,67 @@ void CheckPm4ContextStateOperations(RenderContext& renderer) { processor.GetCtx().GetRenderTargetMask() == 0x1234abcd, "push changed live Cx state"); processor.GetCtx().SetRenderTargetMask(0x01020304); - Require("Pm4ContextState", "push-pop restore", - invoke(ContextStateOperation::Pop) == Pm4ProcessResult::Complete && - processor.GetCtx().GetRenderTargetMask() == 0x1234abcd, - "ordinary push/pop did not restore Cx state"); + Require("Pm4ContextState", "push-pop restore", + invoke(ContextStateOperation::Pop) == + Pm4ProcessResult::Complete && + processor.GetCtx().GetRenderTargetMask() == 0x1234abcd, + "ordinary push/pop did not restore Cx state"); - Require("Pm4ContextState", "clear", - invoke(ContextStateOperation::Clear) == Pm4ProcessResult::Complete && - !processor.GetCtx().GetRenderControl().depth_clear_enable && - processor.GetCtx().GetRenderTargetMask() == HW::Context {}.GetRenderTargetMask() && - processor.GetUcfg().GetPrimType() == 0x35 && - processor.GetShCtx().GetPs().ps_regs.data_addr == shader_address, - "clear reset state outside the Cx register domain"); + Require("Pm4ContextState", "clear", + invoke(ContextStateOperation::Clear) == + Pm4ProcessResult::Complete && + !processor.GetCtx().GetRenderControl().depth_clear_enable && + processor.GetCtx().GetRenderTargetMask() == + HW::Context{}.GetRenderTargetMask() && + processor.GetUcfg().GetPrimType() == 0x35 && + processor.GetShCtx().GetPs().ps_regs.data_addr == + shader_address, + "clear reset state outside the Cx register domain"); - processor.GetCtx().SetRenderTargetMask(0xabcdef01); - Require("Pm4ContextState", "push before processor reset", - invoke(ContextStateOperation::Push) == Pm4ProcessResult::Complete, - "push before reset failed"); - processor.Reset(); - processor.GetCtx().SetRenderTargetMask(0x76543210); - Require("Pm4ContextState", "processor reset discards push", - invoke(ContextStateOperation::Push) == Pm4ProcessResult::Complete && - invoke(ContextStateOperation::Pop) == Pm4ProcessResult::Complete && - processor.GetCtx().GetRenderTargetMask() == 0x76543210, - "processor reset retained an invalid saved Cx state"); + processor.GetCtx().SetRenderTargetMask(0x0badc0de); + Require("Pm4ContextState", "push before clear-state packet", + invoke(ContextStateOperation::Push) == + Pm4ProcessResult::Complete, + "push before CLEAR_STATE failed"); + processor.GetCtx().SetRenderTargetMask(0xfeedface); + std::array clear_state_packet{0xc0001200, 0}; + Pm4Execution clear_state_execution; + Require("Pm4ContextState", "clear-state packet", + processor.Process( + clear_state_execution, clear_state_packet.data(), + clear_state_packet.size()) == Pm4ProcessResult::Complete && + processor.GetCtx().GetRenderTargetMask() == + HW::Context{}.GetRenderTargetMask(), + "CLEAR_STATE did not clear the current Cx state"); + Require("Pm4ContextState", "pop after clear-state packet", + invoke(ContextStateOperation::Pop) == + Pm4ProcessResult::Complete && + processor.GetCtx().GetRenderTargetMask() == 0x0badc0de, + "CLEAR_STATE discarded the pushed Cx state"); - Require("Pm4ContextState", "HLE packet size", - Gen5::GraphicsDcbContextStateOpGetSize(0) == 20 && - Gen5::GraphicsDcbContextStateOpGetSize(1) == 108 && - Gen5::GraphicsDcbContextStateOpGetSize(2) == 108 && - Gen5::GraphicsDcbContextStateOpGetSize(3) == 128 && - Gen5::GraphicsDcbContextStateOpGetSize(4) == 0, - "context-state HLE sizes do not match libSceAgc"); - std::printf("[host] %-32s ok\n", "Pm4ContextState"); + processor.GetCtx().SetRenderTargetMask(0xabcdef01); + Require("Pm4ContextState", "push before processor reset", + invoke(ContextStateOperation::Push) == + Pm4ProcessResult::Complete, + "push before reset failed"); + processor.Reset(); + processor.GetCtx().SetRenderTargetMask(0x76543210); + Require("Pm4ContextState", "processor reset discards push", + invoke(ContextStateOperation::Push) == + Pm4ProcessResult::Complete && + invoke(ContextStateOperation::Pop) == + Pm4ProcessResult::Complete && + processor.GetCtx().GetRenderTargetMask() == 0x76543210, + "processor reset retained an invalid saved Cx state"); + + Require("Pm4ContextState", "HLE packet size", + Gen5::GraphicsDcbContextStateOpGetSize(0) == 20 && + Gen5::GraphicsDcbContextStateOpGetSize(1) == 108 && + Gen5::GraphicsDcbContextStateOpGetSize(2) == 108 && + Gen5::GraphicsDcbContextStateOpGetSize(3) == 128 && + Gen5::GraphicsDcbContextStateOpGetSize(4) == 0, + "context-state HLE sizes do not match libSceAgc"); + std::printf("[host] %-32s ok\n", "Pm4ContextState"); } void CheckPm4WaitResume(RenderContext& renderer) { diff --git a/tests/VirtualMemoryAllocationTests.cpp b/tests/VirtualMemoryAllocationTests.cpp index b500a5f88..f78e3ced4 100644 --- a/tests/VirtualMemoryAllocationTests.cpp +++ b/tests/VirtualMemoryAllocationTests.cpp @@ -8,15 +8,28 @@ #include "kernel/memory.h" #include "kernel/pthread.h" #include "libs/errno.h" +#include "loader/redZonePatcher.h" #include "loader/runtimeLinker.h" #include "loader/systemContent.h" +#include #include #include #include #include #include #include +#include + +#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#ifdef DeleteFile +#undef DeleteFile +#endif +#endif namespace { @@ -133,6 +146,105 @@ void InitSubsystems() { initialized = true; } +#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS +void* g_red_zone_fault_page = nullptr; + +LONG CALLBACK RedZoneFaultHandler(EXCEPTION_POINTERS* exception) { + if (exception == nullptr || exception->ExceptionRecord == nullptr || + exception->ContextRecord == nullptr || + exception->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION || + reinterpret_cast(exception->ExceptionRecord->ExceptionInformation[1]) != + g_red_zone_fault_page) { + return EXCEPTION_CONTINUE_SEARCH; + } + + // Model the Windows exception stack footprint that prompted the static patch: + // data below the interrupted RSP is not part of the Windows ABI contract. + *reinterpret_cast(exception->ContextRecord->Rsp - 0x18) = 0; + DWORD old_protection = 0; + if (VirtualProtect(g_red_zone_fault_page, 0x1000, PAGE_READWRITE, &old_protection) == FALSE) { + return EXCEPTION_CONTINUE_SEARCH; + } + return EXCEPTION_CONTINUE_EXECUTION; +} + +void TestWindowsGuestRedZoneStaticPatcher() { + const char* test = "WindowsGuestRedZoneStaticPatcher"; + constexpr uint64_t SENTINEL = 0x1122334455667788ull; + constexpr uint64_t CODE_SIZE = 0x4000; + constexpr uint64_t TRAMPOLINE_SIZE = 0x4000; + const auto mapping = Libs::LibKernel::Memory::AllocateProgramMemory( + 0x0000000902000000ull, CODE_SIZE + TRAMPOLINE_SIZE, + Common::VirtualMemory::Mode::ExecuteReadWrite, "red_zone_patcher_test"); + Check(test, mapping != 0, "failed to allocate patch test code"); + + std::vector code; + const auto emit = [&code](std::initializer_list bytes) { + code.insert(code.end(), bytes.begin(), bytes.end()); + }; + const auto emit64 = [&code](uint64_t value) { + const auto offset = code.size(); + code.resize(offset + sizeof(value)); + std::memcpy(code.data() + offset, &value, sizeof(value)); + }; + emit({0x48, 0xb8}); + emit64(SENTINEL); // movabs rax, sentinel + emit({0x48, 0x89, 0x44, 0x24, 0xe8}); // mov [rsp-0x18], rax + emit({0x48, 0x8b, 0x07}); // mov rax, [rdi] (faultable, 3 bytes) + emit({0x48, 0x8b, 0x44, 0x24, 0xe8}); // mov rax, [rsp-0x18] + emit({0x48, 0xb9}); + emit64(SENTINEL); // movabs rcx, sentinel + emit({0x48, 0x39, 0xc8}); // cmp rax, rcx + emit({0x0f, 0x94, 0xc0}); // sete al + emit({0x0f, 0xb6, 0xc0, 0xc3}); // movzx eax, al; ret + Check(test, code.size() < CODE_SIZE, "generated patch test code is too large"); + std::memcpy(reinterpret_cast(mapping), code.data(), code.size()); + Check(test, Common::VirtualMemory::FlushInstructionCache(mapping, code.size()), + "failed to flush generated test code"); + + g_red_zone_fault_page = VirtualAlloc(nullptr, 0x1000, MEM_RESERVE | MEM_COMMIT, PAGE_NOACCESS); + Check(test, g_red_zone_fault_page != nullptr, "failed to allocate fault page"); + auto* handler = AddVectoredExceptionHandler(1, RedZoneFaultHandler); + Check(test, handler != nullptr, "failed to install test exception handler"); + + using GuestFunction = uint64_t(KYTY_SYSV_ABI*)(const uint64_t*); + const auto function = reinterpret_cast(mapping); + const bool unpatched_was_corrupted = function(static_cast(g_red_zone_fault_page)) == 0; + + DWORD old_protection = 0; + Check(test, VirtualProtect(g_red_zone_fault_page, 0x1000, PAGE_NOACCESS, &old_protection) != FALSE, + "failed to reset fault page protection"); + Loader::RegisterRedZonePatchModule(reinterpret_cast(mapping), CODE_SIZE, + reinterpret_cast(mapping + CODE_SIZE), + TRAMPOLINE_SIZE); + const std::array function_starts = {static_cast(mapping)}; + const auto result = Loader::PatchRedZoneMemoryInstructions( + mapping, code.size(), function_starts); + Check(test, Common::VirtualMemory::FlushInstructionCache(mapping, CODE_SIZE + TRAMPOLINE_SIZE), + "failed to flush patched test code"); + const bool patched_preserved = function(static_cast(g_red_zone_fault_page)) == 1; + + Loader::UnregisterRedZonePatchModule(reinterpret_cast(mapping)); + RemoveVectoredExceptionHandler(handler); + VirtualFree(g_red_zone_fault_page, 0, MEM_RELEASE); + g_red_zone_fault_page = nullptr; + const bool freed = Libs::LibKernel::Memory::FreeGuestMemory(mapping, CODE_SIZE + TRAMPOLINE_SIZE); + + Check(test, unpatched_was_corrupted, "test harness did not reproduce red-zone corruption"); + Check(test, result.red_zone_function_count == 1 && result.memory_instruction_count >= 1 && + result.patched_memory_instruction_count >= 1 && + result.unrelocatable_memory_instruction_count == 0, + "static patcher did not cover the faultable instruction"); + Check(test, patched_preserved, "patched fault still corrupted the guest red zone"); + Check(test, freed, "failed to free patch test code"); + std::printf("[host] %-48s ok\n", test); +} +#else +void TestWindowsGuestRedZoneStaticPatcher() { + std::printf("[host] %-48s skipped\n", "WindowsGuestRedZoneStaticPatcher"); +} +#endif + void RunTest(void (*test_func)()) { if (g_failed_tests != 0) { return; @@ -2332,9 +2444,14 @@ void TestModuleRelocationUsesWritableHostMapping() { } // namespace -int main() { +int main(int argc, char** argv) { InitSubsystems(); + if (argc == 2 && std::strcmp(argv[1], "--red-zone-patcher-only") == 0) { + RunTest(TestWindowsGuestRedZoneStaticPatcher); + return g_failed_tests == 0 ? 0 : 1; + } + RunTest(TestWindowsGuestRedZoneStaticPatcher); RunTest(TestProsperoArgumentAndInfoSizeContracts); RunTest(TestGuestAddressSpaceOwnsReservationsBeforeBacking); RunTest(TestGuestAddressSpaceHasNoFixedFallback);