CrosshairPickData crash fix

This commit is contained in:
Eddoursul 2026-01-27 05:18:08 +01:00
parent c59ed9dcac
commit d263cf9e84
2 changed files with 153 additions and 1 deletions

View File

@ -16,6 +16,7 @@
#include "Patches/PluginsTxtPatch.h" #include "Patches/PluginsTxtPatch.h"
#include "Patches/FormTypeCollisionDetector.h" #include "Patches/FormTypeCollisionDetector.h"
#include "Patches/GogVramLeakFix.h" #include "Patches/GogVramLeakFix.h"
#include "Patches/CrosshairPickDataCrashFix.h"
using namespace SKSE; using namespace SKSE;
@ -30,7 +31,8 @@ static std::map<std::string, bool> g_settings{
{ "ForceBorderless", true }, { "ForceBorderless", true },
{ "AttachLightHitEffectCrashFix", true }, { "AttachLightHitEffectCrashFix", true },
{ "AutoScaleHeroMenu", true }, { "AutoScaleHeroMenu", true },
{ "GogVramLeakFix", true } { "GogVramLeakFix", true },
{ "CrosshairPickDataCrashFix", true }
}; };
namespace { namespace {
@ -80,6 +82,10 @@ namespace {
logger::info("Installing light attach crash fix..."); logger::info("Installing light attach crash fix...");
AttachLightHitEffectCrash::Install(); AttachLightHitEffectCrash::Install();
} }
if (g_settings.at("CrosshairPickDataCrashFix")) {
logger::info("Installing crosshair pick data crash fix...");
CrosshairPickDataCrashFix::Install();
}
if (g_settings.at("StayAtSystemPage")) { if (g_settings.at("StayAtSystemPage")) {
if (const auto pluginInfo = GetLoadInterface()->GetPluginInfo("StayAtSystemPage"); pluginInfo) { 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); MessageBoxW(NULL, L"Stay At The System Page is already included in Enderal, please, disable it.", L"Enderal SE Error", MB_OK | MB_ICONERROR);

View File

@ -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<std::int32_t>(
reinterpret_cast<std::uintptr_t>(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<std::uintptr_t> 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<std::uintptr_t> funcBase{ REL::ID(42697) };
constexpr std::size_t sePatchOffset = 0xA7;
InstallPatch(funcBase.address(), sePatchOffset);
}
}
}