Files
ReSkate/Engine/Game/Build/20260929/unload_guard.h
T
AyeZeeBB 464e189d61 Guard the game's level unload against an entry with no asset
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.
2026-10-09 14:49:33 -05:00

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);
}