diff --git a/source/Enderal DLL/src/Main.cpp b/source/Enderal DLL/src/Main.cpp index 6263deb7c..1fd823ebc 100644 --- a/source/Enderal DLL/src/Main.cpp +++ b/source/Enderal DLL/src/Main.cpp @@ -16,6 +16,7 @@ #include "Patches/PluginsTxtPatch.h" #include "Patches/FormTypeCollisionDetector.h" #include "Patches/GogVramLeakFix.h" +#include "Patches/CrosshairPickDataCrashFix.h" using namespace SKSE; @@ -30,7 +31,8 @@ static std::map g_settings{ { "ForceBorderless", true }, { "AttachLightHitEffectCrashFix", true }, { "AutoScaleHeroMenu", true }, - { "GogVramLeakFix", true } + { "GogVramLeakFix", true }, + { "CrosshairPickDataCrashFix", true } }; namespace { @@ -80,6 +82,10 @@ namespace { logger::info("Installing light attach crash fix..."); AttachLightHitEffectCrash::Install(); } + if (g_settings.at("CrosshairPickDataCrashFix")) { + logger::info("Installing crosshair pick data crash fix..."); + CrosshairPickDataCrashFix::Install(); + } if (g_settings.at("StayAtSystemPage")) { if (const auto pluginInfo = GetLoadInterface()->GetPluginInfo("StayAtSystemPage"); pluginInfo) { MessageBoxW(NULL, L"Stay At The System Page is already included in Enderal, please, disable it.", L"Enderal SE Error", MB_OK | MB_ICONERROR); diff --git a/source/Enderal DLL/src/Patches/CrosshairPickDataCrashFix.h b/source/Enderal DLL/src/Patches/CrosshairPickDataCrashFix.h new file mode 100644 index 000000000..133b19ff8 --- /dev/null +++ b/source/Enderal DLL/src/Patches/CrosshairPickDataCrashFix.h @@ -0,0 +1,146 @@ +#pragma once + +// Fix crash in CrosshairPickData::Pick collision result quicksort +// +// The vanilla quicksort has a backward partition loop that lacks bounds checking. +// When collision data becomes corrupted or is modified during sorting (race condition), +// the loop can iterate past array boundaries, causing an access violation. +// +// Crash signature: SkyrimSE.exe+73CA0A (SE) / +7D4C4B (AE) / +7675AA (VR) +// Callstack: CrossHairPickData::Pick -> quicksort -> crash +// +// This fix adds bounds checking to the backward partition loop to prevent +// the index from going below the left boundary. +// +// Address Library IDs: +// SE 1.5.97: 42697 (function at 0x14073C960, patch at offset 0xA7) +// AE 1.6.*: 43870 (function varies, patch at offset 0xA8) +// VR 1.4.15: Raw address 0x140767500 (patch at offset 0xA7) + +namespace CrosshairPickDataCrashFix +{ + // The problematic loop in the quicksort function (SE example): + // + // LAB_14073ca01: ; Backward partition loop + // DEC EDX ; Decrement right index + // LEA RCX, [RCX + -0x40] ; Move pointer back by element size + // DEC RSI ; Decrement current position + // COMISS XMM4, dword ptr [RCX] ; Compare pivot with element <-- CRASH + // JC LAB_14073ca01 ; Loop while pivot < element + // MOV dword ptr [RSP + 0x20], EDX ; Store result + // + // The fix patches from DEC RSI to after JC with a jump to our safe loop + + struct SafeBackwardLoop : Xbyak::CodeGenerator + { + SafeBackwardLoop(std::uintptr_t a_loopBody, std::uintptr_t a_loopExit) + { + Xbyak::Label loopBodyLabel; + Xbyak::Label loopExitLabel; + Xbyak::Label continueLoop; + Xbyak::Label exitLoop; + + // Recreate: DEC RSI + dec(rsi); + + // NEW: Bounds check - exit if RSI < RDI (current index < left boundary) + cmp(rsi, rdi); + jl(exitLoop); + + // Recreate: COMISS XMM4, [RCX] + comiss(xmm4, dword[rcx]); + + // Recreate: JC back to loop body + // JC = jump if carry = jump if XMM4 < [RCX] + jc(continueLoop); + + // Fall through to exit + L(exitLoop); + // Recreate: MOV [RSP + 0x20], EDX + mov(dword[rsp + 0x20], edx); + // Jump back to original code after the patched section + jmp(ptr[rip + loopExitLabel]); + + L(continueLoop); + // Jump back to start of loop body + jmp(ptr[rip + loopBodyLabel]); + + L(loopBodyLabel); + dq(a_loopBody); + + L(loopExitLabel); + dq(a_loopExit); + } + }; + + inline void InstallPatch(std::uintptr_t a_funcBase, std::size_t a_patchOffset) + { + // We patch from DEC RSI through JC (8 bytes total) + // SE/VR: offset 0xA7, size 8 (A7-AE inclusive, then AF is MOV which we recreate) + // AE: offset 0xA8, size 8 (A8-AF inclusive, then B0 is next instruction) + constexpr std::size_t patchSize = 8; + + std::uintptr_t patchAddr = a_funcBase + a_patchOffset; + + // Calculate loop body and exit addresses + // Loop body is 6 bytes before DEC RSI (at DEC EDX) + std::uintptr_t loopBody = patchAddr - 6; + // Loop exit is 6 bytes after patch start (after the JC instruction, at CMP RSI, RDI) + std::uintptr_t loopExit = patchAddr + 6; + + // Generate the safe loop code + SafeBackwardLoop patch(loopBody, loopExit); + patch.ready(); + + // Allocate trampoline space for our code cave + auto& trampoline = SKSE::GetTrampoline(); + SKSE::AllocTrampoline(patch.getSize() + 14); + + void* codeCave = trampoline.allocate(patch); + + // Write a JMP from the patch location to our code cave + // We have 8 bytes available, JMP rel32 uses 5, so we NOP the rest + std::uint8_t jmpCode[8]; + jmpCode[0] = 0xE9; // JMP rel32 + + std::int32_t relOffset = static_cast( + reinterpret_cast(codeCave) - (patchAddr + 5)); + std::memcpy(&jmpCode[1], &relOffset, 4); + + // Fill remaining bytes with NOPs + jmpCode[5] = 0x90; // NOP + jmpCode[6] = 0x90; // NOP + jmpCode[7] = 0x90; // NOP + + REL::safe_write(patchAddr, jmpCode, patchSize); + + logger::info("CrosshairPickData crash fix installed at {:X}", patchAddr); + } + + inline void Install() + { + if (REL::Module::IsVR()) { + // VR 1.4.15: Raw address, no Address Library entry for this function + // Function at 0x140767500, patch at offset 0xA7 + constexpr std::uintptr_t vrFuncAddr = 0x140767500; + constexpr std::size_t vrPatchOffset = 0xA7; + + // Verify we're at the expected VR version + if (REL::Module::get().version() == REL::Version(1, 4, 15, 0)) { + InstallPatch(vrFuncAddr, vrPatchOffset); + } else { + logger::info("CrosshairPickData crash fix: Unknown VR version, skipping"); + } + } else if (REL::Module::get().version() >= REL::Version(1, 6, 0, 0)) { + // AE versions: ID 43870, patch at offset 0xA8 + REL::Relocation funcBase{ REL::ID(43870) }; + constexpr std::size_t aePatchOffset = 0xA8; + InstallPatch(funcBase.address(), aePatchOffset); + } else { + // SE 1.5.97: ID 42697, patch at offset 0xA7 + REL::Relocation funcBase{ REL::ID(42697) }; + constexpr std::size_t sePatchOffset = 0xA7; + InstallPatch(funcBase.address(), sePatchOffset); + } + } +}