From 2fd92ba9899a33a42e2652c850b945654b0ad08a Mon Sep 17 00:00:00 2001 From: ReGlitched <64649587+ReGlitched@users.noreply.github.com> Date: Sat, 3 Oct 2026 00:23:44 -0600 Subject: [PATCH] crash fix + launch arg --- .../Build/20260929/startup_interventions.h | 24 +- Extension/Boot/startup_interventions.cpp | 378 +++++++++--------- Extension/Boot/startup_interventions.h | 65 ++- Launcher/gui.cpp | 58 ++- Launcher/gui.h | 3 +- Launcher/gui_internal.h | 6 +- Launcher/gui_launcher.cpp | 12 + Launcher/gui_settings.cpp | 3 + 8 files changed, 298 insertions(+), 251 deletions(-) diff --git a/Engine/Game/Build/20260929/startup_interventions.h b/Engine/Game/Build/20260929/startup_interventions.h index 11f7164..78a79b1 100644 --- a/Engine/Game/Build/20260929/startup_interventions.h +++ b/Engine/Game/Build/20260929/startup_interventions.h @@ -3,15 +3,23 @@ #include namespace dingosdk::game::build::v20260929::startup_interventions { -// Interpreter frame clear (REP STOSB) that overlaps the protected frame state. -inline constexpr std::uintptr_t frame = 0x0880d7d5; -inline constexpr std::array frame_instruction{0xf3, 0xaa}; -// Handler and data expected in R9/R14 at the frame clear. -inline constexpr std::uintptr_t frame_handler = 0x0880d710; -inline constexpr std::uintptr_t frame_data = 0x086772fc; -// Exit thunk (JMP [rip+...]) and the return address it is expected to be called from. +// Build 25414733 clears the saved native frame through the VM's byte store, +// not the 20260908 REP STOSB handler. Capture before its first zero byte. +inline constexpr std::uintptr_t frame = 0x08ae2785; +inline constexpr std::array frame_instruction{0x40, 0x88, 0x32}; // MOV [RDX], SIL +inline constexpr std::array frame_fingerprint{ + 0x40, 0x88, 0x32, 0x5e, 0x66, 0x8b, 0x41, 0x08}; +inline constexpr std::uintptr_t frame_data = 0x08b7bf3e; // R11: clear bytecode +inline constexpr std::uintptr_t frame_cursor = 0x08b7bf72; // [RCX + 0xd8] +inline constexpr std::uintptr_t frame_size = 0xc0; +inline constexpr std::uintptr_t frame_stack_offset = 0xb70; +inline constexpr std::uintptr_t frame_vm_offset = 0xb40; +inline constexpr std::uintptr_t frame_saved_registers_offset = 0x1b0; +inline constexpr std::uintptr_t frame_cursor_offset = 0xd8; +inline constexpr std::uint64_t frame_count = 0xdaf400c0; +inline constexpr std::uintptr_t exit_stack_offset = 0x128; inline constexpr std::uintptr_t exit_thunk = 0x08a06ff3; -inline constexpr std::uintptr_t exit_return = 0x087755f2; +inline constexpr std::uintptr_t exit_return = 0x087fe7fb; inline constexpr std::array exit_instruction{ 0xff, 0x25, 0x7f, 0x43, 0x5e, 0xfd}; } diff --git a/Extension/Boot/startup_interventions.cpp b/Extension/Boot/startup_interventions.cpp index 5686681..0453d6a 100644 --- a/Extension/Boot/startup_interventions.cpp +++ b/Extension/Boot/startup_interventions.cpp @@ -9,195 +9,201 @@ namespace dingosdk { namespace { - -// Re-observed on the supported image: the interpreter frame is based on RBP, -// and its 0x630-byte clear overlaps that frame. These are not Season 4 offsets. -constexpr std::uint64_t protected_state = 0x343cfc26; -constexpr std::uint32_t direction_flag = 0x400; +namespace pins = addr::startup_interventions; constexpr std::uint8_t breakpoint_opcode = 0xcc; - StartupInterventionMachine machine; std::atomic installed_base{}; -// The frame site sits in protector code that is regenerated for every game -// build; when the table has no verified site for this build only the exit -// intervention is armed. -std::atomic_bool frame_armed{}; PVOID handler_handle{}; +SRWLOCK handler_lock = SRWLOCK_INIT; +std::array saved_frame{}; +std::uintptr_t saved_frame_address{}; + +struct HandlerGuard { + DWORD last_error{GetLastError()}; + HandlerGuard() noexcept { AcquireSRWLockExclusive(&handler_lock); } + ~HandlerGuard() { ReleaseSRWLockExclusive(&handler_lock); SetLastError(last_error); } +}; template bool read_local(std::uintptr_t address, T& output) noexcept { SIZE_T count{}; return address && ReadProcessMemory(GetCurrentProcess(), - reinterpret_cast(address), &output, sizeof(output), &count) && - count == sizeof(output); + reinterpret_cast(address), &output, sizeof(output), &count) && count == sizeof(output); } -bool executable_image_range(std::uintptr_t image_base, std::uintptr_t address, - std::size_t size) noexcept { +bool writable_range(std::uintptr_t address, std::size_t size) noexcept { + MEMORY_BASIC_INFORMATION memory{}; + if (!address || !size || !VirtualQuery(reinterpret_cast(address), &memory, sizeof(memory)) || + memory.State != MEM_COMMIT || (memory.Protect & (PAGE_GUARD | PAGE_NOACCESS))) return false; + switch (memory.Protect & 0xff) { + case PAGE_READWRITE: case PAGE_WRITECOPY: case PAGE_EXECUTE_READWRITE: case PAGE_EXECUTE_WRITECOPY: break; + default: return false; + } + const auto region = reinterpret_cast(memory.BaseAddress); + return address >= region && address - region < memory.RegionSize && size <= memory.RegionSize - (address - region); +} + +bool write_local(std::uintptr_t address, const void* data, std::size_t size) noexcept { + SIZE_T count{}; + return writable_range(address, size) && WriteProcessMemory(GetCurrentProcess(), + reinterpret_cast(address), data, size, &count) && count == size; +} + +bool executable_image_range(std::uintptr_t image_base, std::uintptr_t address, std::size_t size) noexcept { MEMORY_BASIC_INFORMATION memory{}; if (!image_base || !address || !size || - !VirtualQuery(reinterpret_cast(address), &memory, sizeof(memory))) return false; - const auto executable = [](DWORD protection) { - if (protection & (PAGE_GUARD | PAGE_NOACCESS)) return false; - switch (protection & 0xff) { - case PAGE_EXECUTE: - case PAGE_EXECUTE_READ: - case PAGE_EXECUTE_READWRITE: - case PAGE_EXECUTE_WRITECOPY: - return true; - default: - return false; - } - }; - const auto end = reinterpret_cast(memory.BaseAddress) + memory.RegionSize; + !VirtualQuery(reinterpret_cast(address), &memory, sizeof(memory)) || + (memory.Protect & (PAGE_GUARD | PAGE_NOACCESS))) return false; + switch (memory.Protect & 0xff) { + case PAGE_EXECUTE: case PAGE_EXECUTE_READ: case PAGE_EXECUTE_READWRITE: case PAGE_EXECUTE_WRITECOPY: break; + default: return false; + } + const auto region = reinterpret_cast(memory.BaseAddress); return memory.State == MEM_COMMIT && memory.Type == MEM_IMAGE && reinterpret_cast(memory.AllocationBase) == image_base && - executable(memory.Protect) && address <= end && size <= end - address; + address >= region && address - region < memory.RegionSize && size <= memory.RegionSize - (address - region); } bool write_opcode(std::uintptr_t address, std::uint8_t opcode) noexcept { DWORD old_protection{}; - if (!VirtualProtect(reinterpret_cast(address), 1, PAGE_EXECUTE_READWRITE, - &old_protection)) return false; + if (!VirtualProtect(reinterpret_cast(address), 1, PAGE_EXECUTE_READWRITE, &old_protection)) return false; SIZE_T count{}; - const auto wrote = WriteProcessMemory(GetCurrentProcess(), reinterpret_cast(address), - &opcode, sizeof(opcode), &count) && count == sizeof(opcode); - const auto flushed = wrote && FlushInstructionCache( - GetCurrentProcess(), reinterpret_cast(address), 1) != FALSE; + const auto wrote = WriteProcessMemory(GetCurrentProcess(), reinterpret_cast(address), &opcode, 1, &count) && count == 1; + const auto flushed = wrote && FlushInstructionCache(GetCurrentProcess(), reinterpret_cast(address), 1) != FALSE; DWORD ignored{}; - const auto protected_again = VirtualProtect( - reinterpret_cast(address), 1, old_protection, &ignored) != FALSE; + const auto protected_again = VirtualProtect(reinterpret_cast(address), 1, old_protection, &ignored) != FALSE; return wrote && flushed && protected_again; } -StartupInterventionRegisters capture_registers(const CONTEXT& context, - std::uintptr_t exception_address) noexcept { - return {exception_address, context.Rsp, context.Rax, context.Rbx, context.Rcx, - context.Rdx, context.Rbp, context.Rsi, context.Rdi, context.R8, - context.R9, context.R14, context.EFlags}; +StartupInterventionRegisters capture_registers(const CONTEXT& c, std::uintptr_t address) noexcept { + return {address, c.Rsp, c.Rax, c.Rbx, c.Rcx, c.Rdx, c.Rbp, c.Rsi, c.Rdi, + c.R8, c.R9, c.R10, c.R11, c.R14, c.R15, c.EFlags}; } -void apply_registers(CONTEXT& context, const StartupInterventionRegisters& registers) noexcept { - context.Rip = registers.rip; - context.Rsp = registers.rsp; - context.Rax = registers.rax; - context.Rcx = registers.rcx; - context.Rdi = registers.rdi; +bool frame_shape_matches(std::uintptr_t base, const StartupInterventionRegisters& r) noexcept { + return base == supported_build::preferred_image_base && r.rip == base + startup_frame_rva && + r.rbp >= pins::frame_stack_offset && r.rsp == r.rbp - pins::frame_stack_offset && + r.rcx == r.rbp - pins::frame_vm_offset && r.rdx == r.rbp - startup_frame_size && + r.r15 == r.rbp - pins::frame_saved_registers_offset && + r.r11 == base + startup_frame_data_rva && r.r9 == pins::frame_count && r.r10 == r.r9 && + (r.rsi & 0xff) == 0; } LONG CALLBACK intervention_handler(EXCEPTION_POINTERS* pointers) { if (!pointers || !pointers->ExceptionRecord || !pointers->ContextRecord || - pointers->ExceptionRecord->ExceptionCode != EXCEPTION_BREAKPOINT) + pointers->ExceptionRecord->ExceptionCode != EXCEPTION_BREAKPOINT) return EXCEPTION_CONTINUE_SEARCH; + const auto base = installed_base.load(std::memory_order_acquire); + const auto address = reinterpret_cast(pointers->ExceptionRecord->ExceptionAddress); + if (!base || (address != base + startup_frame_rva && address != base + startup_exit_rva)) return EXCEPTION_CONTINUE_SEARCH; - const auto image_base = installed_base.load(std::memory_order_acquire); - if (!image_base) return EXCEPTION_CONTINUE_SEARCH; - const auto address = reinterpret_cast( - pointers->ExceptionRecord->ExceptionAddress); - auto registers = capture_registers(*pointers->ContextRecord, address); + HandlerGuard guard; + auto& context = *pointers->ContextRecord; + auto registers = capture_registers(context, address); + const auto thread = GetCurrentThreadId(); - if (address == image_base + startup_frame_rva) { - std::array patched{}; - std::uint64_t state{}; - if (!read_local(address, patched) || patched[0] != breakpoint_opcode || - registers.rbp > std::numeric_limits::max() - startup_frame_state_offset || - !read_local(registers.rbp + startup_frame_state_offset, state)) { - machine.fail(); - return EXCEPTION_CONTINUE_SEARCH; + if (address == base + startup_frame_rva) { + std::array instruction{}; + if (!read_local(address, instruction) || + (instruction[0] != breakpoint_opcode && instruction[0] != startup_frame_instruction[0])) return EXCEPTION_CONTINUE_SEARCH; + const bool patched = instruction[0] == breakpoint_opcode; + instruction[0] = startup_frame_instruction[0]; + if (instruction != startup_frame_instruction) { machine.fail(); return EXCEPTION_CONTINUE_SEARCH; } + // Another thread may have fetched INT3 before capture restored the site. + if (!patched) { context.Rip = address; return EXCEPTION_CONTINUE_EXECUTION; } + if (machine.phase() == StartupInterventionPhase::armed && frame_shape_matches(base, registers)) { + StartupFrameMetadata metadata{}; + if (!read_local(registers.rcx + startup_frame_cursor_offset, metadata.cursor)) { + machine.fail(); return EXCEPTION_CONTINUE_SEARCH; + } + if (startup_frame_clear_matches(base, registers, instruction, metadata)) { + const auto frame_address = registers.rbp - startup_frame_size; + if (!read_local(frame_address, saved_frame) || + !machine.capture_frame(base, registers, instruction, metadata, thread) || + !write_opcode(address, startup_frame_instruction[0])) { + machine.fail(); return EXCEPTION_CONTINUE_SEARCH; + } + saved_frame_address = frame_address; + // Let the original clear run; restore this exact frame only if the + // guarded exit arrives on the same thread and native stack frame. + context.Rip = address; + logging::log(logging::Level::info, logging::Channel::runtime, + "Startup frame captured: {} bytes, site {:#x}, bytecode {:#x}", + saved_frame.size(), startup_frame_rva, startup_frame_data_rva); + return EXCEPTION_CONTINUE_EXECUTION; + } } - const std::array original{startup_frame_instruction[0], patched[1]}; - if (!machine.preserve_frame(image_base, registers, original, state) || - !write_opcode(address, startup_frame_instruction[0])) { + // This VM opcode also performs ordinary byte stores before the target + // clear. Reproduce MOV [RDX], SIL without changing flags or registers. + const auto byte = static_cast(registers.rsi); + if (!write_local(registers.rdx, &byte, sizeof(byte))) { machine.fail(); - return EXCEPTION_CONTINUE_SEARCH; + logging::write(logging::Level::error, logging::Channel::runtime, + "Startup byte-store intervention could not replay an ordinary store"); + if (!write_opcode(address, startup_frame_instruction[0])) return EXCEPTION_CONTINUE_SEARCH; + context.Rip = address; + return EXCEPTION_CONTINUE_EXECUTION; } - apply_registers(*pointers->ContextRecord, registers); + context.Rip = address + startup_frame_instruction.size(); return EXCEPTION_CONTINUE_EXECUTION; } - if (address == image_base + startup_exit_rva) { - std::array patched{}; - std::uint64_t return_address{}; - if (!read_local(address, patched) || patched[0] != breakpoint_opcode || - registers.rsp > std::numeric_limits::max() - sizeof(return_address) || - !read_local(registers.rsp, return_address)) { - machine.fail(); - return EXCEPTION_CONTINUE_SEARCH; - } - patched[0] = startup_exit_instruction[0]; - logging::log(logging::Level::info, logging::Channel::runtime, - "Startup exit intervention hit: return {:#x}, rcx {:#x}, rdx {:#x}, frame site {}", - return_address >= image_base ? return_address - image_base : return_address, registers.rcx, - registers.rdx, frame_armed.load() ? "armed" : "not armed"); - if (!machine.return_from_exit(image_base, registers, patched, return_address) || - !write_opcode(address, startup_exit_instruction[0])) { - machine.fail(); - return EXCEPTION_CONTINUE_SEARCH; - } - apply_registers(*pointers->ContextRecord, registers); - return EXCEPTION_CONTINUE_EXECUTION; + std::array instruction{}; + std::uint64_t return_address{}; + if (!read_local(address, instruction) || instruction[0] != breakpoint_opcode || + !read_local(registers.rsp, return_address)) { machine.fail(); return EXCEPTION_CONTINUE_SEARCH; } + instruction[0] = startup_exit_instruction[0]; + logging::log(logging::Level::info, logging::Channel::runtime, + "Startup exit intervention hit: return {:#x}, rcx {:#x}, rdx {:#x}, frame captured {}", + return_address >= base ? return_address - base : return_address, registers.rcx, registers.rdx, + machine.phase() == StartupInterventionPhase::frame_captured); + if (!machine.return_from_exit(base, registers, instruction, return_address, thread) || + !write_local(saved_frame_address, saved_frame.data(), saved_frame.size()) || + !write_opcode(address, startup_exit_instruction[0])) { + machine.fail(); + logging::write(logging::Level::error, logging::Channel::runtime, "Startup exit/frame restoration guard failed"); + return EXCEPTION_CONTINUE_SEARCH; } - return EXCEPTION_CONTINUE_SEARCH; + context.Rax = registers.rax; + context.Rip = registers.rip; + context.Rsp = registers.rsp; + logging::log(logging::Level::info, logging::Channel::runtime, + "Startup frame restored: {} bytes; exit intervention completed", saved_frame.size()); + return EXCEPTION_CONTINUE_EXECUTION; } - } // namespace +bool startup_frame_clear_matches(std::uintptr_t base, const StartupInterventionRegisters& registers, + const std::array& instruction, const StartupFrameMetadata& metadata) noexcept { + return frame_shape_matches(base, registers) && instruction == startup_frame_instruction && + metadata.cursor == base + startup_frame_cursor_rva; +} + bool StartupInterventionMachine::arm() noexcept { auto expected = StartupInterventionPhase::uninitialized; - return phase_.compare_exchange_strong(expected, StartupInterventionPhase::armed, - std::memory_order_acq_rel); + return phase_.compare_exchange_strong(expected, StartupInterventionPhase::armed, std::memory_order_acq_rel); } -bool StartupInterventionMachine::preserve_frame( - std::uintptr_t image_base, StartupInterventionRegisters& registers, - const std::array& instruction, - std::uint64_t state) noexcept { +bool StartupInterventionMachine::capture_frame(std::uintptr_t base, const StartupInterventionRegisters& registers, + const std::array& instruction, const StartupFrameMetadata& metadata, std::uint32_t thread) noexcept { auto expected = StartupInterventionPhase::armed; - if (!phase_.compare_exchange_strong(expected, StartupInterventionPhase::handling_frame, - std::memory_order_acq_rel)) return false; - const auto matches = image_base == supported_build::preferred_image_base && - registers.rip == image_base + startup_frame_rva && registers.rsp == registers.rbp && - registers.rcx == 0x630 && - registers.rdi <= std::numeric_limits::max() - 0x210 && - registers.rdi + 0x210 == registers.rsp && registers.r8 == registers.rdi && - registers.r9 == image_base + startup_frame_handler_rva && - registers.r14 == image_base + startup_frame_data_rva && - (registers.rax & 0xff) == 0 && !(registers.eflags & direction_flag) && - instruction == startup_frame_instruction && state == protected_state && - registers.rdi <= std::numeric_limits::max() - registers.rcx; - if (!matches) { - phase_.store(StartupInterventionPhase::failed, std::memory_order_release); - return false; - } - registers.rdi += registers.rcx; - registers.rcx = 0; - registers.rip += instruction.size(); - phase_.store(StartupInterventionPhase::frame_preserved, std::memory_order_release); + if (!phase_.compare_exchange_strong(expected, StartupInterventionPhase::handling_frame, std::memory_order_acq_rel)) return false; + if (!thread || !startup_frame_clear_matches(base, registers, instruction, metadata)) { fail(); return false; } + frame_base_ = registers.rbp; + frame_thread_ = thread; + phase_.store(StartupInterventionPhase::frame_captured, std::memory_order_release); return true; } -bool StartupInterventionMachine::return_from_exit( - std::uintptr_t image_base, StartupInterventionRegisters& registers, - const std::array& instruction, - std::uint64_t return_address) noexcept { - const bool frame = frame_armed.load(std::memory_order_acquire); - auto expected = frame ? StartupInterventionPhase::frame_preserved : StartupInterventionPhase::armed; - if (!phase_.compare_exchange_strong(expected, StartupInterventionPhase::handling_exit, - std::memory_order_acq_rel)) return false; - // With the frame site verified the exact protector return address is pinned - // too; without it, TerminateProcess(current process, 0x32) from this build's - // protector thunk back into the image is the guard. - const bool return_matches = return_address == image_base + startup_exit_return_rva || - (!frame && return_address >= image_base && return_address - image_base < startup_expected_image_size); - const auto matches = image_base == supported_build::preferred_image_base && - registers.rip == image_base + startup_exit_rva && - registers.rcx == std::numeric_limits::max() && - registers.rdx == 0x32 && return_matches && - instruction == startup_exit_instruction && - registers.rsp <= std::numeric_limits::max() - sizeof(return_address); - if (!matches) { - phase_.store(StartupInterventionPhase::failed, std::memory_order_release); - return false; - } +bool StartupInterventionMachine::return_from_exit(std::uintptr_t base, StartupInterventionRegisters& registers, + const std::array& instruction, std::uint64_t return_address, std::uint32_t thread) noexcept { + auto expected = StartupInterventionPhase::frame_captured; + if (!phase_.compare_exchange_strong(expected, StartupInterventionPhase::handling_exit, std::memory_order_acq_rel)) return false; + const bool matches = base == supported_build::preferred_image_base && registers.rip == base + startup_exit_rva && + registers.rcx == std::numeric_limits::max() && registers.rdx == 0x32 && + instruction == startup_exit_instruction && return_address == base + startup_exit_return_rva && + thread == frame_thread_ && registers.rbp == frame_base_ && + frame_base_ >= pins::exit_stack_offset && registers.rsp == frame_base_ - pins::exit_stack_offset; + if (!matches) { fail(); return false; } registers.rax = FALSE; registers.rip = return_address; registers.rsp += sizeof(return_address); @@ -205,84 +211,60 @@ bool StartupInterventionMachine::return_from_exit( return true; } -void StartupInterventionMachine::fail() noexcept { - phase_.store(StartupInterventionPhase::failed, std::memory_order_release); +void StartupInterventionMachine::fail() noexcept { phase_.store(StartupInterventionPhase::failed, std::memory_order_release); } +StartupInterventionPhase StartupInterventionMachine::phase() const noexcept { return phase_.load(std::memory_order_acquire); } + +bool startup_intervention_fingerprints_match(std::uintptr_t base, std::uint32_t size, + const std::array& frame, const std::array& exit) noexcept { + return base == supported_build::preferred_image_base && size == startup_expected_image_size && + frame == startup_frame_fingerprint && exit == startup_exit_instruction; } -StartupInterventionPhase StartupInterventionMachine::phase() const noexcept { - return phase_.load(std::memory_order_acquire); -} - -bool startup_intervention_fingerprints_match( - std::uintptr_t image_base, std::uint32_t image_size, - const std::array& exit_instruction) noexcept { - return image_base == supported_build::preferred_image_base && - image_size == startup_expected_image_size && - exit_instruction == startup_exit_instruction; -} - -bool start_startup_interventions(std::uintptr_t image_base, std::string& error) noexcept { +bool start_startup_interventions(std::uintptr_t base, std::string& error) noexcept { error.clear(); try { - if (handler_handle || installed_base.load()) - throw std::runtime_error("Startup interventions are already installed"); + if (handler_handle || installed_base.load()) throw std::runtime_error("Startup interventions are already installed"); IMAGE_DOS_HEADER dos{}; - if (!read_local(image_base, dos) || dos.e_magic != IMAGE_DOS_SIGNATURE || - dos.e_lfanew <= 0 || dos.e_lfanew > 0x100000) + if (!read_local(base, dos) || dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew <= 0 || dos.e_lfanew > 0x100000) throw std::runtime_error("Startup intervention image has an invalid DOS header"); IMAGE_NT_HEADERS64 nt{}; - if (!read_local(image_base + static_cast(dos.e_lfanew), nt) || - nt.Signature != IMAGE_NT_SIGNATURE || - nt.FileHeader.Machine != IMAGE_FILE_MACHINE_AMD64 || - nt.OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) + if (!read_local(base + static_cast(dos.e_lfanew), nt) || nt.Signature != IMAGE_NT_SIGNATURE || + nt.FileHeader.Machine != IMAGE_FILE_MACHINE_AMD64 || nt.OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) throw std::runtime_error("Startup intervention image has an invalid PE header"); - const auto frame_address = image_base + startup_frame_rva; - const auto exit_address = image_base + startup_exit_rva; - if (exit_address < image_base || - !executable_image_range(image_base, exit_address, startup_exit_instruction.size())) + const auto frame_address = base + startup_frame_rva; + const auto exit_address = base + startup_exit_rva; + if (!executable_image_range(base, frame_address, startup_frame_fingerprint.size()) || + !executable_image_range(base, exit_address, startup_exit_instruction.size())) throw std::runtime_error("Startup intervention sites are outside executable image pages"); - std::array frame{}; + std::array frame{}; std::array exit{}; - if (!read_local(exit_address, exit) || - !startup_intervention_fingerprints_match(image_base, - nt.OptionalHeader.SizeOfImage, exit)) + if (!read_local(frame_address, frame) || !read_local(exit_address, exit) || + !startup_intervention_fingerprints_match(base, nt.OptionalHeader.SizeOfImage, frame, exit)) throw std::runtime_error("Startup intervention image fingerprint does not match"); - const bool arm_frame = frame_address >= image_base && - executable_image_range(image_base, frame_address, startup_frame_instruction.size()) && - read_local(frame_address, frame) && frame == startup_frame_instruction; - frame_armed.store(arm_frame, std::memory_order_release); - - installed_base.store(image_base, std::memory_order_release); + installed_base.store(base, std::memory_order_release); handler_handle = AddVectoredExceptionHandler(1, intervention_handler); - if (!handler_handle) { - installed_base.store(0, std::memory_order_release); - throw std::runtime_error("Cannot install startup intervention exception handler"); + if (!handler_handle) { installed_base.store(0); throw std::runtime_error("Cannot install startup intervention exception handler"); } + bool frame_attempted = false; + if (!write_opcode(exit_address, breakpoint_opcode)) + error = "Cannot arm the guarded startup-exit intervention"; + if (error.empty()) { + frame_attempted = true; + if (!write_opcode(frame_address, breakpoint_opcode)) + error = "Cannot arm the guarded frame-capture intervention"; } - bool frame_patched = false; - bool exit_patched = false; - if (write_opcode(exit_address, breakpoint_opcode)) exit_patched = true; - else error = "Cannot arm the guarded startup-exit intervention"; - if (error.empty() && arm_frame) { - if (write_opcode(frame_address, breakpoint_opcode)) frame_patched = true; - else error = "Cannot arm the guarded frame-preservation intervention"; - } - if (error.empty() && !machine.arm()) - error = "Cannot arm the startup intervention state machine"; + if (error.empty() && !machine.arm()) error = "Cannot arm the startup intervention state machine"; if (!error.empty()) { - if (frame_patched) write_opcode(frame_address, startup_frame_instruction[0]); - if (exit_patched) write_opcode(exit_address, startup_exit_instruction[0]); + if (frame_attempted) write_opcode(frame_address, startup_frame_instruction[0]); + write_opcode(exit_address, startup_exit_instruction[0]); RemoveVectoredExceptionHandler(handler_handle); handler_handle = nullptr; - installed_base.store(0, std::memory_order_release); + installed_base.store(0); machine.fail(); return false; } + logging::log(logging::Level::info, logging::Channel::runtime, + "Startup interventions armed: frame {:#x}, exit {:#x}", startup_frame_rva, startup_exit_rva); return true; - } catch (const std::exception& exception) { - error = exception.what(); - machine.fail(); - return false; - } + } catch (const std::exception& exception) { error = exception.what(); machine.fail(); return false; } } - } // namespace dingosdk diff --git a/Extension/Boot/startup_interventions.h b/Extension/Boot/startup_interventions.h index aa36416..02639b8 100644 --- a/Extension/Boot/startup_interventions.h +++ b/Extension/Boot/startup_interventions.h @@ -1,6 +1,5 @@ #pragma once #include "Engine/Game/Build/image_identity.h" - #include "Engine/Game/Build/supported_build.h" #include "Engine/Game/Build/addresses.h" #include "Engine/Game/Build/20260929/startup_interventions.h" @@ -14,67 +13,61 @@ namespace dingosdk { inline constexpr std::uint32_t startup_expected_image_size = supported_build::game_image_size; inline constexpr std::uintptr_t startup_frame_rva = addr::startup_interventions::frame; -inline constexpr std::uintptr_t startup_frame_handler_rva = addr::startup_interventions::frame_handler; inline constexpr std::uintptr_t startup_frame_data_rva = addr::startup_interventions::frame_data; -inline constexpr std::uintptr_t startup_frame_state_offset = 0x70; +inline constexpr std::uintptr_t startup_frame_cursor_rva = addr::startup_interventions::frame_cursor; +inline constexpr std::uintptr_t startup_frame_size = addr::startup_interventions::frame_size; +inline constexpr std::uintptr_t startup_frame_cursor_offset = addr::startup_interventions::frame_cursor_offset; inline constexpr std::uintptr_t startup_exit_rva = addr::startup_interventions::exit_thunk; inline constexpr std::uintptr_t startup_exit_return_rva = addr::startup_interventions::exit_return; inline constexpr auto startup_frame_instruction = addr::startup_interventions::frame_instruction; +inline constexpr auto startup_frame_fingerprint = addr::startup_interventions::frame_fingerprint; inline constexpr auto startup_exit_instruction = addr::startup_interventions::exit_instruction; enum class StartupInterventionPhase : std::uint32_t { - uninitialized, - armed, - handling_frame, - frame_preserved, - handling_exit, - complete, - failed, + uninitialized, armed, handling_frame, frame_captured, handling_exit, complete, failed, }; struct StartupInterventionRegisters { - std::uint64_t rip{}; - std::uint64_t rsp{}; - std::uint64_t rax{}; - std::uint64_t rbx{}; - std::uint64_t rcx{}; - std::uint64_t rdx{}; - std::uint64_t rbp{}; - std::uint64_t rsi{}; - std::uint64_t rdi{}; - std::uint64_t r8{}; - std::uint64_t r9{}; - std::uint64_t r14{}; + std::uint64_t rip{}, rsp{}, rax{}, rbx{}, rcx{}, rdx{}, rbp{}, rsi{}, rdi{}; + std::uint64_t r8{}, r9{}, r10{}, r11{}, r14{}, r15{}; std::uint32_t eflags{}; }; -// Pure guarded state machine shared by the runtime exception handler and its -// focused tests. A matching transition is accepted once; a guard mismatch at -// an armed site permanently fails the sequence. +struct StartupFrameMetadata { + std::uint64_t cursor{}; +}; + +bool startup_frame_clear_matches(std::uintptr_t image_base, + const StartupInterventionRegisters& registers, + const std::array& instruction, + const StartupFrameMetadata& metadata) noexcept; + +// One-shot capture/exit sequence. The runtime serializes transitions and owns +// the frame bytes; ordinary VM byte stores do not advance this state machine. class StartupInterventionMachine { public: bool arm() noexcept; - bool preserve_frame(std::uintptr_t image_base, StartupInterventionRegisters& registers, - const std::array& instruction, - std::uint64_t protected_state) noexcept; + bool capture_frame(std::uintptr_t image_base, const StartupInterventionRegisters& registers, + const std::array& instruction, + const StartupFrameMetadata& metadata, std::uint32_t thread_id) noexcept; bool return_from_exit(std::uintptr_t image_base, StartupInterventionRegisters& registers, const std::array& instruction, - std::uint64_t return_address) noexcept; + std::uint64_t return_address, std::uint32_t thread_id) noexcept; void fail() noexcept; StartupInterventionPhase phase() const noexcept; private: std::atomic phase_{StartupInterventionPhase::uninitialized}; + std::uint64_t frame_base_{}; + std::uint32_t frame_thread_{}; }; -// The frame site is checked separately: it is optional (see start_startup_interventions). -bool startup_intervention_fingerprints_match( - std::uintptr_t image_base, std::uint32_t image_size, - const std::array& exit_instruction) noexcept; +bool startup_intervention_fingerprints_match(std::uintptr_t image_base, std::uint32_t image_size, + const std::array& frame, const std::array& exit) noexcept; -// Installs two process-wide, one-shot breakpoint interventions for the exact -// inspected Skate image. The caller keeps all existing game threads suspended -// during installation; future worker threads are covered by the code sites. +// Installs both verified sites while the caller holds existing game threads +// suspended. Capture retires the byte-store breakpoint; exit restores the +// captured native frame and retires the exit breakpoint before returning FALSE. bool start_startup_interventions(std::uintptr_t image_base, std::string& error) noexcept; } // namespace dingosdk diff --git a/Launcher/gui.cpp b/Launcher/gui.cpp index b11eeb0..57ea50d 100644 --- a/Launcher/gui.cpp +++ b/Launcher/gui.cpp @@ -173,6 +173,25 @@ void apply_style() { } +// True once `process` shows its startup splash (ReSkateStartupWindow) or, if the +// splash is disabled, the game's own window. +bool game_window_shown(DWORD process) { + struct Search { DWORD process; bool found; } search{process, false}; + EnumWindows([](HWND window, LPARAM parameter) -> BOOL { + auto& target = *reinterpret_cast(parameter); + DWORD owner{}; + GetWindowThreadProcessId(window, &owner); + if (owner != target.process || !IsWindowVisible(window)) return TRUE; + std::array name{}; + GetClassNameW(window, name.data(), static_cast(name.size())); + const std::wstring_view kind(name.data()); + if (kind == L"ReSkateStartupWindow" || kind == L"Skate") { target.found = true; return FALSE; } + return TRUE; + }, reinterpret_cast(&search)); + return search.found; +} + + } // namespace } // namespace detail @@ -258,6 +277,9 @@ int run(const launcher_app::Session& session, const std::vector& a auto& ui = *ui_storage; ModsPanel mods_panel; bool running = true; + HANDLE game{}; + DWORD game_id{}; + bool hidden{}; while (running) { MSG message; while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) { @@ -270,12 +292,35 @@ int run(const launcher_app::Session& session, const std::vector& a launcher.restart(); break; } - // The launcher is done the moment injection succeeded and Skate is running - // on its own, so close immediately instead of staying beside the game. This - // is the earliest safe point: a launch that fails never sets launched(), so - // it still gets to report itself in the window. - if (launcher.launched()) break; - if (IsIconic(window)) { Sleep(50); continue; } + // A successful launch closes immediately unless the user keeps the + // launcher alive. In that mode it sleeps hidden while the game runs. + if (launcher.launched() && !launcher.settings().keep_open_after_launch) break; + if (!game && launcher.game()) { + game_id = launcher.game(); + game = OpenProcess(SYNCHRONIZE, FALSE, game_id); + } + if (game) { + if (!hidden && game_window_shown(game_id)) { + ShowWindow(window, SW_HIDE); + hidden = true; + } + if (hidden && !launcher.busy() && launcher.snapshot().phase == Phase::failed) { + ShowWindow(window, SW_SHOWNORMAL); + SetForegroundWindow(window); + hidden = false; + } + if (WaitForSingleObject(game, 0) == WAIT_OBJECT_0) { + CloseHandle(game); + game = nullptr; + launcher.game_exited(hidden); + if (hidden) { + ShowWindow(window, SW_SHOWNORMAL); + SetForegroundWindow(window); + hidden = false; + } + } + } + if (hidden || IsIconic(window)) { Sleep(hidden ? 100 : 50); continue; } ImGui_ImplDX12_NewFrame(); ImGui_ImplWin32_NewFrame(); ImGui::NewFrame(); @@ -284,6 +329,7 @@ int run(const launcher_app::Session& session, const std::vector& a renderer.render(); } ShowWindow(window, SW_HIDE); + if (game) CloseHandle(game); launcher.cancel(); // Closing the window with Settings still open must not lose changes. try { launcher.save(); } catch (...) {} diff --git a/Launcher/gui.h b/Launcher/gui.h index 6cb961d..9e0c07c 100644 --- a/Launcher/gui.h +++ b/Launcher/gui.h @@ -7,7 +7,8 @@ namespace dingosdk::launcher_gui { -// Runs the windowed launcher until the user closes it or Skate starts. +// Runs the launcher until it is closed. The saved setting can retain it hidden +// while Skate runs and show it again after the game exits. // `arguments` are the launcher's own command-line arguments, kept for a // restart after a self-update. int run(const launcher_app::Session& session, const std::vector& arguments); diff --git a/Launcher/gui_internal.h b/Launcher/gui_internal.h index 6c71b5a..1908ff0 100644 --- a/Launcher/gui_internal.h +++ b/Launcher/gui_internal.h @@ -122,6 +122,7 @@ struct Settings { int console_key{static_cast(launcher::default_console_key)}; int log_level{2}; std::string arguments; + bool keep_open_after_launch{}; // Off: never replace ReSkate.dll or the launcher (keeps a test build someone handed out). bool updates{true}; // Off: a crash uploads nothing (RESKATE_CRASH_REPORTING=0 for the launcher and the game). @@ -159,9 +160,10 @@ public: bool restart_requested() const { return restart_; } // Process id of the Skate this launcher started, once it exists. DWORD game() const { return game_; } - // Injection finished and the game is running on its own; the launcher closes - // as soon as this is true. + // Injection finished and the game is running on its own. bool launched() const { return launched_; } + // Resets the launch state after the retained game process exits. + void game_exited(bool seen); Settings& settings() { return settings_; } const launcher_app::Session& session() const { return session_; } void save(); diff --git a/Launcher/gui_launcher.cpp b/Launcher/gui_launcher.cpp index 7f50ad1..127fc1f 100644 --- a/Launcher/gui_launcher.cpp +++ b/Launcher/gui_launcher.cpp @@ -56,6 +56,8 @@ Settings load_settings(const fs::path& path) { for (std::size_t index = 0; index < log_levels.size(); ++index) if (level == log_levels[index]) settings.log_level = static_cast(index); settings.arguments = json.value("arguments", std::string()); + // Keep the original settings key so existing launcher preferences survive. + settings.keep_open_after_launch = !json.value("close_on_launch", !settings.keep_open_after_launch); settings.updates = json.value("updates", settings.updates); settings.crash_reports = json.value("crash_reports", settings.crash_reports); settings.steam_username = json.value("steam_username", std::string()); @@ -79,6 +81,7 @@ void save_settings(const fs::path& path, const Settings& settings) { json["console_key"] = settings.console_key; json["log_level"] = log_levels[static_cast(settings.log_level)]; json["arguments"] = settings.arguments; + json["close_on_launch"] = !settings.keep_open_after_launch; json["updates"] = settings.updates; json["crash_reports"] = settings.crash_reports; json["steam_username"] = settings.steam_username; @@ -131,6 +134,14 @@ State Launcher::snapshot() { return state_; } +void Launcher::game_exited(bool seen) { + game_ = 0; + launched_ = false; + if (busy_ || snapshot().phase == Phase::failed) return; + if (seen) check(); + else fail("Skate closed while it was starting."); +} + void Launcher::save() { save_settings(settings_path(session_), settings_); // The game inherits the launcher's environment, so this reaches the next launch; the @@ -390,6 +401,7 @@ void Launcher::run_play() { launched_ = true; set(Phase::launching, "Skate is starting", options.offline || !launcher_app::steam_signed_in() ? "Offline mode: playing without Steam as Unknown Player; multiplayer is hidden." : + settings_.keep_open_after_launch ? "The launcher hides while you play and returns when Skate closes." : "The launcher closes now; Skate keeps starting on its own."); } diff --git a/Launcher/gui_settings.cpp b/Launcher/gui_settings.cpp index a0291ca..5654415 100644 --- a/Launcher/gui_settings.cpp +++ b/Launcher/gui_settings.cpp @@ -183,6 +183,9 @@ void settings_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& ui break; default: section_caption(fonts, "LAUNCHER"); + setting_check("Keep the launcher open after launch", + "Hide while you play and return when Skate closes. Turn off to close after a successful launch.", + settings.keep_open_after_launch); setting_check("Install ReSkate updates", "Turn off to keep the ReSkate.dll and launcher you have, such as a test build someone gave you. " "Skate's own Steam updates are not affected.",