mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-29 03:46:50 +01:00
Leaving a level could crash the game: an access violation in Skate.exe at +0x3f5fc52, on one of its worker threads, with no ReSkate code on the stack. Three reports from one PC show the same crash at the same address (two on 2.0.0, one on 1.1.8), each about half a second after leaving Skate3Improved on a server of 81 to 101 players. While a level unloads, a job walks each unloading object's table of 0x30-byte entries and reads a number at +0xc8 of the asset each entry points to. An entry whose asset never resolved holds null there, and the game reads it unchecked. The loop is rewritten in place at startup so such an entry is skipped. The 2-byte nop before the loop makes room for a jz straight after the pointer is masked, the two moves it displaced follow it, and the loop's jump back is retargeted to the new start. Everything from the read on stays where it was; nothing else in the function jumps into the rewritten bytes. The patch is applied only when the game's 50 bytes are exactly what it was written for. Otherwise the game runs as shipped and the log says why; startup goes on either way. The log shows "Level unload crash guard: ready" when it is in. Not known: which asset is unresolved (the map's, or something another player had equipped), and why it only happens some of the time. The reports hold stacks only.
41 lines
2.4 KiB
C++
41 lines
2.4 KiB
C++
#pragma once
|
|
#include <array>
|
|
#include <cstdint>
|
|
|
|
// Level unload crash guard. A job run while a level unloads walks each unloading object's table
|
|
// of 0x30-byte entries and reads a number at +0xc8 of the asset each entry points to
|
|
// (`mov rax,[rbx+8]; and rax,-5; mov edx,[rax+0xc8]`). An entry whose asset never resolved
|
|
// holds null there and the game crashes (seen leaving a mod map on a busy server). The loop is
|
|
// rewritten in place to skip such an entry: the 2-byte nop before it makes room for a `jz`
|
|
// straight after the `and`, the two moves the jump displaced follow it, and the loop's jump
|
|
// back is retargeted to the new start. Everything from the read on is where it was.
|
|
namespace dingosdk::game::build::v20260929::unload_guard {
|
|
inline constexpr std::uintptr_t rva = 0x3f5fc3e;
|
|
inline constexpr std::array<unsigned char, 50> contract{
|
|
0x66,0x90, // nop
|
|
0x48,0x8b,0x43,0x08, // loop: mov rax,[rbx+8]
|
|
0x4c,0x8b,0xcd, // mov r9,rbp
|
|
0x44,0x8b,0x43,0x24, // mov r8d,[rbx+0x24]
|
|
0x48,0x83,0xe0,0xfb, // and rax,-5
|
|
0x48,0x8b,0xcf, // mov rcx,rdi
|
|
0x8b,0x90,0xc8,0x00,0x00,0x00, // mov edx,[rax+0xc8]
|
|
0x8b,0x43,0x20,0x48,0xc1,0xe2,0x20,0x48,0x0b,0xd0,0xe8,0x99,0xa5,0xff,0xff,
|
|
0x48,0x83,0xc3,0x30, // next: add rbx,0x30
|
|
0x48,0x3b,0xde,0x75,0xd0}; // cmp rbx,rsi; jne loop
|
|
inline constexpr std::array<unsigned char, 50> patch{
|
|
0x48,0x8b,0x43,0x08, // loop: mov rax,[rbx+8]
|
|
0x48,0x83,0xe0,0xfb, // and rax,-5
|
|
0x74,0x1f, // jz next
|
|
0x4c,0x8b,0xcd, // mov r9,rbp
|
|
0x44,0x8b,0x43,0x24, // mov r8d,[rbx+0x24]
|
|
0x48,0x8b,0xcf, // mov rcx,rdi
|
|
0x8b,0x90,0xc8,0x00,0x00,0x00, // mov edx,[rax+0xc8]
|
|
0x8b,0x43,0x20,0x48,0xc1,0xe2,0x20,0x48,0x0b,0xd0,0xe8,0x99,0xa5,0xff,0xff,
|
|
0x48,0x83,0xc3,0x30, // next: add rbx,0x30
|
|
0x48,0x3b,0xde,0x75,0xce}; // cmp rbx,rsi; jne loop (two bytes earlier)
|
|
// The jz sits at +8 and ends at +10; the entry's step is at +41.
|
|
static_assert(patch[9] == 41 - 10);
|
|
// The jne ends at +50 and the loop now starts at +0.
|
|
static_assert(static_cast<signed char>(patch[49]) == 0 - 50 && static_cast<signed char>(contract[49]) == 2 - 50);
|
|
}
|